SwiftUI: Why doesn't ObservedObject work in AppDelegate? - swift

I've tried the example from the ObservableObject documentation.
class Contact: ObservableObject {
#Published var name: String = "me"
#Published var age: Int = 7
}
When I make a Swift Playground with the code:
let c = Contact()
c.objectWillChange.sink { print("This prints") }
c.age += 1
objectWillChange triggers and the line prints.
So far so good.
I now make a View in SwiftUI:
struct ContentView: View {
#ObservedObject var contact = Contact
...
I create this View in the AppDelegate, and do:
contentView.contact.objectWillChange.sink { print("This doesn't print.") }
I've connected the contact to various controls, and changing any fields updates all the controls. Doing onReceive(contact.objectWillChange) also works fine. But not connecting to it in the AppDelegate. I've tried logging deinit() to make sure we're talking about the same object. I've tried using ImmediateScheduler. No dice. Why is this not working?

When you create a subscription with .sink you have to save the AnyCancellable object returned
let cancellable = publisher.sink { ... }
And if you assign it to a variable, make sure it is not short lived. As soon as the cancellable object gets deallocated, the subscription will get cancelled too.

Related

Update UI on Singleton property change without sharing dependency between Views

I want to make my UI update itself on singleton's class property change without passing the singleton object between views as .environmentObject or any another.
I have NetworkManager class, which is singleton:
final class NetworkManager: ObservableObject {
let monitor = NWPathMonitor()
let queue = DispatchQueue(label: "NetworkManager")
#Published var isConnected = true
static let shared: NetworkManager = {
NetworkManager()
}()
private init() {
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async {
self.isConnected = path.status == .satisfied
}
}
monitor.start(queue: queue)
}
}
Whenever #Published var isConnected property of this class changes, I want to update the UI for example with this piece of code: SOME_VIEW.disabled(!NetworkManager.shared.isConnected)
Unfortunately, SOME_VIEW is disabled only when I exit and entry this view once again. I want it to update itself in real time. Is it possible without #EnvironmentObject and only having my NetworkManager class as Singleton?
The ObservableObject updates view only via ObservedObject/StateObject/EnvironmentObject (which are actually have same nature). That wrapper (one of them) actually observers changes in published properties and refreshes view.
So your case can be solved like
struct ParentView: View {
#StateObject private var networkNanager = NetworkManager.shared // << observer !!
var body: some View {
SOME_VIEW.disabled(!networkNanager.isConnected) // injects dependency !!
}
}

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

SwiftUI: Why does the changed property of my Data not refresh the UI?

I have the following data model:
class MyImage: : Identifiable, Equatable, ObservableObject {
let id = UUID()
var path: String
#Published var coordinate: CLLocationCoordinate2D?
}
class MyImageCollection : ObservableObject {
#Published var images: [MyImage] = []
#Published var selection: Set<UUID> = []
}
extension Array where Element == MyImage {
func haveCoordinates() -> Array<MyImage> {
return filter { (image) -> Bool in
return image.coordinate != nil
}
}
}
I use the collection in the views as follows:
# Top View
#StateObject var imageModel: MyImageCollection = MyImageCollection()
# Dependend Views
#ObservedObject var imageModel: MyImageCollection
So in my SwiftUI, whenever I add a new instance of MyImage via imageCollection.images.append(anImage) everything works perfectly and any View is updated accordingly, also any View using imageCollection.haveCoordinates() is updated. But I also want to have any views updated, when I change a property of an image like imageCollection.images[0].coordinate = someCoordinate. That does not work currently.
Any hints?
Your subviews need to directly observe your 'MyImage' class to be updated accordingly. Pass in your 'MyImage' instances directly into an observed object variable. Here's what that may look like...
ForEach(collection.images) { myImage in
YourSubView(image: myImage)
}
Where the image parameter is passed to an observed object property in your subview.
Switch the #ObservedObject to #EnvironmentObject in the DependantView and initialize DependentView().environmentObject(imageModel). Apple Documentation.This connects the two instances.
Also, If you want to Observe each MyImage you have to create an MyImageView that observes it directly
struct MyImage: View{
#ObservedObject var myImage: MyImage
//Rest of code
}
Thanks for your replies, very appreciated. I was trying to create a Subview with a reference to the object as ObservableObject too, but failed because I was using MapAnnotation and that is initialized with the coordinate property. Need to figure that out in detail what the difference is here, the documentation from Apple does not help very much to see any difference at a glance. I'm using some kind of workaround now by "invalidating" the MyImage instance with setting a new UUID:
class MyImage: : Identifiable, Equatable, ObservableObject {
var id = UUID()
var path: String
#Published var coordinate: CLLocationCoordinate2D? {
didSet {
id = UUID()
}
}
}

Using Firebase Realtime Database with SwiftUI List - Clearing a list

I'm wondering what's the most effective way to clear a list's data that's presented in a View via an observable object? At the moment, everytime I reload the view the data gets duplicated (as we are checking the query for updates and parsing the responses). This is not the expected behaviour. The expected behaviour would be to only update the #Published property "if" the database indicates that a new notification has been received.
I know the culprit is the code within the .onAppear block - I'm just not sure architecturally how I might solve this. How could I use this listener while only parsing new data, not data that was previously parsed?
I attempted to clear the list .onAppear, but resulted in a crash indicated that I tried to delete a section while there were already 0 sections so that didn't work.
I've thought of possibly providing the Message object with a Static Unique ID to upload with the Message object when sending to Firebase (Or using the firebase key itself). That way I could use a set of dictionary objects using the unique ID to identify the object in the dictionary. This may help me avoid duplicate entries.
struct Updates: View {
#ObservedObject var dataController = DataController()
var body: some View {
ZStack {
VStack {
List {
ForEach(self.dataController.messages, id: \.id) { message in
Text(message.message)
}
}
}.onAppear {
self.dataController.query.observe(.childAdded) { snapshot in
let data = JSON(snapshot.value as Any)
if let message = Message.parseFirebaseQuery(dataJSON: data){
self.dataController.messages.append(message)
}
}
}
}
}
}
class DataController: ObservableObject {
#Published var query = ChatPathsAndReferences.refs.databaseMessages.queryLimited(toLast:100).queryOrdered(byChild: "date")
#Published var messages = [Message]()
}
I resolved this by adding an init block to my ObservableObject
class DataController: ObservableObject {
#Published var query = ChatPathsAndReferences.refs.databaseMessages.queryLimited(toLast:100).queryOrdered(byChild: "date")
#Published var messages = [Message]()
init() {
self.query.observe(.childAdded) { snapshot in
let data = JSON(snapshot.value as Any)
if let chatMessage = ChatMessage.parseFirebaseQuery(dataJSON: data){
self.messages.append(chatMessage)
}
}
}
}
So that now, when I create the view, the object initialises the observer in the data controller rather than the view.
struct Updates: View {
// As you can see, we initialise the data controller which inits the observer.
// Since the messages in the data controller are #Published,
// we don't need to worry about checking for updates again in the view
#ObservedObject var chatDataController: DataController = DataController()
var body: some View {
GeometryReader { geometry in
ZStack {
Color("Background")
VStack {
// messages
List {
ForEach(self.chatDataController.messages, id: \.id) { message in
Text(message.message)
}
}
}
}
}
}

SwiftUI ForEach not iterating when array changes. List is empty and ForEach does run

I'm trying to use MultiPeer Connectivity framework with swift ui and am having issues with using ForEach in my view. I have a singleton that I'm using to track connected users in an array:
class MPCManager: NSObject {
static let instance = MPCManager()
var devices: [Device] = []
...
And my device class:
class Device: NSObject {
let peerID: MCPeerID
var session: MCSession?
var name: String
var state = MCSessionState.notConnected
var lastMessageReceived: Message?
...
}
When the MultiPeer connectivity frame finds new peers the MPCManager is appending new devices to the array. I have confirmed this in the debugger. The problem comes when I try to display the devices in a list. Here is the code that I'm using:
struct ContentView : View {
var devices: [Device] = MPCManager.instance.devices
var body: some View {
List {
ForEach(self.devices.identified(by: \.name)) { device in
Text(device.name)
}
}
}
}
When the app starts, the list is displayed but it is empty. When I put a breakpoint in the view code inside the ForEach execution never stops. When I change the array to a hardcoded list of values, it displays just fine. I have also tried referencing the array from the static instance directly in my view like this:
ForEach(self.devices.identified(by: \.name)) { device in
Text(device.name)
}
Still nothing. I'm very new to swift so there may be something easy that I'm missing but I just don't see it. Any ideas?
There are a couple issues here as far as I can tell.
First, I would suggest you try this with your MPCManager:
import SwiftUI
import Combine
class MPCManager: NSObject, BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var devices: [Device] = [] {
didSet {
self.didChange.send(())
}
}
}
Then, in your ContentView, do this:
struct ContentView : View {
#ObjectBinding var manager: MPCManager = MPCManager()
var body: some View {
List {
ForEach(self.manager.devices.identified(by: \.name)) { device in
Text(device.name)
}
}
}
}
The main difficulty with answering your question is that I can't run your code. Your question would be more useful to others (and much easier to answer) if you could distill your code down to something that people who might know the answer could just copy and paste into Xcode.
Update
As of Xcode Beta 4, identified(by:) has been replaced by specific initializers for List and ForEach, and as of Xcode Beta 5, BindableObject has been replaced by ObservableObject and #ObjectBinding has been replaced by #ObservedObject.
import SwiftUI
import Combine
class MPCManager: NSObject, ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
var devices: [Device] = [] {
willSet {
self.objectWillChange.send()
}
}
}
struct ContentView : View {
#ObservedObject var manager: MPCManager = MPCManager()
var body: some View {
List {
ForEach(self.manager.devices, id: \.name) { device in
Text(device.name)
}
}
}
}