반응형
앱의 종류에 따라서 필요시 웹 뷰를 구현하는 앱들이 있다. 단순히 보여주는 용도도 가능하나
하이브리드 앱은 웹과 앱이 서로 상호작용하도록 구현을 많이하는데 일반 사용자는 웹화면을 보고있는지 모르는 것처럼 느낄 수 있다.
단순히 보여주기만 할 수도 있지만 서로 상호작용 할 수도 할 수 있다.
1. WebView
WebView.swift
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: String
func makeUIView(context: Context) -> WKWebView {
guard let url = URL(string: self.url) else {
return WKWebView()
}
let webView = WKWebView()
webView.load(URLRequest(url: url))
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebView(url: "https://pgnt.tistory.com/112")
}
}
UIViewRepresentable: 해당 뷰를 스위프트UI 뷰 계층 구조에 통합하는 데 사용하는 UIKit 뷰의 래퍼 기능을 제공한다.
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
VStack{
NavigationLink(
destination: WebView(url: "https://pgnt.tistory.com/112")){
Text("TISTORY")
}.padding()
NavigationLink(
destination: WebView(url: "https://www.google.com")){
Text("GOOGLE")
}.padding()
NavigationLink(
destination: WebView(url: "https://www.naver.com")){
Text("NAVER")
}.padding()
NavigationLink(
destination: WebView(url: "https://www.facebook.com")){
Text("FACEBOOK")
}.padding()
Spacer()
}
.padding()
.padding(.top, 40)
.navigationTitle("WebView")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
여기까지 작성하면 단순하게 웹뷰를 스위프트UI의 계층구조에 통합하여 보여줄 수 있다.
하지만 어떠한 상호작용도 하지않고 단순히 표시만 해주기 때문에 좀 더 코드를 추가하여야 한다.
2. WebView + javascript message handler
ContentView.swift
import SwiftUI
//blog host
//https://pgnt.tistory.com/
import SwiftUI
struct ContentView: View {
// 관찰 가능한 개체에 구독하고 관찰 가능한 개체가 변경될 때마다 뷰를 무효화하는 속성 래퍼 유형입니다.
@ObservedObject var viewModel = WebViewModel()
@State var bar: Int = 0
var body: some View {
VStack {
// WebView(url: "https://pgnt.tistory.com/", viewModel: viewModel)
WebView(url: "http://localhost:18080/", viewModel: viewModel)
HStack {
Text("\(bar)")
Button(action: {
self.viewModel.foo.send(bar)
}) {
Text("보내기")
}
}
}
// RunLoop: 입력 소스를 관리하는 개체에 대한 프로그래밍 방식 인터페이스 .main: 메인 스레드의 런 루프를 반환.
.onReceive(self.viewModel.bar.receive(on: RunLoop.main)){ value in
self.bar = value + 1
}
}
}
WebView.swift
//
// WebView.swift
// example
//
// Created by 이정훈 on 2022/12/18.
//
import UIKit
import SwiftUI
import Combine
import WebKit
// 브릿지 통신안하는 일반적인 웹 뷰 생성
//struct WebView: UIViewRepresentable {
//
// var url: String
// func makeUIView(context: Context) -> WKWebView {
//
// guard let url = URL(string: self.url) else {
// return WKWebView()
// }
//
// let webView = WKWebView()
// webView.load(URLRequest(url: url))
// return webView
// }
//
// func updateUIView(_ uiView: UIViewType, context: Context) {
//
// }
//}
// 'webview.coordinator'가 예상 유형인 'WKScriptMessageHandler'와 일치하지 않기때문에 extention
// WKScriptMessageHandler: 웹 페이지에서 실행되는 JavaScript 코드에서 메시지를 수신하기 위한 인터페이스입니다.
extension WebView.Coordinator: WKScriptMessageHandler{
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage){
// 수신한 메시지의 네임이 일치하는경우
if message.name == "EXAMPLE" {
// 대리자의 필수 정의인 received
delegate?.receivedJsonValueFromWebView(value: message.body as! [String : Any?])
}
}
}
// javascript와 Native간의 데이터 통신을 위한 프로토콜/함수 정의
protocol WebViewHandlerDelegate {
func receivedJsonValueFromWebView(value: [String: Any?])
}
//protocol UIViewRepresentable: 해당 뷰를 SwiftUI 뷰 계층 구조에 통합하는 데 사용하는 UIKit 보기에 대한 래퍼입니다.
struct WebView: UIViewRepresentable, WebViewHandlerDelegate {
var url: String
@ObservedObject var viewModel: WebViewModel = WebViewModel()
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func receivedJsonValueFromWebView(value: [String : Any?]) {
print("from javascript JSON : \n", value)
if let action = value["action"] as? String {
print("action : \(action)")
switch action {
case "pong":
if let param = value["param"] as? [String: Any?] {
pong(param: param)
}
default:
return
}
}
}
func pong(param: [String: Any?]){
if let count = param["count"] as? String {
print("count : \(count)")
self.viewModel.bar.send(Int(count)!)
}
}
// 필수 정의
func makeUIView(context: Context) -> WKWebView {
print("makeUIView")
// 웹 사이트에 적용할 표준 동작을 캡슐화하는 개체.
let preferences = WKPreferences()
// JavaScript가 사용자 상호 작용없이 창을 열 수 있는지 여부
preferences.javaScriptCanOpenWindowsAutomatically = false
// 웹 보기를 초기화하는 데 사용하는 속성 모음.
let configuration = WKWebViewConfiguration()
// 웹 보기에 대한 기본 설정 관련 설정을 관리하는 개체 정의
configuration.preferences = preferences
// 앱의 기본 코드와 웹 페이지의 스크립트 및 기타 콘텐츠 간의 상호 작용을 조정하는 개체.
configuration.userContentController.add(self.makeCoordinator(), name: "EXAMPLE")
// 웹 뷰를 생성하고 지정된 프레임 및 구성 데이터로 초기화. CGRect: 직사각형의 위치와 치수를 포함하는 구조., .zero: 원점과 크기가 모두 0인 사각형.
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
// 웹보기의 탐색 동작을 관리하는 데 사용하는 개체
webView.navigationDelegate = context.coordinator
// 가로로 스와이프 동작이 페이지 탐색을 앞뒤로 트리거하는지 여부
webView.allowsBackForwardNavigationGestures = true
// 웹보기와 관련된 스크롤보기에서 스크롤 가능 여부
webView.scrollView.isScrollEnabled = true
if let url = URL(string: url) {
// 지정된 URL 요청 개체에서 참조하는 웹 콘텐츠를로드하고 탐색
webView.load(URLRequest(url: url))
}
return webView
}
// 필요하지 않는 이상 코드 추가하지 않아도 됨.
func updateUIView(_ uiView: WKWebView, context: Context) {
}
//class NSOpject: 대부분의 Objective-C 클래스 계층의 루트 클래스로, 하위 클래스가 런타임 시스템에 대한 기본 인터페이스와 Objective-C 개체로 작동하는 기능을 상속
//protocol WKNavigationDelegate: 탐색 변경을 수락 또는 거부하고 탐색 요청의 진행 상황을 추적하는 메소드
class Coordinator : NSObject, WKNavigationDelegate {
var parent: WebView
var foo: AnyCancellable? = nil
var delegate: WebViewHandlerDelegate?
// 생성자
init(_ uiWebView: WebView) {
self.parent = uiWebView
self.delegate = parent
}
// 소멸자
deinit {
foo?.cancel()
}
//탐색 요청 허용 또는 거부
// 지정된 기본 설정 및 작업 정보를 기반으로 새 콘텐츠를 탐색할 수 있는 권한을 대리자에게 요청
// func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
// print("지정된 기본 설정 및 작업 정보를 기반으로 새 콘텐츠를 탐색할 수 있는 권한을 대리자에게 요청")
// decisionHandler(.allow, preferences)
// }
// 지정된 작업 정보를 기반으로 새 콘텐츠를 탐색할 수 있는 권한을 대리인에게 요청합니다.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
print("지정된 작업 정보를 기반으로 새 콘텐츠를 탐색할 수 있는 권한을 대리인에게 요청합니다.")
// if let host = navigationAction.request.url?.host {
// // 특정 도메인을 제외한 도메인을 연결하지 못하게 할 수 있다.
// if host != "pgnt.tistory.com" {
// decisionHandler(.cancel)
// return
// }
// }
self.foo = self.parent.viewModel.foo.receive(on: RunLoop.main).sink(receiveValue: { value in
webView.evaluateJavaScript("fromNative('\(value)')", completionHandler: { result, error in
if let anError = error {
print("Error \(anError.localizedDescription)")
}
print("Result: \(result ?? "")")
})
})
decisionHandler(.allow)
}
// 탐색 요청에 대한 응답이 알려진 후 대리인에게 새 콘텐츠 탐색 권한을 요청
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
print("탐색 요청에 대한 응답이 알려진 후 대리인에게 새 콘텐츠 탐색 권한을 요청")
decisionHandler(.allow)
}
//요청의 로드 진행률 추적
// 주 프레임에서 탐색이 시작되었음을 대리자에게 알림
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print("주 프레임에서 탐색이 시작되었음을 대리자에게 알림")
}
// 웹 보기가 요청에 대한 서버 리디렉션을 수신했음을 대리자에게 알림
func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) {
print("웹 보기가 요청에 대한 서버 리디렉션을 수신했음을 대리자에게 알림")
}
// 웹 보기가 메인 프레임에 대한 콘텐츠를 수신하기 시작했음을 대리자에게 알림
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
print("웹 보기가 메인 프레임에 대한 콘텐츠를 수신하기 시작했음을 대리자에게 알림")
}
// 탐색이 완료되었음을 대리자에게 알림
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("탐색이 완료되었음을 대리자에게 알림")
}
//인증 문제에 응답
// 대리자에게 인증 질문에 응답하도록 요청
// func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// print("대리자에게 인증 질문에 응답하도록 요청")
// }
// 사용되지 않는 버전의 TLS를 사용하는 연결을 계속할지 여부를 대리인에게 묻음
func webView(_ webView: WKWebView, authenticationChallenge challenge: URLAuthenticationChallenge, shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void) {
print("사용되지 않는 버전의 TLS를 사용하는 연결을 계속할지 여부를 대리인에게 물음")
}
//탐색 오류에 응답하기
// 탐색 중 오류가 발생했음을 대리자에게 알림
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("탐색 중 오류가 발생했음을 대리자에게 알림")
}
//초기 탐색 프로세스 중에 오류가 발생했음을 대리자에게 알림
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
print("초기 탐색 프로세스 중에 오류가 발생했음을 대리자에게 알림")
}
// 웹 보기의 콘텐츠 프로세스가 종료되었음을 대리자에게 알림
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
print("웹 보기의 콘텐츠 프로세스가 종료되었음을 대리자에게 알림")
}
// //인스턴스 메소드
// func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
//
// }
// func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
//
// }
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebView(url: "https://pgnt.tistory.com/112")
}
}
WebViewModel.swift
import Foundation
import Combine
// ObservableObject: 개체가 변경되기 전에 내보내는 게시자가 있는 개체 유형입니다.
class WebViewModel: ObservableObject {
// PassthroughSubject: 다운스트림 구독자에게 요소를 브로드캐스트 하는 주제
var foo = PassthroughSubject<Int, Never>()
var bar = PassthroughSubject<Int, Never>()
}
간단한 서버 생성
index.html (간단하게 만들것이여서 static에 넣고 localhost:{port} )
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Insert title here</title>
<style>
html, body {
margin: 0;
}
button {
width: 100%;
}
</style>
<script>
// process and send message
function toNative(){
count = document.getElementById("count").value;
const sendData = {
action: "pong",
param: {
count: count
}
};
window.webkit.messageHandlers.EXAMPLE.postMessage(sendData);
}
// receive and procoess and response
function fromNative(data){
document.getElementById("count").value = Number(data) + 1;
return "from native data processing completed";
}
</script>
</head>
<body>
<button onclick="toNative();">퐁</button>
<input id="count" value="0"/>
</body>
</html>
http 호출시 초기 탐색 프로세스 중에 오류가 발생하였다면
ATS(App Transport Security)하위의 Key가 Allow Arbitrary Loads인 것의 Value를 YES로 변경해줍니다.
(키를 사용하여 앱의 다른 곳에서 ATS 보호를 유지하면서 웹 보기에 대한 안전하지 않은 로드를 허용 합니다.)
실행결과
반응형
'📱Mobile > 🔥Swift' 카테고리의 다른 글
[Swift] SwiftUI TabView(bottom navigation bar) + SideMenubar 예제 (0) | 2021.11.01 |
---|---|
[Swift] SwiftUI KeyChain Service 예제 (0) | 2021.10.30 |
[Swift] SwiftUI SHA256을 이용한 간단한 로그인 구현 예제 (0) | 2021.09.28 |
[Swift] SwiftUI/StoryBoard LaunchScreen + Sleep 예제 (0) | 2021.09.27 |
[Swift] Optional 개념정리 (변수뒤에 !와 ?) optional 예제 (0) | 2020.12.16 |