Make create Back commit like Android so notes appear in the list and the next New note opens empty. Harden memo-card press feedback, allow programmatic WKWebView keyboard focus, and pin the format toolbar above the scrolling editor body so it no longer jumps under the software keyboard.
80 lines
3.1 KiB
Swift
80 lines
3.1 KiB
Swift
import ObjectiveC
|
|
import UIKit
|
|
import WebKit
|
|
|
|
/// WKWebView defaults to "keyboard only after a user tap".
|
|
/// Programmatic TipTap/ProseMirror `focus()` then draws a caret but **never** raises the IME —
|
|
/// the same issue React Native WebView solves with `keyboardDisplayRequiresUserAction={false}`.
|
|
///
|
|
/// We install a one-time method override on the internal WK content view so
|
|
/// `_elementDidFocus:…userIsInteracting:…` always reports `userIsInteracting = true`.
|
|
/// This matches RNCWebViewImpl's well-known approach (iOS 13+ selector).
|
|
enum WKWebViewProgrammaticKeyboard {
|
|
private static var installedClassName: String?
|
|
private static var pending = false
|
|
|
|
/// Call once the web view has a content subview (after first layout / load).
|
|
static func allowProgrammaticKeyboard(on webView: WKWebView) {
|
|
if installedClassName != nil { return }
|
|
if pending { return }
|
|
|
|
guard let contentView = contentView(in: webView) else {
|
|
pending = true
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
pending = false
|
|
allowProgrammaticKeyboard(on: webView)
|
|
}
|
|
return
|
|
}
|
|
|
|
let cls: AnyClass = type(of: contentView)
|
|
let className = NSStringFromClass(cls)
|
|
if installedClassName == className { return }
|
|
|
|
// iOS 13+ (still used on current SDKs by react-native-webview).
|
|
let selector = sel_getUid(
|
|
"_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:"
|
|
)
|
|
guard let method = class_getInstanceMethod(cls, selector) else {
|
|
#if DEBUG
|
|
NSLog("WKWebViewProgrammaticKeyboard: selector missing on \(className)")
|
|
#endif
|
|
return
|
|
}
|
|
|
|
let original = method_getImplementation(method)
|
|
typealias OriginalFn = @convention(c) (
|
|
AnyObject, Selector, UnsafeRawPointer?, Bool, Bool, Bool, AnyObject?
|
|
) -> Void
|
|
|
|
let block: @convention(block) (
|
|
AnyObject, UnsafeRawPointer?, Bool, Bool, Bool, AnyObject?
|
|
) -> Void = { me, arg0, _, arg2, arg3, arg4 in
|
|
let fn = unsafeBitCast(original, to: OriginalFn.self)
|
|
// Force userIsInteracting = true so the software keyboard is allowed.
|
|
fn(me, selector, arg0, true, arg2, arg3, arg4)
|
|
}
|
|
method_setImplementation(method, imp_implementationWithBlock(block))
|
|
installedClassName = className
|
|
#if DEBUG
|
|
NSLog("WKWebViewProgrammaticKeyboard: installed on \(className)")
|
|
#endif
|
|
}
|
|
|
|
/// Prefer the internal content view as first responder (WKWebView itself often refuses).
|
|
@discardableResult
|
|
static func becomeFirstResponder(for webView: WKWebView) -> Bool {
|
|
allowProgrammaticKeyboard(on: webView)
|
|
if let content = contentView(in: webView), content.becomeFirstResponder() {
|
|
return true
|
|
}
|
|
return webView.becomeFirstResponder()
|
|
}
|
|
|
|
private static func contentView(in webView: WKWebView) -> UIView? {
|
|
webView.scrollView.subviews.first { view in
|
|
NSStringFromClass(type(of: view)).hasPrefix("WK")
|
|
}
|
|
}
|
|
}
|