Apple-provided environment var `dismissSearch` not working - swiftui

Answers to this question are eligible for a +50 reputation bounty. Bounty grace period has ended.
fingia wants to draw more attention to this question.
In this SwiftUI-based code, the dismissSearch, as specified here, does not work.
That is, upon tapping "Search" on the keyboard, I expected the search bar to cancel, clear the search text field, and have the nav bar with the gear icon visible. As you can see in the GIF below, the keyboard disappears (expected) but the search bar neither clears the text nor "cancels".
Do note that a print out of disappearSearch prints DismissSearchAction(state: nil).
What could be going on?
struct GroceryListView2: View {
#Environment(\.dismissSearch) private var dismissSearch
#State private var searchQuery = ""
var body: some View {
NavigationView {
Text("Hi")
.navigationBarItems(
trailing:
Button(action: {}, label: { Image(systemName: "gear")})
)
.searchable(
text: $searchQuery,
placement: .navigationBarDrawer(displayMode: .automatic),
prompt: "Add or search"
)
.onSubmit(of: .search) {
print("onSubmit", dismissSearch) // DismissSearchAction(state: nil)
dismissSearch()
}
}
}
}

Wow. You have stumbled on some really weird behavior:
The #Environment(\.dismissSearch) property doesn't get populated with a useful value if it's in the same View that applies the .searchable modifier. My guess is searchable is responsible for putting the DismissSearchAction in the environment.
The onSubmit modifier doesn't work if it's applied inside (before) the searchable modifier. My guess is onSubmit stores the callback in the environment for searchable to read.
This leads to needing a convoluted view structure where you apply onSubmit outside (after) the searchable modifier and have it set an #State property which you pass down as a Binding and read in an onChange modifier on your inner view, where you have the #Environment(\.dismissSearch) property available.
I've wrapped it all up into a handy modifier, searchableOnce, which is like searchable but dismisses on submit. Here's how your code looks with this modifier:
struct GroceryListView2: View {
#State private var searchQuery = ""
var body: some View {
NavigationView {
Text("Hi")
.navigationBarItems(
trailing:
Button(action: {}, label: { Image(systemName: "gear")})
)
.searchableOnce(
text: $searchQuery,
placement: .navigationBarDrawer(displayMode: .automatic),
prompt: "Add or search"
)
}
}
}
And here's the modifier implementation:
#available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension SwiftUI.View {
public func searchableOnce(text: SwiftUI.Binding<Swift.String>, placement: SwiftUI.SearchFieldPlacement = .automatic, prompt: SwiftUI.Text? = nil) -> some SwiftUI.View {
return SearchableOnce(content: self, text: text, placement: placement, prompt: prompt)
}
public func searchableOnce(text: SwiftUI.Binding<Swift.String>, placement: SwiftUI.SearchFieldPlacement = .automatic, prompt: SwiftUI.LocalizedStringKey) -> some SwiftUI.View {
return SearchableOnce(content: self, text: text, placement: placement, prompt: Text(prompt))
}
#_disfavoredOverload public func searchableOnce<S>(text: SwiftUI.Binding<Swift.String>, placement: SwiftUI.SearchFieldPlacement = .automatic, prompt: S) -> some SwiftUI.View where S : Swift.StringProtocol {
return SearchableOnce(content: self, text: text, placement: placement, prompt: Text(prompt))
}
}
#available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
fileprivate struct SearchableOnce<Content: View>: View {
#State private var wantDismiss = false
var content: Content
var text: Binding<String>
var placement: SearchFieldPlacement
var prompt: Optional<Text>
var body: some View {
Dismisser(wantDismiss: $wantDismiss, content: content)
.searchable(text: text, placement: placement, prompt: prompt)
.onSubmit(of: .search) {
wantDismiss = true
}
}
}
#available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
fileprivate struct Dismisser<Content: View>: View {
#Environment(\.dismissSearch) private var dismissSearch
#Binding var wantDismiss: Bool
var content: Content
var body: some View {
content
.onChange(of: wantDismiss) { newValue in
if newValue {
dismissSearch()
wantDismiss = false
}
}
}
}

Related

Touch events seemingly not registering at top of screen

I'm seeing very strange behavior within a view. Here's my layout:
struct EventDetailViewContainer: View {
let eventID: EventRecord.ID
#State var event: EventRecord = EventRecord(keyResults: [], text: "", achievesKR: false)
#State var editing: Bool = true
var body: some View {
if #available(iOS 15.0, *) {
VStack {
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
EventDetailView(event: $event, editing: $editing)
}
} else {
// Fallback on earlier versions
}
}
}
#available(iOS 15.0, *)
struct EventDetailView: View {
#Binding var event: EventRecord
#Binding var editing: Bool
#FocusState var textIsFocused: Bool
var body: some View {
VStack {
TextField(
"Event text",
text: $event.text
)
.focused($textIsFocused)
.disabled(!editing)
.padding()
DatePicker("Event Date:", selection: $event.date)
.disabled(!editing)
.padding()
Toggle("Goal is Reached?", isOn: $event.achievesKR)
.disabled(!editing)
.padding()
HStack {
Text("Notes:")
Spacer()
}
.padding()
TextEditor(text: $event.notes)
.disabled(!editing)
.padding()
Spacer()
}
}
}
struct EventRecord: Identifiable, Equatable {
typealias ID = Identifier
struct Identifier: Identifiable, Equatable, Hashable {
typealias ID = UUID
let id: UUID = UUID()
}
let id: ID
var keyResults: [KeyResult.ID]
var date: Date
var text: String
var notes: String
var achievesKR: Bool
init(
id: ID = ID(),
keyResults: [KeyResult.ID],
date: Date = Date(),
text: String,
notes: String = "",
achievesKR: Bool
) {
self.id = id
self.keyResults = keyResults
self.date = date
self.text = text
self.notes = notes
self.achievesKR = achievesKR
}
}
So this works perfectly when I run it as an iPad app, but when I run it on the simulator, the the top toggle doesn't respond to text input.
The strange thing is, when I simply duplicate the toggle, the top one doesn't work and the bottom one works perfectly:
struct EventDetailViewContainer: View {
let eventID: EventRecord.ID
#State var event: EventRecord = EventRecord(keyResults: [], text: "", achievesKR: false)
#State var editing: Bool = true
var body: some View {
if #available(iOS 15.0, *) {
VStack {
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
HStack {
Spacer()
Toggle("Editing", isOn: $editing)
.padding()
}
EventDetailView(event: $event, editing: $editing)
}
} else {
// Fallback on earlier versions
}
}
}
It seems like this should be totally unrelated to the touch behavior of the other views.
Btw this is being displayed in the context of a navigation view.
Is there anything that can explain this? And how can I get it working without adding this extra view on top?
edit: Here's a gif of this behavior being demonstrated. The two controls are exactly the same, but the lower one responds to touch and the upper one does not.

TextField swiftui xcode 13

I have been working with xcode 12 and swiftui. In my app I have textFiel with a localizable placeholder in Spanish and English, I switch to xcode 13 and it doesn't show me my localizable placeholder
this only happens in TextField, with SecureField it does not happen even with Text
this is my code
struct ContentView: View {
#State var email:String = ""
var body: some View {
VStack () {
TextField("login5", text: self.$email)
.autocapitalization(.none)
.padding()
.background(RoundedRectangle(cornerRadius: 50).stroke(Color("grayColor")))
}.padding(.horizontal, 20)
}
}
Localizable.strings
"login5" = "Correo eléctronico";
with SecureField in ios15, you can use the prompt parameter to get your localized string:
SecureField("purpose", text: $password, prompt: Text("login6"))
or using the label:
SecureField(text: $password) {
Text("login6")
}
EDIT1:
This is the test code I'm using to show a working localized TextField and SecureField.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var email = ""
#State var password = ""
#State var isVisible = false
var body: some View {
VStack (spacing: 55) {
Button(action: { isVisible.toggle() }) {
Text("Toggle isVisible")
}
TextField("login5", text: $email).border(.black)
if isVisible {
TextField("login6", text: $password).border(.green)
} else {
SecureField("password", text: $password, prompt: Text("login6")).border(.red)
}
}.padding(.horizontal, 20)
}
}
Test Localizable.strings file.
"login5" = "hola login5";
"login6" = "contraseña";
EDIT2: alternative approach of manually using LocalizedStringKey,
TextField(LocalizedStringKey("login5"), text: $email)
Your main Problem is, like workingdog already said, you need to use text: $variable.
That means for you declare your variable as #State var password = "" and use it like this..
struct ContentView: View {
#State var password = ""
...
if self.visible{
TextField("login6", text: $password)
....
} else {
SecureField("login6", text: $password)
....
}
}
Btw. next time post your code as code not as picture. Its easier to help you :)
Hope I understand your problem correctly and this will be your solution.
I've had the exact same problem, going from Xcode 12 to 13. All of a sudden some (not all) of my text fields no longer show localized string. I was able to fix the problem by forcing:
TextField(LocalizedString("usernameLabel"), text: $username)
Instead of
Textfield("usernameLabel", text: $username)

SwiftUI - making a textfield inside a view be the first responder on touch to that view

On a view I have something like this:
TextFieldUsername()
this shows something like
So, this view shows an icon and the textfield username.
Below that, I have another one for the password.
Making that username field in focus is unnecessarily hard. The textfield is not small, but making the username field to focus is a matter of tapping on the exact position and perhaps you have to tap 2 or 3 times to make it happen.
I would like to make the whole TextFieldUsername() tappable or to increase the hit area of that textfield. I would like better to make the whole thing tappable and once tapped, make its textfield in focus.
This is TextFieldUsername
struct TextFieldUsername: View {
#State var username:String
var body: some View {
HStack {
Image(systemName: "person.crop.circle")
.renderingMode(.template)
.foregroundColor(.black)
.opacity(0.3)
.fixedSize()
TextField(TextFieldUsernameStrings.username, text: $username)
.textFieldStyle(PlainTextFieldStyle())
.textContentType(.username)
.autocapitalization(.none)
}
}
}
Is that possible in SwiftUI without using any external library like introspect?
Using a custom TextField like the one from Matteo Pacini
you can do something like this:
struct CustomTextField1: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
var didBecomeFirstResponder = false
init(text: Binding<String>) {
_text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
#Binding var text: String
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField1>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField1.Coordinator {
return Coordinator(text: $text)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField1>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
struct ContentView : View {
#State var text: String = ""
#State var isEditing = false
var body: some View {
CustomTextField1(text: $text, isFirstResponder: isEditing)
.frame(width: 300, height: 50)
.background(Color.red)
.onTapGesture {
isEditing.toggle()
}
}
}
It's a bit complex, but should get the work done. As for a pure SwiftUI answer, it's currently unavailable.

SwiftUI: Add ClearButton to TextField

I am trying to add a ClearButton to TextField in SwiftUI when the particular TextField is selected.
The closest I got was creating a ClearButton ViewModifier and adding it to the TextField using .modifer()
The only problem is ClearButton is permanent and does not disappear when TextField is deselected
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier {
#Binding var text: String
public func body(content: Content) -> some View {
HStack {
content
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
}
}
}
}
Use ZStack to position the clear button appear inside the TextField.
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier
{
#Binding var text: String
public func body(content: Content) -> some View
{
ZStack(alignment: .trailing)
{
content
if !text.isEmpty
{
Button(action:
{
self.text = ""
})
{
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
.padding(.trailing, 8)
}
}
}
}
Use .appearance() to activate the button
var body: some View {
UITextField.appearance().clearButtonMode = .whileEditing
return TextField(...)
}
For reuse try with this:
func TextFieldUIKit(text: Binding<String>) -> some View{
UITextField.appearance().clearButtonMode = .whileEditing
return TextField("Nombre", text: text)
}
=== solution 1(best): Introspect https://github.com/siteline/SwiftUI-Introspect
import Introspect
TextField("", text: $text)
.introspectTextField(customize: {
$0.clearButtonMode = .whileEditing
})
=== solution 2: ViewModifier
public struct ClearButton: ViewModifier {
#Binding var text: String
public init(text: Binding<String>) {
self._text = text
}
public func body(content: Content) -> some View {
HStack {
content
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
.opacity(text == "" ? 0 : 1)
.onTapGesture { self.text = "" } // onTapGesture or plainStyle button
}
}
}
Usage:
#State private var name: String
...
Form {
Section() {
TextField("NAME", text: $name).modifier(ClearButton(text: $name))
}
}
=== solution 3: global appearance
UITextField.appearance().clearButtonMode = .whileEditing
You can add another Binding in your modifier:
#Binding var visible: Bool
then bind it to opacity of the button:
.opacity(visible ? 1 : 0)
then add another State for checking textField:
#State var showClearButton = true
And lastly update the textfield:
TextField("Some Text", text: $someBinding, onEditingChanged: { editing in
self.showClearButton = editing
}, onCommit: {
self.showClearButton = false
})
.modifier( ClearButton(text: $someBinding, visible: $showClearButton))
Not exactly what you're looking for, but this will let you show/hide the button based on the text contents:
HStack {
if !text.isEmpty {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle")
}
}
}
After initializing a new project we need to create a simple view modifier which we will apply later to our text field. The view modifier has the tasks to check for content in the text field element and display a clear button inside of it, if content is available. It also handles taps on the button and clears the content.
Let’s have a look at that view modifier:
import SwiftUI
struct TextFieldClearButton: ViewModifier {
#Binding var text: String
func body(content: Content) -> some View {
HStack {
content
if !text.isEmpty {
Button(
action: { self.text = "" },
label: {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
)
}
}
}
}
The code itself should be self explanatory and easy to understand as there is no fancy logic included in our tasks.
We just wrap the textfield inside a HStack and add the button, if the text field is not empty. The button itself has a single action of deleting the value of the text field.
For the clear icon we use the delete.left icon from the SF Symbols 2 library by Apple, but you could also use another one or even your own custom one.
The binding of the modifier is the same as the one we apply to the text field. Without it we would not be able to check for content or clear the field itself.
Inside the ContentView.swift we now simply add a TextField element and apply our modifier to it — that’s all!
import SwiftUI
struct ContentView: View {
#State var exampleText: String = ""
var body: some View {
NavigationView {
Form {
Section {
TextField("Type in your Text here...", text: $exampleText)
.modifier(TextFieldClearButton(text: $exampleText))
.multilineTextAlignment(.leading)
}
}
.navigationTitle("Clear button example")
}
}
}
The navigation view and form inside of the ContentView are not required. You could also just add the TextField inside the body, but with a form it’s much clearer and beautiful. 🙈
And so our final result looks like this:
I found this answer from #NigelGee on "Hacking with Swift".
.onAppear {
UITextField.appearance().clearButtonMode = .whileEditing
}
It really helped me out.
Simplest solution I came up with
//
// ClearableTextField.swift
//
// Created by Fred on 21.11.22.
//
import SwiftUI
struct ClearableTextField: View {
var title: String
#Binding var text: String
init(_ title: String, text: Binding<String>) {
self.title = title
_text = text
}
var body: some View {
ZStack(alignment: .trailing) {
TextField(title, text: $text)
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
.onTapGesture {
text = ""
}
}
}
}
struct ClearableTextField_Previews: PreviewProvider {
#State static var text = "some value"
static var previews: some View {
Form {
// replace TextField("Original", text: $text) with
ClearableTextField("Clear me", text: $text)
}
}
}

InputAccessoryView / View Pinned to Keyboard with SwiftUI

Is there an equivalent to InputAccessoryView in SwiftUI (or any indication one is coming?)
And if not, how would you emulate the behavior of an InputAccessoryView (i.e. a view pinned to the top of the keyboard)? Desired behavior is something like iMessage, where there is a view pinned to the bottom of the screen that animates up when the keyboard is opened and is positioned directly above the keyboard. For example:
Keyboard closed:
Keyboard open:
iOS 15.0+
macOS 12.0+,Mac Catalyst 15.0+
ToolbarItemPlacement has a new property in iOS 15.0+
keyboard
On iOS, keyboard items are above the software keyboard when present, or at the bottom of the screen when a hardware keyboard is attached.
On macOS, keyboard items will be placed inside the Touch Bar.
https://developer.apple.com
struct LoginForm: View {
#State private var username = ""
#State private var password = ""
var body: some View {
Form {
TextField("Username", text: $username)
SecureField("Password", text: $password)
}
.toolbar(content: {
ToolbarItemGroup(placement: .keyboard, content: {
Text("Left")
Spacer()
Text("Right")
})
})
}
}
iMessage like InputAccessoryView in iOS 15+.
struct KeyboardToolbar<ToolbarView: View>: ViewModifier {
private let height: CGFloat
private let toolbarView: ToolbarView
init(height: CGFloat, #ViewBuilder toolbar: () -> ToolbarView) {
self.height = height
self.toolbarView = toolbar()
}
func body(content: Content) -> some View {
ZStack(alignment: .bottom) {
GeometryReader { geometry in
VStack {
content
}
.frame(width: geometry.size.width, height: geometry.size.height - height)
}
toolbarView
.frame(height: self.height)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
extension View {
func keyboardToolbar<ToolbarView>(height: CGFloat, view: #escaping () -> ToolbarView) -> some View where ToolbarView: View {
modifier(KeyboardToolbar(height: height, toolbar: view))
}
}
And use .keyboardToolbar view modifier as you would normally do.
struct ContentView: View {
#State private var username = ""
var body: some View {
NavigationView{
Text("Keyboar toolbar")
.keyboardToolbar(height: 50) {
HStack {
TextField("Username", text: $username)
}
.border(.secondary, width: 1)
.padding()
}
}
}
}
I got something working which is quite near the wanted result. So at first, it's not possible to do this with SwiftUI only. You still have to use UIKit for creating the UITextField with the wanted "inputAccessoryView". The textfield in SwiftUI doesn't have the certain method.
First I created a new struct:
import UIKit
import SwiftUI
struct InputAccessory: UIViewRepresentable {
func makeUIView(context: Context) -> UITextField {
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 44))
customView.backgroundColor = UIColor.red
let sampleTextField = UITextField(frame: CGRect(x: 20, y: 100, width: 300, height: 40))
sampleTextField.inputAccessoryView = customView
sampleTextField.placeholder = "placeholder"
return sampleTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
}
With that I could finally create a new textfield in the body of my view:
import SwiftUI
struct Test: View {
#State private var showInput: Bool = false
var body: some View {
HStack{
Spacer()
if showInput{
InputAccessory()
}else{
InputAccessory().hidden()
}
}
}
}
Now you can hide and show the textfield with the "showInput" state. The next problem is, that you have to open your keyboard at a certain event and show the textfield. That's again not possible with SwiftUI and you have to go back to UiKit and making it first responder. If you try my code, you should see a red background above the keyboard. Now you only have to move the field up and you got a working version.
Overall, at the current state it's not possible to work with the keyboard or with the certain textfield method.
I've solved this problem using 99% pure SwiftUI on iOS 14.
In the toolbar you can show any View you like.
That's my implementation:
import SwiftUI
struct ContentView: View {
#State private var showtextFieldToolbar = false
#State private var text = ""
var body: some View {
ZStack {
VStack {
TextField("Write here", text: $text) { isChanged in
if isChanged {
showtextFieldToolbar = true
}
} onCommit: {
showtextFieldToolbar = false
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
VStack {
Spacer()
if showtextFieldToolbar {
HStack {
Spacer()
Button("Close") {
showtextFieldToolbar = false
UIApplication.shared
.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
.foregroundColor(Color.black)
.padding(.trailing, 12)
}
.frame(idealWidth: .infinity, maxWidth: .infinity,
idealHeight: 44, maxHeight: 44,
alignment: .center)
.background(Color.gray)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I managed to create a nicely working solution with some help from this post by Swift Student, with quite a lot of modification & addition of functionality you take for granted in UIKit. It is a wrapper around UITextField, but that's completely hidden from the user and it's very SwiftUI in its implementation. You can take a look at it in my GitHub repo - and you can bring it into your project as a Swift Package.
(There's too much code to put it in this answer, hence the link to the repo)
I have a implementation that can custom your toolbar
public struct InputTextField<Content: View>: View {
private let placeholder: LocalizedStringKey
#Binding
private var text: String
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
private let content: () -> Content
#State
private var isShowingToolbar: Bool = false
public init(placeholder: LocalizedStringKey = "",
text: Binding<String>,
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = { },
#ViewBuilder content: #escaping () -> Content) {
self.placeholder = placeholder
self._text = text
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
self.content = content
}
public var body: some View {
ZStack {
TextField(placeholder, text: $text) { isChanged in
if isChanged {
isShowingToolbar = true
}
onEditingChanged(isChanged)
} onCommit: {
isShowingToolbar = false
onCommit()
}
.textFieldStyle(RoundedBorderTextFieldStyle())
VStack {
Spacer()
if isShowingToolbar {
content()
}
}
}
}
}
You can do it this way without using a UIViewRepresentable.
Its based on https://stackoverflow.com/a/67502495/5718200
.onReceive(NotificationCenter.default.publisher(for: UITextField.textDidBeginEditingNotification)) { notification in
if let textField = notification.object as? UITextField {
let yourAccessoryView = UIToolbar()
// set your frame, buttons here
textField.inputAccessoryView = yourAccessoryView
}
}
}