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()
}
Related
I have to make validation on ui text field which used in library called RSFloatInputView.
Here is my xib
import UIKit
import RSFloatInputView
class TextInputLayout: UIView {
#IBOutlet weak var revealButton: UIButton!
#IBOutlet weak var warningLabel: UILabel!
#IBOutlet weak var rsFloatingView: RSFloatInputView!
var contentView: UIView?
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
func xibSetup() {
contentView = loadViewFromNib()
contentView!.frame = bounds
contentView!.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
addSubview(contentView!)
}
func loadViewFromNib() -> UIView! {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "TextInputLayout", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
revealButton.tintColor = Color.grayColor()
warningLabel.textColor = UIColor.red
return view
}
}
I want to implement this in this view controller, when i click on next button
import UIKit
import DLRadioButton
class SecureWalletViewController: UIViewController,UITextFieldDelegate {
#IBOutlet weak var securityPinStackView: UIStackView!
#IBOutlet weak var securityPin: TextInputLayout!
#IBOutlet weak var confirmSecurityPin: TextInputLayout!
#IBAction func onNextButtonTap(_ sender: Any) {
}
func textInputLayout(at index:Int) -> TextInputLayout {
return securityPinStackView.arrangedSubviews[index] as! TextInputLayout
}
}
Use validations for UITextFieldDelegate method like given below:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return true
}
Or use custom validation function Here
Iam using this
// add func in swift class
struct validatorConstants{
static let errorMsg = "your error messages"
static let customMsg = "your error messages"
static let emailMsg = "your error messages"
}
class Validators: NSObject {
//MARK: Validation on any Empty TextField
func validators(TF1:UITextField,errorMsg:String = validatorConstants.errorMsg,fieldName:String = "") -> Bool {
var error = validatorConstants.errorMsg
if fieldName.count > 0 {
error = fieldName + " is missing"
}
if TF1.text?.isEmpty == true{
kAppDelegate.showNotification(text: error)
return false
}
return true
}
//MARK: Validation on any Email TextField
func validatorEmail(TF1:UITextField,errorMsg:String = validatorConstants.errorMsg ,errorMsgEmail:String = validatorConstants.emailMsg,fieldName:String = "Email" ) -> Bool {
var error = validatorConstants.errorMsg
if fieldName.count > 0 {
error = fieldName + " is missing"
}
if TF1.text?.isEmpty == true{
kAppDelegate.showNotification(text: error)
return false
}
if TF1.text?.isValidEmail == false{
kAppDelegate.showNotification(text: errorMsgEmail)
return false
}
return true
}
}
// call this func like this
// your ViewController
var validator:Validators!
// In viewdidload
validator = Validators()
// call this func on button Action
guard validator.validators(TF1: txtfied,fieldName: "your txtfield name") == false
else
{
//do something what you want
return
}
// Its works for me hope its works for you
I'd recommend to use a UITextField subclass with 2 UI states (regular / invalid) and a validation rule (e.g. not empty / match regex / etc)
class ValidationTextField: UITextField {
enum ValidationRule {
case notEmpty
// case matchRegex(regex: NSRegularExpression)
// ...
}
var validationRule: ValidationRule?
private(set) var isValid:Bool = true {
didSet {
updateUIForCurrentState()
}
}
// Call this method on the "next" button click
// (or from the delegate on the textFieldDidEndEditing event for early validation)
func validate() {
guard let rule = validationRule else {
// nothing to validate
return;
}
switch rule {
case .notEmpty:
if let length = text?.count {
isValid = length > 0
}
else {
isValid = false
}
// process other cases (e.g. matchRegex)
}
}
/// Configure your state-specific layout here.
private func updateUIForCurrentState() {
// Current implementation adds a red border in case of invalid input
if isValid {
layer.borderWidth = 0
layer.borderColor = nil
}
else {
layer.borderWidth = 2
layer.borderColor = UIColor.red.cgColor
}
}
}
You can use SwiftValidator, It is rule based validator.
let validator = Validator()
//Register the fields that you want to validate
validator.registerField(fullNameTextField, rules: [RequiredRule(), FullNameRule()])
#IBAction func signupTapped(sender: AnyObject) {
validator.validate(self)
}
I'm new to RxSwift and attempting to do as the title states with an MVVM input output approach.
I can't figure out the best approach to do the following.
Validate the phoneNumberTextField values when submitButton is tapped
Stop the Alamofire Request from being submitted if phoneNumberTextField is invalid and throw a client side error
Show a display indicator when loading takes place. This is the least important right now
A few things to note.
There is nothing tracking the phone number text at the moment
I do not want to disable the submit button until the form is valid as seen in examples all over.
Here is my view controller
import UIKit
import RxSwift
import RxCocoa
class SplashViewController: BaseViewController {
// MARK: – View Variables
#IBOutlet weak var phoneNumberTextField: UITextField!
#IBOutlet weak var phoneNumberBackgroundView: UIView!
#IBOutlet weak var submitButton: BaseButton!
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var separatorView: UIView!
#IBOutlet weak var countryCodeButton: UIButton!
#IBOutlet weak var parentVerticalStackView: UIStackView!
// MARK: – View Model & RxSwift Setup
private let disposeBag = DisposeBag()
private let viewModel: SplashMVVM = SplashMVVM()
// MARK: – View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// RxSwift handling
setupViewModelBinding()
setupCallbacks()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: true)
}
// MARK: – RxSwift Handling
private func setupViewModelBinding() {
submitButton.rx.controlEvent(.touchUpInside)
.bind(to: viewModel.input.submit)
.disposed(by: disposeBag)
}
private func setupCallbacks() {
viewModel.output.success.asObservable()
.filter { $0 != nil }
.observeOn(MainScheduler())
.subscribe({ _ in
self.pushVerifyPhoneNumberViewController()
})
.disposed(by: disposeBag)
viewModel.output.error.asObservable()
.filter { $0 != nil }
.observeOn(MainScheduler())
.subscribe({ _ in
SwiftMessages.show(.error, message: "There was an error. Please try again.")
})
.disposed(by: disposeBag)
}
// MARK: – Navigation
func pushVerifyPhoneNumberViewController() {
let viewController = VerifyPhoneNumberViewController.fromStoryboard("Authentication")
self.navigationController?.pushViewController(viewController, animated: true)
}
}
Here is my view model.
import Foundation
import RxSwift
import RxCocoa
import Alamofire
final class SplashMVVM: InputOutputModelType {
let input: SplashMVVM.Input
let output: SplashMVVM.Output
var submitSubject = PublishSubject<Void>()
struct Input {
let submit: AnyObserver<Void>
}
struct Output {
let success: Observable<VerifyMobilePhone?>
let error: Observable<Error?>
}
init() {
input = Input(submit: submitSubject.asObserver())
let request = Alamofire.request(VerifyMobileRouter.post("+16306996540")).responseDecodableRx(VerifyMobilePhone.self)
let requestData = submitSubject.flatMapLatest {
request
}
let success = requestData.map { $0.value ?? nil }
let error = requestData.map { $0.error ?? nil }
output = Output(
success: success,
error: error
)
}
}
Here is what I came up with.
final class SplashMVVM: InputOutputModelType {
let input: SplashMVVM.Input
let output: SplashMVVM.Output
var submitSubject = PublishSubject<Void>()
var phoneNumberSubject = PublishSubject<String>()
struct Input {
let phoneNumber: AnyObserver<String>
let submit: AnyObserver<Void>
}
struct Output {
let validationError: Observable<String>
let success: Observable<VerifyMobilePhone>
let error: Observable<Error>
}
init() {
input = Input(phoneNumber: phoneNumberSubject.asObserver(), submit: submitSubject.asObserver())
let request = submitSubject.asObservable().withLatestFrom(phoneNumberSubject.asObservable()).filter {
$0.isValidPhoneNumber(region: "US")
}.flatMap { number in
Alamofire.request(VerifyMobileRouter.post(number)).responseDecodableRx(VerifyMobilePhone.self)
}.share()
let validationError = submitSubject.asObservable().withLatestFrom(phoneNumberSubject.asObservable()).filter {
!$0.isValidPhoneNumber(region: "US")
}.map { _ in
"This phone number is invalid"
}
let success = request.filter { $0.isSuccess }.map { $0.value! }
let error = request.filter { $0.isFailure }.map { $0.error! }
output = Output(
validationError: validationError,
success: success,
error: error
)
}
}
View controller changes…
private func setupViewModelBinding() {
submitButton.rx.controlEvent(.touchUpInside).bind(to: viewModel.input.submit).disposed(by: disposeBag)
phoneNumberTextField.rx.text.orEmpty.bind(to: viewModel.input.phoneNumber).disposed(by: disposeBag)
}
private func setupCallbacks() {
viewModel.output.validationError.bind { string in
SwiftMessages.show(.error, message: string)
}.disposed(by: disposeBag)
viewModel.output.success.bind { verifyMobilePhone in
self.pushVerifyPhoneNumberViewController()
}.disposed(by: disposeBag)
viewModel.output.error.bind { error in
SwiftMessages.show(.error, message: "There was an error. Please try again.")
}.disposed(by: disposeBag)
}
You are close, you're just missing the phone number text as input into your view model.
struct SplashInput {
let phoneNumber: Observable<String>
let submit: Observable<Void>
}
struct SplashOutput {
let invalidInput: Observable<Void>
let success: Observable<VerifyMobilePhone>
let error: Observable<Error>
}
extension SplashOutput {
init(_ input: SplashInput) {
let request: Observable<Event<VerifyMobilePhone>> = input.submit.withLatestFrom(input.phoneNumber)
.filter { $0.isValidPhoneNumber }
.flatMap { number in
Alamofire.request(VerifyMobileRouter.post(number)).responseDecodableRx(VerifyMobilePhone.self)
.materialize()
}
.share()
invalidInput = input.submit.withLatestFrom(input.phoneNumber)
.filter { $0.isValidPhoneNumber == false }
success = request
.map { $0.element }
.filter { $0 != nil }
.map { $0! }
error = request
.map { $0.error }
.filter { $0 != nil }
.map { $0! }
}
}
Your SplashViewController would have:
override func viewDidLoad() {
super.viewDidLoad()
let input = SplashInput(
phoneNumber: phoneNumberTextField.rx.text.orEmpty.asObservable(),
submit: submitButton.rx.tap.asObservable()
)
let viewModel = SplashOutput(input)
viewModel.invalidInput
.bind {
SwiftMessages.show(.invalid, message: "You entered an invalid number. Please try again.")
}
.disposed(by: bag)
viewModel.success
.bind { [unowned self] verifyMobilePhone in
self.pushVerifyPhoneNumberViewController(verifyMobilePhone)
}
.disposed(by: bag)
viewModel.error
.bind { error in
SwiftMessages.show(.error(error), message: "There was an error. Please try again.")
}
}
(I took some liberties with what you already have written, but the above should make sense.)
I'm implementing the mvvm using RxSwif.
Here is what happens:
Validate Fields (write 7 characters in both textFields).
Tap the login button.
API is called which is fine.
Tap the button again.
API is not called.
I noticed that the "validObservable" changes every time that I write something in textFields which is fine.
ViewController:
var viewModel: LoginViewModelType!
let disposeBag = DisposeBag()
#IBOutlet weak var dniTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var logInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
logInButton.rx
.tap
.bind(to: viewModel.inputs.logInButtonDidTap)
.disposed(by: disposeBag)
dniTextField.rx
.text
.bind(to: viewModel.inputs.dniChanged)
.disposed(by: disposeBag)
passwordTextField.rx
.text
.bind(to: viewModel.inputs.passwordChanged)
.disposed(by: disposeBag)
/*viewModel.outputs.isLoginButtonEnabled
.drive(onNext: { [weak self] isEnabled in
guard let `self` = self else { return }
self.logInButton.isEnabled = isEnabled
})
.disposed(by: disposeBag)
*/
viewModel.outputs.logIn
.drive(onNext: { [weak self] user in
guard let `self` = self else { return }
self.performSegue(withIdentifier: "showMainController", sender: user)
})
.disposed(by: disposeBag)
}
override func awakeFromNib() {
super.awakeFromNib()
viewModel = LoginViewModel()
}
ViewModel:
var inputs: LoginViewModelInputs { return self }
var outputs: LoginViewModelOutputs { return self }
// ---------------------
// MARK: - Inputs
// ---------------------
var dniChanged: BehaviorRelay<String?>
var passwordChanged: BehaviorRelay<String?>
var logInButtonDidTap: PublishSubject<Void> = PublishSubject<Void>()
// ---------------------
// MARK: - Outpuds
// ---------------------
var logIn: Driver<User>
var isLoginButtonEnabled: Driver<Bool>
private let disposeBag = DisposeBag()
public init () {
dniChanged = BehaviorRelay<String?>(value: "")
passwordChanged = BehaviorRelay<String?>(value: "")
let dniObservable = dniChanged.asDriver().filterNil().asObservable()
let passwordObservable = passwordChanged.asDriver().filterNil().asObservable()
let dniValidation = dniObservable.map { $0.count > 4 }
let passwordValidation = passwordObservable.map { $0.count > 3 }
let validObservable = Observable.combineLatest(dniValidation, passwordValidation) { return $0 && $1 }.filter { $0 }
isLoginButtonEnabled = validObservable.asDriver(onErrorDriveWith: .empty())
let loginSuccessObservable = Observable.combineLatest(dniObservable, passwordObservable, validObservable) { (dni, password, valid) -> LogInRequest in
return LogInRequest(dni: dni, password: password)
}
logIn = logInButtonDidTap.withLatestFrom(loginSuccessObservable).flatMapLatest({ request -> Observable<User> in
return API.shared.post(endpoint: EndPoints.Authorize, type: User.self, body: request)
}).asDriver(onErrorDriveWith: .empty())
}
I'd say that .empty() is causing the observable to complete, and the subscription to be disposed of as a consequence.
I'd put some .debug() instructions to make sure what gets disposed and when.
I got this problem because the button size is zero
Here's a User model class. This model will be container for data while registering new user, logging an already registered user and displaying profile.
struct User {
typealias message = (Bool,String)
var name: String?
var username: String
var password: String
var image: String?
func isValidForLogin() -> message {
let emailMessage = isValidEmail(testStr: username)
let passwordMessage = isValidPassowrd(testStr: password)
if emailMessage.0 && passwordMessage.0 {
return (true,"Valid")
}
if !emailMessage.0{
return (emailMessage.0, emailMessage.1)
}else{
return (passwordMessage.0, passwordMessage.1)
}
}
func isValidForRegister() -> message {
if let name = self.name{
let nameMessage = isValidName(testStr: name)
let emailMessage = isValidEmail(testStr: username)
let passwordMessage = isValidPassowrd(testStr: password)
if emailMessage.0 && passwordMessage.0 && nameMessage.0{
return (true,"Valid")
}
if !emailMessage.0{
return (emailMessage.0, emailMessage.1)
}else if !passwordMessage.0{
return (passwordMessage.0, passwordMessage.1)
}else{
return (nameMessage.0, nameMessage.1)
}
}
return (false, "Name " + Constants.emptyField)
}
private func isValidName(testStr: String) -> message{
if testStr.isEmpty{
return (false, "Name " + Constants.emptyField )
}
return (true, "Valid")
}
private func isValidPassowrd(testStr: String) -> (Bool, String) {
if testStr.isEmpty{
return (false, "Password " + Constants.emptyField )
}
if testStr.count > 6{
return (true, "Valid")
}
return (false, Constants.invalidPassword)
}
private func isValidEmail(testStr: String) -> message {
if testStr.isEmpty{
return (false, "Email " + Constants.emptyField)
}
let emailRegEx = "^(?:(?:(?:(?: )*(?:(?:(?:\\t| )*\\r\\n)?(?:\\t| )+))+(?: )*)|(?: )+)?(?:(?:(?:[-A-Za-z0-9!#$%&’*+/=?^_'{|}~]+(?:\\.[-A-Za-z0-9!#$%&’*+/=?^_'{|}~]+)*)|(?:\"(?:(?:(?:(?: )*(?:(?:[!#-Z^-~]|\\[|\\])|(?:\\\\(?:\\t|[ -~]))))+(?: )*)|(?: )+)\"))(?:#)(?:(?:(?:[A-Za-z0-9](?:[-A-Za-z0-9]{0,61}[A-Za-z0-9])?)(?:\\.[A-Za-z0-9](?:[-A-Za-z0-9]{0,61}[A-Za-z0-9])?)*)|(?:\\[(?:(?:(?:(?:(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5]))\\.){3}(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5]))))|(?:(?:(?: )*[!-Z^-~])*(?: )*)|(?:[Vv][0-9A-Fa-f]+\\.[-A-Za-z0-9._~!$&'()*+,;=:]+))\\])))(?:(?:(?:(?: )*(?:(?:(?:\\t| )*\\r\\n)?(?:\\t| )+))+(?: )*)|(?: )+)?$"
let emailTest = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
let result = emailTest.evaluate(with: testStr)
if result{
return (result, "Valid")
}else{
return (result, Constants.invalidEmail)
}
}
}
I am trying to follow MVVM pattern. So, my ViewModel class for RegisterViewViewModel:
struct RegisterViewModel {
private let minUserNameLength = 4
private let minPasswordLength = 6
var name: String
var email: String
var password: String
private var userModel: User{
return User(name: name, username: email, password: password, image: "")
}
func isValid() -> (Bool, String) {
return userModel.isValidForRegister()
}
func register(){
....
}
}
And in my RegisterViewController :
class RegisterViewController: UIViewController{
#IBOutlet weak var txtName: UITextField!
#IBOutlet weak var txtUsername: UITextField!
#IBOutlet weak var txtPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func btnSignUpPressed(_ sender: UIButton) {
if let name = txtName.text, let email = txtUsername.text, let password = txtPassword.text{
let userModel = RegisterViewModel(name: name, email: email, password: password)
let validate = userModel.isValid()
if validate.0{
userModel.register()
}else{
//do error handling here
print(validate.1)
}
}
}
}
Am I going in right direction? Any suggestion will be appreciated.
I would recommend you to use RxSwift with MVVM. Also you could export validation to a separate ValidationService class. Otherwise you will probably have to copy same validation methods between different models.
enum ValidationResult {
case ok
case empty
case validating
case failed(message: String)
}
extension ValidationResult {
var isValid: Bool {
switch self {
case .ok:
return true
default:
return false
}
}
var isEmpty: Bool {
switch self {
case .empty:
return true
default:
return false
}
}
}
class ValidationService {
let minPasswordCount = 4
static let shared = ValidationService()
func validateName(_ name: String) -> Observable<ValidationResult> {
if name.isEmpty {
return .just(.empty)
}
if name.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil {
return .just(.failed(message: "Invalid name"))
}
return .just(.ok)
}
}
What you are trying to do is not MVVM pattern.
You are creating a new ViewModel when button is clicked. It is the same as you are creating a business class to handle some business logics.
ViewModel and View are communicating through data binding. If you are familiar with RxSwift, the I suggest to use this library: https://github.com/duyduong/DDMvvm
I wrote this library after using it a lot on private projects. There are examples for you to start and understand how MVVM works. Give it a try!
To implement MVVM in iOS we can use a simple combination of Closure and didSet to avoid third-party dependencies.
public final class Observable<Value> {
private var closure: ((Value) -> ())?
public var value: Value {
didSet { closure?(value) }
}
public init(_ value: Value) {
self.value = value
}
public func observe(_ closure: #escaping (Value) -> Void) {
self.closure = closure
closure(value)
}
}
An example of data binding from ViewController:
final class ExampleViewController: UIViewController {
private func bind(to viewModel: ViewModel) {
viewModel.items.observe(on: self) { [weak self] items in
self?.tableViewController?.items = items
// self?.tableViewController?.items = viewModel.items.value // This would be Momory leak. You can access viewModel only with self?.viewModel
}
// Or in one line:
viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
}
protocol ViewModelInput {
func viewDidLoad()
}
protocol ViewModelOutput {
var items: Observable<[ItemViewModel]> { get }
}
protocol ViewModel: ViewModelInput, ViewModelOutput {}
final class DefaultViewModel: ViewModel {
let items: Observable<[ItemViewModel]> = Observable([])
// Implmentation details...
}
Later it can be replaced with SwiftUI and Combine (when a minimum iOS version in of your app is 13)
In this article, there is a more detailed description of MVVM
https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3
class RegisterViewController: UIViewController {
var user = User() {
didSet {
// update UI
}
}
}
Most MVVM/RxSwift developers don't understand the notion of "over-engineering", as can be seen from all previous answers. Two of them refer you to a even more complicated design pattern, and one of them built the said pattern from scratch.
You don't need any of the RxSwift nonsense. MVVM isn't about having an object called view model and shoving everything to it.
Build a model so that when it changes, it updates associated view.
Simple, as all things should be.
Below is the pinnacle of over-engineering
protocol ViewModel: ViewModelInput, ViewModelOutput {}
After you define all these, write them down, train colleagues, draw diagrams, and implement them, you would've realized that it's all boilerplate and you should just drop them.
I'm newer to RxSwift. I want to refresh the tableview to show new data.The first request that I can get the data. but when I pull down the tableview, the request didn't finished. I have no ideas about this? My code is belowing:
1: My viewController's code:
class RecommendViewController: UIViewController {
lazy var tableView = DefaultManager.createTableView(HomeImageCell.self,
HomeImageCell.idenfitier)
let disposeBag = DisposeBag()
lazy var viewModel = HomeViewModel()
lazy var dataSource: [HomeListDetailModel] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchRecommendList("answer_feed",0)
setupTableView()
configureRefresh()
bindDataToTableView()
}
func setupTableView() {
view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.edges.equalTo(0)
}
tableView.estimatedHeight(200)
}
func bindDataToTableView() {
viewModel.recommend
.observeOn(MainScheduler.instance)
.do(onNext: { [unowned self] model in
print("endAllRefresh")
self.endAllRefresh()
}, onError: { (error) in
self.endAllRefresh()
print("error = \(error)")
})
.map { [unowned self] model in
return self.handleData(model)
}.bind(to: tableView.rx.items(cellIdentifier: HomeImageCell.idenfitier , cellType: HomeImageCell.self )) { index, model, cell in
cell.updateCell(data: model)
}.disposed(by: disposeBag)
}
func configureRefresh() {
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { [unowned self] in
let model = self.dataSource[0]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { [unowned self] in
let model = self.dataSource[self.dataSource.count - 1]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
}
func endAllRefresh() {
self.tableView.mj_header.endRefreshing()
self.tableView.mj_footer.endRefreshing()
}
func handleData(_ model: HomeListModel) -> [HomeListDetailModel] {
guard let data = model.detailData else {
return dataSource
}
self.dataSource = data
return data
}
}
2: My ViewModel
protocol HomeProtocol {
func fetchRecommendList(_ category: String, _ behot_time: Int)
}
class HomeViewModel: HomeProtocol {
lazy var provider = HTTPServiceProvider.shared
var recommend: Observable<HomeListModel>!
init() {}
init(_ provider: RxMoyaProvider<MultiTarget>) {
self.provider = provider
}
func fetchRecommendList(_ category: String, _ behot_time: Int) {
recommend = provider.request(MultiTarget(HomeAPI.homeList(category: category,behot_time: behot_time)))
.debug()
.mapObject(HomeListModel.self)
}
}
When I made a breakpoint at request method, it didn't do a request? Does anyone know it ? Thanks first
SomeOne told me the reason,So I write it here. In my ViewModel recommend should be backed by PublishSubject or BehaviourSubject or ReplaySubject and then I should share this for View as Observable. In fetchRecommentList method I should bind request to created Subject.
Now I have created observable, but request will run after subsribe or bind