Publishing Updates Using Protocols with Combine on iOS
If you've tried using POP (Protocol-Oriented programming) with SwiftUI and MVVM, you may have run into the problem of being able to observe published changes from your protocol. Unfortunately, protocols don't support property wrappers, which makes it hard to figure out how one would use the @Published
property wrapper and still observe updates. Let's take a look at a work around.
Let's say there is a protocol responsible for managing the signed in state of the user:
protocol SessionService {
var isSignedIn: Bool { get set }
}
If the SessionService
is passed to a ViewModel, logic can be written against whether a user is signed in or not.
Ideally, the concrete class would look something like this:
class MyAppSessionService: SessionService {
@Published var isSignedIn: Bool = false
}
And the ViewModel for the View would look something like the following:
class AuthViewViewModel: ObservableObject {
var isSignedIn: Bool {
sessionService.isSignedIn
}
private var sessionService: SessionService = MyAppSessionService()
func signIn() {
sessionService.isSignedIn = true
}
func signOut() {
sessionService.isSignedIn = false
}
}
The sessionService
is initialized with the concrete class MyAppSessionService
but has an explicit type SessionService
.
The View can then access the different members of the ViewModel to show relevant info to the user:
struct AuthView: View {
@ObservedObject var viewModel: AuthViewViewModel = .init()
var body: some View {
VStack(spacing: 40) {
Text(viewModel.isSignedIn ? "Signed In" : "Signed Out")
Button("Sign In", action: viewModel.signIn)
Button("Sign Out", action: viewModel.signOut)
}
}
}
At first glance, it seems like pressing the buttons should update the Text that is being rendered to the screen. However, whenever the user taps either button, nothing seems to happen. This is because the ViewModel doesn't know that the value inside of the SessionService
has changed.
To fix this problem, start by updating the SessionService
protocol to the following:
protocol SessionService {
var isSignedIn: Bool { get set }
var isSignedInPublisher: Published<Bool>.Publisher { get }
}
The added isSignedInPublisher
is a publisher that can notify the ViewModel that changes have been made.
The concrete needs to be updated too:
class MyAppSessionService: SessionService {
@Published var isSignedIn: Bool = false
var isSignedInPublisher: Published<Bool>.Publisher { $isSignedIn }
}
The value of isSignedInPublisher
is simply the published binding of isSignedIn
.
Lastly, the ViewModel needs to simply observe the changes to the isSignedIn
value:
init() {
observeStatus()
}
var token: AnyCancellable?
func observeStatus() {
token = sessionService.isSignedInPublisher.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
When the ViewModel is initialized, it begins observing isSignedInPublisher
and objectWillChange.send()
is called since the ViewModel conforms to ObservableObject
.
The UI now updates successfully! 🎉