iOS Tutorial - The standard login screen but in rx and mvvm !
In this tutorial we gonna explain how mvvm can be used along with rx
In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change. With this paradigm it is possible to express static (e.g., arrays) or dynamic (e.g., event emitters) data streams with ease, and also communicate that an inferred dependency within the associated execution model exists, which facilitates the automatic propagation of the changed data flow.[citation needed]
an implementation of rx for swift
You can think about the view model as the logic blackbox that receive inputs and output results.
In our case using rx it receives "streams of inputs" and produces streams of outputs
something like "Reacting to actions"
MVVM tries to separate the logic from the controller and make it "Testable" so you don't have to complicate things to test something
Before we begin our tutorial you have to put one thing in your mind :D "This is An implementation combine rx with mvvm "
You can think in other implementation or find a better one for it
First step :
create an new Xcode project and make sure that its hierarchy look like this:
1- protocols : hold all the protocols which needs to be implemented
2- vc login : login view controller which will hold ref to login view model
3- vm login : The login view model which will contains many login services including for ex facebook , native login for application and gmail, etc.
4- native login service : the network logic for the native login service
Second step :
storyboard configuration :
we have only one screen , the login screen its so simple because we focus on the pattern more:
Third Step:
The UIStackView constraints
4th step:
Creating the protocols:
the ViewModel protocol :
protocol ViewModelProtocol: class {
associatedtype Input
associatedtype Output
}
This means every view model will have these types of variables Input and Output
The controller protocol:
protocol ControllerType : class {
associatedtype ViewModelType: ViewModelProtocol
func configure(with viewModel: ViewModelType)
/// Factory function for view controller instatiation
///
/// - Parameter viewModel: View model object
/// - Returns: View controller of concrete type
static func create(with viewModel: ViewModelType) -> UIViewController
}
it has function to create a view controller binds to a view model
(this helps if you designing views by code you may have to instantiate the view controller an present it )
(this helps if you designing views by code you may have to instantiate the view controller an present it )
and another function to configure it if you using the storyboards
ViewModelType is a a type must conformed to, and must extend ViewModelProtocol
5th step :
The Login view model:
preparing the inputs and outputs:
We need an email ? maybe password also the sign tap event
class LoginInput {
let email: BehaviorSubject<String>
let password: BehaviorSubject<String>
let signInDidTap: PublishSubject<Void>
init() {
email = BehaviorSubject<String>(value: "")
password = BehaviorSubject<String>(value: "")
signInDidTap = PublishSubject<Void>()
}
}
the login output we need to observe the login result (user in this case) and errors
class LoginOutput {
let loginResultObservable: PublishSubject<User>
let errorsObservable: PublishSubject<Error>
init() {
loginResultObservable = PublishSubject<User>()
errorsObservable = PublishSubject<Error>()
}
}
The view model itself:
class LoginViewModel : ViewModelProtocol {
typealias Input = LoginInput
typealias Output = LoginOutput
let input : Input
let output: Output
let disposeBag = DisposeBag()
var loginServices : [String : LoginService] = [:]
func addLoginService (loginService : LoginService , name : String) {
loginServices[name] = loginService
}
init() {
input = Input()
output = Output()
input.signInDidTap
.subscribe({_ in
self.loginServices["native"]?
.signIn(userName: try! self.input.email.value() ,
password: try! self.input.password.value())
.subscribe(onNext: {
(user) in
self.output.loginResultObservable.onNext(user)
}, onError: {
(err) in
self.output.errorsObservable.onNext(err)
}).disposed(by: self.disposeBag)
})
.disposed(by: disposeBag)
}
}
You must confirm to ViewModelProtocol to tell him what types that confirms to LoginInput and LoginOutput
we use typealias to do this
in init function we just initialize the input and output with its dummy values
then subscribe to sign in tap to call sign in api and pass the outputs for it to the view model outputs
Implementation of native login service:
enum Authentication : Error {
case invalidCredintials (String)
}
class NativeLoginService : LoginService {
var users = [ "user0" : "12345678" ,
"user1" : "12345678",
"user2" : "951753" ]
func signIn(userName: String, password: String) -> Observable<User> {
return Observable.create { observer in
if let exsitUser = self.users.first(where: { (arg0) -> Bool in
let (key, value) = arg0
return (userName == key) && (password == value)
}) {
observer.onNext(User(name : exsitUser.key))
}
else {
observer.onError(Authentication.invalidCredintials("INVALID USER NAME OR PASSWORD PLEASE TRY LATER"))
}
return Disposables.create()
}
}
}
simulating the network logic by creating a key value map holds some users
6th step:
creating the login view controller it self
class VCLogin : UIViewController , ControllerType {
@IBOutlet weak var passwordTextfield: UITextField!
@IBOutlet weak var emailTextfield: UITextField!
@IBOutlet weak var signInButton: UIButton!
typealias ViewModelType = LoginViewModel
var viewModel : ViewModelType!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
configure(with: viewModel)
}
func presentError(_ e:Error? ) {
print(e ?? "")
presentMessage(e?.localizedDescription ?? "")
}
func presentMessage(_ msg : String) {
let alert = UIAlertController(title: "Login", message: msg, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
create an instance of it
extension VCLogin {
static func create(with viewModel: ViewModelType) -> UIViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "VCLogin") as! VCLogin
controller.viewModel = viewModel
return controller
}
}
and this function to configure and connect the UIElements to associated input and output
think about wiring it up with the view model cables
func configure(with viewModel: LoginViewModel) {
// wiring the email text field to viewmodel email input
emailTextfield.rx.text.orEmpty
.subscribe(viewModel.input.email)
.disposed(by: disposeBag)
// the same
passwordTextfield.rx.text.orEmpty
.subscribe(viewModel.input.password)
.disposed(by: disposeBag)
signInButton.rx.tap.asObservable()
.subscribe(viewModel.input.signInDidTap)
.disposed(by: disposeBag)
viewModel.output.errorsObservable
.subscribe({ [unowned self] (error) in
self.presentError(error.element)
})
.disposed(by: disposeBag)
viewModel.output.loginResultObservable
.subscribe({ [unowned self] (user) in
self.presentMessage("User successfully signed in \(user.element?.name ?? "")")
})
.disposed(by: disposeBag)
}
Now we have completed the tutorial resulting a clean implementation of login screen with rx and mvvm pattern!
refs :
Full project : https://github.com/madadoux/RxMvvmSwift






Comments
Post a Comment