Combine link UIViewController to #Published - swift

I've set up a nice little view model with an #Published state:
class ViewModel {
#Published private(set) var state = State.loading
private var cancellable: Set<AnyCancellable> = []
enum State {
case data([Users.UserData])
case failure(Error)
case loading
}
}
I've then linked to this in my UIViewController's viewDidLoad function:
private var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
cancellable = viewModel.$state.sink{ [weak self] _ in
DispatchQueue.main.async {
self?.render()
}
}
}
however, when I link directly to the state it doesn't change when the state is changed from the view model (not shown in the code).
private func render() {
switch viewModel.state {
case .loading:
// Show loading spinner
print("loading render")
case .failure(let error):
// Show error view
print("failing render")
case .data(let userData):
// Show user's profile
self.applySnapshot(userData: userData)
print("loaded \(userData) render")
}
}
Now I can pass the state from my viewDidLoad, but how can I link directly to the viewModel state using Combine and UIKit?

Make sure your ViewModel conforms to ObservableObject like so:
class ViewModel: ObservableObject. This way it knows to react to changes, despite being a UIViewController.

Related

How to pass a parent ViewModel #Published property to a child ViewModel #Binding using MVVM

I'm using an approach similar to the one described on mockacoding - Dependency Injection in SwiftUI where my main ViewModel has the responsibility to create child viewModels.
In the code below I am not including the Factory, as it's very similar to the contents of the post above: it creates the ParentViewModel, passes to it dependencies and closures that construct the child view models.
struct Book { ... } // It's a struct, not a class
struct ParentView: View {
#StateObject var viewModel: ParentViewModel
var body: some View {
VStack {
if viewModel.book.bookmarked {
BookmarkedView(viewModel: viewModel.makeBookMarkedViewModel())
} else {
RegularView(viewModel: viewModel.makeBookMarkedViewModel())
}
}
}
}
class ParentViewModel: ObservableObject {
#Published var book: Book
// THIS HERE - This is how I am passing the #Published to #Binding
// Problem is I don't know if this is correct.
//
// Before, I was not using #Binding at all. All where #Published
// and I just pass the reference. But doing that would cause for
// the UI to NEVER update. That's why I changed it to use #Binding
private var boundBook: Binding<Book> {
Binding(get: { self.book }, set: { self.book = $0 })
}
// The Factory object passes down these closures
private let createBookmarkedVM: (_ book: Binding<Book>) -> BookmarkedViewModel
private let createRegularVM: (_ book: Binding<Book>) -> RegularViewModel
init(...) {...}
func makeBookmarkedViewModel() {
createBookmarkedVM(boundBook)
}
}
class BookmarkedView: View {
#StateObject var viewModel: BookmarkedViewModel
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text(book.title) // <---- THIS IS THE PROBLEM. Not being updated
Button("Remove bookmark") {
viewModel.removeBookmark()
}
}
.onReceive(timer) { _ in
print("adding letter") // <-- this gets called
withAnimation {
viewModel.addLetterToBookTitle()
}
}
}
}
class BookmarkedViewModel: ObservableObject {
#Binding var book: Book
// ... some other dependencies passed by the Factory object
init(...) { ... }
public func removeBookmark() {
// I know a class would be better than a struct, bear with me
book = Book(title: book.title, bookmarked: false)
}
/// Adds an "a" to the title
public func addLetterToBookTitle() {
book = Book(title: book.title + "a", bookmarked: book.bookmarked)
print("letter added") // <-- this gets called as well
}
}
From the code above, let's take a look at BookmarkedView. If I click the button and viewModel.removeBookmark() gets called, the struct is re-assigned and ParentView now renders RegularView.
This tells me that I successfully bound #Published book: Book from ParentViewModel to #Binding book: Book from BookmarkedViewModel, through its boundBook computed property. This felt like the most weird thing I had to make.
However, the problem is that even though addLetterToBookTitle() is also re-assigning the book with a new title, and it should update the Text(book.title), it's not happening. The same title is being displayed.
I can guarantee that the book title has change (because of some other components of the app I'm omitting for simplicity), but the title's visual is not being updated.
This is the first time I'm trying out these pattern of having a view model build child view models, so I appreciate I may be missing something fundamental. What am I missing?
EDIT:
I made an MVP example here: https://github.com/christopher-francisco/TestMVVM/tree/main/MVVMTest.xcodeproj
I'm looking for whether:
My take at child viewmodels is fundamentally wrong and I should start from scratch, or
I have misunderstood #Binding and #Published attributes, or
Anything really
Like I said initially #Binding does not work in a class you have to use .sink to see the changes to an ObservableObject.
See below...
class MainViewModel: ObservableObject {
#Published var timer = YourTimer()
let store: Store
let nManager: NotificationManager
let wManager: WatchConnectionManager
private let makeNotYetStartedViewModelClosure: (_ parentVM: MainViewModel) -> NotYetStartedViewModel
private let makeStartedViewModelClosure: (_ parentVM: MainViewModel) -> StartedViewModel
init(
store: Store,
nManager: NotificationManager,
wManager: WatchConnectionManager,
makeNotYetStartedViewModel: #escaping (_ patentVM: MainViewModel) -> NotYetStartedViewModel,
makeStartedViewModel: #escaping (_ patentVM: MainViewModel) -> StartedViewModel
) {
self.store = store
self.nManager = nManager
self.wManager = wManager
self.makeNotYetStartedViewModelClosure = makeNotYetStartedViewModel
self.makeStartedViewModelClosure = makeStartedViewModel
}
}
// MARK: - child View Models
extension MainViewModel {
func makeNotYetStartedViewModel() -> NotYetStartedViewModel {
self.makeNotYetStartedViewModelClosure(self)
}
func makeStartedViewModel() -> StartedViewModel {
self.makeStartedViewModelClosure(self)
}
}
class NotYetStartedViewModel: ObservableObject {
var parentVM: MainViewModel
var timer: YourTimer{
get{
parentVM.timer
}
set{
parentVM.timer = newValue
}
}
var cancellable: AnyCancellable? = nil
init(parentVM: MainViewModel) {
self.parentVM = parentVM
//Subscribe to the parent
cancellable = parentVM.objectWillChange.sink(receiveValue: { [self] _ in
//Trigger reload
objectWillChange.send()
})
}
func start() {
// I'll make this into a class later on
timer = YourTimer(remainingSeconds: timer.remainingSeconds, started: true)
}
}
class StartedViewModel: ObservableObject {
var parentVM: MainViewModel
var timer: YourTimer{
get{
parentVM.timer
}
set{
parentVM.timer = newValue
}
}
var cancellable: AnyCancellable? = nil
init(parentVM: MainViewModel) {
self.parentVM = parentVM
cancellable = parentVM.objectWillChange.sink(receiveValue: { [self] _ in
//trigger reload
objectWillChange.send()
})
}
func tick() {
// I'll make this into a class later on
timer = YourTimer(remainingSeconds: timer.remainingSeconds - 1, started: timer.started)
}
func cancel() {
timer = YourTimer()
}
}
But this is an overcomplicated setup, stick to class or struct. Also, maintain a single source of truth. That is basically the center of how SwiftUI works everything should be getting its value from a single source.

Easier way of dealing with CurrentValueSubject

I have a Complex class which I pass around as an EnvironmentObject through my SwiftUI views. Complex contains several CurrentValueSubjects. I don't want to add the Published attribute to the publishers on class Complex, since Complex is used a lot around the views and that will force the views to reload on every published value.
Instead, I want a mechanism which can subscribe to specific publisher which Complex holds. That way, Views can choose on which publisher the view should re-render itself.
The code below works, but I was wondering if there was an easier solution, it feels like a lot of work just to listen to the updates CurrentValueSubject gives me:
import SwiftUI
import Combine
struct ContentView: View {
let complex = Complex()
var body: some View {
PublisherView(boolPublisher: .init(publisher: complex.boolPublisher))
.environmentObject(complex)
}
}
struct PublisherView: View {
#EnvironmentObject var complex: Complex
#ObservedObject var boolPublisher: BoolPublisher
var body: some View {
Text("\(String(describing: boolPublisher.publisher))")
}
}
class Complex: ObservableObject {
let boolPublisher: CurrentValueSubject<Bool, Never> = .init(true)
// A lot more...
init() {
startToggling()
}
func startToggling() {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [unowned self] in
let newValue = !boolPublisher.value
print("toggling to \(newValue)")
boolPublisher.send(newValue)
startToggling()
}
}
}
class BoolPublisher: ObservableObject {
private var cancellableBag: AnyCancellable? = nil
#Published var publisher: Bool
init(publisher: CurrentValueSubject<Bool, Never>) {
self.publisher = publisher.value
cancellableBag = publisher
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
self?.publisher = value
}
}
}

SwiftUI - changes in nested View Model classes not detected using onChange method

I have a nested View Model class WatchDayProgramViewModel as an ObservableObject. Within WatchDayProgramViewModel, there is a WorkoutModel that is a child class. I want to detect any updates in the currentHeartRate to trigger data transfer to iPhone.
Hence, I tried from ContentView using WatchDayProgramViewModel as an EnvironmentObject and detecting changes in WorkoutModel via onChange() method. But it seems that SwiftUI views does not detect any property changes in WorkoutModel.
I understand that this issue could be due to ObservableObject not detecting changes in child/nested level of classes, and SO answer (SwiftUI change on multilevel children Published object change) suggests using struct instead of class. But changing WorkoutModel to struct result in various #Published properties and functions to show error.
Is there any possible way to detect changes in child View Model from the ContentView itself?
ContentView
struct ContentView: View {
#State var selectedTab = 0
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
WatchControlView().id(0)
NowPlayingView().id(1)
}
.environmentObject(watchDayProgramVM)
.onChange(of: self.watchDayProgramVM.workoutModel.currentHeartRate) { newValue in
print("WatchConnectivity heart rate from contentView \(newValue)")
}
}
}
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
init() {
}
}
WorkoutModel
import Foundation
import HealthKit
class WorkoutModel: NSObject, ObservableObject {
let healthStore = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
#Published var currentHeartRate: Double = 0
#Published var workout: HKWorkout?
//Other functions to start/run workout hidden
func updateForStatistics(_ statistics: HKStatistics?) {
guard let statistics = statistics else {
return
}
DispatchQueue.main.async {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
self.currentHeartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
default:
return
}
}//end of dispatchqueue
}// end of function
}
extension WorkoutModel: HKLiveWorkoutBuilderDelegate {
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return
}
let statistics = workoutBuilder.statistics(for: quantityType)
updateForStatistics(statistics)
}
}
}
Try to change
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
with
#ObservedObject var watchDayProgramVM = WatchDayProgramViewModel()
Figure it out. Just had to create another AnyCancellable variable to call objectWillChange publisher.
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
var cancellable: AnyCancellable?
init() {
cancellable = workoutModel.objectWillChange
.sink { _ in
self.objectWillChange.send()
}
}
}
While I have provided my answer, that worksaround with viewmodels, I would love to see/get advice on other alternatives.

View not reacting to changes of Published property when its chained from another ObservedObject

Below is the SwiftUI view which owns a ViewModel and the logic is if the viewModel.authenticationService.user contains a user object then it will show the HomeView, else case will be asked for Login. So initially the viewModel.authenticationService.user is nil and user logins successful the user object in no more nil.
View
struct WelcomeView: View {
#ObservedObject private var viewModel: WelcomeView.Model
#State private var signInActive: Bool = false
init(viewModel: WelcomeView.Model) {
self.viewModel = viewModel
}
var body: some View {
if viewModel.authenticationService.user != nil {
HomeView()
} else {
LoginView()
}
}
ViewModel
extension WelcomeView {
final class Model: ObservableObject {
#ObservedObject var authenticationService: AuthenticationService
init(authenticationService: AuthenticationService) {
self.authenticationService = authenticationService
}
}
}
AuthenticationService
final class AuthenticationService: ObservableObject {
#Published var user: User?
private var authenticationStateHandle: AuthStateDidChangeListenerHandle?
init() {
addListeners()
}
private func addListeners() {
if let handle = authenticationStateHandle {
Auth.auth().removeStateDidChangeListener(handle)
}
authenticationStateHandle = Auth.auth()
.addStateDidChangeListener { _, user in
self.user = user
}
}
static func signIn(email: String, password: String, completion: #escaping AuthDataResultCallback) {
if Auth.auth().currentUser != nil {
Self.signOut()
}
Auth.auth().signIn(withEmail: email, password: password, completion: completion)
}
}
However, when the user object is updated with some value it does not update the View. I am not sure as I am new to reactive way of programming. There is a chain of View -> ViewModel -> Service and the published user property is in the Service class which gets updated successfully once user login.
Do I need to add a listener in the ViewModel which reacts to Service published property? Or is there any direct way for this scenario to work and get the UI Updated?
In case of scenario where there is a chain from View -> ViewModel -> Services:
#ObservedObject does not work on classes. Only works in case of SwiftUI View(structs). So if you want to observe changes on your view model from a service you need to manually listen/subscribe to it.
self.element.$value.sink(
receiveValue: { [weak self] _ in
self?.objectWillChange.send()
}
)
You can also use the .assign. Find more details here

#Published requires willSet to fire

I have a class:
final class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
let objectWillChange = PassthroughSubject<Void, Never>()
private let locationManager = CLLocationManager()
#Published var status: String? {
willSet { objectWillChange.send() }
}
#Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
// ...other code
}
And then I have a view that observes this class:
struct MapView: UIViewRepresentable {
#ObservedObject var lm = LocationManager()
// ...other view code
}
Everything works fine and the view updates when the published property changes. However, if I remove the willSet { objectWillChange.send() } then the view that observes an instance of LocationManager does not update when the published location changes. Which brings me to my question: I thought that by putting #Published next to a var that any #ObservedObject will invalidate the current view when the published property changes, essentially a default objectWillChange.send() implementation but this doesn't seem to be happening. Instead I have to manually call the update. Why is that?
You're doing too much work. It looks like you're trying to write ObservableObject. You don't need to; it already exists. The whole point is that ObservableObject is already observable, automatically. Here's a non-SwiftUI example:
final class Thing: NSObject, ObservableObject {
#Published var status: String?
}
class ViewController: UIViewController {
var storage = Set<AnyCancellable>()
let thing = Thing()
override func viewDidLoad() {
self.thing.objectWillChange
.sink {_ in print("will change")}.store(in: &self.storage)
self.thing.$status
.sink { print($0) }.store(in: &self.storage)
}
#IBAction func doButton (_ sender:Any) {
self.thing.status = (self.thing.status ?? "") + "x"
}
}
The thing to notice is that, although the observable object contains no code at all, it is emitting a signal every time its status property is set, before the property changes. Then the status property itself emits a signal, namely its new value.
The same thing happens in SwiftUI.
Apple documentation states:
By default an ObservableObject synthesizes an objectWillChange
publisher that emits the changed value before any of its #Published
properties changes.
This means you don't need to declare your own objectWillChange. You can just remove objectWillChange from your code including the following line:
let objectWillChange = PassthroughSubject<Void, Never>()