310 lines
13 KiB
Swift
310 lines
13 KiB
Swift
//
|
|
// 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 {
|
|
}
|
|
|
|
|
|
}
|
|
}
|