How to use BindableObjects (EnviromentObject)? - ios

Im using the new SwiftUI. I have a UserUpdate class which is a Bindable Object and I want to modify these variables and automatically Update the UI.
I update these Values successfully but the views in my UI struct isn't updating when I change the variable in the UserUpdate class.
It only changes when I modify the #EnviromentObject variable in the UI struct itself.
That's my Bindable Object Class:
final class UserUpdate: BindableObject {
let didChange = PassthroughSubject<Any, Never>()
var allUsers: [User] = [] {
didSet {
print(allUsers)
didChange.send(allUsers)
}
}
var firstName: String = "" {
didSet {
didChange.send(firstName)
}
}
var lastName: String = "" {
didSet {
didChange.send(lastName)
}
}
}
That's my User class:
struct User: Identifiable {
let id: Int
let firstName, lastName: String
}
Here's how I configure my UI:
struct ContentView : View {
#EnvironmentObject var bindableUser: UserUpdate
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("All Users:").bold().padding(.leading, 10)
List {
ForEach(bindableUser.allUsers) { user in
Text("\(user.firstName) \(user.lastName)")
}
}
}
}
}
}
Here I modify the variables in UserUpdate:
class TestBind {
static let instance = TestBind()
let userUpdate = UserUpdate()
func bind() {
let user = User(id: userUpdate.allUsers.count, firstName: "Heyy", lastName: "worked")
userUpdate.allUsers.append(user)
}
}

I found out that I had to call the method from my UI to get it working so its on the same stream.
For example by this:
struct ContentView : View {
#EnvironmentObject var networkManager: NetworkManager
var body: some View {
VStack {
Button(action: {
self.networkManager.getAllCourses()
}, label: {
Text("Get All Courses")
})
List(networkManager.courses.identified(by: \.name)) {
Text($0.name)
}
}
}
}

If I'm not wrong, you should inject the UserUpdate instance in your ContentView, probably in the SceneDelegate, using ContentView().environmentObject(UserUpdate()).
In that case, you have 2 different instance of the UserUpdate class, the first one created in the SceneDelegate, and the second created in the TestBind class.
The problem is that you have one instance that is bond to view (and will trigger view reload on update), and the one that you actually modify (in TestBind class) which is totally unrelated to the view.
You should find a way to use the same instance in the view and in the TestBind class (for example by using ContentView().environmentObject(TestBind.instance.userUpdate)

Related

SwiftUI Bind to #ObservableObject in array

How do I pass a bindable object into a view inside a ForEach loop?
Minimum reproducible code below.
class Person: Identifiable, ObservableObject {
let id: UUID = UUID()
#Published var healthy: Bool = true
}
class GroupOfPeople {
let people: [Person] = [Person(), Person(), Person()]
}
public struct GroupListView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
//MARK: Other properties
let group: GroupOfPeople = GroupOfPeople()
//MARK: Body
public var body: some View {
ForEach(group.people) { person in
//ERROR: Cannot find '$person' in scope
PersonView(person: $person)
}
}
//MARK: Init
}
public struct PersonView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
#Binding var person: Person
//MARK: Other properties
//MARK: Body
public var body: some View {
switch person.healthy {
case true:
Text("Healthy")
case false:
Text("Not Healthy")
}
}
//MARK: Init
init(person: Binding<Person>) {
self._person = person
}
}
The error I get is Cannot find '$person' in scope. I understand that the #Binding part of the variable is not in scope while the ForEach loop is executing. I'm looking for advice on a different pattern to accomplish #Binding objects to views in a List in SwiftUI.
The SwiftUI way would be something like this:
// struct instead of class
struct Person: Identifiable {
let id: UUID = UUID()
var healthy: Bool = true
}
// class publishing an array of Person
class GroupOfPeople: ObservableObject {
#Published var people: [Person] = [
Person(), Person(), Person()
]
}
struct GroupListView: View {
// instantiating the class
#StateObject var group: GroupOfPeople = GroupOfPeople()
var body: some View {
List {
// now you can use the $ init of ForEach
ForEach($group.people) { $person in
PersonView(person: $person)
}
}
}
}
struct PersonView: View {
#Binding var person: Person
var body: some View {
HStack {
// ternary instead of switch
Text(person.healthy ? "Healthy" : "Not Healthy")
Spacer()
// Button to change, so Binding makes some sense :)
Button("change") {
person.healthy.toggle()
}
}
}
}
You don't need Binding. You need ObservedObject.
for anyone still wondering... it looks like this has been added
.onContinuousHover(perform: { phase in
switch phase {
case .active(let location):
print(location.x)
case .ended:
print("ended")
}
})

How can I load state from en EnvironmentObject in SwiftUI?

So I am working on a view where I want to load state from an EnvironmentObject which acts something like a database.
I would like to achieve something like this:
class MyDB: ObservableObject {
func getName(_ id: RecordID) -> String { ... }
func getChildren(_ id: RecordID) -> [RecordID] { ... }
var didUpdate: PassthroughSubject...
}
struct TreeView: View {
let id: RecordID
#EnvironmentObject var db: DB
#State var name: String
#State var children: [RecordID]
func loadState() {
self.name = db.getName(id)
self.children = db. getChildren(id)
}
var body: some View {
Text(self.name)
List(self.children) { child in
TreeView(id: child)
}
.onReceive(self.db.didUpdate) { _ in
self.loadState()
}
}
}
So basically I would like to just pass the id of the node in the tree view to the child view, and then load the state from this environment object with the loadState function before the view is displayed.
Is there any way to achieve this? For instance, is there some kind of lifecycle function I could implement which will be called after the environment is bound?
Or for example can I implement loadState inside a custom init?
What would be the idiomatic way to handle this?
I have provided an explanation here if you want to check it out.
You will need to pass your MyDB instance using .environmentObject(myDBInstance) on a parent view, so all children views can read from the environment through #EnvironmentObject.
Try using a different approach, such as the following code,
where children and name are published var of MyDB, and
the functions just load the data into those.
// for testing
struct RecordID: Identifiable {
let id = UUID().uuidString
var thing: String = ""
}
class MyDB: ObservableObject {
#Published var didUpdate: Bool = false
#Published var children: [RecordID] = []
#Published var name: String = ""
func getName(_ id: RecordID) {
// ...
name = "xxx" // whatever
}
func getChildren(_ id: RecordID) {
// ...
children = [RecordID(), RecordID()] // whatever
}
}
struct TreeView: View {
#EnvironmentObject var db: MyDB
#State var id: RecordID
var body: some View {
Text(db.name)
List(db.children) { child in
// TreeView(id: child) // <-- here recursive call, use OutlineGroup instead
Text("\(child.id)")
}
.onReceive(db.$didUpdate) { _ in
loadState()
}
}
func loadState() {
db.getName(id)
db.getChildren(id)
}
}
struct ContentView: View {
#StateObject var myDB = MyDB()
let recId = RecordID()
var body: some View {
TreeView(id: recId).environmentObject(myDB)
}
}

SwiftUI Published variable doesn't trigger UI update

I have an app with a navigation view list that doesn't update when new elements get added later on in the app. the initial screen is fine and everything get triggered at this moment no matter how I code them, but beyond that, it stays that way. At some point I had my "init" method as an .onappear, and dynamic elements wouldn't come in, but the static ones would get added multiple times when I would go back and forth in the app, this is no longer part of my code now though.
here what my content view look like, I tried to move the navigation view part to the class that has the published var, in case it help, visually it dint change anything, dint help either.
struct ContentView: View {
#ObservedObject var diceViewList = DiceViewList()
var body: some View {
VStack{
Text("Diceimator").padding()
diceViewList.body
Text("Luck Selector")
}
}
}
and the DiceViewList class
import Foundation
import SwiftUI
class DiceViewList: ObservableObject {
#Published var list = [DiceView]()
init() {
list.append(DiceView(objectID: "Generic", name: "Generic dice set"))
list.append(DiceView(objectID: "Add", name: "Add a new dice set"))
// This insert is a simulation of what add() does with the same exact values. it does get added properly
let pos = 1
let id = 1
self.list.insert(DiceView(objectID: String(id), dice: Dice(name: String("Dice"), face: 1, amount: 1), name: "Dice"), at: pos)
}
var body: some View {
NavigationView {
List {
ForEach(self.list) { dView in
NavigationLink(destination: DiceView(objectID: dView.id, dice: dView.dice, name: dView.name)) {
HStack { Text(dView.name) }
}
}
}
}
}
func add(dice: Dice) {
let pos = list.count - 1
let id = list.count - 1
self.list.insert(DiceView(objectID: String(id), dice: dice, name: dice.name), at: pos)
}
}
I'm working on the latest Xcode 11 in case it matter
EDIT: Edited code according to suggestions, problem didnt change at all
struct ContentView: View {
#ObservedObject var vm: DiceViewList = DiceViewList()
var body: some View {
NavigationView {
List(vm.customlist) { dice in
NavigationLink(destination: DiceView(dice: dice)) {
Text(dice.name)
}
}
}
}
}
and the DiceViewList class
class DiceViewList: ObservableObject {
#Published var customlist: [Dice] = []
func add(dice: Dice) {
self.customlist.append(dice)
}
init() {
customlist.append(Dice(objectID: "0", name: "Generic", face: 1, amount: 1))
customlist.append(Dice(objectID: "999", name: "AddDice", face: 1, amount: 1))
}
}
SwiftUI is a paradigm shift from how you would build a UIKit app.
The idea is to separate the data that "drives" the view - which is the View model, from the View presentation concerns.
In other words, if you had a ParentView that shows a list of ChildView(foo:Foo), then the ParentView's view model should be an array of Foo objects - not ChildViews:
struct Foo { var v: String }
class ParentVM: ObservableObject {
#Published let foos = [Foo("one"), Foo("two"), Foo("three")]
}
struct ParentView: View {
#ObservedObject var vm = ParentVM()
var body: some View {
List(vm.foos, id: \.self) { foo in
ChildView(foo: foo)
}
}
}
struct ChildView: View {
var foo: Foo
var body = Text("\(foo.v)")
}
So, in your case, separate the logic of adding Dice objects from DiceViewList (I'm taking liberties with your specific logic for brevity):
class DiceListVM: ObservableObject {
#Published var dice: [Dice] = []
func add(dice: Dice) {
dice.append(dice)
}
}
struct DiceViewList: View {
#ObservedObject var vm: DiceListVM = DiceListVM()
var body: some View {
NavigationView {
List(vm.dice) { dice in
NavigationLink(destination: DiceView(for: dice)) {
Text(dice.name)
}
}
}
}
If you need more data than what's available in Dice, just create a DiceVM with all the other properties, like .name and .dice and objectId.
But the takeaway is: Don't store and vend out views. - only deal with the data.
While testing stuff I realized the problem. I Assumed declaring #ObservedObject var vm: DiceViewList = DiceViewList() in every other class and struct needing it would make them find the same object, but it doesn't! I tried to pass the observed object as an argument to my subview that contain the "add" button, and it now work as intended.

Why published Array does not change after I change him in SwiftUI?

So, this is just a sample code from my project. After I call SecondView, I want to change names in the array to "LoL", and display them. Why init() does not change my array? Since it does not display new names
struct Person: Identifiable {
let id = UUID()
var name: String
var index: Int
}
class User: ObservableObject {
#Published var array = [Person(name: "Nick", index: 0),
Person(name: "John", index: 1)
]
}
struct ContentView: View {
#ObservedObject var user = User()
var body: some View {
VStack {
ForEach (user.array) { row in
SecondView(name: row.name, index: row.index)
}
}
}
}
struct SecondView: View {
#ObservedObject var user = User()
var name = ""
var index = 0
init() {
user.array[index].name = "LoL"
}
init(name: String, index: Int) {
self.name = name
self.index = index
}
var body: some View {
VStack {
Text(name)
}
}
}
It is because you are not calling init() method in SecondView but you are calling init(name:, index:). Notice how you use SecondView initializer inside your FirstView's iteration (ie. ForEach) loop.
Also your second view displays the name that is passed along the initializer init(name:index:), not the one from your user array. So, if you want to change name to something, do that before this init(name:index:) is called, and pass the name from user array.
You can do it inside your first view.
struct ContentView: View {
#ObservedObject var user = User()
init() {
user.array[0].name = "LoL"
}
var body: some View {
VStack {
ForEach (user.array) { row in
SecondView(name: row.name, index: row.index)
}
}
}
}
Notice that it will now change the first name to Lol because we change it inside ContentView's initializer which then uses the same modified name.

How to tell SwiftUI views to bind to nested ObservableObjects

I have a SwiftUI view that takes in an EnvironmentObject called appModel. It then reads the value appModel.submodel.count in its body method. I expect this to bind my view to the property count on submodel so that it re-renders when the property updates, but this does not seem to happen.
Is this a bug? And if not, what is the idiomatic way to have views bind to nested properties of environment objects in SwiftUI?
Specifically, my model looks like this...
class Submodel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#Published var submodel: Submodel = Submodel()
}
And my view looks like this...
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
Text("Count: \(appModel.submodel.count)")
.onTapGesture {
self.appModel.submodel.count += 1
}
}
}
When I run the app and click on the label, the count property does increase but the label does not update.
I can fix this by passing in appModel.submodel as a property to ContentView, but I'd like to avoid doing so if possible.
Nested models does not work yet in SwiftUI, but you could do something like this
class SubModel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#Published var submodel: SubModel = SubModel()
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}
Basically your AppModel catches the event from SubModel and send it further to the View.
Edit:
If you do not need SubModel to be class, then you could try something like this either:
struct SubModel{
var count = 0
}
class AppModel: ObservableObject {
#Published var submodel: SubModel = SubModel()
}
Sorin Lica's solution can solve the problem but this will result in code smell when dealing with complicated views.
What seems to better advice is to look closely at your views, and revise them to make more, and more targeted views. Structure your views so that each view displays a single level of the object structure, matching views to the classes that conform to ObservableObject. In the case above, you could make a view for displaying Submodel (or even several views) that display's the property from it that you want show. Pass the property element to that view, and let it track the publisher chain for you.
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
SubView(submodel: appModel.submodel)
}
}
struct SubView: View {
#ObservedObject var submodel: Submodel
var body: some View {
Text("Count: \(submodel.count)")
.onTapGesture {
self.submodel.count += 1
}
}
}
This pattern implies making more, smaller, and focused views, and lets the engine inside SwiftUI do the relevant tracking. Then you don't have to deal with the book keeping, and your views potentially get quite a bit simpler as well.
You can check for more detail in this post: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
I wrote about this recently on my blog: Nested Observable Objects. The gist of the solution, if you really want a hierarchy of ObservableObjects, is to create your own top-level Combine Subject to conform to the ObservableObject protocol, and then encapsulate any logic of what you want to trigger updates into imperative code that updates that subject.
For example, if you had two "nested" classes, such as
class MainThing : ObservableObject {
#Published var element : SomeElement
init(element : SomeElement) {
self.element = element
}
}
class SomeElement : ObservableObject {
#Published var value : String
init(value : String) {
self.value = value
}
}
Then you could expand the top-level class (MainThing in this case) to:
class MainThing : ObservableObject {
#Published var element : SomeElement
var cancellable : AnyCancellable?
init(element : SomeElement) {
self.element = element
self.cancellable = self.element.$value.sink(
receiveValue: { [weak self] _ in
self?.objectWillChange.send()
}
)
}
}
Which grabs a publisher from the embedded ObservableObject, and sends an update into the local published when the property value on SomeElement class is modified. You can extend this to use CombineLatest for publishing streams from multiple properties, or any number of variations on the theme.
This isn't a "just do it" solution though, because the logical conclusion of this pattern is after you've grown that hierarchy of views, you're going to end up with potentially huge swatches of a View subscribed to that publisher that will invalidate and redraw, potentially causing excessive, sweeping redraws and relatively poor performance on updates. I would advise seeing if you can refactor your views to be specific to a class, and match it to just that class, to keep the "blast radius" of SwiftUI's view invalidation minimized.
#Published is not designed for reference types so it's a programming error to add it on the AppModel property, even though the compiler or runtime doesn't complain. What would've been intuitive is adding #ObservedObject like below but sadly this silently does nothing:
class AppModel: ObservableObject {
#ObservedObject var submodel: SubModel = SubModel()
}
I'm not sure if disallowing nested ObservableObjects was intentional by SwiftUI or a gap to be filled in the future. Wiring up the parent and child objects as suggested in the other answers is very messy and hard to maintain. What seems to be the idea of SwiftUI is to split up the views into smaller ones and pass the child object to the subview:
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
SubView(model: appModel.submodel)
}
}
struct SubView: View {
#ObservedObject var model: SubModel
var body: some View {
Text("Count: \(model.count)")
.onTapGesture {
model.count += 1
}
}
}
class SubModel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
var submodel: SubModel = SubModel()
}
The submodel mutations actually propagate when passing into a subview!
However, there's nothing stopping another dev from calling appModel.submodel.count from the parent view which is annoying there's no compiler warning or even some Swift way to enforce not doing this.
Source: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
If you need to nest observable objects here is the best way to do it that I could find.
class ChildModel: ObservableObject {
#Published
var count = 0
}
class ParentModel: ObservableObject {
#Published
private var childWillChange: Void = ()
let child = ChildModel()
init() {
child.objectWillChange.assign(to: &$childWillChange)
}
}
Instead of subscribing to child's objectWillChange publisher and firing parent's publisher, you assign values to published property and parent's objectWillChange triggers automatically.
All three ViewModels can communicate and update
// First ViewModel
class FirstViewModel: ObservableObject {
var facadeViewModel: FacadeViewModels
facadeViewModel.firstViewModelUpdateSecondViewModel()
}
// Second ViewModel
class SecondViewModel: ObservableObject {
}
// FacadeViewModels Combine Both
import Combine // so you can update thru nested Observable Objects
class FacadeViewModels: ObservableObject {
lazy var firstViewModel: FirstViewModel = FirstViewModel(facadeViewModel: self)
#Published var secondViewModel = secondViewModel()
}
var anyCancellable = Set<AnyCancellable>()
init() {
firstViewModel.objectWillChange.sink {
self.objectWillChange.send()
}.store(in: &anyCancellable)
secondViewModel.objectWillChange.sink {
self.objectWillChange.send()
}.store(in: &anyCancellable)
}
func firstViewModelUpdateSecondViewModel() {
//Change something on secondViewModel
secondViewModel
}
Thank you Sorin for Combine solution.
I have a solution that I believe is more ellegant than subscribing to the child (view)models. It's weird and I don't have an explanation for why it works.
Solution
Define a base class that inherits from ObservableObject, and defines a method notifyWillChange() that simply calls objectWillChange.send(). Any derived class then overrides notifyWillChange() and calls the parent's notifyWillChange() method.
Wrapping objectWillChange.send() in a method is required, otherwise the changes to #Published properties do not cause the any Views to update. It may have something to do with how #Published changes are detected. I believe SwiftUI/Combine use reflection under the hood...
I have made some slight additions to OP's code:
count is wrapped in a method call which calls notifyWillChange() before the counter is incremented. This is required for the propagation of the changes.
AppModel contains one more #Published property, title, which is used for the navigation bar's title. This showcases that #Published works for both the parent object and the child (in the example below, updated 2 seconds after the model is initialized).
Code
Base Model
class BaseViewModel: ObservableObject {
func notifyWillUpdate() {
objectWillChange.send()
}
}
Models
class Submodel: BaseViewModel {
#Published var count = 0
}
class AppModel: BaseViewModel {
#Published var title: String = "Hello"
#Published var submodel: Submodel = Submodel()
override init() {
super.init()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
guard let self = self else { return }
self.notifyWillChange() // XXX: objectWillChange.send() doesn't work!
self.title = "Hello, World"
}
}
func increment() {
notifyWillChange() // XXX: objectWillChange.send() doesn't work!
submodel.count += 1
}
override func notifyWillChange() {
super.notifyWillChange()
objectWillChange.send()
}
}
The View
struct ContentView: View {
#EnvironmentObject var appModel: AppModel
var body: some View {
NavigationView {
Text("Count: \(appModel.submodel.count)")
.onTapGesture {
self.appModel.increment()
}.navigationBarTitle(appModel.title)
}
}
}
I liked solution by sorin-lica. Based upon that I've decided to implement a custom Property Wrapper (following this amazing article) named NestedObservableObject to make that solution more developer friendly.
This allow to write your model in the following way
class Submodel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#NestedObservableObject var submodel: Submodel = Submodel()
}
Property Wrapper implementation
#propertyWrapper
struct NestedObservableObject<Value : ObservableObject> {
static subscript<T: ObservableObject>(
_enclosingInstance instance: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
) -> Value {
get {
if instance[keyPath: storageKeyPath].cancellable == nil, let publisher = instance.objectWillChange as? ObservableObjectPublisher {
instance[keyPath: storageKeyPath].cancellable =
instance[keyPath: storageKeyPath].storage.objectWillChange.sink { _ in
publisher.send()
}
}
return instance[keyPath: storageKeyPath].storage
}
set {
if let cancellable = instance[keyPath: storageKeyPath].cancellable {
cancellable.cancel()
}
if let publisher = instance.objectWillChange as? ObservableObjectPublisher {
instance[keyPath: storageKeyPath].cancellable =
newValue.objectWillChange.sink { _ in
publisher.send()
}
}
instance[keyPath: storageKeyPath].storage = newValue
}
}
#available(*, unavailable,
message: "This property wrapper can only be applied to classes"
)
var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
private var cancellable: AnyCancellable?
private var storage: Value
init(wrappedValue: Value) {
storage = wrappedValue
}
}
I've published code on gist
I do it like this:
import Combine
extension ObservableObject {
func propagateWeakly<InputObservableObject>(
to inputObservableObject: InputObservableObject
) -> AnyCancellable where
InputObservableObject: ObservableObject,
InputObservableObject.ObjectWillChangePublisher == ObservableObjectPublisher
{
objectWillChange.propagateWeakly(to: inputObservableObject)
}
}
extension Publisher where Failure == Never {
public func propagateWeakly<InputObservableObject>(
to inputObservableObject: InputObservableObject
) -> AnyCancellable where
InputObservableObject: ObservableObject,
InputObservableObject.ObjectWillChangePublisher == ObservableObjectPublisher
{
sink { [weak inputObservableObject] _ in
inputObservableObject?.objectWillChange.send()
}
}
}
So on the call side:
class TrackViewModel {
private let playbackViewModel: PlaybackViewModel
private var propagation: Any?
init(playbackViewModel: PlaybackViewModel) {
self.playbackViewModel = playbackViewModel
propagation = playbackViewModel.propagateWeakly(to: self)
}
...
}
Here's a gist.
See following post for a solution: [arthurhammer.de/2020/03/combine-optional-flatmap][1] . This is solving the question in a Combine-Way with the $ publisher.
Assume class Foto has an annotation struct and and annotation publisher, which publish an annotation struct. Within Foto.sample(orientation: .Portrait) the annotation struct gets "loaded" through the annotation publisher asynchroniously. Plain vanilla combine.... but to get that into a View & ViewModel, use this:
class DataController: ObservableObject {
#Published var foto: Foto
#Published var annotation: LCPointAnnotation
#Published var annotationFromFoto: LCPointAnnotation
private var cancellables: Set<AnyCancellable> = []
init() {
self.foto = Foto.sample(orientation: .Portrait)
self.annotation = LCPointAnnotation()
self.annotationFromFoto = LCPointAnnotation()
self.foto.annotationPublisher
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.assign(to: \.annotation, on: self)
.store(in: &cancellables)
$foto
.flatMap { $0.$annotation }
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.assign(to: \.annotationFromFoto, on: self)
.store(in: &cancellables)
}
}
Note: [1]: https://arthurhammer.de/2020/03/combine-optional-flatmap/
Pay attention the $annotation above within the flatMap, it's a publisher!
public class Foto: ObservableObject, FotoProperties, FotoPublishers {
/// use class not struct to update asnyc properties!
/// Source image data
#Published public var data: Data
#Published public var annotation = LCPointAnnotation.defaultAnnotation
......
public init(data: Data) {
guard let _ = UIImage(data: data),
let _ = CIImage(data: data) else {
fatalError("Foto - init(data) - invalid Data to generate CIImage or UIImage")
}
self.data = data
self.annotationPublisher
.replaceError(with: LCPointAnnotation.emptyAnnotation)
.sink {resultAnnotation in
self.annotation = resultAnnotation
print("Foto - init annotation = \(self.annotation)")
}
.store(in: &cancellables)
}
You can create a var in your top view that is equal to a function or published var in your top class. Then pass it and bind it to every sub view. If it changes in any sub view then the top view will be updated.
Code Structure:
struct Expense : Identifiable {
var id = UUID()
var name: String
var type: String
var cost: Double
var isDeletable: Bool
}
class Expenses: ObservableObject{
#Published var name: String
#Published var items: [Expense]
init() {
name = "John Smith"
items = [
Expense(name: "Lunch", type: "Business", cost: 25.47, isDeletable: true),
Expense(name: "Taxi", type: "Business", cost: 17.0, isDeletable: true),
Expense(name: "Sports Tickets", type: "Personal", cost: 75.0, isDeletable: false)
]
}
func totalExpenses() -> Double { }
}
class ExpenseTracker: ObservableObject {
#Published var name: String
#Published var expenses: Expenses
init() {
name = "My name"
expenses = Expenses()
}
func getTotalExpenses() -> Double { }
}
Views:
struct MainView: View {
#ObservedObject var myTracker: ExpenseTracker
#State var totalExpenses: Double = 0.0
var body: some View {
NavigationView {
Form {
Section (header: Text("Main")) {
HStack {
Text("name:")
Spacer()
TextField("", text: $myTracker.name)
.multilineTextAlignment(.trailing)
.keyboardType(.default)
}
NavigationLink(destination: ContentView(myExpenses: myTracker.expenses, totalExpenses: $totalExpenses),
label: {
Text("View Expenses")
})
}
Section (header: Text("Results")) {
}
HStack {
Text("Total Expenses")
Spacer()
Text("\(totalExpenses, specifier: "%.2f")")
}
}
}
.navigationTitle("My Expense Tracker")
.font(.subheadline)
}
.onAppear{
totalExpenses = myTracker.getTotalExpenses()
}
}
}
struct ContentView: View {
#ObservedObject var myExpenses:Expenses
#Binding var totalExpenses: Double
#State var selectedExpenseItem:Expense? = nil
var body: some View {
NavigationView{
Form {
List {
ForEach(myExpenses.items) { item in
HStack {
Text("\(item.name)")
Spacer()
Button(action: {
self.selectedExpenseItem = item
} ) {
Text("View")
}
}
.deleteDisabled(item.isDeletable)
}
.onDelete(perform: removeItem)
}
HStack {
Text("Total Expenses:")
Spacer()
Text("\(myExpenses.totalExpenses(), specifier: "%.2f")")
}
}
.navigationTitle("Expenses")
.toolbar {
Button {
let newExpense = Expense(name: "Enter name", type: "Expense item", cost: 10.00, isDeletable: false)
self.myExpenses.items.append(newExpense)
self.totalExpenses = myExpenses.totalExpenses()
} label: {
Image(systemName: "plus")
}
}
}
.fullScreenCover(item: $selectedExpenseItem) { myItem in
ItemDetailView(item: myItem, myExpenses: myExpenses, totalExpenses: $totalExpenses)
}
}
func removeItem(at offsets: IndexSet){
self.myExpenses.items.remove(atOffsets: offsets)
self.totalExpenses = myExpenses.totalExpenses()
}
}
Just noting that I'm using the NestedObservableObject approach from #bsorrentino in my latest app.
Normally I'd avoid this but the nested object in question is actually a CoreData model so breaking things out into smaller views doesn't really work in this regard.
This solution seemed best since the world treats NSManagedObjects as (mostly) ObservableObjects and I really, really need to trigger an update if the CodeData object model is changed down the line.
The var submodel in AppModel doesn't need the property wrapper #Published.
The purpose of #Published is to emit new values and objectWillChange.
But the variable is never changed but only initiated once.
Changes in submodel are propagated to the view by the subscriber anyCancellable and ObservableObject-protocol via the sink-objectWillChange construction and causes a View to redraw.
class SubModel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
let submodel = SubModel()
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}
}
}
Nested ObservableObject models do not work yet.
However, you can make it work by manually subscribing each model. The answer gave a simple example of this.
I wanted to add that you can make this manual process a bit more streamlined & readable via extensions:
class Submodel: ObservableObject {
#Published var count = 0
}
class AppModel: ObservableObject {
#Published var submodel = Submodel()
#Published var submodel2 = Submodel2() // the code for this is not defined and is for example only
private var cancellables: Set<AnyCancellable> = []
init() {
// subscribe to changes in `Submodel`
submodel
.subscribe(self)
.store(in: &cancellables)
// you can also subscribe to other models easily (this solution scales well):
submodel2
.subscribe(self)
.store(in: &cancellables)
}
}
Here is the extension:
extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {
func subscribe<T: ObservableObject>(
_ observableObject: T
) -> AnyCancellable where T.ObjectWillChangePublisher == ObservableObjectPublisher {
return objectWillChange
// Publishing changes from background threads is not allowed.
.receive(on: DispatchQueue.main)
.sink { [weak observableObject] (_) in
observableObject?.objectWillChange.send()
}
}
}
It looks like bug. When I update the xcode to the latest version, it work correctly when binding to nested ObservableObjects

Resources