**BLOB 다운로드 관련 참고사이트
iOS에서 blob url로 파일 다운로드 받기
꽤 오랜 시간..? 깃헙 블로그에 포스팅하다가 다시 Midium으로 돌아왔습니다..! 뭔가 iOS 포스팅은 여기서 하는게 잘 어울리는 느낌이네요..ㅎㅎ
medium.com
WKWebView로 자바스크립트 연동시 function 방식으로 구현 하기 위해 구조를 변경함.
보통은 웹에서 APP 함수를 호출할 경우 아래 처럼 호출 하는 함수 명을 계속 등록해 줘야되는 문제 있다.
웹에서 기능별로 앱 함수를 호출하는 방식으로 아래와 같이 갯수가 많아지면 앱에서도 WKUserContentController
에 함수 명을 갯수만큼 등록해 줘야됨.
window.webkit.messageHandlers.deviceInfoSetting.postMessage(JSON.stringify(jsonObj));
window.webkit.messageHandlers.pushTokenInfo.postMessage(JSON.stringify(jsonObj));
window.webkit.messageHandlers.iconChange.postMessage(JSON.stringify(jsonObj));
window.webkit.messageHandlers.locationInfo.postMessage(JSON.stringify(jsonObj));
앱에서도 동일하게 호출 명 등록
let contentController = WKUserContentController()
contentController.add(self, name: "deviceInfoSetting")
contentController.add(self, name: "pushTokenInfo")
contentController.add(self, name: "iconChange")
contentController.add(self, name: "locationInfo")
.
WKScriptMessageHandler의 userContentController 에서 swich 로 분기해서 처리해줘야된다.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
swich message.name {
case "deviceInfoSetting":
case "pushTokenInfo":
case "iconChange":
case "locationInfo":
}
}
하이브리드 앱 개발시 웹에서 앱으로 앱에서 웹으로 수십개의 함수가 오간다 생각하면 코드가 얼마나 더러워 질지....
좋은 라이브러리들도 있지만 프로젝트를 하다보면 라이브러리를 사용 못하는 경우가 많아 간단하게 보기 좋은
코드를 만들기 위해서 시도해 봤다.
MainViewController.swift
랩핑한 IWKWebView 클래스 선언
class MainViewController: UIViewController {
@IBOutlet weak var webBaseView: IWKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webBaseView.delegate = self
}
}
//MARK: IWKWebViewDelegate
extension MainViewController: IWKWebViewDelegate {
//웹 페이지 로딩 완료
func webviewDidFinishNavigation(webView: WKWebView) {
let urlString: String = webView.url?.absoluteString ?? ""
let string = String(describing: urlString.removingPercentEncoding)
log(direction: .RECEIVE, ofType: self, datas: "WEBVIEW : \(string)")
}
}
IWKWebView.swift
WKWebView 랩핑 클래스로 BLOB / data 스키마 파일 다운로드 기능도 포함되어 있다.
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",
"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?
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)
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.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)
})
}
}
/// 웹뷰 페이지 로드
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 + " IOS"
}
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: "ios_javascript", 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
}
}
// 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) {
self.schemeMessageHandler(schemeUrl: url)
}
}
case .ios_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> {
self.javascriptMessageHandler(function: jsFunc+":", param: params)
}
}
}
}
case .download:
if let bodyString = message.body as? String {
dataDownload(dataString: bodyString)
}
default: break
}
}
//MARK: 자바스크립트 메세지 핸들러
func javascriptMessageHandler(function: String, param: Dictionary<String, Any>) {
/*
Javascript 데이터의 String 값으로 메서드를 지정하기 때문에
인스턴스 메서드가 있는지 꼭 확인해야됨. 없는 메서드 호출시 오류 발생함.
*/
let selector = Selector(function)
let originalMethod = class_getInstanceMethod(JavascriptMessageHandler.self, selector)
if originalMethod == nil {
log(direction: .ERROR, ofType: self, datas: "JS->Native \(selector) : 호출할 수 없는 함수 입니다.")
return
}
self.javascriptMessageHandler.perform(selector, with: param)
}
//MARK: 커스텀 스키마 메세지 핸들러
func schemeMessageHandler(schemeUrl: URL) {
if let scheme = schemeUrl.scheme {
switch scheme {
case "mgapp":
if let function = schemeUrl.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 = schemeUrl.queryDictionary
self.customSchemeMessageHandler.perform(selector, with: param)
}
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() {" +
" 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)")
}
}
}
//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)
}
}
}
// 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)
}
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)")
}
/// 네비게이션이 완료되면 호출됩니다.
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
log(direction: .WEBVIEW, ofType: self, datas: "Finish :\(String(describing: webView.url?.absoluteString))")
delegate?.webviewDidFinishNavigation(webView: webView)
}
}
// 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) {
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: "다운로드 실패!!")
}
}
JavascriptMessageHandler.swift
Web -> App 함수 호출시 JavascriptMessageHandler 클래스의 함수로 연결
웹에서 호출 종류가 추가 되더라고 해당 클래스에서 함수만 추가해서 웹에서 함수 이름으로 호출하면 끝...
class JavascriptMessageHandler: NSObject {
// App -> JS 함수 호출
var evaluateJSCompleted:((_ function: String, _ params: String) -> Void)?
/// Native -> Javascript 전송
private func evaluateJavaScript(function: String, jsonData: [String : Any]) {
self.evaluateJSCompleted?(function, jsonData.jsonString())
}
//MARK: 디바이스 정보 요청
@objc func deviceInfoSetting(_ params: Dictionary<String, Any>?) {
log(direction: .JAVASCRIPT, ofType: self, datas: "function : \(String(describing: params))")
// 현재 스토어 앱 버전 정보
StoreUpdateCheckWrapper.getStoreVersion { [weak self] (version) in
guard let `self` = self else {
return
}
let jsParam: [String : Any] = ["code" : 0,
"result" : ["current_version" : Bundle.appVersion(),
"new_version" : version,
"device_model" : UIDevice.deviceModelName(),
"device_os_version" : "\(UIDevice.deviceSystemName) \(UIDevice.deviceSystemVersion)"]
]
DispatchQueue.main.async {
self.evaluateJavaScript(function: "deviceInfoSetting", jsonData: jsParam)
log(direction: .JAVASCRIPT, ofType: self, datas: "디바이스 정보", jsParam.jsonString())
}
}
}
//MARK: Push Token 정보 요청
@objc func pushTokenInfo(_ params: Dictionary<String, Any>?) {
log(direction: .JAVASCRIPT, ofType: self, datas: "function : \(String(describing: params))")
let jsParam: [String: Any] = ["code" : 0,
"result" : ["platform" : "IOS",
"push_token" : UserData.deviceToken]
]
self.evaluateJavaScript(function: "pushTokenInfo", jsonData: jsParam)
log(direction: .JAVASCRIPT, ofType: self, datas: "Push Token", jsParam.jsonString())
}
}
ios_javascript.html
테스트용 html 파일
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style type='text/css'>
body {
-webkit-touch-callout: none;
-webkit-user-select: none;
}
h2 {
display: inline-block;
vertical-align: middle;
color: black;
margin: 0.5rem;
padding: 0;
}
nav {
display: inline-block;
vertical-align: middle;
}
ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}
li.icon {
flex-basis: 25%;
}
a {
display: block;
text-align: center;
margin: .25rem;
padding: .5rem 1rem;
text-decoration: none;
font-weight: bold;
color: white;
background: teal;
}
a:hover {
background: yellowgreen;
}
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
<!-- log 함수용 CSS -->
html { font-family:Helvetica; color:#222; }
h1 { color:#FE8FBD; font-size:24px; margin-top:24px; }
button { margin:0 3px 10px; font-size:14px; border: 2px solid #000000; }
.logLine_Native { border-bottom:1px solid #FFA67C; padding:4px 2px; font-family:courier; font-size:12px; }
.logLine_JS { border-bottom:1px solid #D867D8; padding:4px 2px; font-family:courier; font-size:12px; }
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="./ios-interface.js" defer></script>
</head>
<body>
<script>
window.onerror = function(err) {
log('window.onerror: ' + err)
}
var uniqueId = 1
function log(message, data, type) {
var log = document.getElementById('log')
var el = document.createElement('div')
el.className = type == 'native' ? 'logLine_Native' : 'logLine_JS'
el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
if (log.children.length) { log.insertBefore(el, log.children[0]) }
else { log.appendChild(el) }
}
/////////////////////////////////////////////
// 디바이스 정보
function deviceInfoSetting() {
var jsonObj = {"idx":1, "name":"twok"}; //파라메터
// Web -> App 함수 호출
//ios_interface.js 객체 호출할 App함수명 넘겨줄 파라메터
iosInterface.callNativeFunction("deviceInfoSetting", JSON.stringify(jsonObj), res => {
// App -> JS 넘겨준 데이터 처리 Handler function
console.log(res);
log('iosInterface Call', JSON.stringify(res))
})
}
/////////////////////////////////////////////
// Push Token 정보
function pushTokenInfo() {
var jsonObj = {};
iosInterface.callNativeFunction("pushTokenInfo", JSON.stringify(jsonObj), res => {
console.log(res);
log('iosInterface Call', JSON.stringify(res))
})
}
</script>
<!-- log 함수용 div -->
<div id='buttons'></div>
<div id='log'></div>
<h2>디바이스정보</h2>
<ul>
<li><a href="javascript:deviceInfoSetting();">디바이스정보</a></li>
</ul>
<h2>Push Token 정보</h2>
<ul>
<li><a href="javascript:pushTokenInfo();">푸쉬토큰정보</a></li>
</ul>
</body>
</html>
ios-interface.js
class IOSInterface {
callNativeFunction (functionName, dataParam, callbackFunction) {
/* functionName dataParam
형식 - {"deviceInfoSetting" : {"idx":1, "name":"twok"}}
functionName 을 dataParam의 Key로 삽입해서 Native로 전달 해야됨.
*/
var iosJsonObj = new Object();
var params = JSON.parse(dataParam);
iosJsonObj[functionName] = params;
console.log("JSON Data" + JSON.stringify(iosJsonObj));
// ios_javascript : IOS Webview 고정 Handler 이름
if (!window.webkit.messageHandlers.ios_javascript) {
return new Promise((resolve, reject) => {
callbackFunction({
code: 'E',
reason: new Error('Native handler was not ready.'),
});
resolve();
});
}
const callbackFromNative = arg => {
try {
// const res = JSON.parse(arg);
return new Promise((resolve, reject) => {
callbackFunction({
type: 'S',
data: arg,
});
resolve();
});
} catch (err) {
// Fail
return new Promise((resolve, reject) => {
callbackFunction({
code: 'E',
reason: err,
});
resolve();
});
}
};
if (!window.NativeAppInterface) {
window.NativeAppInterface = {};
}
window.NativeAppInterface[functionName] = callbackFromNative;
window.webkit.messageHandlers.ios_javascript.postMessage(JSON.stringify(iosJsonObj));
}
callNativeFunctionWithoutCallback (functionName, dataParam) {
window.webkit.messageHandlers.ios_javascript.postMessage(JSON.stringify(iosJsonObj));
}
}
const iosInterface = new IOSInterface();
//export default iosInterface;'Swift > WKWebview' 카테고리의 다른 글
| [SWIFT]Webview Log Consol 앱에서 출력하기 (0) | 2023.09.13 |
|---|---|
| [SWIFT]Custom WKWebView version 1.0 (0) | 2023.07.19 |
| [SWIFT]웹뷰의 보여지는 화면 스크린샷 이미지 생성 (0) | 2023.05.08 |
| [SWIFT]WKWebview 탭 제스쳐 적용 (0) | 2023.04.24 |
| [SWIFT]Javascript (JS <-> Native)/ Custom Scheme 연동 (0) | 2023.04.17 |