Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I have this class
class StoreWrapper:ObservableObject {
#Published var displaySpinner: Bool = true
}
and I am trying to use this var to control the visibility of a spinner on ContentView, like this
CONTENT VIEW
private var storeWrapper = StoreWrapper.sharedInstance
var body: some View {
if storeWrapper.displaySpinner {
Spinner()
}
I can change the displaySpinner variable to false and the spinner will not hide.
But if I do this:
CONTENT VIEW
#State var displaySpinner = true
var body: some View {
if displaySpinner {
Spinner()
}
and change displaySpinner to false, the spinner will hide.
Any ideas?
Try using #StateObject to watch the property. Here's the example I set up to test it.
struct ContentView: View {
#StateObject var displayWrapper = StoreWrapper()
var body: some View {
VStack {
if displayWrapper.displaySpinner {
Text("I'm spinning")
} else {
Text("No spins here.")
}
Button("Toggle Spinner") {
displayWrapper.displaySpinner.toggle()
}
}
}
}
You need to let the View know that the storeWrapper is an observed object.
This line:
private var storeWrapper = StoreWrapper.sharedInstance
should be:
#ObservedObject private var storeWrapper = StoreWrapper.sharedInstance
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 11 months ago.
Improve this question
Good afternoon!
I have two Views
LoginView and ProfileView
LoginView has a number of values:
#State private var email: String = "Email"
#State private var password: String = "Password"
#State private var showToggleSignup: Bool = false
#State private var showToggleLogin: Bool = true
#State private var showToggleReset: Bool = false
#State private var show Profile View: Bool = false
These values are changed by clicking on various buttons in the LoginView itself for example by:
Button {
showToggleLogin.toggle()
showToggleReset.toggle()
} label: {
Gradient Button(text: "forgot pass?")
.frame(maxWidth: .infinity, alignment: .leading)
.opacity(showToggleLogin ? 0 : 1)
}
How can I change these values for LoginView while in Profile View?
I would really appreciate your help!
I need to change the showProfile in View 2 from false to true while in View 1
Maybe I don't understand something, but I'm just learning!
If you have an #State variable in one view and you want it changed in another view, you define the variable as an #Binding in that other view and pass it on.
So in the login view you have:
struct LoginView: View {
#State private var showToggleSignup: Bool = false
var body: some View {
ProfileView(showToggleSignp: $showTogglesignup)
}
}
Then the profile view looks like:
struct ProfileView: View {
#Binding var showToggleSignup: Bool
var body: some View {
// Do whatever you need to do
}
}
Good luck,
MacUserT
If you have a lot of variables like above you can also bundle them in an Observed Object, a class which conforms to the ObservableObject protocol. There you #Publish the variables. Use .enviroment to bypass these object or declare the Object with #EnviromentObject. There are a lot of tutorials for that out there, like
https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject
We realized today that our app was targeting iOS 14, and changed it to iOS 13.
We found out we are not able to use StateObject on iOS 13, and some problems arose. This is what we have:
AlertState.swift
final class CardState: ObservableObject {
static let shared = CardState()
#Published var shouldShowCard = false
private init() {}
// Some other methods and variables
}
Then, we use it like this:
ContentView.swift
struct ContentView: View {
#StateObject var cardState = CardState.shared
var body: some View {
ZStack(alignment: .center) {
if cardState.shouldShowCard {
Card()
}
}
}
}
Card.swift
struct Card: View {
#StateObject var cardState = CardState.shared
var body: some View {
// View
}
}
AlertState holds more data, such as the text we show in the card. The card can be triggered from any screen of the app.
So, we targeted iOS 13 and replaced StateObject with ObservedObject, but then it stopped animating when the card gets hidden by switching shouldShowCard to false, the View just disappears.
What should we use in order to achieve what we used to have when using StateObject? We are a bit lost and tried everything we found.
Thanks in advance.
Here's possible solution. We are wrapping ObservedObjects in a views with #State.
struct ViewModelWrapper<V: View, ViewModel: ObservableObject>: View {
private let contentView: V
#State private var contentViewModel: ViewModel
init(contentView: #autoclosure () -> V, vm: #autoclosure () -> ViewModel) {
self._contentViewModel = State(initialValue: vm())
self.contentView = contentView()
}
var body: some View {
contentView
.environmentObject(contentViewModel)
}
}
#State preserves model object from recreation while .environmentObject allows to pass exactly the same model every time view re-renders.
I recently encountered the following problem in SwiftUI with ObservedObject/ObservableObject:
If the ObservedObject/ObservableObject is publishing in a View, the body property is recalculated - as expected.
But if there is a sub View in the body property which also has an ObservedObject, the View, strangely enough, not only recalculates the body property of the sub View, but the entire object.
The state of the ObservedObject is of course lost from the sub View.
Of course, you can prevent this by adding the ObservableObject to the sub View through .environmentObject(), but I don't think that's the best solution, especially with more complex view hierarchies.
Here an example Code:
struct ContentView: View {
#ObservedObject var contentViewModel: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
Button {
self.contentViewModel.counter += 1
} label: {
Text(String(self.contentViewModel.counter))
}
SubView()
}
}
}
class ContentViewModel: ObservableObject {
#Published var counter: Int = 0
}
And the sub View:
struct SubView: View {
#ObservedObject var subViewModel: SubViewModel = SubViewModel()
var body: some View {
Button {
self.subViewModel.counter += 1
} label: {
Text(String(self.subViewModel.counter))
}
}
}
class SubViewModel: ObservableObject {
#Published var counter: Int = 0
}
And here how the sample Code looks/works:
The last weird thing I realised, this is only the case if you use Observed Object. If I would replace in the sub View #ObservedObject var subViewModel: SubViewModel = SubViewModel() with #State var counter: Int = 0 it is working fine again and the state is preserved.
Maybe im missing out on something, but im really confused. Im very grateful for any answers and solutions. If you have further questions fell free to leave a comment, I will answer it within 24h (as long as the question is open).
Declare your ViewModel with #StateObject. #StateObject is not recreated for every view re-render more
struct SubView: View {
#StateObject var subViewModel: SubViewModel = SubViewModel() //<--- Here
var body: some View {
Button {
self.subViewModel.counter += 1
} label: {
Text(String(self.subViewModel.counter))
}
}
}
I found a perfect solution:
Either you replace the #ObservedObject with #StateObject (thx #Raja Kishan) in iOS 14 (SwiftUI v2.0) or you can create a Wrapper for the View and simulate the behaviour of #StateObject in iOS 13 (SwiftUI v1.0):
struct SubViewWrapper: View {
#State var subViewModel: SubViewModel = SubViewModel()
var body: some View {
SubView(subViewModel: self.subViewModel)
}
}
and then use SubViewWrapper() in ContentView instead of SubView()
struct ContentView: View {
#ObservedObject var contentViewModel: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
Button {
self.contentViewModel.counter += 1
} label: {
Text(String(self.contentViewModel.counter))
}
SubViewWrapper()
}
}
}
Why can't optional be assigned?
Index has been allocated, but still no value is displayed
Help me, Thank you!
struct TestView: View {
#State private var index: Int? = nil
#State private var show: Bool = false
var body: some View {
VStack {
Text("Hello, world!")
.onTapGesture {
self.index = 1
self.show = true
print(self.index as Any)
}
}
.fullScreenCover(isPresented: $show) {
if let index = self.index {
Text("value:\(index)")
} else {
Text("not value")
}
}
}
}
Xcode Version 12.0 beta 2
SwiftUI relies upon the #State variable causing the body getter to be recalculated when it changes. For this to work, the body getter must depend in certain definite ways on the #State variable. The problem in your code is that it doesn't.
To see this, we can reduce your code to a simpler example:
struct ContentView: View {
#State var message = "Hey"
#State var show: Bool = false
var body: some View {
VStack {
Button("Test") {
message = "Ho"
show = true
}
}
.sheet(isPresented: $show) {Text(message)}
}
}
We change message to Ho, but when the sheet is presented, it still says Hey. This is because nothing happened to make the body recalculate. You might say: What about the phrase Text(message)? Yes, but that's in a closure; it has already been calculated, and message has already been captured.
To see that what I'm saying is right, just add a Text displaying message directly to the main interface:
struct ContentView: View {
#State var message = "Hey"
#State var show: Bool = false
var body: some View {
VStack {
Button("Test") {
message = "Ho"
show = true
}
Text(message)
}
.sheet(isPresented: $show) {Text(message)}
}
}
Now your code works! Of course, we are also displaying an unwanted Text in the interface, but the point is, that plain and simple Text(message), not in a closure, is sufficient to cause the whole body getter to be recalculated when message changes. So we have correctly explained the phenomenon you're asking about.
So what's the real solution? How can we get the content closure to operate as we expect without adding an extra Text to the interface? One way is like this:
struct ContentView: View {
#State var message = "Hey"
#State var show: Bool = false
var body: some View {
VStack {
Button("Test") {
message = "Ho"
show = true
}
}
.sheet(isPresented: $show) {[message] in Text(message)}
}
}
By including message in the capture list for our closure, we make the body getter depend on the message variable, and now the code behaves as desired.
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).