Joakim Sjöstedt
Joakim Sjöstedt

Reputation: 948

How do I get get set to work with protocols?

I'm confused when using get set in protocols. Using only get works fine, but the set part doesnt'.

protocol MainViewModelProtocol {
    var localDoor: LocalDoorCoreDataObject { get set }
}

extension MainViewModelProtocol {
    var localDoor: LocalDoorCoreDataObject {
        get { return MainViewModel.instance.localDoor }
        set { localDoor = newValue }
    }
}

final class MainViewModel: MainViewModelProtocol {
    var localDoor: LocalDoorCoreDataObject = LocalDoorCoreDataObject()
...

Then when I use it in the viewController

self.mainViewModel.localDoor = $0

But this gives me the error

Cannot assign to property: 'mainViewModel' is a get-only property

How do I set it up properly?

EDIT

Initiation of the viewModel is done with factory based dependency injection

protocol MainViewModelInjected {
    var mainViewModel: MainViewModelProtocol { get }
}

extension MainViewModelInjected {
    var mainViewModel: MainViewModelProtocol { return MainViewModel.instance }
}

Upvotes: 1

Views: 271

Answers (2)

Pramod Kumar
Pramod Kumar

Reputation: 2662

It is totally depends on how you create object for mainViewModel. Let's create some cases with your code:

import UIKit
typealias LocalDoorCoreDataObject = String

protocol MainViewModelProtocol {
    var localDoor: LocalDoorCoreDataObject { get set }
}

extension MainViewModelProtocol {
    var localDoor: LocalDoorCoreDataObject {
        get { return MainViewModel.instance.localDoor }
        set { localDoor = newValue }
    }
}

final class MainViewModel: MainViewModelProtocol {
    static let instance = MainViewModel()
    var localDoor: LocalDoorCoreDataObject = LocalDoorCoreDataObject()
}

protocol MainViewModelInjected {
    var mainViewModel: MainViewModelProtocol { get }
}

extension MainViewModelInjected {
    var mainViewModel: MainViewModelProtocol { return MainViewModel.instance }
}

Case 1

Here we are creating an object and assigning object through getter as a closure. So, here mainViewModel has only getter not setter i.e. it'a get-only property.

var mainViewModel: MainViewModelProtocol { MainViewModel.instance }
mainViewModel.localDoor = "assign some thing" // Error: Cannot assign to property: 'mainViewModel' is a get-only property

Case 2

Here we are directly assigning object to mainViewModelOther. So, this will be a normal property and you can make changes in properties of model.

var mainViewModelOther: MainViewModelProtocol = MainViewModel.instance
mainViewModelOther.localDoor = "assign some thing"

Case 3

You can also create a class that will hold your model object, and created another object of your class. You can make changes in properties of model.

class MyClass {
    var mainViewModel: MainViewModelProtocol = MainViewModel.instance
}

let myClassObj = MyClass()
myClassObj.mainViewModel.localDoor = "assign some thing"

Upvotes: 1

Alex Hoppen
Alex Hoppen

Reputation: 358

TL;DR

Mark your MainViewModelProtocol as being class-only (i.e. protocol MainViewModelProtocol: class { ... }) to solve the issue.

The long answer

To understand why marking your MainViewModelProtocol as class-only fixes the problem, we need to take couple steps back and look at how structs and classes are stored internally.

Case 1: MainViewModelProtocol is a reference-type (i.e. class)

First, let's consider the case where MainViewModel is a class: Classes are reference-types, which means that after you retrieve the your view model through the mainViewModel property, you have a pointer to the same view model that is stored inside your view controller. Modifying the referenced type will also modify the view model of the view itself (since they both point to the same object). As an example

/* ... */
class MainViewModel: MainViewModelProtocol { /* ... */ }

var viewModel = myViewController.mainViewModel
viewModel.localDoor = /* something */ 

modifies the view model that's shared between the local variable viewModel and the view controller. This is exactly what you want.

Case 2: MainViewModelProtocol is a value type (i.e. struct)

Now let's consider if the MainViewModel was a struct: structs are value-types, so retrieving the view model through the mainViewModel computed property essentially clones the view model. Now you might modify the retrieved view model as much as you like locally, but there is no way assign it back to your view controller

/* ... */
struct MainViewModel: MainViewModelProtocol { /* ... */ }

var viewModel = myViewController.mainViewModel
viewModel.localDoor = /* something */ 

just modifies the local copy of the view model stored in the viewModel variable. There is no way to assign the local variable back to myViewController.

Conclusion

I hope this illustrates why your pattern only works with reference-types and not value types.

Now the Swift compiler needs to be conservative and consider both cases since it doesn't know if all types conforming to MainViewModelProtocol will be classes or structs (consider public protocols vended as a library to which library-users can conform). If you add the class-constraint to the protocol, you tell the compiler that using the pattern from Case 1 is totally fine – just grab a shared instance and modify it – and that there is no need for a setter to modify the view model.

Upvotes: 0

Related Questions