Rxswift ios - Validate OTP field and Confirm OTP field on button click - ios

I am new in RxSwift and want to implement a feature in my project.
I have to validate 2 fields, OTP and Confirm OTP using input/output MVVM using RxSwift on submit click.
Case1: If any textfield is empty submit button should be disabled, so if user starts typing the first textfield the submit button will be enabled (Same for confirm OTP textfield also)
Case2: On submit click i need to validate if either textfield is empty or not and show error on screen, Also if both the textfields value doesnt matches, it will show the error on submit button click.
let otpChangedText = BehaviorSubject<String>(value:"")
let confirmOtpChangedText = BehaviorSubject<String>(value:"")
let submitButtonTapped = PublishSubject<Void>()
let otp1Validation = otpChangedText.skipWhile { $0.isEmpty}.map {Validator.isEmpty(string: $0)}
let isValidOtp = otp1Validation.asDriver(onErrorJustReturn:false)
outputs = Outputs (isValidOTP: isValidOtp)
I have achieved the submit button disable state somehow but not getting any idea how should i show the error on screen if any of the field is empty and if the value of both the textfields dont match.
Please guide me. Thanks

As an aside, from the book "Intro To Rx":
Subjects provide a convenient way to poke around Rx, however they are not recommended for day to day use.
You shouldn't be throwing all those Subjects into the mix...
First let's create your business rules:
// Case 1
func eitherFilled(first: String?, second: String?) -> Bool {
first != nil && !first!.isEmpty || second != nil && !second!.isEmpty
}
// Case 2
func hasError(first: String?, second: String?) -> Bool {
first == nil || second == nil || first != second
}
You can test these independently from Rx or anything else to assure yourself that they are correct.
Now create your view models:
// Case 1
func buttonEnabled(first: Observable<String?>, second: Observable<String?>) -> Observable<Bool> {
Observable.combineLatest(first, second, resultSelector: eitherFilled)
}
// Case 2
func displayError(first: Observable<String?>, second: Observable<String?>) -> Observable<Void> {
Observable.combineLatest(first, second, resultSelector: hasError)
.filter { $0 }
.map { _ in }
}
Again, these are very easy to test using RxTest.
Now you can install your view models into your view controller and you know they work because you have tested them. (Note: I have not tested them. I leave it to you to make sure the logic is correct.)
final class MyViewController: UIViewController {
#IBOutlet weak var otpField: UITextField!
#IBOutlet weak var confirmOtpField: UITextField!
#IBOutlet weak var button: UIButton!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Case 1
buttonEnabled(
first: otpField.rx.text.asObservable(),
second: confirmOtpField.rx.text.asObservable()
)
.bind(to: button.rx.isEnabled)
.disposed(by: disposeBag)
// Case 2
displayError(
first: otpField.rx.text.asObservable(),
second: confirmOtpField.rx.text.asObservable()
)
.bind {
finalPresentScene(animated: true) {
UIAlertController(title: "Error", message: "OTP fileds do not match.", preferredStyle: .alert).scene { $0.connectOK() }
}
}
.disposed(by: disposeBag)
}
}
So with all the above, you have two models representing your business rules, two view models converting those business rules into view updates, and your view controller serves to connect the view models to the views.
(If you want to learn more about the code I'm using to present the alert, see this GitHub repository: https://github.com/danielt1263/CLE-Architecture-Tools)

Related

Observables bind 2 textfields

So now in the app i'm currently developing I decided to refactor it by moving to the MVVM design pattern. And here it is where I got to know the famous "Observables".
I managed to understand how they work and the importance of their existence when using MVVM, I've read a couple of explanations on the different techniques for the implementation. By techniques I mean:
Observables (the one I'm currently using)
Event Bus / Notification Center
FRP Techinque (ReactiveCocoa / RxSwift)
I've declared my Bindable class like this:
import UIKit
class Bindable<T> {
var value: T? {
didSet {
observer?(value)
}
}
var observer: ((T?) -> ())?
func bind(observer: #escaping (T?) -> ()) {
self.observer = observer
}
}
What I wanted to do is to bind 2 UITextField's (that are inside one of my ViewController's) with the respective ViewModel. Inside my ViewController there are 2 textfields (emailInput - passwordInput) and a 'Log In' button, that I want it to be disabled unless both textfields aren't empty.
For that I've added both textfield's this target:
emailInput.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
passwordInput.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
Then:
/// Enable / Disable --> Log In button
#objc func textFieldDidChange(_ textField: UITextField) {
if (emailInput.text == "") || (passwordInput.text == "") {
logInButton.enableButton(false)
} else {
logInButton.enableButton(true)
}
}
But my question is... How could I implement this same thing inside my ViewModel??
And is it possible to do a two-way binding using my Bindable class?
(If more code is needed to solve this, just ask me to and I'll edit the question)
Observable is used to communicate changes from the view model to the view. There is no need for your view model to use the Observable pattern in order to respond to the updates in your text fields. You can provide a simple function setCredentials(email: String, password: String). In this function you can check if those values are empty and set var loginEnabled: Bindable<Bool>. Your view observes the loginEnabled and sets the login button state accordingly.
struct ViewModel {
var loginEnabled = Bindable<Bool>()
var email = ""
var password = ""
init() {
self.loginEnabled.value = false
}
func setCredentials(email: String, password: String) {
self.email = email
self.password = password
self.loginEnabled.value = !email.isEmpty && !password.isEmpty
}
}
Then in your view controller you have something like
var viewModel: ViewModel
override func viewDidLoad {
super.viewDidLoad()
self.viewModel.loginEnabled.bind { value in
self.logInButton.isEnabled = value ?? false
}
}
#objc func textFieldDidChange(_ textField: UITextField) {
self.viewModel.setCredentials(email: self.emailInput.text ?? "", password: self.passwordInput.text ?? "")
}

RxCocoa-observable is emitting new data from API but tableview.rx.items doesn't show new data

I am new to both RxSwift and RxCocoa.
I have a working REST service on local which returns a Movie object for given "id".
On my storyboard there is a tableView (UITableView) which has custom cells (MovieCell). Each movie cell has a UILabel named "nameTextLabel". I also have a textField to get requested movie.id from user.
I am trying to show movie.title on each row of my tableView.
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet var textField: UITextField!
#IBOutlet var tableView: UITableView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
configureReactiveBinding()
}
private func configureReactiveBinding() {
textField.rx.text.asObservable()
.map { ($0 == "" ? "15" : $0!).lowercased() }
.map { GetMovieByIdRequest(id: $0) }
.flatMap { request -> Observable<Movie> in
return ServiceManager.instance.send(apiRequest: request)
}
.debug("movie")
// output:
//2018-10-17 16:13:49.320: movie -> subscribed
//2018-10-17 16:13:49.401: movie -> Event next(MovieAdvisor.Movie)
//2018-10-17 16:13:52.021: movie -> Event next(MovieAdvisor.Movie)
.toArray()
.debug("tableView")
// output:
//2018-10-17 16:13:49.318: tableView -> subscribed
.bind(to: tableView.rx.items(cellIdentifier: "movie_cell", cellType:MovieCell.self)) { index, model, cell in
cell.nameTextLabel.text = model.title
}
.disposed(by: bag)
}
}
Each time textLabel is changed the new movie.id is printed, but tableView doesn't show any data-not even with initial value (when textField is "").
Any idea how can i fix this?
After adding debug() lines, i figured that tableView is subscribed just once and no next events triggered tableView. What should i do?
Code is uploaded on GitHub
github
The problem is your toArray() operator. That operator waits until the sequence completes and then emits the entire sequence as a single array. Your sequence doesn't complete until after you have exited the view controller.
Replace that with .map { [$0] } instead and the movie will show. You should ask yourself though why your endpoint is only returning one movie instead of an array of movies.
Try replacing
`a.subscribe(onNext: {movie in
print(movie.title)
})
.disposed(by: bag)
with .observeOn(MainScheduler.instance) and see whether it does the trick.
Perhaps move .toArray() to directly after ServiceManager.instance.send(apiRequest: request).
Also you can add *.debug("my custom message")* after every RxSwift operator to see whether it is getting executed.
BTW I would recommend using RxDataSources framework for this, not bare RxCocoa.

Why does my UITextfField seems to be NOT nil, even if I leave the field empty? My "else" part of the if-statment is not being read

I am practicing swift programing by making an app that holds customers' data. The app has several text fields where people are supposed to type their name, email address, phone number, and quantity of products purchased. Then with a 'submit' button they save that information into the data base. However, if one of the fields is empty, an error should be thrown and a message should appear to the user letting him/her know that one of the fields empty.
The problem is that even if the textfield is empty, the compiler is not reading it as nil, so my condition is not working.
For simplicity, I am omitting a lot of code. I will only work with one UITextField (customer's name), and my condition will check if the TextField is empty or not. If empty, it should print "Please enter a valid name." if it is NOT nil, it should print the customer's name.
class RaffleViewControler: UIViewController, UITextFieldDelegate {
#IBOutlet weak var customerNameTextField: UITextField!
#IBAction func addCustomerName(_ sender: UITextField) {
}
#IBOutlet weak var submitButton: UIButton!
#IBAction func submitCustomer(_ sender: UIButton) {
if let name = customerNameTextField.text {
print(name)
} else {
print("Please enter a valid name")
}
}
Alternatively, I was also checking my condition in the following format:
#IBAction func submitCustomer(_ sender: UIButton) {
if customerNameTextField.text != nil {
print("Congrats. Your data is in our database")
} else {
print("Please enter a valid name")
}
}
The problem:
Even if I leave the textfield empty, the compiler does not read the "else" part of the condition. Which seems to indicate that the field is never nil. What could be happening?
According to the Apple docs:
This string is #"" by default.
I.e. it is not nil by default.
To check empty UITextField your can use simply
txtFieldName.text?.isEmpty
this will give you BOOL value.
You can simply check:
if self.customerNameTextField.text == "" {
// An empty text field.
} else {
// Not empty.
}

Referencing IBOutlet in another View Controller

So, I have been having some major trouble figuring this out and I have searched extensively for a solution but I surprisingly could not find one. I am attempting to create a multiple page (5, to be exact) Sign-Up for users.
I'll start off by showing you the layout of page 1 and 5 (since solving that issue will solve the issue for page 2-4):
Sign Up Page #1
Sign Up Page #5
As you may see (from the page control dots), I am using a page view controller to allow users to scroll from page to page. What I am trying to accomplish is giving the user the ability to enter their sign-up information in pages 1-5 before submitting it all at once (which can be located on page 5).
Here is the current code I am using for page #1:
class SignUpInfoViewController: UIViewController {
#IBOutlet weak var emailTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here is the current code I am using for page #5:
class TermsOfUseViewController: UIViewController {
let minPasswordCharCount = 6
#IBAction func signUpAction(_ sender: Any) {
let providedEmailAddress = SignUpInfoViewController().emailTextField.text!
let providedPassword = SignUpInfoViewController().passwordTextField.text!
let trimmedPassword = providedPassword.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if !(validEmail(enteredEmail: providedEmailAddress) && validPassword(enteredPassword: trimmedPassword)) {
invalidCredentialsAlert()
}
else {
FIRAuth.auth()?.createUser(withEmail: providedEmailAddress, password: providedPassword) { user, error in
if error == nil {
FIRAuth.auth()!.signIn(withEmail: providedEmailAddress,
password: providedPassword)
}
else {
let alertController = UIAlertController(title: "Error", message: error?.localizedDescription, preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(defaultAction)
self.present(alertController, animated: true, completion: nil)
}
}
}
}
// Email is valid if it has a standard email format
func validEmail(enteredEmail: String) -> Bool {
let emailFormat = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format:"SELF MATCHES %#", emailFormat)
return emailPredicate.evaluate(with: enteredEmail)
}
// Password is valid if it is not empty or greater than a specified number of characters
func validPassword(enteredPassword: String) -> Bool {
if (enteredPassword != "" && enteredPassword.characters.count >= minPasswordCharCount) {
return true
}
return false
}
In the TermsOfUseViewController class, I am attempting to use the emailTextField and passwordTextField outlets from the SignUpInfoViewController, but I am receiving the following error:
fatal error: unexpectedly found nil while unwrapping an Optional value
I debugged the error and saw that the emailTextField property from SignUpInfoViewController is nil and so force unwrapping it will cause the app to crash (Note: I have correctly connected the IBOutlets to the SignUpInfoViewController, so no issue there).
How can I safely transfer the usage of the IBOutlets from the SignUpInfoViewController class to the TermsOfUseViewController class without it crashing? In other words, how can I make it to where the IBOutlets are no longer nil when I reference them in the TermsOfUseViewController class?
Thank you!
That is a perfect scenario for delegate pattern
protocol SignUpProtocol: class {
func didProvideUserData(username: String ,password: String)
}
In your signup class declare a delegate: public weak var delegate:SignUpProtocol?
I am assuming when the user has provided the require info, they need to press some button to go to the next step: Thus in that button you should raise the delegate
#IBAction func nextButton(sender:UIButton) {
guard let username = usernameTextfield?.text, let password = passwordTextField?.text, else { fatalError("textfields were empty") }
if delegate != nil { // this saying when someone is listening to me, I will expose any method associated to me
delegate?.didProvideUserData(username:username, password:password) // passing the username and password from textfield
}
}
if you don't have a button, then look at property observer, where you could have some property
var didFulfill:Bool? = nil {
didSet {
if didFulfill != nil && didFulfill == true {}
// here you check if your textfields are sets then raise the delegate
}
}
set this property didFulfill = when both textfields are not empty :)
Now in your Terms class, just subscribe to that delegate
class TermsOfUseViewController: UIViewController, SignUpProtocol {
var signUpVc: SignUpInfoViewController?
override func viewDidLoad() {
super.viewDidLoad()
signUpVc = SignUpInfoViewController()
signUpVc?.delegate = self
}
func didProvideUserData(username: String, password:String) {
// there is your data
}
}
You have to take in account that you don't have all references for all UIPageViewControllers all the time. That being said, I would suggest either to keep object in UIPageViewController with updated information or using Singleton Pattern to use it to store info into it and later use it. UIPageViewController are being reused and you might have one before and one after and relying onto having them would be wrong.
You can use UIPageViewController as self.parentViewController or something like that.

Counting beans with ReactiveCocoa 4 and an NSButton

I have the following:
Two interesting classes: a ViewController and a ViewModel
A button nsButtonMorePlease:NSButton in the view of ViewController
A text box nsTextView:NSTextView in the view as well
I want the following behavior:
When you launch the program, the "count" starts at 0 and is displayed in the text box nsTextView
When you press the button nsButtonMorePlease, the count is incremented by 1 and the updated count is reflected in the nsTextView
I would like to ensure:
I use ReactiveCocoa 4 (that's the point kind of)
The model class contains numberOfBeans: MutableProperty<Int> starting at 0
The design is purely functional or close to it - that is (if I understand the term), every link in the chain mapping the event of mouse click to the MutableProperty of numberOfBeans to responding to it in the text view, is all without side effects.
Here's what I have. Fair warning: this doesn't come close to working or compiling I believe. But I do feel like maybe I want to use one of combineLatest, collect, reduce, etc. Just lost on what to do specifically. I do feel like this makes something easy quite hard.
class CandyViewModel {
private let racPropertyBeansCount: MutableProperty<Int> = MutableProperty<Int>(0)
lazy var racActionIncrementBeansCount: Action<AnyObject?, Int, NoError> = {
return Action { _ in SignalProducer<Int, NoError>(value: 1)
}
}()
var racCocoaIncrementBeansAction: CocoaAction
init() {
racCocoaIncrementBeansAction = CocoaAction.init(racActionIncrementBeansCount, input: "")
// ???
var sig = racPropertyBeansCount.producer.combineLatestWith(racActionIncrementBeansCount.)
}
}
class CandyView: NSViewController {
#IBOutlet private var guiButtonMoreCandy: NSButton!
#IBOutlet private var guiTextViewCandyCt: NSTextView!
}
class CandyViewModel {
let racPropertyBeansCount = MutableProperty<Int>(0)
let racActionIncrementBeansCount = Action<(), Int, NoError>{ _ in SignalProducer(value: 1) }
init() {
// reduce the value from the action to the mutableproperty
racPropertyBeansCount <~ racActionIncrementBeansCount.values.reduce(racPropertyBeansCount.value) { $0 + $1 }
}
}
class CandyView: NSViewController {
// define outlets
let viewModel = CandyViewModel()
func bindObservers() {
// bind the Action to the button
guiButtonMoreCandy.addTarget(viewModel.racActionIncrementBeansCount.unsafeCocoaAction, action: CocoaAction.selector, forControlEvents: .TouchUpInside)
// observe the producer of the mutableproperty
viewModel.racPropertyBeansCount.producer.startWithNext {
self.guiTextViewCandyCt.text = "\($0)"
}
}
}

Resources