SwiftUI

SwiftUI - @State / @Binding / @StateObject / @EnvironmentObject

GREEN.1229 2022. 3. 21. 20:15

안녕하세요. 그린입니다🟢

이번 포스팅에서는 전 포스팅에 이어 SwiftUI에서 구조체 내부에서 값을 변경하여 반영해주거나

전역적으로 값의 업데이트를 반영해주는 그런 친구들인 타이틀의 세친구 즉,

@State, @Binding, @StateObject, @EnvironmentObject에 대해서 학습해보겠씁니다🙋🏻

 

우선 하나씩 차례대로 진행해보겠습니다👍

@State

정의를 먼저보면, SwiftUI에서 값을 읽고 쓸 수 있는 유형의 어노테이션입니다.

@frozen @propertyWrapper struct State<Value>

역시 propertyWrapper이고 구조체입니다.

SwiftUI에서 우선 뷰는 구조체입니다.

즉, 내부 값을 변경해줄 수 없는데 이 @State를 이용해 변경해줄 수 있습니다.

또한 구조체이기에 이닛과 디이닛이 이뤄지기에 값의 안전함을 보장할 수 없습니다.

그럴때도 이 @State가 안전함을 보장해줍니다.

기본적으로 Private 선언이기에 다른 뷰와 값을 소통할 수 없습니다.

다른 뷰와 소통하려면 @Binding 혹은 앞서 배운 @ObservedObject 그리고 또 나올 전역적인 @EnvironmentObject를 이용해주면 됩니다.

정리해보자면 이 어노테이션은 해당 뷰에서만 이뤄지는 아주 간단한 프로퍼티에 대해서만 사용하는게 좋을것 같아요!🧑🏻‍⚖️

 

그럼 사용에 대한 공식문서의 예시를 살펴보시죠!

struct PlayButton: View {
    @State private var isPlaying: Bool = false

    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            isPlaying.toggle()
        }
    }
}

아주 간단합니다.

isPlaying이라는 @State 변수를 버튼에 매칭시켜줬습니다.

버튼이 클릭될때마다 타이틀이 변하고 isPlaying 변수 값도 토글되어 변경됩니다.

@Binding

이어서 이 어노테이션을 볼껀데요.

위에서 말했듯 @State는 자신만의 뷰에서만 변경이 이뤄지게됩니다.

그런데 SwiftUI는 뷰들의 부모/자식 뷰의 계층관계를 가질때가 많고 구조적으로 분리할 때도 많습니다.

이에 상위 뷰에서 @State의 변수가 있어 해당 값을 하위 뷰에서 캐치해 변화가 주어져야할때 사용됩니다.

일단 공식문서 정의를 보시죠!

@frozen @propertyWrapper @dynamicMemberLookup struct Binding<Value>

별거없으니 바로 사용하는 예시를 보시죠!

struct PlayButton: View {
    @Binding var isPlaying: Bool

    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            isPlaying.toggle()
        }
    }
}
struct PlayerView: View {
    var episode: Episode
    @State private var isPlaying: Bool = false

    var body: some View {
        VStack {
            Text(episode.title)
                .foregroundStyle(isPlaying ? .primary : .secondary)
            PlayButton(isPlaying: $isPlaying) // Pass a binding.
        }
    }
}

우선 상위뷰는 PlayerView이고 하위뷰는 PlayButton입니다.

하위뷰에서 @Binding 변수를 선언해줍니다.

이 변수는 상위뷰의 isPlaying @State 변수와 바인딩됩니다.

그럼 상위뷰에서 구현부를 보시면 PlayButton을 호출하는데 인자로 @State 변수의 값을 넘겨주며 바인딩해줍니다.

그러면 하위뷰에서는 바인딩 변수가 존재함으로 상위뷰에서의 변화를 하위뷰에서도 캐치하여 감지할 수 있게됩니다.

고로 @State와 @Binding의 사용은 같이 이뤄질때가 많습니다🙌

@StateObject

이전 포스팅에서 @ObservedObject에 대해 알아본적이 있습니다!

"ObservableObject를 구독하며 값 업데이트가 발생하면 뷰를 갱신해주는 친구입니다."

라고 설명했는데요, 이와 거의 동일하다고 보시면됩니다.

즉, 관찰 가능한 객체를 인스턴스화 하는 속성 래퍼입니다.

@frozen @propertyWrapper struct StateObject<ObjectType> where ObjectType : ObservableObject

정의된것도 거의 유사해요. ObservableObject를 따르구요!

다만 차이라함은 SwiftUI의 뷰는 업데이트 될때가 많습니다.

즉 뷰의 렌더링이 일어나면 @ObservedObject의 인스턴스가 초기화될때가 있게됩니다.

이를 @StateObject는 보완해준다고 보면 될것 같아요.

위에서 @State 어노테이션을 사용해 뷰의 프로퍼티를 구성해준것과 비슷한 느낌일것 같아요🙃

@EnvironmentObject

마지막으로 이 친구에 대해 알아보시죠!

아까 소개할때 전역적인 친구라고 말했습니다.

그 이유는 여태까지 뷰 자체에서 놀거나 상위/하위뷰에서 바인딩해줘서 관리하고 했다면 이 친구는 다릅니다.

음.. 약간 싱글턴 같은 인스턴스라고 볼 수 있어요.

즉 이 어노테이션을 붙여주는 클래스 타입의 정의는 앱 전역적으로 사용될 데이터의 집합으로 이뤄지는게 좋을것 같습니다!

그럼 자세히 공식문서 정의를 보죠!

@frozen @propertyWrapper struct EnvironmentObject<ObjectType> where ObjectType : ObservableObject

ObservableObject 프로토콜을 따르군요 역시

흐름을 보자면 전역적으로 사용할 데이터에 대한 클래스를 만듭니다.

그리고 전역적 사용을 위해 SceneDelegate에서 .environmentObject(_:) 메서드를 통해 값을 매칭합니다.

마지막으로 사용될 뷰들에서 프로퍼티로 @EnvironmentObject 어노테이션을 붙인 변수를 생성합니다.

한번 예시로 보시죠!

class Info: ObservableObject {
  @Published var age = 10
}

class SceneDelegate {
...
  var info = Info() 
  window.rootViewController = UIHostingController(rootView: MainView().environmentObject(info))
...
}

struct MainView: View {
  @EnvironmentObject var info: Info

  var body: some View {
    Button(action: {
      self.info.age += 1
    }) {
      Text("Click Me for plus age")
    }
    SubView()
  }
}

struct SubView: View {
  @EnvironmentObject var info: Info

  var body: some View {
    Button(action: {
      self.info.age -= 1
    }) {
      Text("Click Me for minus age")
    }
  }
}

요렇게 간단히 쓰임을 볼 수 있을것 같아요.

먼저 앱 전역적으로 사용될 인스턴스를 위한 Info 클래스를 생성합니다.

요 안에서는 @Published 변수 age를 가지고 있어요.

이 어노테이션이 뭐라했쬬? 값의 변경이 일어나면 바로 업데이트 쳐주는! 그런 ㅎㅎ

그다음 SceneDelegate에서 전역적인 인스턴스를 만들어주고 .environmentObject(_:) 메서드를 이용해 해당 인스턴스를

담아줍니다.

그다음 메인뷰와 서브뷰를 볼 수 있어요.

해당 인스턴스 타입의 @EnvironmentObject 변수를 만들어줍니다.

그리고 이에 대해 각 역할을 부여해주면 됩니다.

요러면 끝...! 쉽죠??

마무리

자 이렇게 이전과 오늘에 연결되어 Combine과 SwiftUI에서의 뷰와 데이터 변화와 흐름을 이해하고 사용하는것에 대해 알아봤습니다!

써봐야 감이 잘 올거에요!

그럼 긴말 필요없이 써봅시다🙌

[참고자료]

https://developer.apple.com/documentation/swiftui/state

 

Apple Developer Documentation

 

developer.apple.com

https://developer.apple.com/documentation/swiftui/binding

 

Apple Developer Documentation

 

developer.apple.com

https://developer.apple.com/documentation/swiftui/stateobject

 

Apple Developer Documentation

 

developer.apple.com

https://developer.apple.com/documentation/swiftui/environmentobject

 

Apple Developer Documentation

 

developer.apple.com