Passed in data MVVM RxSwift - ios

I want to learn MVVM RxSwift with input and output method, I want to get a username from textfield.
I have a scenario when user not enter a username it will present an error and when user enter a username it will present in viewController.
This is when I confuse. I got the error message and successfully present error but, how can I catch the query in my viewModel and passed the data to viewController.
This is how I setup my searchViewModel
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
class SearchViewModel: ViewModelType {
// MARK: Binding
struct Input {
let searchText: Observable<String>
let validate: Observable<Void>
}
struct Output {
let username: Driver<String>
}
func transform(input: Input) -> Output {
let username = input.validate
.withLatestFrom(input.searchText)
.map { query in
if query.isEmpty {
return "Please enter a username. We need to know who to look for"
} else {
return query
}
}.asDriver(onErrorJustReturn: "")
return Output(username: username)
}
}
and this is my viewDidLoad in SearchViewController
let searchTextField = GFTextField()
let calloutBtn = GFButton(backgroundColor: .systemGreen, title: "Get followers")
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupImageView()
setupTextfield()
setupCalloutBtn()
let input = SearchViewModel.Input(
searchText: searchTextField.rx.text.orEmpty.asObservable(),
validate: calloutBtn.rx.tap.asObservable())
let output = viewModel.transform(input: input)
output.username.drive { [weak self] username in
guard let self = self else { return }
self.presentGFAlertOnMainThread(title: "Empty Username", message: username, buttonTitle: "Dismiss")
}.disposed(by: disposeBag)
}

It depends on what you want to do with the text of course, but below I assume you want to make a network request. But of course that requires understanding what your API layer looks like. I have to make some assumptions there as well, but the key is that you need to inject your API layer into your view model through its constructor/init method.
Like this:
class SearchViewModel: ViewModelType {
struct Input {
let username: Observable<String>
let getFollowers: Observable<Void>
}
struct Output {
let errorMessage: Driver<String>
let followers: Driver<[Follower]>
}
let api: API
init(api: API) {
self.api = api
}
func transform(input: Input) -> Output {
let followersResponse = input.getFollowers
.withLatestFrom(input.username)
.filter { !$0.isEmpty }
.map { makeEndpoint(using: $0) }
.flatMap { [api] in
api.load($0)
}
.share()
let missingName = input.getFollowers
.withLatestFrom(input.username)
.compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }
let errorMessage = Observable.merge(
api.error.map { $0.localizedDescription },
missingName
)
.asDriver(onErrorJustReturn: "")
let followers = followersResponse
.asDriver(onErrorJustReturn: [])
return Output(errorMessage: errorMessage, followers: followers)
}
}
-- EDIT --
If all you want to do is push the non-empty text field back to the view controller, then it would look like this:
class SearchViewModel: ViewModelType {
struct Input {
let username: Observable<String>
let getFollowers: Observable<Void>
}
struct Output {
let errorMessage: Driver<String>
let username: Driver<String>
}
func transform(input: Input) -> Output {
let errorMessage = input.getFollowers
.withLatestFrom(input.username)
.compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }
.asDriver(onErrorJustReturn: "")
let username = input.getFollowers
.withLatestFrom(input.username)
.filter { !$0.isEmpty }
.asDriver(onErrorJustReturn: "")
return Output(errorMessage: errorMessage, username: username)
}
}
The key here is that you need a Driver for each output that the view controller will want to subscribe to.

Related

RxSwift, invoking a network call

I am new to Rx and trying to one network call.
i mange to do that as follow:
struct Genre: Codable {
var genres: [Genres]?
}
struct Genres: Codable {
var id: Int?
var name: String?
}
final class GenreViewModel {
let disposeBag = DisposeBag()
var networking: Networking!
let getGeners = PublishRelay<[Genres]>()
var items: PublishRelay<[Genres]>?
var itemsDriver: Driver<[Genres]>?
let isLoading: Driver<Bool>
init(networking: Networking = Networking()) {
self.networking = networking
let shouldLoadItems = Observable.merge(
).debug("merge.debug(🚘)")
.startWith(())
let gg = shouldLoadItems.flatMap {
networking.preformNetwokTask(
endPoint: TheMoviedbApi.genre,
type: Genre.self)
.debug("🚘 network call")
}
isLoading = Observable.merge(
shouldLoadItems.map { true },
gg.map { _ in false }
)
.asDriver(onErrorJustReturn: false).debug("🚘 is loading")
itemsDriver = gg
.map { $0.genres! }
.asDriver(onErrorJustReturn: []).debug("🚘drive")
}
i am trying to figure a way of doing it without "let shouldLoadItems".
something like that:
final class GenreViewModel {
let disposeBag = DisposeBag()
var networking: Networking!
let getGeners = PublishRelay<[Genres]>()
var items: PublishRelay<[Genres]>?
var itemsDriver: Driver<[Genres]>?
let isLoading: Driver<Bool>
init(networking: Networking = Networking()) {
self.networking = networking
let geners = getGeners
.flatMap { geners in
networking.preformNetwokTask(
endPoint: TheMoviedbApi.genre,
type: Genre.self)
.debug("🚘 network call not in use")
}.startWith(())
.share()
isLoading = Observable.merge(
shouldLoadItems.map { true },
gg.map { _ in false }
)
.asDriver(onErrorJustReturn: false).debug("🚘 is loading")
itemsDriver = geners
.map { $0.genres! }
.asDriver(onErrorJustReturn: []).debug("🚘drive")
}
The vc:
func bindRx() {
viewModel.isLoading
.drive(hud.rx.animation)
.disposed(by: disposeBag)
viewModel.itemsDriver?.drive(collectionView.rx.items(cellIdentifier: GenreCollectionViewCell.reuseIdentifier, cellType: GenreCollectionViewCell.self)) { (row,item,cell) in
cell.config(item: item)
}.disposed(by: disposeBag)
}
Yet let geners not called.
what am i missing out?
I expect all the questions in the comments was a bit of a pain, but the thing that was missing in this question was the "reactive" part of "functional reactive programming".
In FRP, a network request doesn't happen in isolation. It's usually triggered by an event producer and feeds an event consumer. In order for the request to happen at all, an event consumer must exist.
A view model, on its own and not connected to anything, does nothing. In order for your networking event to happen, something must be listening for the result. Your view model is like the plumbing in your house. The water doesn't flow unless someone opens a tap.
struct GenreViewModel {
let items: Observable<[Genres]>
let error: Observable<Error>
let isLoading: Observable<Bool>
init(networking: Networking) {
let result = networking.preformNetwokTask(endPoint: TheMoviedbApi.genre, type: Genre.self)
.compactMap(\.genres)
.materialize()
.share(replay: 1)
items = result
.compactMap { $0.element }
error = result
.compactMap { $0.error }
isLoading = result
.map { _ in false }
.startWith(true)
}
}
final class GenreViewController: UIViewController {
var tableView: UITableView!
var activityIndicatorView: UIActivityIndicatorView!
var viewModel: GenreViewModel!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.items
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { _, genres, cell in
cell.textLabel?.text = genres.name
}
.disposed(by: disposeBag)
viewModel.error
.bind(onNext: presentScene(animated: true) {
UIAlertController(title: "Error", message: $0.localizedDescription, preferredStyle: .alert)
.scene { $0.connectOK() }
})
.disposed(by: disposeBag)
viewModel.isLoading
.bind(to: activityIndicatorView.rx.isAnimating)
.disposed(by: disposeBag)
}
}
In the above code, the network request will start as soon as the bind call is executed. If an error emits, it is routed to the error Observable and will display a UIAlertController with the error message (It uses the CLE-Architecture-Tools to do this.) If it succeeds, the genres objects' names will be displayed in the table view.

How to conditionally load view upon app launch in SwiftUI?

I'm currently trying to implement an auto-login feature to my app using UserDefaults. What I would like to do before loading any view is get the UserDefaults email and password and call the login function from my API. If successful, go to Home view, else go to LoginView. My apologies, I'm very new to Swift and on a tight schedule with my project. Here is my code segment. I'm not sure where I can add my logic:
import SwiftUI
#main
struct MyApp: App {
init() {
let email = UserDefaults.standard.string(forKey: "email");
let pw = UserDefaults.standard.string(forKey: "pw");
let api = MyAppAPI()
api.signInUser(email: email, password: pw) { result in
//JSON response contains an 'isError' field
let isError = result.value(forKey: "error") as! Bool
if !isError {
//successful login - what to do from here?
}
}
}
var body: some Scene {
WindowGroup {
LoginView()
}
}
}
Here is a simple way of doing this, you can do this onAppear
import SwiftUI
struct ContentView: View {
let email: String
let pass: String
init() {
self.email = UserDefaults.standard.string(forKey: "email") ?? ""
self.pass = UserDefaults.standard.string(forKey: "pw") ?? ""
}
#State private var result: Bool?
var body: some View {
Group {
if let unwrappedResult: Bool = result {
if unwrappedResult {
Text("Home View, Welcome!")
}
else {
Text("Wrong User or Pass, try again!")
}
}
else {
Text("loading...")
}
}
.onAppear() { loginFunction(email: email, pass: pass) { value in result = value } }
}
}
func loginFunction(email: String, pass: String, completion: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(3000)) { completion(Bool.random()) }
}

Making Api call on View DidLoad with MVVM swift

I have an application where I want to make an API call once the screen is awaken in the ViewController. Basically, I am using Universal Link to activate the ViewCOntroller and when it displays the UIViewController, I want to make an API call based on the Data got. I am currently using the MVVM Architecture and I have added my code below
My ViewModel
class EmailVerificationViewModel: ViewModel, ViewModelType {
struct Input {
let editEmailTrigger: Driver<Void>
}
struct Output {
}
let routeManager: BehaviorRelay<RouteMatchResult?>
let currentEmail: BehaviorRelay<String?>
init(routeManager: RouteMatchResult?, provider: Api, currentEmail: String?) {
self.routeManager = BehaviorRelay(value: routeManager)
self.currentEmail = BehaviorRelay(value: currentEmail)
super.init(provider: provider)
}
func transform(input: Input) -> Output {
// THE CALL I WANT TO MAKE
routeManager.errorOnNil().asObservable()
.flatMapLatest { (code) -> Observable<RxSwift.Event<User>> in
log("=========++++++++++++==========")
// guard let code = code else {return}
let params = code.values
let challengeId = Int(params["xxx"] as? String ?? "0")
let login = LoginResponseModel(identifier: params["xxxx"] as? String, key: params["xxxxxx"] as? String, oth: params["xxxxx"] as? String, id: 0, challengeId: challengeId)
return self.provider.postVerifyApp(challengeId: login.challengeId!, oth: login.oth!, identifier: login.identifier!)
.trackActivity(self.loading)
.trackError(self.error)
.materialize()
}.subscribe(onNext: { [weak self] (event) in
switch event {
case .next(let token):
log(token)
AuthManager.setToken(token: token)
// self?.tokenSaved.onNext(())
case .error(let error):
log(error.localizedDescription)
default: break
}
}).disposed(by: rx.disposeBag)
return Output()
}
}
My Viewcontroller
override func bindViewModel() {
super.bindViewModel()
guard let viewModel = viewModel as? EmailVerificationViewModel else { return }
let input = EmailVerificationViewModel.Input(editEmailTrigger: editEmailBtn.rx.tap.asDriver())
let output = viewModel.transform(input: input)
viewModel.loading.asObservable().bind(to: isLoading).disposed(by: rx.disposeBag)
viewModel.parsedError.asObservable().bind(to: error).disposed(by: rx.disposeBag)
isLoading.asDriver().drive(onNext: { [weak self] (isLoading) in
isLoading ? self?.startAnimating() : self?.stopAnimating()
}).disposed(by: rx.disposeBag)
error.subscribe(onNext: { [weak self] (error) in
var title = ""
var description = ""
let image = R.image.icon_toast_warning()
switch error {
case .serverError(let response):
title = response.message ?? ""
}
self?.view.makeToast(description, title: title, image: image)
}).disposed(by: rx.disposeBag)
}
so how can I make the call on the commented like THE CALL I WANT TO MAKE once the application catches the universal link and loads up. Basically making an API call on viewDidLoad
The code in your sample was way more than is needed to answer the question. Here is how you make a network call on viewDidLoad:
class ViewController: UIViewController {
var viewModel: ViewModel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let input = ViewModel.Input()
let output = viewModel.transform(input: input)
output.viewData
.bind(onNext: { viewData in
// setup the view with viewData
})
.disposed(by: disposeBag)
}
}
class ViewModel {
struct Input { }
struct Output {
let viewData: Observable<ViewData>
}
init(api: API) {
self.api = api
}
func transform(input: Input) -> Output {
let viewData = api.networkCall()
.map { ViewData(from: $0) }
return Output(viewData: viewData)
}
let api: API
}

Model for MVVM in iOS

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.

enable button with RxSwift logic

I'm new to RxSwift and want to achieve the following. I have a email and password TextField. When you've entered a text in both textfields a button should be enabled.
In my ViewController I do the following:
txtEmail.rx.text.asObservable()
.bindTo(viewModel.email)
.addDisposableTo(disposeBag)
txtPassword.rx.text.asObservable()
.bindTo(viewModel.password)
.addDisposableTo(disposeBag)
viewModel.buttonEnabled
.bindTo(btnLogin.rx.isEnabled)
.addDisposableTo(disposeBag)
And here is my ViewModel:
import Foundation
import RxSwift
import RxCocoa
class LoginViewModel {
let email = Variable<String?>("")
let password = Variable<String?>("")
var buttonEnabled: Observable<Bool>
init() {
var processedEmail: Observable<String>!
var processedPassword: Observable<String>!
processedEmail = email.asObservable().map(String.toLower as! (String?) -> String).map(String.trimWhiteSpace as! (String?) -> String)
processedPassword = password.asObservable().map(String.toLower as! (String?) -> String).map(String.trimWhiteSpace as! (String?) -> String)
let emailValid = processedEmail.asObservable().map(String.isNotEmpty)
let passwordValid = processedPassword.asObservable().map(String.isNotEmpty)
buttonEnabled = Observable.combineLatest(emailValid, passwordValid) {
return $0 && $1
}
}
func didTapLoginButton() {
print("hello \(email.value)")
}
}
For some reason the init method of my viewmodel never gets finished.
Can someone help me?
It seems like the definition of processedEmail and processedPassword are generating the crash preventing the view model init to complete.
More specifically, the String.toLower and String.trimWhiteSpace method probably have type (String) -> String but here they are force cast to (String?) -> String.
processedEmail = email.asObservable()
.map { $0 ?? "" }
.map(String.toLower)
.map(String.trimWhiteSpace)
processedPassword = password.asObservable()
.map { $0 ?? "" }
.map(String.toLower)
.map(String.trimWhiteSpace)
I've only added .map { $0 ?? "" }. Because the original observable has type Observable<String?> the map call will transform it to Observable<String> which will then be processable by String.toLower and String.trimWhiteSpace
I tried this example of init and it seems working:
init() {
let processedEmail = email.asObservable()
.map { $0 ?? "" }
.map { $0.lowercased() }
.map { !$0.isEmpty }
let processedPassword = password.asObservable()
.map { $0 ?? "" }
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
.map { !$0.isEmpty }
buttonEnabled = Observable.combineLatest(processedEmail, processedPassword) {
return $0 && $1
}
}

Resources