# iOS

<figure><img src="/files/U6sDn6w4dwsmJjHU9976" alt=""><figcaption></figcaption></figure>

***

iOS 가이드 문서는 Swift UIKit 기준으로 작성되었습니다.

* 사용언어 : <mark style="color:blue;">`Swift UI`</mark>
  * <mark style="color:blue;">`Objective-C`</mark> 형태의 가이드는 제공되지 않습니다.
* 세로모드 고정
  * 게임은 세로모드 해상도에 최적화 되어 세로모드 잠금 기능을 추가해야 합니다.

```swift
// blp_sample_app_iosApp.swift
import SwiftUI

@main
struct blp_sample_app_iosApp: App {
    // 앱 세로모드 잠금 처리
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return .portrait
    }
}
```

***

## 1단계: 최상위 ContentView 파일 생성

최상위 ContentView 파일 내 <mark style="color:blue;">`NavigationStack`</mark> 으로 화면 단위 View를 컨트롤 합니다.

홈화면을 <mark style="color:blue;">`HomeContentView`</mark> 로 선언 하였으며, <mark style="color:blue;">`navigationDestination`</mark> 메서드를 통해 런처를 실행할 <mark style="color:blue;">`LauncherContentView`</mark> 로 이동할 케이스를 선언합니다.

```swift
// ContentView.swift
import SwiftUI

// 최상위 View
struct ContentView: View {
    @StateObject private var navigationModel = NavigationModel()
    
    var body: some View {
        NavigationStack(path: $navigationModel.path) {
            HomeContentView(navigationModel: navigationModel)
                .navigationDestination(for: NavigationDestination.self) { destination in
                    switch destination {
                    case .launcher:
                        // 런처 화면
                        LauncherContentView(navigationModel: navigationModel)
                    }
                }
        }
    }
}

#Preview {
    ContentView()
}
```

Navigation 전환을 위해 <mark style="color:blue;">`NavigationModel`</mark> 클래스 파일을 선언합니다.

```swift
// NavigationModel.swift
import Foundation

// 화면 전환 case들 (ContentView)
enum NavigationDestination: String, Identifiable {
    case launcher
    
    var id: String { rawValue }
}

class NavigationModel: ObservableObject {
    @Published var path: [NavigationDestination] = []
}
```

***

## 2단계: 런처 WebView 파일 생성

런처 호출 URL을 로드할 <mark style="color:blue;">`WebView`</mark> 구조체가 필요합니다.

* <mark style="color:blue;">`Coordinator`</mark> 클래스 내 <mark style="color:blue;">`userContentController`</mark> 에서 script 메세지를 핸들링 하는 로직을 추가합니다.

```swift
// LauncherWebView.swift
import WebKit
import SwiftUI

// UIViewRepresentable를 사용하여 WKWebView를 wrapping해서 사용
struct LauncherWebView: UIViewRepresentable {
    let url: URL
    
    // 뒤로가기 처리
    let onCloseLauncher: () -> Void

    class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
        var parent: LauncherWebView
        
        init(parent: LauncherWebView) {
            self.parent = parent
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            // WebView finished loading
        }
        
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            // 런처 뒤로가기 이벤트 수신 시 처리
            if message.name == "closeLauncher" {
                parent.onCloseLauncher()
            }
            
            // 런처 로드 완료
            if message.name == "launcherLoaded" {
                parent.onLauncherLoaded()
            }
            
            // 타이머 미션 완료
            if message.name == "timerMissionComplete" {
                parent.onTimerMissionComplete()
            }
            
            // 육성완료
            if message.name == "giftReceived" {
                parent.onGiftReceived()
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()

        let contentController = webView.configuration.userContentController
        // javascript에서 전달하는 이벤트 등록
        contentController.add(context.coordinator, name: "closeLauncher")
        contentController.add(context.coordinator, name: "launcherLoaded")
        contentController.add(context.coordinator, name: "timerMissionComplete")
        contentController.add(context.coordinator, name: "giftReceived")
        
        webView.navigationDelegate = context.coordinator
        let request = URLRequest(url: url, cachePolicy:NSURLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10.0)
        webView.load(request)
        
        return webView
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        //
    }

    static func dismantleUIView(_ uiView: WKWebView, coordinator: Self.Coordinator) {
        uiView.configuration.userContentController.removeScriptMessageHandler(forName: "closeLauncher")
        uiView.configuration.userContentController.removeScriptMessageHandler(forName: "launcherLoaded")
        uiView.configuration.userContentController.removeScriptMessageHandler(forName: "timerMissionComplete")
        uiView.configuration.userContentController.removeScriptMessageHandler(forName: "giftReceived")
    }
}
```

***

## 3단계: 런처 ContentView 파일 생성

런처를 실행할 <mark style="color:blue;">`LauncherContentView`</mark> 파일을 생성합니다.

* <mark style="color:blue;">`LauncherContentView`</mark> 구조체를 선언합니다.
* body 변수에는 런처가 로드될 <mark style="color:blue;">`WebView`</mark> 를 선언합니다.
  * 2단계에서 선언된 <mark style="color:blue;">`LauncherWebView`</mark> 구조체를 사용합니다.
  * <mark style="color:blue;">`런처 호출 URL`</mark> 을 로드합니다.
* 런처 이벤트 처리를 위한 핸들러를 구현합니다.

```swift
// LauncherContentView.swift
import SwiftUI
import WebKit

struct LauncherContentView: View {
    // navigation model observer
    @ObservedObject var navigationModel: NavigationModel
    
    var body: some View {
        LauncherWebView(
            url: URL(string: "런처 URL 정보"),
            onCloseLauncher: {
                // 런처 뒤로가기 처리
                handleCloseLauncher()
            },
            onLauncherLoaded: {
                // 런처 로드 완료
                handleLauncherLoaded()
            },
            onTimerMissionComplete: {
                // 타이머 미션 완료
                handleTimerMissionComplete()
            },
            onGiftReceived: {
                // 육성완료
                handleGiftReceived()
            },
        )
        .navigationBarBackButtonHidden(true)
    }
    
    // 뒤로가기 처리
    private func handleCloseLauncher() {
        navigationModel.path.removeLast()
    }
    
    // 런처 로드 완료
    private func handleLauncherLoaded() {
        // 런처의 로드가 완료된 후 추가적인 처리 필요 시 사용
    }
    
    // 타이머 미션 완료
    private func handleTimerMissionComplete() {
        // 타이머 미션 완료 후 추가적인 처리 필요 시 사용
    }
    
    // 육성완료
    private func handleGiftReceived() {
        // 육성완료 후 추가적인 처리 필요 시 사용
    }
}

#Preview {
    LauncherContentView(navigationModel: NavigationModel())
}
```

***

## 4단계: 화면 전환 설정

### 1. WebView 구조체에서 Script Message 핸들링 추가

* <mark style="color:blue;">`WKWebView`</mark> 를 사용하며 <mark style="color:blue;">`userContentController`</mark> 에서 javascript를 전달 받는 이벤트를 등록합니다.
* 런처에서 딥링크 호출 시 <mark style="color:blue;">`makeUIView`</mark> 함수 내에서 추가로 스크립트를 주입합니다.
* 해당 가이드에서는 <mark style="color:blue;">`deepLinkHandler`</mark> 라는 이름으로 메시지 핸들러 네이밍을 정의하였습니다.
* <mark style="color:blue;">`Coordinator`</mark> 클래스 내 <mark style="color:blue;">`userContentController`</mark> 함수에서 부모의 <mark style="color:blue;">`onDeepLink`</mark> 콜백을 호출합니다.

```swift
// LauncherWebView.swift
import WebKit
import SwiftUI

struct LauncherWebView: UIViewRepresentable {
    ...
    
    // Deep Link 처리할 콜백 함수
    let onDeepLink: (URL) -> Void

    class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
        ...
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            // deepLinkHandler 케이스 추가
            if message.name == "deepLinkHandler", let urlString = message.body as? String, let url = URL(string: urlString) {
                parent.onDeepLink(url)
            }
        }
        ...
    }
    
    ...

    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()

        let contentController = webView.configuration.userContentController
        
        // javascript에서 전달하는 이벤트 등록
        ...
        contentController.add(context.coordinator, name: "deepLinkHandler")
        
        // script 주입
        let script = """
            window.open = function(url) {
                window.webkit.messageHandlers.deepLinkHandler.postMessage(url);
            };
        """
        let scriptToInject = WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
        contentController.addUserScript(scriptToInject)
        
        webView.navigationDelegate = context.coordinator
        let request = URLRequest(url: url)
        webView.load(request)
        
        return webView
    }

    ...

    static func dismantleUIView(_ uiView: WKWebView, coordinator: Self.Coordinator) {
        ...
        uiView.configuration.userContentController.removeScriptMessageHandler(forName: "deepLinkHandler")
    }
}
```

### 2. WebView가 포함된 ContentView 내에서 화면 이동 처리 추가

WebView 내에서 딥링크 이벤트가 발생하면 상위 ContentView에서 해당 이벤트를 전달 받아 네비게이션 이동을 수행합니다.

<mark style="color:blue;">`queryParameters`</mark> 로 넘어오는 데이터는 화면 전환 시 <mark style="color:blue;">`NavigationModel`</mark> 에 추가로 정의하여 전달합니다.

```swift
// LauncherContentView.swift
import SwiftUI
import WebKit

struct LauncherContentView: View {
    ...
    var body: some View {
        LauncherWebView(
            url: URL(string: "런처 URL"),
            // Deep Link 이벤트 핸들링 처리
            onDeepLink: { url in
                handleDeepLink(url)
            }
        )
        .navigationBarBackButtonHidden(true)
    }
    ...
    
    // Deep Link 처리
    private func handleDeepLink(_ url: URL) {
        // Deep Link URL 내 포함된 스키마, 호스트, 파라미터 데이터로 네비게이션 이동 처리 추가
        if url.scheme == "{스키마}"{
            if url.host == "{호스트명}" {
                if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems,
                   let exampleValue = queryItems.first(where: { $0.name == "{파라미터명}" })?.value {
                    navigationModel.exampleValue = exampleValue
                    navigationModel.path.append(.{호스트명})
                }
            }
        }
    }
}
...
```

{% hint style="info" %}
개발에 대한 추가 설명이 더 필요하신가요?

"[<mark style="color:orange;">\[Client Admin\]</mark>](https://client-admin.bleepy.io/login) 로그인 → 오른쪽 하단 채널톡 위젯" 클릭 후 개발 카테고리에 문의 남겨주시면 기술 개발팀에서 확인 후 연락드리겠습니다.
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://bleepy.gitbook.io/bleepy-developers/game-promotion/ios.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
