** 개인적으로 UIView를 상속받아 WKWebView를 랩핑하여 사용

import UIKit
import WebKit

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

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

//다운로드를 허용할 파일 mimeType, 확장자명 추가
internal let fileExtensions = [
    "image/jpeg": "jpg",
    "image/png": "png",
    "image/gif": "gif",
    "application/pdf": "pdf",
    "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);
    /// 자바스크립트 메세지 핸들러
    @objc optional func javascriptMessageHandler(function: String, param: Dictionary<String, Any>);
    /// 커스텀스키마  메세지 핸들러
    @objc optional func schemeMessageHandler(schemeUrl: URL);
    
}

var processPool: WKProcessPool?
class IWKWebView: UIView {
    
    var webView: WKWebView?
    var webViewList = [WKWebView]()
    
    weak var delegate: IWKWebViewDelegate?
    weak var parentVC: UIViewController?
    
    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.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.javascript.rawValue)
        contentController.add(self, name: WebMessageHandler.download.rawValue)
        
        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 = false
        webView?.isOpaque = false
        // 스와이프로 뒤로가기
//        webView?.allowsBackForwardNavigationGestures = true
        // Long Press 미리보기 차단
        webView?.allowsLinkPreview = false
        
        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
    }
    
    /// 웹뷰 페이지 로드
    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)
        
        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
            }
            
            self.webView?.load(request)
        })
    }
    
    /*
    //    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 페이지 로드
    func webViewHtmlLoad() {
        if let url = Bundle.main.url(forResource: "test", withExtension: "html") {
            webView?.load(URLRequest(url: url))
        }
    }
    
    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 = 1001
        self.addSubview(newWebView)
        
        newWebView.customUserAgent = webView?.customUserAgent
        webViewList.append(newWebView)
        
        return newWebView
    }
}

 

WKScriptMessageHandler 구현 (BLOB, Data 스키마 다운로드 포함)

// 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 {
                print(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) {
        
        let handlerName = WebMessageHandler(rawValue: message.name)
        
        // Scheme 및 JS -> Native 구분
        switch handlerName {
        case .customScheme:
            if let bodyString = message.body as? String {
                if let url = URL(string: bodyString) {
                    delegate?.schemeMessageHandler?(schemeUrl: url)
                }
            }
        case .javascript:
            if let bodyString = message.body as? String {
                let params = bodyString.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: "\\", with: "")
                
                if let jsDic = convertToDictionary(text: params) {
                    /*       함수명           파라메터
                     형식 - {"login" : {"idx":1, "name":"twok"}}
                           {"logout" : {}}
                     
                     Web -> Native javascript 함수 호출 처리
                     참고자료 : https://life-shelter.tistory.com/315
                     */
                    if let jsFunc = jsDic.keys.first?.description {
                        if let params = jsDic[jsFunc] as? Dictionary<String, Any> {
                            
                            delegate?.javascriptMessageHandler?(function: jsFunc+":", param: params)
                        }
                    }
                }
            }
        case .download:
            if let bodyString = message.body as? String {
                dataDownload(dataString: bodyString)
            }
            
        default: break
        }
    }
    
    /// 자바스크립트 호출
    func evaluateJavaScript(javaScriptFuncString: String, param: Any, 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() {" +
            
            "            window.location.href = reader.result;" +
            "            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)")
            }
        }
    }

    /* iCloud 사용시 테스트 필요
     let fileManager = FileManager.default
     if let icloudFolderURL = fileManager.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents"),
     let urls = try? fileManager.contentsOfDirectory(at: icloudFolderURL, includingPropertiesForKeys: nil, options: []) {
     
     log(direction: .WEBVIEW, ofType: self, datas: icloudFolderURL, urls)
     /* 파일 경로에 디렉토리가 없을 경우 디렉토리 생성
     do {
         try FileManager.default.createDirectory(at: fileURL, withIntermediateDirectories: true)
     } catch {
         print("error \(error)")
     }*/
     }*/
    
//MARK: Blob IOS 14.5 이하 버전 및 Data 스키마 다운로드
    /*
     - Blob 방식의 경우 downloadBlobFile 메소드의 window.webkit.messageHandlers.download 스키마 통해서 데이터 전달됨.
     - Data 스키마 decidePolicyFor 통해서 다운로드
     */
    private func dataDownload(dataString: String) {
        
        log(direction: .WEBVIEW, ofType: self, datas: "download Data: \(dataString)")
        
        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"
        
        
        //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(filePath: fullPath, data: imageData)
                }
                
            case ImageMimeType.png.rawValue:
                if let imageData = UIImage(data: data)?.pngData() {
                    fileSave(filePath: fullPath, data: imageData)
                }
                
            default: //문서 및 비디오 등등 (gif, mp4, pdf...)
                fileSave(filePath: fullPath, data: data)
            }
        }
    }
    
    private func fileSave(filePath: String, data: Data) {
        
        if FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) == true {
            log(direction: .ETC, ofType: self, datas: "IOS 14.5 이하 File Download Success!!", filePath)
        } else {
            log(direction: .ETC, ofType: self, datas: "IOS 14.5 이하 File Download Fail!!", filePath)
        }
    }
}

 

WKNavigationDelegate 구현

// MARK: WKNavigationDelegate
extension IWKWebView: WKNavigationDelegate {
    /// 탐색 허용 또는 취소여부 결정합니다. (즉시 블록을 호출하거나 나중에 블록을 비동기식으로 호출할 수 있다.)
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        
        
        log(direction: .WEBVIEW, ofType: self, datas: "\(String(describing: navigationAction.request.url?.absoluteString))")
        
        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" :
                externalUrlOpen(externalUrl: url.absoluteString)
                return decisionHandler(.cancel)
                
            case "blob":
                if #available(iOS 14.5, *) {
                    return decisionHandler(.download)
                    
                } else {
                    let urlString = url.absoluteString
                    downloadBlobFile(blobUrl: urlString)
                    return decisionHandler(.cancel)
                }
            case "data":
                if #available(iOS 14.5, *) {
                    return decisionHandler(.download)
                    
                } else {
                    let urlString = url.absoluteString
                    dataDownload(dataString: urlString)
                    return decisionHandler(.cancel)
                }
                /*
                let urlString = url.absoluteString
                dataDownload(dataString: urlString)
                return decisionHandler(.cancel)
                 */
            default: break
            }
        }
        
        // 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))")
            externalUrlOpen(externalUrl: 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)
    }
    /*
    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!)
            completionHandler(URLSession.AuthChallengeDisposition.useCredential,credential)
            return
        }
        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));
         */
    }*/
    
    /// URL이 잘못되었거나 네트워크 오류가 발생해 웹 페이지 자체를 아예 불러오지 못했을 때 호출
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        
        log(direction: .ERROR, ofType: self, datas: "\(error.localizedDescription)")
    }
    
    /// 콘텐츠를 로드하는 동안 오류가 발생하면 호출됩니다.
    func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        
        log(direction: .ERROR, ofType: self, datas: "\(error.localizedDescription)")
        
//        delegate?.webViewdidFailProvisionalNavigation?(webView: self)
        //        handleError(error: error as NSError)
    }
    
    /// 네비게이션이 완료되면 호출됩니다.
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        
        log(direction: .WEBVIEW, ofType: self, datas: "Finish :\(String(describing: webView.url?.absoluteString))")
        
        delegate?.webviewDidFinishNavigation(webView: webView)
    }
}

 

WKDownloadDelegate 구현 (#ios 14.5 이상에서 Blob/data 스키마 다운로드 방식 사용시)

//MARK: BLOB 다운로드 IOS 14.5 이상 지원
extension IWKWebView: WKDownloadDelegate {
    @available(iOS 14.5, *)
    func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
        download.delegate = self
        
        log(direction: .WEBVIEW, ofType: self, datas: webView.url)
    }
    
    @available(iOS 14.5, *)
    func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
        // 웹에서 지정한 파일 명
        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!!")
        showPopup(message: "다운로드 완료!!")
    }
    
    @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: "다운로드 실패!!")
    }
}

 

 

WKUIDelegate 구현

// MARK: WKUIDelegate
extension IWKWebView: WKUIDelegate {
    
    func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
       // 새창 요청시 View 형태로 추가
        return addNewWebView(config: configuration)
    }
    
    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)")
        
    }
}

+ Recent posts