// // MainWebVC.swift // WebAppUIKitBase // // Created by Sean Kim on 10/21/24. // import UIKit import WebKit import SnapKit class MainWebVC: UIViewController { var webView, openView: WKWebView? // iOS는 뒤로 제스처를 구현 안하면 뒤로 갈 방법이 없어서 네이티브 자체에서 구현을 해줌 private lazy var closeBtnView: UIView = { let view = UIView() var xBtn: UIButton = { let btn = UIButton() btn.setImage(UIImage(systemName: "xmark"), for: .normal) btn.addTarget(self, action: #selector(tappedCloseBtn), for: .touchUpInside) return btn }() view.backgroundColor = .white [ xBtn ].forEach{view.addSubview($0)} xBtn.snp.makeConstraints { $0.top.equalToSuperview().offset(8) $0.bottom.equalToSuperview().offset(-8) $0.trailing.equalToSuperview().offset(16) $0.height.equalTo(32) } return view }() // close 버튼 클릭시 동작 @objc func tappedCloseBtn() { if openView != nil { self.closeBtnView.removeFromSuperview() self.openView?.removeFromSuperview() self.openView = nil } } override func viewDidLoad() { super.viewDidLoad() let contentController = self.bridgeWebKit() let configuration = WKWebViewConfiguration() URLCache.shared.removeAllCachedResponses() URLCache.shared.diskCapacity = 0 URLCache.shared.memoryCapacity = 0 configuration.userContentController = contentController self.webView = WKWebView(frame: .zero, configuration: configuration) guard let webView = self.webView else { return } // 로드상태, 실패, 리디렉션 등 네비게이션 이벤트 처리 할 수 있게 하는 프로토콜로 이를 통해 커스터마이징 할 수 있음 // WKNavigationDelegate 채택 webView.navigationDelegate = self // 링크 미리보기 설정 webView.allowsLinkPreview = false // 웹뷰의 경계가 넘는 콘텐츠인 경우 화면에 표시 설정 - true = 넘으면 표시 안되게 함 webView.clipsToBounds = true //웹 콘텐츠에서 발생하는 팝업, 경고, 콘텍스트 메뉴 등과 관련된 사용자 인터페이스 이벤트 처리 // WKUIDelegate 채택 webView.uiDelegate = self // 스크롤 뷰가 끝까지 드래그 되었을떄 통통 튕기는 애니메이션 설정 webView.scrollView.bounces = false // 세로 특화 (물론 가로 특화도 있음) webView.scrollView.alwaysBounceVertical = false // 사용자가 정의하는 스크립트를 웹뷰에 추가하는 역할을 한다. // 해당 스크립트는 웹페이지에서 확대/축소 기능을 막는 기능이다. webView.configuration.userContentController.addUserScript(self.getZoomDisableScript()) // 웹 페이지에서 JS가 자동으로 새 창을 열 수 있나 허용하는 설정 webView.configuration.preferences.javaScriptCanOpenWindowsAutomatically = true #if DEBUG // webview inspector 가능하도록 설정: 개발자 도구로 웹뷰내 콘텐츠 디버깅 가능 webView.isInspectable = true #endif // WEB_SERVER 부분에 이동할 URL 연결하면 이동함 if let url = URL(string: WEB_SERVER) { let request = URLRequest(url: url) webView.load(request) } self.view.addSubview(webView) webView.snp.makeConstraints { $0.edges.equalTo(self.view.safeAreaLayoutGuide) } // 앱이 활성화 되었을 때 발생 (foreground로 돌아왔을 때 NotificationCenter.default.addObserver(self, selector: #selector(backForeground(noti:)), name: UIApplication.didBecomeActiveNotification, object: nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.disableDragAndDropInteraction() } // MARK: - 브릿지 추가 메서드 // 브릿지 형태로 사용할 경우 이 메서드를 사용 private func bridgeWebKit() -> WKUserContentController { let contentController = WKUserContentController() contentController.add(self, name: "name") return contentController } @objc func backForeground(noti: Notification) { if noti.name.rawValue == UIApplication.didBecomeActiveNotification.rawValue { // foreground } else { } } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { // 여기서 네비게이션 마다 자바스크립트 허용 여부를 설정 preferences.allowsContentJavaScript = true decisionHandler(.allow, preferences) } private func getZoomDisableScript() -> WKUserScript { let source: String = "var meta = document.createElement('meta');" + "meta.name = 'viewport';" + "meta.content = 'width=device-width, initial-scale=1.0, maximum- scale=1.0, user-scalable=no';" + "var head = document.getElementsByTagName('head')[0];" + "head.appendChild(meta);" return WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true) } // 드래그 앤 드롭 방지용 코드 private func disableDragAndDropInteraction() { var webScrollView: UIView? = nil var contentView: UIView? = nil guard let noDragWebView = webView else { return } webScrollView = noDragWebView.subviews.compactMap { $0 as? UIScrollView }.first contentView = webScrollView?.subviews.first(where: { $0.interactions.count > 1 }) guard let dragInteraction = (contentView?.interactions.compactMap { $0 as? UIDragInteraction }.first) else { return } contentView?.removeInteraction(dragInteraction) } } // MARK: - WKUIDelegate extension MainWebVC : WKUIDelegate{ // JS에서 alert() 함수가 호출될때 실행이 되는 함수 func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { makeUIAlert(title: message, message: "", okTitle: "확인"){ _ in completionHandler() } } //JS 에서 confirm() 함수가 호출되었을 떄 실행이된다. func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { makeUITwoAlert(title: message, message: "", okTitle: "취소",okAction: {_ in completionHandler(false) }, cancelTitle: "확인"){ _ in completionHandler(true) } } // 문자열로 된 URL을 입력받고 이를 통해 외부 링크를 여는 동작을 한다. func openExternalLink(urlStr: String, _ handler: (() -> Void)? = nil) { guard let url = URL(string: urlStr) else { return } UIApplication.shared.open(url, options: [:]) { _ in handler?() } } // 새로운 웹뷰를 생성해야 하는 상황에서 호출. e.g. 일반적으로 팝업창을 열거나 JS에서 window.open()시 동작 func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { // view 초기화 부분 let frame = UIScreen.main.bounds self.openView = WKWebView(frame: frame, configuration: configuration) guard let openView = self.openView else { return nil } openView.navigationDelegate = self openView.uiDelegate = self // 오토레이아웃 설정 [ self.closeBtnView, openView ].forEach{view.addSubview($0)} self.closeBtnView.snp.makeConstraints { $0.top.leading.trailing.equalTo(view.safeAreaLayoutGuide) } openView.snp.makeConstraints{ $0.top.equalTo(self.closeBtnView.snp.bottom) $0.bottom.leading.trailing.equalToSuperview() } return openView } // 팝업 웹뷰를 닫을 때 호출 func webViewDidClose(_ webView: WKWebView) { if webView == self.openView { self.openView?.removeFromSuperview() self.openView = nil } view.subviews.forEach { if $0 == self.closeBtnView { self.closeBtnView.removeFromSuperview() } } } } // MARK: - WKNavigationDelegate extension MainWebVC: WKNavigationDelegate { // 네비게이션 요청이 발생했을때 이를 허용, 차단을 결정하는 메서드 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let scheme = navigationAction.request.url?.scheme, scheme != "http" && scheme != "https" { printLog("SCHEME: \(scheme)") // 네비게이션 동작이 들어올 경우 새로운 페이지를 여는 동작을 한다. if let openApp = navigationAction.request.url, UIApplication.shared.canOpenURL(openApp){ UIApplication.shared.open(openApp, options: [:], completionHandler: nil) } else { // 그외의 동작에 대해서 처리 } decisionHandler(WKNavigationActionPolicy.cancel) return } else { decisionHandler(WKNavigationActionPolicy.allow) } return } // 로딩 시작시 호출 -> 로딩 인디케이터, 초기화 작업등을 시작 func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { printLog("WebView = \(webView), navigation = \(navigation)") } // 로딩 실패시 호출 -> 재시도 로직 구현 func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) { printLog("WebView Load fail = \(error)") } // 콘텐츠가 로드되기 시작할 때 호출 -> 페이지의 실제 시작 시점 감지 가능, 로딩 상태 업뎃 func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { printLog("WebView Content start loading") } // 페이지 로딩 완료시 호출 -> 로딩 끝난 후 추가 작업을 할 수 있음 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { printLog("WebView Page Loaded") // 페이지의 모든 요소에서 텍스트의 선택과 콜아웃을 막지만 입력칸에서는 예외 처리 let javascriptStyle = "var css = '*:not(input, textarea){-webkit-touch-callout:none;-webkit-user-select:none}'; var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode(css)); head.appendChild(style);" webView.evaluateJavaScript(javascriptStyle) } // 페이지 로딩 단계에서 실패시 호출 -> 네트워크나 초기 리디렉션 오류 등으로 로딩 못한 경우 func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: any Error) { makeUIAlert(title: "서비스 연결 문제 발생", message: """ 일시적인 장애 또는 네트워크 문제로 서비스에 연결하지 못하였습니다. 문제가 계속될 경우 고객센터로 문의해 주세요. """, okTitle: "확인") { _ in exit(1) } } } // MARK: - WKScriptMessageHandler // 실제로 브릿지에서 생성 해둔거 구현하는 부분은 여기가 됨 extension MainWebVC: WKScriptMessageHandler{ func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { printLog("Message name: \(message.name)") printLog("Message body type: \(type(of: message.body))") printLog("Message body: \(message.body as? String)") // MARK: name - Description / Parameter: JSON String / Script: O if message.name == "name", let body = message.body as? String { } } }