Tab to next text field when 'next/return' key hit on keyboard in SwiftUI - keyboard

This is strictly for SwiftUI.
I would like to have the keyboard move to the next available text field when the user hits the 'return' key on the keyboard.
I have the following view:
var body: some View {
VStack {
NavigationView {
Form {
TextField("First name", text: $model.firstname)
.tag(1)
TextField("Last name", text: $model.lastname)
.tag(2)
}
.navigationBarTitle("Add a Person", displayMode: .inline)
}
}
}
And the following code that should allow the tab:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
let nextTag = textField.tag + 1
if let nextResponder = textField.superview?.viewWithTag(nextTag) {
nextResponder.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
return true
}
I am just not sure how to implement it in SwiftUI?
How do I assign it to the delegate of the textfield?!
****UPDATE****
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
print("Current Tag: ", textField.tag) // Works correctly
let nextTag = textField.tag + 1
print("Next Tag: ", nextTag) // Works correctly
let nextResponder = textField.superview?.viewWithTag(nextTag) as UIResponder? // ALWAYS RETURN NIL
....
Not sure why the assignment of nextResponder always returns nil?!

iOS15+
Now it can be easily done with FocusState+.focused(,equals:) specifying named tags and updating focus state on needed action.
Tested with Xcode 13.3 / iOS 15.4
Here is main part:
#FocusState private var infocus: Field?
enum Field {
case first, last
}
// ...
TextField("First name", text: $firstname,
onCommit: { infocus = .last }) // << here !!
.focused($infocus, equals: .first)
Complete test module in project is here

Related

How to select an item from a search file and place in textfield in another file

Using SwiftUI - Xcode 14.2 - iOS 16.0
I have tried different search tutorials to create a search file for my project but am unable to find out how to select the item in the search file and place that selected item in a textfield in another file. I have searched this site for other posts, i tried searching through Google, YouTube, etc...
In File 1, I have a textfield that that has a prompt 'start typing' and when selected, it directs you to the Search file to select the item you want, so it can be placed in place of the prompt.
File 1 (where the textfield is needed to paste the selected item):
VStack {
NavigationLink(destination: NameSearch()) {
TextField("Name", text: .constant(""), prompt: Text(" Start typing ")
.foregroundColor(.blue))
.multilineTextAlignment(.leading)
.padding()
}
}
Once I click on the 'start typing' prompt, it navigates to NameSearch.swift file, as seen below.
NameSearch.swift:
import SwiftUI
struct NameSearch: View {
let name = [
"Jane", "George", "Sam", "Henry", "Sally", "Liz", "John"
]
#State private var searchText = ""
var body: some View {
NavigationStack {
VStack {
// Search view
SearchBarView(searchText: $searchText)
List {
// Filtered list of names
ForEach(name.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self) {
searchText in Text(searchText)
}
}
.navigationBarTitle(Text("Search Name"))
.resignKeyboardOnDragGesture()
}
}
}
}
struct NameSearch_Previews: PreviewProvider {
static var previews: some View {
Group {
NameSearch()
.environment(\.colorScheme, .light)
NameSearch()
.environment(\.colorScheme, .dark)
}
}
}
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged{_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
modifier(ResignKeyboardOnDragGesture())
}
}
struct SearchBarView: View {
#Binding var searchText: String
#State private var showCancelButton: Bool = false
var onCommit: () ->Void = {print("onCommit")}
var body: some View {
HStack {
HStack {
Image(systemName: "magnifyingglass")
// Search text field
ZStack (alignment: .leading) {
if searchText.isEmpty { // Separate text for placeholder to give it the proper color
Text("Search")
}
TextField("", text: $searchText, onEditingChanged: { isEditing in
self.showCancelButton = true
}, onCommit: onCommit).foregroundColor(.primary)
}
// Clear button
Button(action: {
self.searchText = ""
}) {
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary) // For magnifying glass and placeholder test
.background(Color(.tertiarySystemFill))
.cornerRadius(10.0)
if showCancelButton {
// Cancel button
Button("Cancel") {
UIApplication.shared.endEditing(true) // this must be placed before the other commands here
self.searchText = ""
self.showCancelButton = false
}
.foregroundColor(Color(.systemBlue))
}
}
.padding(.horizontal)
.navigationBarHidden(showCancelButton)
}
}
Question 1: How do I hide all the names from showing in the list so that I just see the search bar and the cancel button and an empty list?
Question 2: Once I type the name I am looking for, it should pop up and I want to select name - how can I do this?
once I type the name in search bar, it appears in the empty list
I select that name
it then takes me back to File 1
replaces the 'start typing' prompt with the name i just selected in the Search file.
Question 3: I have noticed in the Search file, I am getting a warning with the following code. How can I resolve it?
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
The warning that appears is:
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a
relevant window scene instead
Firstly, thank you for providing a working example of your code.
As you're building for iOS 15+, you should probably be using the .searchable modifier rather than rolling your own.
The 2021 WWDC video introducing this feature is here https://developer.apple.com/wwdc21/10176
Some new features from 2022 here: https://developer.apple.com/wwdc22/10052

SwiftUI UISearchController replacement: search field, results and some scrollable content fail to coexist in a meaningful manner

Starting with this
var body: some View {
ScrollView {
VStack(spacing: 0.0) {
Some views here
}
}
.edgesIgnoringSafeArea(.top)
}
How would I add
List(suggestions, rowContent: { text in
NavigationLink(destination: ResultsPullerView(searchText: text)) {
Text(text)
}
})
.searchable(text: $searchText)
on top if that scrollable content?
Cause no matter how I hoax this together when
#State private var suggestions: [String] = []
gets populated (non empty) the search results are not squeezed in (or, better yet, shown on top of
"Some views here"
So what I want to achieve in different terms: search field is on top, scrollable content driven by the search results is underneath, drop down with search suggestions either temporarily squeeses scrollable content down or is overlaid on top like a modal sheet.
Thanks!
If you are looking for UIKit like search behaviour you have to display your results in an overlay:
1. Let's declare a screen to display the results:
struct SearchResultsScreen: View {
#Environment(\.isSearching) private var isSearching
var results: [String]?
var body: some View {
if isSearching, let results {
if results.isEmpty {
Text("nothing to see here")
} else {
List(results, id: \.self) { fruit in
NavigationLink(destination: Text(fruit)) {
Text(fruit)
}
}
}
}
}
}
2. Let's have an ObservableObject to handle the logic:
class Search: ObservableObject {
static private let fruit = [
"Apples 🍏",
"Cherries 🍒",
"Pears 🍐",
"Oranges 🍊",
"Pineapples 🍍",
"Bananas 🍌"
]
#Published var text: String = ""
var results: [String]? {
if text.isEmpty {
return nil
} else {
return Self.fruit.filter({ $0.contains(text)})
}
}
}
3. And lastly lets declare the main screen where the search bar is displayed:
struct ContentView: View {
#StateObject var search = Search()
var body: some View {
NavigationView {
LinearGradient(colors: [.orange, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.overlay(SearchResultsScreen(results: search.results))
.searchable(text: $search.text)
.navigationTitle("Find that fruit")
}
}
}

Publish background context Core Data changes in a SwiftUI view without blocking the UI

After running a background-context core data task, Xcode displays the following purple runtime warning when the updates are published in a SwiftUI view:
"[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates."
Besides the ContentView.swift code below, I also added container.viewContext.automaticallyMergesChangesFromParent = true to init in the default Persistence.swift code.
How can I publish the background changes on the main thread to fix the warning? (iOS 14, Swift 5)
Edit: I've changed the code below, in response to the first answer, to clarify that I'm looking for a solution that doesn't block the UI when a lot of changes are saved.
struct PersistenceHelper {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) {
self.context = context
}
public func fetchItem() -> [Item] {
do {
let request: NSFetchRequest<Item> = Item.fetchRequest()
var items = try self.context.fetch(request)
if items.isEmpty { // Create items if none exist
for _ in 0 ..< 250_000 {
let item = Item(context: context)
item.timestamp = Date()
item.data = "a"
}
try! context.save()
items = try self.context.fetch(request)
}
return items
} catch { assert(false) }
}
public func updateItemTimestamp(completionHandler: #escaping () -> ()) {
PersistenceController.shared.container.performBackgroundTask({ backgroundContext in
let start = Date(), request: NSFetchRequest<Item> = Item.fetchRequest()
do {
let items = try backgroundContext.fetch(request)
for item in items {
item.timestamp = Date()
item.data = item.data == "a" ? "b" : "a"
}
try backgroundContext.save() // Purple warning appears here
let interval = Double(Date().timeIntervalSince(start) * 1000) // Artificial two-second delay so cover view has time to appear
if interval < 2000 { sleep(UInt32((2000 - interval) / 1000)) }
completionHandler()
} catch { assert(false) }
})
}
}
// A cover view with an animation that shouldn't be blocked when saving the background context changes
struct CoverView: View {
#State private var toggle = true
var body: some View {
Circle()
.offset(x: toggle ? -15 : 15, y: 0)
.frame(width: 10, height: 10)
.animation(Animation.easeInOut(duration: 0.25).repeatForever(autoreverses: true))
.onAppear { toggle.toggle() }
}
}
struct ContentView: View {
#State private var items: [Item] = []
#State private var showingCoverView = false
#State private var refresh = UUID()
let persistence = PersistenceHelper()
let formatter = DateFormatter()
var didSave = NotificationCenter.default
.publisher(for: .NSManagedObjectContextDidSave)
// .receive(on: DispatchQuene.main) // Doesn't help
var body: some View {
ScrollView {
LazyVStack {
Button("Update Timestamp") {
showingCoverView = true
persistence.updateItemTimestamp(completionHandler: { showingCoverView = false })
}
ForEach(items, id: \.self) { item in
Text(formatter.string(from: item.timestamp!) + " " + (item.data ?? ""))
}
}
}
.id(refresh)
.onAppear {
formatter.dateFormat = "HH:mm:ss"
items = persistence.fetchItem()
}
.onReceive(didSave) { _ in
items = persistence.fetchItem()
}
.fullScreenCover(isPresented: $showingCoverView) {
CoverView().onDisappear { refresh = UUID() }
}
}
}
Since you are performing a background task, you are on a background thread - rather than the main thread.
To switch to the main thread, change the line producing the runtime warning to the following:
DispatchQueue.main.async {
try backgroundContext.save()
}
You should use Combine and observe changes to your background context and update State values for your UI to react.
#State private var coreDataAttribute = ""
var body: some View {
Text(coreDataAttribute)
.onReceive(
CoreDataManager.shared.moc.publisher(for: \.hasChanges)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.global())
.map{_ in CoreDataManager.shared.fetchCoreDataValue()}
.filter{$0 != coreDataAttribute}
.receive(on: DispatchQueue.main))
{ value in
coreDataAttribute = value
}
}

SwiftUI - Perform action when cancel is clicked - .searchable function

When using the .searchable(text: $text) function, a cancel button appears in the search bar when searching.
Is there any way to perform an action when the cancel button is clicked? I would like to call a function when cancel is clicked, but cannot figure out how to perform an action when cancel is tapped.
The Apple Documentation does not mention anything about this. Back in UIKit there was the func searchBarCancelButtonClicked(searchBar: UISearchBar) { to do this.
Below is an image of the cancel button I am referring to:
You can use the isSearching environment value (https://developer.apple.com/documentation/swiftui/environmentvalues/issearching?changes=_6) to see if a search is being performed. To do an action upon cancelation, you could watch for a change from true to false using onChange:
struct ContentView: View {
#State private var searchText = ""
#Environment(\.dismissSearch) var dismissSearch
var body: some View {
NavigationView {
VStack {
ChildView()
Text("Searching for \(searchText)")
}
.searchable(text: $searchText)
.navigationTitle("Searchable Example")
}
}
}
struct ChildView : View {
#Environment(\.isSearching) var isSearching
var body: some View {
Text("Child")
.onChange(of: isSearching) { newValue in
if !newValue {
print("Searching cancelled")
}
}
}
}
Probably important to note that it seems like isSearching has to be inside a child view of the searchable modifier in order for it to work properly
Based on #jnpdx 's answer, something equivalent, but more generic is:
struct SearchView<Content: View>: View {
#Environment(\.isSearching) var isSearching
let content: (Bool) -> Content
var body: some View {
content(isSearching)
}
init(#ViewBuilder content: #escaping (Bool) -> Content) {
self.content = content
}
}
And then, use it like:
struct ContentView: View {
#State private var searchText = ""
#Environment(\.dismissSearch) var dismissSearch
var body: some View {
NavigationView {
VStack {
SearchView { isSearching in
Text("Child")
.onChange(of: isSearching) { newValue in
if !newValue {
print("Searching cancelled")
}
}
}
Text("Searching for \(searchText)")
}
.searchable(text: $searchText)
.navigationTitle("Searchable Example")
}
}
}
use isEmpty and onAppear.
struct SearchView: View {
#State var text: String = ""
var body: some View {
NavigationView {
VStack {
if text.isEmpty {
main.onAppear {
print("empty")
// code here
}
} else {
main
}
}.searchable(text: $text)
.onSubmit(of: .search) {
print("submit")
}
}
}
var main: some View {
Text("search").searchable(text: $text)
}
}

Action when user click on the delete button on the keyboard in SwiftUI

I try to run a function when the user click on the delete button on the keyboard when he try to modify a Textfield.
How can I do that ?
Yes it is possible, however it requires subclassing UITextField and creating your own UIViewRepresentable
This answer is based on the fantastic work done by Costantino Pistagna in his medium article but we need to do a little more work.
Firstly we need to create our subclass of UITextField, this should also conform to the UITextFieldDelegate protocol.
class WrappableTextField: UITextField, UITextFieldDelegate {
var textFieldChangedHandler: ((String)->Void)?
var onCommitHandler: (()->Void)?
var deleteHandler: (() -> Void)?
override func deleteBackward() {
super.deleteBackward()
deleteHandler?()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField {
nextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let currentValue = textField.text as NSString? {
let proposedValue = currentValue.replacingCharacters(in: range, with: string)
textFieldChangedHandler?(proposedValue as String)
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
onCommitHandler?()
}
}
Because we are creating our own implementation of a TextField we need three functions that we can use for callbacks.
textFieldChangeHandler this will be called when the text property updates and allows us to change the state value associated with our Textfield.
onCommitHandler this will be called when we have finished editing our TextField
deleteHandler this will be called when we perform he delete action.
The code above shows how these are used. The part that you are particularly interested in is the override func deleteBackward(), by overriding this we are able to hook into when the delete button is pressed and perform an action on it. Depending on your use case, you may want the deleteHandler to be called before you call the super.
Next we need to create our UIViewRepresentable.
struct MyTextField: UIViewRepresentable {
private let tmpView = WrappableTextField()
//var exposed to SwiftUI object init
var tag:Int = 0
var placeholder:String?
var changeHandler:((String)->Void)?
var onCommitHandler:(()->Void)?
var deleteHandler: (()->Void)?
func makeUIView(context: UIViewRepresentableContext<MyTextField>) -> WrappableTextField {
tmpView.tag = tag
tmpView.delegate = tmpView
tmpView.placeholder = placeholder
tmpView.onCommitHandler = onCommitHandler
tmpView.textFieldChangedHandler = changeHandler
tmpView.deleteHandler = deleteHandler
return tmpView
}
func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<MyTextField>) {
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
}
This is where we create our SwiftUI version of our WrappableTextField. We create our WrappableTextField and its properties. In the makeUIView function we assign these properties. Finally in the updateUIView we set the content hugging properties, but you may choose not to do that, it really depends on your use case.
Finally we can create a small working example.
struct ContentView: View {
#State var text = ""
var body: some View {
MyTextField(tag: 0, placeholder: "Enter your name here", changeHandler: { text in
// update the state's value of text
self.text = text
}, onCommitHandler: {
// do something when the editing finishes
print("Editing ended")
}, deleteHandler: {
// do something here when you press delete
print("Delete pressed")
})
}
}

Resources