ObservableObject is missing in Motherview - ios

I have the following viewfile:
import SwiftUI
class Progress: ObservableObject {
#Published var index = 0
}
struct RegistrationView: View {
#ObservedObject var progress: Progress
var body: some View {
TabView(selection: $progress.index) {
setUsernameView(progress: self.progress).tabItem{}.tag(0)
setMasterPasswordView().tabItem{}.tag(1)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
}
}
when I am finished with my setUserNameView() I want to update index to 1 so that it switches to setMasterPasswordView. Im passing progress to the view and then inside to another view which updates the value.
Now xcode complains: Missing argument for parameter 'progress' in call the call is in the contentView().
import SwiftUI
struct ContentView: View {
var body: some View {
RegistrationView()
}
}
My question is, why does the view or any other view above RegistrationView need this object too and how do I save me from that work. I tried to make progress private but that doesn't work. I am defining the class and all other things in this view and I only need it for itself and the child of a child.
P.S. is there a better methode for observable objects? For example when I need the value in a main view and can change it in a child.child.child.view I have to pass it all the way down. And that seems kind of unnecessary work.

If you're not going to use it in parent view then just use locally as StateObject
struct RegistrationView: View {
#StateObject var progress = Progress() // << here !!
// ... other code

Related

SwiftUI - IOS 16 - How to use new NavigationStack and NavigationPath for programatic navigation in MVVM architecture?

Description
For programatic navigation you could previously use NavigationLink(isActive:, destination:, label:) which would fire navigation when the isActive param is true. In IOS 16 this became deprecated and NavigationStack, NavigationLink(value:, label:) and NavigationPath was introduced.
To read about the usage of these follow the links:
https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
https://www.hackingwithswift.com/articles/250/whats-new-in-swiftui-for-ios-16 (search for NavigationStack)
My question is how should I use and maintain the array with the content of the navigation stack (like the NavigationPath object) if I'd like to use it in different Views and in their ViewModels?
As you can see in the code below I created a NavigationPath object to hold my navigation stack in the BaseView or BaseView.ViewModel. This way I can do programatic navigation from this BaseView to other pages (Page1, Page2), which is great.
But if I go to Page1 and try to navigate from there to Page2 programatically I need to have access to the original NavigationPath object object, the one that I use in BaseView.
What would be the best way to access this original object?
It is possible that I misunderstand the usage of this new feature but if you have any possible solutions for programatic navigation from a ViewModel I would be glad to see it :)
Code
What I've tried to do:
struct BaseView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack(path: $viewModel.paths) {
VStack {
Button("Page 1", action: viewModel.goToPage1)
Button("Page 2", action: viewModel.goToPage2)
}
.navigationDestination(for: String.self) { stringParam in
Page1(stringParam: stringParam)
}
.navigationDestination(for: Int.self) { intParam in
Page2(intParam: intParam)
}
}
}
}
extension BaseView {
#MainActor class ViewModel: ObservableObject {
#Published var paths = NavigationPath()
func goToPage1() {
let param = "Some random string" // gets the parameter from some calculation or async network call
paths.append(param)
}
func goToPage2() {
let param = 19 // gets the parameter from some calculation or async network call
paths.append(param)
}
}
}
struct Page1: View {
#StateObject var viewModel = ViewModel()
let stringParam: String
var body: some View {
VStack {
Button("Page 2", action: viewModel.goToPage2)
}
}
}
extension Page1 {
#MainActor class ViewModel: ObservableObject {
func goToPage2() {
// Need to add value to the original paths variable in BaseView.ViewModel
}
}
}
struct Page2: View {
#StateObject var viewModel = ViewModel()
let intParam: Int
var body: some View {
Text("\(intParam)")
}
}
extension Page2 {
#MainActor class ViewModel: ObservableObject {
}
}
There is no need for MVVM in SwiftUI because the View struct plus property wrappers is already equivalent to a view model object but faster and less error prone. Also in SwiftUI we don't even have access to the traditional view layer - it takes our View data structs, diffs them to create/update/remove UIView/NSView objects, using the best ones for the platform/context. If you use an object for view data instead, then you'll just have the same consistency problems that SwiftUI was designed to eliminate.
Sadly the web (and Harvard University) is filled with MVVM SwiftUI articles by people that didn't bother to learn it properly. Fortunately things are changing:
I was wrong! MVVM is NOT a good choice for building SwiftUI applications (Azam Sharp)
How MVVM devs get MVVM wrong in SwiftUI: From view model to state (Jim Lai)
Stop using MVVM for SwiftUI (Apple Developer Forums)

Can't show view in Swift UI with layers of views and onAppear

There is a strange case where if you show a view through another view the contents (list of 3 items) of the second view won't show when values are set using onAppear. I'm guessing SwiftUI gets confused since the second views onAppear is called prior to the first views onAppear, but I still think this is weird since both of the views data are only used in their own views. Also, there is no problem if I don't use view models and instead have the data being set using state directly in the view, but then there is yet another problem that the view model declaration must be commented out otherwise I get "Thread 1: EXC_BAD_ACCESS (code=1, address=0x400000008)". Furthermore, if I check for nil in the first view on the data that is set there before showing the second one, then the second view will be shown the first time you navigate (to the first containing the second), but no other times. I also tried removing content view and starting directly at FirstView and then the screen is just black. I want to understand why these problems happen, setting data through init works but then the init will be called before it's navigated to since that's how NavigationView works, which in turn I guess I could work around by using a deferred view, but there are cases where I would like to do stuff in the background with .task as well and it has the same problem as .onAppear. In any case I would like to avoid work arounds and understand the problem. See comments for better explanation:
struct ContentView: View {
var body: some View {
NavigationView {
// If I directly go to SecondView instead the list shows
NavigationLink(destination: FirstView()) {
Text("Go to first view")
}
}
}
}
class FirstViewViewModel: ObservableObject {
#Published var listOfItems: [Int]?
func updateList() {
listOfItems = []
}
}
struct FirstView: View {
#ObservedObject var viewModel = FirstViewViewModel()
// If I have the state in the view instead of the view model there is no problem.
// Also need to comment out the view model when using the state otherwise I get Thread 1: EXC_BAD_ACCESS runtime exception
//#State private var listOfItems: [Int]?
var body: some View {
// Showing SecondView without check for nil and it will never show
SecondView()
// If I check for nil then the second view will show the first time its navigated to, but no other times.
/*Group {
if viewModel.listOfItems != nil {
SecondView()
} else {
Text("Loading").hidden() // Needed in order for onAppear to trigger, EmptyView don't work
}
}*/
// If I comment out onAppear there is no problem
.onAppear {
print("onAppear called for first view after onAppear in second view")
viewModel.updateList()
// listOfItems = []
}
}
}
class SecondViewViewModel: ObservableObject {
#Published var listOfItems = [String]()
func updateList() {
listOfItems = ["first", "second", "third"]
}
}
struct SecondView: View {
#ObservedObject var viewModel = SecondViewViewModel()
// If I set the items through init instead of onAppear the list shows every time
init() {
// viewModel.updateList()
}
var body: some View {
Group {
List {
ForEach(viewModel.listOfItems, id: \.self) { itemValue in
VStack(alignment: .leading, spacing: 8) {
Text(itemValue)
}
}
}
}
.navigationTitle("Second View")
.onAppear {
viewModel.updateList()
// The items are printed even though the view don't show
print("items: \(viewModel.listOfItems)")
}
}
}
We don't use view model objects in SwiftUI. For data transient to a View we use #State and #Binding to make the View data struct behave like an object.
And FYI initing an object using #ObservedObject is an error causing a memory leak, it will be discarded every time the View struct is init. When we are creating a Combine loader/fetcher object that we want to have a lifetime tied to the view we init the object using #StateObject.
Also you must not do id: \.self with ForEach for an array of value types cause it'll crash when the data changes. You have to make a struct for your data that conforms to Identifiable to be used with ForEach. Or if you really do want a static ForEach you can do ForEach(0..<5) {

What is the difference between ObservedObject and StateObject in SwiftUI

If I have an ObservableObject in SwiftUI I can refer to it as an #ObservedObject:
class ViewModel: ObservableObject {
#Published var someText = "Hello World!"
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text(viewModel.someText)
}
}
Or as a #StateObject:
class ViewModel: ObservableObject {
#Published var someText = "Hello World!"
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Text(viewModel.someText)
}
}
But what's the actual difference between the two? Are there any situations where one is better than the other, or they are two completely different things?
#ObservedObject
When a view creates its own #ObservedObject instance it is recreated every time a view is discarded and redrawn:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
}
On the contrary a #State variable will keep its value when a view is redrawn.
#StateObject
A #StateObject is a combination of #ObservedObject and #State - the instance of the ViewModel will be kept and reused even after a view is discarded and redrawn:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
}
Performance
Although an #ObservedObject can impact the performance if the View is forced to recreate a heavy-weight object often, it should not matter much when the #ObservedObject is not complex.
When to use #ObservedObject
It might appear there is no reason now to use an #ObservedObject, so when should it be used?
You should use #StateObject for any observable properties that you
initialize in the view that uses it. If the ObservableObject instance
is created externally and passed to the view that uses it mark your
property with #ObservedObject.
Note there are too many use-cases possible and sometimes it may be desired to recreate an observable property in your View. In that case it's better to use an #ObservedObject.
Useful links:
What’s the difference between #StateObject and #ObservedObject?
What’s the difference between #ObservedObject, #State, and #EnvironmentObject?
What is the #StateObject property wrapper?
Apple documentation did explain why initializing with ObservedObject is unsafe.
SwiftUI might create or recreate a view at any time, so it’s important that initializing a view with a given set of inputs always results in the same view. As a result, it’s unsafe to create an observed object inside a view.
The solution is StateObject.
At the same time, the documentation showed us how we should create data models in a view (or app/scene) when it can hold on to the truth, and pass it to another view.
struct LibraryView: View {
#StateObject var book = Book() // Hold on to the 1 truth
var body: some View {
BookView(book: book) // Pass it to another view
}
}
struct BookView: View {
#ObservedObject var book: Book // From external source
}
Even though pawello2222's answer have nicely explained the differences when the view itself creates its view model, it's important to note the differences when the view model is injected into the view.
When you inject the view model into the view, as long as the view model is a reference type, there are no differences between #ObservedObject and #StateObject, since the object that injected the view model into your view should hold a reference to view model as well, hence the view model isn't destroyed when the child view is redrawn.
class ViewModel: ObservableObject {}
struct ParentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
ChildView(viewModel: viewModel) // You inject the view model into the child view
}
}
// Even if `ChildView` is discarded/redrawn, `ViewModel` is kept in memory, since `ParentView` still holds a reference to it - `ViewModel` is only released and hence destroyed when `ParentView` is destroyed/redrawn.
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
}
Here is an example to illustrate the difference.
Every time you click the Refresh button the StateObjectClass is recreated from scratch only for CountViewObserved. This means it's #Published count property gets the default value of 0 when this happens.
The difference between #StateObject and #ObservedObject is clear. The #StateObject version of the observed StateObjectClass preserves its state since it is never deinitted. The #ObservedObject version does not as it is recreated. So you should use #StateObject for the owner of an ObservableObject.
import SwiftUI
class StateObjectClass: ObservableObject {
enum ObserverType: String {
case stateObject
case observedObject
}
#Published var count = 0
let type: ObserverType
let id = UUID()
init(type: ObserverType) {
self.type = type
}
deinit {
print(#function, "type: \(type.rawValue) id: \(id)")
}
}
struct CountViewState: View {
#StateObject var state = StateObjectClass(type: .stateObject)
var body: some View {
VStack {
Text("#StateObject's count: \(state.count)")
Button("ADD 1"){
state.count += 1
}
}
}
}
struct CountViewObserved: View {
#ObservedObject var state = StateObjectClass(type: .observedObject)
var body: some View {
VStack {
Text("#ObservedObject's count: \(state.count)")
Button("Add 1") {
state.count += 1
}
}
}
}
struct ContentView: View {
#State private var count = 0
var body: some View {
VStack {
Text("Refresh CounterView's count: \(count)")
Button("Refresh") {
count += 1
}
CountViewState()
.padding()
CountViewObserved()
.padding()
}
}
}
#StateObject is a state of a given view, thus the instance of it is retained by SwiftUI across body updates. It is not retained though when running in Preview.
#ObservedObject on the other hand is just an object being observed by given View, thus is not retained by SwiftUI (it has to be retained outside of the View).
In other words - it looks like SwiftUI keeps a strong reference of #StateObject and unowned reference of #ObservedObject.
Retained vs non-retained source, Previews behavior source, around ~8:30.
The difference between let's say :
#ObservedObject var book: BookModel
And
#StateObject var book: BookModel
#ObservedObject does NOT own the instance book, its your responsibility to manage the life cycle of the instance..
But when you want to tie the life cycle of your observable object book to your view like in #State you can use #StateObject.
In this case SwiftUI will OWN the observable object and the creation and destruction will be tied to the view's life cycle
SwiftUI will keep the object alive for the whole life cycle of the view
This is great for expensive resources, you do not need to fiddle with onDisappear anymore to release resources.
This clarification is taken from WWDC2020 Data essentials in SwiftUI

Observing Binding or State variables

I'm looking for a way of observing #State or #Binding value changes in onReceive. I can't make it work, and I suspect it's not possible, but maybe there's a way of transforming them to Publisher or something while at the same time keeping the source updating value as it's doing right now?
Below you can find some context why I need this:
I have a parent view which is supposed to display half modal based on this library: https://github.com/AndreaMiotto/PartialSheet
For this purpose, I've created a #State private var modalPresented: Bool = false and I'm using it to show and hide this modal view. This works fine, but my parent initializes this modal immediately after initializing self, so I completely loose the onAppear and onDisappear modifiers. The problem is that I need onAppear to perform some data fetching every time this modal is being presented (ideally I'd also cancel network task when modal is being dismissed).
use ObservableObject / ObservedObject instead.
an example
import SwiftUI
class Model: ObservableObject {
#Published var txt = ""
#Published var editing = false
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
TextField("Email", text: self.$model.txt, onEditingChanged: { edit in
self.model.editing = edit
}).onReceive(model.$txt) { (output) in
print("txt:", output)
}.onReceive(model.$editing) { (output) in
print("editing:", output)
}.padding().border(Color.red)
}
}

What does Apple mean by #ObjectBinding should be passed over through view?

I've been newly studying SwiftUI.
And since I've seen Data flow over SwiftUI video from Apple explaining difference between #ObjectBinding and #EnvironmentObject, a question has come to my mind.
What does apple mean by :
You have to pass around the model from hop to hop in #ObjectBinding ? (29':00")
Do we have to pass the object using #binding in another views for using them ?
What if we don't use #binding and reference to it using another #ObjectBinding ?
Does that make an inconvenience or make SwiftUI not to work correctly or views not being sync with each other ?
[Edit: note that #ObjectBinding is no longer around; instead you can use #State to mark an instance variable as requiring a view refresh when it changes.]
When a view declares an #State or #Binding variable, its value must be explicitly passed from the view's parent. So if you have a long hierarchy of views with some piece of data from the top being used in the bottom, you must code every level of that hierarchy to know about and pass down the data.
In his comment at 29:00, he is contrasting this to using #EnvironmentVariable in a child view, which searches the whole hierarchy for that piece of data. This means any views that do not explicitly need the data can effectively ignore it. Registering a variable needs only be done once (via .environmentObject(_) on a view).
Here is a contrived example. Given some data type conforming to ObservableObject,
class SampleData: ObservableObject {
#Published var contents: String
init(_ contents: String) {
self.contents = contents
}
}
Consider this view hierarchy:
struct ContentView: View {
#State private var data = SampleData("sample content")
var body: some View {
VStack {
StateViewA(data: self.data)
EnvironmentViewA()
.environmentObject(self.data)
}
}
}
struct StateViewA: View {
#State var data: SampleData
var body: some View {
StateViewB(data: self.data)
}
}
struct StateViewB: View {
#State var data: SampleData
var body: some View {
Text(self.data.contents)
}
}
struct EnvironmentViewA: View {
var body: some View {
EnvironmentViewB()
}
}
struct EnvironmentViewB: View {
#EnvironmentObject var data: SampleData
var body: some View {
Text(self.data.contents)
}
}
The result in ContentView will be two views that display the same piece of text. In the first, StateViewA must pass the data on to its child (i.e. the model is passed from "hop to hop"), whereas in the second, EnvironmentViewA does not have to be aware of the data at all.

Resources