SwiftUI Keyboard Toolbar Scope - ios

Say we have the following view of two text fields:
struct ContentView: View {
#State private var first = ""
#State private var second = ""
var body: some View {
VStack {
TextField("First", text: $first)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Test") { }
}
}
TextField("Second", text: $second)
}
}
}
The toolbar modifier is applied only to the "first" text field. My expectation is therefore that it only shows up on the keyboard, when the "first" text field is in focus.
What happens in practice though, it that it also shows up when the "second" text field is in focus.
Is this intended behaviour? And if so, how can I have different keyboard toolbars for different text fields?

The only thing that I've found so far that solves this problem works, but doesn't feel right. It also generates some layout constraint warnings in the console.
If you wrap each TextField in a NavigationView each `TextField will have its own context and thus its own toolbar.
Something like this:
struct ContentView: View {
#State private var first = ""
#State private var second = ""
var body: some View {
VStack {
NavigationView {
TextField("First", text: $first)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Test") { }
}
}
}
NavigationView {
TextField("Second", text: $second)
}
}
}
}

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

TextField is not moved back down with keyboard avoidance on

Does any know what I've done wrong here?
Problem
When I tap on the text field and the keyboard appears. Tap on the navgation link to get to the second screen. Then go back, the text field has not returned to the "non-focused" position.
iOS: 16
Sim: iPhone 14 Pro
Expectation
I am expecting to see the text field back at its original starting place. That is, when I tap the field, the keyboard avoidance causes that field to move up. Then I tap return on the keyboard to dismiss the keyboard, the text field returns to its starting position. When I navigation between view, I expect the same behaviour because the keyboard has been dismissed.
Steps
Tap the textField (assuming software keyboard is on)
Tap the navigationLink
Tap Back
import SwiftUI
struct ContentView: View {
#State private var text: String = "Hello, world!"
var body: some View {
NavigationView {
VStack {
NavigationLink("Hello", destination: { Text("World") })
TextField("Whoops", text: $text)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Try hide keyboard first and then navigate to the destination as following : -
#State private var text: String = "Hello, world!"
#State var action : Int? = 0
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("World"), tag: 1, selection: $action) {
EmptyView()
}
Button {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
action = 1
} label: {
Text("Hello")
}
TextField("Whoops", text: $text)
}
}
}
You should change the focus of your textField before pushing another View. Since iOS 15 we can user #FocusState which does exactly that:
#State private var text: String = "Hello, world!"
#State private var pushView:Bool = false
#FocusState private var isFocused: Bool
var body: some View {
NavigationView {
VStack {
Button {
isFocused = false
pushView = true
} label: {
Text("Hello")
}
NavigationLink(isActive: $pushView) {
Text("World")
} label: {
EmptyView()
}
TextField("Whoops", text: $text).onSubmit {
pushView = true
}.focused($isFocused)
}
}
}
As for the Still view you can try implementing .ignoresSafeArea(.keyboard)

SwiftUI Modal Inherits SearchBar during Sheet Presentation

Consider the following example with a list and a button wrapped in a HStack that opens up a sheet:
struct ContentView: View {
#State var text: String = ""
#State var showSheet = false
var body: some View {
NavigationView {
List {
HStack {
button
}
Text("Hello World")
}
.searchable(text: $text)
}
}
var button: some View {
Button("Press", action: { showSheet = true })
.sheet(isPresented: $showSheet) {
modalView
}
}
var modalView: some View {
NavigationView {
List {
Text("Test")
}
}
}
}
On press of the button, a modal is presented to the user. However, the searchable modifier gets passed to the modal, see this video.
Now if the HStack is removed, everything works fine:
List {
button
Text("Hello World")
}
In addition, everything works also fine if the modal is not a NavigationView:
var modalView: some View {
List {
Text("Test")
}
}
Does somebody know what the problem here might be or is it once again one of those weird SwiftUI bugs?
putting the sheet, outside of the button and the List, works for me. I think .sheet is not meant to be inside a List, especially where searchable is operating.
struct ContentView: View {
#State var text: String = ""
#State var showSheet = false
var body: some View {
NavigationView {
List {
HStack {
button
}
Text("Hello World")
}
.searchable(text: $text)
}
.sheet(isPresented: $showSheet) {
modalView
}
}
var button: some View {
Button("Press", action: { showSheet = true })
}
var modalView: some View {
NavigationView {
List {
Text("Test")
}
}
}
}
Another workaround is to use navigationBarHidden = true, but then you must live without the navigation bar in the sheet view.
var modalView: some View {
NavigationView {
List {
Text("Test")
}
.navigationBarHidden(true)
}
}
Btw, on iPadOS it helps to use .searchable(text: $text, placement: .sidebar)

SwiftUI updating #Binding value in NavigationLink(destination:label) causes destination to reappear

I have a weird issue, where I navigate to EditView and edit a state variable from ContentView. After that I dismiss EditView with presentation.wrappedValue.dismiss(). The problem is, as soon as the view is dismissed, it reappears again.
I'm using XCode 12.4 and my iOS deployment target is set to 14.4
Observations:
EditView doesn't reappear if the value isn't edited
removing the changed value Text("Value: \(title)") >> Text("Value") from ContentView tree resolves the issue, but that obviously isn't a solution.
moving the NavigationLink from .toolbar e.g.
VStack {
Text("Value: \(title)")
NavigationLink(destination: EditView(title: $title)){
Text("Edit")
}
}
also resolves the issue, but that seems like a hack. Besides, I'd like to keep using .toolbar because I like the .navigationTitle animation and I can't have a button in the upper-right corner of the screen if I have a navigation title without the toolbar.
Here's the full code:
import SwiftUI
struct ContentView: View {
#State var title: String = "Title"
#State var isActive: Bool = false
var body: some View {
NavigationView {
Text("Value: \(title)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: EditView(title: $title, isActive: $isActive), isActive: $isActive){
Text("Edit")
}
}
}
}
}
}
struct EditView: View {
#Environment(\.presentationMode) var presentation
#Binding var title: String
#Binding var isActive: Bool
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
// presentation.wrappedValue.dismiss()
isActive = false
}){
Text("Done")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
As far as I can tell this is a .toolbar bug, and if it turns out that way I'll report it to Apple, but in the meantime, does anyone have a better solution and/or explanation for this?
Cheers!
EDIT:
I updated the code with isActive value for the NavigationLink. It doesn't work when written like that, but uncommenting the commented out line makes it work. But that's quite hacky.
You are mixing up 2 things NavigationLink will push the view on stack, just like NavigationController in swift. That’s why you can see back button after navigating to second view. When you hit back button, it will pop top most view out of stack. presentationMode is not needed , dismissing presented view will not pop it of the stack.
To present a view and dismiss it you can check below code.
import SwiftUI
struct ContentViewsss: View {
#State var title: String = "Title"
#State var isPresented = false
var body: some View {
NavigationView {
Text("Value: \(title)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isPresented.toggle()
}){
Text("Edit")
}
}
}
}.sheet(isPresented: $isPresented, content: {
EditView(title: $title, state: $isPresented)
})
}
}
struct EditView: View {
#Binding var title: String
#Binding var state: Bool
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
state.toggle()
}){
Text("Done")
}
}
}
If you want NavigationLink functionality, you can just remove presentationMode code from second view, and keep ContentView as it is -:
struct EditView: View {
//#Environment(\.presentationMode) var presentation
#Binding var title: String
var body: some View {
Button(action: {
title = "\(Date().timeIntervalSince1970)"
// presentation.wrappedValue.dismiss()
}){
Text("Done")
}
}
}

Prevent SwiftUI from resetting #State

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

Resources