When a property value is changed in a class via #Published, some of the observedObjects wont get updated? - ios

These are all the codes.
the SurveyQuestion class
class SurveyQuestion: ObservableObject {
#Published var text: String
init() {
print("Initialising now ...")
self.text = "HELLO"
changeText()
}
func changeText() {
print("Changing Text Now from \(text).. ")
if self.text == "HELLO"{
self.text = "BYE"
}
else{
self.text = "HELLO"
}
print("to \(self.text) \n")
}
}
SubView.swift
struct SubView: View {
#ObservedObject var someOtherClass = SurveyQuestion()
var body: some View {
Text("Text now is \(someOtherClass.text)")
}
}
ContentView.swift
struct ContentView: View {
#ObservedObject var someClass = SurveyQuestion()
var body: some View {
VStack{
Button(action: {
print("Changing Text Now !")
self.someClass.changeText()
}) {
Text("Change Text ")
}
Text("Text now is \(someClass.text)")
SubView()
}
}
}
Whenever I click 'changeText' button, it changes the text in Text("Text now is (someClass.text)") but not Subview(). They should all be updated with the same text change.
Any idea what went wrong here?

To make it work you should use same instance of ObservableObject, like below
struct SubView: View {
#ObservedObject var someOtherClass: SurveyQuestion // to be injected
var body: some View {
Text("Text now is \(someOtherClass.text)")
}
}
struct ContentView: View {
#ObservedObject var someClass = SurveyQuestion() // created
var body: some View {
VStack{
Button(action: {
print("Changing Text Now !")
self.someClass.changeText()
}) {
Text("Change Text ")
}
Text("Text now is \(someClass.text)")
SubView(someOtherClass: someClass) // << injected
}
}
}

Your someClass and someOtherClass are two completely different objects. Changing one has no effect on the other. If this was supposed to be a singleton observable that could affect different views simultaneously, you wanted an environment object.

Related

How to properly implement a global variable in SwiftUI

I am going to create a SwiftUI application where I want to be able to swap between 3 modes. I am trying EnvironmentObject without success. I am able to change the view displayed locally, but from another View (in the end will be a class) I get a
fatal error: No ObservableObject of type DisplayView found. A View.environmentObject(_:) for DisplayView may be missing as an ancestor of this view.
Here is my code. The first line of the ContentView if/else fails.
enum ViewMode {
case Connect, Loading, ModeSelection
}
class DisplayView: ObservableObject {
#Published var displayMode: ViewMode = .Connect
}
struct ContentView: View {
#EnvironmentObject var viewMode: DisplayView
var body: some View {
VStack {
if viewMode.displayMode == .Connect {
ConnectView()
} else if viewMode.displayMode == .Loading {
LoadingView()
} else if viewMode.displayMode == .ModeSelection {
ModeSelectView()
} else {
Text("Error.")
}
TestView() //Want this to update the var & change UI.
}
.environmentObject(viewMode)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(DisplayView())
}
}
//FAILS
struct TestView: View {
#EnvironmentObject var showView: DisplayView
var body: some View {
HStack {
Button("-> load") {
self.showView.displayMode = .Loading
}
}
}
}
struct ConnectView: View {
var body: some View {
Text("Connect...")
}
}
struct LoadingView: View {
var body: some View {
Text("Loading...")
}
}
struct ModeSelectView: View {
var body: some View {
Text("Select Mode")
}
}
I would like to be able to update DisplayView from anywhere and have the ContentView UI adapt accordingly. I can update from within ContentView but I want to be able update from anywhere and have my view change.
I needed to inject BEFORE - so this fixed things up:
#main
struct fooApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DisplayView()) //super key!
}
}
}
I also tried a Singleton class to store some properties - and thus they are available from anywhere and can be updated anywhere - without having to declare EnvironmentObject. It's just another way that can work in different circumstances.
class PropContainerModel {
public var foo = "Hello"
static let shared = PropContainerModel()
private override init(){}
}
And then somewhere else
let thisFoo = PropContainerModel.shared.foo
//
PropContainerModel.shared.foo = "There"
Update here (Singleton but changes reflect in the SwiftUI UI).
class PropContainerModel: ObservableObject
{
#Published var foo: String = "Foo"
static let shared = PropContainerModel()
private init(){}
}
struct ContentView: View
{
#ObservedObject var propertyModel = PropContainerModel.shared
var body: some View {
VStack {
Text("foo = \(propertyModel.foo)")
.padding()
Button {
tapped(value: "Car")
} label: {
Image(systemName:"car")
.font(.system(size: 24))
.foregroundColor(.black)
}
Spacer()
.frame(height:20)
Button {
tapped(value: "Star")
} label: {
Image(systemName:"star")
.font(.system(size: 24))
.foregroundColor(.black)
}
}
}
func tapped(value: String)
{
PropContainerModel.shared.foo = value
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI view parameter does not update as expected

I am curious why this .fullScreenCover display of a view does not update properly with a passed-in parameter unless the parameter is using the #Binding property wrapper. Is this a bug or intended behavior? Is this the fact that the view shown by the fullScreenCover is not lazily generated?
import SwiftUI
struct ContentView: View {
#State private var showFullScreen = false
#State private var message = "Initial Message"
var body: some View {
VStack {
Button {
self.message = "new message"
showFullScreen = true
} label: {
Text("Show Full Screen")
}
}.fullScreenCover(isPresented: $showFullScreen) {
TestView(text: message)
}
}
}
struct TestView: View {
var text: String
var body: some View {
Text(text)
}
}
There is a different fullScreenCover for passing in dynamic data, e.g.
import SwiftUI
struct CoverData: Identifiable {
var id: String {
return message
}
let message: String
}
struct FullScreenCoverTestView: View {
#State private var coverData: CoverData?
var body: some View {
VStack {
Button {
coverData = CoverData(message: "new message")
} label: {
Text("Show Full Screen")
}
}
.fullScreenCover(item: $coverData, onDismiss: didDismiss) { item in
TestView(text: item.message)
.onTapGesture {
coverData = nil
}
}
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct TestView: View {
let text: String
var body: some View {
Text(text)
}
}
More info and an example in the docs:
https://developer.apple.com/documentation/SwiftUI/AnyView/fullScreenCover(item:onDismiss:content:)

How to pass data between ViewModels in SwiftUI

I have this use case where I have a parent view and a child view. Both of the views have their own corresponding ViewModels.
ParentView:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView()) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}
}
ParentViewModel:
class ParentViewModel: ObservableObject {
#Published var newListName: String = ""
func saveList() {
// some logic to save to CoreData, method would be called via a button
// how do I reference "someString" from ChildViewModel in this ViewModel?
}
}
ChildView:
struct ChildView: View {
#StateObject var childViewModel = ChildViewModel()
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
childViewModel.alterData()
}
}
}
}
}
ChildViewModel:
class ChildViewModel: ObservableObject {
#Published var someString: String = ""
func alterData() {
someString = "Toast"
}
}
My question now is, how do I pass the new value of "someString" from ChildViewModel into the ParentViewModel, in order to do some further stuff with it?
I've tried to create a #StateObject var childViewModel = ChildViewModel() reference in the ParentViewModel, but that does obviously not work, as this will create a new instance of the ChildViewModel and therefore not know of the changes made to "someString"
Solution:
As proposed by Josh, I went with the approach to use a single ViewModel instead of two. To achieve this, the ParentView needs a .environmentObject(T) modifier.
ParentView:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView()) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}.environmentObject(parentViewModel)
}
The ChildView then references that environment Object via #EnvironmentObject without an initializer:
struct ChildView: View {
#EnvironmentObject var parentViewModel: ParentViewModel
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
parentViewModel.alterData()
}
}
}
}
}
Most likely you would use a binding for this situation:
struct ChildView: View {
#Binding var name: String
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
name = "Altered!"
}
}
}
}
}
And in the parent:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView(name: $parentViewModel.newListName)) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}
}
Also, I think you can remove the NavigationView view from ChildView. Having it ParentView is enough.

How to pass value form view to view model in SwiftUI?

I am having one email filed in view, that value I want to pass to viewModel. But I am not sure how.
Email View
import SwiftUI
struct EmailView: View {
#State private var email: String = ""
init(viewModel:EmailViewModel) {
self.viewModel = viewModel
}
TextField("", text: $email)
func sendButtonAction() {
viewModel.updateDataToServer()
}
}
Email Viewmodel
import Foundation
import SwiftUI
class EmailViewModel: NSObject, ObservableObject {
var emailText = ""
convenience init(emailText: String) {
self.init()
self.emailText = emailText
}
func updateDataToServer() {
print("Show email text" + emailText). // not getting email value here???
}
}
I am coming to email screen from other screen. How should I pass email value form here?
NavigationLink(destination: EmailView(viewModel: EmailViewModel(emailText: "")), isActive: $pushToMail) {
EmptyView()
}.hidden()
Try this type of approach as shown in the example code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var emailModel = EmailViewModel()
var body: some View {
NavigationView {
VStack (spacing: 55) {
NavigationLink(destination: EmailView(viewModel: emailModel)) {
Text("go to EmailView")
}
Text("email was: \(emailModel.emailText)")
}
}.navigationViewStyle(.stack)
}
}
class EmailViewModel: ObservableObject {
#Published var emailText = ""
func updateDataToServer() {
print("----> Show email text " + emailText)
}
}
struct EmailView: View {
#ObservedObject var viewModel: EmailViewModel
var body: some View {
TextField("Enter your email", text: $viewModel.emailText).border(.green)
.onSubmit {
viewModel.updateDataToServer()
}
}
}

SwiftUI #State not triggering update when value changed by separate method

I have this (simpilied) section of code for a SwiftUI display:
struct ContentView: View {
private var errorMessage: String?
#State private var showErrors: Bool = false
var errorAlert: Alert {
Alert(title: Text("Error!"),
message: Text(errorMessage ?? "oops!"),
dismissButton: .default(Text("Ok")))
}
init() {}
var body: some View {
VStack {
Text("Hello, World!")
Button(action: {
self.showErrors.toggle()
}) {
Text("Do it!")
}
}
.alert(isPresented: $showErrors) { errorAlert }
}
mutating func display(errors: [String]) {
errorMessage = errors.joined(separator: "\n")
showErrors.toggle()
}
}
When the view is displayed and I tape the "Do it!" button then the alert is displayed as expected.
However if I call the display(errors:...) function the error message is set, but the display does not put up an alert.
I'm guessing this is something to do with the button being inside the view and the function being outside, but I'm at a loss as to how to fix it. It should be easy considering the amount of functionality that any app would have that needs to update a display like this.
Ok, some more reading and a refactor switched to using an observable view model like this:
class ContentViewModel: ObservableObject {
var message: String? = nil {
didSet {
displayMessage = message != nil
}
}
#Published var displayMessage: Bool = false
}
struct ContentView: View {
#ObservedObject private var viewModel: ContentViewModel
var errorAlert: Alert {
Alert(title: Text("Error!"), message: Text(viewModel.message ?? "oops!"), dismissButton: .default(Text("Ok")))
}
init(viewModel: ContentViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Button(action: {
self.viewModel.displayMessage.toggle()
}) {
Text("Do it!")
}
}
.alert(isPresented: $viewModel.displayMessage) { errorAlert }
}
}
Which is now working as expected. So the takeaway from this is that using observable view models is more useful even in simpler code like this.

Resources