# iOS

<figure><img src="https://169964493-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FwMb5WkedgvZ9Yk3Yxqep%2Fuploads%2FoCyaDTPB6oJRvlgbwXqa%2FFlutter.png?alt=media&#x26;token=d20e6582-bf40-40cc-96a5-06c601be77b4" 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 %}
