How do I update a text label in SwiftUI? - ios

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

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

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

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

SwiftUI Using ObservableObject in View on New Sheet

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

Resources