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

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

Related

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 isSearching dismiss behaviour on iOS 15

I have a SwiftUI view with a search bar on iOS 15. When the search bar is activated, a search list is presented, when no search is active, a regular content view is shown.
The problem I am facing is that when I activate a navigation link from the search list, when the navigation starts to take effect, the isSearching flag is turned to false and the regular content view is shown, even though I would want to search to stay active, just like when we would have a list/table and the user would select a row: the search stays active, and when the user navigates back, the search results are still displayed.
Is there a way in SwiftUI to control how the isSearching is changed?
I put together a small sample project that demoes the problem:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: ContentView.ViewModel
var body: some View {
NavigationView {
VStack {
ContentViewWrapper(viewModel: viewModel)
}
.navigationTitle("Searchable")
.navigationBarTitleDisplayMode(.inline)
.searchable(text: $viewModel.searchString, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search")
.navigationViewStyle(.stack)
.edgesIgnoringSafeArea(.top)
}
}
}
// MARK: View model for the content view
extension ContentView {
class ViewModel: ObservableObject {
#Published var isShowingDestinationScreen = false
#Published var isSearching = false
#Published var searchString = ""
func buttonTapped() {
if !isShowingDestinationScreen {
isShowingDestinationScreen = true
}
}
func isSearchingHasChanged(newValue: Bool) {
if isSearching != newValue {
isSearching = newValue
}
}
}
}
// MARK: Wrapper for the content view so it can be used with the searchable API
struct ContentViewWrapper: View {
#ObservedObject var viewModel: ContentView.ViewModel
#Environment(\.isSearching) var isSearching
var body: some View {
VStack {
if viewModel.isSearching {
NavigationLink(
isActive: $viewModel.isShowingDestinationScreen,
destination: {
DestinationView()
.navigationTitle("Destination")
}, label: {
EmptyView()
}
)
SearchList() {
viewModel.buttonTapped()
}
} else {
ContentViewMenu()
}
}
.onChange(of: isSearching) { newValue in
viewModel.isSearchingHasChanged(newValue: newValue)
}
}
}
// MARK: Just three simple screens below
struct ContentViewMenu: View {
var body: some View {
Text("Content View Menu")
}
}
struct SearchList: View {
var destinationButtonTapped: () -> Void
var body: some View {
VStack {
Text("Search list")
Button("Go to destination") {
destinationButtonTapped()
}
}
}
}
struct DestinationView: View {
var body: some View {
Text("Destination")
}
}
Also here is a short video showing the behaviour: note how when the Go to destination button is tapped, the screen is updated to the content view because isSearching turns false.
Is there a way to keep isSearching true in this case?
I believe you have two options here:
Normally, when users click on a search field, we expect them to always enter something. It is not possible that someone clicks on a search field without typing anything, otherwise it's just an accident touch, so anything should not execute because of this. Your solution here is: you don't have to do anything at all. Just type anything to the search bar after you clicked on it; you can even just input a space, then your search and search result will always remain active no matter what.
If you still want your search bar to be active even though there is zero interaction or input with the search bar, you can adjust some part of your ContentViewWrapper as below(But I think it's not practical to do this because why would you want your search bar to be active without any input?):
code:
struct ContentViewWrapper: View {
#ObservedObject var viewModel: ContentView.ViewModel
#Environment(\.isSearching) var isSearching
//new code
#State var isShowing = false
var body: some View {
VStack {
//new code
if viewModel.isSearching || isShowing {
NavigationLink(
isActive: $viewModel.isShowingDestinationScreen,
destination: {
DestinationView()
.navigationTitle("Destination")
}, label: {
EmptyView()
}
)
.onAppear {
isShowing = true
}
}
//new code
if isShowing {
SearchList() {
viewModel.buttonTapped()
}
}
}
.onChange(of: isSearching) { newValue in
viewModel.isSearchingHasChanged(newValue: newValue)
}
}
}

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)

In Swiftui How to automatically close modal with rotation to landscape

I currently use an landscape environmentobject based on this code - https://stackoverflow.com/a/58503841/412154
Within my view I have modals that appear and disappear appropriately using #State/#Binding depending on a "Done" Button press. My app does show a different view when rotated to landscape and I would like for the modal to dismiss automatically on the rotation, but couldn't figure out how to change the #binding var based on another #ennvironmentobject
Here is a simplified sample of my Modal View
struct StepsView: View {
#Binding var isPresented:Bool
#EnvironmentObject var orientation:Orientation
var body: some View {
VStack(alignment: .center) {
Text("Step")
}
.navigationBarItems(trailing: Button(action: {
//print("Dismissing steps view...")
self.isPresented = false
}) {
Text("Done").bold()
})
}
thanks for any help!
Appreciate #davidev's answer but I wanted each Modal to act a little differently so I did it this way
struct StepsView: View {
#Binding var isPresented:Bool
#EnvironmentObject var orientation:Orientation
private var PortraitView:some View {
VStack(alignment: .center) {
Text("Modal")
}
.navigationBarItems(trailing: Button(action: {
self.isPresented = false
}) {
Text("Done").bold()
})
}
var body: some View {
buildView(isLandscape: orientation.isLandScape, isShowing: &isPresented)
}
func buildView(isLandscape:Bool, isShowing:inout Bool) -> AnyView {
if !isLandscape {
return AnyView(PortraitView)
} else {
isShowing = false
return AnyView(EmptyView())
}
}
Here is a possible approach with extending the Device class with a variable which keeps track of the opened modal View.
import Combine
final class Orientation: ObservableObject {
#Published var isLandscape: Bool = false {
willSet {
objectWillChange.send()
if (newValue)
{
self.showModal = false
}
}
}
#Published var showModal : Bool = false
}
Whenever landscape changes, and the orientation is landscape, showModal will be set to false.
Here the ContentView..
struct ContentView6: View {
#EnvironmentObject var orientation:Orientation
// 1.
#State private var showModal = false
var body: some View {
Button("Show Modal") {
// 2.
self.orientation.isLandscape.toggle()
// 3.
}.sheet(isPresented: self.$orientation.isLandscape) {
ModalView(isPresented: self.$orientation.isLandscape).environmentObject(self.orientation)
}
}
}

SwiftUI transition from modal sheet to regular view with Navigation Link

I'm working with SwiftUI and I have a starting page. When a user presses a button on this page, a modal sheet pops up.
In side the modal sheet, I have some code like this:
NavigationLink(destination: NextView(), tag: 2, selection: $tag) {
EmptyView()
}
and my modal sheet view is wrapped inside of a Navigation View.
When the value of tag becomes 2, the view does indeed go to NextView(), but it's also presented as a modal sheet that the user can swipe down from, and I don't want this.
I'd like to transition from a modal sheet to a regular view.
Is this possible? I've tried hiding the navigation bar, etc. but it doesn't seem to make a difference.
Any help with this matter would be appreciated.
You can do this by creating an environmentObject and bind the navigationLink destination value to the environmentObject's value then change the value of the environmentObject in the modal view.
Here is a code explaining what I mean
import SwiftUI
class NavigationManager: ObservableObject{
#Published private(set) var dest: AnyView? = nil
#Published var isActive: Bool = false
func move(to: AnyView) {
self.dest = to
self.isActive = true
}
}
struct StackOverflow6: View {
#State var showModal: Bool = false
#EnvironmentObject var navigationManager: NavigationManager
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: self.navigationManager.dest, isActive: self.$navigationManager.isActive) {
EmptyView()
}
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}
}
}
.sheet(isPresented: self.$showModal) {
secondView(isPresented: self.$showModal).environmentObject(self.navigationManager)
}
}
}
struct StackOverflow6_Previews: PreviewProvider {
static var previews: some View {
StackOverflow6().environmentObject(NavigationManager())
}
}
struct secondView: View {
#EnvironmentObject var navigationManager: NavigationManager
#Binding var isPresented: Bool
#State var dest: AnyView? = nil
var body: some View {
VStack {
Text("Modal view")
Button(action: {
self.isPresented = false
self.dest = AnyView(thirdView())
}) {
Text("Press me to navigate")
}
}
.onDisappear {
// This code can run any where but I placed it in `.onDisappear` so you can see the animation
if let dest = self.dest {
self.navigationManager.move(to: dest)
}
}
}
}
struct thirdView: View {
var body: some View {
Text("3rd")
.navigationBarTitle(Text("3rd View"))
}
}
Hope this helps, if you have any questions regarding this code, please let me know.

Resources