stevengharris/MarkupEditor

Keyboard dismissing when I select the "Bold" option from tool bar

Opened this issue ยท 16 comments

I created a simple project, added the framework using SPM and using the SwiftUI example. If I tap somewhere and select the "Bold" option to write something bolded, the keyboard is dismissed... I have to tap again in the same place and start writing.
iOS version 17.1
Xcode version 15.0.1
Framework version 0.6.0

Note 1: This happens only when I activate the bold, underline, italic and strike, when i disable the effect, the keyboard remains. And if I tap again, the keyboard is dismissed.
Note 2: For other options (dots list etc) works fine.

Thanks very much for reporting this and the additional notes you supplied. I think it must be a regression (maybe for iOS 17?), but I should be able to take a look at it within the next couple of days.

@stevengharris Any ETA for this issue ?

I started to look at it, thinking I had a good idea how to fix it, but it turns out not to be as simple as I thought. The fact it's not happening with things like indent is a good clue, considering I'm conceptually doing the same thing (invoke a JavaScript function, modify the DOM, notify the Swift side that there has been a change). Maybe some kind of change in the underlying WKWebView to accommodate changes in UITextInteraction in iOS 17 (e.g., https://developer.apple.com/videos/play/wwdc2023/10058/), but considering I never do anything with UITextInteraction directly in the MarkupEditor, I'm not sure. So, the short answer is: no ETA, but I'm looking at it.

FWIW, the simulator shows this problem does not show up in iOS 16.

Also in iOS 17, when positioning the cursor in the middle of a word using the "loupe" (i.e., long press showing a magnifier above the touch point), the console spits out:

Error: this application, or a library it uses, has passed an invalid numeric value (NaN, or not-a-number) to CoreGraphics API and this value is being ignored. Please fix this problem.
If you want to see the backtrace, please set CG_NUMERICS_SHOW_BACKTRACE environmental variable.

And if you put CG_NUMERICS_SHOW_BACKTRACE in the environment, you get this backtrace:

  <CGPathAddLineToPoint+71>
   <+[UIBezierPath _continuousRoundedRectBezierPath:withRoundedCorners:cornerRadii:segments:smoothPillShapes:]+2718>
    <+[UIBezierPath _continuousRoundedRectBezierPath:withRoundedCorners:cornerRadius:segments:]+167>
     <+[UIBezierPath _roundedRectBezierPath:withRoundedCorners:cornerRadius:segments:legacyCorners:]+338>
      <-[_UITextMagnifiedLoupeView layoutSubviews]+2240>
       <-[UIView(CALayerDelegate) layoutSublayersOfLayer:]+2141>
        <_ZN2CA5Layer16layout_if_neededEPNS_11TransactionE+527>
         <_ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE+67>
          <_ZN2CA7Context18commit_transactionEPNS_11TransactionEdPd+706>
           <_ZN2CA11Transaction6commitEv+728>
            <_UIApplicationFlushRunLoopCATransactionIfTooLate+70>
             <__processEventQueue+9465>
              <__eventFetcherSourceCallback+163>
               <__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__+17>
                <__CFRunLoopDoSource0+157>
                 <__CFRunLoopDoSources0+215>
                  <__CFRunLoopRun+919>
                   <CFRunLoopRunSpecific+557>
                    <GSEventRunModal+137>
                     <-[UIApplication _run]+972>
                      <UIApplicationMain+123>
                       <__swift_destroy_boxed_opaque_existential_1Tm+12707>
                        <$sSo21UIApplicationDelegateP5UIKitE4mainyyFZ+123>
                         <$s11SwiftUIDemo11AppDelegateC5$mainyyFZ+39>
                          <main+24>
                           109d023ee                            1179a43a6

The loupe positioning still works okay, but ๐Ÿค”.

Some potentially related issues:

https://developer.apple.com/forums/thread/731700

simonbs/Runestone#314

Mostly just makes me think this is going to require some kind of weird hack to fix without Apple themselves fixing it. I will probably try to reproduce it using a very simple test case with only WKWebView and submit the problem as a formal Technical Support Incident.

We can give it a try. Let me know after you solve this, and I will test it out :D

@stevengharris also, another issue I noticed on a freshly installed app, when the MarkupEditorView begins editing, the UIPasteBoard permission is triggered and it ask to allow the paste action...

I had thought that was all taken care of in #78, but, again, maybe something changed for iOS 17. I will check it out and raise a separate issue for it if needed.

@stevengharris any update on this issue?

I have not had the time to dig in further. If you would open a separate issue for the UIPasteboard permission problem with any details to reproduce, it will help me track it. Thx.

@stevengharris any update on this issue ?

No, I think I will have to dig into UITextInteraction to fix it but have not had the time.

Hello @stevengharris,
I have the same issue as described above. Do you maybe have a fix or a workaround for this ?

Unfortunately, no. I spent some time trying to sort out UITextInteraction, and am not sure it is a path to fixing the issue, which seems to be in WKWebView itself. Anyway, sorry, but I haven't been able to make any progress on it. Do you know if it is still a problem on iOS 18?

Yes it is still an issue on iOS 18.

It occurred to me that maybe a workaround would be to force the keyboard to show after bold/italic etc, although doing so takes some swizzling. This was discussed a long time ago in this issue/comment #88 (comment). As Gavin notes in the comment, I once had commented-out code in MarkupWKWebView to do this, but I removed it completely when I did what I felt was a proper job on getting focus. Here is the old code I just resurrected from git (no idea if it works, but putting it here for my own purposes and in case someone wants to take a closer look themselves). The idea would be to restore this code in working form to MarkupWKWebView and then call showKeyboard(), perhaps inside the evaluateJavaScript closure of MarkupWKWebView.bold(handler:), italic, etc.

typealias NewClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Voi
func setKeyboardRequiresUserInteraction(_ value: Bool) {
    guard
        let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else {
        print("Cannot find the WKContentView class")
        return
    }
    let ios13Selector: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:")
    if let method = class_getInstanceMethod(WKContentViewClass, ios13Selector) {
        swizzleAutofocusMethod(method, ios13Selector, value)
    }

func unsetKeyboardRequiresUserInteraction() {
    guard
        let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else {
        print("Cannot find the WKContentView class")
        return
    }
    let ios13Selector: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:")
    if let method = class_getInstanceMethod(WKContentViewClass, ios13Selector) {
        unswizzleAutofocusMethod(method, ios13Selector)
    }

func swizzleAutofocusMethod(_ method: Method, _ selector: Selector, _ value: Bool) {
    print("swizzling")
    let originalImp: IMP = method_getImplementation(method)
    let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self)
    let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
        original(me, selector, arg0, !value, arg2, arg3, arg4)
    }
    let imp: IMP = imp_implementationWithBlock(block)
    method_setImplementation(method, imp)

func unswizzleAutofocusMethod(_ method: Method, _ selector: Selector) {
    print("unswizzling")
    let originalImp: IMP = method_getImplementation(method)
    let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self)
    let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in
        original(me, selector, arg0, arg1, arg2, arg3, arg4)
    }
    let imp: IMP = imp_implementationWithBlock(block)
    method_setImplementation(method, imp)

public func showKeyboard() {
    setKeyboardRequiresUserInteraction(false)
    becomeFirstResponder()
    unsetKeyboardRequiresUserInteraction()
}

Since it's clear this problem isn't going to go away on the WKWebView end even in iOS 18, I will try to circle back around to this approach as a workaround.