SwiftUI: 웹 뷰(WKWebView) 추가하기 및 자바스크립트 실행 (Representable 사용)
SwiftUI: 웹 뷰(WKWebView) 추가하기 및 자바스크립트 실행 (Representable 사용)
[rcblock id=”5440”]
소개
SwiftUI 내부에 웹 뷰(WKWebView)를 추가하는 방법입니다. 원래 웹 페이지를 표시하는 WKWebView는 UIKit과 호환되는 요소인데, 찾아본 결과 SwiftUI에는 웹을 표시할 수 있는 뷰가 없는 것처럼 보여서 역시 Representable을 이용해서 간접적으로 추가하는 방법을 설명하겠습니다.
방법
두 가지 방법을 알아보겠습니다.
- 단순히 SwiftUI에 웹 뷰를 추가하는 방법입니다.
- 위 예제의 웹 뷰에서
evaluateJavaScript(_:)를 원하는 시점에 실행할 수 있도록 합니다.
웹 뷰를 추가하는 방법
1) UIViewRepresentable을 준수(conform)하고 웹 뷰를 감싸는 WebView를 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct WebView: UIViewRepresentable {
typealias UIViewType = WKWebView
var url: URL?
func makeUIView(context: Context) -> UIViewType {
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = false // JavaScript가 사용자 상호 작용없이 창을 열 수 있는지 여부
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.allowsBackForwardNavigationGestures = true // 가로로 스와이프 동작이 페이지 탐색을 앞뒤로 트리거하는지 여부
webView.scrollView.isScrollEnabled = true // 웹보기와 관련된 스크롤보기에서 스크롤 가능 여부
if let url = url {
webView.load(URLRequest(url: url)) // 지정된 URL 요청 개체에서 참조하는 웹 콘텐츠를 로드하고 탐색
}
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
// ... 잠시 후 작성 ... //
}
}
UIViewRepresentable에 관한 자세한 내용은 SwiftUI: Representable을 이용해서 UIViewController 띄우기를 참고해주세요. (뷰 컨트롤러에 관한 글이지만 뷰(UIView)도 내용이 거의 같습니다.)makeUIView함수에서WKWebView인스턴스를 리턴합니다. 필요한 경우 함수 내부에서 각종 설정 등을 미리 지정합니다.- var url
- 파라미터로
URL을 받습니다.
- 파라미터로
2) SwiftUI의 뷰(ContentView 등) 내부에 위의 Representable을 추가합니다.
1
2
3
4
5
var body: some View {
// ... //
WebView(url: URL(string: "https://www.website.con"))
// ... //
}
url 파라미터에 URL을 입력해서 유효한 주소인 경우 웹 페이지가 표시됩니다. 아래 스크린샷은 SwiftUI의 뷰 내부에 WebView를 삽입한 예제입니다.
웹 뷰에서 evaluateJavaScript(_:)를 원하는 시점에 실행
SwiftUI 환경에서 ContentView 내부에 버튼이 있는데 이 버튼을 누르면 웹 페이지에서 특정 자바스크립트 코드를 실행하고 싶다면 어떻게 할까요?
일반 UIKit이었다면 단순히 버튼 이벤트 내부에 webView.evaluateJavasScript("스크립트")를 넣었으면 되었지만 SwiftUI 환경에서는 굉장히 복잡합니다.
1) WebViewData 클래스를 추가
1
2
3
4
5
6
7
import Combine
// ... //
class WebViewData: ObservableObject {
var functionCaller = PassthroughSubject<String, Never>()
var shouldUpdateView = true
}
- functionCaller
PassthroughSubject<Output, Failure>타입입니다.String값을 내보냅니다.
- shouldUpdateView
updateUIView를 실행해야 하는지 여부에 대한Bool값입니다.true로 지정합니다.
- [심화] PassthroughSubject
- Downstream 구독자(subscribers)들에게 값을 전파하는 subject(send 메서드를 호출해서 stream에 값을 주입하기 위해 사용하는 Publisher)입니다.
CurrentValueSubject와 달리 value값 접근 불가, 최신값을 저장하지 않는다는 차이점이 있습니다.- 기존의 명령형(imperative) 코드를
Combine모델로 적용할 때 편리한 방법을 제공하는 Subject Class입니다. - 구독자가 없거나 demand 값이 0인 경우 값을 버립니다.
- 자세한 내용: https://0urtrees.tistory.com/324
2) WebView 내에 WebViewData에 대한 상태 변수 추가
1
2
3
4
5
struct WebView: UIViewRepresentable {
// ... //
@StateObject var data: WebViewData
// ... //
}
3) WebView 내에 코디네이터를 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Combine
// ... //
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, WKNavigationDelegate {
/// WebView Representable
var parentWebView: WebView
var webView: WKWebView? = nil
private var cancellable: AnyCancellable?
init(_ parentWebView: WebView) {
self.parentWebView = parentWebView
super.init()
}
func tieFunctionCaller(data: WebViewData) {
print("Passthrough:", #function)
cancellable = data.functionCaller.sink(receiveValue: { js in
self.webView?.evaluateJavaScript(js)
})
}
}
- 코디네이터에 관한 자세한 내용은 SwiftUI: Representable을 이용해서 UIViewController 띄우기를 참고해주세요.
- parentWebView
- Representable View의 인스턴스를 담습니다.
- webView
- Representable View의
makeUIView(...)를 통해 생성된WKWebView인스턴스를 담습니다. - 잠시 후 자세히 설명합니다.
- Representable View의
- init(_ parentWebView: WebView)
- 파라미터로
WebView를 지정합니다. - 이것을
makeCoordinator()에서 리턴시킵니다.
- 파라미터로
- tieFunctionCaller(…)
WebViewData의functionCalller를 소환하는 역할을 합니다.functionCalller는PassthroughSubject이므로sink를 호출할 수 있습니다.- 어느 특정 조건이 되면(예: 버튼을 누른 경우)
String값이 배출됩니다. sink를 통해 배출된 js값(String타입)을webView?.evaluateJavaScript(js)로 실행합니다.
4) WebView 내에 updateUIView를 작성합니다.
1
2
3
4
5
6
7
8
9
func updateUIView(_ uiView: UIViewType, context: Context) {
guard data.shouldUpdateView else {
data.shouldUpdateView = false
return
}
context.coordinator.tieFunctionCaller(data: data)
context.coordinator.webView = uiView
}
updateUIView는 웹뷰가 실행된 시점에 바로 실행되며,makeUIView다음에 실행됩니다.uiView는 현재 실행되고 있는WebView(=>UIViewType)입니다.context는UIViewRepresentable(=>WebView또는UIViewType)에 대한 컨텍스트 변수입니다.- context.coordinator
- 현재 컨텍스트에 있는 코디네이터입니다.
teiFunctionCaller함수를 실행합니다.- 코디네이터의
webView를uiView와 연결합니다.
5) SwiftUI의 뷰(ContentView 등) 내부에 WebViewData를 추가합니다.
1
2
3
4
5
struct ContentView: View {
// ... //
@StateObject var webViewData = WebViewData()
// ... //
}
6) SwiftUI의 뷰(ContentView 등) 내부에 WebView를 추가합니다.
1
2
3
var body: some View {
WebView(url: URL(string: "https://example.con"), data: webViewData)
}
앞 섹션과의 차이점은 WebView의 파라미터로 data가 추가된 점입니다. 여기서 webViewData 상태 변수를 추가합니다.
7) 버튼을 누르면 특정 자바스크립트가 실행되도록 하기
webViewData를 이용합니다.
1
2
3
4
5
6
7
8
9
Button {
webViewData.functionCaller.send(
"""
document.querySelector("button[id^='playbut']").click()
"""
)
} label: {
Image(systemName: "play.fill")
}
evaluateJavaScript가 실행되기 까지의 과정을 간략하게 설명하면 다음과 같습니다.
- updateUIView(_:context:) <-
tieFunctionCaller실행,webView:WKWebView등록 - 커스텀 JS 데이터 전달
@StateObject webViewData.functionCaller.send("CUSTOM_JS") - tieFunctionCaller(data:)에서 data.functionCalller.sink… <-
webView?.evaluateJS실행
http://www.giphy.com/gifs/uiSo2m6jk28v4uUf0H
전체 코드
https://gist.github.com/ayaysir/925f2497e33efe252836300c575b334a
This post is licensed under
CC BY 4.0
by the author.
