IWKWebView 코드에 자바스크립트 핸들러 추가

     핸들러에 함수명을 적어주는게 보편적이지만 JS -> Native 많은 함수를 호출할 경우 여러개를 등록해야되서 스키마와 자바스크립트 두가지로만 구분하고 Web에서 파라메터를 이용해서 함수명을 호출하게 구현했다. (test.html 예제를 보면 알 수 있음)

func configuration() {
						.... 생략 ....

	// Scheme 및 JS 함수 핸들러 등록
    let contentController = WKUserContentController()
    contentController.add(self, name: WebMessageHandler.customScheme.rawValue)
    contentController.add(self, name: WebMessageHandler.javascript.rawValue)
    
    let config = WKWebViewConfiguration()
    config.processPool = processPool!
    config.preferences.javaScriptCanOpenWindowsAutomatically = true
    config.preferences.javaScriptEnabled = true
        
    // WKUserContentController 자바스크립트 핸들러 등록 WebView 연결
    config.userContentController = contentController
    				    .... 생략 ....
}

 

IWKwebViewDelegate 

@objc protocol IWKWebViewDelegate: AnyObject {
    /// 웹뷰 로딩 완료
    @objc func webviewDidFinishNavigation(webView: WKWebView);
    /// 웹뷰 새창 추가 완료
    @objc optional func webviewDidFinishAddNewWebView(webView: WKWebView);
    /// 웹뷰 로딩 실패
    @objc optional func webViewdidFailProvisionsNavigation(webView: WKWebView);
    /// 자바스크립트 메세지 핸들러
    @objc optional func javascriptMessageHandler(function: String, param: Dictionary<String, Any>);
    /// 커스텀스키마  메세지 핸들러
    @objc optional func schemeMessageHandler(schemeUrl: URL);
    
}

 

WKScriptMessageHandler 프로토콜 구현

// MARK: WKScriptMessageHandler
extension IWKWebView: WKScriptMessageHandler {
    
    /*
     WKScriptMessageHandler
     참고자료 - https://swieeft.github.io/2020/02/23/ImportSMSiOS.html
     JS -> Native 연동
     Web -> Scheme 연동
     
     위에서 등록한 customScheme / javascript 핸들러 메세지 받는다.
     */
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        
        let handlerName = WebMessageHandler(rawValue: message.name)
        
        // Scheme 및 JS -> Native 구분
        switch handlerName {
        case .customScheme:
            
            if let bodyString = message.body as? String {
                if let url = URL(string: bodyString) {
                    delegate?.schemeMessageHandler?(schemeUrl: url)
                }
            }
        case .javascript:

            if let bodyString = message.body as? String {
                let params = bodyString.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\\", with: "")
                
                if let jsDic = convertToDictionary(text: params) {
                    /*       함수명           파라메터
                     형식 - {"login" : {"idx":1, "name":"twok"}}
                           {"logout" : {}}
                     
                     Web -> Native javascript 함수 호출 처리
                     참고자료 : https://life-shelter.tistory.com/315
                     */
                    if let jsFunc = jsDic.keys.first?.description {
                        if let params = jsDic[jsFunc] as? Dictionary<String, Any> {
                            delegate?.javascriptMessageHandler?(function: jsFunc+":", param: params)
                        }
                    }

                }
            }
        default: break
        }
    }
    
    // Json String -> Dictionary 형태로 변경
    func convertToDictionary(text: String) -> [String: Any]? {
        if let data = text.data(using: .utf8) {
            do {
                return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
            } catch {
                print(error.localizedDescription)
            }
        }
        return nil
    }
    
    /// Native -> Javascript 호출
    func evaluateJavaScript(javaScriptFuncString: String, param: String, completionHandler: ((Any?, Error?) -> Void)?) {
        log(direction: .SEND, ofType: self, datas: javaScriptFuncString, param)
        
        if let lastWebView = webViewList.last {
            lastWebView.evaluateJavaScript("\(javaScriptFuncString)(\(param))", completionHandler: completionHandler)
        }
    }
}

 

MainViewContoller.swift

extension URL {
    /// URL의 파라메터 값을 Dictionary로 변경
    var queryDictionary: [String: String]? {
        guard let query = self.query else { return nil}

        var queryStrings = [String: String]()
        for pair in query.components(separatedBy: "&") {

            let key = pair.components(separatedBy: "=")[0]

            let value = pair
                .components(separatedBy:"=")[1]
                .replacingOccurrences(of: "+", with: " ")
                .removingPercentEncoding ?? ""

            queryStrings[key] = value
        }
        return queryStrings
    }
}

// Native -> JS 델리게이트
extension MainViewController: MainViewControllerDelegate {
    
    func webviewEvaluateJavaScript(jsFunctionString: String, param: String, completionHandler: ((Any?, Error?) -> Void)?) {
        
        self.webBaseView.evaluateJavaScript(javaScriptFuncString: jsFunctionString, param: param) { result, error in
            
            completionHandler?(result, error)
        }
    }
}


class MainViewController: UIViewController {

    @IBOutlet weak var webView: IWKWebView!
    
    // JS -> Native Message Handler Class
    let javascriptMessageHandler = JavascriptMessageHandler()
    let customSchemeMessageHandler = CustomSchemeMessageHandler()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webView.delegate = self
//        webView.webViewLoad(urlString: "https://www.naver.com", httpMethod: .GET)
        webView.webViewHtmlLoad()
    } 
}

//MARK: IWKWebViewDelegate
extension MainViewController: IWKWebViewDelegate {
    //웹 페이지 로딩 완료
    func webviewDidFinishNavigation(webView: WKWebView) {
        let urlString: String = webView.url?.absoluteString ?? ""
        let string = String(describing: urlString.removingPercentEncoding)
        log(direction: .RECEIVE, ofType: self, datas: "WEBVIEW : \(string)")
    }
    
    //자바스크립트 메세지 핸들러
    func javascriptMessageHandler(function: String, param: Dictionary<String, Any>) {
        
        /*
         Javascript 데이터의 String 값으로 메서드를 지정하기 때문에
         인스턴스 메서드가 있는지 꼭 확인해야됨. 없는 메서드 호출시 오류 발생함.
         */
        let selector = Selector(function)
        let originalMethod = class_getInstanceMethod(JavascriptMessageHandler.self, selector)
        if originalMethod == nil {
            log(direction: .ERROR, ofType: self, datas: "JS->Native \(selector) : 호출할 수 없는 함수 입니다.")
            return
        }
        
        javascriptMessageHandler.perform(selector, with: param)
    }
    
    //커스텀 스키마 메세지 핸들러
    func schemeMessageHandler(schemeUrl: URL) {
        
        if let scheme = schemeUrl.scheme {
            switch scheme {
            case "mgapp":
                if let function = schemeUrl.host {
                    let selector = Selector(function+":")
                    let originalMethod = class_getInstanceMethod(CustomSchemeMessageHandler.self, selector)
                    if originalMethod == nil {
                        log(direction: .ERROR, ofType: self, datas: "CustomScheme->Native \(selector) : 호출할 수 없는 함수 입니다.")
                        return
                    }
                    
                    let param = schemeUrl.queryDictionary
                    customSchemeMessageHandler.perform(selector, with: param)
                }       
            default: break
            }
        }
    }
}

 

ScriptMessageHandler.swift  (JavaScript <-> Native 연동)

웹에서 호출하는 함수가 많을 경우가 있어 별도의 파일을 새로 만들고 JS -> Native 함수만 별도로 구현하게 분리

이부분은 개인의 성향에 맞게 구현 하면 된다.

import UIKit
import WebKit

class JavascriptMessageHandler: NSObject {
    
    weak var delegate: MainViewControllerDelegate?
    
    /// Native -> Javascript 전송
    private func evaluateJavaScript(function: String, jsonData: [String : Any]) {
        self.delegate?.webviewEvaluateJavaScript(jsFunctionString: function, param: jsonData.jsonString(), completionHandler: { result, error in
        	// JS Result 처리
        })
    }
    
    /*
     Web -> Native javascript 함수 호출 처리
     참고자료 : https://life-shelter.tistory.com/315
     */

//MARK: JS -> Native Function Call
    // 테스트용
    @objc func login(_ params: Dictionary<String, Any>?) {
        log(direction: .JAVASCRIPT, ofType: self, datas: "function : \(String(describing: params))")
        
        if params?.count == 0 {
            // 처리할 파라메터 없는 경우
            return
        }
        
        if let params = params {
            let jsParam = createJsonForJavaScript(for: (params))
            
            // 테스트용 코드 받은 파라메터를 다시 자바스크립트 함수로 전송 (Native -> JS)
            self.evaluateJavaScript(function: "nativeToJs", jsonData: jsParam) { result, error in
                
                log(direction: .JAVASCRIPT, ofType: self, datas: "result : \(String(describing: result))", "error : \(String(describing: error?.localizedDescription))")
            }
        }
    }
    
    @objc func logout(_ params: Dictionary<String, Any>?) {
        log(direction: .JAVASCRIPT, ofType: self, datas: "function : \(String(describing: params))")
    }
}

 

SchemeMessageHandler.swift  (Custom Scheme 연동)

웹에서 호출하는 스키마가 많을 경우가 있어 별도의 파일을 만들어 별도로 구현

import UIKit
import WebKit

class CustomSchemeMessageHandler: NSObject {
    
    weak var delegate: MainViewControllerDelegate?

//MARK: Custom SCheme -> Native Function Call
    @objc func externalBrowser(_ params: Dictionary<String, Any>?) {
        log(direction: .SCHEME, ofType: self, datas: "Params : \(String(describing: params))")
        
//        if params?.count == 0 {
//            // 처리할 파라메터 없는 경우
//            return
//        }
        
        if let jsParam = params?.jsonString() {
            
            self.delegate?.webviewEvaluateJavaScript(jsFunctionString: "webLog", param: jsParam, completionHandler: { result, error in
                
                log(direction: .JAVASCRIPT, ofType: self, datas: "result : \(String(describing: result))", "error : \(String(describing: error.debugDescription))")
            })
        }
    }
}

 

 

test.html 

<!--<html lang="ko">-->
<!doctype html>
<head>
<!--<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">-->
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style type='text/css'>
    html { font-family:Helvetica; color:#222; }
    h1 { color:#FE8FBD; font-size:24px; margin-top:24px; }
    button { margin:0 3px 10px; font-size:14px; border: 2px solid #000000; }
    .logLine_Native { border-bottom:1px solid #FFA67C; padding:4px 2px; font-family:courier; font-size:12px; }
    .logLine_JS { border-bottom:1px solid #D867D8; padding:4px 2px; font-family:courier; font-size:12px; }
</style>
</head>
<body>

<script>
    
    window.onerror = function(err) {
        log('window.onerror: ' + err)
    }
    
    var uniqueId = 1
    
    
    function log(message, data, type) {
        var log = document.getElementById('log')
        var el = document.createElement('div')
        el.className = type == 'native' ? 'logLine_Native' : 'logLine_JS'
        el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
        if (log.children.length) { log.insertBefore(el, log.children[0]) }
        else { log.appendChild(el) }
    }

   ////////////////////////////////////////////////
   // JS -> Native 테스트
    function jsToNative1() {
        //              함수명              파라메터
        // 파라메터 있는 경우
        var jsonObj = {"login" : {"idx":1, "name":"twok"}};
        
        window.webkit.messageHandlers.javascript.postMessage(JSON.stringify(jsonObj));
    }
    
    function jsToNative2() {
        //              함수명     파라메터
        // 파라메터 없는 경우
        var jsonObj = {"logout" : {}};
        
        window.webkit.messageHandlers.javascript.postMessage(JSON.stringify(jsonObj));
    }
    
    // Native -> JS 테스트
    function nativeToJs(message) {
        log('JS Call', message)
        return "OK"
        
    }
    
    // 커스텀 스키마 테스트
    function schemeTest() { // 커스텀 스키마 호출
        window.webkit.messageHandlers.customScheme.postMessage("myapp://externalBrowser?url=https://naver.com&target=")
    }
    
                                   
</script>

<div id='buttons'></div>
<div id='log'></div>
<h2> <a href="javascript:jsToNative1();">JS -> Native(Params)</a> </h2> <br />
<h2> <a href="javascript:jsToNative2();">JS -> Native(Null) </a> </h2> <br />
<h2> <a href="javascript:schemeTest();">schemeTest</a> </h2> <br />
</body>
</html>
 
 

+ Recent posts