관련 글

 

소개

UIDelegate에서 구현해야되는 자바스크립트 경고창 (Alert, Confirm, Prompt) 표시는 어떻게 해야 할까요?

  1. Coordinator 클래스를 만들고 WKUIDelegate를 준수(conform)
    • 부모 뷰 (WebViewRP)를 참조하는 변수를 정의하고 이 변수를 통해 코디네이터와 통신
  2. 코디네이터 클래스 안에 자바스크립트 경고창 표시와 관련된 함수를 오버라이딩 및 정의
  3. 제목 메시지 및 Alert 표시 여부는 @State, @Binding 변수들을 통해 전달
    • 참고) @State@Binding 변수들은 ObservableObject 뷰모델의 @Published 로 대체할 수 있습니다.
  4. Alert의 확인버튼, Confirm의 예/아니오 버튼, Prompt의 텍스트 필드 및 전송/취소 버튼이 클릭되었을 때 해야 할 작업등은 (파라미터) -> Void 형태의 클로저 함수로 전달
    • 참고) ObservableObject 뷰모델을 사용하는 경우 뷰모델에 해당 클로저 함수를 저장하도록 지정할 수 있습니다.

ObservableObject를 사용하여 분리한 코드는 여기에서 볼 수 있습니다.

 

코드 (@State, @Binding 사용)


import SwiftUI
struct ContentView: View {
@State private var showAlert = false
@State private var showConfirm = false
@State private var showPrompt = false
@State private var alertMessage = ""
@State private var promptInput = ""
@State private var alertHandler: (() -> Void)?
@State private var confirmHandler: ((Bool) -> Void)?
@State private var promptHandler: ((String?) -> Void)?
var body: some View {
WebViewRP(
showAlert: $showAlert,
showConfirm: $showConfirm,
showPrompt: $showPrompt,
alertMessage: $alertMessage,
alertHandler: $alertHandler,
confirmHandler: $confirmHandler,
promptHandler: $promptHandler
)
.alert(alertMessage, isPresented: $showAlert) {
Button("OK", role: .none) {
alertHandler?()
}
}
.alert(alertMessage, isPresented: $showConfirm) {
Button("No", role: .cancel) {
confirmHandler?(false)
}
Button("Yes", role: .none) {
confirmHandler?(true)
}
}
.alert(alertMessage, isPresented: $showPrompt) {
TextField("Prompt", text: $promptInput)
Button("제출", role: .none) {
promptHandler?(promptInput)
promptInput = ""
}
Button("취소", role: .cancel) {
promptHandler?(nil)
promptInput = ""
}
}
}
}


struct WebViewRP: UIViewRepresentable {
typealias UIViewType = WKWebView
@Binding var showAlert: Bool
@Binding var showConfirm: Bool
@Binding var showPrompt: Bool
@Binding var alertMessage: String
@Binding var alertHandler: (() -> Void)?
@Binding var confirmHandler: ((Bool) -> Void)?
@Binding var promptHandler: ((String?) -> Void)?
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
let url = URL(string: "https://testpages.herokuapp.com/styled/alerts/alert-test.html")!
webView.load(URLRequest(url: url))
webView.uiDelegate = context.coordinator
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, WKUIDelegate {
let parent: WebViewRP
init(_ parent: WebViewRP) {
self.parent = parent
}
func webView(
_ webView: WKWebView,
runJavaScriptAlertPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo
) async {
await withCheckedContinuation { continuation in
DispatchQueue.main.async {
self.parent.showAlert = true
self.parent.alertMessage = message
// 대기(await)중이던 async 함수를 재개한다.
self.parent.alertHandler = {
continuation.resume()
}
}
}
}
// Confirm창: completionHandler 전송으로 구현 (간단)
func webView(
_ webView: WKWebView,
runJavaScriptConfirmPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping @MainActor (Bool) -> Void
) {
DispatchQueue.main.async {
self.parent.showConfirm = true
self.parent.alertMessage = message
self.parent.confirmHandler = completionHandler
}
}
// Prompt창: async/await로 구현 (복잡)
func webView(
_ webView: WKWebView,
runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?,
initiatedByFrame frame: WKFrameInfo
) async -> String? {
return await withCheckedContinuation { continuation in
DispatchQueue.main.async {
self.parent.showPrompt = true
self.parent.alertMessage = prompt
self.parent.promptHandler = { inputValue in
continuation.resume(returning: inputValue)
}
}
}
}
}
}

view raw

WebViewRP.swift

hosted with ❤ by GitHub

 

CompletionHandler를 사용하는 메서드에서 completionHandler의 역할

웹 페이지의 자바스크립트 실행 부분에서 경고창이 뜨면 다음 명령을 기다리게 됩니다. iOS 앱에서 Handler 함수가 실행되면 다음에 해야 할 자바스크립트 작업을 이어서 실행합니다.

이 핸들러 함수를 컨텐트 뷰에 전달하고 SwiftUI의 경고창에서 버튼을 눌렀을 때 전달받은 핸들러를 실행하면 웹페이지의 자바스크립트가 다음 작업을 실행하게 되는 것입니다.

 

async/await 메서드에서 self.parent.alertHandler = {…} 부분의 동작 원리

핵심 개념

alertHandler@Binding으로 전달된 클로저입니다. Swift의 클로저는 코드 블록을 캡처하고, 나중에 실행될 수 있는 “일급 객체”입니다. 이 클로저를 continuation.resume()로 연결하여, JavaScript alert()가 비동기적으로 처리될 수 있도록 합니다.

 

동작 원리
  1. alertHandler 초기 설정
    • parent.alertHandler는 UIViewRepresentable의 SwiftUI View와 연결된 상태 변수입니다.
    • 초기값은 비어 있지만, webView(_:runJavaScriptAlertPanelWithMessage:initiatedByFrame:) 호출 시 새로운 클로저를 할당합니다.
  2. withCheckedContinuation 사용
    • Swift Concurrency에서 withCheckedContinuation은 콜백 기반 코드를 async/await로 변환하는 데 사용됩니다.
    • JavaScript alert() 이벤트는 비동기로 처리되므로, Continuation을 사용해 비동기 흐름을 제어합니다.
  3. 클로저 저장
    • JavaScript alert() 호출 시 self.parent.alertHandler에 다음 동작을 정의한 클로저를 할당합니다:
    self.parent.alertHandler = {
      continuation.resume()
    }
    • 이 클로저는 SwiftUI의 Alert의 “OK” 버튼과 연결되어, 버튼이 눌릴 때 실행됩니다.
  4. Alert 표시
    • showAlerttrue로 설정하면 SwiftUI에서 Alert이 표시됩니다.
    • Alert의 “OK” 버튼을 누르면 alertHandler()가 실행되고, Continuation이 재개됩니다.
  5. Continuation 재개
    • continuation.resume()가 호출되면, await가 완료되고 webView(_:runJavaScriptAlertPanelWithMessage:)의 호출이 끝납니다.
    • 이로써 비동기 흐름이 정상적으로 처리됩니다.