** 개인적으로 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)")
}
}'Swift > WKWebview' 카테고리의 다른 글
| [SWIFT]Custom WKWebView version 1.0 (0) | 2023.07.19 |
|---|---|
| [SWIFT]Javascript (JS <-> Native)/ Custom Scheme 연동 V2 (0) | 2023.06.07 |
| [SWIFT]웹뷰의 보여지는 화면 스크린샷 이미지 생성 (0) | 2023.05.08 |
| [SWIFT]WKWebview 탭 제스쳐 적용 (0) | 2023.04.24 |
| [SWIFT]Javascript (JS <-> Native)/ Custom Scheme 연동 (0) | 2023.04.17 |