0

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
                )
            )
        }
    }
}

scaly
  • 509
  • 8
  • 18
  • I would not say it is a bug... tap gesture is not a mouse pointer, it is detected by spot, not by one hot pixel. And it is not by padding - the behavior will be the same if you remove padding and tap a bit outside of secure field - gesture will fire. – Asperi Aug 14 '20 at 05:26
  • @Asperi If it's not a bug, then please tell me how we're supposed to add a tap gesture recognizer to only the SecureField (which is easy to do in UIKit)β€”or if this is impossible, then why it should be impossible? If it's impossible, then how else do they expect us to do something as simple as changing the border color of the field any time it is the first responder (where typed-in text is going)? I've read all the documentation and SwiftUI headers around Gesture, and TextField, and tried many possibilities too numerous to list here, but a solution has yet to present itself. – scaly Aug 14 '20 at 08:41

1 Answers1

0

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
    }
}
scaly
  • 509
  • 8
  • 18