Add keyboard shortcut for Search - ios

Overview
I have a SwiftUI list with a search field
I would like to add a keyboard shortcut for search field to be in focus
Questions:
How can I add a keyboard shortcut for search field?
import SwiftUI
struct ContentView: View {
#State private var searchText = ""
var body: some View {
NavigationStack {
List(0..<100) { index in
Text("Cell \(index)")
}
.searchable(text: $searchText)
}
}
}

Related

How to add functionality to SearchResultsButton in search bar with SwiftUI?

I am trying to have a sheet pop up when the SearchResultsButton on my search bar is tapped. The below picture shows the button I want to add functionality for:
This is the code I have currently:
struct ContentView: View {
#State private var isSearching = ""
var body: some View {
NavigationView {
List {
Text("Hello")
Text("World")
}
.navigationTitle("Hello")
.searchable(text: $isSearching)
.onAppear {
UISearchBar.appearance().showsSearchResultsButton = true
}
}
}
}
How would I go about accomplishing my goal? Any help would be greatly appreciated.

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 Keyboard Toolbar Scope

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

SwiftUI, cannot dismiss sheet after the keyboard

In SwiftUI, I open a CommentsView sheet like this:
#State private var selectedCategory: Category?
Button(category.name) {
selectedCategory = category
}
.sheet(item: $selectedCategory) { category in
CommentsView(category: category)
}
CommentsView:
struct CommentsView: View {
#Environment(\.presentationMode) private var presentationMode
#State private var enteredComment: String = ""
let category: Category
var body: some View {
VStack {
TextField("Add a comment", text: $enteredComment)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Close") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
The problem is: I cannot dismiss the CommentsView after I focus on the text field and see the keyboard. Before focusing the "Close" button works as expected.
Change the position of .sheet to be at the top VStack/HStack or ZStack, NOT at the Button View
.sheet(item: $selectedCategory) { category in
CommentsView(category: category)
}

How to automatically detect NavigationBarTitle displayMode become .inline or .large in SwiftUI?

I have a List inside a navigationView and I want to change the List Section text whenever the list is scrolled and the navigationBarTitle become .inline or .large
This is my code:
import SwiftUI
struct ContentView: View {
#State private var scrolledUp = false
var body: some View {
NavigationView {
if scrolledUp {
List {
Section(header: Text("Moved UP"))
{
Text("Line1").bold()
Text("Line2").bold()
Text("Line2").bold()
}
.navigationBarTitle("Setting")
}
} else {
List {
Section(header: Text("Not Moved"))
{
Text("Line1").bold()
Text("Line2").bold()
Text("Line2").bold()
}
}
.navigationBarTitle("Setting")
}
}
}
}
how can I find out the list is scrolled and the navigationBar is changed to .title?
For now I used geometryReader to detect the y position of the List section header. During the scroll. If the position is less than 80, the navigationBarTitle is changed to .title.
It works perfectly. However, I still looking for a better solution.

Resources