SwiftUI - Keyboard will show redraws parent view - ios

I have a parent view - First View which is part of a navigation stack.
I want to display modally a Second View which has some textfields.
When the Second View appears it looks okay , but the second I select one of the textfields the parent view redraws (making the isSecondViewPresented boolean false again and dismisses the Second View).
Any reason why that would happen ?
I added a check to see what object is changing , but the entire First View is just being redrawn without any other changes happening.
Code :
struct FirstView: View {
#StateObject var loginStatus = LoginStatus()
#State var passedObject: Object
…
.fullScreenCover(isPresented: $isSecondViewPresented, content: {
SecondView(passedObject: $passedObject).navigationBarHidden(true)
})
…
}
}
struct SecondVIew: View {
#State var text: String = ""
#Binding var passObject: Object
var body: some View {
VStack(spacing: 0) {
TextField.init("", text: $text)
Divider()
.frame(height: 1)
}
.padding(.bottom, 20)
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}

Related

iOS SwiftUI - NavigationLink open new view and close the current one

This is the Login() view:
struct Login: View {
#Environment(\.presentationMode) var presentationMode
var body: some View{
VStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "xmark")
.resizable()
.frame(width: 18, height: 18)
}
}
NavigationLink(destination: CreateAccount().navigationBarBackButtonHidden(true), label: {
Text("create account")
// and close the current view Login()
})
}
}
}
Is it possible to open a new view, in this case CreateAccount() and closing the current view Login()?
In order to do this, I would suggest skipping the NavigationView altogether, see here for more info. An example for your situation:
//You need an `ObservedObject` to do this, and a overall holder view
enum ViewStates{
//Declare possible views
case ContentView
case Login
case CreateAccount
}
//Then use an observableObject
class viewControl: ObservableObject{
#Published var currentView: ViewStates = .ContentView
}
//Finally, pass this into your views. Take a look at the second part of the tutorial I posted below for more info
//such as using `EnvironmentObject` and adding animation. Example implimentation below:
struct ControllerView: View{
#StateObject var controller: viewControl
var body: some View{
switch controller.currentView{
case .ContentView:
ContentView(controller: controller)
case .Login:
Login(controller: controller)
case .CreateAccount:
CreateAccount(controller: controller)
}
}
}
Next, you need to have #ObservedObject var controller: viewControl in all of your views. Note that you don't need a default statement in the switch clause, because the enum declares all possible values. The following is an example CreateAccount view. You also no longer need the dismiss - in fact, that will no longer work.
struct CreateAccount: View{
#ObservedObject var controller: viewControl
var body: some View{
//Content
Button("Dismiss"){
controller.currentView = .ContentView
}
}
}
This will allow you to switch the view by clicking. Instead of a NavigationLink in ContentView, do this:
Button{
controller.currentView = .CreateAccount
} label: {
Text("Create Account")
}
To go back, you just set the value again. This can also be expanded to show more views.
Second part of the tutorial

The data I pass to another view is not working

I'm new to swiftUI here and I want to try out to pass data between two views. But it doesn't seem to work.
I'm using Xcode 13.2 & iOS 15 for the simulator.
This is my code for the first view:
struct ContentView: View {
#State var myName: String = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter your name", text: $myName)
Text(self.myName)
NavigationLink(destination: BView(myName: self.$myName), label: {
Image(systemName: "arrowshape.turn.up.left")
})
}//: VSTACK
.padding(.horizontal, 20)
}//:NAVIGATION VIEW
}
}
This is code for the second view:
struct BView: View {
#Binding var myName: String
var body: some View {
NavigationView {
Text("BView")
Text(self.myName)
}//:NAVIGATION VIEW
}
}
I want myName to be input in the first page which is ContentView() and then pass down the input data to BView().
Unfortunately, once I run it on the simulator, the input data doesn't;t show up.
Your code is fine just add VStack in BView.
struct BView: View {
#Binding var myName: String
var body: some View {
NavigationView {
VStack { // HERE
Text("BView")
Text(self.myName)
}
}//:NAVIGATION VIEW
}
}
Please use #EnvironmentObject to pass the data to view.
https://developer.apple.com/documentation/swiftui/environmentobject

How to dismiss a presenting view to the root view of tab view in SwiftUI?

I'm using TabView on my home page. Let's just say I have 4 tabs.
On second tab, i can go to another view using NavigationLink and I go to another 2 views using NavigationLink. Then on the latest view, there is a button to present a view and i use .fullScreenCover (since I want to present it full screen).
In the presenting view, I add an X mark on the left side of the navigationBarItems to dismiss. I use #Environment(\.presentationMode) var presentationMode and presentationMode.wrappedValue.dismiss() to dismiss. But it only dismiss the presenting view to the previous view, while actually I want to dismiss it to the root of my view which is the 2nd tab of my TabView.
Is there a way to do this? Because I have looked up to some articles and nothing relevant especially in TabView context.
I also have a question tho:
Is it a right approach to use .fullScreenCover? Or is there another possible solution for example presenting a modal with full screen style (if there's any cause i'm not sure either).
Any suggestions will be very appreciated, thankyou in advance.
The presentationMode is one-level effect value, ie changing it you close one currently presented screen.
Thus to close many presented screens you have to implement this programmatically, like in demo below.
The possible approach is to use custom EnvironmentKey to pass it down view hierarchy w/o tight coupling of every level view (like with binding) and inject/call only at that level where needed.
Demo tested with Xcode 12.4 / iOS 14.4
struct ContentView: View {
var body: some View {
TabView {
Text("Tab1")
.tabItem { Image(systemName: "1.square") }
Tab2RootView()
.tabItem { Image(systemName: "2.square") }
}
}
}
struct Tab2RootView: View {
#State var toRoot = false
var body: some View {
NavigationView {
Tab2NoteView(level: 0)
.id(toRoot) // << reset to root !!
}
.environment(\.rewind, $toRoot) // << inject here !!
}
}
struct Tab2NoteView: View {
#Environment(\.rewind) var rewind
let level: Int
#State private var showFullScreen = false
var body: some View {
VStack {
Text(level == 0 ? "ROOT" : "Level \(level)")
NavigationLink("Go Next", destination: Tab2NoteView(level: level + 1))
Divider()
Button("Full Screen") { showFullScreen.toggle() }
.fullScreenCover(isPresented: $showFullScreen,
onDismiss: { rewind.wrappedValue.toggle() }) {
Tab2FullScreenView()
}
}
}
}
struct RewindKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var rewind: Binding<Bool> {
get { self[RewindKey.self] }
set { self[RewindKey.self] = newValue }
}
}
struct Tab2FullScreenView: View {
#Environment(\.presentationMode) var mode
var body: some View {
Button("Close") { mode.wrappedValue.dismiss() }
}
}
You have 2 options:
With .fullScreenCover you will have a binding that results in it being presented you can pass this binding through to the content and when the user taps on x set to to false
You can use the #Environment(\.presentationMode) var presentationMode then call presentationMode.wrappedValue.dismiss() in your button body.
Edit:
If you want to unwind all the way you should make the TabView be binding based. I like to use SceneStorage for this take a look at this post then you can access this SceneStorage value anywhere in your app to respond to it but also to update and change the navigation (this also has the benefit of providing you proper state restoration!)
If you make your TabView in this way:
struct ContentView: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
TabView(selection: $selectedTab) {
CarTrips()
.tabItem {
Image(systemName: "car")
Text("Car Trips")
}.tag(Tab.car)
TramTrips()
.tabItem {
Image(systemName: "tram.fill")
Text("Tram Trips")
}.tag(Tab.tram)
AirplaneTrips()
.tabItem {
Image(systemName: "airplane")
Text("Airplane Trips")
}.tag(Tab.airplaine)
}
}
}
enum Tab: String {
case car
case tram
case airplaine
}
Then deep within your app in the place you want to change the navigation you can create a button view.
struct ViewCarButton: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
Button("A Button") {
selectedTab = .car
}
}
}
This will forced the selected tab to be the car tab.
if instead of this you do not want to change tab but rather change what the navigation view is navigated to you can use the same concept for that, NavigationLink that's a binding if this binding is created using a #SceneStorage then in your ViewCarButton you can make changes to it that will change the navigation state.

How to create a view that works with #Binding. It should refresh itself while working with #Binding data

This question is identical to SwiftUI #Binding update doesn't refresh view, but the accepted answer is not applicable for my case.
The accepted answer says
A View using a #Binding will update when the underlying #State change, but the #State must be defined within the view hierarchy. (Else you could bind to a publisher)
In my case, the view hierarchy doesn't have the view which is having the #State. The view having the binding is presented modally to the user.
To summarize the issue again
I want to create a view, similar to Toggle which initializes from a Binding. This view will show the contents from the wrapped value and as it performs the updates, the original storage of the value will get updated automatically.
As I have learnt, updating the #Binding in a view, doesn't invalidate it. Then how to implement such a view.
Also I can't depend on the parent view to eventually update this view, because the view is shown on a modally presented screen.
I don't want to use workarounds like using a #State to explicitly trigger a refresh. So what is the correct way to implement such a view.
Code example
The view TextModifier takes a Binding. The view does some modifications to the view. For now it just appends "_Updated" to the value passed.
I initialize the view as TextModifier(text: <some_binding_var>)
struct TextModifier: View {
#Binding var text: String
var body: some View {
Text(text)
.onTapGesture {
text += "_Updated"
}
}
}
This view shows the text and on tapping it updates it in the original source, but as expected the view doesn't update itself on tapping.
So, how to implement this view so that it also updates itself when it updates the binding value.
The accepted answer to the linked question also says
Else you could bind to a publisher
I don't know how to do this. Does anybody know how to implement this and also provide a code example. Thanks.
Updated with full code and gif
struct ContentView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
List {
ForEach(viewModel.itemsList, id: \.self) { item in
ItemView(text: $viewModel.itemsList[getItemIndex(item)])
}
}
}
private func getItemIndex(_ item: String) -> Int {
viewModel.itemsList.firstIndex { $0 == item }!
}
}
class TestViewModel: ObservableObject {
#Published var itemsList = ["Item 1", "Item 2", "Item 3"]
}
struct ItemView: View {
#Binding var text: String
#State private var showEditorView = false
var body: some View {
Text(text)
.onTapGesture {
showEditorView = true
}
.sheet(isPresented: $showEditorView) {
TextModifier(text: $text, showView: $showEditorView)
}
}
}
struct TextModifier: View {
#Binding var text: String
#Binding var showView: Bool
var body: some View {
VStack(spacing: 20) {
Text("Tap on the text to update it")
.foregroundColor(.blue)
Text(text)
.onTapGesture {
text += "_Updated"
}
Button {
showView = false
} label: {
Text("Dismiss")
.foregroundColor(.blue)
}
}
}
}

How it is possible to dismiss a view from a subtracted subview in SwiftUI

Whenever my code gets too big, SwiftUI starts acting weird and generates an error:
"The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
So I started breaking up my code into Extracted Subviews, one of the problems I came across is how to dismiss a view from a subtracted subview.
Example: we have here LoginContentView this view contains a button when the button is clicked it will show the next view UsersOnlineView.
struct LoginContentView: View {
#State var showUsersOnlineView = false
var body: some View {
Button(action: {
self.showUsersOnlineView = true
}) {
Text("Show the next view")
}
.fullScreenCover(isPresented: $showUsersOnlineView, content: {
UsersOnlineView()
})
}
On the other hand, we have a button that is extracted to subview, to dismiss the modal and go back to the original view:
import SwiftUI
struct UsersOnlineView: View {
var body: some View {
ZStack {
VStack {
CloseViewButton()
}
}
}
}
struct CloseViewButton: View {
var body: some View {
Button(action: {
// Close the Modal
}) {
Text("Close the view")
}
}
}
Give the sbview the state property that defines if the view is shown.
struct CloseViewButton: View {
#Binding var showView: Bool
var body: some View {
Button(
ShowView = false
}) {
Text("Close the view")
}
}
}
When you use the sub view give it the property
CloseButtonView(showView: $showOnlineView)
To allow the sub view to change the isShown property it needs to get a binding.
On the presentation mode. I think this only works with Swiftui presentations like sheet and alert.
The simplest solution for this scenario is to use presentationMode environment variable:
struct CloseViewButton: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Close the view")
}
}
}
Tested with Xcode 12.1 / iOS 14.1

Resources