How do you get the cursor to appear programmatically in a custom UITextInput with UITextInteraction?

I have created a custom input field by conforming to UITextInput. It is setup to use UITextInteraction. Everything works very well. If the user taps on the custom field, the cursor (provided by UITextInteraction) appears. The user can type, select, move the cursor, etc.

But I'm stumped trying to get the cursor to appear automatically. With a normal UITextField or UITextView you simply call becomeFirstResponder(). But doing that with my custom UITextInput does not result in the cursor appearing. It only appears if the user taps on the custom field.

I don't know what I'm missing. I don't see any API in UITextInteraction that can be called to say "activate the cursor layer".

Does anyone know what steps are required with a custom UITextInput using UITextInteraction to activate the cursor programmatically without the user needing to tap on the custom field?

Replies

Interesting! I also have some code that has for years used UITextInput on a custom view; I was drawing a cursor myself.

I've just tried - with a very quick hack - to add UITextInteraction. It seems promising, not least because it provides the loupe (magnifier) functionality. But as you say, the cursor ("beam") is not initially shown. I can enter characters and it does call my caretRectForPosition method for the position where the cursor should be, but it doesn't draw it until I tap somewhere.

I'll let you know if I discover anything useful.

  • I’ve made attempts to generate touch events or directly call target/action pairs of the gestures provided by UITextInteraction but so far no luck. Too many private APIs involved for me to find a working hack along those lines.

Add a Comment

Hi Rick,

I may have solved this! Try this:

  1. Add the UITextInteraction to your view.
  2. Iterate view.interactions. One of them will be a UITextSelectionDisplayInteraction that was added by the UITextInteraction.
  3. When you call becomeFirstResponder to present the keyboard, also set activated = true on the UITextSelectionDisplayInteraction. The cursor will appear!

Note that has only been tested for about 30 seconds so far!

You may be interested to know that I found this while trying to solve the related problem of how to avoid a "ghost" edit menu being left when I call resignFirstResponder; see https://developer.apple.com/forums/thread/751307

It may be worth not using UITextInteraction, and instead using UITextSelectionDisplayInteraction and UIEditMenuInteraction (etc?) directly, with your own logic to replace the glue provided by UITextInteraction.

  • Sorry, just saw your reply. This is great! This ended up being so simple - for iOS 17. Unfortunately I need to support iOS 15+ and UITextSelectionDisplayInteraction is only for iOS 17+. I've spent the last several hours trying to find a solution for iOS 16. Why is this so hard? Thanks so much at least for the iOS 17 solution. I'll keep digging for iOS 15/16. I'll post a full solution when I come up with one.

Add a Comment

After lots of digging, I've finally come up with a solution that works under iOS 15 - iOS 17 (tested with iOS 15.4, 16.4, 17.4, and 17.5).

Under iOS 17, the use of UITextSelectionDisplayInteraction activated only worked if my custom UITextInput already had some text in it. I don't know fi that's an issue with my custom input view or not but since my hack for iOS 15/16 also worked with iOS 17 I didn't spend any time trying to figure it out.

I created an extension to UITextInput that add a method to activate the cursor that you can call from the becomeFirstResponder of the custom input view. It makes use of a class that fakes a tap gesture enough to make the code work. This is an ugly hack that makes use of several private APIs. It works in development. I've made no attempt to use this code in a production App Store app.

@objcMembers class MyFakeTap: NSObject {
    private let myView: UIView

    init(view: UIView) {
        self.myView = view

        super.init()
    }

    func tapCount() -> Int { return 1 }

    func touchesForTap() -> [UITouch] {
        return []
    }

    var view: UIView? {
        myView
    }

    var state: Int {
        get {
            return 1
        }
        set {

        }
    }

    func locationInView(_ view: UIView?) -> CGPoint {
        return .init(x: 5, y: 5)
    }
}

extension UITextInput {
    private var textSelectionInteraction: UITextInteraction? {
        if let clazz = NSClassFromString("UITextSelectionInteraction") {
            return textInputView?.interactions.first { $0.isKind(of: clazz) } as? UITextInteraction
        } else {
            return nil
        }
    }

    func activateCursor() {
        if let textSelectionInteraction {
            let tap = MyFakeTap(view: self.textInputView ?? UIView())
            textSelectionInteraction.perform(NSSelectorFromString("_handleMultiTapGesture:"), with: tap)
        }
    }
}

Note that under iOS 15, if you call activateCursor() from becomeFirstResponder, be sure you only do so if the view is not already the first responder. Otherwise you will end up in an infinite loop of calls to becomeFirstResponder.

    override func becomeFirstResponder() -> Bool {
        // Under iOS 15 we end up in an infinite loop due to activeCursor indirectly calling becomeFirstResponder.
        // So don't go any further if already the first responder.
        if isFirstResponder { return true }

        let didBecomeResponder = super.becomeFirstResponder()

        if didBecomeResponder {
            self.activateCursor()
        }

        return didBecomeResponder
    }