์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸชŸ iOS ์•ฑ ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ ์ง€์› ๊ตฌํ˜„ ์™„์ „์ •๋ณต โ€” ์•„์ดํŒจ๋“œ์—์„œ ์ฐฝ ๋‘ ๊ฐœ ๋„์šฐ๋Š” ๊ทธ๋‚ ๊นŒ์ง€
๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ/์•ฑ ยท ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ

๐ŸชŸ iOS ์•ฑ ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ ์ง€์› ๊ตฌํ˜„ ์™„์ „์ •๋ณต โ€” ์•„์ดํŒจ๋“œ์—์„œ ์ฐฝ ๋‘ ๊ฐœ ๋„์šฐ๋Š” ๊ทธ๋‚ ๊นŒ์ง€

UIScene๋ถ€ํ„ฐ SwiftUI WindowGroup, Stage Manager๊นŒ์ง€
์นœ๊ตฌ๊ฐ€ ์˜†์—์„œ ์•Œ๋ ค์ฃผ๋“ฏ ํ’€์–ด์ฃผ๋Š” ์‹ค์ „ ๊ฐ€์ด๋“œ

์ž, ์ปคํ”ผ ํ•œ ์ž” ์ฑ™๊ฒจ์™”์–ด? ๐Ÿ˜„

์˜ค๋Š˜ ์–˜๊ธฐํ•  ์ฃผ์ œ๋Š” iOS/iPadOS์˜ ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ(Multiple Windows)์•ผ.
์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด ๋‚ด ์•ฑ์„ ํ•œ ํ™”๋ฉด์— ๋‘ ๊ฐœ, ์„ธ ๊ฐœ ๋„์šฐ๋Š” ๊ธฐ๋Šฅ์ด์ง€.

๋ฉ”๋ชจ ์•ฑ์—์„œ ์™ผ์ชฝ์—” ํšŒ์˜๋ก, ์˜ค๋ฅธ์ชฝ์—” ํ•  ์ผ ๋ชฉ๋ก.
์‚ฌํŒŒ๋ฆฌ์—์„œ ์ฐฝ ๋‘ ๊ฐœ ๋„์›Œ๋†“๊ณ  ๋น„๊ตํ•˜๋Š” ๊ทธ๊ฑฐ.
"๋‚˜๋„ ๋‚ด ์•ฑ์— ์ €๊ฑฐ ๋„ฃ๊ณ  ์‹ถ๋‹ค!" ์‹ถ์—ˆ๋˜ ์  ์žˆ์ง€?

๊ทผ๋ฐ ๋ง‰์ƒ ์‹œ์ž‘ํ•˜๋ฉด AppDelegate? SceneDelegate? UISceneSession? NSUserActivity? ํ•˜๋ฉด์„œ ๋ฉ˜๋ถ•์ด ์˜ค๊ฑฐ๋“ .
๊ทธ๋ž˜์„œ ์˜ค๋Š˜ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€, ์ˆœ์„œ๋Œ€๋กœ ์ •๋ฆฌํ•ด์ค„๊ฒŒ.

๐ŸŽฌ 1. ์™œ ๊ฐ‘์ž๊ธฐ Scene์ด๋ผ๋Š” ๊ฒŒ ์ƒ๊ฒผ์„๊นŒ?

์˜›๋‚  iOS ์•ฑ์€ ๊ตฌ์กฐ๊ฐ€ ๋‹จ์ˆœํ–ˆ์–ด.
์•ฑ ํ•˜๋‚˜ = ํ™”๋ฉด ํ•˜๋‚˜ = UIWindow ํ•˜๋‚˜. ๋.

๊ทธ๋ž˜์„œ AppDelegate๊ฐ€ ๋ชจ๋“  ๊ฑธ ๋‹ค ํ–ˆ์ง€.
์•ฑ์ด ์ผœ์ง€๋ฉด application(_:didFinishLaunchingWithOptions:), ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๊ฐ€๋ฉด applicationDidEnterBackground(_:).
์•ฑ ์ƒ๋ช…์ฃผ๊ธฐ = UI ์ƒ๋ช…์ฃผ๊ธฐ์˜€๋˜ ๊ฑฐ์•ผ.

๊ทธ๋Ÿฐ๋ฐ iOS 13 / iPadOS 13 (2019๋…„)์—์„œ ์• ํ”Œ์ด ์‚ฌ๊ณ ๋ฅผ ์ณค์–ด.
"์ด์ œ ์•ฑ ํ•˜๋‚˜๊ฐ€ ์ฐฝ์„ ์—ฌ๋Ÿฌ ๊ฐœ ๋„์šธ ์ˆ˜ ์žˆ๋‹ค!" ๋ผ๊ณ  ์„ ์–ธํ•œ ๊ฑฐ์ง€.

๐Ÿ˜ต ๋ฌธ์ œ ๋ฐœ์ƒ: ์ฐฝ์ด 3๊ฐœ์ธ๋ฐ ๊ทธ์ค‘ 1๊ฐœ๋งŒ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๊ฐ”์–ด.
๊ทธ๋Ÿผ applicationDidEnterBackground๋ฅผ ๋ถˆ๋Ÿฌ์•ผ ํ•ด, ๋ง์•„์•ผ ํ•ด?

โ†’ ๋‹ต: ๋ชป ๋ถˆ๋Ÿฌ. ์•ฑ ์ƒ๋ช…์ฃผ๊ธฐ๋ž‘ UI ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๋ถ„๋ฆฌํ•ด์•ผ๋งŒ ํ–ˆ์ง€.

๊ทธ๋ž˜์„œ ๋“ฑ์žฅํ•œ ๊ฒŒ UIScene์ด์•ผ.

๐Ÿ“Œ ๊ฐœ๋… ์ •๋ฆฌ

UIApplication โ†’ ์•ฑ ํ”„๋กœ์„ธ์Šค ์ „์ฒด (๋ฐ์ดํ„ฐ, ๋„คํŠธ์›Œํฌ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…)

UIScene(UIWindowScene) โ†’ UI ์ธ์Šคํ„ด์Šค ํ•˜๋‚˜ = ์‚ฌ์šฉ์ž ๋ˆˆ์— ๋ณด์ด๋Š” "์ฐฝ" ํ•˜๋‚˜

UIWindow โ†’ Scene ์•ˆ์— ๋“ค์–ด๊ฐ€๋Š” ์‹ค์ œ ๋ทฐ ์ปจํ…Œ์ด๋„ˆ

UISceneSession โ†’ Scene์˜ "์˜์ˆ˜์ฆ". ์ฐฝ์˜ ์ƒํƒœยท์„ค์ •์„ ๋ณด๊ด€ํ•˜๋Š” ์ง€์† ๊ฐ์ฒด

์ฆ‰ ๊ตฌ์กฐ๊ฐ€ ์ด๋ ‡๊ฒŒ ๋ฐ”๋€ ๊ฑฐ์•ผ.

[ UIApplication ]  โ† ์•ฑ์€ ํ•˜๋‚˜
      โ”‚
      โ”œโ”€โ”€ UISceneSession A โ”€โ”€ UIWindowScene A โ”€โ”€ UIWindow โ”€โ”€ RootVC
      โ”œโ”€โ”€ UISceneSession B โ”€โ”€ UIWindowScene B โ”€โ”€ UIWindow โ”€โ”€ RootVC
      โ””โ”€โ”€ UISceneSession C โ”€โ”€ UIWindowScene C โ”€โ”€ UIWindow โ”€โ”€ RootVC

์—ฌ๊ธฐ์„œ ์ง„์งœ ์ค‘์š”ํ•œ ํฌ์ธํŠธ ํ•˜๋‚˜!
Scene์ด ์—ฌ๋Ÿฌ ๊ฐœ์—ฌ๋„ ํ”„๋กœ์„ธ์Šค๋Š” ํ•˜๋‚˜์•ผ.
๋ฉ”๋ชจ๋ฆฌ๋„ ๊ณต์œ ํ•˜๊ณ , ์‹ฑ๊ธ€ํ†ค๋„ ๊ณต์œ ํ•ด.
์ด๊ฑฐ ๋ชจ๋ฅด๊ณ  static var currentUser ๊ฐ™์€ ๊ฑธ๋กœ ํ™”๋ฉด ์ƒํƒœ ๊ด€๋ฆฌํ•˜๋ฉดโ€ฆ ์ฐฝ ๋‘ ๊ฐœ ๋„์šฐ๋Š” ์ˆœ๊ฐ„ ์ง€์˜ฅ๋ฌธ์ด ์—ด๋ ค. ๐Ÿ˜ˆ

๐Ÿ—บ๏ธ 2. ์ „์ฒด ์•„ํ‚คํ…์ฒ˜ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ

iOS Multi-Window Architecture UIApplication ํ”„๋กœ์„ธ์Šค 1๊ฐœ ยท ๋ฐ์ดํ„ฐ ยท ๋ฐฑ๊ทธ๋ผ์šด๋“œ Scene A UISceneSession UIWindowScene UIWindow ๋ฌธ์„œ ๋ชฉ๋ก ํ™”๋ฉด Scene B UISceneSession UIWindowScene UIWindow ๋ฌธ์„œ ์ƒ์„ธ ํŽธ์ง‘ Scene C UISceneSession UIWindowScene UIWindow ์„ค์ • ์ฐฝ ๊ณต์œ  ๋ ˆ์ด์–ด : CoreData / Repository / Network / ํŒŒ์ผ์‹œ์Šคํ…œ

๊ทธ๋ฆผ ๋ณด๋ฉด ๊ฐ ์˜ค์ง€?
์œ„๋Š” ํ•˜๋‚˜, ๊ฐ€์šด๋ฐ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ, ์•„๋ž˜๋Š” ๋‹ค์‹œ ํ•˜๋‚˜.
์ด ๊ตฌ์กฐ๋ฅผ ๋จธ๋ฆฌ์— ๋ฐ•์•„๋‘๋ฉด ๋‚˜๋จธ์ง€๋Š” ์ „๋ถ€ ๋””ํ…Œ์ผ์ด์•ผ.

โš™๏ธ 3. ์ฒซ ๋‹จ์ถ” โ€” Info.plist์™€ ํ”„๋กœ์ ํŠธ ์„ค์ •

๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ๋Š” ์ฝ”๋“œ๋ณด๋‹ค ์„ค์ •์ด ๋จผ์ €์•ผ.
Xcode์—์„œ ํƒ€๊ฒŸ โ†’ General โ†’ "Supports multiple windows" ์ฒดํฌ๋ฐ•์Šค ํ•˜๋‚˜๋ฉด ๋๋‚˜๊ธด ํ•˜๋Š”๋ฐ,
์‹ค์ œ๋กœ Info.plist์— ๋ญ๊ฐ€ ๋“ค์–ด๊ฐ€๋Š”์ง€๋Š” ์•Œ์•„๋‘ฌ์•ผ ํ•ด.

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <true/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
        <key>UISceneStoryboardFile</key>
        <string>Main</string>
      </dict>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Editor Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).EditorSceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

๐Ÿ’ก ํฌ์ธํŠธ 3๊ฐ€์ง€

โ‘  UIApplicationSupportsMultipleScenes๊ฐ€ true์—ฌ์•ผ ์ฐฝ์„ ์—ฌ๋Ÿฌ ๊ฐœ ๋„์šธ ์ˆ˜ ์žˆ์–ด. false๋ฉด Scene ๊ตฌ์กฐ๋Š” ์“ฐ๋˜ ์ฐฝ์€ ํ•˜๋‚˜๋งŒ.

โ‘ก Configuration์„ ์—ฌ๋Ÿฌ ๊ฐœ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด. "๋ฉ”์ธ ์ฐฝ"๊ณผ "ํŽธ์ง‘ ์ „์šฉ ์ฐฝ"์˜ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ ์•„์˜ˆ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ด์•ผ.

โ‘ข $(PRODUCT_MODULE_NAME) ์ ‘๋‘์‚ฌ ๋นผ๋จน์œผ๋ฉด Swift์—์„œ ํด๋ž˜์Šค๋ฅผ ๋ชป ์ฐพ์•„์„œ ๊ฒ€์€ ํ™”๋ฉด์ด ๋œฌ๋‹ค. ์ด๊ฑฐ ์ง„์งœ ์ž์ฃผ ๊ฒช๋Š” ํ•จ์ •. ๐Ÿ˜…

์ฐธ๊ณ ๋กœ ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ๋Š” iPadOS์™€ macOS(Catalyst/Designed for iPad)์—์„œ๋งŒ ์‹ค์ œ๋กœ ๋™์ž‘ํ•ด.
์•„์ดํฐ์€ Scene ๊ตฌ์กฐ๋Š” ์“ฐ์ง€๋งŒ ์ฐฝ์€ ํ•ญ์ƒ 1๊ฐœ์•ผ. ์ด๊ฑฐ ํ—ท๊ฐˆ๋ฆฌ์ง€ ๋งˆ!

๐Ÿงฉ 4. AppDelegate์™€ SceneDelegate ์—ญํ•  ๋‚˜๋ˆ„๊ธฐ

AppDelegate๋Š” "ํšŒ์‚ฌ ์ „์ฒด", SceneDelegate๋Š” "๊ฐ ์ง€์ "

์ด๋ ‡๊ฒŒ ๋น„์œ ํ•˜๋ฉด ๋”ฑ ์ดํ•ด๋ผ.
ํšŒ์‚ฌ ์ „์ฒด ๋ฐฉ์นจ(๋กœ๊ทธ์ธ ์„ธ์…˜, DB ์ดˆ๊ธฐํ™”, ํ‘ธ์‹œ ๋“ฑ๋ก)์€ AppDelegate.
๊ฐ ์ง€์  ์ธํ…Œ๋ฆฌ์–ด(์œˆ๋„์šฐ ์ƒ์„ฑ, ํ™”๋ฉด ๋ณต์›)๋Š” SceneDelegate.

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        DatabaseStack.shared.setup()   // ์ „์—ญ 1ํšŒ ์ดˆ๊ธฐํ™”
        return true
    }

    // ์ƒˆ Scene์ด ํ•„์š”ํ•  ๋•Œ ์–ด๋–ค ์„ค์ •์„ ์“ธ์ง€ ๊ฒฐ์ •
    func application(_ application: UIApplication,
                     configurationForConnecting session: UISceneSession,
                     options: UIScene.ConnectionOptions) -> UISceneConfiguration {

        if session.role == .windowApplication,
           options.userActivities.first?.activityType == DocumentActivity.type {
            return UISceneConfiguration(name: "Editor Configuration", sessionRole: session.role)
        }
        return UISceneConfiguration(name: "Default Configuration", sessionRole: session.role)
    }

    // ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ ์Šค์œ„์ฒ˜์—์„œ ์ฐฝ์„ ๋ฒ„๋ ธ์„ ๋•Œ โ†’ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ
    func application(_ application: UIApplication,
                     didDiscardSceneSessions sessions: Set<UISceneSession>) {
        sessions.forEach { DraftStore.shared.removeDraft(for: $0.persistentIdentifier) }
    }
}

๊ทธ๋ฆฌ๊ณ  SceneDelegate ์ชฝ.

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options: UIScene.ConnectionOptions) {

        guard let windowScene = scene as? UIWindowScene else { return }

        let window = UIWindow(windowScene: windowScene)

        // 1) ์ƒˆ ์ฐฝ ์š”์ฒญ์— ๋”ธ๋ ค์˜จ activity
        // 2) ์‹œ์Šคํ…œ์ด ๋ณต์›ํ•ด์ฃผ๋Š” stateRestorationActivity
        let activity = options.userActivities.first ?? session.stateRestorationActivity

        window.rootViewController = RootBuilder.make(from: activity)
        window.makeKeyAndVisible()
        self.window = window
    }

    // ์ฐฝ์ด ๋‹ซํžˆ๊ธฐ ์ง์ „, ์ƒํƒœ๋ฅผ activity์— ๋‹ด์•„ ์‹œ์Šคํ…œ์— ๋งก๊ธด๋‹ค
    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        (window?.rootViewController as? StateRestorable)?.currentActivity()
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        DraftStore.shared.flush()
    }
}

โš ๏ธ ์ดˆ๋ณด์ž ์ตœ๋‹ค ์‹ค์ˆ˜ TOP 3

1๏ธโƒฃ AppDelegate์— var window: UIWindow?๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘๊ณ  ๊ฑฐ๊ธฐ์— rootVC ๊ฝ‚๊ธฐ โ†’ Scene ํ™˜๊ฒฝ์—์„  ๋ฌด์‹œ๋จ.

2๏ธโƒฃ UIApplication.shared.keyWindow ์‚ฌ์šฉ โ†’ iOS 13๋ถ€ํ„ฐ deprecated. ์ฐฝ์ด ์—ฌ๋Ÿฌ ๊ฐœ๋ฉด "key"๊ฐ€ ๋ญ”์ง€ ์• ๋งคํ•˜๊ฑฐ๋“ .

3๏ธโƒฃ SceneDelegate ์•ˆ์— ๋ทฐ๋ชจ๋ธยท๋ฐ์ดํ„ฐ๋ฅผ ํ†ต์งธ๋กœ ๋“ค๊ณ  ์žˆ๊ธฐ โ†’ ์ฐฝ๋งˆ๋‹ค ์ค‘๋ณต ์ƒ์„ฑ๋ผ์„œ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ ๋ฐœ์ƒ.

keyWindow ๋Œ€์‹  ์ด๋ ‡๊ฒŒ ์“ฐ์ž

extension UIViewController {
    var currentWindowScene: UIWindowScene? {
        view.window?.windowScene
    }
}

// ์ „์—ญ์œผ๋กœ ์ฐพ์•„์•ผ ํ•  ๋•Œ (์ตœํ›„์˜ ์ˆ˜๋‹จ)
let activeScene = UIApplication.shared.connectedScenes
    .compactMap { $0 as? UIWindowScene }
    .first { $0.activationState == .foregroundActive }

๊ฐ€๋Šฅํ•˜๋ฉด "๋‚ด๊ฐ€ ์†ํ•œ ๋ทฐ์—์„œ window๋ฅผ ํƒ€๊ณ  ์˜ฌ๋ผ๊ฐ€๋Š” ๋ฐฉ์‹"์„ ์จ.
์ „์—ญ ๊ฒ€์ƒ‰์€ ์ฐฝ์ด ์—ฌ๋Ÿฌ ๊ฐœ์ผ ๋•Œ ์—‰๋šฑํ•œ ์ฐฝ์„ ์žก์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ .

๐Ÿšช 5. ์ง„์งœ ์ƒˆ ์ฐฝ ๋„์šฐ๊ธฐ โ€” requestSceneSessionActivation

์ด์ œ ๋ณธ๋ก ! ๋ฒ„ํŠผ ๋ˆŒ๋Ÿฌ์„œ ์ฐฝ ๋„์šฐ๋Š” ์ฝ”๋“œ.

func openDocumentInNewWindow(_ document: Document) {
    let activity = NSUserActivity(activityType: DocumentActivity.type)
    activity.title = document.title
    activity.userInfo = ["documentID": document.id.uuidString]
    activity.targetContentIdentifier = document.id.uuidString  // ์ค‘๋ณต ์ฐฝ ๋ฐฉ์ง€ ํ‚ค

    UIApplication.shared.requestSceneSessionActivation(
        nil,                 // nil = ์ƒˆ Scene ์ƒ์„ฑ
        userActivity: activity,
        options: nil,
        errorHandler: { error in
            print("์ฐฝ ์—ด๊ธฐ ์‹คํŒจ: \(error)")
        }
    )
}

๐Ÿ”‘ ํŒŒ๋ผ๋ฏธํ„ฐ ๋œฏ์–ด๋ณด๊ธฐ

์ฒซ ๋ฒˆ์งธ(session) โ€” nil์ด๋ฉด ์ƒˆ ์ฐฝ. ๊ธฐ์กด UISceneSession์„ ๋„˜๊ธฐ๋ฉด ๊ทธ ์ฐฝ์„ ์•ž์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ(ํ™œ์„ฑํ™”) ํ•ด.

userActivity โ€” ์ƒˆ ์ฐฝ์—๊ฒŒ ์ „๋‹ฌํ•  "ํƒ๋ฐฐ ์ƒ์ž". ์–ด๋–ค ํ™”๋ฉด์„ ์—ด์ง€ ์•Œ๋ ค์ฃผ๋Š” ์œ ์ผํ•œ ํ†ต๋กœ์•ผ.

options โ€” UIWindowScene.ActivationRequestOptions. ์–ด๋А ์ฐฝ์—์„œ ํŒŒ์ƒ๋๋Š”์ง€(requestingScene) ์•Œ๋ ค์ฃผ๋ฉด ๋ฐฐ์น˜๊ฐ€ ์ž์—ฐ์Šค๋Ÿฌ์›Œ์ ธ.

์ด๋ฏธ ์—ด๋ ค ์žˆ๋Š” ์ฐฝ์ด๋ฉด ์ƒˆ๋กœ ๋งŒ๋“ค์ง€ ๋ง์ž

๊ฐ™์€ ๋ฌธ์„œ๋ฅผ ๋‘ ๋ฒˆ ์—ด๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ํ—ท๊ฐˆ๋ ค.
๊ทธ๋ž˜์„œ ์ค‘๋ณต ์ฒดํฌ๋ฅผ ๋จผ์ € ํ•ด์ฃผ๋Š” ๊ฒŒ ์˜ˆ์˜์ง€. ๐Ÿ™‡

func focusOrOpen(documentID: String) {
    let existing = UIApplication.shared.openSessions.first { session in
        session.stateRestorationActivity?.userInfo?["documentID"] as? String == documentID
    }

    let activity = DocumentActivity.make(id: documentID)
    let options = UIWindowScene.ActivationRequestOptions()
    options.requestingScene = view.window?.windowScene

    UIApplication.shared.requestSceneSessionActivation(
        existing,            // ์žˆ์œผ๋ฉด ๊ทธ ์ฐฝ ํ™œ์„ฑํ™”, ์—†์œผ๋ฉด nil โ†’ ์ƒˆ ์ฐฝ
        userActivity: activity,
        options: options,
        errorHandler: nil
    )
}

์ฐฝ ๋‹ซ๊ธฐ

if let session = view.window?.windowScene?.session {
    UIApplication.shared.requestSceneSessionDestruction(
        session,
        options: nil,
        errorHandler: nil
    )
}

์ฐธ๊ณ ๋กœ requestSceneSessionDestruction์„ ์“ฐ๋ฉด
didDiscardSceneSessions๊ฐ€ ํ˜ธ์ถœ๋˜๋‹ˆ๊นŒ ์ž„์‹œ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ๋Š” ๊ฑฐ๊ธฐ์„œ ํ•˜๋ฉด ๋ผ.

๐ŸŽ’ 6. NSUserActivity โ€” ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ์˜ ์‹ฌ์žฅ

์—ฌ๊ธฐ๊ฐ€ ์ œ์ผ ์ค‘์š”ํ•ด. ์ง„์งœ์•ผ.

์ƒˆ ์ฐฝ์„ ์—ด ๋•Œ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ๋„˜๊ธธ ์ˆ˜๊ฐ€ ์—†์–ด.
์™œ๋ƒ๊ณ ? ์‹œ์Šคํ…œ์ด ์ฐฝ์„ ์–ธ์ œ๋“  ์ข…๋ฃŒํ–ˆ๋‹ค๊ฐ€ ๋‚˜์ค‘์— ๋ณต์›ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ .
๊ทธ๋•Œ ๋ฉ”๋ชจ๋ฆฌ์— ์žˆ๋˜ ๊ฐ์ฒด๋Š” ์ด๋ฏธ ์‚ฌ๋ผ์ง„ ๋’ค์•ผ.

๊ทธ๋ž˜์„œ ์• ํ”Œ์€ ์ด๋ ‡๊ฒŒ ์ •ํ–ˆ์–ด.
"์ฐฝ์˜ ์ƒํƒœ๋Š” NSUserActivity๋ผ๋Š” ์ง๋ ฌํ™” ๊ฐ€๋Šฅํ•œ ๋ด‰ํˆฌ์— ๋‹ด์•„๋ผ."

NSUserActivity ์™•๋ณต ์—ฌํ–‰ ๊ธฐ์กด ์ฐฝ ๋ฌธ์„œ ๋ชฉ๋ก์—์„œ "์ƒˆ ์ฐฝ์œผ๋กœ ์—ด๊ธฐ" NSUserActivity activityType userInfo: {id, scroll} ์ƒˆ Scene willConnectTo์—์„œ ํ™”๋ฉด ์žฌ๊ตฌ์„ฑ ์‹œ์Šคํ…œ ์ €์žฅ์†Œ ์•ฑ ์ข…๋ฃŒ๋ผ๋„ ์œ ์ง€ ์•ฑ ์Šค์œ„์ฒ˜ ์Šค๋ƒ…์ƒท ์ €์žฅ ๋ณต์› ํ•ต์‹ฌ: ๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ "๋ณต์› ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ"๋งŒ ๋‹ด์„ ๊ฒƒ

Activity๋ฅผ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ์‹ธ๊ธฐ

enum DocumentActivity {
    static let type = "com.jaenung.editor.document"

    static func make(id: String, scrollOffset: CGFloat = 0) -> NSUserActivity {
        let a = NSUserActivity(activityType: type)
        a.title = "๋ฌธ์„œ ํŽธ์ง‘"
        a.userInfo = ["documentID": id, "scroll": scrollOffset]
        a.targetContentIdentifier = id
        a.isEligibleForHandoff = true     // ๋งฅ/๋‹ค๋ฅธ ๊ธฐ๊ธฐ๋กœ ์ด์–ด๋ฐ›๊ธฐ
        return a
    }

    static func documentID(from activity: NSUserActivity?) -> String? {
        guard activity?.activityType == type else { return nil }
        return activity?.userInfo?["documentID"] as? String
    }
}

๐Ÿšซ userInfo์— ๋„ฃ์œผ๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ

UIImage, UIViewController, ํด๋กœ์ €, ๋Œ€์šฉ๋Ÿ‰ Data, ๋น„๋ฐ€๋ฒˆํ˜ธ/ํ† ํฐ.

userInfo๋Š” property list ํ˜ธํ™˜ ํƒ€์ž…(String, Number, Date, Data, Array, Dictionary)๋งŒ ๊ฐ€๋Šฅํ•˜๊ณ ,
์šฉ๋Ÿ‰๋„ ์ž‘๊ฒŒ ์œ ์ง€ํ•ด์•ผ ํ•ด. ID๋งŒ ๋„˜๊ธฐ๊ณ  ์‹ค์ œ ๋ฐ์ดํ„ฐ๋Š” ์ €์žฅ์†Œ์—์„œ ๋‹ค์‹œ ์ฝ๋Š” ๊ฒŒ ์ •์„์ด์•ผ.

Info.plist์— activity ํƒ€์ž… ๋“ฑ๋ก

<key>NSUserActivityTypes</key>
<array>
  <string>com.jaenung.editor.document</string>
</array>

์ด๊ฑฐ ๋นผ๋จน์œผ๋ฉด Handoff๋ž‘ ์ผ๋ถ€ ๋ณต์› ๋™์ž‘์ด ์กฐ์šฉํžˆ ์•ˆ ๋จนํ˜€. ์ง„์งœ ์กฐ์šฉํžˆ. ๐Ÿ˜‘

๐Ÿ–๏ธ 7. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์œผ๋กœ ์ฐฝ ๊บผ๋‚ด๊ธฐ

iPadOS์˜ ๊ฝƒ์ด์ง€.
๋ชฉ๋ก์—์„œ ์…€์„ ํ™”๋ฉด ๊ฐ€์žฅ์ž๋ฆฌ๋กœ ์ญ‰ ๋Œ๋ฉด ์ฐฝ์ด ์ชผ๊ฐœ์ง€๋Š” ๊ทธ ๋™์ž‘.

๊ตฌํ˜„ ์›๋ฆฌ๋Š” ์˜์™ธ๋กœ ๋‹จ์ˆœํ•ด.
NSItemProvider์— NSUserActivity๋ฅผ ์‹ค์–ด์„œ ๋˜์ ธ์ฃผ๋ฉด ์‹œ์Šคํ…œ์ด ์•Œ์•„์„œ ์ฐฝ์„ ๋งŒ๋“ค์–ด์ค˜.

extension DocumentListViewController: UITableViewDragDelegate {

    func tableView(_ tableView: UITableView,
                   itemsForBeginning session: UIDragSession,
                   at indexPath: IndexPath) -> [UIDragItem] {

        let doc = documents[indexPath.row]
        let activity = DocumentActivity.make(id: doc.id.uuidString)

        let provider = NSItemProvider(object: doc.title as NSString)
        provider.registerObject(activity, visibility: .all)   // โ˜… ํ•ต์‹ฌ

        let item = UIDragItem(itemProvider: provider)
        item.localObject = doc          // ์•ฑ ๋‚ด๋ถ€ ๋“œ๋กญ ์ตœ์ ํ™”์šฉ
        return [item]
    }
}

// viewDidLoad์—์„œ
tableView.dragDelegate = self
tableView.dragInteractionEnabled = true   // iPhone์—์„œ๋„ ๋“œ๋ž˜๊ทธ ์ผœ๋ ค๋ฉด ํ•„์š”

๐Ÿ’ก registerObject(activity, visibility: .all) ์ด ํ•œ ์ค„์ด ๋งˆ๋ฒ•์ด์•ผ.
์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด ๋์œผ๋กœ ๋Œ๋ฉด iPadOS๊ฐ€ activity๋ฅผ ์ฝ๊ณ  configurationForConnecting์„ ํ˜ธ์ถœํ•ด์„œ ์ƒˆ Scene์„ ๋„์›Œ์ค˜.
๊ฐœ๋ฐœ์ž๊ฐ€ ์ฐฝ ์ƒ์„ฑ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์“ธ ํ•„์š”๊ฐ€ ์—†์–ด!

Context Menu์—๋„ ๋„ฃ์–ด์ฃผ์ž

๋“œ๋ž˜๊ทธ๋Š” ๋ฐœ๊ฒฌํ•˜๊ธฐ ์–ด๋ ค์šด ์ œ์Šค์ฒ˜๋ผ, ๋กฑํ”„๋ ˆ์Šค ๋ฉ”๋‰ด์—๋„ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๋„ฃ๋Š” ๊ฒŒ UX ์ •์„์ด์•ผ.

func tableView(_ tableView: UITableView,
               contextMenuConfigurationForRowAt indexPath: IndexPath,
               point: CGPoint) -> UIContextMenuConfiguration? {

    guard UIApplication.shared.supportsMultipleScenes else { return nil }

    let doc = documents[indexPath.row]
    return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
        UIMenu(children: [
            UIAction(title: "์ƒˆ ์ฐฝ์—์„œ ์—ด๊ธฐ",
                     image: UIImage(systemName: "square.on.square")) { [weak self] _ in
                self?.focusOrOpen(documentID: doc.id.uuidString)
            }
        ])
    }
}

UIApplication.shared.supportsMultipleScenes ์ฒดํฌ ์žŠ์ง€ ๋งˆ!
์•„์ดํฐ์—์„œ "์ƒˆ ์ฐฝ์—์„œ ์—ด๊ธฐ"๊ฐ€ ๋ณด์ด๋ฉด ๊ทธ๊ฑด ๋ฒ„๊ทธ์•ผ. ๐Ÿ˜…

๐Ÿ 8. SwiftUI๋กœ ํ•˜๋ฉด ์–ผ๋งˆ๋‚˜ ํŽธํ• ๊นŒ?

์†”์งํžˆ ๋งํ• ๊ฒŒ. ๋น„๊ต๊ฐ€ ์•ˆ ๋˜๊ฒŒ ํŽธํ•ด.

@main
struct EditorApp: App {
    var body: some Scene {
        WindowGroup {
            DocumentListView()
        }
        .handlesExternalEvents(matching: ["document"])
    }
}

WindowGroup์€ ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ "๊ฐ™์€ ์ข…๋ฅ˜์˜ ์ฐฝ ๋ฌถ์Œ"์ด์•ผ.
์‚ฌ์šฉ์ž๊ฐ€ ์ฐฝ์„ ์—ฌ๋Ÿฌ ๊ฐœ ๋งŒ๋“ค๋ฉด ๊ฐ™์€ ๋ทฐ ๊ณ„์ธต์ด ๊ฐ๊ฐ ๋…๋ฆฝ ์ƒํƒœ๋กœ ์ƒ์„ฑ๋ผ.

SwiftUI์—์„œ ์ƒˆ ์ฐฝ ์—ด๊ธฐ

struct DocumentRow: View {
    let document: Document
    @Environment(\.openURL) private var openURL
    @Environment(\.supportsMultipleWindows) private var supportsMulti

    var body: some View {
        HStack {
            Text(document.title)
            Spacer()
            if supportsMulti {
                Button {
                    openURL(URL(string: "myapp://document/\(document.id)")!)
                } label: {
                    Image(systemName: "square.on.square")
                }
            }
        }
    }
}

iOS 16+ ๋ผ๋ฉด ๋” ๊น”๋”ํ•œ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด

@Environment(\.openWindow) ์™€ WindowGroup(id:for:) ์กฐํ•ฉ!

@main
struct EditorApp: App {
    var body: some Scene {
        WindowGroup {
            DocumentListView()
        }

        // ๊ฐ’ ๊ธฐ๋ฐ˜ ์ฐฝ: ๊ฐ™์€ ๊ฐ’์ด๋ฉด ๊ธฐ์กด ์ฐฝ์„ ์žฌ์‚ฌ์šฉํ•ด์ค€๋‹ค
        WindowGroup(id: "editor", for: Document.ID.self) { $docID in
            DocumentDetailView(documentID: docID)
        }
    }
}

struct DocumentRow: View {
    let document: Document
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("์ƒˆ ์ฐฝ์œผ๋กœ ์—ด๊ธฐ") {
            openWindow(id: "editor", value: document.id)
        }
    }
}

๋ดค์ง€? ์ค‘๋ณต ์ฐฝ ๋ฐฉ์ง€๊นŒ์ง€ ์‹œ์Šคํ…œ์ด ์•Œ์•„์„œ ํ•ด์ค˜.
UIKit์—์„œ openSessions ๋’ค์ง€๋˜ ๊ทธ ์ฝ”๋“œ๊ฐ€ ํ†ต์งธ๋กœ ์‚ฌ๋ผ์ง„ ๊ฑฐ์•ผ. ๐Ÿ˜ญ (๊ฐ๊ฒฉ)

๐Ÿงช SwiftUI ์ƒํƒœ ๊ด€๋ฆฌ ์ฃผ์˜

@StateObject๋Š” ์ฐฝ๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑ๋ผ (์ฐฝ๋ณ„ ๋…๋ฆฝ ์ƒํƒœ โ†’ ๋ณดํ†ต ์ด๊ฒŒ ๋งž์Œ).

๊ณต์œ ํ•ด์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” SwiftData / Core Data / Observable ์‹ฑ๊ธ€ํ†ค์„ ํ†ตํ•ด ๊ณต์œ ํ•ด.

@AppStorage๋Š” UserDefaults ๊ธฐ๋ฐ˜์ด๋ผ ๋ชจ๋“  ์ฐฝ์ด ์ž๋™ ๋™๊ธฐํ™”๋ผ. ํŽธ๋ฆฌํ•˜์ง€๋งŒ "์ฐฝ๋ณ„ ์„ค์ •"์—” ์“ฐ๋ฉด ์•ˆ ๋ผ!

๐Ÿง  9. ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ์—์„œ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”ํ•˜๊ธฐ

์—ฌ๊ธฐ๊ฐ€ ์‹ค๋ฌด์—์„œ ์ œ์ผ ๋งŽ์ด ๊นจ์ง€๋Š” ๊ตฌ๊ฐ„์ด์•ผ.

์ฐฝ A์—์„œ ๋ฌธ์„œ ์ œ๋ชฉ์„ ๋ฐ”๊ฟจ๋Š”๋ฐ, ์ฐฝ B์˜ ๋ชฉ๋ก์€ ์˜›๋‚  ์ œ๋ชฉ ๊ทธ๋Œ€๋กœ. ๐Ÿ˜ฑ
์ด๋Ÿฐ ๋ฒ„๊ทธ, ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ ์ง€์›ํ•˜๋ฉด 100% ๋งŒ๋‚˜๊ฒŒ ๋ผ.

๋ฐฉ์‹๋™์ž‘์ถ”์ฒœ ์ƒํ™ฉ
NotificationCenter๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ๊ฐ€๋ณ๊ณ  ๋น ๋ฆ„, ์†Œ๊ทœ๋ชจ ์•ฑ
Combine / AsyncStream๋‹จ์ผ ์†Œ์Šค๊ฐ€ ์ŠคํŠธ๋ฆผ ๋ฐœํ–‰์ค‘๋Œ€ํ˜•, ๋ฐ˜์‘ํ˜• ์•„ํ‚คํ…์ฒ˜
Core Data + NSFetchedResultsController์ปจํ…์ŠคํŠธ ๋ณ‘ํ•ฉ์œผ๋กœ ์ž๋™ ๋ฐ˜์˜์˜์† ๋ฐ์ดํ„ฐ ์ค‘์‹ฌ ์•ฑ
SwiftData @QueryModelContainer ๊ณต์œ , ์ž๋™ ๊ฐฑ์‹ iOS 17+ ์‹ ๊ทœ ํ”„๋กœ์ ํŠธ
@Observable ์‹ฑ๊ธ€ํ†ค Store๋ฉ”๋ชจ๋ฆฌ ๋‹จ์ผ ์†Œ์ŠคSwiftUI ๊ธฐ๋ฐ˜ ์•ฑ

ํ•ต์‹ฌ ์›์น™: Single Source of Truth

final class DocumentStore {
    static let shared = DocumentStore()

    private let subject = CurrentValueSubject<[Document], Never>([])
    var publisher: AnyPublisher<[Document], Never> { subject.eraseToAnyPublisher() }

    func update(_ doc: Document) {
        var list = subject.value
        if let i = list.firstIndex(where: { $0.id == doc.id }) { list[i] = doc }
        subject.send(list)      // ๋ชจ๋“  ์ฐฝ์ด ๋™์‹œ์— ๊ฐฑ์‹ ๋จ
    }
}

๊ทœ์น™์€ ๋”ฑ ํ•˜๋‚˜์•ผ.
"๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์†Œ์œ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ตฌ๋…ํ•  ๋ฟ์ด๋‹ค."
์ด๊ฑธ ์ง€ํ‚ค๋ฉด ์ฐฝ์ด 10๊ฐœ๊ฐ€ ๋ผ๋„ ์•ˆ ๊นจ์ ธ.

โš ๏ธ Core Data ์“ฐ๋Š” ๊ฒฝ์šฐ์—”

viewContext.automaticallyMergesChangesFromParent = true
์„ค์ •๊ณผ
NSPersistentStoreRemoteChange ์•Œ๋ฆผ ์ฒ˜๋ฆฌ๋ฅผ ๊ผญ ํ™•์ธํ•ด.
๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ปจํ…์ŠคํŠธ์—์„œ ์ €์žฅํ•œ ๋‚ด์šฉ์ด ๋‹ค๋ฅธ ์ฐฝ์— ์•ˆ ๋ณด์ด๋Š” ์‚ฌ๊ณ ๊ฐ€ ์—ฌ๊ธฐ์„œ ๋‚˜๊ฑฐ๋“ .

๐Ÿ“ 10. Stage Manager์™€ ๊ฐ€๋ณ€ ์ฐฝ ํฌ๊ธฐ ๋Œ€์‘

iPadOS 16์—์„œ Stage Manager๊ฐ€ ๋“ฑ์žฅํ•˜๋ฉด์„œ ํŒ์ด ๋˜ ๋ฐ”๋€Œ์—ˆ์–ด.
์ด์ œ ์ฐฝ ํฌ๊ธฐ๊ฐ€ ์‚ฌ์šฉ์ž๊ฐ€ ๋“œ๋ž˜๊ทธํ•˜๋Š” ๋Œ€๋กœ ์ž์œ ๋กญ๊ฒŒ ๋ณ€ํ•ด.

์˜ˆ์ „์—” Slide Over / Split View์˜ ์ •ํ•ด์ง„ ๋น„์œจ๋งŒ ๋Œ€์‘ํ•˜๋ฉด ๋๋Š”๋ฐ,
์ด์ œ๋Š” ๊ฑฐ์˜ ๋ชจ๋“  ํฌ๊ธฐ๋ฅผ ๊ฐ€์ •ํ•ด์•ผ ํ•ด.

์ตœ์†Œ ํฌ๊ธฐ ์ง€์ •ํ•˜๊ธฐ (iOS 16+)

func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
           options: UIScene.ConnectionOptions) {
    guard let ws = scene as? UIWindowScene else { return }

    if let geometry = ws.effectiveGeometry as? UIWindowSceneGeometry {
        print("ํ˜„์žฌ ์ฐฝ ํฌ๊ธฐ:", geometry.systemFrame)
    }

    let pref = UIWindowScene.GeometryPreferences.iOS(
        interfaceOrientations: .landscape
    )
    ws.requestGeometryUpdate(pref) { error in
        print("ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹คํŒจ:", error)
    }
}

๊ทธ๋ฆฌ๊ณ  Info.plist์—์„œ ์ตœ์†Œ ํฌ๊ธฐ๋ฅผ ์ •ํ•  ์ˆ˜๋„ ์žˆ์–ด.

<key>UILaunchScreen</key> ...
<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationPreferredDefaultSceneSessionRole</key>
  <string>UIWindowSceneSessionRoleApplication</string>
</dict>

Size Class ๊ธฐ๋ฐ˜ ์ ์‘ํ˜• ๋ ˆ์ด์•„์›ƒ

override func traitCollectionDidChange(_ previous: UITraitCollection?) {
    super.traitCollectionDidChange(previous)

    guard traitCollection.horizontalSizeClass != previous?.horizontalSizeClass else { return }

    if traitCollection.horizontalSizeClass == .compact {
        splitViewController?.preferredDisplayMode = .oneOverSecondary
    } else {
        splitViewController?.preferredDisplayMode = .oneBesideSecondary
    }
}

๐Ÿ“ ์‹ค์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

โœ… ์ฐฝ ๋„ˆ๋น„ 320pt๊นŒ์ง€ ์ค„์—ฌ๋„ ๋ ˆ์ด์•„์›ƒ ์•ˆ ๊นจ์ง€๋‚˜?

โœ… ๊ฐ€๋กœ โ†” ์„ธ๋กœ ์ „ํ™˜ ์‹œ ์Šคํฌ๋กค ์œ„์น˜ ์œ ์ง€๋˜๋‚˜?

โœ… ํ‚ค๋ณด๋“œ๊ฐ€ ์˜ฌ๋ผ์˜ฌ ๋•Œ safeAreaInsets ์ œ๋Œ€๋กœ ๋ฐ˜์˜๋˜๋‚˜?

โœ… Stage Manager์—์„œ ์ฐฝ์„ ๊ฒน์ณ๋†จ์„ ๋•Œ ๊ทธ๋ฆผ์žยท๋ชจ์„œ๋ฆฌ ์ด์ƒ ์—†๋‚˜?

โœ… ์™ธ๋ถ€ ๋””์Šคํ”Œ๋ ˆ์ด ์—ฐ๊ฒฐ ์‹œ ๋ณ„๋„ Scene ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ๊ฐ€?

๐Ÿงช 11. ๋””๋ฒ„๊น… & ํ…Œ์ŠคํŠธ ์š”๋ น

๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ๋Š” ์žฌํ˜„์ด ์–ด๋ ค์šด ๋ฒ„๊ทธ๊ฐ€ ๋งŽ์•„.
๊ทธ๋ž˜์„œ ํ…Œ์ŠคํŠธ ๋ฃจํ‹ด์„ ๋ฏธ๋ฆฌ ์ •ํ•ด๋‘๋Š” ๊ฒŒ ์ข‹์•„.

๐Ÿ” ํ•„์ˆ˜ ์‹œ๋‚˜๋ฆฌ์˜ค 6์ข…

1. ์ฐฝ A์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ • โ†’ ์ฐฝ B์— ์ฆ‰์‹œ ๋ฐ˜์˜๋˜๋Š”์ง€

2. ์ฐฝ B๋ฅผ ์•ฑ ์Šค์œ„์ฒ˜์—์„œ ์Šค์™€์ดํ”„๋กœ ๋ฒ„๋ฆฐ ํ›„ ์žฌ์‹คํ–‰ โ†’ ์ƒํƒœ ๋ณต์›

3. ์ฐฝ 3๊ฐœ ๋„์šด ์ƒํƒœ์—์„œ ๋ฉ”๋ชจ๋ฆฌ ๊ฒฝ๊ณ  โ†’ ๋ฐฑ๊ทธ๋ผ์šด๋“œ Scene ์ •๋ฆฌ

4. ๊ฐ™์€ ๋ฌธ์„œ๋ฅผ ๋‘ ์ฐฝ์—์„œ ๋™์‹œ ํŽธ์ง‘ โ†’ ์ถฉ๋Œ ์ฒ˜๋ฆฌ

5. Stage Manager๋กœ ์ฐฝ ํฌ๊ธฐ ์ตœ์†Œ/์ตœ๋Œ€ โ†’ ๋ ˆ์ด์•„์›ƒ ๊ฒ€์ฆ

6. ์•ฑ ์™„์ „ ์ข…๋ฃŒ ํ›„ ์•„์ดํŒจ๋“œ ์žฌ๋ถ€ํŒ… โ†’ ์ฐฝ ๋ณต์› ์ƒํƒœ

Scene ๋กœ๊ทธ ์ฐ์–ด๋‘๊ธฐ

extension UIScene {
    var debugTag: String {
        "[\(session.persistentIdentifier.prefix(6))]"
    }
}

func sceneDidBecomeActive(_ scene: UIScene) {
    print("\(scene.debugTag) active | total scenes: \(UIApplication.shared.connectedScenes.count)")
}

persistentIdentifier๋Š” ์ฐฝ๋งˆ๋‹ค ๊ณ ์œ ํ•˜๊ณ , ์ฐฝ์ด ์‚ด์•„์žˆ๋Š” ๋™์•ˆ ์œ ์ง€๋ผ.
๋กœ๊ทธ ์•ž์— ์ด๊ฑธ ๋ถ™์—ฌ๋‘๋ฉด "์–ด๋А ์ฐฝ์—์„œ ๋‚œ ๋กœ๊ทธ์ธ์ง€" ๋ฐ”๋กœ ๊ตฌ๋ถ„๋ผ์„œ ์‚ถ์˜ ์งˆ์ด ์˜ฌ๋ผ๊ฐ€. ๐Ÿ‘

๐Ÿ› ํ”ํ•œ ํฌ๋ž˜์‹œ: Scene ์—†๋Š” ์ƒํƒœ์—์„œ UI ์ ‘๊ทผ

๋ฐฑ๊ทธ๋ผ์šด๋“œ Scene์€ ์‹œ์Šคํ…œ์ด ์–ธ์ œ๋“  sceneDidDisconnect๋กœ ๋Š์„ ์ˆ˜ ์žˆ์–ด.
๋Š๊ธด Scene์˜ window์— ์ ‘๊ทผํ•˜๋ฉด nil์ด๊ฑฐ๋‚˜, ์˜ค๋ž˜๋œ ๋ทฐ๋ฅผ ๋ถ™์žก๊ณ  ์žˆ๊ฒŒ ๋ผ.
โ†’ sceneDidDisconnect์—์„œ ํƒ€์ด๋จธยท์˜ต์ €๋ฒ„ยทwindow ์ฐธ์กฐ๋ฅผ ๋ฐ˜๋“œ์‹œ ์ •๋ฆฌํ•˜์ž.

๐ŸŽจ 12. UX ๊ด€์  โ€” ์–ธ์ œ ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ๋ฅผ ์ง€์›ํ•ด์•ผ ํ• ๊นŒ?

๊ธฐ์ˆ ์ ์œผ๋กœ ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ๋‹ค ๋„ฃ๋Š” ๊ฒŒ ์ •๋‹ต์€ ์•„๋‹ˆ์•ผ.
๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ๊ฐ€ ์ง„์งœ ๊ฐ€์น˜ ์žˆ๋Š” ์•ฑ ์œ ํ˜•์€ ์ •ํ•ด์ ธ ์žˆ์–ด.

์ž˜ ์–ด์šธ๋ฆฌ๋Š” ์•ฑ์ด์œ 
๋ฌธ์„œ/๋…ธํŠธ ์—๋””ํ„ฐ์—ฌ๋Ÿฌ ๋ฌธ์„œ ๋น„๊ตยท์ฐธ์กฐ ์ž‘์—…์ด ์žฆ์Œ
๋ฉ”์ผยท๋ฉ”์‹ ์ €๋Œ€ํ™” ์ฝ์œผ๋ฉฐ ๋‹ค๋ฅธ ๋Œ€ํ™” ์ž‘์„ฑ
์ฝ”๋“œ ์—๋””ํ„ฐ / IDEํŒŒ์ผ ๊ฐ„ ์ด๋™ ์—†์ด ๋ณ‘๋ ฌ ํŽธ์ง‘
๋””์ž์ธ ํˆด์บ”๋ฒ„์Šค + ๋ ˆํผ๋Ÿฐ์Šค ๋™์‹œ ํ‘œ์‹œ
๋ธŒ๋ผ์šฐ์ € / ๋ฆฌ๋”ํƒญ๋ณด๋‹ค ์ง๊ด€์ ์ธ ๋น„๊ต

๋ฐ˜๋Œ€๋กœ ๋‹จ์ผ ํ๋ฆ„ ์ค‘์‹ฌ ์•ฑ(๊ฐ„๋‹จํ•œ ๊ณ„์‚ฐ๊ธฐ, ๊ฒฐ์ œ ์•ฑ, ๋‹จ์ˆœ ๋ทฐ์–ด)์€
๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ๋ฅผ ๋„ฃ์–ด๋„ ์‚ฌ์šฉ์ž๊ฐ€ ์“ธ ์ผ์ด ๊ฑฐ์˜ ์—†์–ด.
์˜คํžˆ๋ ค ์ƒํƒœ ๋™๊ธฐํ™” ๋ฒ„๊ทธ๋งŒ ๋Š˜์–ด๋‚˜์ง€. ๐Ÿ˜‡

๐Ÿ’ฌ ์žฌ๋Šฅ๋„ท '์ง€์‹์ธ์˜ ์ˆฒ'์—์„œ ์ž์ฃผ ๋‚˜์˜ค๋Š” ์งˆ๋ฌธ

Q. ์•„์ดํฐ์—์„œ๋„ ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ ๋˜๋‚˜์š”?
A. ์•ˆ ๋ผ. Scene ๊ตฌ์กฐ๋Š” ๋™์ผํ•˜๊ฒŒ ์“ฐ์ง€๋งŒ ํ‘œ์‹œ๋˜๋Š” ์ฐฝ์€ ํ•ญ์ƒ 1๊ฐœ์•ผ. ๋‹จ, Scene ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์–ด๋‘๋ฉด ์•„์ดํŒจ๋“œยท๋น„์ „OSยท๋งฅ ํ™•์žฅ์ด ํ›จ์”ฌ ์ˆ˜์›”ํ•ด์ ธ.

Q. ๊ธฐ์กด AppDelegate ์•ฑ์„ Scene์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๊ธฐ ์–ด๋ ต๋‚˜์š”?
A. ์ƒ๊ฐ๋ณด๋‹ค ํ•  ๋งŒํ•ด. โ‘  SceneDelegate ์ถ”๊ฐ€ โ†’ โ‘ก window ์„ธํŒ… ์ฝ”๋“œ ์ด๋™ โ†’ โ‘ข keyWindow ์ œ๊ฑฐ โ†’ โ‘ฃ ์ „์—ญ ์ƒํƒœ๋ฅผ Store๋กœ ๋ถ„๋ฆฌ. ์ด 4๋‹จ๊ณ„๊ฐ€ ์ „๋ถ€์•ผ.

๐Ÿš€ 13. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋กœ๋“œ๋งต

๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ ๋„์ž… 5๋‹จ๊ณ„ 1 Scene ์ „ํ™˜ SceneDelegate ์ถ”๊ฐ€ยทํ”Œ๋ฆฌ์ŠคํŠธ 2 ์ „์—ญ์ƒํƒœ ์ œ๊ฑฐ keyWindow ์‹ฑ๊ธ€ํ†ค ์ •๋ฆฌ 3 Activity ์„ค๊ณ„ ์ƒํƒœ ์ง๋ ฌํ™” ๋ณต์› ๋กœ์ง 4 ์ง„์ž…์  ์ถ”๊ฐ€ ๋“œ๋ž˜๊ทธยท๋ฉ”๋‰ด ๋‹จ์ถ•ํ‚ค 5 ๊ฒ€์ฆ ๋™๊ธฐํ™”ยท๋ณต์› ๋ ˆ์ด์•„์›ƒ 2๋‹จ๊ณ„๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋ฉด 3~5๋‹จ๊ณ„์—์„œ ๋ฐ˜๋“œ์‹œ ๋˜๋Œ์•„์˜ต๋‹ˆ๋‹ค

์ง„์‹ฌ์œผ๋กœ ์กฐ์–ธ ํ•˜๋‚˜ ํ• ๊ฒŒ.
2๋‹จ๊ณ„(์ „์—ญ ์ƒํƒœ ์ œ๊ฑฐ)๊ฐ€ ์ „์ฒด ์ž‘์—…์˜ 70%์•ผ.

Scene ๋ถ™์ด๋Š” ๊ฑด ๋ฐ˜๋‚˜์ ˆ์ด๋ฉด ๋๋‚˜.
๊ทผ๋ฐ "ํ˜„์žฌ ์„ ํƒ๋œ ํ•ญ๋ชฉ"์„ ์‹ฑ๊ธ€ํ†ค์— ๋ฐ•์•„๋‘” ์ฝ”๋“œ,
"ํ™”๋ฉด ์ „ํ™˜"์„ AppDelegate์—์„œ ํ•˜๋˜ ์ฝ”๋“œ,
์ด๋Ÿฐ ๊ฑธ ์ •๋ฆฌํ•˜๋Š” ๋ฐ ๋ฉฐ์น ์ด ๊ฑธ๋ ค.

๊ทธ๋ž˜์„œ ์‹ ๊ทœ ํ”„๋กœ์ ํŠธ๋ผ๋ฉด ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ๋ฅผ ๊ฐ€์ •ํ•˜๊ณ  ์„ค๊ณ„ํ•˜๋Š” ๊ฒŒ ํ›จ์”ฌ ์‹ธ๊ฒŒ ๋จนํ˜€.

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ โ€” ์ •๋ฆฌํ•˜๋ฉด ์ด๊ฒƒ๋งŒ ๊ธฐ์–ตํ•ด

โ‘  ์•ฑ ์ƒ๋ช…์ฃผ๊ธฐ โ‰  UI ์ƒ๋ช…์ฃผ๊ธฐ
AppDelegate๋Š” ํ”„๋กœ์„ธ์Šค, SceneDelegate๋Š” ์ฐฝ.

โ‘ก ์ฐฝ ๊ฐ„ ํ†ต์‹ ์€ ๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ ๋ฐ์ดํ„ฐ๋กœ
NSUserActivity์—๋Š” ID๋งŒ, ์‹ค์ œ ๋ฐ์ดํ„ฐ๋Š” ๊ณต์œ  ์ €์žฅ์†Œ์—์„œ.

โ‘ข Single Source of Truth๋Š” ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜
์ฐฝ์ด 2๊ฐœ๊ฐ€ ๋˜๋Š” ์ˆœ๊ฐ„ ์ „์—ญ ๋ณ€์ˆ˜๋Š” ํญํƒ„์ด ๋œ๋‹ค.

โ‘ฃ keyWindow๋Š” ์žŠ์–ด๋ผ
์–ธ์ œ๋‚˜ "๋‚ด ๋ทฐ๊ฐ€ ์†ํ•œ windowScene"์„ ๊ธฐ์ค€์œผ๋กœ.

โ‘ค SwiftUI๋ผ๋ฉด WindowGroup + openWindow
iOS 16+ ํƒ€๊ฒŸ์ด๋ฉด ๊ณ ๋ฏผํ•  ํ•„์š”๋„ ์—†์ด ์ด๊ฒŒ ์ •๋‹ต.

โ‘ฅ ์ค‘๋ณต ์ฐฝ ๋ฐฉ์ง€์™€ ์ƒํƒœ ๋ณต์›์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ฒด๊ฐํ•˜๋Š” ํ’ˆ์งˆ
์—ฌ๊ธฐ์„œ ์™„์„ฑ๋„๊ฐ€ ๊ฐˆ๋ฆฐ๋‹ค.

์ฒ˜์Œ์—” "Scene์ด ๋ญ์•ผ, ์™œ ์ด๋ ‡๊ฒŒ ๋ณต์žกํ•ด" ์‹ถ์—ˆ์„ ๊ฑฐ์•ผ.
๊ทผ๋ฐ ํ•œ ๋ฒˆ ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋ฉด, ์˜คํžˆ๋ ค ์•ฑ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์กŒ๋‹ค๋Š” ๊ฑธ ๋А๋ผ๊ฒŒ ๋ผ.

์™œ๋ƒ๋ฉด ๋ฉ€ํ‹ฐ ์œˆ๋„์šฐ๋ฅผ ์ง€์›ํ•˜๋ ค๋ฉด
์–ด์ฉ” ์ˆ˜ ์—†์ด ๋ฐ์ดํ„ฐ์™€ ํ™”๋ฉด์„ ์ œ๋Œ€๋กœ ๋ถ„๋ฆฌํ•˜๊ฒŒ ๋˜๊ฑฐ๋“ .
๊ฒฐ๊ตญ ์ข‹์€ ์„ค๊ณ„๋กœ ๋Œ๋ ค๊ฐ€๊ฒŒ ๋˜๋Š” ์…ˆ์ด์ง€. ๐Ÿ˜Œ

๊ทธ๋ฆฌ๊ณ  ์š”์ฆ˜์€ ๋น„์ „OS(visionOS)๊นŒ์ง€ ๋‚˜์˜ค๋ฉด์„œ
"์—ฌ๋Ÿฌ ์ฐฝ์„ ๊ณต๊ฐ„์— ๋„์šฐ๋Š”" ๊ฒฝํ—˜์ด ๋” ์ค‘์š”ํ•ด์กŒ์–ด.
Scene ๊ธฐ๋ฐ˜ ์„ค๊ณ„๋ฅผ ํ•ด๋‘๋ฉด ๊ทธ์ชฝ ํ™•์žฅ๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ด์–ด์ ธ.

๋” ๊นŠ์€ iOS ์•„ํ‚คํ…์ฒ˜ ์ด์•ผ๊ธฐ๋‚˜ ์‹ค๋ฌด ์ฝ”๋“œ ๋ฆฌ๋ทฐ๊ฐ€ ํ•„์š”ํ•˜๋ฉด
์žฌ๋Šฅ๋„ท์˜ ๊ฐœ๋ฐœ ๋ถ„์•ผ ์ „๋ฌธ๊ฐ€๋“ค์—๊ฒŒ ์ง์ ‘ ๋ฌผ์–ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ.
ํ˜ผ์ž ์‚ฝ์งˆํ•˜๋Š” ์‹œ๊ฐ„๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฅด๊ฒŒ ๋‹ต์ด ๋‚˜์˜ฌ ๋•Œ๊ฐ€ ๋งŽ๊ฑฐ๋“ . ๐Ÿ™‚

์ž, ์ด์ œ Xcode ์ผœ๊ณ  "Supports multiple windows" ์ฒดํฌ๋ถ€ํ„ฐ ํ•ด๋ณด์ž!
์•„์ดํŒจ๋“œ์—์„œ ๋‚ด ์•ฑ ์ฐฝ์ด ๋‘ ๊ฐœ ๋œจ๋Š” ์ˆœ๊ฐ„, ์ง„์งœ ์งœ๋ฆฟํ•˜๋‹ค. โœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.