present()로 띄운 UIViewController의 WKWebView에서 사진 첨부를 하면 UIViewController가 닫혀(dismiss()) 버린다.

present() 로 띄운 UIViewController에 있는 UIWebView/WKWebView에서 input type=”file” / 을 눌러서 사진을 첨부하려 하면 해당 UIViewController까지 함께 닫혀 버리는 문제가 있습니다.

이 문제는 iOS 8.0.2 부터 발생한 문제로… iOS 9는 물론 iOS 10에서도 고쳐지지 않았죠.

이쯤되니 OS 자체 버그가 아니라 의도된 것 같다는 생각이 듭니다. 의도된 것이 맞다면 어떤식으로 개발을 해야 되는 건지 모르겠네요.

그래서 사용한 임시 방법.

dismiss()를 override 해버리는 방법입니다.

// MARK: - Variable
var isDismiss: Bool = false

// MARK: - Override
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
    if !self.isDismiss && (self.presentedViewController == nil || self.presentedViewController == self) {
        return
    }

    self.isDismiss = false
    super.dismiss(animated: flag, completion: completion)
}

// MARK: - Function
func close(animated: Bool, completion: (() -> Void)? = nil) {
    self.isDismiss = true
    self.dismiss(animated: animated, completion: completion)
}
// MARK: - Action
@IBAction func close(_ sender: UIButton) {
    self.close(animated: true)
}

편법이지만 제일 간단한 방법이죠…

Advertisements

WKWebView 사용 시 필수적으로 넣어줘야 하는Delegate 넷.

WKWebView를 사용한다면 아래의 넷은 반드시 넣어 주어야 한다. 복사->붙여 넣기 후 개발을 시작하면 된다.

우선, WKUIDelegate 셋.

func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping () -> Void) {
    let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert)
    alertController.addAction(UIAlertAction(title: "확인", style: .default, handler: { (action) in
        completionHandler()
    }))

    self.present(alertController, animated: true, completion: nil)
}

func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (Bool) -> Void) {
    let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert)
    alertController.addAction(UIAlertAction(title: "확인", style: .default, handler: { (action) in
        completionHandler(true)
    }))
    alertController.addAction(UIAlertAction(title: "취소", style: .default, handler: { (action) in
        completionHandler(false)
    }))

    self.present(alertController, animated: true, completion: nil)
}

func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (String?) -> Void) {
    let alertController = UIAlertController(title: "", message: prompt, preferredStyle: .alert)
    alertController.addTextField { (textField) in
        textField.text = defaultText
    }
    alertController.addAction(UIAlertAction(title: "확인", style: .default, handler: { (action) in
        if let text = alertController.textFields?.first?.text {
            completionHandler(text)
        } else {
            completionHandler(defaultText)
        }
    }))

    alertController.addAction(UIAlertAction(title: "취소", style: .default, handler: { (action) in
        completionHandler(nil)
    }))

    self.present(alertController, animated: true, completion: nil)
}

이건 iOS 9.0 이후 지원이긴 한데… WKNavigationDelegate 하나.

public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
    // 중복적으로 리로드가 일어나지 않도록 처리 필요.
    webView.reload()
}

앱이 백그라운드 상태에서 포그라운드로 돌아 올때 간혹 WKWebView가 하얀 화면으로 되어 있는 경우가 있는데, 보통 자바스크립트 등으로 체크해서 대응을 하고 있지만 iOS 9이상에선 지원하고 있다.

앱을 다시 시작하면 WKWebView 쿠키가 모두 사라져요.

WKWebView에서 생성된 쿠키가 앱을 재시작할때 모두 사라지는 문제는 여러 해결 방법이 있지만, 그 중 가장 단순한 건 역시 모든 통신 종료 시점에 쿠키를 저장했다가 앱을 시작하면 쿠키를 수동으로 생성 시켜 주는 방법일 것입니다.

이전에도 Javascript를 애용했으므로 이번에도 Javascript를 이용하도록 하겠습니다.

예제 상에서 파일을 저장하고 불러 오는 것은 Devg.framework를 이용하도록 하겠습니다.
(https://bitbucket.org/ZenR/devg.git)

우선 모든 웹 통신이 종료될 시 가지고 있던 쿠키를 평문으로 저장하도록 하겠습니다. 보안에 좀 더 신경을 쓴다면 한번 암호화를 거치는 것도 좋겠죠.

func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation) {
    webView.evaluateJavaScript("document.cookie") { (object, error) in // 자바스크립트로 모든 쿠키를 가져와서
         if let string: String = object as? String { // 스트링으로 변환 후 저장.
             Devg.createFile("COOKIE", contents: string) // 파일 저장하는 부분이므로 대체 하세요.
         }
    }
}

 

 

이제 저장한 쿠키 정보를 불러와서 웹뷰에서 생성하는 작업을 해주어야 합니다.
적절한 도메인에 쿠키를 생성해 주기 위해 첫 웹뷰 로딩은 그냥 진행 시키도록 할게요.
결국 Javascript 기반이기 때문에 쿠키 생성도 didFinishNavigation 에서 진행해 주어야 하는데 쿠키 파일 저장과 생성하는 것이 한 곳에서 이루어 지므로 구분 하기 위해 isFirstLoad라는 멤버 변수를 만들어 주겠습니다.

var isFirstLoad: Bool = true

이제 isFirstLoad 변수를 참조하여 didFinishNavigation에 쿠키 생성하는 코드를 추가 합니다.

func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation) {
    if self.isFirstLoad { // 앱 처음 실행인지 판단.
        self.isFirstLoad = false // 쿠키 생성을 다시 하지 않도록 처리.
        if let string: String = Devg.getContents("COOKIE") { // 저장된 쿠키 파일을 불러 옴.
            // 쿠키를 생성하는 Javascript
            let cookies: [String] = string.componentsSeparatedByString(" ")
            var javascript: String = ""
            for cookie: String in cookies {
                javascript = javascript + "document.cookie='\(cookie)';"
            }
            webView.evaluateJavaScript(javascript, completionHandler: { (object, error) in // 쿠키를 생성하는 Javascript를 실행.
                webView.reload() // 쿠기 생성 후 쿠키 정보가 반영된 웹페이지를 불러 올 수 있도록 새고 고침을 해줍니다.
            })
        }
    }
    else {
        webView.evaluateJavaScript("document.cookie") { (object, error) in // 자바스크립트로 모든 쿠키를 가져와서
             if let string: String = object as? String { // 스트링으로 변환 후 저장.
                 Devg.createFile("COOKIE", contents: string) // 파일 저장하는 부분이므로 대체 하세요.
             }
        }
    }
}

https://ip.devg.net 은 사용자 아이피, 헤더 정보 등을 확인할 수 있는 웹페이지인데, 쿠키 정보도 볼 수 있으므로 간단히 테스트 후 실제 URL을 적용하시면 됩니다.

이름으로 쿠키 가져오기 in Javascript

function getCookie(name) {
  var value = "; " + document.cookie;
  var parts = value.split("; " + name + "=");
  if (parts.length == 2) return parts.pop().split(";").shift();
}

Swift에서도 쿠키를 가져 올 수 있으나 가급적 웹뷰에 대한 부분은 Javascript에 의존하기로 합시다.

위 Javascript를 아래와 같이 적용하면 필요한 쿠키를 손쉽게 가져올 수 있습니다.

webView.evaluateJavaScript("function getCookie(name) {  var value = \"; \" + document.cookie;  var parts = value.split(\"; \" + name + \"=\");  if (parts.length == 2) return parts.pop().split(\";\").shift();}; getCookie('basket_cnt');") { (object, error) in
    print(object)
}

 

WKWebView 에서 Ajax 통신을 감지하는 방법. feat. Javascript

UIWebView 시절에도 그랬지만, WKWebView 에서도 Ajax 통신을 감지할 수 없습니다.

WKWebView 에서 추가된

@available(iOS 8.0, *)
optional public func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void)

에서는 <iframe /> 통신(완료)은 감지 가능하지만 역시 Ajax 통신을 감지할 수 없습니다.

웹은 Swift의 전문 분야가 아닙니다. 웹의 전문 분야인 Javascript에게 도움을 요청합시다.

@available(iOS 8.0, *)
optional public func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!)

didFinishNavigation에서 일반 통신이 종료되는 시점을 감지하여 Ajax 통신을 감지하는 자바스크립트 명령을 삽입합니다.

self.webKit.evaluateJavaScript("_send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function() { alert('ajax:'); var callback = this.onreadystatechange; this.onreadystatechange = function() { if (this.readyState === 4 &amp;amp;&amp;amp; this.status === 200) { alert('ajaxDone'); } callback.apply(this, arguments); }; _send.apply(this, arguments); }; 'script inserted';", completionHandler: { (object, error) in
    print(object)
})

위 명령은 Ajax 를 관장하는 XMLHttpRequest의 send() 메소드를 오버라이딩하여 Ajax 통신 상태를 앱에 전달하여 주기 위한 자바스크립트입니다.

_send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
    alert('ajax');
    var callback = this.onreadystatechange;
    this.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
            alert('ajaxDone');
        }

        callback.apply(this, arguments);
    };

    _send.apply(this, arguments);
};

Javascript와 WKWebView간의 효과적인 통신 방법은 WKUserContentController를 이용하는 방법이 있습니다. 하지만 이것은 다음에 기회가 되면 다시 설명 드리기로 하고 여기에서는 간단하게 Javascript의 alert()과 WKUIDelegate의

@available(iOS 8.0, *)
optional public func webView(webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: () -> Void)

를 이용하는 방법을 사용하겠습니다. 위 protocol은 WKWebView에서 alert() 자바스크립트가 작동되면 호출됩니다.

위 자바스크립트를 이용하여 Ajax(XMLHttpRequest)가 작동하면 alert(‘ajax’); 를 호출 하도록 했고, 통신이 정상적으로 완료 되면 alert(‘ajaxDone’); 를 호출 하도록 하였습니다.

Swift에서는 아래와 같이 각 메세지를 받아서 분기 처리를 할 수 있습니다.

func webView(webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: () -> Void) {
    if message == "ajax" {
        // Ajax 통신 시 웹뷰의 URL을 변경 시키는 경우가 있으므로 이를 확인.
        // Ajax 통신이 이루어지는 URL과는 다름.
        print(webView.URL?.absoluteString)
        completionHandler()

        return
    }
    else if message == "ajaxDone" {
        // Ajax 통신으로 달라 진 HTML (DOM) 을 확인.
        self.webKit.evaluateJavaScript("document.documentElement.outerHTML", completionHandler: { (object, error) in
            print(object)
        })
        completionHandler()

        return
    }

    // UIAlertController 사용 message 내용 띄우기.

    completionHandler()
}

이제 WKWebView는 Ajax를 포함한 모든 통신을 감지할 수 있게 되었습니다.

단순히 웹뷰에 웹을 보여 주는 한계에서 벗어 나 앱의 제어권을 더 넒힌 하이브리드앱의 제작이 가능해졌습니다.

WKWebView 서로 간에 Cookie 공유하는 방법.

iOS 8 이상 부터 사용할 수 있는 WKWebView는 UIWebView와 다르게 서로 다른 인스턴스 간에 쿠키가 공유되지 않는다.

쿠키를 서로 공유하게 하기 위해선 서로 같은 WKProcessPool을 사용하게 하면 된다.

self.processPool = WKProcessPool()

let configuration1: WKWebViewConfiguration = WKWebViewConfiguration()
configuration1.processPool = self.processPool

let webView1: WKWebView = WKWebView(frame: CGRectZero, configuration: configuration)

let configuration2: WKWebViewConfiguration = WKWebViewConfiguration()
configuration1.processPool = self.processPool

let webView2: WKWebView = WKWebView(frame: CGRectZero, configuration: configuration)