diff --git a/README.md b/README.md index 7b934a3..7ab95c4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,48 @@ - none ## 일지 + ### 24.10.21 + +
+일지 + - 프로젝트 시작 + - swift, UIKit 채택 + - 코드베이스 프로젝트 생성 + - 스토리보드 제거 + - import snapKit +- Prefix 파일 추가 + - 구현을 미리 해두어 자주 사용하거나 메서드들 정의된 코드 추가 + +- 싱글턴 클래스 추가 + - 싱글턴 클래스를 하나 추가하여 전역적인 처리가 필요한 부분에 대한 코드들을 정의해둠 + +- IntroVC + - 실질적으로 MainWebVC로 넘어가기 전에 처리할 내용들을 정의함 + 1. 탈옥 기기 확인 + 2. 네트워크 이상 없는지 확인 + 3. 버전 체크 확인 + - 버전 체크 같은 경우는 서버가 필수적으로 있어야 하므로 해당 부분에 대한 서버가 없다면 해당 메서드를 삭제해야 함 + 4. 위치정보확인 + - 앱에서 기본적으로 자주 사용되는 위치, 푸시 등의 기능 중 일단 위치 정보를 받아옴 + +
+ +--- + +### 24.10.22 + +
+일지 + +- MainWebVC + - webView 설정 + - 브릿지 방식으로 연결해서 사용하는 WebView 시스템 구축 + - 위에 새로 openWeb으로 띄우는 경우 x 로 close 버튼까지 구현 + +- WebView Base 완료 + +
diff --git a/WebAppUIKitBase.xcodeproj/project.xcworkspace/xcuserdata/seankim.xcuserdatad/UserInterfaceState.xcuserstate b/WebAppUIKitBase.xcodeproj/project.xcworkspace/xcuserdata/seankim.xcuserdatad/UserInterfaceState.xcuserstate index a0b0982..1f94ee0 100644 Binary files a/WebAppUIKitBase.xcodeproj/project.xcworkspace/xcuserdata/seankim.xcuserdatad/UserInterfaceState.xcuserstate and b/WebAppUIKitBase.xcodeproj/project.xcworkspace/xcuserdata/seankim.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/WebAppUIKitBase/Scene/IntroVC.swift b/WebAppUIKitBase/Scene/IntroVC.swift index 35836a3..065313a 100644 --- a/WebAppUIKitBase/Scene/IntroVC.swift +++ b/WebAppUIKitBase/Scene/IntroVC.swift @@ -45,12 +45,10 @@ class IntroVC: UIViewController { private func checkAppVersion() { Task { do { + // VER_URL, VER_PARAM, VER_HEADERS는 사용자의 설정에 맞춰야 함 let result = try await CommonUtils.shared.afGET(url:VER_URL, param: VER_PARAM, headers: VER_HEADERS) -// let result = """ -// {"status":{"code":"000","message":"성공"},"data":{"finalVer":"2.0.8","forceVer":"2.0.6","forceUpdtYn":"P","remark":"기능추가","checkVer":"9.9.9"}} -// """ printLog("Success : \(result)") let response = CommonUtils.shared.jsonToType("\(result)", as: VersionResponse.self) diff --git a/WebAppUIKitBase/Scene/MainWebVC.swift b/WebAppUIKitBase/Scene/MainWebVC.swift index 5e19d60..83fd2dc 100644 --- a/WebAppUIKitBase/Scene/MainWebVC.swift +++ b/WebAppUIKitBase/Scene/MainWebVC.swift @@ -11,9 +11,299 @@ 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() - self.view.backgroundColor = .white + + 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 { + } + + } }