Enable/Disable button with validating phone number entered in a textfield - ios

I'm very new to ReactiveSwift and MVVM as a whole. I'm trying to validate phone numbers entered into a textfield and enable/disable a button depending on the validation result.
In the app, there is a textfield and a UIButton button called Submit. For phone number validating, I'm using an open source library called [PhoneNumberKit][1]. It also provides a UITextField subclass which formats the user input.
I mashed together a solution that looks like this.
class ViewController: UIViewController {
#IBOutlet weak var textField: PhoneNumberTextField!
#IBOutlet weak var submitButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
textField.becomeFirstResponder()
submitButton.isEnabled = false
let textValuesSignal = textField.reactive.continuousTextValues
SignalProducer(textValuesSignal).start { result in
switch result {
case .value(let value):
print(value)
self.submitButton.isEnabled = self.isPhoneNumberValid(value)
case .failed(let error):
print(error)
self.submitButton.isEnabled = false
case .interrupted:
print("inturrupted")
self.submitButton.isEnabled = false
case .completed:
print("completed")
}
}
}
func isPhoneNumberValid(_ phoneNumberString: String) -> Bool {
do {
let phoneNumber = try PhoneNumberKit().parse(phoneNumberString)
let formattedPhoneNumber = PhoneNumberKit().format(phoneNumber, toType: .e164)
print("Phone number is valid: \(formattedPhoneNumber)")
return true
} catch let error {
print("Invalid phone number: \(error)")
return false
}
}
}
This does the job but not very elegantly. Also there is a significant lag between user input and the UI changing.
Another thing is my above solution doesn't conform to MVVM. I gave it another go.
class ViewController: UIViewController {
#IBOutlet weak var textField: PhoneNumberTextField!
#IBOutlet weak var submitButton: UIButton!
private let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
textField.becomeFirstResponder()
submitButton.reactive.isEnabled <~ viewModel.isPhoneNumberValid
viewModel.phoneNumber <~ textField.reactive.continuousTextValues
}
}
class ViewModel {
let phoneNumber = MutableProperty("")
let isPhoneNumberValid = MutableProperty(false)
init() {
isPhoneNumberValid = phoneNumber.producer.map { self.validatePhoneNumber($0) } // Cannot assign value of type 'SignalProducer<Bool, NoError>' to type 'MutableProperty<Bool>'
}
private func validatePhoneNumber(_ phoneNumberString: String) -> Bool {
do {
let phoneNumber = try PhoneNumberKit().parse(phoneNumberString)
let formattedPhoneNumber = PhoneNumberKit().format(phoneNumber, toType: .e164)
print("Phone number is valid: \(formattedPhoneNumber)")
return true
} catch let error {
print("Invalid phone number: \(error)")
return false
}
}
}
I'm getting the below error in the initializer when I'm assigning the result from the validatePhoneNumber function's to the isPhoneNumberValid property.
Cannot assign value of type 'SignalProducer' to type 'MutableProperty'
I can't figure out how to hook up the phone number validation part with the submit button's isEnabled property and the tap action properly.
Demo project

Try setting the property in init and mapping the property itself rather than a producer:
let isPhoneNumberValid: Property<Bool>
init() {
isPhoneNumberValid = phoneNumber.map { ViewModel.validatePhoneNumber($0) }
}
You’ll have to make validatePhoneNumber a static method because self won’t be available yet.
This is a more typical reactive way of doing this because you define one property completely in terms of another one during the view model’s initialization.

Related

The logic of the interaction between UISegmentedControl and GUI doesn't work in my app

The functionality I tried to implement in the quiz game is looks like this:
Here is the SettingsViewController which contains the settings of an app, where user can choose the difficulity of the game and the sequence of the questions the game have using UISegmentedControl.
The SettingsViewController contains these methods using to implement this functionality:
#IBAction func didChooseDifficulty(_ sender: UISegmentedControl) {
difficulityDelegate?.didChooseDifficulity(
difficulity: selectedDifficulty)
UserDefaults.standard.set(
sender.selectedSegmentIndex,
forKey: "chosenDifficulity")
}
private var selectedDifficulty: Difficulty {
switch self.difficultyControl.selectedSegmentIndex {
case 0: return .easy
case 1: return .hard
case 2: return .insane
default: return .hard
}
}
#IBAction func didChooseSequence(_ sender: UISegmentedControl) {
sequenceDelegate?.didChooseSequence(
sequence: selectedSequence)
UserDefaults.standard.set(
sender.selectedSegmentIndex,
forKey: "chosenSequence")
}
private var selectedSequence: Sequence {
switch self.sequenceControl.selectedSegmentIndex {
case 0: return .sequentally
case 1: return .shuffled
default: return .sequentally
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupQuestionsSequence()
setupDifficulty()
}
func setupQuestionsSequence() {
if let sequence = UserDefaults.standard.value(
forKey: "chosenSequence") {
let selectedIndex = sequence as! Int
sequenceControl.selectedSegmentIndex = selectedIndex
}
}
func setupDifficulty() {
if let difficulity = UserDefaults.standard.value(
forKey: "chosenDifficulity") {
let selectedIndex = difficulity as! Int
difficultyControl.selectedSegmentIndex = selectedIndex
}
}
I use the DifficulityDelegate and ChooseQuestionsSequenceDelegate protocols here:
protocol DifficulityDelegate: AnyObject {
func didChooseDifficulity(
difficulity: Difficulty)
}
protocol ChooseQuestionsSequenceDelegate: AnyObject {
func didChooseSequence(
sequence: Sequence)
}
Also here is the GameDelegate protocol, the main protocol of the game I use:
protocol GameDelegate: AnyObject {
func didEndGame(
difficulty: Difficulty,
withScore score: Int,
name: String,
removeTwoUsed: Bool,
callFriendUsed: Bool,
audienceHelpUsed: Bool)
}
Here is the GameSession class using for the main logic of the game:
class GameSession {
var questionNumber: Int = 0
var callFriendUsed = false
var audienceHelpUsed = false
var removeTwoUsed = false
var difficulty: Difficulty = .hard
var sequence: Sequence = .sequentally
}
Here is the Question structure with the questions is there:
struct Question: Codable {
let question: String
let answers: [String]
let rightAnswer: Int
let difficulty: Difficulty
}
And here is the GameViewController, the main controller where is the game itself occurs.
I need to gain some understanding of why it's nothing happens when I change the UISegmentedControl states. There is no any changes of the game difficulty and the sequence of the questions occurs.
Here is the some code from the GameViewController:
weak var delegate: GameDelegate?
weak var sequenceDelegate: ChooseQuestionsSequenceDelegate?
weak var difficulityDelegate: DifficulityDelegate?
private var questions = [Question]()
var gameSession = GameSession()
var score = 0
var questionNumber = Observable<Int>(0)
var difficulty: Difficulty = .hard {
didSet {
gameSession.difficulty = difficulty
}
}
var sequence: Sequence = .sequentally {
didSet {
gameSession.sequence = sequence
}
}
private var chooseDifficultyStrategy: DiffilultyStrategy {
switch difficulty {
case .easy: return EasyModeStrategy()
case .hard: return HardModeStrategy()
case .insane: return InsaneModeStrategy()
}
}
private var setupSelectedSequence: SequenceStrategy {
switch sequence {
case .sequentally: return SequentiallyStrategy()
case .shuffled: return ShuffledStrategy()
}
}
func initialSetup() {
delegate = Game.instance
Game.instance.gameSession = gameSession
questions = chooseDifficultyStrategy.setupGame(
lifelineButtons: lifelineButtonsCollection)
questions = setupSelectedSequence.setupGame()
}
It might be stupid, I'll be very glad if someone will help me understand what am I doing wrong. Thanks!
Speaking in very broad terms, I'd say that the protocol-and-delegate mechanism isn't what's wanted here. The view controllers that interact with the user's preferences, GameViewController and SettingsViewController, don't necessarily exist at the same time, so they cannot communicate with one another.
What's more likely needed is some central repository, as it were, where the SettingsViewController can write down the user's desires and the GameViewController can read them. A common place to keep preferences like this is UserDefaults; it has the advantage that it persists over time, even between launches of the app, and that it is globally available (meaning any of your code anywhere can interact with it).

How to bind image to UIImageView with rxswift?

I have viewModel:
class EditFoodViewViewModel {
private var food: Food
var foodImage = Variable<NSData>(NSData())
init(food: Food) {
self.food = food
self.foodImage.value = food.image!
}
}
And ViewController:
class EditFoodViewController: UIViewController {
public var food: EditFoodViewViewModelType?
#IBOutlet weak var foodThumbnailImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
guard let foodViewModel = food else { return }
foodViewModel.foodImage.asObservable().bind(to: foodThumbnailImageView.rx.image).disposed(by: disposeBag)
}
}
In the last line of viewController (where my UIImageView) a get error:
Generic parameter 'Self' could not be inferred
How to solve my problem? How to set image to imageView with rxSwift?
Almost invariably, when you see the error: "Generic parameter 'Self' could not be inferred", it means that the types are wrong. In this case you are trying to bind an Observable<NSData> to an Observable<Image?>.
There's a few other issues with your code as well.
it is very rare that a Subject type should be defined with the var keyword and this is not one of those rare times. Your foodImage should be a let not a var.
Variable has been deprecated; don't use it. In this case, you don't even need a subject at all.
NSData is also inappropriate in modern Swift. Use Data instead.
Based on what you have shown here, I would expect your code to look more like this:
class EditFoodViewViewModel: EditFoodViewViewModelType {
let foodImage: Observable<UIImage?>
init(food: Food) {
self.foodImage = Observable.just(UIImage(data: food.image))
}
}
class EditFoodViewController: UIViewController {
#IBOutlet weak var foodThumbnailImageView: UIImageView!
public var food: EditFoodViewViewModelType?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
guard let foodViewModel = food else { return }
foodViewModel.foodImage
.bind(to: foodThumbnailImageView.rx.image)
.disposed(by: disposeBag)
}
}

How to dispose RxSwift observable in viewmodel

I am learning RxSwift and I have tried a basic login UI using it. My implementation is as follows.
LoginViewController:
fileprivate let loginViewModel = LoginViewModel()
fileprivate var textFieldArray: [UITextField]!
override func viewDidLoad() {
super.viewDidLoad()
textFieldArray = [textFieldUserName, textFieldPassword, textFieldConfirmPassword]
textFieldUserName.delegate = self
textFieldPassword.delegate = self
textFieldConfirmPassword.delegate = self
loginViewModel.areValidFields.subscribe(
onNext: { [weak self] validArray in
for i in 0..<validArray.count {
if validArray[i] {
self?.showValidUI(index: i)
} else {
self?.showInValidUI(index: i)
}
}
},
onCompleted: {
print("### COMPLETED ###")
},
onDisposed: {
print("### DISPOSED ###")
}).disposed(by: loginViewModel.bag)
}
func showValidUI(index: Int) {
textFieldArray[index].layer.borderColor = UIColor.clear.cgColor
}
func showInValidUI(index: Int) {
textFieldArray[index].layer.borderColor = UIColor.red.cgColor
textFieldArray[index].layer.borderWidth = 2.0
}
extension LoginViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let inputText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
switch textField {
case textFieldUserName:
loginViewModel.updateUserName(text: inputText)
case textFieldPassword:
loginViewModel.updatePassword(text: inputText)
case textFieldConfirmPassword:
loginViewModel.updateConfirmedPassword(text: inputText)
default:
return false
}
return true
}
}
LoginViewModel:
class LoginViewModel {
private var username: String!
private var password: String!
private var confirmedPassword: String!
fileprivate let combinedSubject = PublishSubject<[Bool]>()
let bag = DisposeBag()
var areValidFields: Observable<[Bool]> {
return combinedSubject.asObservable()
}
init() {
self.username = ""
self.password = ""
self.confirmedPassword = ""
}
/*deinit {
combinedSubject.onCompleted()
}*/
func updateUserName(text: String) {
username = text
if username.count > 6 {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([false, true, true])
}
}
func updatePassword(text: String) {
password = text
if password.count > 6 {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([true, false, true])
}
}
func updateConfirmedPassword(text: String) {
confirmedPassword = text
if confirmedPassword == password {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([true, true, false])
}
}
}
With this code, the disposed message gets printed when i move back the navigation stack.
However, if I move forward, the disposed message is not printed. What is the proper way to dispose the observable?
When you move forward, the view controller is not removed from the stack. It remains so that when the user taps the back button, it is ready and still in the same state as the last time the user saw it. That is why nothing is disposed.
Also, since you said you are still learning Rx, what you have is not anywhere near best practices. I would expect to see something more like this:
class LoginViewModel {
let areValidFields: Observable<[Bool]>
init(username: Observable<String>, password: Observable<String>, confirm: Observable<String>) {
let usernameValid = username.map { $0.count > 6 }
let passValid = password.map { $0.count > 6 }
let confirmValid = Observable.combineLatest(password, confirm)
.map { $0 == $1 }
areValidFields = Observable.combineLatest([usernameValid, passValid, confirmValid])
}
}
In your view model, prefer to accept inputs in the init function. If you can't do that, for e.g. if some of the inputs don't exist yet, then use a Subject property and bind to it. But in either case, your view model should basically consist only of an init function and some properties for output. The DisposeBag does not go in the view model.
Your view controller only needs to create a view model and connect to it:
class LoginViewController: UIViewController {
#IBOutlet weak var textFieldUserName: UITextField!
#IBOutlet weak var textFieldPassword: UITextField!
#IBOutlet weak var textFieldConfirmPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = LoginViewModel(
username: textFieldUserName.rx.text.orEmpty.asObservable(),
password: textFieldPassword.rx.text.orEmpty.asObservable(),
confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
)
let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]
viewModel.areValidFields.subscribe(
onNext: { validArray in
for (field, valid) in zip(textFieldArray, validArray) {
if valid {
field.layer.borderColor = UIColor.clear.cgColor
}
else {
field.layer.borderColor = UIColor.red.cgColor
field.layer.borderWidth = 2.0
}
}
})
.disposed(by: bag)
}
private let bag = DisposeBag()
}
Notice that all of the code ends up in the viewDidLoad function. That's the ideal so you don't have to deal with [weak self]. In this particular case, I would likely put the onNext closure in a curried global function, in which case it would look like this:
class LoginViewController: UIViewController {
#IBOutlet weak var textFieldUserName: UITextField!
#IBOutlet weak var textFieldPassword: UITextField!
#IBOutlet weak var textFieldConfirmPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = LoginViewModel(
username: textFieldUserName.rx.text.orEmpty.asObservable(),
password: textFieldPassword.rx.text.orEmpty.asObservable(),
confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
)
let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]
viewModel.areValidFields.subscribe(
onNext:update(fields: textFieldArray))
.disposed(by: bag)
}
private let bag = DisposeBag()
}
func update(fields: [UITextField]) -> ([Bool]) -> Void {
return { validArray in
for (field, valid) in zip(fields, validArray) {
if valid {
field.layer.borderColor = UIColor.clear.cgColor
}
else {
field.layer.borderColor = UIColor.red.cgColor
field.layer.borderWidth = 2.0
}
}
}
}
Notice here that the update(fields:) function is not in the class. That way we aren't capturing self and so don't have to worry about weak self. Also, this update function may very well be useful for other form inputs in the app.
You have added disposable in to the dispose bag of LoginViewModel object, which gets released when LoginViewController object gets released.
This means the disposable returned by LoginViewModel observable won't be disposed until LoginViewController gets released or you receive completed or error on areValidFields Observable.
This is in sync with the accepted behaviour in most of the observable cases.
But, in case if you want to dispose the observable when LoginViewController moves out of screen, you can manually dispose:
var areValidFieldsDisposbale:Disposable?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
areValidFieldsDisposbale = loginViewModel.areValidFields.subscribe(
onNext: { [weak self] validArray in
for i in 0..<validArray.count {
if validArray[i] {
self?.showValidUI(index: i)
} else {
self?.showInValidUI(index: i)
}
}
},
onCompleted: {
print("### COMPLETED ###")
},
onDisposed: {
print("### DISPOSED ###")
})
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
areValidFieldsDisposbale?.dispose()
}

Alamofire request doesn't return

I want to create an app that with a textField and a label. The textField will read the input and using Alamofire to check if the input exist. (The url will return JSON Bool.) A helper function called getReturnVal is to get the return value from the url.
However, the nameExist variable is always nil and it seems like the helper function never goes into the two cases. Please advise how should I fix this problem.
Thank you in advance.
My code is as below.
import UIKit
import Alamofire
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var nameField: UITextField!
#IBOutlet weak var nameLabel: UILabel!
var nameValid: Bool = false
func textFieldDidEndEditing(_ textField: UITextField) {
if textField == nameField {
guard let username = textField.text, !username.isEmpty else {
nameValid = false
return
}
let url = "https://bismarck.sdsu.edu/hometown/nicknameexists?name=" + username
print(url)
let nameExist = getReturnVal(url: url, type: Bool.self)
if nameExist! {
nameLabel.text = "Exist"
nameValid = false
} else {
nameLabel.text = "Valid"
nameValid = true
}
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func getReturnVal<T>(url: String, type: T.Type) -> T? {
var jsonObject: T?
print("enter------------")
Alamofire.request(url).validate().responseJSON { response in
switch response.result {
case .success:
if let JSON = response.result.value {
jsonObject = (JSON as! T)
print("SUCCESS--------")
print("JSON: \(String(describing: jsonObject))")
}
case .failure(let error):
print("--------------")
print(error)
jsonObject = false
}
}
return jsonObject
}
override func viewDidLoad() {
super.viewDidLoad()
nameField.delegate = self
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
The Alamofire function is asynchronous, because it’s a network request. This means the function returns before the data from the network request is returned, so it will always return nil because jsonObject is returned before it’s modified. What you need to do is pass in a callback function as a parameter, so that the callback function body gets called when the Alamofire function returns.
The callback signature could be something like this:
callback: (T) -> ()
Then inside the getReturnVal function you can call callback(jsonObject) right after the variable jsonObject is set (so twice). Then when you when you call getReturnVal you need to pass in a function that takes as a parameter that jsonObject.

Add a spinner on making a moya request using RxSwift and mvvm and dismiss it when user receives a response

I have an app where I am trying to implement RxSwift using MVVM.
I have the SignInViewModel where I am doing the validation and I am updating the login observable with the rest response boolean that I am listening to .
In the controller class when ever the validations pass the login button gets enabled.
In a similar manner I want to be able to start a spinner on click of the button and dismiss when the user receives a response.
When I try to listen to the loginObservable in from view model in the controller class. it does not hit the bind block.
I am not able to figure out what the problem is.
Any help will be appreciated
Following is my SignInViewModel
class SignInViewModel {
let validatedEmail: Observable<Bool>
let validatedPassword: Observable<Bool>
let loginEnabled: Observable<Bool>
let loginObservable: Observable<Bool>
init(username: Observable<String>,
password: Observable<String>,
loginTap: Observable<Void>) {
self.validatedEmail = username
.map { $0.characters.count >= 5 }
.shareReplay(1)
self.validatedPassword = password
.map { $0.characters.count >= 2 }
.shareReplay(1)
self.loginEnabled = Observable.combineLatest(validatedEmail, validatedPassword ) { $0 && $1 }
let userAndPassword = Observable.combineLatest(username, password) {($0,$1)}
self.loginObservable = loginTap.withLatestFrom(userAndPassword).flatMapLatest{ (username, password) in
return RestService.login(username: username, password: password).observeOn(MainScheduler.instance)
}
}
}
Following is the moyaRequest class
final class MoyaRequest{
func signIn(userData: Creator) -> Observable<Response> {
return provider.request(.signIn(userData))
.filter(statusCode: 200)
}
}
Following is my RestService class
class RestService:NSObject {
static var moyaRequest = MoyaRequest()
static var disposeBag = DisposeBag()
static func login(username: String, password: String) -> Observable<Bool> {
let userData = Creator()
userData?.username = username
userData?.password = password
print("Username password", userData?.username, userData?.password)
return Observable.create { observer in moyaRequest.signIn(userData: userData!).subscribe{ event -> Void in
switch event {
case .next(let response):
print("Response",response)
case .error(let error):
let moyaError: MoyaError? = error as? MoyaError
let response: Response? = moyaError?.response
let statusCode: Int? = response?.statusCode
print("Sample Response code error" + String(describing: statusCode))
default:
break
}
}
return Disposables.create()
}
}
}
I am trying to bind the view model in the controller class.
class SignInViewController: UIViewController{
let disposeBag = DisposeBag()
#IBOutlet weak var passwordTextfield: UITextField!
#IBOutlet weak var usernameTextfield: UITextField!
private var viewModel : SignInViewModel!
#IBOutlet weak var signInButton: UIButton!
override func viewDidLoad() {
setUpRxViewModel()
}
func setUpRxViewModel(){
self.viewModel = SignInViewModel(username: self.usernameTextfield.rx.text.orEmpty.asObservable(),
password: self.passwordTextfield.rx.text.orEmpty.asObservable(),
loginTap: self.signInButton.rx.tap.asObservable())
self.viewModel.loginEnabled.bind{ valid in
self.signInButton.isEnabled = valid
}.addDisposableTo(disposeBag)
self.viewModel.loginObservable.bind{ input in
print("Login Clicked")
}.addDisposableTo(disposeBag)
}
}
In your login method you are not dispatching any events to your observer. It should be:
case .next(let response):
observer.on(.next(true))
print("Response",response)
case .error(let error):
observer.on(.error(error))
//or observer.on(.next(false)) if you intend to use Bool as indicator of operation success which is a very bad idea.
let moyaError: MoyaError? = error as? MoyaError
let response: Response? = moyaError?.response
let statusCode: Int? = response?.statusCode
furthermore I recommend you use RxMoyaProvider everywhere if you are using Moya with RxSwift. Using Observable.create usually means you are doing something wrong.
You also shouldn't filter off events based on status code at the level of network request because if something goes wrong you are not going to receive any event in your chain.

Resources