프로젝트에서 사용하는 커스텀 웹뷰 클래스

- Custom Scheme, Javascript 연동

- Blob / Data 다운로드 로직 ( 14.5미만 다운로드 가능)

    : iCloud 다운로드 

- 캐시삭제 및 쿠키삭제, 쿠키 가져오기, 쿠키 생성

- tel / mailto / sms / facetime Scheme 

- 사설인증서 예외처리

 

** 소스코드내 다른 클래스는 블러그에 다 있습니다. 찾아서 적용하시면 됩니다.

import UIKit
import WebKit


enum HttpMethod: String {
    case POST   = "POST"
    case GET    = "GET"
}

enum WebMessageHandler: String {
    case customScheme
    case ios_javascript
    case download
}

//다운로드를 허용할 파일 mimeType, 확장자명 추가
internal let fileExtensions = [
    "image/jpeg": "jpg",
    "image/png": "png",
    "image/gif": "gif",
    "application/pdf": "pdf",
    "text/plain": "txt",
    "video/mp4": "mp4"
]

//이미지 mimeType
enum ImageMimeType: String {
    case jpg = "image/jpeg"
    case png = "image/png"
    case gif = "image/gif"
}


@objc protocol IWKWebViewDelegate: AnyObject {
    /// 웹뷰 로딩 완료
    @objc func webviewDidFinishNavigation(webView: WKWebView);
    /// 웹뷰 새창 추가 완료
    @objc optional func webviewDidFinishAddNewWebView(webView: WKWebView);
    /// 웹뷰 로딩 실패
    @objc optional func webViewdidFailProvisionsNavigation(webView: WKWebView);
}

var processPool: WKProcessPool?
class IWKWebView: UIView {
    
    var webView: WKWebView?
    var webViewList = [WKWebView]()
    
    // JS -> Native Message Handler Class
    let javascriptMessageHandler = JavascriptMessageHandler()
    let customSchemeMessageHandler = CustomSchemeMessageHandler()
    
    weak var delegate: IWKWebViewDelegate?
    weak var parentVC: UIViewController?
    
    var httpCookieStore: WKHTTPCookieStore  {
        return WKWebsiteDataStore.default().httpCookieStore
    }
    
    deinit {
        log(direction: .WEBVIEW, ofType: self, datas: "deinit : \(String(describing: webView?.url?.absoluteString))")
        
        removeScriptMessageHandlers()
        webViewList.removeAll()
    }
    
    /// WKUserContentController Handler 삭제
    func removeScriptMessageHandlers() {
        guard let userContController = webView?.configuration.userContentController else {
            return
        }
        
        if #available(iOS 14.0, *) {
            userContController.removeAllScriptMessageHandlers()
        } else {
            userContController.removeScriptMessageHandler(forName: WebMessageHandler.customScheme.rawValue)
            userContController.removeScriptMessageHandler(forName: WebMessageHandler.ios_javascript.rawValue)
            userContController.removeScriptMessageHandler(forName: WebMessageHandler.download.rawValue)
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        configuration()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configuration()
    }
    
    func configuration() {
        
        if (processPool == nil) {
            processPool = WKProcessPool.init()
        }
        
        // Scheme 및 JS 함수 핸들러 등록
        let contentController = WKUserContentController()
        contentController.add(self, name: WebMessageHandler.customScheme.rawValue)
        contentController.add(self, name: WebMessageHandler.ios_javascript.rawValue)
//        contentController.add(self, name: WebMessageHandler.download.rawValue)
        
//        contentController.add(self, name: "openDocument")
//        contentController.add(self, name: "jsError")
        
        let config = WKWebViewConfiguration()
        config.processPool = processPool!
        config.preferences.javaScriptCanOpenWindowsAutomatically = true
        
        if #available(iOS 14.0, *) {
            config.defaultWebpagePreferences.allowsContentJavaScript = true
        } else {
            config.preferences.javaScriptEnabled = true
        }
        
        // WKUserContentController WebView 연결
        config.userContentController = contentController
        
        // WKWebView 생성
        webView = WKWebView(frame: .init(), configuration: config)
        webView?.uiDelegate = self
        webView?.navigationDelegate = self
        webView?.scrollView.bounces = true
        webView?.isOpaque = false
        webView?.tag = 1000
        // 스와이프로 뒤로가기
        webView?.allowsBackForwardNavigationGestures = true
        // Long Press 미리보기 차단
        webView?.allowsLinkPreview = false
        
        // 당겨서 새로고침 리프레쉬 컨트롤
        let refreshControl = UIRefreshControl()
            refreshControl.addTarget(self, action: #selector(reloadWebView(_:)), for: .valueChanged)
        webView?.scrollView.addSubview(refreshControl)
        
        self.addSubview(webView!)
        webViewList.append(webView!)
        
        webView?.translatesAutoresizingMaskIntoConstraints = false
//        webView?.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor).isActive = true
//        webView?.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true

        webView?.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
        webView?.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
        webView?.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
        webView?.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
        
        //MARK: App -> JS 함수 호출
        javascriptMessageHandler.evaluateJSCompleted = { [weak self] (functionName, params) in
            
            self?.evaluateJavaScript(javaScriptFuncString: functionName, param: params, completionHandler: { result, error in
//                log(direction: .WEBVIEW, ofType: self, datas: result, error)
            })
        }
        
        //MARK: App -> JS 함수 호출
        customSchemeMessageHandler.evaluateJSCompleted = { [weak self] (functionName, params) in
            
            self?.evaluateJavaScript(javaScriptFuncString: functionName, param: params, completionHandler: { result, error in
//                log(direction: .WEBVIEW, ofType: self, datas: result, error)
            })
        }
    }
    
    @objc func reloadWebView(_ sender: UIRefreshControl) {
        webView?.reload()
        sender.endRefreshing()
    }
    
    /// 웹뷰 페이지 로드
    func webViewLoad(urlString: String, httpMethod: HttpMethod, params: String = "") {
        
        log(direction: .WEBVIEW, ofType: self, datas: "load url : \(urlString)", "method : \(httpMethod)", "params : \(params)")
        
        guard let url: URL = URL.init(string: urlString) else {
            return
        }
        
        var request = URLRequest(url: url)
        
//        let request: URLRequest = URLRequest.init(url: NSURL.init(string: urlString)! as URL, cachePolicy: URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 10)

                // Local 캐시 무시 (매번 로딩해서 속도가 느려질 수 있다)
//        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
        
        if httpMethod == .POST {
            request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
            request.httpMethod = httpMethod.rawValue
            request.httpBody = params.data(using: .utf8)
        }
        
        webView?.evaluateJavaScript("navigator.userAgent", completionHandler: { [weak self] (result, error) in
            guard let `self` = self else { return }
        
            if let result = result {
                let agentString = "\(String(describing: result))"
                
                //커스텀 Agent 추가 필요시 사용
                self.webView?.customUserAgent = agentString + " IOS"
                
                log(direction: .WEBVIEW, ofType: self, datas: "User Agent", self.webView?.customUserAgent)
                
            }
            
            self.webView?.load(request)
            LoadingHUD.show()
            
        })
    }
    
    /*
    //    MARK: User Agent
    func getUserAgentSuffix() -> String {
        let version = appVersion() ?? "1.0.0"
        let langCD = UserData.deviceLocale.settingLanguageCode
        let countryCD = UserData.deviceLocale.deviceRegionCode
    
        // 플랫폼 /
        // 노치 있는 폰 구분 값
        let isNotch = "Y"
//        let margin = UIApplication.getsafeAreaTopMargin()
//        if UIApplication.getsafeAreaTopMargin() > 20.0 {
//            isNotch = "Y"
//        }
        
        
        let str = "\(APP_AGENT_NAME)/IOS/\(STORE_TYPE)/\(String(describing: version))/\(langCD)/\(countryCD)/\(isNotch)/\(0);"
        
        log(direction: .WEBVIEW, ofType: self, datas: "User Agent : \(str)")
        return str
    }*/
     
    /// 테스트용 로컬 HTML 페이지 로드 blobTest ios_javascript
    func webViewHtmlLoad() {
        
        
//        if let url = Bundle.main.url(forResource: "ios_javascript", withExtension: "html") {
//            webView?.load(URLRequest(url: url))
//        }
        
        guard let url: URL = Bundle.main.url(forResource: "ios_javascript", withExtension: "html") else {
            return
        }
        
        var request = URLRequest(url: url)
        
        webView?.evaluateJavaScript("navigator.userAgent", completionHandler: { [weak self] (result, error) in
            guard let `self` = self else { return }
            
            if let result = result {
                let agentString = "\(String(describing: result))"
                
                //커스텀 Agent 추가 필요시 사용
                self.webView?.customUserAgent = agentString + " IOS"
                
                log(direction: .WEBVIEW, ofType: self, datas: "User Agent", self.webView?.customUserAgent)
                
            }
            
            self.webView?.load(request)
        })
        
    }
    
    func addNewWebView(config: WKWebViewConfiguration) -> WKWebView {
        return addNewWebView(config: config, appendClose: false)
    }
    
    func addNewWebView(config: WKWebViewConfiguration, appendClose: Bool) -> WKWebView {
        let userController = WKUserContentController.init()
        
        config.userContentController = userController
        config.processPool = processPool!
        
        let frame = CGRect.init(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        let newWebView = WKWebView.init(frame: frame, configuration: config)
        newWebView.uiDelegate = self
        newWebView.navigationDelegate = self
        newWebView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        newWebView.isOpaque = false;
        newWebView.scrollView.bounces = false
        newWebView.tag = 1000 + webViewList.count
        self.addSubview(newWebView)
        
        newWebView.customUserAgent = webView?.customUserAgent
        webViewList.append(newWebView)
        
        return newWebView
    }
    
// MARK: Cache
    func deleteCache(completed: @escaping () -> Void) {
        // Cache 삭제 로직
        // 참고자료 : https://velog.io/@dbqls200/iOS-WKWebView-cache-%EC%82%AD%EC%A0%9C
        let webseiteDateTypes = NSSet(array: [WKWebsiteDataTypeOfflineWebApplicationCache,WKWebsiteDataTypeLocalStorage])
        
        let date = NSDate(timeIntervalSince1970: 0)
        
        WKWebsiteDataStore.default().removeData(ofTypes: webseiteDateTypes as! Set, modifiedSince: date as Date, completionHandler: {
            
            completed()
        })
    }
    
// MARK: Cookie
    func deleteCookie() {
        
        // Cookie 삭제 로직
        httpCookieStore.getAllCookies { [weak self] (cookies) in
            for cookie in cookies {
                log(direction: .WEBVIEW, ofType: self, datas: cookie.properties)
                
                self?.httpCookieStore.delete(cookie)
            }
        }
        
       
        /*
        getCookies { cookieDict in
            log(direction: .WEBVIEW, ofType: self, datas: cookieDict)
        }*/
        
    }
    
    // Cookie 가져오기
    func getCookies(for domain: String? = nil, completion: @escaping ([String : Any])->())  {
        
        var cookieDict = [String : AnyObject]()
        httpCookieStore.getAllCookies { (cookies) in
            for cookie in cookies {
                if let domain = domain {
                    if cookie.domain.contains(domain) {
                        cookieDict[cookie.name] = cookie.properties as AnyObject?
                    }
                } else {
                    cookieDict[cookie.name] = cookie.properties as AnyObject?
                }
            }
            completion(cookieDict)
        }
    }
    
    /*
    func setCookies(for domain: String? = nil, completion: @escaping (Bool)->()) {
        
        let session = UserData.sessionCookie
        
        if let session_name = session.sessionName, let session_id = session.sessionID, !UserData.accessToken.isEmpty {
            
            if session_name.isEmpty {
                completion(false)
                return
            }
            
            guard let session_info = session.sessionCookieInfo else {
                completion(false)
                return
            }
            
            
            if let session_time = session_info.lifetime {
                let expires_time = TimeInterval(session_time * 1000)
                
                let expires = NSDate(timeIntervalSinceNow: expires_time)
                
                if let cookie = HTTPCookie(properties: [
                    .name : session_name,
                    .value : session_id,
                    .path : session_info.path ?? "/",
                    .domain : session_info.domain ?? "",
                    .secure : session_info.secure ?? false,
                    .expires : expires
                ]) {
                    self.httpCookieStore.setCookie(cookie) {
                        log(direction: .ETC, ofType: self, datas: "setCookie : \(cookie)")
                        completion(true)
                    }
                }
            }
        } else {
            completion(false)
        }
    }*/
}

// MARK: WKScriptMessageHandler
extension IWKWebView: WKScriptMessageHandler {
    
    func convertToDictionary(text: String) -> [String: Any]? {
        if let data = text.data(using: .utf8) {
            do {
                return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
            } catch let error {
                log(direction: .ERROR, ofType: self, datas: error.localizedDescription)
            }
        }
        return nil
    }
    
    /*
     WKScriptMessageHandler
     참고자료 - https://swieeft.github.io/2020/02/23/ImportSMSiOS.html
     JS -> Native 연동
     Web -> Scheme 연동
     */
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        
        /*
        if (message.name == "openDocument") {
            previewDocument(messageBody: message.body as! String)
        } else if (message.name == "jsError") {
            debugPrint(message.body as! String)
        }
        return
         */
        
        let handlerName = WebMessageHandler(rawValue: message.name)
        
        // Scheme 및 JS -> Native 구분
        switch handlerName {
        case .customScheme:
            if let bodyString = message.body as? String,
                let url = URL(string: bodyString),
                let scheme = url.scheme {
                
                switch scheme {
                case "mgapp":
                    if let function = url.host {
                        let selector = Selector(function+":")
                        let originalMethod = class_getInstanceMethod(CustomSchemeMessageHandler.self, selector)
                        if originalMethod == nil {
                            log(direction: .ERROR, ofType: self, datas: "CustomScheme->Native \(selector) : 호출할 수 없는 함수 입니다.")
                            return
                        }
                        
                        let param = url.queryDictionary
                        customSchemeMessageHandler.perform(selector, with: param)
                    }
                default: break
                }
            }
        case .ios_javascript:
            if let bodyString = message.body as? String {
                
                if let jsDic = convertToDictionary(text: bodyString) {
                    /*       함수명           파라메터
                     형식 - {"login" : {"idx":1, "name":"twok"}}
                           {"logout" : {}}
                     */
                    if let jsFunc = jsDic.keys.first?.description {
                        if let params = jsDic[jsFunc] as? Dictionary<String, Any> {
                            
                            let selector = Selector(jsFunc+":")
                            
                            let originalMethod = class_getInstanceMethod(JavascriptMessageHandler.self, selector)
                            
                            if originalMethod == nil {
                                log(direction: .ERROR, ofType: self, datas: "JS->Native \(selector) : 호출할 수 없는 함수 입니다.")
                                return
                            }
                            
                            javascriptMessageHandler.perform(selector, with: params)
                        }
                    }
                }
            }
//        case .download:
//            if let bodyString = message.body as? String {
//                dataDownload(dataString: bodyString)
//            }
            
        default: break
        }
    }
    
    /// 자바스크립트 호출
    func evaluateJavaScript(javaScriptFuncString: String, param: String, completionHandler: ((Any?, Error?) -> Void)?) {
        
        if let lastWebView = webViewList.last {
            
//            log(direction: .WEBVIEW, ofType: self, datas: "\(javaScriptFuncString)(\(param))")
            // JS -> App 호출된 funtion 명 동일하게 사용하기 위해서 별도 구문 추가
            lastWebView.evaluateJavaScript("javascript:NativeAppInterface.\(javaScriptFuncString)(\(param))", completionHandler: completionHandler)
//            lastWebView.evaluateJavaScript("\(javaScriptFuncString)(\(param))", completionHandler: completionHandler)
        }
    }

//MARK: BLOB IOS 14.5 미만 버전 다운로드
    private func downloadBlobFile(blobUrl: String) {
        
        webView?.evaluateJavaScript(
            "javascript: var xhr = new XMLHttpRequest();" +
            "xhr.open('GET', '" + blobUrl + "', true);" +
            "xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');" +
            "xhr.responseType = 'blob';" +
            "xhr.onload = function(e) {" +
            "    if (this.status == 200) {" +
            "        var blob = this.response; " +
            "        var reader = new FileReader();" +
            "        reader.readAsDataURL(blob);" +
            "        reader.onloadend = function() {" +
            
//            "       var filename = limdongo;" +
//            "       const contentDisp = reader.result.headers.get('content-disposition');" +
//            "       if (contentDisp) {" +
//            "           const match = contentDisp.match(/(^;|)\\s*filename=\\s*(\"([^\"]*)\"|([^;\\s]*))\\s*(;|$)/i);" +
//            "           if (match) {" +
//            "               filename = match[3] || match[4];" +
//            "           }" +
//            "       }" +
            
            "            window.location.href = reader.result;" +
            // 원격지 사용시 data 형태로 decidePolicyFor 통해 다운로드 받을 수 있어 주석처리함.
            //            "            window.webkit.messageHandlers.\(WebMessageHandler.download.rawValue).postMessage(reader.result)" +
            "        }" +
            "    }" +
            "};" +
            "xhr.send();"
            
        ) { (result, err) in
            if let result = result {
                log(direction: .WEBVIEW, ofType: self, datas: "result: \(result)")
            }
            if let err = err{
                log(direction: .WEBVIEW, ofType: self, datas: "error: \(err)")
            }
        }
    }
    
    /*
    private func executeDocumentDownloadScript(forAbsoluteUrl absoluteUrl : String) {
        // TODO: Add more supported mime-types for missing content-disposition headers
        webView?.evaluateJavaScript("""
                (async function download() {
                    const url = '\(absoluteUrl)';
                    try {
                        // we use a second try block here to have more detailed error information
                        // because of the nature of JS the outer try-catch doesn't know anything where the error happended
                        let res;
                        try {
                            res = await fetch(url, {
                                credentials: 'include'
                            });
                        } catch (err) {
                            window.webkit.messageHandlers.jsError.postMessage(`fetch threw, error: ${err}, url: ${url}`);
                            return;
                        }
                        if (!res.ok) {
                            window.webkit.messageHandlers.jsError.postMessage(`Response status was not ok, status: ${res.status}, url: ${url}`);
                            return;
                        }
                        const contentDisp = res.headers.get('content-disposition');
                        if (contentDisp) {
                            const match = contentDisp.match(/(^;|)\\s*filename=\\s*(\"([^\"]*)\"|([^;\\s]*))\\s*(;|$)/i);
                            if (match) {
                                filename = match[3] || match[4];
                            } else {
                                // TODO: we could here guess the filename from the mime-type (e.g. unnamed.pdf for pdfs, or unnamed.tiff for tiffs)
                                window.webkit.messageHandlers.jsError.postMessage(`content-disposition header could not be matched against regex, content-disposition: ${contentDisp} url: ${url}`);
                            }
                        } else {
                            window.webkit.messageHandlers.jsError.postMessage(`content-disposition header missing, url: ${url}`);
                            return;
                        }
                        if (!filename) {
                            const contentType = res.headers.get('content-type');
                            if (contentType) {
                                if (contentType.indexOf('application/json') === 0) {
                                    filename = 'unnamed.pdf';
                                } else if (contentType.indexOf('image/tiff') === 0) {
                                    filename = 'unnamed.tiff';
                                }
                            }
                        }
                        if (!filename) {
                            window.webkit.messageHandlers.jsError.postMessage(`Could not determine filename from content-disposition nor content-type, content-dispositon: ${contentDispositon}, content-type: ${contentType}, url: ${url}`);
                        }
                        let data;
                        try {
                            data = await res.blob();
                        } catch (err) {
                            window.webkit.messageHandlers.jsError.postMessage(`res.blob() threw, error: ${err}, url: ${url}`);
                            return;
                        }
                        const fr = new FileReader();
                        fr.onload = () => {
                            window.webkit.messageHandlers.openDocument.postMessage(`${filename};${fr.result}`)
                        };
                        fr.addEventListener('error', (err) => {
                            window.webkit.messageHandlers.jsError.postMessage(`FileReader threw, error: ${err}`)
                        })
                        fr.readAsDataURL(data);
                    } catch (err) {
                        // TODO: better log the error, currently only TypeError: Type error
                        window.webkit.messageHandlers.jsError.postMessage(`JSError while downloading document, url: ${url}, err: ${err}`)
                    }
                })();
                // null is needed here as this eval returns the last statement and we can't return a promise
                null;
            """) { (result, err) in
            
            if (err != nil) {
                debugPrint("JS ERR: \(String(describing: err))")
            }
        }
    }
     
     /*
      Open downloaded document in QuickLook preview
      */
     private func previewDocument(messageBody: String) {
         // messageBody is in the format ;data:;base64,
         
         // split on the first ";", to reveal the filename
         let filenameSplits = messageBody.split(separator: ";", maxSplits: 1, omittingEmptySubsequences: false)
         
         let filename = String(filenameSplits[0])
         
         // split the remaining part on the first ",", to reveal the base64 data
         let dataSplits = filenameSplits[1].split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false)
         
         let data = Data(base64Encoded: String(dataSplits[1]))
         
         if (data == nil) {
             debugPrint("Could not construct data from base64")
             return
         }
         
         // store the file on disk (.removingPercentEncoding removes possible URL encoded characters like "%20" for blank)
         let localFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename.removingPercentEncoding ?? filename)
         
         do {
             try data!.write(to: localFileURL);
         } catch {
             debugPrint(error)
             return
         }
     }
     */
    
    
//MARK: Blob IOS 14.5 미만 버전 및 Data 스키마 다운로드
    /*
     - Blob 방식의 경우 downloadBlobFile 메소드의 window.webkit.messageHandlers.download 스키마 통해서 데이터 전달됨.
     - Data 스키마 decidePolicyFor 통해서 다운로드
     */
    private func dataDownload(dataString: String) {
        
        let base64plainString = dataString.components(separatedBy: ",")[1] // "/9j/4AAQSkZJRgABAQEAYABgAAD/......"
        
        let typeString = dataString.components(separatedBy: ",")[0]
        let mimeType = typeString.components(separatedBy: ":")[1].components(separatedBy: ";")[0] // "image/jpeg"
//        let fileName = typeString.components(separatedBy: ":")[1].components(separatedBy: ";")[1] // "filename"
        
        log(direction: .WEBVIEW, ofType: self, datas: "download Data: \(typeString)")
        
        //data url에는 mimeType과 base64로 변환된 파일 정보만을 담고 있기에 파일 이름을 알 수 없음. 임의로 파일 이름 저장
        let tempFileName = "MG_\(Date().timeIntervalSince1970)"
//        let dir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        
        if let data = Data(base64Encoded: base64plainString) {
            
            // "jpg","png"... path에 확장자 붙이기
            guard let ext = fileExtensions[mimeType] else {
                return
            }
            // 디폴트 데이터 처리시 IOS 14.5 미만에서 파일이름을 알 수 없어 임시파일 이름으로 저장
//            let fullPath = "\(dir)/\(tempFileName).\(ext)"
            /* IOS 14.5 미만에서 파일이름을 알 수 없어 data 형태 타입에 파일 이름을 추가해서 받을경우 사용
               data:image/gif;filename;base6
             */
//            let fullPath = "\(dir)/\(fileName).\(ext)"
            
            switch mimeType {
            case ImageMimeType.jpg.rawValue:
                if let imageData = UIImage(data: data)?.jpegData(compressionQuality: 1.0) {
                    fileSave(fileName: "\(tempFileName).\(ext)", data: imageData)
                }
                
            case ImageMimeType.png.rawValue:
                if let imageData = UIImage(data: data)?.pngData() {
                    fileSave(fileName: "\(tempFileName).\(ext)", data: imageData)
                }
                
            default: //문서 및 비디오 등등 (gif, mp4, pdf...)
                fileSave(fileName: "\(tempFileName).\(ext)", data: data)
            }
        }
    }
    
    private func fileSave(fileName: String, data: Data) {
        
        if let containerUrl = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents") {
            /*iCloud 파일 저장
            [참고자료]
            https://velog.io/@kuruma-42/iCloud-Data-Back-Up
            https://developer.apple.com/documentation/cloudkit/enabling_cloudkit_in_your_app
             */
            if !FileManager.default.fileExists(atPath: containerUrl.path, isDirectory: nil) {
                do {
                    try FileManager.default.createDirectory(at: containerUrl, withIntermediateDirectories: true, attributes: nil)
                }
                catch let error {
                    log(direction: .ERROR, ofType: self, datas: error.localizedDescription)
                }
            }
            
            let fileUrl = containerUrl.appendingPathComponent(fileName)
            do {
                try data.write(to: fileUrl)
                
                showPopup(message: "다운로드 완료!")
            }
            catch let error {
                log(direction: .ERROR, ofType: self, datas: error.localizedDescription)
                showPopup(message: "다운로드 실패!")
            }
        } else {
            
            // 나의 iPhone 파일 저장
            let dir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
            
            let filePath = "\(dir)/\(fileName)"
            
            if FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) == true {
                log(direction: .ETC, ofType: self, datas: "IOS 14.5 미만 File Download Success!!", filePath)
                showPopup(message: "다운로드 완료!")
            } else {
                log(direction: .ETC, ofType: self, datas: "IOS 14.5 미만 File Download Fail!!", filePath)
                showPopup(message: "다운로드 실패!")
            }
        }
    }
}


// MARK: WKNavigationDelegate
extension IWKWebView: WKNavigationDelegate {
    
    /// 탐색 허용 또는 취소여부 결정합니다. (즉시 블록을 호출하거나 나중에 블록을 비동기식으로 호출할 수 있다.)
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        
//        LoadingHUD.show()
        
        guard let url = navigationAction.request.url else {
            decisionHandler(WKNavigationActionPolicy.cancel)
            return
        }
        
        // 전화, 메일, sms, facetime 등 스키마는 별도로 처리
        if let scheme = url.scheme {
            switch scheme {
            case "tel", "mailto", "sms", "facetime" :
                log(direction: .SCHEME, ofType: self, datas: "\(String(describing: scheme))", url.absoluteString)
                externalOpen(urlString: url.absoluteString)
                return decisionHandler(.cancel)
                
            case "blob":
                log(direction: .SCHEME, ofType: self, datas: url.absoluteString)
                if #available(iOS 14.5, *) {
                    return decisionHandler(.download)
                    
                } else {
                    let urlString = url.absoluteString
                    downloadBlobFile(blobUrl: urlString)
//                    executeDocumentDownloadScript(forAbsoluteUrl: urlString)
                    return decisionHandler(.cancel)
                }
            case "data":
                log(direction: .SCHEME, ofType: self, datas: url.absoluteString)
//                return decisionHandler(.cancel)
                
                if #available(iOS 14.5, *) {
                    return decisionHandler(.download)
                    
                } else {
                    let urlString = url.absoluteString
                    dataDownload(dataString: urlString)
                    return decisionHandler(.cancel)
                }
            case "kakaolink", "itms-appss": //????
                log(direction: .SCHEME, ofType: self, datas: "\(String(describing: scheme))", url.absoluteString)
                externalOpen(urlString: url.absoluteString)
                return decisionHandler(.cancel)
            default: break
            }
        }
        
        log(direction: .WEBVIEW, ofType: self, datas: "\(String(describing: navigationAction.request.url?.absoluteString))")
        
        // itune store URL 이동
        if url.absoluteString.range(of: "//itunes.apple.com/") != nil {
            log(direction: .WEBVIEW, ofType: self, datas:  "App Store URL : \(String(describing: navigationAction.request.url?.absoluteString))")
            externalOpen(urlString: url.absoluteString)
            return decisionHandler(.cancel)
        }
        
        switch navigationAction.navigationType {
        case .linkActivated: break
        case .reload: break
        case .backForward: break
        case .formSubmitted: break
        case .formResubmitted: break
        case .other: break    //custom scheme 일경우 처리 ??
        default: break
        }
        
        decisionHandler(WKNavigationActionPolicy.allow)
    }
    
    //웹 뷰가 request 에 대한 서버 redirection 을 수신했음을 알립니다.
    func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
        log(direction: .WEBVIEW, ofType: self, datas: "Redirect :\(String(describing: webView.url?.absoluteString))")
        LoadingHUD.hide()
    }
    
    //웹 뷰가 메인 프레임에 대한 내용을 수신하기 시작했음을 알립니다.
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
        log(direction: .WEBVIEW, ofType: self, datas: "didCommit :\(String(describing: webView.url?.absoluteString))")
        LoadingHUD.show()
    }
    
    // URL이 잘못되었거나 네트워크 오류가 발생해 웹 페이지 자체를 아예 불러오지 못했을 때 호출
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        
        log(direction: .ERROR, ofType: self, datas: "\(error.localizedDescription)")
        
        LoadingHUD.hide()
    }
    
    // 콘텐츠를 로드하는 동안 오류가 발생하면 호출됩니다.
    func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        
        log(direction: .ERROR, ofType: self, datas: "\(error.localizedDescription)")
        LoadingHUD.hide()
    }
    
    // 네비게이션이 완료되면 호출됩니다.
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        
        log(direction: .WEBVIEW, ofType: self, datas: "웹페이지 로드 완료", webView.url?.absoluteString)
        
        delegate?.webviewDidFinishNavigation(webView: webView)
        
        LoadingHUD.hide()
    }

// MARK: 인증서 예외처리
    // 인증 확인에 응답하도록 요청합니다. 사설인증서 사용시 접속 허용할 수 있게 코드를 추가합니다.
    
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            let credential = URLCredential.init(trust: challenge.protectionSpace.serverTrust!)
            
            DispatchQueue.global().async {
                completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential)
            }
            
            return
        }
        
        DispatchQueue.global().async {
            completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
        }
         
        /*
        guard let serverTrust = challenge.protectionSpace.serverTrust else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        let exceptions = SecTrustCopyExceptions(serverTrust)
        SecTrustSetExceptions(serverTrust, exceptions)
        completionHandler(.useCredential, URLCredential(trust: serverTrust));
         */
    }
     
}

// MARK: WKUIDelegate
extension IWKWebView: WKUIDelegate {
    
    func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
        
        
        guard let urlString = navigationAction.request.url?.absoluteString else {
            return nil
        }
        
        /*
        if urlString.contains("") {
            return addNewWebView(config: configuration)
        } else {
            return addNewWebView(config: configuration, appendClose: true)
        }*/
        log(direction: .WEBVIEW, ofType: self, datas: "새창 Open Deps [\(webViewList.count)]", urlString)
        return addNewWebView(config: configuration)
    }
    
    // - window.colse()가 호출되면 앞에서 생성한 팝업  웹뷰 닫는다.
    func webViewDidClose(_ webView: WKWebView) {
        
        let last = webViewList.popLast()
        last?.removeFromSuperview()
    }
    
    /// JavaScript  Announce Message  Alert Panel ( 확인  )
    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        
        let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert)
        alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
            completionHandler()
        }))
        
        guard let topVC = UIApplication.currentTopViewController() else {
            return
        }
        
        topVC.present(alertController, animated: true, completion: nil)
    }
    
    /// JavaScript Confirm Alert Panel ( 확인 / 취소 )
    func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        
        let alert = UIAlertController.init(title: "", message: message, preferredStyle: UIAlertController.Style.alert)
        
        let action_ok = UIAlertAction.init(title: "OK", style: UIAlertAction.Style.default) { (action: UIAlertAction) in
            completionHandler(true)
        }
        let action_cancel = UIAlertAction.init(title: "Cancel", style: UIAlertAction.Style.default) { (action: UIAlertAction) in
            completionHandler(false)
        }
        
        alert.addAction(action_ok)
        alert.addAction(action_cancel)
        
        DispatchQueue.main.async {
            guard let topVC = UIApplication.currentTopViewController() else {
                return
            }
            topVC.present(topVC, animated: true, completion: nil)
        }
    }
   
    /*강제터치 액션에 대한 응답
     지정된 요소가 미리보기를 표시할지 여부 결정
     */
    @available(iOS, introduced: 10.0, deprecated: 13.0)
    func webView(_ webView: WKWebView, shouldPreviewElement elementInfo: WKPreviewElementInfo) -> Bool {
        
        log(direction: .WEBVIEW, ofType: self, datas: ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>","url : \(String(describing: webView.url?.absoluteString.removingPercentEncoding))","shouldPreviewElement : \(elementInfo)")
        return false
    }
    
    
    // 사용자가 미리보기 작업을 수행 할 때 호출됩니다.
    @available(iOS, introduced: 10.0, deprecated: 13.0)
    func webView(_ webView: WKWebView, previewingViewControllerForElement elementInfo: WKPreviewElementInfo, defaultActions previewActions: [WKPreviewActionItem]) -> UIViewController? {
        log(direction: .WEBVIEW, ofType: self, datas: ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>","url : \(String(describing: webView.url?.absoluteString.removingPercentEncoding))","previewingViewControllerForElement : \(elementInfo)")
        return nil
    }
    
    // 사용자가 미리보기에서 팝업 액션을 수행 할 때 호출됩니다.
    @available(iOS, introduced: 10.0, deprecated: 13.0)
    func webView(_ webView: WKWebView, commitPreviewingViewController previewingViewController: UIViewController) {
        
        log(direction: .WEBVIEW, ofType: self, datas: ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>","url : \(String(describing: webView.url?.absoluteString.removingPercentEncoding))","commitPreviewingViewController : \(previewingViewController.description)")
        
    }
}

//MARK: BLOB 다운로드 IOS 14.5 이상 지원
extension IWKWebView: WKDownloadDelegate {
    @available(iOS 14.5, *)
    func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
        
        LoadingHUD.show()
        
        download.delegate = self
        
        log(direction: .WEBVIEW, ofType: self, datas: "WKDownloadDelegate", webView.url)
    }
    
    @available(iOS 14.5, *)
    func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
        
        if let containerUrl = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents") {
            // iCloud 파일 저장
            // 참고자료 : https://velog.io/@kuruma-42/iCloud-Data-Back-Up
            if !FileManager.default.fileExists(atPath: containerUrl.path, isDirectory: nil) {
                do {
                    try FileManager.default.createDirectory(at: containerUrl, withIntermediateDirectories: true, attributes: nil)
                }
                catch let error {
                    log(direction: .ERROR, ofType: self, datas: error.localizedDescription)
                }
            }
            
            let tempFileName = "\(Date().timeIntervalSince1970)_\(suggestedFilename)"
            let fileUrl = containerUrl.appendingPathComponent(tempFileName)
            completionHandler(fileUrl)
            
        } else {
            // 나의 iPhone 파일 저장
            let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let tempFileName = "\(Date().timeIntervalSince1970)_\(suggestedFilename)"
            let url = documentsURL.appendingPathComponent(tempFileName)
            
            completionHandler(url)
            log(direction: .WEBVIEW, ofType: self, datas: "Download Start", url)
        }
    }
    
    @available(iOS 14.5, *)
    func downloadDidFinish(_ download: WKDownload) {
        log(direction: .WEBVIEW, ofType: self, datas: "Download File Download Success!!", download.originalRequest?.url?.absoluteString)
        
        
        showPopup(message: "다운로드 완료!!")
        LoadingHUD.hide()
    }
    
    @available(iOS 14.5, *)
    func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
        log(direction: .ERROR, ofType: self, datas: "BLOB Download File Download Fail!!", error.localizedDescription)
        showPopup(message: "다운로드 실패!!")
        LoadingHUD.hide()
    }
}

+ Recent posts