#Published requires willSet to fire - swift

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

Related

Data communication between 2 ObservableObjects

I have 2 independent ObservableObjects called ViewModel1 and ViewModel2.
ViewModel2 has an array of strings:
#Published var strings: [String] = [].
Whenever that array is modified i want ViewModel1 to be informed.
What's the recommended approach to achieve this?
Clearly, there are a number of potential solutions to this, like the aforementioned NotificationCenter and singleton ideas.
To me, this seems like a scenario where Combine would be rather useful:
import SwiftUI
import Combine
class ViewModel1 : ObservableObject {
var cancellable : AnyCancellable?
func connect(_ publisher: AnyPublisher<[String],Never>) {
cancellable = publisher.sink(receiveValue: { (newStrings) in
print(newStrings)
})
}
}
class ViewModel2 : ObservableObject {
#Published var strings: [String] = []
}
struct ContentView : View {
#ObservedObject private var vm1 = ViewModel1()
#ObservedObject private var vm2 = ViewModel2()
var body: some View {
VStack {
Button("add item") {
vm2.strings.append("\(UUID().uuidString)")
}
ChildView(connect: vm1.connect)
}.onAppear {
vm1.connect(vm2.$strings.eraseToAnyPublisher())
}
}
}
struct ChildView : View {
var connect : (AnyPublisher<[String],Never>) -> Void
#ObservedObject private var vm2 = ViewModel2()
var body: some View {
Button("Connect child publisher") {
connect(vm2.$strings.eraseToAnyPublisher())
vm2.strings = ["Other strings","From child view"]
}
}
}
To test this, first try pressing the "add item" button -- you'll see in the console that ViewModel1 receives the new values.
Then, try the Connect child publisher button -- now, the initial connection is cancelled and a new one is made to the child's iteration of ViewModel2.
In order for this scenario to work, you always have to have a reference to ViewModel1 and ViewModel2, or at the least, the connect method, as I demonstrated in ChildView. You could easily pass this via dependency injection or even through an EnvironmentObject
ViewModel1 could also be changed to instead of having 1 connection, having many by making cancellable a Set<AnyCancellable> and adding a connection each time if you needed a one->many scenario.
Using AnyPublisher decouples the idea of having a specific types for either side of the equation, so it would be just as easy to connect ViewModel4 to ViewModel1, etc.
I had same problem and I found this method working well, just using the idea of reference type and taking advantage of class like using shared one!
import SwiftUI
struct ContentView: View {
#StateObject var viewModel2: ViewModel2 = ViewModel2.shared
#State var index: Int = Int()
var body: some View {
Button("update strings array of ViewModel2") {
viewModel2.strings.append("Hello" + index.description)
index += 1
}
}
}
class ViewModel1: ObservableObject {
static let shared: ViewModel1 = ViewModel1()
#Published var onReceiveViewModel2: Bool = Bool() {
didSet {
print("strings array of ViewModel2 got an update!")
print("new update is:", ViewModel2.shared.strings)
}
}
}
class ViewModel2: ObservableObject {
static let shared: ViewModel2 = ViewModel2()
#Published var strings: [String] = [String]() {
didSet { ViewModel1.shared.onReceiveViewModel2.toggle() }
}
}

How can I Observe a var in a Class?

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
}

How do I execute a function on ClassB when something changes in ClassA?

I have a game written with SwiftUI with two classes, a Game class to manage the game, and a ScoreStore class that manages an array of high scores. Currently the score of a game is stored (via a function on ScoreStore) when the user presses a button (displayed in the ContentView struct) to start a new game. I want to have the score saved when a game reaches a certain state (considered here to be a certain score).
Both Game and ScoreStore are ObservableObjects and have #Published properties available to ContentView as #EnvironmentObjects. Within Game, score is not #Published because it is a computed property.
class Game: ObservableObject, Codable {
var deck: [Card] = []
#Published var piles: [[Card]] = [[],[],[],[]]
var score: Int {
let fullDeckCount = 52
var cardsOnThePiles = 0
for pile in piles {
cardsOnThePiles += pile.count
}
return fullDeckCount - deck.count - cardsOnThePiles
}
class ScoresStore: Codable, ObservableObject, Identifiable {
#Published var highScores: [Score] = []
func addScore(newScore: Int, date: Date = Date()) {
// Do things to add the score to the array
}
struct ContentView: View {
#EnvironmentObject var game: Game
#EnvironmentObject var scores: ScoresStore
func saveScore() {
scores.addScore(newScore: game.score)
}
var body: some View {
Button(action: { saveScore }) {
Text("New Game?")
}
}
I've looked at questions whose answers reference binding via a #State property but in this case the property I'm "observing" is in Game (not ContentView).
Thanks in advance!
You need a PassThroughSubject to allow your views to be triggered to do something when the #Published values change. Then, on your view, you use an .onReceive to trigger your functions.
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var game: Game
#EnvironmentObject var scores: ScoresStore
func saveScore() {
scores.addScore(newScore: game.score)
}
var body: some View {
Button(action: { saveScore }) {
Text("New Game?")
}
.onReceive(game.objectDidChange, perform: { _ in
//Do something here
})
.onReceive(scores.objectDidChange, perform: { _ in
//Do something here
})
}
}
final class Game: NSObject, ObservableObject, Codable {
let objectDidChange = PassthroughSubject<Void, Never>()
var deck: [Card] = []
#Published var piles: [[Card]] = [[],[],[],[]] {
didSet {
self.objectDidChange.send()
}
}
var score: Int {...}
}
final class ScoresStore: NSObject, Codable, ObservableObject, Identifiable {
let objectDidChange = PassthroughSubject<Void, Never>()
#Published var highScores: [Score] = [] {
didSet {
self.objectDidChange.send()
}
}
func addScore(newScore: Int, date: Date = Date()) {
// Do things to add the score to the array
}
}
Every time Game.piles or ScoreStore.highScores updates, you will get a notice to the .onReceive in your view, so you can deal with it. I suspect you will want to keep track of the game score this way as well, and trigger on that change. You can then test for whatever you want and implement whatever action you desire. You will also note that I did this on a didSet. You will also see this as an objectWillSet placed into a willSet on the variable. Both work, but the willSet sends a trigger before the actual update, whereas didSet sends it after. It may not make a difference, but I have had this as an issue in the past. Lastly, I imported Combine at the top, but I did these in one file. Wherever you define the passThroughSubject you will need to import Combine. The view does not actually need to import it. Good luck.

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")
}
}
}
}

Can you use a Publisher directly as an #ObjectBinding property in SwiftUI?

In SwiftUI, can you use an instance of a Publisher directly as an #ObjectBinding property or do you have to wrap it in a class that implements BindableObject?
let subject = PassthroughSubject<Void, Never>()
let view = ContentView(data:subject)
struct ContentView : View {
#ObjectBinding var data:AnyPublisher<Void, Never>
}
// When I want to refresh the view, I can just call:
subject.send(())
This doesn't compile for me and just hangs Xcode 11 Beta 2. But should you even be allowed to do this?
In your View body use .onReceive passing in the publisher like the example below, taken from Data Flow Through SwiftUI - WWDC 2019 # 21:23. Inside the closure you update an #State var, which in turn is referenced somewhere else in the body which causes body to be called when it is changed.
You can implement a BindableObject wich takes a publisher as initializer parameter.
And extend Publisher with a convenience function to create this BindableObject.
class BindableObjectPublisher<PublisherType: Publisher>: BindableObject where PublisherType.Failure == Never {
typealias Data = PublisherType.Output
var didChange: PublisherType
var data: Data?
init(didChange: PublisherType) {
self.didChange = didChange
_ = didChange.sink { (value) in
self.data = value
}
}
}
extension Publisher where Failure == Never {
func bindableObject() -> BindableObjectPublisher<Self> {
return BindableObjectPublisher(didChange: self)
}
}
struct ContentView : View {
#ObjectBinding var binding = Publishers.Just("test").bindableObject()
var body: some View {
Text(binding.data ?? "Empty")
}
}