How can I Observe a var in a Class? - swift

Here is my code for a simple class, My goal is that observeValueOfModel() function automatically put changes of valueOfModel under control and print the correct message out!
I can manually use this func for getting the Answer, but the goal is this class be able understand and react to value change of valueOfModel. Thanks for help
class Model: ObservableObject {
var valueOfModel: Bool = Bool()
private func observeValueOfModel() {
if valueOfModel {
print("valueOfModel is True!")
}
else {
print("valueOfModel is False!")
}
}
}

The didSet fits in this case
class Model: ObservableObject {
var valueOfModel: Bool = Bool() {
didSet {
observeValueOfModel()
}
}
// ... other code

Combine will help you. Define your var as #Published to be able to subscribe to it
#Published var valueOfModel: Bool = true
You can subscribe to changes in the init or viewDidLoad for example. We store the subscription in the cancelable. Put it in the VC or as class property to keep the subscription alive.
let cancelable: AnyCancellable?
cancelable = valueOfModel.sink { [weak self] value
// this will get called as soon as valueOfModel gets updated
// do smth with value here
}

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.

run when a view redraws in SwiftUI

I have a view in SwiftUI, and I would like it to both redraw and to run a closure whenever a variable in my model changes. I am using this closure to update a state var I am storing in the view, which should be the previous value of the variable in my model before it changes
The following code simulates my situation:
let viewModel = ViewModel()
struct someView: View {
#observedObject var viewModel: ViewModel = viewModel
#State var previousSomeValue: CGFloat = 0
var body: some View {
Text("\(viewModel.model.someValue)")
}
}
class ViewModel: ObservableObject {
#Published var model = Model()
}
struct model {
var someValue: CGFloat = 0
}
With this setup, if someValue ever changes, someView redraws, however, I am unable to fire a closure.
//Solutions I have tried:
The main one was to attach onChangeOf(_ (T)->Void) to my view. With .onChangeOf( viewModel.model.someValue ) { _ in //do something } I was able to fire a closure whenever it changed however, by the time it ran, viewModel.model.someValue had already updated to the newValue, and I wasnt able to capture the old one. I read in the documentation that this is by design and that you must capture the thing you want to store the old value of, but I (to my knowledge) am only able to capture self, viewModel, but not viewModel.model.someValue.
.onChangeOf( viewModel.model.someValue ) { [self] newValue in //do something } //works but doesnt capture the var
.onChangeOf( viewModel.model.someValue ) { [viewModel] newValue in //do something } //works but doesnt capture the var
.onChangeOf( viewModel.model.someValue ) { [viewModel.model.someValue] newValue in //do something } //does not compile ( Expected 'weak', 'unowned', or no specifier in capture list )
I have also tried creating a binding in the view such as Binding { gameView.model.someValue } set: { _ in } and having the onChange observer this instead, but even when I capture self, when the closure is called, the old and new values are identicial.
This seems like a common thing to do (detecting external changes and firing a closure), how should I go about it?
If I correctly understood your needs then you should do this not in view but in view model, like
class ViewModel: ObservableObject {
var onModelChanged: (_ old: Model, _ new: Model) -> Void
#Published var model = Model() {
didSet {
onModelChanged(oldValue, model)
}
}
init(onModelChanged: #escaping (_ old: Model, _ new: Model) -> Void = {_, _ in}) {
self.onModelChanged = onModelChanged
}
}
so instantiating ViewModel you can provide a callback to observe values changed in model and have old and new values, like
#StateObject var viewModel = ViewModel() {
print("Old value: \($0.someValue)")
print("New value: \($1.someValue)")
}

In SwiftUI / Combine, how can two ObservableObjects coordinate?

My app has two ObservableObjects:
#main
struct SomeApp: App {
#StateObject private var foo = Foo()
#StateObject private var bar = Bar()
var body: some Scene {
WindowGroup {
AppView()
.environmentObject(foo)
.environmentObject(bar)
}
}
}
class Foo: ObservableObject {
#Published var operationResult: Bool?
private let operationQueue = OperationQueue()
func start() {
operationQueue.addOperation(SomeOperation())
// SomeOperation will set operationResult
}
}
class Bar: ObservableObject {
#Published var resultsAvailable = false
func updateResults(operationResult: Bool) {
if operationResult && someOtherCondition {
resultsAvailable = true
}
}
}
At some point, Foo is doing some work on a background thread. When it completes, state in Bar needs to be updated, perhaps by calling updateResults(). In a traditional UIKit app, I might coordinate this by setting a callback handler or posting an NSNotification.
Instead of passing operationResult around via a notification or handler, is there a way for Bar to subscribe to changes to Foo's operationResult property? In examples I've found so far, it seems like the subscriptions may only be available inside View structs, but I haven't been able to confirm or deny that.

#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>()

Updating a #Published variable based on changes in an observed variable

I have an AppState that can be observed:
class AppState: ObservableObject {
private init() {}
static let shared = AppState()
#Published fileprivate(set) var isLoggedIn = false
}
A View Model should decide which view to show based on the state (isLoggedIn):
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome
}
In the end a HostView observes the containedView property and displays the correct view based on it.
My problem is that isLoggedIn is not being observed with the code above and I can't seem to figure out a way to do it. I'm quite sure that there is a simple way, but after 4 hours of trial & error I hope the community here can help me out.
Working solution:
After two weeks of working with Combine I have now reworked my previous solution again (see edit history) and this is the best I could come up with now. It's still not exactly what I had in mind, because contained is not subscriber and publisher at the same time, but I think the AnyCancellable is always needed. If anyone knows a way to achieve my vision, please still let me know.
class HostViewModel: ObservableObject, Identifiable {
#Published var contained: DisplayableContent
private var containedUpdater: AnyCancellable?
init() {
self.contained = .welcome
setupPipelines()
}
private func setupPipelines() {
self.containedUpdater = AppState.shared.$isLoggedIn
.map { $0 ? DisplayableContent.mainContent : .welcome }
.assign(to: \.contained, on: self)
}
}
extension HostViewModel {
enum DisplayableContent {
case welcome
case mainContent
}
}
DISCLAIMER:
It is not full solution to the problem, it won't trigger objectWillChange, so it's useless for ObservableObject. But it may be useful for some related problems.
Main idea is to create propertyWrapper that will update property value on change in linked Publisher:
#propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
private var watcher: AnyCancellable?
init(wrappedValue value: Value, _ publisher: P) {
self.wrappedValue = value
watcher = publisher.assign(to: \.wrappedValue, on: self)
}
#Published
private(set) var wrappedValue: Value {
willSet {
objectWillChange.send()
}
}
private(set) lazy var projectedValue = self.$wrappedValue
}
Usage:
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Subscribed(AppState.shared.$isLoggedIn.map({ $0 ? DisplayableContent.navigationWrapper : .welcome }))
var contained: DisplayableContent = .welcome
// each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
// and there's no other way to change the value of `contained`
}
When you add an ObservedObject to a View, SwiftUI adds a receiver for the objectWillChange publisher and you need to do the same. As objectWillChange is sent before isLoggedIn changes it might be an idea to add a publisher that sends in its didSet. As you are interested in the initial value as well as changes a CurrentValueSubject<Bool, Never> is probably best. In your HostViewModel you then need to subscribe to AppState's new publisher and update containedView using the published value. Using assign can cause reference cycles so sink with a weak reference to self is best.
No code but it is very straight forward. The last trap to look out for is to save the returned value from sink to an AnyCancellable? otherwise your subscriber will disappear.
A generic solution for subscribing to changes of #Published variables in embedded ObservedObjects is to pass objectWillChange notifications to the parent object.
Example:
import Combine
class Parent: ObservableObject {
#Published
var child = Child()
var sink: AnyCancellable?
init() {
sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
class Child: ObservableObject {
#Published
var counter: Int = 0
func increase() {
counter += 1
}
}
Demo use with SwiftUI:
struct ContentView: View {
#ObservedObject
var parent = Parent()
var body: some View {
VStack(spacing: 50) {
Text( "\(parent.child.counter)")
Button( action: parent.child.increase) {
Text( "Increase")
}
}
}
}