having a bit of trouble with the following.
I have a List in a VStack as follows:
List{
ForEach(fetchRequest.wrappedValue, id: \.self) { city in
NavigationLink(destination: CityView(city: city, moc: self.moc)) {
cityRow(city: city)
}
}
}
This list is populated from a coreData fetchrequest. Each NavigationLinks navigates to CityView and passes a city Object with it.
CityView had a observable object 'notificationHandler' defined as follows:
struct CityView: View {
#ObservedObject var notificationHandler = NotificationHandler()
#ObservedObject var city: City
var body: some View {
}
}
NotificationHandler() sets up an instance of NotificationHandler and sets up a few notification observers from within init as follows:
import Foundation
import Combine
class NotificationHandler: ObservableObject {
let nc = NotificationHandler.default
#Published var networkActive: Bool = false
init() {
nc.addObserver(self, selector: #selector(networkStart), name: Notification.Name("networkingStart"), object: nil)
nc.addObserver(self, selector: #selector(networkStop), name: Notification.Name("networkingStop"), object: nil)
}
}
My issues is this - when the app boots onto its first view which contins the list above - I'm getting a number of instance of NotificationHandler starting - one for every row of the list. - This has led me to the belief that the NavigationLinks in the list are preemtivly loading the CityView's they hold. However I believe this is no longer the case and lazy load is the defualt behaviour. To add to that adding an onAppear() within CityView shows the they are not being completly loaded.
Any help would be greatly appretiated, I can't work out how this is happening.
Thanks
The Destination on the NavigationView is NOT lazy, so it initializes the Destination view as soon as it is created. An easy work around can be found here: SwiftUI - ObservableObject created multiple times. Wrap your Destination view within the LazyView.
Would like to share the latest updates for the solution of this problem.
https://stackoverflow.com/a/66520131
This is the way by which you can avoid creating multiple instances of ObservedObjects.
Sharing this because just lazy loading in NavigationLink doesn't solve the problem where refreshing the views at runtime as per user's actions creates and destroys ObservedObject multiple times.
Hence we need some solution where our Objects are created only once and also persists even after view is refreshed.
Related
I have a class that manages my NSPersistentContainer for my CloudKit/CoreData. In a view to access its contents or update I use an #EnvironmentObject and call the methods and it works perfectly.
The problem is now I need to use it in a class where I can't access the environment object. So I added a shared property to the my class to access it. It works fine but the published variables don't seem to update on my views when triggered by the class.
A simple example looks as followed:
My Datacontroller:
class DataController: ObservableObject {
static let shared = DataController()
//Coredatastack would be here
let container: NSPersistentContainer
#Published var remaining: Int = 0
#Published var today: DayProgress? = nil
func getRemaining() {
remaining = Int((today?.dailyGoal ?? 0) - (today?.currDailyTotal ?? 0))
}
If I call getRemaining() anytime after updating the values of the published variable today in a view, then anywhere the environmentObject remaining is accessed will update.
But in a class if I use Datacontroller.shared.getRemaining(), it will not update the published variable remaining on the view. I assume this is because im working with different instances of the Datacontroller() but im not sure how to solve this. Thanks for any hints or tips.
Note: The class that is triggers the updates the is triggered from outside the app (shortcut) and I have a function that calls getRemainder() anytime the app opens
I have a view, where I have an array of RealmObjects. When I pass them into another View via #Binding and try to edit them there, the View does not get updated.
This is a simplified example. When the name changes, it dumps the correct name into the console, but the View does not update. Tried to force a reload by changing the .id() of the View, but this doesn't help either.
class Person: Object, ObjectIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name: String = ""
}
struct ParentView: View {
#State private var persons: [Person] = []
var body: some View {
ChildView(persons: $persons)
}
}
struct ChildView: View {
#Binding var persons: [Person]
var body: some View {
ForEach(person) { person in
Text(person.name)
.onTapGesture {
person.name = "New Name"
dump(person)
}
}
}
}
One issue is mixing two different technologies by assigning Realm objects to a Swift array - generally speaking, that may not be the best idea.
Realm objects are lazily-loaded and massive datasets take almost no memory. However, as soon as those objects are cast to Swift functions and constructs they are all loaded into memory and can overwhelm the device.
Best practice is to keep Realm objects within Realm stuctures e.g. Collections like Results and Lists.
To address the question:
Realm provides an object #ObservedResults which is a property wrapper that invalides a view when observed objects change. e.g. If the app has #ObservedResults of a group of People objects, if one of those changes, the associated view will also update. From the docs
You can use this property wrapper to create a view that automatically
updates itself when the observed object changes.
So change this
struct ChildView: View {
#Binding var persons: [Person]
to this
struct ChildView: View {
#ObservedResults(Person.self) var persons
I think the rest of the code is good to go as is.
I'm new to SwiftUI and was wondering if there is a concept similar to React.useEffect in SwiftUI.
Below is my code for listening keyboard events on macos.
import SwiftUI
import PlaygroundSupport
struct ContentView : View {
var hello: String
#State var monitor: Any?
#State var text = ""
init(hello: String) {
self.hello = hello
print("ContentView init")
}
var body: some View {
VStack{
Text(hello)
.padding()
TextField("input", text: $text)
}
.onAppear {
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
print(hello)
return nil
}
}
}
}
struct MainView: View {
#State var hello: String = "h"
var body: some View {
ContentView(hello: hello)
Button(action: {
hello += "_h"
}) {
Text("tap me")
}
}
}
PlaygroundPage.current.setLiveView(MainView())
The playground output is as follows
ContentView init
h
h
ContentView init
h
h
h
Since onAppear trigger only once, even ContentView init multiple times. So the event callback here always prints the first captured value ("h").
So where should I add event listener and where should I remove it?
In React, you use useEffect from within a Component in order to declare a task or operation which causes side effects outside the rendering phase.
Well, SwiftUI is not exactly React, and there are problems and use cases which you would solve in a complete different approach. But, when trying to find something similar:
In SwiftUI you could call any function which is called from any "action" closure, for example from a SwiftUI Button. This function can modify #State variables, without disrupting the rendering phase.
Or you can use the Task Modifier, i.e. calling .task { ... } for a SwiftUI view, which comes probably closest.
Personally, I would not declare to use any task or operation which causes side effects to the AppState or Model within a SwiftUI View's body function. Rather, I prefer to send actions (aka "Intent", "Event") from the user to a Model or a ViewModel, or a Finite State Automaton. These events then get processed in a pure function, call it "update()", which performs the "logic", and this pure function may declare "Effects". These effects will then be called outside this pure update function, cause there side effects wherever they need to, and return a result which is materialised as an event, which itself gets fed into the pure update function again. That update function produces a "view state", which the view needs to render.
Now, I want to clarify some potential misconceptions:
"Since onAppear trigger only once, even ContentView init multiple times"
onAppear
This can be actually called several times for a view which you identify on the screen as a "view".
Usually, it is not always without issues to utilise onAppear for performing some sort of initialisation or setup. There are approaches to avoid these problem altogether, though.
"ContentView init"
You are better off viewing a SwiftUI View as a "function" (what?)
With that "function" you achieve two things:
Create an underlying view whose responsibility is to render pixels and also create (private) data for this view which it needs to render accordingly.
Modify this data or attributes of this underlying view.
For either action, you have to call the SwiftUI View's initialiser.
When either action is done, the SwiftUI View (a struct!) will diminish again. Usually, the struct's value, the SwiftUI View resides on the stack only temporarily.
Variables declared as #State and friends, are associated to the underlying view which is responsible to render the pixels. Their lifetime is bound to this renderable view which you can perceive on the screen.
Now, looking at your code, it should work as expected. You created a private #State variable for the event handler object. This seems to be the right approach. However, #State is meant as a private variable where a change would cause the view to render differently. Your event handler object is actually an "Any", i.e. a reference. This reference never changes: it will be setup at onAppear then it never changes anymore, except onAppear will be called again for the same underlying renderable view. There is probably a better solution than using #State and onAppear for your event handler object (see below later).
Now, when you want to render the event's value (aka mask as NSEvent.EventTypeMask) then you need another #State variable in your SwiftUI View of this type, which you set/update in the notification handler. The variable should be a struct or enum, not a reference!
SwiftUI then notifies the changes to this variable and in turn will call the body function where you explicitly render this value. Note, that you can update a #State variable from any thread.
Problems
According the documentation "You must call removeMonitor(_:) to stop the monitor."
Unfortunately, your #State variable which holds the reference to the event handler object will not call removeMonitor(_:) when the underlying renderable view gets deallocated.
Bummer!
What you have to do is, changing your design. What you need to do is to introduce a "Model" which is an ObservableObject. It should publish a value (a representation of what you receive in the notification handler) which will be rendered in the SwiftUI view accordingly.
This Model should also receive an event (say a function will be called for the Model from the SwiftUI view) when the view appears, where the Model then creates the event handler object, unless it has been created already (which completely solves your onAppear issues). Alternatively, just create the event handler once and only once in the Model's initialiser - which is arguable the better solution.
When the event handler's notification handler will be called, you update the published value of your Model accordingly.
Integrating the Model - an ObservableObject - properly into a SwiftUI view is a standard pattern in SwiftUI. Please look for help on SO, if you are uncertain how to accomplish this.
Now, since the Model is a class value, you can ensure to call removeMonitor(_:) in its deinit function.
Headstart
import SwiftUI
final class EventHandlerModel: ObservableObject {
private var monitor: Any!
#Published private(set) var viewState: String = ""
init() {
monitor = NSEvent.addLocalMonitorForEvents(
matching: .keyDown
) { event in
assert(Thread.isMainThread)
self.viewState = "\(event)"
return event
}
}
deinit {
guard let monitor = self.monitor else {
return
}
NSEvent.removeMonitor(monitor)
}
}
struct ContentView: View {
#StateObject private var model = EventHandlerModel()
var body: some View {
Text(verbatim: model.viewState)
}
}
class Room: ObservableObject { ... }
Contact: ObservableObject {
var chatRoom: Room
}
class Account: ObservableObject {
var rooms: Room { … }
var contacts: [Contact] {
return rooms.map {
Contact(chatRoom: $0)
}
}
func listenForRoomEvents() {
// Called on instantiation of a Room, this fires self.objectWillChange on room updates and is working properly
}
}
struct RoomView: View {
#ObservedObject var room: Room
}
/
THIS IS WORKING
/
struct ParentView: View {
#EnvironmentObject account: Account
var body: some View {
RoomsView(account.rooms)
.onAppear {
self.account.listenForRoomEvents()
}
}
}
struct RoomsView: View {
var rooms: [Room]
var body: some View {
ForEach(rooms) { room in
NavigationLink(destination: RoomView(room: room)) {
RoomListItemView(room: room)
}
}
}
}
/
THIS IS NOT WORKING
/
struct ParentView: View {
#EnvironmentObject account: Account
var body: some View {
Child1(contacts: account.contacts)
.onAppear {
self.account.listenForRoomEvents()
}
}
}
struct Child1: View {
#State var selectedContact: Contact?
var contacts: [Contact]
var body: some View {
RoomView(selectedContact.chatRoom)
UserSelectorView(contacts: contacts, selectedUser: $selectedContact) // View allowing selection of a user
}
}
I outlined my setup above; basically, I am instantiating a RoomView object with a Room instance containing all the chat events and other details. Child1 holds a selected contact state variable which is bound to two of its own subviews, one of which allows for the user to select a different contact and such.
What does not make sense to me is that the RoomView renders just fine with all it's events, but in the second solution I have it does not update when new messages come in or when one should be displayed after sending, for instance. I am passing a reference to the same Room object to it, but cannot for the life of me get it to update properly like it does in the first solution.
When I select a new user and go back to the previous one, the messages are all updated as expected.
Here is what I have tried so far:
Making Contact.chatRoom a Published variable, and then calling self.objectWillChange.send() whenever chatRoom does
Okay I finally figured this out, I have no idea why this works and it might be a dumb solution; I needed to not only pass the selectedContact as a parameter, but also the room as another parameter. The code in the outline isnt exactly as it is in my source, but if you ever run into a problem where a class variable isnt updating properly in a view try to pass the variable down from higher up in the chain.
Which version of Xcode are you running? If you are running Xcode 12.2 beta2, I'd recommend you to try it with Xcode 12.0.
I saw a similar issue with my code. After wasting a lot of hours, I finally figure it out that Xcode (I was runnig Xcode 12.2 beta2) has a bug.
SwiftUI: Updating an array item does not update the child UI immediately
I have a class, a “clock face” with regular updates; it should display an array of metrics that change over time.
Because I’d like the clock to also be displayed in a widget, I’ve found that I had to put the class into a framework (perhaps there’s another way, but I’m too far down the road now). This appears to have caused a problem with SwiftUI and observable objects.
In my View I have:
#ObservedObject var clockFace: myClock
In the clock face I have:
class myClock: ObservableObject, Identifiable {
var id: Int
#Publish public var metric:[metricObject] = []
....
// at some point the array is mutated and the display updates
}
I don’t know if Identifiable is needed but it’s doesn’t make any difference to the outcome. The public is demanded by the compiler, but it’s always been like that anyway.
With these lines I get a runtime error as the app starts:
objc[31175] no class for metaclass
So I took off the #Published and changed to a manual update:
public var metric:[metricObject] = [] {
didSet {
self.objectWillChange.send()`
}
}
And now I get a display and by setting a breakpoint I can see the send() is being called at regular intervals. But the display won’t update unless I add/remove from the array. I’m guessing the computed variables (which make up the bulk of the metricObject change isn’t being seen by SwiftUI. I’ve subsequently tried adding a “dummy” Int to the myClock class and setting that to a random value to trying to trigger a manual refresh via a send() on it’s didSet with no luck.
So how can I force a periodic redraw of the display?
What is MetricObject and can you make it a struct so you get Equatable for free?
When I do this with an Int it works:
class PeriodicUpdater: ObservableObject {
#Published var time = 0
var subscriptions = Set<AnyCancellable>()
init() {
Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.time = self.time + 1
})
.store(in: &subscriptions)
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Text("\(self.updater.time)")
}
}
So it's taken a while but I've finally got it working. The problem seemed to be two-fold.
I had a class defined in my framework which controls the SwiftUI file. This class is sub-classed in both the main app and the widget.
Firstly I couldn't use #Published in the main class within the framework. That seemed to cause the error:
objc[31175] no class for metaclass
So I used #JoshHomman's idea of an iVar that's periodically updated but that didn't quite work for me. With my SwiftUI file, I had:
struct FRMWRKShape: Shape {
func drawShape(in rect: CGRect) -> Path {
// draw and return a shape
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
FRMWRKShape()
//....
FRMWRKShape() //slightly different parameters are passed in
}
}
The ContentView was executed every second as I wanted, however the FRMWRKShape code was called but not executed(?!) - except on first starting up - so the view doesn't update. When I changed to something far less D.R.Y. such as:
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Path { path in
// same code as was in FRMWRKShape()
}
//....
Path { path in
// same code as was in FRMWRKShape()
// but slightly different parameters
}
}
}
Magically, the View was updated as I wanted it to be. I don't know if this is expected behaviour, perhaps someone can say whether I should file a Radar....