CustomKeyboardKit icon indicating copy to clipboard operation
CustomKeyboardKit copied to clipboard

Wrong keyboard shown first time after focusing on a field shown conditionally

Open fraune opened this issue 4 months ago • 16 comments

Hello, I hope you are doing well!

I'm not sure that this is an issue with this library, but I was curious to hear your thoughts.

Description

Background

SwiftUI supports conditionally showing views, by letting you place if-else or switch blocks directly in the view code. This allows SwiftUI to manage when an element should be displayed or removed from the view (other approaches involve modifying an element's opacity conditionally, but that element will still take up space when invisible).

Expectation

When using a conditional to switch to a TextField that has a custom keyboard set on it, I want to also focus the TextField to automatically present the keyboard. However, when doing so, the "standard" keyboard is presented instead. After toggling the conditional again, the TextField can present the custom keyboard.

I don't know how the introspect library works, but I'm guessing someone will tell me that conditional blocks in SwiftUI prevents a view from entering memory or something...

Demonstration

Code to replicate:

import SwiftUI
import CustomKeyboardKit

struct ContentView: View {
    @State private var selectedEntryType = EntryType.date
    @State private var textFieldContent = ""
    @FocusState private var textFieldFocused
    
    var body: some View {
        Form {
            Picker("Entry Type", selection: $selectedEntryType) {
                Text(EntryType.date.rawValue)
                    .tag(EntryType.date)
                Text(EntryType.binary.rawValue)
                    .tag(EntryType.binary)
            }
            .pickerStyle(.segmented)
            .onChange(of: selectedEntryType) { oldState, newState in
                if newState == .binary {
                    textFieldFocused = true
                }
            }
            
            switch selectedEntryType {
            case .date:
                DatePicker("some date", selection: Binding(get: { Date.now }, set: {_ in}))
            case .binary:
                TextField("title", text: $textFieldContent, axis: .vertical)
                    .customKeyboard(.binary)
                    .focused($textFieldFocused)
            }
        }
    }
}

private enum EntryType: String, CaseIterable {
    case date = "Date"
    case binary = "Binary"
}

extension CustomKeyboard {
    static var binary: CustomKeyboard {
        let keyWidth = 20.0
        let keyHeight = 34.0
        return CustomKeyboardBuilder { textDocumentProxy, submit, playSystemFeedback in
            HStack {
                Button {
                    textDocumentProxy.insertText("0")
                    playSystemFeedback?()
                } label: {
                    Text("0")
                        .frame(minWidth: keyWidth, minHeight: keyHeight)
                }
                Button {
                    textDocumentProxy.insertText("1")
                    playSystemFeedback?()
                } label: {
                    Text("1")
                        .frame(minWidth: keyWidth, minHeight: keyHeight)
                }
                .padding(.trailing, 10)
                
                Button {
                    textDocumentProxy.deleteBackward()
                    playSystemFeedback?()
                } label: {
                    Image(systemName: "delete.backward")
                        .frame(minHeight: keyHeight)
                }
                Button {
                    playSystemFeedback?()
                    submit()
                } label: {
                    Text("Done")
                        .frame(minHeight: keyHeight)
                }
            }
            .font(.title)
            .buttonStyle(.bordered)
            .padding([.top, .bottom], 20)
        }
    }
}

My workaround

This little hack is the only way I've found to resolve the issue so far.

// Updating the focus assignment in the demo code:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
    textFieldFocused = true
}

fraune avatar Sep 28 '24 02:09 fraune