UIWebView에는 위 속성을 통해 유저의 interaction이 없더라도 프로그래밍으로 keyboard를 표시할 수 있었지만 해당 스펙이 WKWebView에서는 존재하지 않으므로..ㅎ (대체 왜 안 넣어준거야) 알음알음 검색하여 대안을 찾았다. 함수를 작성해주신 stackoverflow 형님들에게 감사를 전한다. 미래의 나를 위해 아카이빙
https://nshipster.com/wkwebview/
UIWebview의 스펙과 WKWebView의 스펙을 비교해볼 수 있는 페이지
https://stackoverflow.com/questions/32449870/programmatically-focus-on-a-form-in-a-webview-wkwebview
감사합니다.. Objc 코드를 swift로 바꾸어 주셔서..
import UIKit
import Foundation
import WebKit
typealias OldClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Any?) -> Void
typealias NewClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void
extension WKWebView {
func setKeyboardRequiresUserInteraction( _ value: Bool) {
guard
let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else {
print("Cannot find the WKContentView class")
return
}
let olderSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:")
let newSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
let newerSelector: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
let ios13Selector: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:")
if let method = class_getInstanceMethod(WKContentViewClass, olderSelector) {
let originalImp: IMP = method_getImplementation(method)
let original: OldClosureType = unsafeBitCast(originalImp, to: OldClosureType.self)
let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in
original(me, olderSelector, arg0, !value, arg2, arg3)
}
let imp: IMP = imp_implementationWithBlock(block)
method_setImplementation(method, imp)
}
if let method = class_getInstanceMethod(WKContentViewClass, newSelector) {
self.swizzleAutofocusMethod(method, newSelector, value)
}
if let method = class_getInstanceMethod(WKContentViewClass, newerSelector) {
self.swizzleAutofocusMethod(method, newerSelector, value)
}
if let method = class_getInstanceMethod(WKContentViewClass, ios13Selector) {
self.swizzleAutofocusMethod(method, ios13Selector, value)
}
}
func swizzleAutofocusMethod(_ method: Method, _ selector: Selector, _ value: Bool) {
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)
}
}
회사에서 웹뷰를 많이 쓰는데 나는 웹뷰에 대해서 너무 모르는 것 같다. 기회가 있을 때마다 웹뷰에 관련한 건 자세하게 뜯어보는 습관을 가지려 한다.
guard let WKContentViewClass: AnyClass = NSClassFromString("WKContentView") else {
print("Cannot find the WKContentView class")
return
}
isFocusingElement
, resigningFirstResponder
등을 가지고 있었다. let olderSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:")
let newSelector: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
let newerSelector: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:")
let ios13Selector: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:")
if let method = class_getInstanceMethod(WKContentViewClass, olderSelector) {
...
sel_getUid(:UnsafePointer<CChar\>)
내가 register 하고 싶은 메서드를 인자 값으로 넘기면 objc runtime system에 해당 메서드를 register할 수 있다. (아직 메서드를 가져온건 아님!)class_getInstanceMethod(:AnyClass?,:Selector) -> Method
이름 그대로 해당 클래스에서 selector와 일치하는 이름의 메서드 객체를 반환한다.https://github.com/WebKit/WebKit
/Source/WebKit/UIProcess/ios/WKContentViewInteraction.h
선임님의 도움을 받았다. 아마 이러한 오픈소스에서 알아내는 것으로 보인다.
ios 14,15도 ios13과 동일한 메서드를 사용하는지 동작에는 문제가 없다.
코드 상으로는 userIsInteracting parameter에 bool 값을 넘겨서 제어한다.
- (void)_elementDidFocus:(const WebKit::FocusedElementInformation&)information userIsInteracting:(BOOL)userIsInteracting blurPreviousNode:(BOOL)blurPreviousNode activityStateChanges:(OptionSet<WebCore::ActivityState::Flag>)activityStateChanges userObject:(NSObject <NSSecureCoding> *)userObject;
func swizzleAutofocusMethod(_ method: Method, _ selector: Selector, _ value: Bool) {
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)
}
IMP
는 Opaque C pointer를 의미한다 (환장하겠네)method_getImplementation(:Method)->IMP
method의 implementation을 반환한다. (objc에는 헤더랑 impl이 따로 있으니까..그래서일까?)unsafeBitCast(_x:T, to:U.Type)->U
typealias NewClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void
@convention(c)
c function에 swift의 closure를 넘길 수 있는 모양이다. 그러니 이 어노테이션이 붙어있다면 c function에 넘길 closure라고 보면 된다. (callback 함수가 될 것)@convention(block)
objc의 block과 swift의 closure는 근본적으로 다른데 이 어노테이션을 붙이면 closure를 objc runtime에 노출시킬 수 있는 것으로 보인다. (그냥 쉽게 swift closure를 objc block으로 바꿨다고 생각하면 된다.)imp_implementationWithBlock(_ block:) -> IMP
function에 대한 포인터를 생성한다.method_setImplementation(_m: Method, _imp: IMP) -> IMP
이제까지 바리바리 준비한 method의 implement를 최종적으로 set한다그러게 애플은 왜 keyboardDisplayRequiresUserAction를 없애서 이렇게 복잡한 상황을 야기한 것이냐. swift에서 objc 메서드 하나 호출하는게 이렇게 복잡할 일인가. 쓰면서도 정말 머리가 아팠다. 결과적으로 focus를 했을 때에 이것이 UserAction이 아니지만 키보드를 나타나게 만들 수 있도록 setKeyboardRequiresUserInteraction(false)
를 하는 지난한 여정이었다.
감사합니다.
덕분에 사나흘 고민했던 문제가 해결되었습니다.
더하여 깊이있게 고찰하신 내용을 글로 남겨주셔서 제 배움에도 큰 도움이 되었습니다.
첨언드리면 OS 13 ~ 17까지 동작이 되네요.