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)
}
}
}
}
}
}
Related
I'm getting an object from Realm using #ObservedResults and then I need to modify the record in the same screen. Like adding items and deleting items, but I need this to happen without returning to previous NavigationView. And I need to update the view displaying the data as its updated.
It seems that #StateRealmObject should be the solution, but how can I designate a stored Realm object as a StateRealmObject? What would be a better approach?
Model:
class Order: Object, ObjectKeyIdentifiable {
#objc dynamic var orderID : String = NSUUID().uuidString
#objc dynamic var name : String = "No Name"
let orderLineId = List<OrderLine>()
.
.
.
override static func primaryKey() -> String? {
return "orderID"
}
}
View: (simplified)
struct OrderView: View {
var currentOrder : Order?
#ObservedResults (Order.self) var allOrders
var realm = try! Realm()
init(orderId: String) {
currentOrder = allOrders.filter("orderID ==\"\(orderId)\"")[0].thaw()
}
func deleteOrderLine(id: String, sku: String) {
try! realm.write {
let query = allOrderLines.filter("id == \"\(id)\" AND productSku == \"\(sku)\"")[0]
realm.delete(query)
}
}
var body: some View {
//Here is where all order data is displayed. When deleting a line. The view pops out. I //need to change data and keep view, and save changes as they are made.
//If delete line func is called, it works, but it pops the view.
deleteOrderLine(id: String, sku: String)
}
}
As per request, this is the parent navigation view, which I believe is the core of the problem.
struct OrderListView2: View {
#ObservedResults (Order.self) var allOrders
var body: some View {
VStack{
List{
ForEach(allOrders.sorted(byKeyPath: "dateCreated",ascending: false), id: \.self) { order in
NavigationLink(destination: OrderView(orderId: order.orderID)){
Text(order.info...)
}
}
The problem is that your allOrderLines collection is not frozen.
This result set is live so when you delete an entry your SwiftUI view is displaying a result set that is older than your live collection.
Hence the RLMException for the index out of bounds. To solve this problem you must freeze that collection for the view then delete the desired object and then reload your frozen collection. To achieve this i would suggest you use a viewmodel.
class OrderViewModel: ObservableObject {
#Published var orders: Results<Order>
init() {
let realm = try! Realm()
self.orders = realm.objects(Order.self).freeze()
}
func deleteOrderLine(id: String, sku: String) {
let realm = try! Realm()
try! realm.write {
let query = realm.objects(Order.self).filter("id == \"\(id)\" AND productSku == \"\(sku)\"")[0]
realm.delete(query)
}
self.orders = realm.objects(Order.self).freeze()
}
}
You can then use the #Published orders for your view. Initialize the viewmodel with #StateObject var viewModel = OrderViewModel()
I have a ObservableObject-Class which inside this class, I got a published var with name of persones! I do initialize it with some data called: allData.
Then I try to update my allData with action of a Button, and this action apply the wanted update to my allData, but my published var has no idea, that this data got updated!
How we can make published see the new updated allData?
struct PersonData: Identifiable {
let id = UUID()
var name: String
}
var allData = [PersonData(name: "Bob"), PersonData(name: "Nik"), PersonData(name: "Tak"), PersonData(name: "Sed"), PersonData(name: "Ted")]
class PersonDataModel: ObservableObject {
#Published var persones: [PersonData] = allData
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack
{
Button("update allData") { allData = [PersonData(name: "Bob")] }
HStack
{
ForEach(personDataModel.persones) { person in Text(person.name) }
}
}
.font(Font.title)
}
}
PS: I donĀ“t want use .onChange or other things for this, I would like this happens internally in my class.
Also I know I can use down code for this work, but that is not the answer
personDataModel.persones = [PersonData(name: "Bob")]
Having a top-level property (outside of any class or struct) is probably not a good idea. I don't see the whole picture, but it looks like your app needs a global state (e.g., a #StateObject initialised on the App level). Consider this answer:
Add EnvironmentObject in SwiftUI 2.0
If you really need to observe your array, you need to make it observable.
One option is to use CurrentValueSubject from the Combine framework:
var persons = ["Bob", "Nik", "Tak", "Sed", "Ted"].map(PersonData.init)
var allData = CurrentValueSubject<[PersonData], Never>(persons)
class PersonDataModel: ObservableObject {
#Published var persones: [PersonData] = allData.value
private var cancellables = Set<AnyCancellable>()
init() {
allData
.sink { [weak self] in
self?.persones = $0
}
.store(in: &cancellables)
}
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack {
Button("update allData") {
allData.send([PersonData(name: "Bob")])
}
HStack {
ForEach(personDataModel.persones) { person in
Text(person.name)
}
}
}
.font(Font.title)
}
}
The allData is copied into persones at initialization time, so changing it afterwards does nothing to personDataModel. After StateObject created you have to work with it, like
Button("update allData") {
self.personDataModel.persones = [PersonData(name: "Bob")]
}
I think you're doing something wrong.
if you want to update all your views, you have to pass the same object with #EnviromentObject.
I don't know your storage method (JSON, CORE DATA, iCloud) but the correct approach is to update directly the model
class PersonDataModel: ObservableObject
{
#Published var persones: [PersonData] = loadFromJSON //one func that is loading your object stored as JSON file
func updateAllData() {
storeToJSON(persones) //one func that is storing your object as JSON file
}
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack
{
Button("update allData") {
self.personDataModel.persones = [PersonData(name: "Bob")]
}
HStack
{
ForEach(personDataModel.persones) { person in Text(person.name) }
}
}
.font(Font.title)
.onChange($personDataModel.persones) {
persones.updateAllData()
}
}
}
I am trying to understand data flows in swiftUI.
I have created a ViewModel which holds some data from a network request.
import SwiftUI
struct breakdown: Decodable {
var sms: Int
var im: Int
var total: Int
}
struct weeklyOverviewStruct: Decodable {
var data: [breakdown]
}
class WeeklyOverviewViewModel: ObservableObject {
#Published var overviewData: weeklyOverviewStruct?
func getBreakdown(){
semaphore.wait()
Network.Request(method: .GET , parameters: nil, endPoint: "rest/operator/stats/weekly/breakdown", completion: {
result,error in
if(error == nil){
do {
let breakdown = try JSONDecoder().decode(weeklyOverviewStruct.self, from: result!)
DispatchQueue.main.async {
self.overviewData = breakdown
}
}catch{
print("Json Error")
}
}else{
print("\(error!)")
}
})
}
}
It my understanding that I can then observe this ViewModel in a second view and the view will re-draw if the ViewModel changes:
struct SecondView: View {
#ObservedObject var WeeklyOverviewVM = WeeklyOverviewViewModel()
var body: some View {
Text(String.init(describing: WeeklyOverviewVM.overviewData?.data[0].total))
}
}
If however the second view is presented after the getBreakDown() is called the observedObject is nil.
Is there a way of persisting the data so that even if a view is presented after the getBreakdown() function is called, the data from the previous request is observable in the second view?
The SecondView now creates view model every time it is instantiated, so if you want previously fetched data persist, it needs to store that view model instance somewhere outside of SecondView and inject it on its creation, like
struct SecondView: View {
// only declare
#ObservedObject var WeeklyOverviewVM: WeeklyOverviewViewModel
and somewhere in parent view
...
// inject WeeklyOverviewViewModel from own property
SecondView(WeeklyOverviewVM: self.secondViewVM)
...
Here is the scenario: Onappear my view calls the getData method of my ObservableObject Class. During debug I see that it retrieves the data and returns the array as expected. When my view appears on the simulator the data flashes briefly on the list, then the list goes blank. I use this same pattern in several places in my application and in some cases it works (the list appears as it should) and in others it goes blank.
I'm testing on both 13.0 and 13.3 simulators due to various swiftUI bugs that work on one or the other. I thought it may be because of the async call to Firestore, but I've implemented completion handlers. IF I change the ObservedObject to a #State then the list appears correctly, but when new items are added it doesn't refresh.
Here is code for one of the examples that is not working:
My VIEW:
import SwiftUI
import Firebase
struct ManagerListView: View {
var circuitId: String
#State private var isMgr:Bool = true //TODO: reset to false
#EnvironmentObject var session : SessionStore
#ObservedObject var managerArray = ManagerList()
func deleteMgrDb(manager: Manager) {
let docRef = Firestore.firestore().collection("circuits").document(circuitId).collection("managers").document(manager.id)
docRef.delete { (error) in
if error != nil {
print(error!.localizedDescription)
}
}
}
var body: some View {
List {
ForEach(managerArray.managers) { i in
Text("\(i.firstName) \(i.lastName) \(i.phone)").deleteDisabled(!self.isMgr)
}.onDelete { (indexset) in
let manager = self.managerArray.managers[indexset.first!]
print("Here \(self.managerArray.managers[indexset.first!])")
//TODO: Check if owner (can't remove owner) or maybe if at least one manager (then delete owner?)
self.managerArray.managers.remove(atOffsets: indexset)
self.deleteMgrDb(manager: manager)
}.disabled(!isMgr)
}.onAppear(perform: {
self.managerArray.getData(forcircuitid: self.circuitId) { (error) in
if error != nil {
//TODO: Return data error to user
print(error!.localizedDescription)
}
//TODO: isMgr
}
}).onDisappear(perform: {
self.managerArray.stopMgrListListener()
}).navigationBarItems(trailing: isMgr ?
NavigationLink(destination: CircuitManagerAddView(circuitId: self.circuitId, currentmgrs: managerArray.managers), label: {
Image(systemName: "plus")
}): nil) //.disabled(!isMgr))
}
}
My Observable Class:
class ManagerList: ObservableObject {
#Published var managers = [Manager]()
var listener: ListenerRegistration?
func getData(forcircuitid: String, completion: #escaping (Error?)->Void) {
let db = Firestore.firestore().collection("circuits").document(forcircuitid).collection("managers")
self.managers.removeAll()
listener = db.addSnapshotListener { (snapshot, error) in
if error != nil {
print(error!.localizedDescription)
completion(error)
}
else {
for i in snapshot!.documentChanges {
if i.type == .added {
let email = i.document.get("email") as! String
let lastName = i.document.get("lastname") as! String
let firstName = i.document.get("firstname") as! String
let phone = i.document.get("phonenum") as! String
let manager = Manager(id: email, lastName: lastName, firstName: firstName, phone: phone)
self.managers.append(manager)
//TODO: Changed
}
}
completion(nil)
}
}
}
For my lists that are working, I am using basically the exact same code except changing the Firestore documents, data elements, variable names, etc.. They even run the same way in the debugger, only difference is some work and display the list and others don't.
The list items are disappearing because ManagerListView is re-initialised every time your ManagerList data changes. When that happens ManagerList is also re-initialised but the code you have executing in onAppear is only called the first time the view appears and not on subsequent data changes.
SwiftUI makes no guarantees about the lifetime of #ObservedObject variables. You have to manage that yourself. In iOS 14 they introduced #StateObject to make this easier. You can read more here.
As a side note, onDisappear is only called when your view is no longer being displayed. It would be better to remove the Firebase listener in ManagerList from within the classes de-initialiser function.
I have a model type which looks like this:
enum State {
case loading
case loaded([String])
case failed(Error)
var strings: [String]? {
switch self {
case .loaded(let strings): return strings
default: return nil
}
}
}
class MyApi: ObservableObject {
private(set) var state: State = .loading
func fetch() {
... some time later ...
self.state = .loaded(["Hello", "World"])
}
}
and I'm trying to use this to drive a SwiftUI View.
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List($api.state.strings) {
Text($0)
}
}
}
}
It's about here that my assumptions fail. I'm trying to get a list of the strings to render in my List when they are loaded, but it won't compile.
The compiler error is Generic parameter 'Subject' could not be inferred, which after a bit of googling tells me that bindings are two-way, so won't work with both my private(set) and the var on the State enum being read-only.
This doesn't seem to make any sense - there is no way that the view should be able to tell the api whether or not it's loading, that definitely should be a one-way data flow!
I guess my question is either
Is there a way to get a one-way binding in SwiftUI - i.e. some of the UI will update based on a value it cannot change.
or
How should I have architected this code! It's very likely that I'm writing code in a style which doesn't work with SwiftUI, but all the tutorials I can see online neatly ignore things like loading / error states.
You don't actually need a binding for this.
An intuitive way to decide if you need a binding or not is to ask:
Does this view need to modify the passed value ?
In your case the answer is no. The List doesn't need to modify api.state (as opposed to a textfield or a slider for example), it just needs the current value of it at any given moment. That is what #State is for but since the state is not something that belongs to the view (remember, Apple says that each state must be private to the view) you're correctly using some form of an ObservableObject (through Environment).
The final missing piece is to mark any of your properties that should trigger an update with #Published, which is a convenience to fire objectWillChange signals and instruct any observing view to recalculate its body.
So, something like this will get things done:
class MyApi: ObservableObject {
#Published private(set) var state: State = .loading
func fetch() {
self.state = .loaded(["Hello", "World"])
}
}
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List(api.state.strings ?? [], id: \.self) {
Text($0)
}
}
}
}
Not exactly the same problem as I had, but the following direction can help you possibly find a good result when bindings are done with only reads.
You can create a custom binding using a computed property.
I needed to do exactly this in order to show an alert only when one was passed into an overlay.
Code looks something along these lines :
struct AlertState {
var title: String
}
class AlertModel: ObservableObject {
// Pass a binding to an alert state that can be changed at
// any time.
#Published var alertState: AlertState? = nil
#Published var showAlert: Bool = false
init(alertState: AnyPublisher<AlertState?, Never>) {
alertState
.assign(to: &$alertState)
alertState
.map { $0 != nil }
.assign(to: &$showAlert)
}
}
struct AlertOverlay<Content: View>: View {
var content: Content
#ObservedObject var alertModel: AlertModel
init(
alertModel: AlertModel,
#ViewBuilder content: #escaping () -> Content
) {
self.alertModel = alertModel
self.content = content()
}
var body: some View {
ZStack {
content
.blur(radius: alertModel.showAlert
? UserInterfaceStandards.blurRadius
: 0)
}
.alert(isPresented: $alertModel.showAlert) {
guard let alertState = alertModel.alertState else {
return Alert(title: Text("Unexected internal error as occured."))
}
return Alert(title: Text(alertState.title))
}
}
}