You are currently viewing How to Define a Protocol With @Published Property Wrapper Type

How to Define a Protocol With @Published Property Wrapper Type

The Combine framework was introduced in WWDC 2019 and it is mainly used alongside SwiftUI. However, this does not limit us to use the Combine framework on our UIKit apps.

In fact, the @Published property wrapper introduced in Combine is the perfect fit for any UIKit apps with MVVM architecture. We can use @Published to elegantly link up the view controller with its view model so that the view controller can be notified automatically by any changes made on the view model.

Everything works nicely until the day where I wanted to define a protocol for my view model in order to achieve polymorphism using protocol-oriented programming. The problem arises because the current Swift version (5.2) does not support property wrapper definition in a protocol.

How should we go about defining @Published property wrapper type in a protocol? Read on to find out more.


The Problem

Let’s say we have a view controller and a view model with a @Published variable called name. Whenever name is set or updated, the name publisher will notify MyViewController to print out a hello message.

class MyViewModel {

    @Published var name: String

    init(name: String) {
        self.name = name
    }
}

class MyViewController: UIViewController {
    
    var viewModel: MyViewModel!
    
    private var cancellables: Set<AnyCancellable> = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Subscribe to the view model's name publisher
        // and get notify when name is set or updated
        viewModel.$name
            .receive(on: RunLoop.main)
            .sink { (name) in
                
                // Print hello message when name is set or updated
                print("Hello \(name)")
                
        }.store(in: &cancellables)
    }
}

To see the above code in action, let’s try to run it using Xcode Playground.

let viewModel = MyViewModel(name: "Swift Senpai 1")
let viewController = MyViewController()
viewController.viewModel = viewModel

PlaygroundPage.current.liveView = viewController

viewModel.name = "Swift Senpai 2"
viewModel.name = "Swift Senpai 3"

// Output:
// Hello Swift Senpai 1
// Hello Swift Senpai 2
// Hello Swift Senpai 3

As you can see from the output, the name publisher is working according to our expectation.

Now, let’s try to improve the reusability of MyViewController by applying polymorphism on MyViewController‘s view model type.

What we can do here is to create a polymorphic interface using protocol so that MyViewController can accept any view models that conform to this protocol.

// Define a polymorphic interface
protocol ViewModelProtocol {

    @Published var name: String { get }
}

// Conform to ViewModelProtocol
class MyViewModel: ViewModelProtocol {

    @Published var name: String

    init(name: String) {
        self.name = name
    }
}

class MyViewController: UIViewController {

    // Change viewModel type to ViewModelProtocol
    var viewModel: ViewModelProtocol!

    private var cancellables: Set<AnyCancellable> = []

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.$name
            .receive(on: RunLoop.main)
            .sink { (name) in
                print("Hello \(name)")
        }.store(in: &cancellables)
    }
}

Unfortunately, when the code above is executed, the compiler will start complaining about “Property ‘name’ declared inside a protocol cannot have a wrapper“.

Compiler error in Xcode when define protocol with property wrapper
Compiler error when define protocol with property wrapper

This is because the current version of Swift (5.2) does not support property wrapper definition in a protocol. 🙁


The Workaround

In order to work around this limitation, let’s take a step back and understand how we are using the @Published name property wrapper in the view controller.

// Subscribe to the view model's name publisher
// and get notify when name is set or updated
viewModel.$name
    .receive(on: RunLoop.main)
    .sink { (name) in
        print("Hello \(name)")
}.store(in: &cancellables)

As seen in the code snippet above, we are assessing the name publisher via the projected value of @Published name property wrapper. In order word, what we actually need in the view controller is the name publisher but not the @Published name property wrapper.

With that in mind, we are now able to work around the limitation by defining a protocol with name publisher and manually expose the name publisher in the view model implementation.

protocol ViewModelProtocol {

    // Define name publisher
    var namePublisher: Published<String>.Publisher { get }
}

class MyViewModel: ViewModelProtocol {

    @Published var name: String
    
    // Manually expose name publisher in view model implementation
    var namePublisher: Published<String>.Publisher { $name }

    init(name: String) {
        self.name = name
    }
}

class MyViewController: UIViewController {

    var viewModel: ViewModelProtocol!

    private var cancellables: Set<AnyCancellable> = []

    override func viewDidLoad() {
        super.viewDidLoad()

        // Subscribe to the view model's name publisher
        // and get notify when name is set or updated
        viewModel.namePublisher
            .receive(on: RunLoop.main)
            .sink { (name) in
                print("Hello \(name)")
        }.store(in: &cancellables)
    }
}

// Execute code
let viewModel = MyViewModel(name: "Swift Senpai 1")
let viewController = MyViewController()
viewController.viewModel = viewModel

PlaygroundPage.current.liveView = viewController

viewModel.name = "Swift Senpai 2"
viewModel.name = "Swift Senpai 3"

// Output:
// Hello Swift Senpai 1
// Hello Swift Senpai 2
// Hello Swift Senpai 3

With that, we have successfully created a polymorphic interface while retaining the behavior of the view model and view controller. 🥳


Taking One Step Further

The suggested workaround is not only limited to exposing the publisher, we can also use the same concept to expose the @Published property wrapper itself as well as the wrapped value.

protocol ViewModelProtocol {

    // Define name (wrapped value)
    var name: String { get }
    
    // Define name Published property wrapper
    var namePublished: Published<String> { get }
    
    // Define name publisher
    var namePublisher: Published<String>.Publisher { get }
}

class MyViewModel: ViewModelProtocol {

    @Published var name: String
    var namePublished: Published<String> { _name }
    var namePublisher: Published<String>.Publisher { $name }

    // ... ...
    // ... ...
    // ... ...
}

Wrapping Up

As you can see from the above example, the limitation that we are facing is more of a property wrapper limitation rather than the @Published type limitation.

Therefore, with the same idea, you can definitely apply the workaround to any kind of property wrapper.  I’ll leave that as an exercise for you!

If you have any questions, feel free to leave it in the comment section below or you can reach out to me on Twitter.

Thanks for reading. 🧑🏻‍💻


Further Readings


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.