티스토리 뷰
iOS 앱의 경우 한 번 배포할 때마다 짧게는 하루, 길게는 일주일 이상의 시간이 심사에 소요됩니다. 한 번 잘못 배포되면 수정을 하고 싶어도 그저 기다려야 합니다. 웹 뷰를 사용하면 이런 심사 기간없이 빠른 대응과 컨텐츠 추가가 가능해 많은 앱들이 앱 내부에 부분적으로 웹 뷰를 사용하고 있습니다.
SwiftUI에서 WKWebView를 사용해 아이폰 <-> 웹 사이에 데이터를 주고 받아보며 WKWebView 사용법에 대해 알아보겠습니다.
작업 환경
- macOS 14.0 Sonoma
- XCode 15.0
- iOS Simluator 17.0
SwiftUI에서 UIViewRepresentable 사용하기
Swift에서는 WKWebView를 사용해 웹 뷰를 네이티브 앱에 띄어주는 것이 가능합니다. UIKit에서는 바로 사용이 가능하지만 SwiftUI에서는 지원이 안되고 있습니다.
UIViewRepresentable를 사용해 UIView 타입인 WKWebView를 래핑해주겠습니다.
import SwiftUI
import WebKit
struct SUWebView: UIViewRepresentable {
var url: URL?
var webView: WKWebView
init(url: URL? = nil) {
self.url = url
self.webView = WKWebView()
}
func makeUIView(context: Context) -> WKWebView {
guard let url = url else {
return webView
}
webView.load(URLRequest(url: url))
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
return
}
}
UIViewRepresentable 프로토콜을 채택하는 SUWebView를 만들어주었습니다. 웹 뷰에 사용될 URL을 인자로 받고 WKWebView를 생성해 프로퍼티로 가집니다.
makeUIView(context:) 함수에서 load 메소드를 호출해 WebView를 로딩합니다.
웹 화면 만들기
두 가지 파트로 나뉩니다.
1. 텍스트
Swift(네이티브) -> Javascript(웹) 이벤트 전달
아이폰에서 버튼을 만들어 클릭하면 웹의 텍스트를 바꿔줄 것입니다.
2.버튼
Javascript(웹) -> Swift(네이티브) 이벤트 전달
웹에서 버튼 클릭 시 아이폰으로 텍스트를 전달할 것입니다.
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="style.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="center-element">
<p class= "margin-bottom" id="displayText">이 곳은 웹 뷰입니다. 넘어오지 마십쇼 휴먼.</p>
<button id="submit-button" class="button-normal bg-orange margin-bottom">자바스크립트 버튼</button>
</div>
<script src="app.js"></script>
</body>
</html>
CSS와 JS를 포함한 코드는 Github에서 확인할 수 있습니다.
배포된 페이지 - https://never-better.github.io/webview-for-swiftUI/
코드 - https://github.com/never-better/js-webview-for-swiftui/blob/main/index.html
SwiftUI 화면 만들기
앞서 구현한 SUWebView를 사용해 웹 뷰를 보여줄 수 있습니다.
네이버 화면이 잘 뜨네요 ㅎㅎ 이제 한 화면에 웹 뷰와 네이티브(Swift) 뷰를 반반씩 만들어주겠습니다.
struct ContentView: View {
let webView: SUWebView
@State private var text: String = "아이폰입니다."
var body: some View {
VStack {
webView
VStack {
Button(
action: {
webView.sendButtonTapped()
},
label: {
Text("아이폰 버튼").padding()
}
)
.buttonStyle(.borderedProminent)
Spacer()
Text(text)
.font(.title)
Spacer()
}
Spacer()
}
.onReceive(webView.textPublisher(), perform: { newText in
self.text = newText
})
}
}
구현한 화면은 아래 그림처럼 위 아래로 반반씩 보여집니다.
아이폰(Swift) -> 웹(Javascript)
Swift(네이티브)에서는 evaluateJavaScript() 함수를 사용해 Javascript에 있는 텍스트를 실행할 수 있습니다.
webView.evaluateJavaScript("console.log('콘솔 로그'))
위 예시처럼 String인자를 전달하면 그 안에 있는 구문이 실행됩니다.
// app.js
var textArray = [
"이 곳은 웹 뷰입니다. 넘어오지 마십쇼 휴먼.",
"으악",
"내 몸에서 나가!!!",
"지금까지 자바스크립트였습니다.",
"The End",
];
var currentIndex = 0
function randomText() {
currentIndex++;
if (currentIndex === textArray.length) {
currentIndex = 0;
}
displayText.textContent = textArray[currentIndex];
}
Javascript 파일인 app.js에서 텍스트를 바꿔주는 함수를 작성했습니다. 배열을 순회하며 해당하는 인덱스의 String으로 HTML의 텍스트를 바꿔줍니다.
SwiftUI 니까 evaluateJavaScript(, completionHandler: ) 대신 async/await 함수를 사용하겠습니다. 위 app.js의 changeText()를 호출합니다.
func sendButtonTapped() {
Task {
do {
try await webView.evaluateJavaScript("changeText()")
} catch {
print("아이폰 버튼 클릭 에러")
}
}
}
View의 버튼 action에서 구현한 함수를 호출합니다.
Button(
action: {
webView.sendButtonTapped()
},
// 생략
}
실행하면!!
버튼을 클릭하니 View가 안 바뀌고 에러가 납니다;; 공식문서에서 코드를 메인 스레드에서 실행해줘야 된다고 합니다.
Javascript 함수를 호출해 UI를 바꾸기 때문에 메인 스레드 호출이 필요한 것으로 보입니다. DispatchQueue.main.async를 사용해 다시 작성해주었습니다.
// SUWebView
func sendButtonTapped() {
DispatchQueue.main.async {
Task {
do {
try await webView.evaluateJavaScript("changeText()")
} catch {
print("아이폰 버튼 클릭 에러")
}
}
}
}
이젠 잘 될 겁니다!
버튼을 클릭했더니 이번엔 @main 함수에서 에러가 납니다. 언래핑 에러???
뭐지 하고 "evaluateJavaScript fatal error"를 구글에 검색하니 Stack Overflow에 답변이 나옵니다.
Xcode 14.1에서 확인되는 에러인데 이름이 같은 두 함수(1번, 2번)를 컴파일러가 구분을 못하는 것 같다고 합니다. (지금 15.0 인데도 안되는데요 애플선생님?!??)
해결하는데 두 가지 방법이 있다고 합니다.
1. "; 0" 붙이기
try? await webView.evaluateJavaScript("console.log('hello world'); 0")
위 코드 처럼 인자 끝에 붙여주면 "; 0" 된다고 합니다.
2. 기존 completionHandler사용하기
저는 1번을 선택해서 수정을 했습니다.
// 수정 후
try await webView.evaluateJavaScript("changeText(); 0")
Swift(네이티브)에서 Javascript(웹) 코드를 정상적으로 실행해 텍스트가 바뀌는 것을 확인할 수 있습니다.
웹(Javascript) -> 아이폰(Swift)
Javascript의 window.webkit을 통해 메시지를 전달할겁니다.
window.webkit.messageHandlers.{메시지 핸들러 이름}.postMessage("전달할 메시지 입력")
메시지 핸들러 이름을 Swift에서 사용할 거기 때문에 잘 기억해야합니다.
window.webkit.messageHandlers.textfieldText.postMessage(text)
위에서와 마찬가지로 버튼 클릭시 배열을 순회하며 차례대로 텍스트를 보내주겠습니다.
var javascriptArray = [
"웹 뷰에서 왔습니다.",
"아이폰 참 좋네요 ㅎㅎㅎ",
"🧻🧻🧻🧻🧻🧻",
"집들이 선물입니다",
"The End"
];
var jsIndex = 0
document.getElementById("submit-button").addEventListener("click", function() {
let text = javascriptArray[jsIndex]
window.webkit.messageHandlers.textfieldText.postMessage(text)
jsIndex++;
if (jsIndex == javascriptArray.length) {
jsIndex = 0;
}
});
Javascript에서 설정은 다 완료되어서 다시 Swift로 넘어옵니다. Swift에서는 정적 변수를 사용해 메시지 핸들러 이름을 관리해주겠습니다.
enum MyWebViewAction {
static let textfieldText = "textfieldText"
}
WKScriptMessageHandler
WKScriptMessageHandler를 채택해 함수를 구현하면 Javascirpt에서 보내주는 메시지를 읽을 수 있습니다.
UIKit에서는 ViewController에서 Delegate, Handler 프로토콜을 채택해주었는데 SwiftUI에서는 UIViewRepresentable의 Coordinator 클래스에 채택합니다.
struct SUWebView: UIViewRepresentable {
var url: URL?
var webView: WKWebView
var coordinator: Coordinator
init(url: URL? = nil) {
self.url = url
self.webView = WKWebView()
self.coordinator = Coordinator()
self.webView.configuration.userContentController.add(coordinator, name: MyWebViewAction.textfieldText)
}
func makeUIView(context: Context) -> WKWebView {
guard let url = url else {
return webView
}
webView.load(URLRequest(url: url))
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
return
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
// MARK: Coordinator
class Coordinator: NSObject, WKScriptMessageHandler {
var textPublisher = PassthroughSubject<String, Never>()
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == MyWebViewAction.textfieldText {
guard let text = message.body as? String else {
return
}
textPublisher.send(text)
}
}
}
func textPublisher() -> AnyPublisher<String, Never> {
return self.coordinator.textPublisher.eraseToAnyPublisher()
}
}
SUWebView 내부의 Coordinator 클래스가 WKScriptMessageHandler를 채택해 메시지를 읽어옵니다.
View로 데이터를 전달해줄 것이기 때문에 Combine을 사용했습니다. userContentController() 함수 내부에서 PassthroughSubject를 사용해 전달받은 text 값을 publish 해줍니다.
// ContentView
.onReceive(webView.textPublisher(), perform: { newText in
self.text = newText
})
View에서는 onReceive를 사용해 Publisher를 구독하게 됩니다. View의 텍스트를 새로운 텍스트로 바꿔줍니다.
Javascript 버튼을 눌러 네이티브 뷰에 Text가 잘 전달되네요!
정리
SwiftUI에서 WKWebView를 사용해 아이폰과 웹 사이에 데이터를 주고 받아봤습니다.
현재는 간단한 Text만 주고 받았는데 실제로 Textfield 이벤트와 같이 다양한 이벤트를 처리하면 코드가 더 복잡해집니다. (이벤트 처리 안 해줬다고 에러도 겁나 뜹니다)
웹 뷰가 네이티브 뷰보다 느리게 렌더링되는게 한 화면에 있으니 확실히 확인이 가능합니다.
웹 뷰를 사용할 거면 부분적으로 사용하기 보다 전체적으로 사용하는게 사용자 경험 측면에서도 나아보입니다.
끝끝끝끝끝
출처
WKWebView | Apple Developer Documentation
An object that displays interactive web content, such as for an in-app browser.
developer.apple.com
Fatal error while using evaluateJavaScript on WKWebView
WKWebView crashes while trying to evaluate JavaScript on Xcode 14.1, using Swift, tested on iOS but same behaviour should be on macOS. I made a vastly simplified example to try to find a solution, ...
stackoverflow.com
'SwiftUI > SwiftUI 공부' 카테고리의 다른 글
[SwiftUI] @Observable, @ObservationTracked, @ObservationIgnored 알아보기 (0) | 2023.10.16 |
---|---|
SwiftUI Animation 안될 때 (feat. 생명주기) (0) | 2023.10.06 |
[SwiftUI] Color 사용하는 7가지 방법 (0) | 2022.09.12 |
- Total
- Today
- Yesterday
- 부스트캠프7기
- 부스트캠프iOS
- 프로그래머스
- Swift DocC
- Combine
- 부스트캠프
- 날씨어플
- 개발
- UX
- 코딩 테스트
- vapor
- SwiftUI
- Swift 디자인 패턴
- Swift
- 책
- 필독서
- todo앱
- 책리뷰
- Swift공식문서
- swiftUI 기초
- 책후기
- 코딩
- 애플
- Swift 서버
- TODO
- 앱개발
- 코딩테스트
- Swift문법
- ios
- 디자인 패턴
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
29 | 30 |