Searchable not able to keep focus and show keyboard - swiftui

I have the following code:
struct FocusStateTestView: View {
#State var showSheet = false
var body: some View {
VStack {
Text("FocusStateTestView")
Button {
showSheet.toggle()
} label: {
Text("present sheet")
}
}
.sheet(isPresented: $showSheet) {
SheetTestView()
}
}
}
struct SheetTestView: View {
#State var text = ""
var body: some View {
NavigationView {
Form {
Text("hello")
}
.searchable(text: $text)
}
}
}
The sheet presents fine:
Problem:
keyboard does not show up. I cannot type in that text search textfield.
When the sheet is presented the console logs this:
2022-03-24 15:40:11.230674-0600 myApp[92118:2113819] [UIFocus] Failed to update focus with context <UIFocusUpdateContext: 0x6000026bc8c0: previouslyFocusedItem=(null), nextFocusedItem=(null), focusHeading=None>. No additional info available. 2022-03-24 15:40:13.455689-0600 myApp[92118:2113819] [UIFocus] Deferring focus update to item <UISearchBarTextField: 0x14c89da00>. No additional info available. 2022-03-24 15:40:13.455920-0600 myApp[92118:2113819] [UIFocus] Failed to update focus with context <UIFocusUpdateContext: 0x6000026b7660: previouslyFocusedItem=(null), nextFocusedItem=(null), focusHeading=None>. No additional info available.
I then try to tap on the search field, and the console shows this:
objc[92118]: Class _PathPoint is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x12b85f338) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x1395d4fe8). One of the two will be used. Which one is undefined. objc[92118]: Class _PointQueue is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x12b85f310) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x1395d5010). One of the two will be used. Which one is undefined. 2022-03-24 15:40:26.005733-0600 myApp[92118:2113819] [UIFocus] Deferring focus update to item <UISearchBarTextField: 0x14c89da00>. No additional info available. 2022-03-24 15:40:26.005824-0600 myApp[92118:2113819] [UIFocus] Failed to update focus with context <UIFocusUpdateContext: 0x60000269d9a0: previouslyFocusedItem=(null), nextFocusedItem=(null), focusHeading=None>. No additional info available.
Interestingly enough, the same code works on iPhone. I still get some weird logs about it when on iPhone:
objc[92218]: Class _PathPoint is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x12aff7338) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x13f380fe8). One of the two will be used. Which one is undefined. objc[92218]: Class _PointQueue is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x12aff7310) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x13f381010). One of the two will be used. Which one is undefined. 2022-03-24 15:43:25.454520-0600 myApp[92218:2116972] [HardwareKeyboard] -[UIApplication getKeyboardDevicePropertiesForSenderID:shouldUpdate:usingSyntheticEvent:], failed to fetch device property for senderID (778835616971358211) use primary keyboard info instead. 2022-03-24 15:43:25.457774-0600 myApp[92218:2116972] [HardwareKeyboard] -[UIApplication getKeyboardDevicePropertiesForSenderID:shouldUpdate:usingSyntheticEvent:], failed to fetch device property for senderID (778835616971358211) use primary keyboard info instead.
But on iPhone at least it works.
How can I fix this? How can I properly present the keyboard on the iPad and search??
I have the same problem both on simulator and device. running Xcode Version 13.3 (13E113).

In my case, I solved it by adding this:
.searchable(text: $text, placement: .navigationBarDrawer(displayMode: .always))
It changes search bar's appearance a little, but it works for me.

Related

Sheet can't be dismissed when removing a tab

In my app, I have two tabs. The second tab is shown or hidden based on some condition. I find if there is a sheet being presented in the second tab when the tab is to be hidden, the sheet can't be dismissed.
The issue can be consistently reproduced with the code below. To reproduce it, click tab 2, then click "Present Sheet", then click "Hide Tab 2". You will see the sheet isn't removed, though the tab containing it (that is, tab 2) has been removed (you can drag the sheet down to verify it).
It seems a SwiftUI bug to me. Does anyone know how to work around it? I'm close to finish my app but hit this unexpected issue :( Any help will be much appreciated.
struct ContentView: View {
#State var showTab2: Bool = true
var body: some View {
TabView {
// tab 1
NavigationView {
Text("Tab 1")
}
.tabItem {
Label("Tab 1", systemImage: "1.circle")
}
// tab 2
if showTab2 {
NavigationView {
Tab2(showTab2: $showTab2)
}
.tabItem {
Label("Tab 2", systemImage: "2.circle")
}
}
}
}
}
struct Tab2: View {
#State var showSheet: Bool = false
#Binding var showTab2: Bool
var body: some View {
VStack(spacing: 12) {
Text("Tab 2")
Button("Click to present sheet") {
showSheet = true
}
}
.sheet(isPresented: $showSheet, onDismiss: nil) {
NavigationView {
MySheet(showTab2: $showTab2)
}
}
}
}
struct MySheet: View {
#Environment(\.dismiss) var dismiss
#Binding var showTab2: Bool
var body: some View {
Button("Click to hide tab 2") {
// dismiss() works fine if I comment out this line.
showTab2 = false
dismiss()
}
}
}
I have submitted feedback on this to Apple, but I'm not optimistic for any reply (I have never received one).
Update:
The issue can be reproduced in many other scenarios where no sheet is involved. So, the second approach #Asperi gave is not a general solution.
Well, here we see conflict of actions (due to racing): async sheet closing (due to animation) and sync tab removing.
Here are possible approaches:
delay tab removing after sheet closed (implicit way)
Button("Click to hide tab 2") {
dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { // << here !!
showTab2 = false
}
}
remove tab after sheet closed (explicit way)
.sheet(isPresented: $showSheet, onDismiss: { showTab2 = false }) { // << here !!
NavigationView {
MySheet(showTab2: $showTab2)
}
}
Note: Actually when view knows/manages something for parent of parent is not very good design, so option 2 (maybe with some additional conditions/callbacks) are more preferable.
#Asperi gave a great answer. But it's not straightforward to apply his approaches in actual app. I'll explain why and how to do it below.
The key idea in Asperi's approaches is that, since the UI changes have race condition, they should be performed in two steps. In both approaches the sheet is dismissed first, then the tab is hidden.
In practice, however, it may not be obvious how to decouple the two steps. For example, my app works this way (I think it's typical):
The sheet contains a form and call data model API to mutate data model when the form is submitted by user.
Since the data model API may fail, the sheet doesn't dismiss itself as soon as user submits the form. Instead it does that only when the API call succeeds (the API call is synchronous).
When the data model is mutated, it may trigger the condition to hide the tab.
Note the item 2 and 3. It means the sheet have to call data model API first, which may hide the tab, and then dismiss itself.
It took me a while to think out the solution - introduce a dedicated state to control show/hide the tab and hence decouple the two steps. Now the issue left is how to synchronize data model change to that state. Since the purpose is to make them to appear as two separate changes to UI, we can't use Combine. It can be messy if not implemented property because data model can be mutated from everywhere (e.g. Form, ActionSheet, or just Button). Fortunately I find a very elegant approach:
.onChange(of: model.showTab2) { value in
// In my experiments async() works fine, but just to be on the safe side...
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// This is a state outside data model. It hides/shows tab2.
showTab2 = value
}
}
This is another example that there is no problem that can't be solved by adding another layer of abstraction :)

SwiftUI Dismiss Keyboard from UITextField

So for my own reasons, I need the full control that UITextField and its delegates would normally offer, but the screen it's being added to is written with SwiftUI. Needs a .decimal keyboard, so no Return Key.
I have only 2 issues remaining, 1 of which I hope to resolve here. Attempting to add a call to resign first responder and add it to a VStack on the page basically disables the UITextField, since I can't bring up the keyboard.
How can I dismiss this keyboard without adding an arbitrary extra button to the page?
Example Code:
Struct CustomTextView: UIViewRepresentable {
/// Insert init, updateView, binding variable, coordinator, etc
func makeView() -> UITextField {
var textField = UITextField()
textField.delegate = context.coordinator
/// Set up rest of textfield parameters such as Font, etc.
return textField
}
}
extension CustomTextView {
class Coordinator: NSObject, UITextFieldDelegate {
/// UITextfield delegate implementations, extra reference to binding variable, etc
/// Primarily textField.shouldChangeCharactersInRange
}
}
struct ContentView: View {
#State viewModel: ViewModel
var body: some View {
VStack {
CustomTextView($viewModel.parameter)
/// Other views
}
.onTap {
/// Attempting to add the generic call to UIApplication for resignFirstResponder here does not allow CustomTextView to ever hold it even when tapped in
}
}
}
I can't give all the code for privacy reasons, but this should be enough to establish my issue.
I have done this by adding this Function to you view below.
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
Then with a ontapGesture you can make the keyboard go away.
For example you can use this on the background Stack of your whole view. If a user taps on the background the keyboard will dissapear.
.onTapGesture {
self.hideKeyboard()
}
So I found a trick on my own with an epiphany overnight.
First, I would like to share to anyone else a very basic reason why inb4cookies solution wasn't quite adequate. While I had already tried adding a resignFirstResponder call like it to the onTap of the background stack, it was triggering the onTap for the VStack when I was clicking the field.
This is likely because I am using a UITextField as the back end for this component and not a SwiftUI TextField.
However, it was partially used in the final solution. I still applied it, but there is an extra step.
VStack {
CustomTextView($viewModel.parameter)
.onTap {/*Insert literally any compiling code here*/ }
/// Other views
}
.onTap {
self.hideKeyboard()
}
You'll see that above, there is an extra onTap. I tested it with a print statement, but this will override the onTap for the VStack and prevent the keyboard from being dismissed right after it is brought up. Tapping anywhere else on the VStack still closes it, except for Buttons. But I can always add hideKeyboard to those buttons if needed.

In a SwiftUI AppLifecycle Document App, how can I get a menu command in active ContentView?

I'm writing a MacOS document app using the SwiftUI App lifecycle, and all the tricks I see here and elsewhere for sending a menu action to the active window depend on using platform specific implementation, which is (mostly) unavailable in a SwiftUI Lifecycle app. What I'm looking for is something like SideBarCommands(), which adds a menu item that, when selected by mouse or command key, toggles the appearance of the sidebar in the active window. All the Command examples I have seen thus far are trivial, none address a multi-document, multi-window use case.
Given a ContentView declared thusly:
struct ContentView: View {
#Binding var document: TickleDocument
var body: some View {
TextEditor(text: $document.text)
}
public func menuTickle() {
document.text = "Wahoo!"
}
}
and a command, which is added via:
struct TickleApp: App {
public static var target:TickleDocument?
var body: some Scene {
let docGroup = DocumentGroup(newDocument: TickleDocument()) { file in
ContentView(document: file.$document)
}
docGroup
.commands {
CommandMenu("App Tickles") {
Button("Tickle The ContentView") {
// Here's where I need to call menuTickle() on the active ContentView
}.keyboardShortcut("t")
}
}
}
}
}
What do I need to do so the button closure can call menuTickle() on the active ContentView? I know it can be done, because SideBarCommands() does it (unless Apple is using some non-public API to do it...).
For bonus points, tell me how I can detect whether or not I'm the active ContentView while body is being evaluated, and how I can detect when it changes! Tracking the Environment variable scenePhase is worthless - it always reports active, and never changes.
My question is a duplicate of this one.
The answer to that question contains a link to a solution that I have verified works, and can be found here

Odd behavior when using multiple SecureFields following a TextField in a SwiftUI view

Note: This bug has been fixed in Xcode 13.0 beta 3
I am experiencing very odd behavior when trying to use multiple SecureFields when following a TextField in a view. Attempting to enter text in one of the SecureFields stops at one character with the field turning yellow and displaying "Strong Password", as well as duplicating in the second SecureField. This is occurring on iOS 14.2 in Xcode 12.2 on the Xcode preview and in the simulator.
Here is a minimal example that demonstrates the issue:
struct SecureFieldTestView: View {
#State var displayName: String = ""
#State var password = ""
#State var passwordVerifiation = ""
var body: some View {
VStack {
TextField("Display name", text: $displayName)
SecureField("Password", text: $password)
SecureField("Verify Password", text: $passwordVerifiation)
}
.padding()
}
}
struct SecureFieldTestView_Previews: PreviewProvider {
static var previews: some View {
SecureFieldTestView()
}
}
The console shows the following errors when running into the simulator:
[AutoFill] Cannot show Automatic Strong Passwords for app bundleID: <REDACTED BY ME> due to error: iCloud Keychain is disabled
[Assert] View <(null):0x0> does not conform to UITextInput protocol
I have tried to wrap the SecureFields into their own VStack{} and wrapping them all into a Form{}, but the issue remains.
Is there something obvious that I am missing or is this a bug in the SDK?
I would report a bug to Apple regarding this issue...
However, for the moment here is a workaround to fix this and still use the SecureField without the yellow bar on top..
SecureField("First", text: $password)
.textContentType(.newPassword)
Just add textContentType for newPassword and that bar won't appear.
On further investigation, there is another issue that was occurring; the software keyboard would not appear until after some delay. Entering text in the SecureFields before the software keyboard appeared consistantly produced the issue. Waiting for the software keyboard to appear resolves the odd behavior.
Strangely, the delay in the keyboard appearance seems to occur when the device/simulator is not logged into iCloud; the keyboard appearance delay and SecureField issue does not occur when the device is logged in.

SwiftUI how to detect when a view has the focus?

I have two List's in a view and want to be able to determine which list currently has the focus in order to show the correct details of the selected item in the list in a details panel.
The following code never seems to get called, can anyone indicate whether there is another correct way to determine when focus changes.
struct StoreList: View {
#EnvironmentObject private var database: Database
#Binding var selectedStore: Store?
var body: some View {
List(selection: $selectedStore) {
ForEach(database.stores, id: \.self) { store in
StoreRow(store: store).tag(store)
.focusable(true, onFocusChange: { isFocused in
print("focus changed")
if isFocused {
self.database.selectedType = .store
}
})
}
}
.focusable(true, onFocusChange: { isFocused in
print("focus changed")
if isFocused {
self.database.selectedType = .store
}
})
}
}
In the meantime I will explore detecting mouse clicks on the Rows since the user would need to click on an item in the list to move the focus.
Currently I am setting the selectedType value when an item changes (i.e. $selectedStore) in the view model (database) but if the user selected the already selected item in the other list then the value does not get updated but the List and list item does get the focus - well the visual colour change indicates it has the focus.
EDIT:
I have also tried processing the onTapGesture callback which works fine except it replaces the List rows default behaviour. How can I make sure the event is passed through to the List as this might work then.
The easiest method of reacting to focus is
struct DummyView: View {
#Environment(\.isFocused) var isFocused
var body: some View {
Text("My view")
.padding()
.background(isFocused ? Color.orange : Color.black)
}
}