遭遇した実装
swiftで開発していて、
WKWebView内でキーボードのtoolbarをカスタマイズしたかった。
↑ この完了ボタンつけるとかのカスタマイズ。
普段モバイルアプリケーション内だったらtextFieldとかをゴニョゴニョすればいいけど
webView内だとそうはいかなかった。
出会った黒魔術
既存のWKWebViewクラスを拡張して、
addIndexAccessoryViewというtoolbarの設定を反映してくれる関数を生やしてる。
これをwebViewを置いてるVCで使えばtoolbarカスタマイズできちゃうよって実装。
結果実装できたけど、先輩曰く、黒魔術だからNGとのこと。😭
本題: 黒魔術ってなんだろう😈
戻ってきたものはしょうがないし、
知識不足の自分に「黒魔術」ってワードが、
だいぶ刺さったので調べた。
ObjCに黒魔術で調べるとめっちゃ出てくる。正体はランタイム関数らしい。
ランタイム関数
簡単にいうとコンパイル後から実行時までに処理に干渉できる関数のこと。
例えば、コンパイル時になかった関数を追加するとか。。。
(すごいな。冷静に考えたら普通にコワい)
今回のコードで問題だったのもランタイム関数のAssociated Objectがいたからの模様。
海外のサイトでも不吉な呪文とか言われてるのか。。。。
https://nshipster.com/associated-objects/
なんで黒魔術って言われるかはこんな現場の声があったりする。
https://qiita.com/sasho/items/318e28431ddd0338149d
該当の黒魔術入りコード
細かいところ翻訳してみた。
WKWebView置いてるViewController
final class HogeWebViewController: UIViewController {
private var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame:CGRect(x:0, y:0, width:self.view.bounds.size.width, height:self.view.bounds.size.height))
webView.uiDelegate = self
webView.navigationDelegate = self
//webViewに生やしたaddIndexAccessoryViewにカスタムしたtoolbarをセット。
webView.addIndexAccessoryView(toolbar: self.getToolbar())
self.view = webView
}
//toolbarのカスタマイズしてインスタンス化
private func getToolbar() -> UIToolbar? {
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
let doneItem = UIBarButtonItem(title: "完了", style: .done, target: self, action: #selector(done(sender:)) )
let flexibleSpaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
doneItem.tintColor = .black
toolbar.setItems([flexibleSpaceItem, doneItem], animated: false)
toolbar.sizeToFit()
return toolbar
}
@objc func done(sender: UIBarButtonItem) {
webView?.resignFirstResponder()
}
}
WKWebViewの拡張
//extensionではストアドプロパティを含められないからここに逃してる グローバルなの怖いよな。。。
//unsigned short int(UInt): 符号なし整数配列。ポインタが2バイトずつ増加。(8-bit unsigned integer arrays)
//ポインタはアドレスの格納庫。変数を紐付けられる
var ToolbarHandle: UInt8 = 0
extension WKWebView{
//この関数が、カスタムしたtoolbarを渡すと反映してくれるようにwebViewの機能を拡張してる
func addIndexAccessoryView(toolbar: UIView?) {
guard let toolbar = toolbar else {return}
//objcには黒魔術がある = ランタイム関数
//その中でもよく出てくるのが
//Associated Object オブジェクトに保持可能なメンバ変数を追加する関数(← objc_hogehogeみたいな関数たちはこれ)
//関連したオブジェクトを保存しておく事ができる 関連もとのオブジェクトが解放されると一緒に解放
//swiftはではNSObjectのサブクラスであれば利用可能(WKWebVeiw<UIView<UIResponder<NSObjectだから使える)
//Associated Objectは設定にobjc_setAssociatedObject()、取得にobjc_getAssociatedObject()を用いる
//objc_setAssociatedObject: AssociatedObject設定! 関連付けるための特定のキー(ポインタ)と関連付けポリシーを使用して、特定のオブジェクト(webView)に関連する値(toolbar)を設定します。 パラメータ(引数の変数)(object,key,value,policy)
//object: 関連付けたいソースオブジェクト self(webview)
//key: 関連付けるためのキー 渡すのはポインタ(変数の格納場所(アドレス)を指す:本当に知りたい情報は変数でそれを知っているのがポインタ、情報を得るための経由地)
//value: ソースオブジェクトにキーで関連付けたい値。既存の関連付けをなくしたいときはnil渡す
//policy: 関連付けの方針。objc_AssociationPolicyがenumになってるので提供されているポリシーから選択。OBJC_ASSOCIATION_RETAIN_NONATOMICは関連付けが強力で関連付けはマルチスレッドでの同時実行がされない(☁️マルチスレッドで〜を指定してるのなんでだろ)
//&はCの単項演算子。被演算子(元の変数)のアドレスを返す。定数。(ポインタ) &ToolbarHandle ☁️toolbarの置き場っぽい
//被演算子(Operand)が1つの演算子。
objc_setAssociatedObject(self, &ToolbarHandle, toolbar, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
//candidateView = webView内のscrollViewに付属するviewの中から必要なview(WKContentView)を一時的に代入する用
var candidateView: UIView? = nil
for view in self.scrollView.subviews {
let description: String = String(describing: type(of: view))
if description.hasPrefix("WKContent"){
candidateView = view
break
}
}
//candidateViewがWKContentViewじゃなければ(nil)早期リターン
guard let targetView = candidateView else { return }
//WKContentViewを引数にとる自作の実装を定義した新しいクラスを返してる。
let newClass: AnyClass? = classWithCustomAccessoryView(targetView: targetView)
guard let targetNewClass = newClass else { return }
//object_setClass: オブジェクトのクラスを返す(組み合わせを設定) パラメータ(object,class)
//classWithCustomAccessoryViewが使えるようになる
object_setClass(targetView, targetNewClass)
}
//_CustomInputAccessoryViewってクラスができる。getCustomInputAccessoryView()でtoolbarを返せるようになる
func classWithCustomAccessoryView(targetView: UIView) -> AnyClass? {
//targetView.superclass != WKApplicationStateTrackingViewならnil(☁️なんでnil返す必要ある?)
guard let _ = targetView.superclass else { return nil }
//AccessoryViewいじるclass名を指定 プライベートなやつ
let customInputAccessoryViewClassName = "_CustomInputAccessoryView"
//NSClassFromString: クラス名からオブジェクトの呼び出し。存在しない時の戻り値nil
var newClass: AnyClass? = NSClassFromString(customInputAccessoryViewClassName)
//newClass: WKContentViewクラスをスーパークラスに持つ_CustomInputAccessoryViewってクラス
if newClass == nil {
//objc_allocateClassPair: 新しいクラスとメタクラスを作成。
//パラメータ(superclass,name,extraBytes)戻り値: -> AnyClass?(新しいクラスか生成できなかった時Nilクラス)
//superclass: 新しいクラスのスーパークラス。なければnil
//name: クラス名の文字列
//extraBytes: クラスおよびメタクラスオブジェクトの最後にインデックス付きivar(objcの変数。名前と型と、オブジェクトのどこに格納されるかの情報を持つ)に割り当てるバイト数。メモリ領域の確保。通常0。
//object_getClass: 引数のオブジェクトのクラスを返す。(WKContentView)新しいクラスはこれを継承
newClass = objc_allocateClassPair(object_getClass(targetView), customInputAccessoryViewClassName, 0)
} else {
return newClass
}
//newMethod: 新しくつけ足すgetCustomInputAccessoryViewメソッド(追加したい実装)
//class_getInstanceMethod: 指定されたクラスの指定されたインスタンスメソッドを返す。パラメータ(class,selector)
//セレクターは自作。getCustomInputAccessoryViewはtoolbarを取得。
//☁️これがWKWebViewから生えているのはなんでだ。。
let newMethod = class_getInstanceMethod(WKWebView.self, #selector(WKWebView.getCustomInputAccessoryView))
//class_addMethod: 指定された名前と実装を持つ新しいクラス(newClass)に 新しいメソッド(newMethod)を 追加する。
//パラメータ(cls,name,imp,types) 戻り値 ->bool
//cls: メソッドを追加するクラス
//name: 追加されているメソッドの名前を指定するセレクタ(☁️上書きしたいメソッドってことなのだろうか。。。?)
//imp: 追加するメソッドの機能(実装)
//types: メソッドの引数の型を指定。
class_addMethod(newClass.self,
#selector(getter: WKWebView.inputAccessoryView),
//method_getImplementation: メソッドの実装を返す。
//getCustomInputAccessoryViewって追加したメソッドのIMP(Implementation:実装。(メソッドを実装する開始のポインタ))返してる
method_getImplementation(newMethod!),
//method_getTypeEncoding: メソッドの パラメータ(引数の変数)と戻り値の型 の文字列を返す。-> UnsafePointer<Int8>? メモリにアクセスできるようにしてる。
method_getTypeEncoding(newMethod!)
)
//objc_registerClassPair: objc_allocateClassPair(_:_:_:)を使って割り当てられたクラスを登録 パラメータ(cls)
objc_registerClassPair(newClass!)
return newClass
}
//セットしてあるtoolbarを取得
@objc func getCustomInputAccessoryView() -> UIView? {
//WKWebView
var superWebView: UIView? = self
while (superWebView != nil) && !(superWebView is WKWebView) {
superWebView = superWebView?.superview //== WKWebView
}
guard let webView = superWebView else { return nil }
//objc_getAssociatedObject: AssociatedObject取得!関連付けた新しいオブジェクトを返す。引数(object, key)
//object: 関連付けるソースオブジェクト。, key: 関連付けのためのキー。(単項演算子、ポインタ)
//つまりcustomInputAccessoryはtoolbarになる。
let customInputAccessory = objc_getAssociatedObject(webView, &ToolbarHandle)
return customInputAccessory as? UIView
}
}
まとめ
黒魔術ってobjcのランタイム関数のこと。
ランタイム関数は、コンパイル後に処理に干渉できちゃう関数。
せっかく強力なswiftの型付もガン無視できる。
なんかもう力づくでびっくりしたコワい。
もうレイアウト調整くらいで黒魔術は使いません。
ちなみに黒魔術やめて今回どうなったかというと、
アプリケーションの利用国や言語の設定を日本にすることで、日本語化することになりました。
https://qiita.com/ko2ic/items/8918034d940f66fee97d
objcもcもちらっと覗けて言語たちの繋がりが垣間見えて面白かった☺️☺️
参考サイト 🙇🏻♂️🙇🏻♀️
https://qiita.com/fmtonakai/items/e9036dec4af2609b5715
https://stackoverflow.com/questions/51837022/how-to-edit-accessory-view-of-keyboard-shown-from-wkwebview
https://robopress.robotsandpencils.com/swift-swizzling-wkwebview-168d7e657106
https://draveness.me/ao
https://www.grapecity.com/developer/support/powernews/column/clang/024/page03.htm#1
https://github.com/WebKit/webkit
大部分はリファレンスにお世話になりました。やっぱリファレンス大切ですな。。。。
知識浅いのでツッコミ大歓迎です🙇🏻♂️!!!