관리 메뉴

Things take time

[SWIFT] 웹뷰와 자바스크립트 연동 (Native <-> JavaScript 통신 방법) 본문

iOS (기능)

[SWIFT] 웹뷰와 자바스크립트 연동 (Native <-> JavaScript 통신 방법)

겸손할 겸 2017. 5. 17. 14:38

[하이브리드 앱]


하이브리드 앱을 개발할 때는 네이티브(iOS)와 웹(JS)간의 통신을 통해 웹에서 네이티브의 기능을, 네이티브에서 웹을 호출할 수 있는 것이 핵심이다. 물론 그 바탕은 웹뷰에서 이루어진다.


웹뷰는 기본적으로 UIWebview가 아닌, WKWebview기준이다.


1. 환경 설정


1-1) 딜리게이트 설정을 위한, 프로토콜 3개를 상속받는다.

WKNavigationDelegate, WKUIDelegate,  WKScriptMessageHandler

1-2) 연동을 위한 변수를 선언한다.

        let contentController = WKUserContentController()
        let config = WKWebViewConfiguration()

1-3) 웹뷰 변수 및 딜리게이트를 설정한다.

        webView = WKWebView(frame: self.containerView.frame, configuration: config)
        webView.uiDelegate = self
        webView.navigationDelegate = self


2. Native -> JS Call

특이하게도 iOS는 네이티브에서 자바스크립트를 호출할 수 있는 방법이 2가지다. 단, 그 중 하나는 웹뷰의 HTML문서가 시작될 때만 호출 가능하며 나머지 하나는 일반적으로 중간 중간 호출할 수 있다.


2-1) HTML문서 로드 시 JS함수 호출하기

        // native -> js call (문서 시작시에만 가능한, 환경설정으로 사용함), source부분에 함수 대신 HTML직접 삽입 가능
        let userScript = WKUserScript(source: "redHeader()", injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        contentController.addUserScript(userScript)

2-2) Native에서 일반적인 JS함수 호출하기

            webView.evaluateJavaScript("getLocal()", completionHandler: {(result, error) in
                if let result = result {
                    print(result)
                }
            })

2-3) 공통 작업

        config.userContentController = contentController

WKUserContentController라는 클래스 변수인 contentController에 addUserScript를 통해 redHeader()라는 함수를 문서가 읽혀지고 완료되었을 때 호출되도록 지정할 수 있다.

아니면 WKWebview.evauluateJavaScript함수를 통해 해당 HTML페이지에 기술된 자바스크립트 함수를 호출할 수 있으며 그에 대한 콜백 핸들러로 결과 및 에러 메시지를 얻어올 수 있다.


3. Js -> Native Call

하이브리드 앱을 개발하다 보면 네이티브에서 자바스크립트를 호출하는 것보다 웹 자바스크립트에서 네이티브를 호출하는 경우가 많다.

        // js -> native call : name의 값을 지정하여, js에서 webkit.messageHandlers.NAME.postMessage("");와 연동되는 것, userContentController함수에서 처리한다
        contentController.add(self, name: "callbackHandler")

앞서 선언한 WKUserContentController()클래스 변수에 add를 통해 해당 name값을 가진 webkit.messageHanlder를 통해 호출할 수 있다고 미리 명시하는 것이다. 그래서 위의 경우 webkit.messageHanlders.callbakHandler.postMessage("")를 웹에서 호출하면 네이티브를 호출할 수 있다.


이제 위의 경우를 작성했을때, 네이티브에서 처리할 내용을 기술한다.

    // JS -> Native CALL
    @available(iOS 8.0, *)
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){
        if(message.name == "callbackHandler"){
            if(message.body as! String == "abc"){
                abc()
            }
        }
    }

webkit.messageHanlders.callbakHandler.postMessage("")를 호출했을 때, message.name을 먼저 검사하고, /* message.body == ("")안에 들어있는 내용 */ body를 검사하여 네이티브 함수를 호출한다.


4. 실습


4-1) 네이티브쪽에서의 풀소스는 아래와 같다

import UIKit import WebKit class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler { @IBOutlet weak var containerView: UIView! var webView: WKWebView! var activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView() override func loadView() { super.loadView() let contentController = WKUserContentController() let config = WKWebViewConfiguration() // native -> js call (문서 시작시에만 가능한, 환경설정으로 사용함), source부분에 함수 대신 HTML직접 삽입 가능 let userScript = WKUserScript(source: "redHeader()", injectionTime: .atDocumentEnd, forMainFrameOnly: true) contentController.addUserScript(userScript) // js -> native call : name의 값을 지정하여, js에서 webkit.messageHandlers.NAME.postMessage("");와 연동되는 것, userContentController함수에서 처리한다 contentController.add(self, name: "callbackHandler") config.userContentController = contentController

webView = WKWebView(frame: self.containerView.frame, configuration: config) webView.uiDelegate = self webView.navigationDelegate = self // self.view = self.webView! self.view.addSubview(webView) } override func viewDidLoad() { super.viewDidLoad() let myBlog = "test.html" let url = URL(string: myBlog) let request = URLRequest(url: url!) webView.load(request) } @available(iOS 8.0, *) public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Swift.Void){ let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) let otherAction = UIAlertAction(title: "OK", style: .default, handler: {action in completionHandler()}) alert.addAction(otherAction) self.present(alert, animated: true, completion: nil) } // JS -> Native CALL @available(iOS 8.0, *) func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){ if(message.name == "callbackHandler"){ print(message.body) abc() } } func abc(){ print("abc call") } }

4-2) 웹(test.html)의 소스는 아래와 같다.



주석으로 막은 alert('readHeader() CALL'); 부분은 주석이 없다고 가정한다. 테스트하다 경고창이 귀찮아서 해놓은 것이다.


4-3) 앱을 실행하고 웹뷰를 킨다면?



먼저 Alert가 실행되고


IOS 용 페이지라는 글자가 빨간색으로 변하게 된다.

이는 웹의 redHeader() function이 호출된 결과이며, 문서 시작시 네이티브에서 웹의 함수를 호출한 것이다.


4-4) redHeader라는 함수를 호출하지 않는다면? (네이티브 -> 웹 호출이 없다면)


원본은 위와 같다는 이야기


4-5) 네이티브 함수 호출이란 버튼을 클릭하면?





callNative()란 함수를 호출하고 이 자바스크립트 함수는 callbackHanlder란 Name값과 MessageBody란 값을 postMessage의 파라미터로 보낸다. 네이티브쪽에서는 callbackHandler라는 값을 contentController.add를 통해 이미 설정해 놓았고, userContentController라는 함수에서 message라는 파라미터의 name값을 분석한 뒤, callbackHandler라는 name값이 있다면 그 body값을 print한 뒤에 abc()라는 함수를 호출한다.


여기서 body의 값은 postMessage의 파라미터로 전달된 값이므로, 만약 네이티브 함수를 호출할 때 매개 변수를 넣고 싶다면 이 파라미터에 넣으면 된다. 그리고 abc()라는 함수는 abc call을 print하는 함수이므로 결과 값은 위와 같이 나오게 된다.


4-6) 그렇다면 네이티브에서 웹을 호출하는 일반적인 방법은?

여기서 따로 코드로 작성하진 않았던 부분이며, 네이티브에서 웹을 호출하는 가장 일반적인 방법인 WKWebview.evaluateJavaScript는 네이티브에 버튼 하나 만들어서 해당 자바스크립트 함수를 호출하면 된다.


예를 들어, 웹에서는 callWeb()이란 문구를 경고창으로 띄우는 function을 하나 만들고, 이 함수를 네이티브에서 호출하려면.. (버튼 하나 만들어서 연결하는 것으로 실습)

    @IBAction func callWeb(_ sender: UIButton) {
        webView.evaluateJavaScript("callWeb()", completionHandler: {
            (any, err) -> Void in
            print(err ?? "no error")
        })
    }

이렇게 하고 해당 버튼을 누르면?


callWeb()이라는 함수가 JS에 들어있고 alert('callWeb() CALL')로 되어있을 것이다. (따로 테스트로 만든거라 위 풀소스에는 적혀있지 않음)

어쨌든 사용방법만 알면 이 후는 사용하기 편할 것이다.



** 참고로 evaluateJavascript를 사용하면 JS에서 리턴한 값을 any 타입으로 받을 수 있으므로 참고할 것(안드로이드와 동일)




** JS에서 네이티브 함수를 호출하는데 파라미터가 2개 이상인 경우

      		webkit.messageHandlers.kakaoHandler.postMessage({subject: '제목입니다', url: 'http://g-y-e-o-m.tistory.com'});

저 파라미터에는 ()이 기본 틀이기 때문에, () 안에 {key,value} 쌍으로 넣어주면 된다.

받는 네이티브 쪽에서는 message.body를 Dictionary형태로 형 변환 하여 사용한다.

let values:[String:String] = message.body as! Dictionary print("\(values["subject"]) / \(values["url"]")

즉,


message.name은 키 값으로 Native함수를 부를 때, 사용된다. 이 값들을 여러개 사용하려면 contentController.add를 통해 각 값마다 추가해주어야 한다.

message.body는 파라미터 값으로 Native함수에서 받아 처리할 파라미터들이 들어가면 되는 것이다.



18 Comments
댓글쓰기 폼