아주 간단하게 모바일 웹페이지 상단에 앱스토어 배너 넣기

요즘 스마트폰이 활성화 되면서 모바일 서비스를 제공시 iOS, Android 등의 스마트폰용 앱 주력으로 사용하는 경우가 많습니다.
하지만 앱 설치 유도, 접근성 등으로 모바일 웹페이지도 함께 제공을 할 수 밖에 없죠.
물론 주력 서비스는 앱이기 때문에 다양한 방법으로 모바일 웹페이지에서 앱으로의 이동을 유도합니다.

iOS에서는 아주 간편하게 해결 할 수 있습니다.

<meta name=”apple-itunes-app” content=”app-id=앱아이디”>

Meta Tag 하나를 추가하여

이런 형태의 배너를 모바일 웹페이지에 추가할 수 있습니다.

Meta Tag 내에 넣어야 하는 앱아이디는 앱스토어 링크에서 알 수 있습니다.

https://itunes.apple.com/us/app/apple-store/id375380948?mt=8

~store/id 뒤에 있는 것을 사용하면 됩니다. 375380948

 

앱을 설치하기 전에는 위 배너가 앱스토어로 이동되지만 앱을 설치한 후에는 앱으로 열어 줍니다.
앱을 연 주소를 가지고 앱에서 대응을 하려면

AppDelegate.swift 에 application(_:open:options:) 를 추가한 후 URL에 맞게 대응할 수 있습니다.

https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application

 

Advertisements

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)
}

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

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이상에선 지원하고 있다.

HTTP 주소로 iOS 앱을 실행하여 보자. (Support Universal Links)

아래의 방법은 iOS 9 이상에서 정상적으로 지원합니다. 또한 서버는 HTTPS를 지원해야 합니다.
참조 문서 : Support Universal Links

아이폰으로 서핑하다보면 유투브 링크가 있고 해당 링크를 누르면 유투브 앱이 열리는 상황을 경험한 적이 있습니다.

본인이 만든 앱도 이렇게 하고 싶다는 생각을 했을 것입니다.

앱을 설치하지 않은 사용자는 간략한 정보가 표시되는 모바일웹을, 앱을 설치한 사용자는 풀서비스를 이용할 수 있도록 앱으로 연결을.

애플은 이런 개발자들의 욕구를 수용해 Universal Link 기능을 추가하였습니다.
하지만 간단히 앱에서 설정하는 것만으로 해당 기능을 이용할 수 없습니다.
보안을 중요시 여기는 애플 답게 우선 해당 HTTP 주소에 대해 앱이 권한을 가지고 있는 지 확인을 합니다.

우선 해당 HTTP 주소에 앱이 권한을 가지고 있다는 것을 명시하는 방법 부터 소개 하겠습니다.

서버의 https://[당신의 도메인]/ 또는 https://[당신의 도메인]/.well-known/ 으로 접근 가능하도록 apple-app-site-association 파일을 생성해야 합니다.

https://[당신의 도메인]/apple-app-site-association 또는 https://[당신의 도메인]/.well-known/apple-app-site-association 되겠죠.

명심해야 될 2가지는 https를 지원해야 한다는 것과 파일에 확장자가 없어야 한다는 것입니다. 아래의 예제를 보시면 아시겠지만 apple-app-site-association 파일은 JSON 형태입니다. 하지만 .json 을 붙이지 마세요.

{
"applinks": {
    "apps": [ ],
    "details": [
        {
            "appID": "[Prefix].[앱의 Bundle ID]",
            "paths": [ "*/members/*", "/detail/*", "NOT /detail/old" ]
        },
        {
            "appID": "[Prefix].[두번째 앱의 Bundle ID]",
            "paths": [ "*" ]
        }
    ]
}
}

위의 apple-app-site-association에는 2개의 앱이 선언되어 있습니다.
apps는 비워 둡니다.
details에 등록할 앱 정보를 기입합니다.
appID에 Prefix와 앱의 Bundle ID를 기입합니다.
paths는 중요합니다. 모든 HTTP 주소에 앱을 연결 시킬 수 있고 특정 HTTP 주소에만 앱이 연결 되도록 할 수 있기 때문입니다.
당연히 NOT는 제외할 주소입니다. /detail/*는 허용하지만 /detail/old 는 거부할 수 있죠.
* 만 해 놓으면 모든 주소를 연결 시킬 수 있습니다.

유투브의 경우엔 모든 주소를 앱으로 연결 되도록 하고 제외할 주소를 추가하는 방식을 썼습니다.
실용 예제로 삼아 한번 보시기 바랍니다.(Document Root에 파일을 두기 싫었는 지 .well-known 아래에 두었군요.)

http://youtube.com/.well-known/apple-app-site-association

유투브 앱의 Prefix는 EQHXZ8M8AV 이었군요. 모르시는 분은 없으시겠지만 Prefix는 https://developer.apple.com 에서 알 수 있습니다.

Developer > Certificates, Identifiers & Profiles (https://developer.apple.com/account/ios/certificate) > App IDs (https://developer.apple.com/account/ios/identifier/bundle) > 원하는 앱 선택.

teamid

자, 이제 정상적으로 설정이 끝났는 지 확인을 해봅시다.

https://branch.io/resources/universal-links/

간단히 도메인만 넣어 주면 apple-app-site-association 파일을 찾아 테스트를 하여 줍니다.
모든 항목이 초록색이 나오면 준비 끝.
(애플에서도 테스트를 할 수 있는 툴(https://search.developer.apple.com/appsearch-validation-tool/)을 제공하고 있고 앱 설정까지 확인해 주기 때문에 더 정확합니다. 하지만 개발 단계에서 서버에서의 작업만 확인 하기에는 위 툴이 더 편리합니다.)

다음은 앱으로 넘어 갑니다.

Xcode에서 간단히 처리 가능합니다.

associateddomains

Associated Domains 항목을 활성화 후 Domains에 applinks:도메인 을 입력합니다.

끝!

이제 링크를 눌렀을때 앱이 실행됩니다. Custom Scheme를 이용한 Deep Link 시에는 앱을 열 것이냐는 물음이 뜹니다. Universal Link는 그런 물음도 없이 자연스럽게 앱이 실행됩니다.

위 작업을 모두 끝낸 후 테스트 할때 주의 하실 점.

  1. 사파리에서 직접 주소를 치면 웹으로 가고 싶어 하는 의사로 판단 Universal Link가 작동하지 않습니다.
  2. 테스트를 위해 링크를 삽입한 웹페이지가 동일한 도메인이면 웹 내의 정상적인 서핑으로 판단 Universal Link가 작동하지 않습니다.

테스트에 성공하였나요? 축하 드립니다.

참고로 이거 달고 앱 설치하면 Safari에서 모바일 웹 상단에 ‘열기’가 생깁니다. 사용자의 앱 이용을 유도할 수 있는 것이죠.

앱을 실행 시킬때는 HTTP 주소를 함께 전달해 줍니다. AppDelegate.swift 에서 받아서 처리할 수 있습니다.

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Swift.Void) -> Bool {
    // 이곳에서 userActivity.activityType 과 userActivity.webpageURL 을 이용하여 필요한 ViewController 를 띄우는 처리를 하면 됩니다.
    return true
}

앱을 다시 시작하면 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) -&amp;amp;amp;gt; 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를 포함한 모든 통신을 감지할 수 있게 되었습니다.

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