SWIFTUI - OberservableObject class being set to nill each time called - swift

I am trying to learn Swift, and swiftUI.
I have a class : ObservableObject called within a button.
the class fetches some data, and stores it in a data var (an array of (string:any))
the class function fetch data is called within onAppear function in the main view VStack.
the results of the function populates a list.
it all works fine, but each time I recall the view, the list empties itself.
here s the class opening
class fetchDataModel: ObservableObject {
#Published var datas:[data] = [data]()
now the datalist view :
struct allData: View {
#ObservedObject var datas = fetchDataModel()
var body: some View {
VStack{
NavigationView {
List(datas.data){ data in
NavigationLink(destination: data(data: data)){
HStack{
Text(data.someText)
}
}
}
}
}.navigationBarTitle(Text("Landmarks"))
.onAppear(){
self.datas.fetchData()
}
}
}
I have a side menu that populates a motherView, once a button from the side menu is clicked, it calls datalist() (session is an environmentalObject)
if session.currentPage == "data" {
myview = AnyView(allDatas())
}
So I guess, each time I set myView to allDatas, it reinitialises the datas = fetchDataModel() class, and sets all its values to 0. Is there a way to avoid this problem ?

Related

How can I call a function of a child view from the parent view in swiftUI to change a #state variable?

I'm trying to get into swift/swiftui but I'm really struggling with this one:
I have a MainView containing a ChildView. The ChildView has a function update to fetch the data to display from an external source and assign it to a #State data variable.
I'd like to be able to trigger update from MainView in order to update data.
I've experienced that update is in fact called, however, data is reset to the initial value upon this call.
The summary of what I have:
struct ChildView: View {
#State var data: Int = 0
var body: some View {
Text("\(data)")
Button(action: update) {
Text("update") // works as expected
}
}
func update() {
// fetch data from external source
data = 42
}
}
struct MainView: View {
var child = ChildView()
var body: some View {
VStack {
child
Button(action: {
child.update()
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
When googling for a solution, I only came across people suggesting to move update and data to MainView and then pass a binding of data to ChildView.
However, following this logic I'd have to blow up MainView by adding all the data access logic in there. My point of having ChildView at all is to break up code into smaller chunks and to reuse ChildView including the data access methods in other parent views, too.
I just cannot believe there's no way of doing this in SwiftUI.
Is completely understandable to be confused at first with how to deal with state on SwiftUI, but hang on there, you will find your way soon enough.
What you want to do can be achieved in many different ways, depending on the requirements and limitations of your project.
I will mention a few options, but I'm sure there are more, and all of them have pros and cons, but hopefully one can suit your needs.
Binding
Probably the easiest would be to use a #Binding, here a good tutorial/explanation of it.
An example would be to have data declared on your MainView and pass it as a #Binding to your ChildView. When you need to change the data, you change it directly on the MainView and will be reflected on both.
This solutions leads to having the logic on both parts, probably not ideal, but is up to what you need.
Also notice how the initialiser for ChildView is directly on the body of MainView now.
Example
struct ChildView: View {
#Binding var data: Int
var body: some View {
Text("\(data)")
Button(action: update) {
Text("update") // works as expected
}
}
func update() {
// fetch data from external source
data = 42
}
}
struct MainView: View {
#State var data: Int = 0
var body: some View {
VStack {
ChildView(data: $data)
Button(action: {
data = 42
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
ObservableObject
Another alternative would be to remove state and logic from your views, using an ObservableObject, here an explanation of it.
Example
class ViewModel: ObservableObject {
#Published var data: Int = 0
func update() {
// fetch data from external source
data = 42
}
}
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Text("\(viewModel.data)")
Button(action: viewModel.update) {
Text("update") // works as expected
}
}
}
struct MainView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ChildView(viewModel: viewModel)
Button(action: {
viewModel.update()
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}

ObservedObject not working when getting data async for view

Using StateObject allows my view to rerender correctly after getting data asynchronously but ObservedObject does not, why?
I have two swiftui views. First is the root App view and the second is the one consuming an ObservedObject view model.
From the root view I create a ViewModel with a firebase object ID for the second view and pass it in. When the second view appears I'd like to then send a request to Firebase to retrieve some data. While the data is being retrieved I show a 'loading' screen.
When I use ObservedObject my second view never rerenders after the data is retrieved. Using StateObject though fixes the issue but I'm not sure why.
I think I understand the differences between ObservedObject and StateObject having read the docs. But not sure how the differences apply to my case since I don't think the root view should be rerendering and recreating the ViewModel I created there and passed to the second view.
There is a Login view which forces the RootView to rerender after login but from what I know, the SecondView still shouldn't be rendered more than once.
https://www.avanderlee.com/swiftui/stateobject-observedobject-differences/
RootView
#main
struct MyApp: App {
#ObservedObject private var userService = UserService()
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
NavigationView {
Group {
if (userService.currentUser != nil) {
SecondView(secondViewModel: SecondViewModel(somethingId: "someIdGoesHere"))
} else {
LoginView()
}
}
.environmentObject(userService)
}
}
}
}
View Model
class SecondViewModel: ObservableObject {
#Published private(set) var thing: Thing? = nil
private let thingService = ThingService()
private let thingId: String
init(thingId: String) {
self.thingId = thingId
}
func load() async {
self.thing = await thingService.GetThing(thingId: thingId) // Async call to firebase
}
}
Second View
struct SecondView: View {
// Task runs to completion but 'Loading...' never goes away.
#ObservedObject var secondViewModel: SecondViewModel
var body: some View {
VStack(spacing: 5) {
if let thingText = secondViewModel.thing {
Text(thingText)
} else {
Text("Loading...")
}
}
.onAppear {
Task {
await secondViewModel.load()
}
}
}
}

How do you edit the members of a Struct of type View from another Struct of type View? (SwiftUI)

I have the following 2 structs within separate files and displayed in the contentView. What I'm trying to understand is how to maintain the contentView as only displaying and organizing the UI. Placing all of my other views in separate files. My first thought was the correct approach would be to use static variables updated by functions that are called from the button press action. But the buttons text did not update accordingly. As they are dynamically updated according to #State.
update:
I attempted to solve this by using protocols and delegates to no avail. By my understanding this delegate call should be receiving on the other end and updating structcop.ID and the change should be reflected in the content view.
FILE 1
import SwiftUI
struct structdispatch: View {
var radio:RadioDelegate?
func send() {
radio?.update()
self.debug()
}
var body: some View {
Button(action: self.send)
{Text("DISPATCHER")}
}
func debug() {
print("Button is sending?")
}
}
struct structdispatch_Previews: PreviewProvider {
static var previews: some View {
structdispatch()
}
}
**FILE 2:**
import SwiftUI
protocol RadioDelegate {
func update()
}
struct structcop: View, RadioDelegate {
#State public var ID:Int = 3
func update(){
print("message recieved")
self.ID += 1
print(self.ID)
}
var body: some View {
Text(String(self.ID))
}
}
struct structcop_Previews: PreviewProvider {
static var previews: some View {
structcop()
}
}
DEBUG CONSOLE RETURNS:
The Button is working
View is updated on some internal DynamicProperty change, like #State, so here is possible solution
Tested with Xcode 12 / iOS 14
struct structcop: View {
static public var ID = 3
#State private var localID = Self.ID {
didSet {
Self.ID = localID
}
}
var body: some View {
Button(action: printme)
{Text(String(localID))}
}
func printme(){
self.localID = 5
print(structcop.ID)
}
}
Solution:
After some digging I have a working solution but I'm still curious if there is a way to modify properties of other structs while maintaining dynamic view updates.
Solution: store data for display in an observable object which will either read or act as the model which the user is interacting with.
An observable object is a custom object for your data that can be bound to a view from storage in SwiftUI’s environment. SwiftUI watches for any changes to observable objects that could affect a view, and displays the correct version of the view after a change. -apple
A new model type is declared that conforms to the ObservableObject protocol from the Combine framework. SwiftUI subscribes to the observable object and updates relevant views that need refreshing when the data changes. SceneDelegate.swift needs to have the .environmentObject(_:) modifier added to your root view.
Properties declared within the ObservableObject should be set to #Published so that any changes are picked up by subscribers.
For test code I created an ObservableObject called headquarters
import SwiftUI
import Combine
final class hq: ObservableObject {
#Published var info = headQuarters
}
let headQuarters = hqData(id: 3)
struct hqData {
var id: Int
mutating func bump() {
self.id += 1
}
}
In my struct dispatch I subscribed to the object and called a function that iterated the id in the model whenever the button was pressed. My struct cop also subscribed to the object and thus the model and button text updated accordingly to changes.
struct dispatch: View {
#EnvironmentObject private var hq: headqarters
var body: some View {
Button(action: {self.hq.info.bump()}) {
Text("Button")
}
}
}
struct cop: View {
#EnvironmentObject private var hq: headquarters
var body: some View {
Text(String(self.hq.info.id))
}
}

unwanted response - navigationlink cycles through all pages

I am doing a project where I've got some codes I am confused about. its an app to track user's habits, ContentView has a page with a list of habits, each has its own page (HabitPage) showing more detail using navigationLink.
navigationLink worked fine to show a habit page at first, but what I intend to do is the following:
user clicks a button to increase the number of times a habit is done
when the number of times its done reaches the goal, I want the app to show an alert, saying the goal's achieved
App to jump back to ContentView
I managed to do 1,2 but not 3, in my HabitPage I added an Environment var to be dismissed and a Bool to toggle. The Bool is tied to an observableobject class on a separate page. I added the Bool to navigationLink on isActive. Now what it does is that it's cycling through all the HabitPage after clicking into one from ContentView. Can someone explain the logic behind to me as I am lost to what caused it. thanks
ContentView
struct ContentView: View {
#ObservedObject var habitsCV = DataShare()
- function for deletion
var body: some View {
NavigationView {
ZStack {
//background
List {
ForEach (habitsCV.allHabitGeneral, id: \.name) { each in
**NavigationLink(destination: habitView(habitHV: self.habitsCV,singleHabit: each), isActive: self.$habitsCV.isShowingHabit) {**
- stuff for describing the habit
}
}
}
}
.onDelete(perform: itemRemove)
}
.frame(maxWidth: .infinity)
}
- buttons, another sheet which works fine and nav bar title
}
}
}
HabitPage
struct habitView : View {
**#ObservedObject var habitHV : DataShare**
var singleHabit : AllHabits
**#Environment(\.self.presentationMode) var presentationMode**
-function for the app
var body : some View {
NavigationView {
VStack(alignment: .leading) {
- text for descriptions of the habit
Button("increase timesDone") {
var o : Int = 0
- function to retrieve array index o
if self.habitHV.allHabitGeneral[o].timesDone > self.habitHV.allHabitGeneral[o].completionGoal {
-lines for alert
**self.habitHV.isShowingHabit = false**
**self.presentationMode.wrappedValue.dismiss()**
}
}
}
}
-alert
}
}
}
}
the class storing the Bool
class DataShare : ObservableObject {
- alert messages and another Boolean for a functional sheet
**#Published var isShowingHabit : Bool = false**
#Published var allHabitGeneral : [AllHabits] {
-didSet function
- initialiser

SwiftUI - Conditional based on ObservedObject doesn't work in subsequent Views

I have a very simple app which contains two views that are tied together with a NavigationLink. In the first view, ContentView, I can see updates to my ObservedObject as expected. However, when I go to the next View, it seems that the code based on the ObservedObject does not recognize changes.
ContentView.swift (The working view):
import SwiftUI
struct ContentView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
NavigationView {
VStack(spacing: 15) {
Toggle(isOn: self.$toggleObject.isToggled) {
Text("Toggle:")
}
if self.toggleObject.isToggled == true {
Text("ON")
}
else {
Text("OFF")
}
NavigationLink(destination: ShowToggleView()) {
Text("Show Toggle Status")
}
}
}
}
}
ShowToggleView.swift (The view that does not behave as I expect it to):
import SwiftUI
struct ShowToggleView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
Form {
Section {
if self.toggleObject.isToggled {
Text("Toggled on")
}
else {
Text("Toggled off")
}
}
}
}
}
All of this data is stored in a simple file, ToggleObject.swift:
import SwiftUI
class ToggleObject: ObservableObject {
#Published var isToggled = false
}
When I toggle it on in the first View I see the text "ON" which is expected, but when I go into the next view it shows "Toggled off" no matter what I do in the first View... Why is that?
Using Xcode 11.5 and Swift 5
You are almost doing everything correct. However, you are creating another instance of ToggleObject() in your second view, which overrides the data. You basically only create one ObservedObject and then pass it to your subview, so they both access the same data.
Change it to this:
struct ShowToggleView: View {
#ObservedObject var toggleObject : ToggleObject
And then pass the object to that view in your navigation link...
NavigationLink(destination: ShowToggleView(toggleObject: self.toggleObject)) {
Text("Show Toggle Status")
}