Usage of MVVM in iOS - ios

I'm an iOS developer and I'm guilty of having Massive View Controllers in my projects so I've been searching for a better way to structure my projects and came across the MVVM (Model-View-ViewModel) architecture. I've been reading a lot of MVVM with iOS and I have a couple of questions. I'll explain my issues with an example.
I have a view controller called LoginViewController.
LoginViewController.swift
import UIKit
class LoginViewController: UIViewController {
#IBOutlet private var usernameTextField: UITextField!
#IBOutlet private var passwordTextField: UITextField!
private let loginViewModel = LoginViewModel()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func loginButtonPressed(sender: UIButton) {
loginViewModel.login()
}
}
It doesn't have a Model class. But I did create a view model called LoginViewModel to put the validation logic and network calls.
LoginViewModel.swift
import Foundation
class LoginViewModel {
var username: String?
var password: String?
init(username: String? = nil, password: String? = nil) {
self.username = username
self.password = password
}
func validate() {
if username == nil || password == nil {
// Show the user an alert with the error
}
}
func login() {
// Call the login() method in ApiHandler
let api = ApiHandler()
api.login(username!, password: password!, success: { (data) -> Void in
// Go to the next view controller
}) { (error) -> Void in
// Show the user an alert with the error
}
}
}
My first question is simply is my MVVM implementation correct? I have this doubt because for example I put the login button's tap event (loginButtonPressed) in the controller. I didn't create a separate view for the login screen because it has only a couple of textfields and a button. Is it acceptable for the controller to have event methods tied to UI elements?
My next question is also about the login button. When the user taps the button, the username and password values should gte passed into the LoginViewModel for validation and if successful, then to the API call. My question how to pass the values to the view model. Should I add two parameters to the login() method and pass them when I call it from the view controller? Or should I declare properties for them in the view model and set their values from the view controller? Which one is acceptable in MVVM?
Take the validate() method in the view model. The user should be notified if either of them are empty. That means after the checking, the result should be returned to the view controller to take necessary actions (show an alert). Same thing with the login() method. Alert the user if the request fails or go to the next view controller if it succeeds. How do I notify the controller of these events from the view model? Is it possible to use binding mechanisms like KVO in cases like this?
What are the other binding mechanisms when using MVVM for iOS? KVO is one. But I read it's not quite suitable for larger projects because it require a lot of boilerplate code (registering/unregistering observers etc). What are other options? I know ReactiveCocoa is a framework used for this but I'm looking to see if there are any other native ones.
All the materials I came across on MVVM on the Internet provided little to no information on these parts I'm looking to clarify, so I'd really appreciate your responses.

waddup dude!
1a- You're headed in the right direction. You put loginButtonPressed in the view controller and that is exactly where it should be. Event handlers for controls should always go into the view controller - so that is correct.
1b - in your view model you have comments stating, "show the user an alert with the error". You don't want to display that error from within the validate function. Instead create an enum that has an associated value (where the value is the error message you want to display to the user). Change your validate method so that it returns that enum. Then within your view controller you can evaluate that return value and from there you will display the alert dialog. Remember you only want to use UIKit related classes only within the view controller - never from the view model. View model should only contain business logic.
enum StatusCodes : Equatable
{
case PassedValidation
case FailedValidation(String)
func getFailedMessage() -> String
{
switch self
{
case StatusCodes.FailedValidation(let msg):
return msg
case StatusCodes.OperationFailed(let msg):
return msg
default:
return ""
}
}
}
func ==(lhs : StatusCodes, rhs : StatusCodes) -> Bool
{
switch (lhs, rhs)
{
case (.PassedValidation, .PassedValidation):
return true
case (.FailedValidation, .FailedValidation):
return true
default:
return false
}
}
func !=(lhs : StatusCodes, rhs : StatusCodes) -> Bool
{
return !(lhs == rhs)
}
func validate(username : String, password : String) -> StatusCodes
{
if username.isEmpty || password.isEmpty
{
return StatusCodes.FailedValidation("Username and password are required")
}
return StatusCodes.PassedValidation
}
2 - this is a matter of preference and ultimately determined by the requirements for your app. In my app I pass these values in via the login() method i.e. login(username, password).
3 - Create a protocol named LoginEventsDelegate and then have a method within it as such:
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String)
However this method should only be used to notify the view controller of the actual results of attempting to login on the remote server. It should have nothing to do with the validation portion. Your validation routine will be handled as discussed above in #1. Have your view controller implement the LoginEventsDelegate. And create a public property on your view model i.e.
class LoginViewModel {
var delegate : LoginEventsDelegate?
}
Then in the completion block for your api call you can notify the view controller via the delegate i.e.
func login() {
// Call the login() method in ApiHandler
let api = ApiHandler()
let successBlock =
{
[weak self](data) -> Void in
if let this = self {
this.delegate?.loginViewModel_LoginCallFinished(true, "")
}
}
let errorBlock =
{
[weak self] (error) -> Void in
if let this = self {
var errMsg = (error != nil) ? error.description : ""
this.delegate?.loginViewModel_LoginCallFinished(error == nil, errMsg)
}
}
api.login(username!, password: password!, success: successBlock, error: errorBlock)
}
and your view controller would look like this:
class loginViewController : LoginEventsDelegate {
func viewDidLoad() {
viewModel.delegate = self
}
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String) {
if successful {
//segue to another view controller here
} else {
MsgBox(errMsg)
}
}
}
Some would say you can just pass in a closure to the login method and skip the protocol altogether. There are a few reasons why I think that is a bad idea.
Passing a closure from the UI Layer (UIL) to the Business Logic Layer (BLL) would break Separation of Concerns (SOC). The Login() method resides in BLL so essentially you would be saying "hey BLL execute this UIL logic for me". That's an SOC no no!
BLL should only communicate with the UIL via delegate notifications. That way BLL is essentially saying, "Hey UIL, I'm finished executing my logic and here's some data arguments that you can use to manipulate the UI controls as you need to".
So UIL should never ask BLL to execute UI control logic for him. Should only ask BLL to notify him.
4 - I've seen ReactiveCocoa and heard good things about it but have never used it. So can't speak to it from personal experience. I would see how using simple delegate notification (as described in #3) works for you in your scenario. If it meets the need then great, if you're looking for something a bit more complex then maybe look into ReactiveCocoa.
Btw, this also is technically not an MVVM approach since binding and commands are not being used but that's just "ta-may-toe" | "ta-mah-toe" nitpicking IMHO. SOC principles are all the same regardless of which MV* approach you use.

MVVM in iOS means creating an object filled with data that your screen uses, separately from your Model classes. It usually maps all the items in your UI that consume or produce data, like labels, textboxes, datasources or dynamic images. It often does some light validation of input (empty field, is valid email or not, positive number, switch is on or not) with validators. These validators are usually separate classes not inline logic.
Your View layer knows about this VM class and observes changes in it to reflects them and also updates the VM class when the user inputs data. All properties in the VM are tied to items in the UI. So for example a user goes to a user registration screen this screen gets a VM that has none of it's properties filled except the status property that has an Incomplete status. The View knows that only a Complete form can be submitted so it sets the Submit button inactive now.
Then the user starts filling in it's details and makes a mistake in the e-mail address format. The Validator for that field in the VM now sets an error state and the View sets the error state (red border for example) and error message that's in the VM validator in the UI.
Finally, when all the required fields inside the VM get the status Complete the VM is Complete, the View observes that and now sets the Submit button to active so the user can submit it. The Submit button action is wired to the VC and the VC makes sure the VM gets linked to the right model(s) and saved. Sometimes Models are used directly as a VM, that might be useful when you have simpler CRUD-like screens.
I've worked with this pattern in WPF and it works really great. It sounds like a lot of trouble setting up all those observers in Views and putting a lot of fields in Model classes as well as ViewModel classes but a good MVVM framework will help you with that. You just need to link UI elements to VM elements of the right type, assign the right Validators and a lot of this plumbing gets done for you without the need for adding all that boilerplate code yourself.
Some advantages of this pattern:
It only exposes the data you need
Better testability
Less
boilerplate code to connect UI elements to data
Disadvantages:
Now you need to maintain both the M and the VM
You still can't completely get around using the VC iOS.

MVVM architecture in iOS can be easily implemented without using third party dependencies. For data binding, 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

Related

Is there a way to have a "viewWillAppear" fuction that inizalises a class in swiftUI and prevents refrences until inialized?

I am looking for a way to initialize a class before my View loads in SwiftUI. This class takes two args which are the self of the initializer and an Int value. Then when initialized I want to call a get method inside of the Class and get an array of facebook videos (loaded in WebKit) to display. Where the problem I have comes in is if I fetch the get method too soon it crashes, and then it will not let me create a var/let with the type of self of my View calling it. To me, it seems like I need some sort of completion handler but I do not know to do that for this case.
Here is a portion of my fetchDataBook class that is being initialized to get information from a Facebook page I have permission to, and what needs to be initialized before the View loads the values of getWebView().
private let upperBoundRange: Int
private var requests = [URLRequest]()
private var WebViews = [WebView2]()
init(upperBoundRange: Int) {
self.upperBoundRange = upperBoundRange
getToken()
}
//
//
// Create some sort of completsion handler to tell when it is safe to fetch webviews
//
//
func getwebView(index: Int) -> WebView2 {
print("Get views", WebViews[index])
return WebViews[index]
}
private func setWebViews() {
for i in 0...upperBoundRange {
WebViews.append(WebView2(parent: parent, request: requests[i]))
}
}
private func setLinks(data: NSArray) {
// Code Not Shown to get links for setWebViews()
}
// Fetches FaceBook URL for Livestream
private func getJSON(token: String) {
// code not shown
}
private func getToken() {
// code now shown
}
Here is the basic struture of my SwiftUI View:
struct LiveMaster : View {
let fb = fetchDataBook(upperBoundRange 4)
ScrollView{
VStack {
self.fb.getwebView(index: 0)
// Each displays WebSite from fetch in fetchDataBook class but cannot display them until the var in fetchDataBook has the values
}
VStack {
}
VStack {
}
VStack {
}
}
Thank you and if you need more information I would be glad to edit this to clarify. This has been a problem for the past couple of days that I cannot find a solution to, nor an easy way to ask. Thank you.
The answer to my problem is as Paulw11 said Your model should #Publish an optional. This worked perfectly, as soon as I added a Bool value that would change once the values have been set since it was not properly updated with my Array of WebView2's. Then I just use #ObservedObject on my instances of that class and it worked perfectly with the if checks of that Bool value. Thank you #Paulw11 for the suggestion.

iOS - Sharing viewModel between views

I have a view whose ViewModel configures the view. The user can update the ViewModel and this object is later passed onto another view which will reflect the state of the preview view. Here is an example.
struct ViewSettings {
var btn1Selected: Bool
var btn2Selected: Bool
var btn3Selected: Bool
init() {
btn1Selected = true
btn2Selected = true
btn3Selected = true
}
}
class ViewOne: UIView {
var settings: ViewSettings
init(settings: ViewSettings) {
self.settings = settings
}
func configureView() {
btn1.isSelected = settings.btn1Selected
btn2.isSelected = settings.btn2Selected
btn3.isSelected = settings.btn3Selected
}
#objc func tapBtn1(_ sender: UIButton) {
btn1.isSelected = btn1.isSelected.toggle()
settings.btn1Selected.toggle()
}
#objc func tapBtn2(_ sender: UIButton) {
btn2.isSelected = btn2.isSelected.toggle()
settings.btn2Selected.toggle()
}
#objc func tapBtn3(_ sender: UIButton) {
btn3.isSelected = btn3.isSelected.toggle()
settings.btn3Selected.toggle()
}
}
This setting is later used inside another view. If btn1 is selected in ViewOne and when ViewTwo uses that setting, btn1 in ViewTwo is selected too.
Question:
I'm doing a direct mutation on the settings to achieve this. Is there a better design pattern that would let me arrive at the same solution?
your viewModel is struct that means its a value type, even when you think you are passing the same viewModel to other views, in reality you send a different copy of viewModel not the same instance.
So when you mutate value in view1 and pass the copy of it to view2, if view2 changes the value again, your viewModel in view1 will not be updated, its not passed by reference its passed by value.
so If your question was that I am mutating value directly will it cause side effects no because they are different copies, but if you want the changes to be reflected in all the views that holds this viewModel then it won't.
Finally answering your question
Question: I'm doing a direct mutation on the settings to achieve this.
Is there a better design pattern that would let me arrive at the same
solution?
At very first, sharing viewModel across view itself is arguable. Should this be done or not, as such the whole idea is opinion based. Some might say its fine some might say its not!
In general, I have seen people sharing viewModel across views but I personally refrain from doing so, but that doesn't mean that either of the approach is the only right way. Its best left to developers judgement.
Few clarifications:
struct ViewSettings {
var btn1Selected: Bool
var btn2Selected: Bool
var btn3Selected: Bool
init() {
btn1Selected = true
btn2Selected = true
btn3Selected = true
}
}
This looks more like DataModel and less of ViewModel isn't it? Its just a data container, no business logic, no data presentation/modification mechanisms nothing in it, its a plain data model. You are sharing DataModel across view and you wanna mutate and pass it on to next view I think its fine to go ahead with it.

Observable not responding to .drive method

In my app I have a view that is dependent upon a value in persistent storage. The first time in a session (installing or opening after killing the app) the value is saved to persistent storage and rightfully displayed. However, if I sign out and sign back in (same session) the view does not include my value from persistent storage.
I have reason to believe that there's an issue with my subscription to the 'User' observable in the viewmodel but i'm unsure.
I inserted print statements and found that the closure to the .map function never even triggers in this case. Any thoughts?
MyViewController.swift
let myView: MyView
private let viewModel: MyViewModel
override init() {
...
viewModel = MyViewModel(...)
}
override func viewDidLoad(){
...
viewModel.potentialEarnings.drive(myView.rx.potentialEarnings).disposed(by: disposeBag)
}
MyViewModel.swift
let potentialEarnings: Driver<String>
init() {
...
potentialEarnings = User.currentUser(in: getBackgroundContext).map({
user -> String? in
let earnings = user.potentialEarnings
return earnings
}).unwrap().asDriver(onErrorJustReturn: "")
}
MyView.swift
extension Reactive where Base : MyView {
var potentialEarnings: Binder<String> {
return Binder(self.base) { myView, potentialEarnings in
myView.topLabel.text = "Some Text with: \(potentialEarnings)"
}
}
}
It appears your view controller only subscribes upon viewDidLoad, but if the subscription is disposed while logging out/in, you are not going to get any more elements. Since viewDidLoad only triggers once (assuming you do not recreate the view controller), you will not re-subscribe.
Try adding .debug("potential earnings driver"), e.g.:
viewModel.potentialEarnings
.debug("potential earnings driver")
.drive(myView.rx.potentialEarnings)
.disposed(by: disposeBag)
and check the console for if/when the subscription is disposed.

How to communicate between Service and Viewcontroller?

I'm pretty new to iOS and swift development. A lot of swift + firebase tutorials out there, the firebase stuff (such as authentication, fetching and saving data) is done in the ViewController. As far as I got on learning swift, this leads directly to the problem of "Massive View Controllers". In some tutorials they use classes like "DataService.swift" and access them as a singleton:
class DataService {
static let dataService: DataService = DataService()
func createUser(FIRUser: user) {}
...
}
BUT these classes have no communication with the view controller when they're done with e.g. creating the user. Let me be more specific. I guess it should like this:
User taps login button.
Then, ViewController calls dataService.createUser(user) which handles the login stuff and saves the user data to firebase.
If it's finished it should communicate to the view controller, that it's finished.
ViewController checks the result of the createUser() and navigates the user to another view.
How can i do this? At the moment i'm using the delegation pattern. Is this a good way to handle this stuff?
You can do it this way:
// User.swift
struct User {
// Declare necessary properties.
// Such as firstName, lastName, email, etc.
}
// AuthenticationService.swift
protocol AuthenticationService {
func createUser(user: User, completion: (Error?, User?) -> Void)
}
// AuthServiceProvider.swift
class AuthServiceProvider: AuthenticationService {
func createUser(user: User, completion: (Error?, User?) -> Void) {
// Do the necessary work here.
// Convert user to an instance of 'FIRUser' if necessary.
// Use the completion block when you are done.
}
}
// RegisterViewController.swift
class RegisterViewController: UIViewController {
var service = AuthServiceProvider()
#IBAction func didTapRegisterButton(sender: UIButton) {
var user = User()
// Fill the necessary properties to be included.
// Then call the 'createUser' function.
service.createUser(user: user) { (error, user) -> Void in
// This is where you are redirected upon completion.
// Handle always the error if there is.
// If there is none, navigate to your next scene/view.
}
}
}
By the way, as much as possible, just avoid implementing a Singleton Class. This kind of implementation might give you a hard time on doing some unit tests.
Regarding with your concern on "Massive View Controller", you can consider on modularizing your project. But right now, I would suggest that it is better to experience this "MVC" problem than avoiding it.

How to subscribe to delegate events globally?

I have a custom delegate that triggers certain events. For context, it's a bluetooth device that fires events arbitrarily. I'd like my view controllers to optionally subscribe to these events that get triggered by the device delegate.
It doesn't make sense that each view controller conforms to the custom delegate because that means the device variable would be local and would only fire in that view controller. Other view controllers wouldn't be aware of the change. Another concrete example would be CLLocationManagerDelegate - for example what if I wanted all view controllers to listen to the GPS coordinate changes?
Instead, I was thinking more of a global delegate that all view controllers can subscribe to. So if one view controller triggers a request, the device would call the delegate function for all subscribed view controllers that are listening.
How can I achieve this architectural design? Are delegates not the right approach? I thought maybe NotificationCenter can help here, but seems too loosely typed, perhaps throwing protocols would help makes things more manageable/elegant? Any help would be greatly appreciated!
You could have an array of subscribers that would get notified.
class CustomNotifier {
private var targets : [AnyObject] = [AnyObject]()
private var actions : [Selector] = [Selector]()
func addGlobalEventTarget(target: AnyObject, action: Selector) {
targets.append(target)
actions.append(action)
}
private func notifyEveryone () {
for index in 0 ..< targets.count {
if targets[index].respondsToSelector(actions[index]) {
targets[index].performSelector(actions[index])
}
}
}
}
Naturally, you'd have to plan further to maintain the lifecycle of targets and actions, and provide a way to unsubscribe etc.
Note: Also ideal would be for the array of targets and actions to be an of weak objects. This SO question, for instance, deals with the subject.
• NotificationCenter is first solution that comes in mind. Yes, it is loosely typed. But you can improve it. For example like this:
extension NSNotificationCenter {
private let myCustomNotification = "MyCustomNotification"
func postMyCustomNotification() {
postNotification(myCustomNotification)
}
func addMyCustomNotificationObserverUsingBlock(block: () -> ()) -> NSObjectProtocol {
return addObserverForName(myCustomNotification, object: nil, queue: nil) { _ in
block()
}
}
}
• Second solution would be to create some shared object, which will store all delegates or blocks/closures and will trigger them when needed. Such object basically will be the same as using NotificationCenter, but gives you more control.

Resources