iOS
iOS 앱의 WKWebView 브릿지 메시지를 기록해 데스크톱 앱에서 확인합니다
Dari Swift 패키지는 WKWebView 브릿지 메시지를 기록해, Android에서 쓰는 데스크톱 앱으로 보냅니다. 목록, 상세, 필터, 마스킹, 검색, 내보내기가 Android와 똑같이 동작합니다. iOS에는 앱 안의 인스펙터 화면이 없습니다.
iOS 연결은 macOS 전용입니다. iOS Simulator 앱과, Mac에 USB로 연결한 iPhone·iPad 앱이 연결됩니다. iOS 디버그 빌드를 만들고 설치하는 데 이미 Mac이 필요하므로, Windows·Linux의 데스크톱 앱에는 Android 기기만 나타납니다. Wi-Fi 연결은 제공하지 않습니다.
요구 사항
- iOS 15 이상
- Mac에 설치한 Dari 데스크톱 앱. iOS 연결은 macOS에서만 됩니다. Simulator는 Mac에서만 돌고, USB 기기는 macOS의 usbmuxd를 거쳐 연결하기 때문입니다.
- 실기기라면: USB 케이블, 기기 잠금을 한 번 풀고 이 컴퓨터를 신뢰 수락, Xcode로 설치한 앱
- adb는 없어도 됩니다. adb가 없으면 데스크톱 앱에 iOS 앱만 표시됩니다.
설치
- Xcode에서 **File → Add Package Dependencies…**를 고르고
https://github.com/easyhooon/dari를 입력합니다. 3.0.0부터 Up to Next Major Version을 고릅니다.- 앱 타깃에 Dari 라이브러리를 추가합니다.
Package.swift에서는 다음과 같이 추가합니다.
.package(url: "https://github.com/easyhooon/dari", from: "3.0.0")
// 타깃 의존성
.product(name: "Dari", package: "dari")호환성
- 3.0.0부터 iOS SDK와 Android 라이브러리는 같은 버전 번호를 씁니다. iOS를 쓰려면 Dari 데스크톱 3.0.0 이상이 필요합니다.
- 와이어 프로토콜은 버전 1 그대로입니다. 데스크톱 3.x는 Dari 2.x Android 앱에 그대로 연결되고, 데스크톱 2.x도 3.x Android 앱과 계속 동작합니다.
- Android 앱은 3.0.0으로 올릴 때 바꿀 것이 없습니다. 핸드셰이크에 선택 필드
platform,osVersion이 추가되며, 이전 버전은 이 필드를 무시합니다.
Dari 시작
앱 실행 초기에 start()를 한 번 호출합니다. 검사할 빌드에서만 호출합니다.
import Dari
@main
struct MyApp: App {
init() {
#if DEBUG
try? Dari.shared.start()
Dari.shared.setEnvironment(["server": "dev"]) // 데스크톱 앱에 표시할 라벨(선택)
#endif
}
// ...
}Dari가 실행 중일 때는 기기의 loopback에 접근할 수 있는 프로세스라면 무엇이든 기록된 메시지를 읽을 수 있습니다. App Store 빌드에서는 절대 start()를 호출하지 마세요.
start()에는 DariConfiguration을 넘길 수 있습니다.
| 옵션 | 기본값 | 의미 |
|---|---|---|
maxEntries | 500 | 메모리에 보관할 메시지 수. 가장 오래된 것부터 지웁니다 |
maxContentLength | 500,000 | 이보다 긴 payload는 Android와 같은 방식으로 자릅니다 |
fireAndForget | false | 요청을 보내는 즉시 완료로 기록합니다 |
ports | 38670...38677 | 시도할 loopback 포트. 데스크톱 앱도 같은 범위를 확인합니다 |
브릿지 호출 기록
WKWebView마다 interceptor를 하나씩 만들고, 브릿지가 메시지를 받고 응답하는 곳에서 호출합니다. 메서드 구성은 Android의 DariInterceptor와 같습니다.
final class Bridge: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
private let dari = Dari.shared.interceptor(tag: "main")
weak var webView: WKWebView?
// 이후에 기록하는 요청에 이 페이지 URL이 붙습니다. query와 fragment는 빠집니다.
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
dari.onPageUrlChanged(webView.url?.absoluteString)
}
// Web → App
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
let body = message.body as? [String: Any] ?? [:]
let handler = body["handler"] as? String ?? message.name
let requestId = body["requestId"] as? String
dari.onWebToAppRequest(handlerName: handler, requestId: requestId, requestData: body["data"] as? String)
let result = #"{"ok":true}"#
dari.onWebToAppResponse(handlerName: handler, requestId: requestId, responseData: result, isSuccess: true)
// ...결과를 페이지로 전달
}
// App → Web
func notifyPage(_ requestId: String, _ payload: String) {
dari.onAppToWebRequest(handlerName: "onResume", requestId: requestId, data: payload)
webView?.evaluateJavaScript("onResume(\(payload))") { value, error in
self.dari.onAppToWebResponse(requestId: requestId, isSuccess: error == nil, responseData: value.map { "\($0)" })
}
}
}- 요청과 응답은
requestId로 짝을 맞춥니다. 응답은 ID, tag, 방향이 같은 요청 가운데 아직 열려 있는 가장 최근 요청을 완료합니다. requestId가 nil이거나fireAndForget: true를 넘기면, 보내는 즉시 완료된 메시지로 기록합니다.- 핸들러 이름만으로 알아보기 어려우면
displayName:을 넘깁니다. start()전에 한 호출은 무시됩니다.
Android의 Gradle 플러그인처럼 이 호출 없이 자동으로 수집하는 기능은 아직 없습니다. 가능한 방식은 이슈 코멘트에 비교해 두었습니다.
연결
- Simulator나, USB로 연결한 iPhone·iPad에서 앱을 실행합니다.
- 같은 Mac에서 Dari 데스크톱 앱을 엽니다. Simulator 앱은 iOS Simulator 기기 아래에, USB 기기는 그 기기 이름 아래에 나타납니다. 그곳의 Dari 앱이 하나뿐이면 자동으로 연결합니다.
앱마다 127.0.0.1의 38670...38677 중 비어 있는 첫 포트에서 listen하므로, 기기 하나에서 Dari 앱을 최대 8개까지 동시에 실행할 수 있습니다. 앱을 다시 실행해 다른 포트에 자리 잡아도 데스크톱 앱이 다시 찾아 연결합니다.
연결 동작 원리
앱은 네트워크에 포트를 열지 않습니다. 자기 자신의 loopback 주소인 127.0.0.1에서만 listen합니다. 데스크톱 앱은 두 가지 방법 중 하나로 그 주소에 닿습니다.
Simulator. Simulator 앱은 Mac 위의 프로세스로 실행되므로 Mac과 loopback을 같이 씁니다. 데스크톱 앱이 127.0.0.1:38670부터 차례로 바로 접속합니다.
USB 기기. iPhone의 loopback은 iPhone 안에 있어서 Mac이 네트워크로는 닿을 수 없습니다. 대신 macOS에는 Finder와 Xcode가 케이블로 기기와 통신할 때 쓰는 usbmuxd 데몬이 있습니다. Mac의 프로그램이 usbmuxd에 "기기의 이 포트로 연결해 달라"고 요청하면, usbmuxd가 USB 연결을 그 포트로 가는 바이트 통로로 만들어 줍니다. 이것을 포트 터널링이라고 합니다. Android의 adb forward가 하는 일을 iOS에서 하는 셈입니다.
- 데스크톱 앱이 usbmuxd(
/var/run/usbmuxd)에 연결된 기기 목록을 묻습니다. 모든 iOS 기기가 포트 62078에서 실행하는 lockdownd 서비스에서 기기 이름도 읽어 옵니다. 38670...38677의 각 포트마다 usbmuxd에 기기의 그 포트로 연결해 달라고 요청합니다. 아무것도 listen하지 않는 포트는 usbmuxd가 바로 거절합니다.- 연결된 포트에 Dari handshake를 보냅니다. Dari 앱이면 bundle ID로 답하고, 목록에 나타납니다.
- 사용자가 고른 연결은 같은 터널을 계속 씁니다. 케이블을 뽑으면 끊기고, 기기를 다시 꽂으면 데스크톱 앱이 다시 연결합니다.
Wi-Fi, IP 주소, 추가 도구는 쓰지 않습니다. 같은 Wi-Fi에 있어도 케이블로 연결하지 않은 기기는 나타나지 않습니다.
Staging 빌드
Android는 manifest 메타데이터로 opt-in해서, minify가 켜지고 debuggable이 아닌 빌드(staging)도 검사합니다. iOS에는 R8 같은 코드 이름 변경(난독화)이 없습니다. Release 빌드는 최적화(-O, whole-module)와 dead code stripping을 거치지만 Dari에는 영향이 없습니다. Dari는 리플렉션이나 런타임 클래스 조회를 쓰지 않기 때문입니다.
Release와 비슷한 빌드를 검사하려면 빌드 구성(예: Staging)을 추가하고, 그 구성 전용 컴파일 조건으로 Dari를 시작합니다.
- Staging 구성의 Active Compilation Conditions(
SWIFT_ACTIVE_COMPILATION_CONDITIONS)에DARI를 추가합니다. #if DEBUG || DARI로 Dari를 시작합니다.
샘플 앱에 Debug, Staging, Release 구성이 이렇게 설정되어 있습니다.
Swift Package Manager는 패키지를 일부 구성에만 링크할 수 없습니다. 그래서 앱이 start()를 호출하지 않아도 Dari 코드(arm64 기준 약 170KB)는 Release 바이너리에 남습니다. 이 코드는 동작하지 않습니다. 소켓을 열지 않고 아무것도 기록하지 않습니다. 용량이 중요하다면 별도의 디버그 타깃을 두는 식으로 App Store 타깃에서 Dari를 아예 빼세요.
샘플 앱
sample-ios/DariSample.xcodeproj는 Web → App과 App → Web 시나리오가 담긴 페이지를 엽니다. 성공, 에러, 느린 응답, 응답 없음, fire-and-forget, 마스킹을 확인할 수 있습니다. Xcode 16 이상에서 열고 Simulator에서 DariSample scheme을 실행하세요.
문제 해결
- 앱이 목록에 없습니다. 앱이 포그라운드에 있어야 합니다. 일시 정지된 Simulator 앱은 응답하지 않습니다.
start()가 실행됐는지, 포트가 열려 있는지 확인합니다:lsof -nP -iTCP:38670-38677 -sTCP:LISTEN - Simulator 두 대에서 같은 앱을 실행했습니다. 데스크톱 앱은 먼저 찾은 쪽에 연결합니다. 하나를 종료하세요.
- iPhone이 목록에 없습니다. 잠금을 풀고 이 컴퓨터를 신뢰를 수락한 뒤 Finder에 기기가 보이는지 확인합니다. 기기는 케이블로 연결했을 때만 나타납니다.
- iPhone은 보이는데 앱이 없습니다. 앱을 포그라운드로 가져옵니다. iOS는 백그라운드 앱을 일시 정지시키고, 일시 정지된 앱은 응답하지 못합니다.
start()가noFreePort를 던집니다. Dari 앱 8개가 이미 포트를 쓰고 있습니다. 일부를 종료하거나DariConfiguration(ports:)로 다른 범위를 넘기세요. 단, 데스크톱 앱은 기본 범위만 확인합니다.