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“.
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
- Create the Perfect UserDefaults Wrapper Using Property Wrapper
- Building ViewModels with Combine framework
- Polymorphism with Protocols
- Swift: Accomplishing Dynamic Dispatch on PATs (Protocol with Associated Types)
👋🏻 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.