Ver4.0 ๐ช 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. ์ ์ฒด ์ํคํ ์ฒ ํ๋์ ๋ณด๊ธฐ
๊ทธ๋ฆผ ๋ณด๋ฉด ๊ฐ ์ค์ง?
์๋ ํ๋, ๊ฐ์ด๋ฐ๋ ์ฌ๋ฌ ๊ฐ, ์๋๋ ๋ค์ ํ๋.
์ด ๊ตฌ์กฐ๋ฅผ ๋จธ๋ฆฌ์ ๋ฐ์๋๋ฉด ๋๋จธ์ง๋ ์ ๋ถ ๋ํ
์ผ์ด์ผ.
โ๏ธ 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๋ผ๋ ์ง๋ ฌํ ๊ฐ๋ฅํ ๋ดํฌ์ ๋ด์๋ผ."
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 @Query | ModelContainer ๊ณต์ , ์๋ ๊ฐฑ์ | 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. ๋ง์ด๊ทธ๋ ์ด์ ๋ก๋๋งต
์ง์ฌ์ผ๋ก ์กฐ์ธ ํ๋ ํ ๊ฒ.
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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.