Make SwiftUI document available as EnvironmentObject - swift

I am reading a FileDocument using SwiftUI. This document is available though a binding to the top-most view:
struct ContentView: View {
#Binding var document: SomeDocument
var body: some View {
Text(self.document.someString)
NestedChildViews().environmentObject(self.document)
} 
}
Is there are a way to turn this #Binding of the document into an #EnvironmentObject to make it available to all nested views? Simply replacing #Binding with #EnvironmentObject will not work, since FileDocument is a struct.
Property type 'SomeDocument' does not match that of the 'wrappedValue' property of its wrapper type 'EnvironmentObject'
Creating an in-between ObservableObject that is created as a #StateObject inside ContentView won't work, since it has to be initiated before the Binding to document is available:
struct ContentView: View {
#Binding var document: SomeDocument
#StateObject var sharedState = SomeSharedState(document: self.document) // won't work
}
What would be the best way to handle this in-between a document and a series of nested views without passing bindings between all levels of them?

You should initialize your #StateObject from within the view's init, since you are passing in the document into the init:
struct ContentView: View {
#Binding var document: SomeDocument
#StateObject var sharedState: SomeSharedState
init(document: Binding<SomeDocument>) {
_document = document
_sharedState = StateObject(wrappedValue: SomeSharedState(document: document.projectedValue))
}
var body: some View {
Text(document.someString)
.environmentObject(sharedState)
/* TEST TO SHOW IT WORKS
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
document = SomeDocument(someString: "New!")
}
}
*/
}
}
class SomeSharedState: ObservableObject {
#Published var document: Binding<SomeDocument>
init(document: Binding<SomeDocument>) {
_document = Published(initialValue: document)
}
}

Related

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

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.

SwiftUI #EnvironmentObject error: may be missing as an ancestor of this view -- accessing object in the init()

The following code produces the runtime error: #EnvironmentObject error: may be missing as an ancestor of this view. The tState in the environment is an #ObservedObject.
struct TEditorView: View {
#EnvironmentObject private var tState: TState
#State var name = ""
init() {
self._name = State(initialValue: tState.name)
}
var body: some View {
...
}
}
XCode 12.0.1
iOS 14
The answer is that an Environment Object apparently cannot be accessed in an init() function. However, an ObservedObject can be. So I changed the code to this and it works. To make it easy I turned TState into a singleton that I could access anywhere. This could probably replace the use of #EnvironmentObject in many situations.
struct TEditorView: View {
#ObservedObject private var tState = TState.shared
//#EnvironmentObject private var tState: TState
#State var name = ""
init() {
self._name = State(initialValue: tState.name)
}
var body: some View {
...
}
}
A different approach here could be to inject the initial TState value in the constructor and do-away with the #EnvironmentObject completely. Then from the parent view you can use the #EnvironmentObject value when creating the view.
struct TEditorView: View {
#State var name = ""
init(tState: TState) {
self._name = State(initialValue: tState.name)
}
var body: some View {
...
}
}
struct ContentView: View {
#EnvironmentObject private var tState: TState
var body: some View {
TEditorView(state: tState)
}
}
Or use a #Binding instead of #State if the name value is meant to be two-way.
In general I'd also question why you need the #EnvironmentObject in the constructor. The idea is with a #EnvironmentObject is that it's represented the same in all views, so you should only need it body.
If you need any data transformations it should be done in the object model itself, not the view.
The #State should be set as private and per the documentation should only be accessed in the View body.
https://developer.apple.com/documentation/swiftui/state
An #EnvironmentObject should be set using the ContentView().environmentObject(YourObservableObject)
https://developer.apple.com/documentation/combine/observableobject
https://developer.apple.com/documentation/swiftui/stateobject
Below is some Sample code
import SwiftUI
class SampleOO: ObservableObject {
#Published var name: String = "init name"
}
//ParentView
struct OOSample: View {
//The first version of an #EnvironmentObject is an #ObservedObject or #StateObject
//https://developer.apple.com/tutorials/swiftui/handling-user-input
#ObservedObject var sampleOO: SampleOO = SampleOO()
var body: some View {
VStack{
Button("change-name", action: {
self.sampleOO.name = "OOSample"
})
Text("OOSample = " + sampleOO.name)
//Doing this should fix your error code with no other workarounds
ChildEO().environmentObject(sampleOO)
SimpleChild(name: sampleOO.name)
}
}
}
//Can Display and Change name
struct ChildEO: View {
#EnvironmentObject var sampleOO: SampleOO
var body: some View {
VStack{
//Can change name
Button("ChildEO change-name", action: {
self.sampleOO.name = "ChildEO"
})
Text("ChildEO = " + sampleOO.name)
}
}
}
//Can only display name
struct SimpleChild: View {
var name: String
var body: some View {
VStack{
//Cannot change name
Button("SimpleChild - change-name", action: {
print("Can't change name")
//self.name = "SimpleChild"
})
Text("SimpleChild = " + name)
}
}
}
struct OOSample_Previews: PreviewProvider {
static var previews: some View {
OOSample()
}
}

Assign property values together in SwiftUI view?

I'm trying to bind properties together in the view and couldn't find anything better specifically for it. This is what I'm doing:
struct ContentView: View {
#ObservedObject var model: MyModel
#State var selectedID: Int
var body: some View {
Picker("Choose", selection: $selectedID) {
Text("Abc").tag(0)
Text("Def").tag(1)
Text("Ghi").tag(2)
}
.onChange(of: model.item?.selectedID) {
selectedID = $0
}
}
}
Is there a better way to bind properties together?
If it is about unidirectional flow then the only change I see needed in provided snapshot is make similar types. Everything else is ok.
struct ContentView: View {
#ObservedObject var model: MyModel
#State var selectedID: Int? // << make optional as well !!
...

Not receiving event for updated #Published

I'm trying to understand Combine a little bit and have an issue and can't wrap my head around it:
I got this data-source
class NoteManager: ObservableObject, Identifiable {
#Published var notes: [Note] = []
var cancellable: AnyCancellable?
init() {
cancellable = $notes.sink(receiveCompletion: { completion in
print("receiveCompletion \(completion)")
}, receiveValue: { notes in
print("receiveValue \(notes)")
})
}
}
which is used here:
struct ContentView: View {
#State var noteManager: NoteManager
var body: some View {
VStack {
NavigationView {
VStack {
List {
ForEach(noteManager.notes) { note in
NoteCell(note: note)
}
}
...
And I can change the values here:
struct NoteCell: View {
#State var note: Note
var body: some View {
NavigationLink(destination: TextField("title", text: $note.title)
...
Anyways - I'm not receiving the receiveValue event after changing the value (which is also correctly reflected in the ui). receiveValue is only called initially when setting it - is there some other way to receive an event for updated fields?
Add on:
struct Note: Identifiable, Codable {
var id = UUID()
var title: String
var information: String
}
make manager observed object (as it is design pair for observable)
struct ContentView: View {
#ObservedObject var noteManager: NoteManager
make cell editing original note via binding (as note is struct)
struct NoteCell: View {
#Binding var note: Note
transfer note by projected value directly from manager (single source of truth) to cell (editor)
ForEach(Array(noteManager.notes.enumerated()), id: \.element) { i, note in
NoteCell(note: $noteManager.notes[i])
}

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)
}
}
}
}