프로젝트에서 사용하는 커스텀 웹뷰 클래스
- 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()
}
}
'Swift > WKWebview' 카테고리의 다른 글
[Swift] 사파리 브라우저 WebView 디버깅 설정(IOS 16.4 이상) (0) | 2024.05.27 |
---|---|
[SWIFT]Webview Log Consol 앱에서 출력하기 (0) | 2023.09.13 |
[SWIFT]Javascript (JS <-> Native)/ Custom Scheme 연동 V2 (0) | 2023.06.07 |
[SWIFT]웹뷰의 보여지는 화면 스크린샷 이미지 생성 (0) | 2023.05.08 |
[SWIFT]WKWebview 탭 제스쳐 적용 (0) | 2023.04.24 |