marinofelipe / CurrencyText

Currency text field formatter available for UIKit and SwiftUI 💶✏️

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Need a way to set the first focus for SwiftUI

YeungKC opened this issue · comments

Luckily there is a uiTextField callback provided and I can set it up by doing this, but it's very ugly

textFieldConfiguration: { uiTextField in
                                DispatchQueue.main.async {
                                    if !becomeFirstResponderCalled {
                                        uiTextField.becomeFirstResponder()
                                    }
                                    becomeFirstResponderCalled = true
                                }
                            }

If we can use it with https://developer.apple.com/documentation/swiftui/link/focused(_:)?changes=latest_minor, then great~~

Hey @YeungKC.

Glad you found a way to do that already. Yes, it's indeed ugly, and I'd say in general the callback approach for configuring the underlying UITextField is ugly, but it's what we've got with the limitations of SwiftUI and having to bridge from UIKit.

So, textFieldConfiguration is supposed to be called only once, when the underlying text field is first created, so it's safe to call becomeFirstResponder() without that helper in tour snippet, becomeFirstResponderCalled.
I'll try to clarify that textFieldConfiguration is called only once in the documentation.

Another thing one could do is to capture the underlying text field and call becomeFirstResponder() on onAppear. Also ugly, but I expect a bit better in terms of when becomeFirstResponder is called in the lifecycle, and that it doesn't require a dispatch async call on main:

struct SwiftUIExampleView: View {
    // I expect is safe to retain the underlying `textField` here in terms of memory ownership
    @State private var underlyingCurrencyTextField: UITextField?

    var body: some View {
        Form {
            Section {
                CurrencyTextField(
                    configuration: .init(
                        textFieldConfiguration: { uiTextField in
                            self.underlyingCurrencyTextField = uiTextField
                        }
                )
                .onAppear {
                    underlyingCurrencyTextField?.becomeFirstResponder()
                }
          ...

I'll get some time to see if I can add an API into CurrencyTextSwiftUI that would allow doing that more easily and safely from the user side.

Regarding focused(_:), I'll investigate whether it works out of the box or not for CurrencyTextField, I haven't tried it yet. Have you? In case it works, it's great, but would still be good to make it easier for users on iOS 13 & 14 to set CurrencyTextField as first responder.

Thank you very much for your advice, although also ugly but better ~ ~
I have a question though, will referencing underlyingCurrencyTextField cause memory reclamation problems?

I tried and did not work with focused(_:).

Thank you for contribution and reply.

No worries, and thank you for your contribution!

I have a question though, will referencing underlyingCurrencyTextField cause memory reclamation problems?

This is a good question, @YeungKC. Initially I thought there wouldn't be problems given UITextField does not reference back self/the view, view is a value type and etc. But doing some additional investigation I was able to see some leaks with the UITextField being retained, so please avoid that. Looks safer to go for the approach you first mentioned.

I'm not 100% yet where the leak is but I expect somewhere in the SwiftUI's @State mechanism. It's usually not a great idea to have reference strongly references by value types as well, so better indeed to avoid it.

I'll get my head around this more calmly later in the week to better analyze it and work in and see if we can have an CurrencyTextSwiftUI API for focusing/ setting as first responder.

I tried and did not work with focused(_:).

This is good to know 👍 , I'll have to see if I can make it work somehow. I already suspected given CurrencyTextField is not a TextField but just a View.

I came across this and wondered if it would be helpful to you.
https://stackoverflow.com/questions/60558564/making-a-custom-swiftui-view-adapt-to-built-in-modifiers

Thanks, @YeungKC.

One thing that concerns me is that in that answer the textField is retained by the custom view, which as we saw in that other case above, is not a good idea.
There's one comment that in parts make sense to me, regarding using the updateUIView method instead.

One problem with view modifiers here is that they wouldn't work out as far as I checked, since essentially that function would have to mutate self, CurrencyTextField, which breaks SwiftUI ResultBuilder immutability:

@available(iOS 13.0, *)
public struct CurrencyTextField: UIViewRepresentable {
    private let configuration: CurrencyTextFieldConfiguration
    var borderStyle: UITextField.BorderStyle?

    /// Creates a currency text field instance with given configuration.
    ///
    /// - Parameter configuration: The configuration holding settings and properties to configure the text field with.
    public init(configuration: CurrencyTextFieldConfiguration) {
        self.configuration = configuration
    }

    public func makeUIView(
        context: UIViewRepresentableContext<CurrencyTextField>
    ) -> UITextField {
        let textField = WrappedTextField(configuration: configuration)
        textField.placeholder = configuration.placeholder
        textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
        textField.setContentHuggingPriority(.defaultLow, for: .horizontal)
        textField.keyboardType = .numberPad
        configuration.textFieldConfiguration?(textField)

        return textField
    }

    public func updateUIView(
        _ uiView: UITextField,
        context: UIViewRepresentableContext<CurrencyTextField>
    ) {
        guard let textField = uiView as? WrappedTextField else { return }

        textField.borderStyle = borderStyle ?? textField.borderStyle
        textField.updateConfigurationIfNeeded(latest: configuration)
        textField.updateTextIfNeeded()
    }
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View where Self == CurrencyTextField {
    mutating func currencyTextFieldStyle<S>(_ style: S) -> some View where S : TextFieldStyle {
        if style is RoundedBorderTextFieldStyle {
            self.borderStyle = .roundedRect // mutates self, has to be mutating
        }
        return self
    }
}

// from the call site
CurrencyTextField(..)
  .currencyTextFieldStyle(RoundedBorderTextFieldStyle()) // Compiler Error: Cannot use mutating member on immutable value: function call returns immutable value

Other options would be to either:
1. have those properties on CurrencyTextField to be set during initialization, but honesty I think that would be even worst API for users, when in current block they configure what they want in the underlying text field.
One thing I really dislike is that currently the user has to import and rely on UIKit, but that wouldn't change by having properties instead of a block, given not all properties are "translatable" from SwiftUI to UIKit, e.g. Apple doesn't provide a way to transform a SwiftUI.Font into a UIKit.UIFont.

2. As said in that answer to rely on custom EnvironmentKeys which IHMO would be bad because EnvironmentKey/EnvironmentValues are for shared state/propagated through the view hierarchy and not for defining properties for each view instance:

CurrencyTextField()
   .environment(\.myCustomValue, "Some value") // Sets "Some value" for `EnvironmentKey.myCustomValue` - which can be accessed / updated anywhere in the view hierarchy

Please let me know in case you have a different opinion or any other ideas!

I rarely write swift and swiftui, may not know enough depth, may be difficult to help you.

But I api use feelings, later open a new issue.

No worries 👍 , your input was definitely valuable. I will get around focused(:_) cause having that working at least on iOS 15 is important.

Hello @YeungKC, I've added a more cleaner way of settings the text field as first responder, via a new Binding property called hasFocus.
You can see more here: #85 .
I'll do few more tests, update the doc and should make a release in the next few days.

Would be great if you could give it a try or give any inputs you may have. Thanks.