Passing one StateObject class of one view to another StateObject class of a different view in SwiftUI - class

I have two views each with their own class. The first view has an #StateObject of class "DataClass" that initializes a simple struct DataStruct:
struct View1 : View {
#StateObject var dataToPass = DataClass()
var body: some View {
NavigationView{
ZStack{
NavigationLink(destination: View2(data: dataToPass)){
Text("Navigation Link to View2")
}
}
}
}
}
struct dataStruct {
var variable1 : String
var variable2 : String
}
class DataClass : ObservableObject {
var data : dataStruct
init(){
data = dataStruct(variable1: "1", variable2: "2")
}
}
I'm trying to keep this same instance of the DataClass/dataStruct and pass it on to View2 and its View2Class:
struct View2: View {
#ObservedObject var data : DataClass
#StateObject var game = View2Class(data: data)
var body: some View {
Text("Hello")
}
}
class View2Class : ObservableObject {
#ObservedObject var data : DataClass
init(data : DataClass){
self.data = data
}
}
I want it so that there is only ever one instance/initialization of dataStruct and thus dataClass and View2Class has access to it. View2Class must remain a StateObject. As of right now I am getting an error on the declaration of View2's StateObject: "Cannot use instance member 'data' within property initializer; property initializers run before 'self' is available."
I'm sure it is an easy conceptual fix that I am not understanding right now. Thank you!

To fix the error ("Cannot use instance member 'data'...") try something like this:
struct View1 : View {
#StateObject var dataToPass = DataClass()
var body: some View {
NavigationView{
ZStack{
NavigationLink(destination: View2(data: dataToPass, game: View2Class(data: dataToPass))){
Text("Navigation Link to View2")
}
}
}
}
}
// ...
struct View2: View {
#ObservedObject var data: DataClass
#StateObject var game: View2Class
var body: some View {
Text("Hello")
}
}

You cannot use data to declare game, because data is not "available" before View2 is setup. That is the reason you get the error.
Try this approach, using #Published in the ObservableObject classes,
and passing DataStruct from DataClass to your View2Class with a function in
.onAppear {...}:
struct DataStruct {
var variable1: String
var variable2: String
}
class DataClass: ObservableObject {
#Published var data = DataStruct(variable1: "1", variable2: "2")
}
struct View2: View {
#ObservedObject var dataClass: DataClass
#StateObject var game = View2Class()
var body: some View {
Text("Hello")
.onAppear {
game.setData(dataClass.data)
}
}
}
class View2Class: ObservableObject {
#Published var data: DataStruct?
func setData(_ data: DataStruct){
self.data = data
}
}

#StateObject is used to declare a source of truth with a lifetime automatically tied to a view that requires reference type semantics (i.e. needs to perform tasks other than simply holding data where #State would be more suitable). It doesn't make sense to pass data into one because it is no longer a source of truth because it now has a dependency on another object - the problem is the object doesn't know when the data being passed in has changed (Which SwiftUI supports natively with its View struct and body method). I would suggest redesigning your data flow to be more SwiftUI compatible, there are some great WWDC data flow talks like this one.

Related

Initializing class | Swift

If I create in two separate Views reference to the class, do order and myOrder (in example above) are referencing to the same instance of a class, or they are references to different instances of a class (i.e. creating new instances of a class)?
struct ContentView: View {
#ObservedObject var order = Order()
....
}
struct CheckoutView: View {
// instead of #ObservedObject var order: Order
#ObservedObject var myOrder = Order()
.....
}
Class
class Order: ObservableObject {
....
#Published var item = Item()
....
....
init() { }
}
They are creating new instances. If you'd like to share the same Object I guess you could do something like this:
Creating a state object in the first view and pass it to the second view as observed object. Or you could inject it as environment object.
View one with view model and model:
struct ContentView1: View {
#StateObject var contentView1Model = ContentView1Model()
var body: some View {
NavigationView {
VStack{
TextField("ModelName", text: $contentView1Model.model.name)
NavigationLink(destination: ContentView2(contentView2Model: contentView1Model)){
Text("ToContentView2")
}
.navigationBarTitle("ContentView1")
}
}
}
}
class ContentView1Model: ObservableObject {
#Published var model = Model()
}
struct Model {
var text = ""
}
View two:
struct ContentView2: View {
#ObservedObject var contentView2Model: ContentView1Model
var body: some View {
NavigationView {
TextField("ModelName", text: $contentView2Model.model.name)
.navigationBarTitle("ContentView2")
}
}
}

SwiftUI - Observable Object initiated from Swift class does not update #ObservedObject on ContentView()

The ObservableObject class is being instantiated from both the ContentView() as well as another Swift class. When a function of the ObservableObject class is run by the Swift class, it does not update the #ObservedObject of the ContentView().
I am aware that this is due to me instantiating the ObservableObject class twice. What is the best practice to utilise #ObservedObject when the Observable Class is not/cannot be instantiated by the ContentView().
I haven't found a way to make #EnvironmentObject work with Swift classes.
I could use a global variable and run a Timer() to check for changes to it. However, this feels like an ugly way to do it?!?
Please see example code below. Please run on a device, to see the print statement.
import SwiftUI
struct ContentView: View {
#ObservedObject var observedClass: ObservedClass = ObservedClass()
// The callingObservedClass does not exist on the ContentView, but is called
// somewhere in the app with no reference to the ContentView.
// It is included here to better showcase the issue.
let callingObservedClass: CallingObservedClass = CallingObservedClass()
var body: some View {
VStack {
// This Text shall be updated, when
// self.callingObservedClass.increaseObservedClassCount() has been executed.
Text(String(observedClass.count))
Button(action: {
// This updates the count-variable, but as callingObservedClass creates
// a new instance of ObservedClass, the Text(observedClass.count) is not updated.
self.callingObservedClass.increaseObservedClassCount()
}, label: {
Text("Increase")
})
}
}
}
class CallingObservedClass {
let observedClass = ObservedClass()
func increaseObservedClassCount() {
// Returning an Int here to better showcase that count is increased.
// But not in the ObservedClass instance of the ContentView, as the
// Text(observedClass.count) remains at 0.
let printCount = observedClass.increaseCount()
print(printCount)
}
}
class ObservedClass: ObservableObject {
#Published var count: Int = 0
func increaseCount() -> Int {
count = count + 1
return count
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Edit: I suppose my question is how do I get data from a Swift class and update a SwiftUI view when the data changes when I am unable to instantiate the Swift class from the SwiftUI view.
A possible solution to this is to chain the ObservableObject classes. Unfortunately, as of iOS 13.6 this does not work out of the box.
I found the answer via:
How to tell SwiftUI views to bind to nested ObservableObjects
Adjusted & functioning example:
// Add Combine
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject var callingObservedClass: CallingObservedClass = CallingObservedClass()
var body: some View {
VStack {
// Calling the chained ObservableObject
Text(String(callingObservedClass.observedClass.count))
Button(action: {
self.callingObservedClass.increaseObservedClassCount()
}, label: {
Text("Increase")
})
}
}
}
class CallingObservedClass: ObservableObject {
// Chaining Observable Objects
#Published var observedClass = ObservedClass()
// ObservableObject-chaining does not work out of the box.
// The anyCancellable variable with the below init() will do the trick.
// Thanks to https://stackoverflow.com/questions/58406287/how-to-tell-swiftui-views-to-bind-to-nested-observableobjects
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = observedClass.objectWillChange.sink { (_) in
self.objectWillChange.send()
}
}
func increaseObservedClassCount() {
let printCount = observedClass.increaseCount()
print(printCount)
}
}
class ObservedClass: ObservableObject {
#Published var count: Int = 0
func increaseCount() -> Int {
count = count + 1
return count
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am interested on how to access Swift Class data and update an SwiftUI view if ObservableObject-Chaining is not an option.
Please answer below if you have a solution to this.

SwiftUI: ObservableObject does not persist its State over being redrawn

Problem
In Order to achieve a clean look and feel of the App's code, I create ViewModels for every View that contains logic.
A normal ViewModel looks a bit like this:
class SomeViewModel: ObservableObject {
#Published var state = 1
// Logic and calls of Business Logic goes here
}
and is used like so:
struct SomeView: View {
#ObservedObject var viewModel = SomeViewModel()
var body: some View {
// Code to read and write the State goes here
}
}
This workes fine when the Views Parent is not being updated. If the parent's state changes, this View gets redrawn (pretty normal in a declarative Framework). But also the ViewModel gets recreated and does not hold the State afterward. This is unusual when you compare to other Frameworks (eg: Flutter).
In my opinion, the ViewModel should stay, or the State should persist.
If I replace the ViewModel with a #State Property and use the int (in this example) directly it stays persisted and does not get recreated:
struct SomeView: View {
#State var state = 1
var body: some View {
// Code to read and write the State goes here
}
}
This does obviously not work for more complex States. And if I set a class for #State (like the ViewModel) more and more Things are not working as expected.
Question
Is there a way of not recreating the ViewModel every time?
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
Why is #State keeping the State over the redraw?
I know that usually, it is bad practice to create a ViewModel in an inner View but this behavior can be replicated by using a NavigationLink or Sheet.
Sometimes it is then just not useful to keep the State in the ParentsViewModel and work with bindings when you think of a very complex TableView, where the Cells themself contain a lot of logic.
There is always a workaround for individual cases, but I think it would be way easier if the ViewModel would not be recreated.
Duplicate Question
I know there are a lot of questions out there talking about this issue, all talking about very specific use-cases. Here I want to talk about the general problem, without going too deep into custom solutions.
Edit (adding more detailed Example)
When having a State-changing ParentView, like a list coming from a Database, API, or cache (think about something simple). Via a NavigationLink you might reach a Detail-Page where you can modify the Data. By changing the data the reactive/declarative Pattern would tell us to also update the ListView, which would then "redraw" the NavigationLink, which would then lead to a recreation of the ViewModel.
I know I could store the ViewModel in the ParentView / ParentView's ViewModel, but this is the wrong way of doing it IMO. And since subscriptions are destroyed and/or recreated - there might be some side effects.
Finally, there is a Solution provided by Apple: #StateObject.
By replacing #ObservedObject with #StateObject everything mentioned in my initial post is working.
Unfortunately, this is only available in ios 14+.
This is my Code from Xcode 12 Beta (Published June 23, 2020)
struct ContentView: View {
#State var title = 0
var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}
TestView1()
TestView2()
}
.navigationTitle("\(self.title)")
}
}
}
struct TestView1: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}
class ViewModel: ObservableObject {
#Published var title = 0
}
struct TestView2: View {
#StateObject var model = ViewModel()
var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
As you can see, the StateObject Keeps it value upon the redraw of the Parent View, while the ObservedObject is being reset.
I agree with you, I think this is one of many major problems with SwiftUI. Here's what I find myself doing, as gross as it is.
struct MyView: View {
#State var viewModel = MyViewModel()
var body : some View {
MyViewImpl(viewModel: viewModel)
}
}
fileprivate MyViewImpl : View {
#ObservedObject var viewModel : MyViewModel
var body : some View {
...
}
}
You can either construct the view model in place or pass it in, and it gets you a view that will maintain your ObservableObject across reconstruction.
Is there a way of not recreating the ViewModel every time?
Yes, keep ViewModel instance outside of SomeView and inject via constructor
struct SomeView: View {
#ObservedObject var viewModel: SomeViewModel // << only declaration
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
No needs. #ObservedObject is-a already DynamicProperty similarly to #State
Why is #State keeping the State over the redraw?
Because it keeps its storage, ie. wrapped value, outside of view. (so, see first above again)
You need to provide custom PassThroughSubject in your ObservableObject class. Look at this code:
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//#ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input: ")
TextInput().environmentObject(state)
}
}
}
}
struct TextInput: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: $state.text)
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
First, I using TextChanger to pass new value of .text to .onReceive(...) in CustomState View. Note, that onReceive in this case gets PassthroughSubject, not the ObservableObjectPublisher. In last case you will have only Publisher.Output in perform: closure, not the NewValue. state.text in that case would have old value.
Second, look at the ComplexState class. I made an objectWillChange property to make text changes send notification to subscribers manually. Its almost the same like #Published wrapper do. But, when the text changing it will send both, and objectWillChange.send() and textChanged.send(newValue). This makes you be able to choose in exact View, how to react on state changing. If you want ordinary behavior, just put the state into #ObservedObject wrapper in CustomStateContainer View. Then, you will have all the views recreated and this section will get updated values too:
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
If you don't want all of them to be recreated, just remove #ObservedObject. Ordinary text View will stop updating, but CustomState will. With no recreating.
update:
If you want more control, you can decide while changing the value, who do you want to inform about that change.
Check more complex code:
//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
// var objectWillChange: ObservableObjectPublisher
// #Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
if !onlyPassthroughSend{
objectWillChange.send()
}
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//var state = ComplexState()
#ObservedObject var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input with full state update: ")
TextInput().environmentObject(state)
}
HStack{
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
}
}
}
}
struct TextInputNoUpdate: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding( get: {self.state.text},
set: {newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
}
))
}
}
struct TextInput: View {
#State private var text: String = ""
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding(
get: {self.text},
set: {newValue in
self.state.text = newValue
// self.text = newValue
}
))
.onAppear(){
self.text = self.state.text
}.onReceive(state.textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
I made a manual Binding to stop broadcasting objectWillChange. But you still need to gets new value in all the places you changing this value to stay synchronized. Thats why I modified TextInput too.
Is that what you needed?
My solution is use EnvironmentObject and don't use ObservedObject at view it's viewModel will be reset, you pass through hierarchy by
.environmentObject(viewModel)
Just init viewModel somewhere it will not be reset(example root view).

Binding value from an ObservableObject

Aim:
I have a model which is an ObservableObject. It has a Bool property, I would like to use this Bool property to initialise a #Binding variable.
Questions:
How to convert an #ObservableObject to a #Binding ?
Is creating a #State the only way to initialise a #Binding ?
Note:
I do understand I can make use of #ObservedObject / #EnvironmentObject, and I see it's usefulness, but I am not sure a simple button needs to have access to the entire model.
Or is my understanding incorrect ?
Code:
import SwiftUI
import Combine
import SwiftUI
import PlaygroundSupport
class Car : ObservableObject {
#Published var isReadyForSale = true
}
struct SaleButton : View {
#Binding var isOn : Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "On" : "Off")
}
}
}
let car = Car()
//How to convert an ObservableObject to a Binding
//Is creating an ObservedObject or EnvironmentObject the only way to handle a Observable Object ?
let button = SaleButton(isOn: car.isReadyForSale) //Throws a compilation error and rightly so, but how to pass it as a Binding variable ?
PlaygroundPage.current.setLiveView(button)
Binding variables can be created in the following ways:
#State variable's projected value provides a Binding<Value>
#ObservedObject variable's projected value provides a wrapper from which you can get the Binding<Subject> for all of it's properties
Point 2 applies to #EnvironmentObject as well.
You can create a Binding variable by passing closures for getter and setter as shown below:
let button = SaleButton(isOn: .init(get: { car.isReadyForSale },
set: { car.isReadyForSale = $0} ))
Note:
As #nayem has pointed out you need #State / #ObservedObject / #EnvironmentObject / #StateObject (added in SwiftUI 2.0) in the view for SwiftUI to detect changes automatically.
Projected values can be accessed conveniently by using $ prefix.
You have several options to observe the ObservableObject. If you want to be in sync with the state of the object, it's inevitable to observe the state of the stateful object. From the options, the most commons are:
#State
#ObservedObject
#EnvironmentObject
It is upto you, which one suits your use case.
No. But you need to have an object which can be observed of any change made to that object in any point in time.
In reality, you will have something like this:
class Car: ObservableObject {
#Published var isReadyForSale = true
}
struct ContentView: View {
// It's upto you whether you want to have other type
// such as #State or #ObservedObject
#EnvironmentObject var car: Car
var body: some View {
SaleButton(isOn: $car.isReadyForSale)
}
}
struct SaleButton: View {
#Binding var isOn: Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "Off" : "On")
}
}
}
If you are ready for the #EnvironmentObject you will initialize your view with:
let contentView = ContentView().environmentObject(Car())
struct ContentView: View {
#EnvironmentObject var car: Car
var body: some View {
SaleButton(isOn: self.$car.isReadyForSale)
}
}
class Car: ObservableObject {
#Published var isReadyForSale = true
}
struct SaleButton: View {
#Binding var isOn: Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "On" : "Off")
}
}
}
Ensure you have the following in your SceneDelegate:
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.environmentObject(Car())
In my case i used .constant(viewModel) to pass viewModel to ListView #Binding var viewModel
Example
struct CoursesView: View {
#StateObject var viewModel = CoursesViewModel()
var body: some View {
ZStack {
ListView(viewModel: .constant(viewModel))
ProgressView().opacity(viewModel.isShowing)
}
}
}
struct ListView: View {
#Binding var viewModel: CoursesViewModel
var body: some View {
List {
ForEach(viewModel.courses, id: \.id) { course in
Text(couse.imageUrl)
}
}
}
}

SwiftU NavigationView: how to update previous view every time coming back from the secondary view

In ContentView.swift, I have:
List(recipeData) { recipe in NavigationLink(destination: RecipeView(recipe: recipe)){
Text(recipe.name)
}
}
In the RecipeView, user might update the recipeData variable. However, when the RecipeView is closed, ContentView is not updated based on the updated recipeData.
recipeData is not a #State array but a normal one that is declared outside the ContentView struct. I cannot easily make it a #State var because it is used in other parts of the app.
Thanks!
Using #ObservableObject and #Published you can achieve your requirements.
ViewModel
final class RecipeListViewModel: ObservableObject {
#Published var recipeData: [Recipe] = []
....
....
//write code to fetch recipes from the server or local storage and fill the recipeData
....
....
}
View
struct RepositoryListView : View {
#ObservedObject var viewModel: RecipeListViewModel
var body: some View {
NavigationView {
List(viewModel.recipeData) { recipe in
NavigationLink(destination: RecipeView(recipe: recipe)) {
Text(recipe.name)
}
}
}
}