[iOS] ObservableObject에게 데이터를 묻다

유인호·2024년 4월 26일
1

iOS

목록 보기
43/73
post-custom-banner

[iOS] ObservedObject, 그는 신인가?의 후속작임.


0. 서론

이전 게시물에 결론을 열심히 적었지만, 사실 이해가 하나도 되어있지 않은 상태였다. 그러다가 오늘 Custom App Delegate와 Custom Scene Delegate를 만들고나서 드디어 왜 아이패드에서 같은 데이터를 공유하게 되었는지 이해하게 되었음.

1. 조건


조건은 이전 게시물과 같다. 하지만, 여기서 추가된 것이 ObservableObject들에 init을 붙여 print되게 하였고, App, Scene Delegate에도 init을 붙여 print되게 하였음.

@main
struct SwiftUIPracticeApp: App {

	init() {
		print("SwiftUIPracticeApp init")
	}
	@UIApplicationDelegateAdaptor var delegate: MyAppDelegate
	var body: some Scene {
		WindowGroup {
			NavigationStack {
				TestViews333()
					.font(.title)
			}
		}
	}
}


class MyAppDelegate: NSObject, UIApplicationDelegate {

	override init() {
		super.init()
		print("AppDelegate init")
	}

  func application(
	 _ application: UIApplication,
	 configurationForConnecting connectingSceneSession: UISceneSession,
	 options: UIScene.ConnectionOptions
  ) -> UISceneConfiguration {
	  print(#function)
	 let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
	 sceneConfig.delegateClass = MySceneDelegate.self
	 return sceneConfig
  }
}

class MySceneDelegate: NSObject, UIWindowSceneDelegate {
	 var window: UIWindow?

	override init() {
		super.init()
		print("MySceneDelegate init")
	}

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

	 func sceneDidDisconnect(_ scene: UIScene) {
		 print(#function)
	 }

	 func sceneDidBecomeActive(_ scene: UIScene) {
		 print(#function)
	 }

	 func sceneWillResignActive(_ scene: UIScene) {
		 print(#function)
	 }

	 func sceneWillEnterForeground(_ scene: UIScene) {
		 print(#function)
	 }

	 func sceneDidEnterBackground(_ scene: UIScene) {
		 print(#function)
	 }
}

2. 결과

SwiftUIPracticeApp init
AppDelegate init
application(:configurationForConnecting:options:)
MySceneDelegate init
ObservedObject init
TestSingleton init
TestViews333 init
scene(
:willConnectTo:options:)
sceneWillEnterForeground(:)
StateObject init
application(
:configurationForConnecting:options:)
MySceneDelegate init
scene(:willConnectTo:options:)
sceneWillEnterForeground(
:)
StateObject init
sceneDidBecomeActive(:)
sceneDidBecomeActive(
:)

앱을 두개씩 실행했기에 SceneDelegate가 2번씩 호출되는건 당연한거고, 여기서 중요하다고 생각된 부분만 따로 추출하자면,

SwiftUIPracticeApp init
AppDelegate init
MySceneDelegate init
ObservedObject init
TestSingleton init
TestViews333 init
StateObject init

ObservableObject가 TestView333이 init되기 이전에 호출된다! 반면에 StateObject의 경우 TestView333이 init이 되고나서 init이 되는걸 볼 수 있었다.

상식적인 범주에서라면, StateObject의 init이 조금 더 상식적 일 것이다.

다만, 무엇 때문인지는 몰라도 ObservableObject가 뷰가 만들어지기 이전부터 init이 되어 만들어지고 있었고, 이는 ObservableObject는 TestView333과 무관하다고 볼 수 있다.

다시 말하자면 StateObject는 일반적인 Class처럼 TestView333에 종속되어 있다고 볼 수 있다.

현상을 알았으니 왜 인지를 알아봐야 한다.

3. 왜?

ObservableObject의 애플 공식문서를 보면, 이렇게 설명되어 있다.

Don't call this initializer directly. Instead, declare
an input to a view with the `@ObservedObject` attribute, and pass a
value to this input when you instantiate the view. Unlike a
``StateObject`` which manages data storage, you use an observed
object to refer to storage that you manage elsewhere

Explicitly calling the observed object initializer in `MySubView` would
behave correctly, but would needlessly recreate the same observed object
instance every time SwiftUI calls the view's initializer to redraw the view.

이를 전지전능하신 G선생님께 번역을 맡기고, 아래에 있던 예제 코드로 보충설명을 하자면,

이니셜라이저를 직접 호출하지 마세요. 대신에 뷰의 입력으로 @ObservedObject 속성을 선언하고, 뷰를 인스턴스화할 때 이 입력에 값을 전달하세요. 데이터 저장소를 관리하는 StateObject와 달리, observed object는 다른 곳에서 관리하는 저장소를 참조하는 데 사용됩니다.

Observed object 이니셜라이저를 명시적으로 호출하는 것은 MySubView에서 올바르게 동작할 수 있지만, SwiftUI가 뷰를 다시 그릴 때마다 동일한 observed object 인스턴스를 불필요하게 재생성해야 합니다.

// following example:

class DataModel: ObservableObject {
	@Published var name = "Some Name"
	@Published var isEnabled = false
}

struct MyView: View {
	@StateObject private var model = DataModel()

	var body: some View {
		Text(model.name)
		MySubView(model: model)
	}
}

struct MySubView: View {
	@ObservedObject var model: DataModel

	var body: some View {
		Toggle("Enabled", isOn: $model.isEnabled)
	}
}

애초에 태생부터 그렇게 쓰지 말라고 만들어진거였다.

ObservableObject를 선언할때 명시적으로 init을 해주는것이 아니라, StateObject로 만들어진 것들을 Binding하듯 사용하는거였다.

따라서, 아이패드에서 같은 메모리 주소를 공유했던 이유는 아이패드에서 같은 앱을 켜도 App Delegate는 하나, Scene Delegate는 2개로 운용되게 되는데, App Delegate에서 ObservableObject가 init된걸 서로 다른 앱들이 똑같이 가져왔다. 라고 이해하면 좋을것 같다.

4. 결론

이전 프로젝트를 보면 StateObject와 ObservableObject의 차이를 이해하지 못하고, ObservableObject만 주먹구구 사용하기 바빴다. 지금 머릿속에 스쳐지나가는 코드들을 떠올려 봤을때 그렇게 말도 안되게 짰는데 잘 버틴 아이폰도 굉장히 대단하다고 느껴지기도 하고, 앞으로는 용도에 맞춰 사용해야겠다.

ps. 그럼... 인터넷에 수없이 나돌아다니는 클론코딩들은 대체 무엇이였는지.. 다 거기서 배웠는데...

profile
🍎Apple Developer Academy @ POSTECH 2nd, 🌱SeSAC iOS 4th
post-custom-banner

0개의 댓글