Files
sodalive-ios/SodaLive/Sources/V2/Component/Address/KakaoPostcodeSearchView.swift
T

166 lines
6.1 KiB
Swift

import Foundation
import SwiftUI
import WebKit
struct KakaoPostcodeResult {
let zonecode: String
let address: String
init?(payload: Any) {
guard let values = payload as? [String: Any],
let zonecode = values["zonecode"] as? String,
let address = values["address"] as? String,
!zonecode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
!address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil }
self.zonecode = zonecode
self.address = address
}
}
struct KakaoPostcodeSearchView: View {
let onSelect: (KakaoPostcodeResult) -> Void
@Environment(\.dismiss) private var dismiss
@State private var isLoading = true
@State private var hasFailed = false
var body: some View {
VStack(spacing: 0) {
HStack {
Text("우편번호 검색").appFont(.body2)
Spacer()
Button("닫기") { dismiss() }
.appFont(.body3)
.frame(minHeight: 44)
.accessibilityLabel("우편번호 검색 닫기")
}
.foregroundColor(.white)
.padding(.horizontal, SodaSpacing.s14)
ZStack {
KakaoPostcodeWebView(onSelect: onSelect) { failed in
hasFailed = failed
isLoading = false
}
.opacity(hasFailed ? 0 : 1)
.allowsHitTesting(!hasFailed)
.accessibilityHidden(hasFailed)
if hasFailed {
Text("주소 검색을 불러오지 못했습니다. 닫은 후 다시 시도해 주세요.")
.appFont(.body3)
.foregroundColor(.white)
.padding(SodaSpacing.s14)
} else if isLoading {
ProgressView("주소 검색을 불러오는 중")
.tint(.white)
.foregroundColor(.white)
.padding(SodaSpacing.s14)
.background(Color.gray900)
.cornerRadius(SodaRadius.r14)
}
}
}
.background(Color.black)
}
}
private struct KakaoPostcodeWebView: UIViewRepresentable {
let onSelect: (KakaoPostcodeResult) -> Void
let onLoad: (Bool) -> Void
private static let handlerName = "postcodeSelection"
func makeCoordinator() -> Coordinator {
Coordinator(onSelect: onSelect, onLoad: onLoad)
}
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.userContentController.add(context.coordinator, name: Self.handlerName)
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.loadHTMLString(Self.html, baseURL: URL(string: "https://postcode.map.kakao.com/"))
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
static func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
coordinator.isActive = false
uiView.stopLoading()
uiView.configuration.userContentController.removeScriptMessageHandler(forName: handlerName)
uiView.navigationDelegate = nil
uiView.uiDelegate = nil
}
private static let html = """
<!doctype html>
<html><head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>html,body,#postcode{margin:0;width:100%;height:100%;}</style>
<script src="https://t1.kakaocdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"
onerror="document.documentElement.dataset.postcodeError='true'"></script>
</head><body><div id="postcode"></div>
<script>
try {
new kakao.Postcode({
width: '100%', height: '100%',
oncomplete: function(data) {
window.webkit.messageHandlers.postcodeSelection.postMessage({ zonecode: data.zonecode, address: data.address });
}
}).embed(document.getElementById('postcode'));
} catch (error) {
document.documentElement.dataset.postcodeError = 'true';
}
</script></body></html>
"""
final class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
private let onSelect: (KakaoPostcodeResult) -> Void
private let onLoad: (Bool) -> Void
fileprivate var isActive = true
private var hasFailed = false
private var hasSelected = false
init(onSelect: @escaping (KakaoPostcodeResult) -> Void, onLoad: @escaping (Bool) -> Void) {
self.onSelect = onSelect
self.onLoad = onLoad
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard isActive, !hasFailed, !hasSelected,
message.name == KakaoPostcodeWebView.handlerName,
message.frameInfo.isMainFrame,
let result = KakaoPostcodeResult(payload: message.body) else { return }
hasSelected = true
onSelect(result)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.dataset.postcodeError === 'true'") { [weak self] value, error in
guard let self, self.isActive, !self.hasFailed else { return }
if error != nil || value as? Bool != false {
self.fail()
} else {
self.onLoad(false)
}
}
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
fail()
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
fail()
}
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
fail()
}
private func fail() {
guard isActive else { return }
hasFailed = true
onLoad(true)
}
}
}