WKWebView

  • 처음 프로젝트를 세팅하고 나면, 보통 웹뷰를 하나둘씩 앱에 넣게 될텐데, iOS 개발자가 WKWebView를 올려놓고 딱히 신경쓰지 않으면 결국 문제가 되는 부분이 있다. 이런 부분에 대해 기록해두려고 한다.

window.alert

  • 딱히 브릿지로 약속을 하지 않았다면 웹뷰 개발자가 프로덕션에서 충분히 쓸법한 메소드인데, 따로 구현을 안해주면 WKWebView에서는 기본동작을 즉시 확인버튼을 누른것처럼 동작한다고 한다. ~~~문서가 objc로 연결되네…~~~
/*! @abstract Displays a JavaScript alert panel.
 @param webView The web view invoking the delegate method.
 @param message The message to display.
 @param frame Information about the frame whose JavaScript initiated this
 call.
 @param completionHandler The completion handler to call after the alert
 panel has been dismissed.
 @discussion For user security, your app should call attention to the fact
 that a specific website controls the content in this panel. A simple forumla
 for identifying the controlling website is frame.request.URL.host.
 The panel should have a single OK button.
 
 If you do not implement this method, the web view will behave as if the user selected the OK button.
 */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(WK_SWIFT_UI_ACTOR void (^)(void))completionHandler;
 
  • 이를 해결하려면 WKUIDelegate를 채택하고 구현해주면 된다.비슷한 맥락으로 window.confirm() , window.prompt() 도 같이 구현할 수 있다.
  • 웹브라우저에서는 타이틀에 어떤 윈도우에서 알럿창이 열렸는지 주소가 적히는데, 웹뷰에서 주소 보여주기도 이상하기 때문에 적당히 타이틀과 메시지를 설정해주면 된다.
self.webview.uiDelegate = self
 
extension WKWebViewController: WKUIDelegate {
func webView(
        _ webView: WKWebView,
        runJavaScriptAlertPanelWithMessage message: String,
        initiatedByFrame frame: WKFrameInfo,
        completionHandler: @escaping () -> Swift.Void
    ) {
        let alertController = UIAlertController(
            title: message,
            message: nil,
            preferredStyle: .alert
        )
        
        let cancelAction = UIAlertAction(
            title: "OK",
            style: .cancel
        ) {
            _ in completionHandler()
        }
        
        alertController
            .addAction(
                cancelAction
            )
        
        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: nil,
            preferredStyle: .alert
        )
        
        let cancelAction = UIAlertAction(
            title: "Cancel",
            style: .cancel
        ) {
            _ in completionHandler(
                false
            )
        }
        
        let okAction = UIAlertAction(
            title: "OK",
            style: .default
        ) {
            _ in completionHandler(
                true
            )
        }
        
        alertController
            .addAction(
                cancelAction
            )
        alertController
            .addAction(
                okAction
            )
        
        self.present(
            alertController,
            animated: true,
            completion: nil
        )
}
 

앱스킴 연결

  • 웹뷰에서 앱스킴을 가지고 있는 경우, 직접 링크의 스킴을 검사해 해당 앱스킴으로 이동하고 싶은 경우 처리한다. 이때 스킴 자체는 당연히 Info.plist의 LSApplicationQueriesSchemes에 등록되어 있어야 한다.
func webView(
  _ webView: WKWebView,
  decidePolicyFor navigationAction: WKNavigationAction,
  decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
	if let url = navigationAction.request.url,
	url.scheme != "http" && url.scheme != "https" {
		defer {
			decisionHandler(.cancel)
		}
		
		guard UIApplication.shared.canOpenURL(url) else {
	        /// 에러처리
		}
		
	    UIApplication.shared.open(url) { success in
	        if !success {
		        /// 에러처리
	        }
		}
		return
    }
	/// 이후 동작 처리 -> decisionHandler(.allow)
}

새 창 열기

  • 웹뷰 내 컨텐츠에서 a태그를 이용해 하이퍼링크를 걸고, target="_blank"를잡으면, webView(_:decidePolicyFor:decisionHandler:)에서 WKNavigationActionnavigationTypelinkActivated로 콜백이 떨어지는데, targetFrame 이 없으면 이 동작이 기본적으로 꺼져있어 링크가 열리지 않기 때문에 처리를 해 줘야 한다.
  • 사파리같은 웹브라우저에서는 탭이 있으니 새 탭으로 열리는데, 웹앱 특성상 보통 탭이 없으니 그냥 현재 webView에 url를 로드시켜버리는 방법이 많이 쓰인다.
  • 그리고 웹에서 window.open()을 실행시키는 경우 webView(_:createWebViewWith:for:windowFeatures:)에서 처리하면 된다. webView(_:decidePolicyFor:decisionHandler:)에서 decisionHandler를 allow하면 이곳으로 떨어진다.
func webView(
	_ webView: WKWebView,
	decidePolicyFor navigationAction: WKNavigationAction,
	decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void
) {
	
	switch navigationAction.navigationType {
	case .linkActivated:
		if navigationAction.targetFrame == nil {
			decisionHandler(.cancel)
			self.handleNewWebView(urlRequest: navigationAction.request)
		} else {
			decisionHandler(.allow)
		}
	default:
		decisionHandler(.allow)
	}
}
 
func handleNewWebView(urlRequest: URLRequest) {
	self.webview.load(urlRequest)
}
 
func webView(
	_ webView: WKWebView,
	createWebViewWith configuration: WKWebViewConfiguration,
	for navigationAction: WKNavigationAction,
	windowFeatures: WKWindowFeatures
) -> WKWebView? {
	self.handleNewWebView(urlRequest: navigationAction.request)
	return nil
}

백화 방지

  • WKWebView는 앱과 다른 프로세스로 실행된다. 따라서 WKWebView만 단독으로 킬을 맞을 수 있는데(보통은 메모리 이슈) 이때 webViewWebContentProcessDidTerminate가 불리게 된다. 처리하지 않으면 웹뷰만 죽어버리기 때문에 리로드시켜주면 된다.
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
	self.webview.reload()
}

ETC

  • iOS 16.4부터 생긴 WebView Inspector를 컨트롤할 수 있는 isInspectable의 기본값이 false로 되었기 때문에 따로 설정해 주어야 한다.
let isBeta = true /// 앱 환경설정
if #available(iOS 16.4, *), isBeta {
    self.webView.isInspectable = true
}
  • 만약 서비스가 iPad를 지원하고 있다면, 웹뷰 개발자는 보통 Request Header의 User-Agent로 분기를 칠 텐데, iOS 13.0(iPadOS의 독립)부터 User-Agent가아이패드가 아닌 맥으로 찍히기 때문에 웹뷰 개발자의 기분이 안 좋을 것이다. 최대 터치포인트 개수를 세서 분기를 친다던데…
  • 웹뷰 config를 바꿔주면 이를 방지할 수 있다. 만약 기본 동작을 유지하고 직접 커스텀하고싶으면 UIDevice.current.userInterfaceIdiom == .pad로 분기쳐서 직접 세팅해주면 된다.
extension WKWebViewConfiguration {
    static let defaultConfig: WKWebViewConfiguration = {
        let config = WKWebViewConfiguration()
            
        if #available(iOS 13.0, *) {
            let preference = WKWebpagePreferences()
            preference.preferredContentMode = .mobile
            config.defaultWebpagePreferences = preference
        }
        
        return config
    }()
}
 
let webView = WKWebView(frame: .zero, configuration: .defaultConfig)