Files
edgeever/apps/ios/EdgeEver/Editor/WKWebView+ProgrammaticKeyboard.swift
tianma-if 96e4473a1b fix(ios): create-back commit, press motion, editor keyboard and toolbar
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.
2026-08-07 14:54:04 +08:00

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