Observable not responding to .drive method - ios

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.

Related

Communication between ViewModel + View in SwiftUI

I am new to Combine and am struggling with a few concepts around communication. I come from a web background and before that was UIKit, so a different landscape to SwiftUI.
I am very keen to use MVVM to keep business logic away from the View layer. This means that any view that is not a reusable component, has a ViewModel to handle API requests, logic, error handling etc.
The problem I have run into is what is the best way to pass events to the View when something happens in the ViewModel. I understand that a view should be a reflection of state, but for things that are event driven, it requires a bunch of variables that I think is messy and so keen to get other approaches.
The example below is a ForgotPasswordView. It is presented as a sheet, and when a successful reset occurs, it should close + show a success toast. In the case that it fails, there should be an error toast shown (for context the global toast coordinator is managed via an #Environment variable injected at the root of the app).
Below is a limited example
View
struct ForgotPasswordView: View {
/// Environment variable to dismiss the modal
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
/// The forgot password view model
#StateObject private var viewModel: ForgotPasswordViewModel = ForgotPasswordViewModel()
var body: some View {
NavigationView {
GeometryReader { geo in
ScrollView {
// Field contents + button that calls method
// in ViewModel to execute the network method. See `sink` method for response
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(leading: self.closeButton /* Button that fires `closeSheet` */)
}
}
/// Close the presented sheet
private func closeSheet() -> Void {
self.presentationMode.wrappedValue.dismiss()
}
}
ViewModel
class ForgotPasswordViewModel: ObservableObject {
/// The value of the username / email address field
#Published var username: String = ""
/// Reference to the reset password api
private var passwordApi = Api<Response<Success>>()
/// Reference to the password api for cancelling
private var apiCancellable: AnyCancellable?
init() {
self.apiCancellable = self.passwordApi.$status
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let result = result else { return }
switch result {
case .inProgress:
// Handle in progress
case let .success(response):
// Handle success
case let .failed(error):
// Handle failure
}
}
}
}
The above ViewModel has all the logic, and the View simply reflects the data and calls methods. Everything good so far.
Now, in order to handle the success and failed states of the server response, and get that information to the UI, is where I run into issues. I can think of a few ways, but I either dislike, or seem not possible.
With variables
Create individual #Published variables for each state e.g.
#Published var networkError: String? = nil
then set them is the different states
case let .failed(error):
// Handle failure
self.networkError = error.description
}
In the View I can then subscribe to this via onRecieve and handle the response
.onReceive(self.viewModel.$networkError, perform: { error in
if error {
// Call `closeSheet` and display toast
}
})
This works, but this is a single example and would require me to create a #Published variable for every state. Also, these variables would have to be cleaned up too (setting them back to nil.
This could be made more graceful by using an enum with associated values, so that there is only a single listener + variable that needs to be used. The enum doesn't however deal with the fact that the variable has to be cleaned up.
With PassthroughSubject
Building on this, I went looking at PassthroughSubject thinking that if I create a single #Publisher like
#Publisher var events: PassthoughSubject = PassthroughSubject<Event, Never>
And publish events like this:
.sink { [weak self] result in
guard let result = result else { return }
switch result {
case let .success(response):
// Do any processing of success response / call any methods
self.events.send(.passwordReset)
case let .failed(error):
// Do any processing of error response / call any methods
self.events.send(.apiError(error)
}
}
Then I could listen to it like this
.onReceive(self.viewModel.$events, perform: { event in
switch event {
case .passwordReset:
// close sheet and display success toast
case let .apiError(error):
// show error toast
})
This is better that the variable, as the events are sent with .send and so the events variable doesn't need cleaning up.
Unfortunately though, it doesn't seem that you can use onRecieve with a PassthroughSubject. If i made it a Published variable but with the same concept, then I would run into the having to clean it up again issue that the first solution had.
With everything in the view
The last scenario, that I have been trying to avoid is to handle everything in the View
struct ForgotPasswordView: View {
/// Environment variable to dismiss the modal
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
/// Reference to the reset password api
#StateObject private var passwordApi = Api<Response<Success>>()
var body: some View {
NavigationView {
GeometryReader { geo in
ScrollView {
// Field contents + button that all are bound/call
// in the view.
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(leading: self.closeButton /* Button that fires `closeSheet` */)
.onReceive(self.passwordApi.$status, perform: { status in
guard let result = result else { return }
switch result {
case .inProgress:
// Handle in progress
case let .success(response):
// Handle success via closing dialog + showing toast
case let .failed(error):
// Handle failure via showing toast
}
})
}
}
}
The above is a simple example, but if there is more complex processing or data manipulating that needs to happen, I don't want that to be in the View as it is messy. Additionally, in this case the success / failure event perfectly matches the events that are needed to be handled in the UI, not every view would fall into that category though, so even more processing could need to be done.
I have run into this conundrum for nearly every view that has a model, if something happens in the ViewModel that is a basic event, how should it be communicated to the View. I feel like there should be a nicer way to do this, which also leads me to think I am doing it wrong.
That was a large wall of text, but I am keen to ensure that the architecture of the app in maintainable, easily testable, and views focus on displaying data and calling mutations (but not at the expense of there being lots of boilerplate variables in the ViewModel)
Thanks
You can have the result of your reset password request delivered to a #Published property of your view model. SwiftUI will automatically update the associated view when the state changes.
In the following I wrote a password reset form similar to yours, with a view and an underlying view model. The view model has a state with four possible values from a nested State enum:
idle as initial state or after the username has been changed.
loading when the reset request is being performed.
success and failure when the result of the reset request is known.
I simulated the password reset request with a simple delayed publisher which fails when an invalid username is detected (for simplicity only usernames containing # are considered valid). The publisher result is directly assigned to the published state property using .assign(to: &$state), a very convenient way to connect publishers together:
import Combine
import Foundation
final class ForgotPasswordViewModel: ObservableObject {
enum State {
case idle
case loading
case success
case failed(message: String)
}
var username: String = "" {
didSet {
state = .idle
}
}
#Published private(set) var state: State = .idle
// Simulate some network request to reset the user password
private static func resetPassword(for username: String) -> AnyPublisher<State, Never> {
return CurrentValueSubject(username)
.delay(for: .seconds(.random(in: 1...2)), scheduler: DispatchQueue.main)
.map { username in
return username.contains("#") ? State.success : State.failed(message: "The username does not exist")
}
.eraseToAnyPublisher()
}
func resetPassword() {
state = .loading
Self.resetPassword(for: username)
.receive(on: DispatchQueue.main)
.assign(to: &$state)
}
}
The view itself instantiates and stores the view model as #StateObject. The user can enter their name and trigger ask for a password reset. Each time the view model state changes a body update is automatically triggered, which allows the view to adjust appropriately:
import SwiftUI
struct ForgotPasswordView: View {
#StateObject private var model = ForgotPasswordViewModel()
private var statusMessage: String? {
switch model.state {
case .idle:
return nil
case .loading:
return "Submitting"
case .success:
return "The password has been reset"
case let .failed(message: message):
return "Error: \(message)"
}
}
var body: some View {
VStack(spacing: 40) {
Text("Password reset")
.font(.title)
TextField("Username", text: $model.username)
Button(action: resetPassword) {
Text("Reset password")
}
if let statusMessage = statusMessage {
Text(statusMessage)
}
Spacer()
}
.padding()
}
private func resetPassword() {
model.resetPassword()
}
}
The code above can easily be tested in an Xcode project.

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.

SwiftUI custom View's ViewBuilder doesn't re-render/update on subclassed ObservedObject update

This one I've been researching for a few days, scouring the Swift & SwiftUI docs, SO, forums, etc. and can't seem to find an answer.
Here is the problem;
I have a SwiftUI custom View that does some state determination on a custom API request class to a remote resource. The View handles showing loading states and failure states, along with its body contents being passed through via ViewBuilder so that if the state from the API is successful and the resource data is loaded, it will show the contents of the page.
The issue is, the ViewBuilder contents does not re-render when the subclassed ObservedObject updates. The Object updates in reaction to the UI (when buttons are pressed, etc.) but the UI never re-renders/updates to reflect the change within the subclassed ObservedObject, for example the ForEach behind an array within the subclassed ObservedObject does not refresh when the array contents change. If I move it out of the custom View, the ForEach works as intended.
I can confirm the code compiles and runs. Observers and debugPrint()'s throughout show that the ApiObject is updating state correctly and the View reflects the ApiState change absolutely fine. It's just the Content of the ViewBuilder. In which I assume is because the ViewBuilder will only ever be called once.
EDIT: The above paragraph should have been the hint, the ApiState updates correctly, but after putting extensive logging into the application, the UI was not listening to the publishing of the subclassed ObservedObject. The properties were changing and the state was too, but the UI wasn't being reactive to it.
Also, the next sentence turned out to be false, I tested again in a VStack and the component still didn't re-render, meaning I was looking in the wrong place!
If this is the case, how does VStack and other such elements get around this?
Or is it because my ApiObjectView is being re-rendered on the state change, in which causes the child view to 'reset'? Although in this circumstance I'd expect it to then take on the new data and work as expected anyway, its just never re-rendering.
The problematic code is in the CustomDataList.swift and ApiObjectView.swift below. I've left comments to point in the right direction.
Here is the example code;
// ApiState.swift
// Stores the API state for where the request and data parse is currently at.
// This drives the ApiObjectView state UI.
import Foundation
enum ApiState: String
{
case isIdle
case isFetchingData
case hasFailedToFetchData
case isLoadingData
case hasFailedToLoadData
case hasUsableData
}
// ApiObject.swift
// A base class that the Controllers for the app extend from.
// These classes can make data requests to the remote resource API over the
// network to feed their internal data stores.
class ApiObject: ObservableObject
{
#Published var apiState: ApiState = .isIdle
let networkRequest: NetworkRequest = NetworkRequest(baseUrl: "https://api.example.com/api")
public func apiGetJson<T: Codable>(to: String, decodeAs: T.Type, onDecode: #escaping (_ unwrappedJson: T) -> Void) -> Void
{
self.apiState = .isFetchingData
self.networkRequest.send(
to: to,
onComplete: {
self.apiState = .isLoadingData
let json = self.networkRequest.decodeJsonFromResponse(decodeAs: decodeAs)
guard let unwrappedJson = json else {
self.apiState = .hasFailedToLoadData
return
}
onDecode(unwrappedJson)
self.apiState = .hasUsableData
},
onFail: {
self.apiState = .hasFailedToFetchData
}
)
}
}
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
// Subclassed from the ApiObject, inheriting ObservableObject
import Foundation
import Combine
class CustomDataController: ApiObject
{
#Published public var customData: [CustomDataStruct] = []
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
This is the View that has the problem with re-rendering its ForEach on the ObservedObject change to its bound array property.
// CustomDataList.swift
// This is the SwiftUI View that drives the content to the user as a list
// that displays the CustomDataController.customData.
// The ForEach in this View
import SwiftUI
struct CustomDataList: View
{
#ObservedObject var customDataController: CustomDataController = CustomDataController()
var body: some View
{
ApiObjectView(
apiObject: self.customDataController,
onQuit: {}
) {
List
{
Section(header: Text("Custom Data").padding(.top, 40))
{
ForEach(self.customDataController.customData, id: \.self, content: { customData in
// This is the example that doesn't re-render when the
// customDataController updates its data. I have
// verified via printing at watching properties
// that the object is updating and pushing the
// change.
// The ObservableObject updates the array, but this ForEach
// is not run again when the data is changed.
// In the production code, there are buttons in here that
// change the array data held within customDataController.customData.
// When tapped, they update the array and the ForEach, when placed
// in the body directly does reflect the change when
// customDataController.customData updates.
// However, when inside the ApiObjectView, as by this example,
// it does not.
Text(customData.textProperty)
})
}
}
.listStyle(GroupedListStyle())
}
.navigationBarTitle(Text("Learn"))
.onAppear() {
self.customDataController.fetch()
}
}
}
struct CustomDataList_Previews: PreviewProvider
{
static var previews: some View
{
CustomDataList()
}
}
This is the custom View in question that doesn't re-render its Content.
// ApiObjectView
// This is the containing View that is designed to assist in the UI rendering of ApiObjects
// by handling the state automatically and only showing the ViewBuilder contents when
// the state is such that the data is loaded and ready, in a non errornous, ready state.
// The ViewBuilder contents loads fine when the view is rendered or the state changes,
// but the Content is never re-rendered if it changes.
// The state renders fine and is reactive to the object, the apiObjectContent
// however, is not.
import SwiftUI
struct ApiObjectView<Content: View>: View {
#ObservedObject var apiObject: ApiObject
let onQuit: () -> Void
let apiObjectContent: () -> Content
#inlinable public init(apiObject: ApiObject, onQuit: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content) {
self.apiObject = apiObject
self.onQuit = onQuit
self.apiObjectContent = content
}
func determineViewBody() -> AnyView
{
switch (self.apiObject.apiState) {
case .isIdle:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isFetchingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .isLoadingData:
return AnyView(
ActivityIndicator(
isAnimating: .constant(true),
style: .large
)
)
case .hasFailedToFetchData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasFailedToLoadData:
return AnyView(
VStack
{
Text("Failed to load data!")
.padding(.bottom)
QuitButton(action: self.onQuit)
}
)
case .hasUsableData:
return AnyView(
VStack
{
self.apiObjectContent()
}
)
}
}
var body: some View
{
self.determineViewBody()
}
}
struct ApiObjectView_Previews: PreviewProvider {
static var previews: some View {
ApiObjectView(
apiObject: ApiObject(),
onQuit: {
print("I quit.")
}
) {
EmptyView()
}
}
}
Now, all the above code works absolutely fine, if the ApiObjectView isn't used and the contents placed in the View directly.
But, that is horrendous for code reuse and architecture, this way its nice and neat, but doesn't work.
Is there any other way to approach this, e.g. via a ViewModifier or a View extension?
Any help on this would be really appreciated.
As I said, I can't seem to find anyone with this problem or any resource online that can point me in the right direction to solve this problem, or what might be causing it, such as outlined in documentation for ViewBuilder.
EDIT: To throw something interesting in, I've since added a countdown timer to CustomDataList, which updates a label every 1 second. IF the text is updated by that timer object, the view is re-rendered, but ONLY when the text on the label displaying the countdown time is updated.
Figured it out after pulling my hair out for a week, its an undocumented issue with subclassing an ObservableObject, as seen in this SO answer.
This is particularily annoying as Xcode obviously prompts you to remove the class as the parent class provides that inheritence to ObservableObject, so in my mind all was well.
The fix is, within the subclassed class to manually fire the generic state change self.objectWillChange.send() via the willSet listener on the #Published variable in question, or any you require.
In the examples I provided, the base class ApiObject in the question remains the same.
Although, the CustomDataController needs to be modified as follows:
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
#Published public var customData: [CustomDataStruct] = [] {
willSet {
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
As soon as I added that manual publishing, the issue is resolved.
An important note from the linked answer: Do not redeclare objectWillChange on the subclass, as that will again cause the state not to update properly. E.g. declaring the default
let objectWillChange = PassthroughSubject<Void, Never>()
on the subclass will break the state updating again, this needs to remain on the parent class that extends from ObservableObject directly, either my manual or automatic default definition (typed out, or not and left as inherited declaration).
Although you can still define as many custom PassthroughSubject declarations as you require without issue on the subclass, e.g.
// DataController.swift
// This is a genericised example of the production code.
// These controllers build, manage and serve their resource data.
import Foundation
import Combine
class CustomDataController: ApiObject
{
var customDataWillUpdate = PassthroughSubject<[CustomDataStruct], Never>()
#Published public var customData: [CustomDataStruct] = [] {
willSet {
// Custom state change handler.
self.customDataWillUpdate.send(newValue)
// This is the generic state change fire that needs to be added.
self.objectWillChange.send()
}
}
public func fetch() -> Void
{
self.apiGetJson(
to: "custom-data-endpoint ",
decodeAs: [CustomDataStruct].self,
onDecode: { unwrappedJson in
self.customData = unwrappedJson
}
)
}
}
As long as
The self.objectWillChange.send() remains on the #Published properties you need on the subclass
The default PassthroughSubject declaration is not re-declared on the subclass
It will work and propagate the state change correctly.

Why aren't views rerendered when the same class object with different values is passed as data source

I'm exploring SwiftUI and I've run into something I cannot quite figure out.
I have created a container view that can fetch data on appear as described in this post, but instead of completely changing the object referenced in the rendering view, I just load some of its properties.
The loader is an ObservableObject that the container view observes. When the loader indicates it(s value) has changed, the container view reloads its body property and displays the rendering views with the new data. However, when the object that needs to be loaded is a class, not all subviews in the body property reload.
This is some "pseudo"code of my implementation.
protocol ValueLoader: Combine.ObservableObject {
associatedtype Value
var data: Value { get set }
func load()
}
struct ValueLoadingContainerView<ValueConsumer: View,
ValueContainer: ValueLoader>: View {
#ObservedObject var valueLoader: ValueContainer
let containedView: (ValueContainer.Value) -> ValueConsumer
init(_ loader: ValueContainer,
#ViewBuilder contained: #escaping (ValueContainer.Value) -> ValueConsumer) {
self.valueLoader = loader
self.containedView = contained
}
var body: some View {
containedView(valueLoader.data)
.onAppear(perform: load)
}
private func load() {
self.valueLoader.load()
}
}
class Object {
let id: String
var title: String?
func load(from ...) {
self.title = ...
}
}
struct ConcreteLoader: ValueLoader {
#Published var data: Object
func load() {
guard shouldLoad() else { return } // To prevent infinite recursion
...
// self.objectWillChange is synthesised by conforming to
// ObservableObject and having a property with #Published
self.objectWillChange.send()
self.data.load(from: ...)
}
}
struct ObjectRenderingView: View {
let object: Object
var body: some View {
Text(object.title ?? "ObjectRenderingView is waiting...")
}
}
let object = Object(id: "1", title: nil)
ValueLoadingContainer(ConcreteLoader(object),
contained: { obj in
Text(obj.title ?? "Text is waiting...") // 1
Divider()
ObjectRenderingView(object: obj) // 2
})
When the loader has loaded the properties of object it calls the passed #ViewBuilder closure with the object again, but now its properties are loaded.
If I add print statements, I clearly see that the contained #ViewBuilder closure is called twice: once with the unloaded object and once with the loaded object. These are the same object, but the second time, the properties have been loaded.
The Text label (1) is updated correctly, changing from "Text is waiting..." to the actual title, but the ObjectRenderingView (2) does not update its subviews.
The init of ObjectRenderingView is called with the new data, but the body property is never accessed. This indicates to me that SwiftUI thinks the data has not changed and no rerendering is needed.
I think I understand why it doesn't work: the identity of the obj has not changed, so SwiftUI thinks the ObjectRenderingView does not need to be reloaded. As the identity of the value obj.title has changed from nil to the actual title, the Text view is reloaded. What I can't figure out is how to get SwiftUI to reload ObjectRenderingView as well.
Thanks!
It should be a value type. Please take a look at the docs of ObservedObject in Xcode.
If Value is not value semantic, the updating behavior for any views
that make use of the resulting Binding is unspecified.

Usage of MVVM in 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

Resources