Prevent SwiftUI from resetting #State - ios

I don't often understand when SwiftUI resets the state of a view (i.e. all that is marked with #State). For example, take a look at this minimum example:
import SwiftUI
struct ContentView: View {
#State private var isView1Active = true
let view1 = View1()
let view2 = View2()
var body: some View {
VStack {
if isView1Active {
view1
} else {
view2
}
Button(action: {
self.isView1Active.toggle()
}, label: {
Text("TAP")
})
}
}
}
struct View1: View {
#State private var text = ""
var body: some View {
TextField("View1: type something...", text: $text)
}
}
struct View2: View {
#State private var text = ""
var body: some View {
TextField("View2: type something...", text: $text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'd want the two TextField to keep their content, but if you run this example some weird behaviours occur:
If you run the example on the preview only the View1 TextField content persists:
If you, instead, run the example on the simulator (or on an actual device) neither the first textfield content, nor the second one persist:
So, what's happening here? Is there a way to tell SwiftUI not to reset #State for a view? Thanks.

The issue is that View1 and View2 are being recreated every time isView1Active is changed (because it is using #State which reloads the body of ContentView).
A solution would be to keep the text properties of the TextFields in the ContentView as shown below and use #Binding:
struct ContentView: View {
#State private var isView1Active = true
#State private var view1Text = ""
#State private var view2Text = ""
var body: some View {
VStack {
if isView1Active {
View1(text: $view1Text)
} else {
View2(text: $view2Text)
}
Button(action: {
self.isView1Active.toggle()
}, label: {
Text("TAP")
})
}
}
}
struct View1: View {
#Binding var text: String
var body: some View {
TextField("View1: type something...", text: $text)
}
}
struct View2: View {
#Binding var text: String
var body: some View {
TextField("View2: type something...", text: $text)
}
}
Shown in action:

It view1 and view2 are completely independent and enclosure, like there is no contextmenuor sheet, you may use ZStack and opacity combinations.
var body: some View {
VStack {
ZStack{
if isView1Active {
view1.opacity(1)
view2.opacity(0)
} else {
view1.opacity(0)
view2.opacity(1)
}}
Button(action: {
self.isView1Active.toggle()
}, label: {
Text("TAP")
})
}
}

Related

SwiftUI - How to focus a TextField within a sheet as it is appearing?

I have a search TextField within a View that is triggered to appear within a sheet on top of the ContentView.
I'm able to automatically focus this TextField when the sheet appears using #FocusState and onAppear, however, I'm finding that the sheet needs to fully appear before the TextField is focused and the on screen keyboard appears.
This feels quite slow and I've noticed in many other apps that they are able to trigger the on screen keyboard and the sheet appearing simultaneously.
Here is my code:
struct ContentView: View {
#State var showSearch = false
var body: some View {
Button {
showSearch = true
} label: {
Text("Search")
}
.sheet(isPresented: $showSearch) {
SearchView()
}
}
}
struct SearchView: View {
#State var searchTerm = ""
#FocusState private var searchFocus: Bool
var body: some View {
TextField("Search", text: $searchTerm)
.focused($searchFocus)
.onAppear() {
searchFocus = true
}
}
}
Is there a different way to do this that will make the keyboard appear as the sheet is appearing, making the overall experience feel more seamless?
Here is an approach with a custom sheet that brings in the keyboard somewhat earlier. Not sure if its worth the effort though:
struct ContentView: View {
#State var showSearch = false
var body: some View {
ZStack {
Button {
withAnimation {
showSearch = true
}
} label: {
Text("Search")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
if showSearch {
SearchView(isPresented: $showSearch)
.transition(.move(edge: .bottom))
}
}
// .sheet(isPresented: $showSearch) {
// SearchView()
// }
}
}
struct SearchView: View {
#Binding var isPresented: Bool
#State var searchTerm = ""
#FocusState private var searchFocus: Bool
var body: some View {
Form {
TextField("Search", text: $searchTerm)
.focused($searchFocus)
Button("Close") {
searchFocus = false
withAnimation {
isPresented = false
}
}
}
.onAppear() {
searchFocus = true
}
}
}

Background doesn't show up in the View

I have ready view with background but when i call it here, i don't see it. What should i do?
When i deleted Form{}, my background appeared.
import SwiftUI
struct HomeView: View {
#State private var salaryPh: String = "" // should be int
var body: some View {
NavigationView {
ZStack {
BackgroundView()
Form {
Section(header: Text("Your netto-salary per hour")) {
TextField("My salary is...", text: $salaryPh)
}
}
}
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
The opaque Form is on top of the BackgroundView, so hides it.
To make your BackgroundView visible, just add
.scrollContentBackground(.hidden)
to your From, e.g.
struct ContentView: View {
#State private var salaryPh: String = "" // should be int
var body: some View {
NavigationView {
ZStack {
BackgroundView()
Form {
Section(header: Text("Your netto-salary per hour")) {
TextField("My salary is...", text: $salaryPh)
}
}
.scrollContentBackground(.hidden)
}
}
}
}
struct BackgroundView: View {
var body: some View {
Color.red
}
}

Can't go back to InitialView with Modal transition

I'm a beginner of SwiftUI. (Sorry in advance if my English is hard to understand.)
I want to enable multiple screen transitions using Modal.
【Go back to the initial view when the view reached to the last View and pressed the button】
That's what I want to realize.
I thought my code would work perfectly but when I pressed the button of last view it stopped at the second view, not the initial view.
Can't figure out what's wrong and where to fix, any solutions?
Here's my code.
`
import SwiftUI
struct ContentView: View {
#State var isShowSecondView = false
var body: some View {
Button("To SecondView") {
isShowSecondView = true
}
.font(.largeTitle)
.fullScreenCover(isPresented: $isShowSecondView) {
SecondView(isShowSecondView: $isShowSecondView)
}
}
}
struct SecondView: View {
#Binding var isShowSecondView: Bool
#State var isShowThirdView = false
var body: some View {
Button("To ThirdView") {
isShowThirdView = true
}
.font(.largeTitle)
.fullScreenCover(isPresented: $isShowThirdView) {
ThirdView(isShowNextView: $isShowSecondView,
isShowThirdView: $isShowThirdView)
}
}
}
struct ThirdView: View {
#Binding var isShowNextView: Bool
#Binding var isShowThirdView: Bool
#State var isShowForthView = false
var body: some View {
Button("To ForthView") {
isShowForthView = true
}
.font(.largeTitle)
.fullScreenCover(isPresented: $isShowForthView) {
ForthView(isShowNextView: $isShowNextView,
isShowThirdView: $isShowThirdView, isShowForthView: $isShowForthView)
}
}
}
struct ForthView: View {
#Binding var isShowNextView: Bool
#Binding var isShowThirdView: Bool
#Binding var isShowForthView: Bool
var body: some View {
Button("Back to FirstView") {
isShowNextView = false
isShowThirdView = false
isShowForthView = false
}
.font(.largeTitle)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
`

SwiftUI: detecting the NavigationView back button press

In SwiftUI I couldn't find a way to detect when the user taps on the default back button of the navigation view when I am inside DetailView1 in this code:
struct RootView: View {
#State private var showDetails: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
}
}
}
struct DetailView1: View {
#State private var showDetails: Bool = false
var body: some View {
NavigationLink(destination: DetailView2(), isActive: $showDetails) {
Text("show DetailView2")
}
.navigationBarTitle("DetailView1")
}
}
struct DetailView2: View {
var body: some View {
Text("")
.navigationBarTitle("DetailView2")
}
}
Using .onDisappear doesn't solve the problem as its closure is called when the view is popped off or a new view is pushed.
The quick solution is to create a custom back button because right now the framework have not this possibility.
struct DetailView : View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body : some View {
Text("Detail View")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
self.mode.wrappedValue.dismiss()
}){
Image(systemName: "arrow.left")
})
}
}
As soon as you press the back button, the view sets isPresented to false, so you can use an observer on that value to trigger code when the back button is pressed. Assume this view is presented inside a navigation controller:
struct MyView: View {
#Environment(\.isPresented) var isPresented
var body: some View {
Rectangle().onChange(of: isPresented) { newValue in
if !newValue {
print("detail view is dismissed")
}
}
}
}
An even nicer (SwiftUI-ier?) way of observing the published showDetails property:
struct RootView: View {
class ViewModel: ObservableObject {
#Published var showDetails = false
}
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
.onReceive(self.viewModel.$showDetails) { isShowing in
debugPrint(isShowing)
// Maybe do something here?
}
}
}
}
Following up on my comment, I would react to changes in the state of showDetails. Unfortunately didSet doesn't appear to trigger with #State variables. Instead, we can use an observable view model to hold the state, which does allow us to do intercept changes with didSet.
struct RootView: View {
class ViewModel: ObservableObject {
#Published var showDetails = false {
didSet {
debugPrint(showDetails)
// Maybe do something here?
}
}
}
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
Text("show DetailView1")
}
}
.navigationBarTitle("RootView")
}
}
}

Share value between child and parent view in SwiftUI MVVM

I'm trying to write an app using SwiftUI and the MVVM architecture. I understand how a view is automatically updated when its view model changes, but I don't get how I can access that view model in a parent view. For example, I have this view which contains a text field with a red background:
class CustomTextFieldViewModel: ObservableObject {
#Published var text: String = "abc"
}
struct CustomTextField: View {
#ObservedObject var viewModel: CustomTextFieldViewModel
var body: some View {
ZStack {
Color.red
.frame(height: 50)
TextField("Enter text...", text: $viewModel.text)
}
}
}
Then I have my main content view with an instance of this CustomTextField as well as a Text that's supposed to reference the text field's text:
class ContentViewModel: ObservableObject {
#Published var textFieldModel = CustomTextFieldViewModel()
}
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
VStack {
CustomTextField(viewModel: $viewModel.textFieldModel)
TextField("type", text: $viewModel.textFieldModel.text)
}
}
}
When I first launch the app, the Text label does show the initial value of "abc", but when I type in the text field this doesn't update.
How can I "synchronise" these both?
Here is fix
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
VStack {
CustomTextField(viewModel: viewModel.textFieldModel) // << fix !!
Text("\(viewModel.textFieldModel.text)")
}
}
}
I ended up fixing it by doing this:
class ContentViewModel: ObservableObject {
#Published var textFieldModel = CustomTextFieldViewModel()
}
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
VStack {
CustomTextField(viewModel: $viewModel.textFieldModel)
Text("\($viewModel.textFieldModel.text.wrappedValue)") //<< wrappedValue
}
}
}
and
class CustomTextFieldViewModel: ObservableObject {
#Published var text: String = "abc"
}
struct CustomTextField: View {
#Binding var viewModel: CustomTextFieldViewModel //<< #Binding
var body: some View {
ZStack {
Color.red
.frame(height: 50)
TextField("Enter text...", text: $viewModel.text)
}
}
}

Resources