SwiftUI Using ObservableObject in View on New Sheet - ios

I am struggling with figuring out how to use a value assigned to a variable in an ObservableObject class in another view on another sheet. I see that it gets updated, but when I access it in the new view on the new sheet it is reset to the initialized value. How do I get it to retain the new value so I can use it in a new view on a new sheet?
ContentData.swift
import SwiftUI
import Combine
class ContentData: ObservableObject {
#Published var text: String = "Yes"
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var contentData = ContentData()
#State private var inputText: String = ""
#State private var showNewView: Bool = false
var body: some View {
VStack {
TextField("Text", text: $inputText, onCommit: {
self.assignText()
})
Button(action: {
self.showNewView = true
}) {
Text("Go To New View")
}
.sheet(isPresented: $showNewView) {
NewView(contentData: ContentData())
}
}
}
func assignText() {
print(contentData.text)
contentData.text = inputText
print(contentData.text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(contentData: ContentData())
}
}
NewView.swift
import SwiftUI
struct NewView: View {
#ObservedObject var contentData = ContentData()
var body: some View {
VStack {
Text(contentData.text)
}
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView(contentData: ContentData())
}
}
I have tried many, many different methods I have seen from other examples. I tried doing it with #EnviromentObject but could not get that to work either. I also tried a different version of the NewView.swift where I initialized the value with:
init(contentData: ContentData) {
self.contentData = contentData
self._newText = State<String>(initialValue: contentData.text)
}
I think I am close, but I do not see what I am missing. Any help would be very much appreciated. Thanks.

#ObservedObject var contentData = ContentData()
ContentData() in the above line creates a new instance of the class ContentData.
You should pass the same instance from ContentView to NewView to retain the values. Like,
.sheet(isPresented: $showNewView) {
NewView(contentData: self.contentData)
}
Stop creating new instance of ContentData in NewView and add the ability to inject ContentData from outside,
struct NewView: View {
#ObservedObject var contentData: ContentData
...
}

Related

cannot find $exercisePlan in scope (SwiftUI) [duplicate]

How do I generate a preview provider for a view which has a binding property?
struct AddContainer: View {
#Binding var isShowingAddContainer: Bool
var body: some View {
Button(action: {
self.isShowingAddContainer = false
}) {
Text("Pop")
}
}
}
struct AddContainer_Previews: PreviewProvider {
static var previews: some View {
// ERROR HERE <<<-----
AddContainer(isShowingAddContainer: Binding<Bool>()
}
}
In Code above, How to pass a Binding<Bool> property in an initialiser of a view?
Just create a local static var, mark it as #State and pass it as a Binding $
struct AddContainer_Previews: PreviewProvider {
#State static var isShowing = false
static var previews: some View {
AddContainer(isShowingAddContainer: $isShowing)
}
}
If you want to watch the binding:
Both other solutions [the "static var" variant AND the "constant(.false)"-variant work for just seeing a preview that is static. But you cannot not see/watch the changes of the value intended by the button action, because you get only a static preview with this solutions.
If you want to really watch (in the life preview) this changing, you could easily implement a nested view within the PreviewProvider, to - let's say - simulate the binding over two places (in one preview).
import SwiftUI
struct BoolButtonView: View {
#Binding var boolValue : Bool
var body: some View {
VStack {
Text("The boolValue in BoolButtonView = \(boolValue.string)")
.multilineTextAlignment(.center)
.padding()
Button("Toggle me") {
boolValue.toggle()
}
.padding()
}
}
}
struct BoolButtonView_Previews: PreviewProvider {
// we show the simulated view, not the BoolButtonView itself
static var previews: some View {
OtherView()
.preferredColorScheme(.dark)
}
// nested OTHER VIEW providing the one value for binding makes the trick
struct OtherView : View {
#State var providedValue : Bool = false
var body: some View {
BoolButtonView(boolValue: $providedValue)
}
}
}
Other way
struct AddContainer_Previews: PreviewProvider {
static var previews: some View {
AddContainer(isShowingAddContainer: .constant(false))
}
}

Updating EnvironmentObject from within the View Model

In SwiftUI, I want to pass an environment object to a view model so I can change/update it. The EnvironmentObject is a simple AppState which consists of a single property counter.
class AppState: ObservableObject {
#Published var counter: Int = 0
}
The view model "CounterViewModel" updates the environment object as shown below:
class CounterViewModel: ObservableObject {
var appState: AppState
init(appState: AppState) {
self.appState = appState
}
var counter: Int {
appState.counter
}
func increment() {
appState.counter += 1
}
}
The ContentView displays the value:
struct ContentView: View {
#ObservedObject var counterVM: CounterViewModel
init(counterVM: CounterViewModel) {
self.counterVM = counterVM
}
var body: some View {
VStack {
Text("\(counterVM.counter)")
Button("Increment") {
counterVM.increment()
}
}
}
}
I am also injecting the state as shown below:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
let appState = AppState()
ContentView(counterVM: CounterViewModel(appState: appState))
.environmentObject(appState)
}
}
}
The problem is that when I click the increment button, the counterVM.counter never returns the updated value. What am I missing?
Your class CounterViewModel is an ObservableObject, but it has no #Published properties – so no changes will be published automatically to the views.
But you can manually publish changes by using objectWillChange.send():
func increment() {
objectWillChange.send()
appState.counter += 1
}
We actually don't use view model objects in SwiftUI for view data. We use an #State struct and if we need to mutate it in a subview we pass in a binding, e.g.
struct Counter {
var counter: Int = 0
mutating func increment() {
counter += 1
}
}
struct ContentView: View {
#State var counter = Counter()
var body: some View {
ContentView2(counter: $counter)
}
}
struct ContentView2: View {
#Binding var counter: Counter // if we don't need to mutate it then just use let and body will still be called when the value changes.
var body: some View {
VStack {
Text(counter.counter, format: .number) // the formatting must be done in body so that SwiftUI will update the label automatically if the region settings change.
Button("Increment") {
counter.increment()
}
}
}
}
I'm not sure why both the CounterViewModel and the AppState need to be observable objects, since you are using a view model to format the content of your models. I would consider AppState to be a model and I could therefore define it as a struct. The CounterViewModel will then be the ObservableObject and it published the AppState. In this way your code is clean and works.
Code for AppState:
import Foundation
struct AppState {
var counter: Int = 0
}
Code for CounterViewModel:
import SwiftUI
class CounterViewModel: ObservableObject {
#Published var appState: AppState
init(appState: AppState) {
self.appState = appState
}
var counter: Int {
appState.counter
}
func increment() {
appState.counter += 1
}
}
Code for the ContentView:
import SwiftUI
struct ContentView: View {
#StateObject var counterVM = CounterViewModel(appState: AppState())
var body: some View {
VStack {
Text("\(counterVM.counter)")
Button("Increment") {
counterVM.increment()
}
}
}
}
Do remind, that in the View where you first define an ObservableObject, you define it with #StateObject. In all the views that will also use that object, you use #ObservedObject.
This code will work.
Did you check your xxxxApp.swift (used to be the AppDelegate) file ?
Sometimes Xcode would do it for you automatically, sometimes won't you have to add it manually and add your environment object. * It has to be the view that contains all the view you want to share the object to.
var body: some Scene {
WindowGroup {
VStack {
ContentView()
.environmentObject(YourViewModel())
}
}
}

How to return View for editing its property?

I'm making a class that returns its editor view.
But got an error
Cannot convert value of type 'Published.Publisher' to expected
argument type 'Binding'
This is simple test code for Playgrounds.
import SwiftUI
import PlaygroundSupport
struct EditorView: View {
#Binding var text: String
var body: some View {
HStack {
Text("Name:")
TextField("Jerry", text: $text)
}
}
}
class MyModel: ObservableObject {
#Published var name: String = "Tom"
func editorView() -> some View {
EditorView(text: $name) // [ERROR] Cannot convert value of type 'Published.Publisher' to expected argument type 'Binding<String>'
}
}
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
model.editorView()
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 375, height: 400))
}
}
let viewController = UIHostingController(rootView: ContentView())
let nav = UINavigationController(rootViewController: viewController)
PlaygroundPage.current.liveView = nav
What is wrong?
2022.03.23 Added
I tested to make a view without binding, it works. I think that the class can create a view with its property. But can't pass the #Binding.
import SwiftUI
import PlaygroundSupport
struct EditorView: View {
#Binding var text: String
var body: some View {
HStack {
Text("Name:")
TextField("Jerry", text: $text)
}
}
}
// Just display the name parameter
struct DispView: View {
let name: String
var body: some View {
Text(name)
}
}
class MyModel: ObservableObject {
#Published var name: String = "Tom"
#ViewBuilder
func editorView() -> some View {
// EditorView(text: $name) // Error at this line.
DispView(name: name) // Pass the property.
}
}
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
model.editorView()
}
}
struct ContentView_Preview: PreviewProvider {
static var previews: some View {
ContentView()
.previewLayout(.fixed(width: 375, height: 400))
}
}
let viewController = UIHostingController(rootView: ContentView())
let nav = UINavigationController(rootViewController: viewController)
PlaygroundPage.current.liveView = nav
You create EditorView in wrong place, put it in your ContentView instead of your ViewModel. ViewModel is for holding data (or model) instead of View. I believe SwiftUI is not designed for carrying view along.
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
editorView
}
}
extension ContentView {
#ViewBuilder
var editorView: some View {
EditorView(text: $model.name)
}
}
If you still want to put it in your ViewModel, this should be the working code. You can create new Binding as parameter for your view initializer.
#ViewBuilder
func editorView() -> some View {
EditorView(text:
Binding { name } set: { newName in self.name = newName }
)
}
Binding is provided by StateObject wrapper (on ObservableObject), and a view should be created in body (ie. in view hierarchy), so it should look like
struct ContentView: View {
#StateObject var model: MyModel = .init()
var body: some View {
EditorView(text: model.$name) // << here !!
}
}
Note: alternate would be to make EditorView dependent to MyModel directly and then inside such view you could create a wrapper which would provide needed binding.

#EnvironmentObject with parent child classes

I am trying to create a global loader which needs to work across all loading activity in the app. Created a baseStore with loading property which holds the boolean indicator for whole app. All other stores inherit BaseStore.
When trying to update the published variable from child views its not updating header indicator. Any ideas what am I doing wrong?
class BaseStore : ObservableObject {
#Published var isLoading = false
}
class ChildStore: BaseStore {
func update(){
super.isLoading = true
}
}
somewhere in the view
import SwiftUI
struct HeaderView: View {
#EnvironmentObject var baseStore: BaseStore
var body: some View {
if(baseStore.isLoading == true){
ProgressView()
.scaleEffect(0.6, anchor: .center).progressViewStyle(ConfiguredProgressViewStyle())
}
}
}
struct HeaderView_Previews: PreviewProvider {
static var previews: some View {
return HeaderView()
.environmentObject(BaseStore());
}
}
struct MainView: View {
#EnvironmentObject var childStore: ChildStore
var body: some View {
}.onAppear(perform: {childStore.update()})
}

How do I update a text label in SwiftUI?

I have a SwiftUI text label and i want to write something in it after I press a button.
Here is my code:
Button(action: {
registerRequest() // here is where the variable message changes its value
}) {
Text("SignUp")
}
Text(message) // this is the label that I want to change
How do I do this?
With only the code you shared it is hard to say exactly how you should do it but here are a couple good ways:
The first way is to put the string in a #State variable so that it can be mutated and any change to it will cause an update to the view. Here is an example that you can test with Live Previews:
import SwiftUI
struct UpdateTextView: View {
#State var textToUpdate = "Update me!"
var body: some View {
VStack {
Button(action: {
self.textToUpdate = "I've been udpated!"
}) {
Text("SignUp")
}
Text(textToUpdate)
}
}
}
struct UpdateTextView_Previews: PreviewProvider {
static var previews: some View {
UpdateTextView()
}
}
If your string is stored in a class that is external to the view you can use implement the ObservableObject protocol on your class and make the string variable #Published so that any change to it will cause an update to the view. In the view you need to make your class variable an #ObservedObject to finish hooking it all up. Here is an example you can play with in Live Previews:
import SwiftUI
class ExternalModel: ObservableObject {
#Published var textToUpdate: String = "Update me!"
func registerRequest() {
// other functionality
textToUpdate = "I've been updated!"
}
}
struct UpdateTextViewExternal: View {
#ObservedObject var viewModel: ExternalModel
var body: some View {
VStack {
Button(action: {
self.viewModel.registerRequest()
}) {
Text("SignUp")
}
Text(self.viewModel.textToUpdate)
}
}
}
struct UpdateTextViewExternal_Previews: PreviewProvider {
static var previews: some View {
UpdateTextViewExternal(viewModel: ExternalModel())
}
}

Resources