Experiencing a weird bug in SwiftUI in Xcode 11.6 / iOS 13.6 with the below code.

It will render two text fields with a white border around them (you can change to black border if you're not using Dark Mode). The bug is that the hit area of the tap gesture recognizer is not the same as the hit area of the SecureField. So you can tap on the edges of the field and although the recognizer fires, the actual text field does not become the first responder.

To illustrate this, I set the background color of both UITextFields (which SwiftUI uses under the hood) to green. To make a given UITextField "become the first responder," you must tap inside this green area, otherwise you cannot type into that field.

The problem is that because our app is to be cross-platform, i.e. it would run on AppKit or UIKit, we do not want to use any methods based on UIKit to handle how the UI works. We need to know which field the user is currently editing so we can set the border color of that field to green (as per design requirements of our UIX team). So, since we cannot set up delegates to listen on the normal UITextField delegate methods, we decided upon using tap gesture recognizers to determine when the user is editing a given field.

However now we're experiencing this issue where the areas of padding are included in the tapGestureRecognizer that we added in SwiftUI, but the underlying text field itself does not receive such a gesture unless it's in the smaller area inside the padding.

What is a solution to this that can be done purely in SwiftUI?

text fields

import PlaygroundSupport
import SwiftUI
import UIKit

public extension View {
    var any: AnyView { AnyView(self) }
}
struct Value: Identifiable {
    typealias ID = Int
    let id: ID
    var text: String = ""
}
struct MyView: View {
    @State var fields: [Value] = [Value(id: 0), Value(id: 1)]
    init() { 
        UITextField.appearance().backgroundColor = .green
    }
    
    var body: some View {
        ForEach(fields) { field  in
            SecureField(
                " ",
                text: self.$fields[field.id].text,
                onCommit: {
                    print("committed")
            })
                .onTapGesture {
                    print("tapped")
            }
            .padding(12)
            .overlay(
                RoundedRectangle(
                    cornerRadius: 5
                ).stroke(
                    Color.white,
                    lineWidth: 2
                )
            )
        }
    }
}

1

There are 1 best solutions below

0
On

Laugh if you will but this freakin' works (have it in my env. object):

class MrEnvironment: ObservableObject {
       /// Set this to id in .onTapGesture of the View
       @Published public var tappedId: Int? = nil
        
       /// Call this later from wherever to resign first responder
       public var resignFirstResponder: (() -> Bool)?

       private static var textFieldDidStartEditingPublisher = 
           NotificationCenter.default.publisher(
               for: UITextField.textDidBeginEditingNotification) 

       /// Only publishes when user tapped on the field AND started editing it
       private var whichTextFieldIsEditingPublisher =
           Publishers.Zip(Self.textFieldDidStartEditingPublisher, $tappedId)
       
       let win: AnyCancellable = 
           whichTextFieldIsEditingPublisher
               .sink { [weak self] notification, _ in   
                   let view = notification.object as? UIView
                   self?.resignFirstResponder = view?.resignFirstResponder
                   // now since we know id of the field we can do anything to it
                   // like set the border color etc.
               }




    // etc. other stuff
    }
}