SwiftUI를 다루기 위해서는
SwiftUI의 Property Wrappers는 무엇인지
정확하게 알고 짚고 넘어가야 한다.
SwiftUI사용하게 되면, 필수적으로 접하고
사용하게 될 부분이기도 하기 때문이다.
Property Wrappers
Swift 자체에서 특정한 기능이나 패턴 등
이러한 것들을 재사용하기 위해서
속성 즉, 프로퍼티에 적용할 수 있는
사용자 정의 속성 modifier 라고
간단하게 이야기 할 수 있겠다.
SwiftUI에서 Property Wrappers들이
어떤 기능을 해주고 어떻게 유용한가?
[ Property Wrappers ]
- @State
- @Binding
- ObservableObject
- @Published
- @ObservedObject
- @StateObject
- @Environment
- @EnvironmentObject
3번의 경우는 Property Wrappers가 아닌
프로토콜 이지만, 그 외 Property Wrappers들과
함께 사용되며 이해하기에 적합하여 정리해보겠다.
우선은
기본적으로 사용되는
Property Wrappers 으로는
1번, 2번이 있으며
3번과 주로 사용되는 4번도
기본적인 Property Wrappers이다.
뷰에서 3번을 따르는
객체의 상태변화를 감지하여
해당 뷰를 자동으로 업데이트 하는데 사용되는
5번과 6번
환경설정 값에
접근할 수 있게 도와주는 7번
공유 데이터를
여러곳에서 사용할 수 있게 해주는 8번
이렇게 정리해 볼 수 있다.
@State
- SwiftUI에서 상태를 처리하는 방법이다.
- 뷰의 상태를 저장하는 프로퍼티로 상태 관리 주체는 각 해당 선언된 위치에서의 뷰이다.
- 기본적으로 Private 선언이기에 다른 뷰와 값을 소통하려면 Binding을 이용해야한다.
- 값이 변경될 때마다 이 업데이트한다.
struct ContentView.View {
@State private var isPlaying: Bool = false
var body: some View {
Button(isPlaying ? "Pause" : "Play") {
isPlaying.toggle()
}
}
}
이 코드는
버튼을 누를 때마다 상태값이 변하고,
그 값에 따라 버튼의 레이블이
실시간으로 바뀌는 구조를 보여주는
간단한 예시이다.
isPlaying은 @State로 선언된
Bool 타입 프로퍼티이며,
초기값은 false다.
body에 구현된 버튼은
isPlaying이 true면 "Pause", false면
"Play"라는 텍스트를 표시한다.
즉, 이 프로퍼티는 뷰의 상태를 저장하고
UI에 반영하는 역할을 한다.
버튼의 액션 클로저에서는
isPlaying.toggle()을 호출해
값이 true ↔ false로 전환된다.
사용자가 버튼을 누를 때마다
isPlaying 값이 바뀌고,
그에 맞춰 버튼의 텍스트도 변경된다.
결국, UI 업데이트는 isPlaying이라는
@State 프로퍼티의 변경을 통해
자동으로 이루어진다는 것을 확인할 수 있다.
@Binding
- 뷰와 상태를 바인딩 하는 방법
- 상위 @State 변수를 전달 받아 하위 뷰에서 캐치해 변화 감지 및 연결
- Binding은 다른 뷰가 소유한 속성을 연결하기에 소유권 및 저장 공간이 없음
설명으로는 잘 이해가 안갈 수 있어서
코드로 보자면 아래와 같다.
[1번 상위뷰 코드]
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) // Binding
}
}
}
[2번 하위뷰 코드]
struct PlayButton: View {
@Binding var isPlaying: Bool
var body: some View t
Button(isPlaying ? "pause" : "Play") {
isPlaying.toggle()
}
}
}
1번 코드부터 설명하자면,
PlayerView라는 상위 뷰가 존재한다.
이 뷰는 현재 재생 중인
에피소드의 타이틀을 보여주고,
해당 에피소드를 재생할 수 있는
플레이 버튼 뷰를 포함한다.
여기서 플레이 버튼 뷰가
하위 뷰에 해당한다.
PlayerView 상위 뷰에는
isPlaying이라는 @State 프로퍼티가 있으며,
이 값은 에피소드 타이틀의 스타일에도 쓰이고,
하위 플레이 버튼에서도 사용하고 싶다.
즉, 상태값을 하위 뷰와 공유하려는 것이다.
이를 위해 하위 뷰인 플레이 버튼에서는
상위 뷰에서 전달받을 isPlaying 프로퍼티를
@Binding으로 선언한다.
하위 뷰에서 이 프로퍼티 값을 변경하면
상위 뷰에서도 동일하게 값이 변하며,
반대로 상위 뷰에서 값이 변하면
하위 뷰에도 반영된다.
다시 상위 뷰로 돌아와 보면,
플레이 버튼을 생성할 때 isPlaying 변수에
$isPlaying을 전달한다.
여기서 $ 기호는 @State 변수의 참조를 생성해
다른 뷰나 속성과 양방향으로 연결할 수 있도록 해준다.
여기까지,
@State와 @Binding의 관계를 이해했다면
SwiftUI의 절반은 이해한 것이라고 해도
과언은 아니다.
ObservableObject
- 클래스 프로토콜로 관찰하는 어떠한 값이 변경되면 변경사항을 알려주기 위해 채택하여 사용한다. (주로 뷰모델에서 채택)
- 뷰에서 인스턴스 변화를 감시하기 위해 모델 객체로 생성할 때 자주 사용함
[1번 클래스 코드]
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func haveBirthday() -> Int {
age += 1
return age
}
}
[2번 코드]
let john = Contact(name: "John Appleseed", age: 24)
cancellable = john.objectWillChange
.sink { _ in
print("\(john.age) will change")
}
print(john.haveBirthday())
1번 코드를 보면 Contact라는 클래스가 있는데,
ObservableObject 프로토콜을 채택하고 있다.
이 말은 뭐냐면,
이 클래스에서 어떤 값이 바뀌면
그 변화를 다른 쪽에 알려줄 수 있다는 거다.
보통 뷰모델에서 이런 식으로 많이 쓴다.
여기 안을 보면 name이랑 age라는 프로퍼티가 @Published로 선언돼 있다.
이게 붙으면 값이 바뀔 때마다 자동으로 변경 알림을 내보낸다.
그래서 이 값을 감시하고 있는 뷰는 알아서 업데이트된다.
haveBirthday()라는 함수는 말 그대로 나이를 한 살 늘려준다.
age가 @Published라서, 이 함수가 실행돼서
값이 바뀌면 바로 알림이 발행되는 거다.
두번째 코드를 보면
여기서 john이라는 인스턴스를 만들고,
objectWillChange로 이 객체 변화를 감시하고 있다.
sink 안에 있는 코드는
값이 바뀌기 직전에 실행되는 부분이다.
그래서 haveBirthday()를 호출하면
먼저 "24 will change"가 찍히고,
그 다음에 나이가 25로 바뀌면서
반환된 값이 print로 출력된다.
즉, 이 코드는 값이 바뀌기 직전 알림 → 값 변경 → 변경된 값 출력 이
흐름을 보여주는 예시다.
SwiftUI에선 이걸 뷰에 연결하면
값이 바뀔 때 UI도 자동으로 같이 바뀐다.
@Published
- 위에서 구현했듯이 Observableobject를 구현한 클래스 내에서 프로퍼티 선언 시 사용한다.
- @Published로 선언된 프로퍼티를 뷰에서 관찰할 수 있다.
- Observableobject의 objectWillChange.send() 기능을 @Published 프로퍼티가 변경되면 자동으로 호출한다.
@ObservedObject
- 뷰에서 Observableobject 타입의 인스턴스 선언 시 사용
- Observableobject의 값이 업데이트되면 뷰를 업데이트
[1번 코드]
class User: ObservableObject {
@Published var age = 10
}
[2번 코드]
struct ContentView: View {
@ObservedObject var user: User
var body: some View {
Button("Plus Age")
user.age += 1
}
}
}
@ObservedObject는 뷰에서
ObservableObject 타입의 인스턴스를 감시할 때 쓰는 거다.
이걸 쓰면 그 객체 안에 있는 값이 바뀔 때마다
뷰도 같이 다시 그려진다.
먼저 1번 코드를 보면,
User라는 클래스가 있고,
ObservableObject를 채택했다.
age라는 프로퍼티에 @Published가 붙어 있어서
값이 바뀌면 변경 알림을 뿌린다.
즉, 이걸 감시하고 있는 뷰가 있으면
값이 변하는 순간 UI도 같이 업데이트된다.
그 다음 2번 코드,
여기서 user는 @ObservedObject로 선언돼 있다.
이 말은, 이 user 인스턴스를
뷰가 직접 만들지 않고 밖에서 받아서 감시한다는 거다.
버튼을 누르면 user.age가 1씩 증가하고,
age는 @Published라서
값이 변하자마자 뷰가 다시 그려진다.
정리하면,
@ObservedObject는 외부에서 받은
ObservableObject를 감시하는 역할이고,
그 안에 @Published가 붙은 프로퍼티가 바뀌면
자동으로 UI 업데이트가 일어나는 구조다.
@StateObject
- 뷰에서 Observableobject 타입의 인스턴스 선언 시 사용한다.
- 뷰마다 하나의 인스턴스를 생성하며, 뷰가 사라지기 전까지 같은 인스턴스 유지한다.
- @observedobject의 뷰 렌더링 시 인스턴스 초기화 이슈 해결을 위한 방법이다.
- 매번 인스턴스가 새롭게 생성되는것처럼 외부에서 주입 받는 경우가 아닌 최초 생성 선언 시에 @Stateobject를 사용하는것이 적절한 방법이다.
@Environment
- 미리 정의되어 있는 시스템 공유 데이터
- 사용하려는 공유 데이터의 이름을 KeyPath로 전달하여 사용
- 시스템 공유 데이터는 가변하기에 var로 선언 필요
- 뷰가 생성되는 시점에 값이 자동으로 초기화됨
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Text ("Hello, world!")
•foregroundColor(colorScheme == .dark? white: black)
}
}
@Environment는
시스템에서 미리 정의해둔 공유 데이터를
뷰에서 꺼내 쓸 때 쓰는 거다.
쉽게 말하면,
앱 전역에서 이미 갖고 있는
설정 값이나 환경 값 같은 걸 뷰 안에서
바로 가져다 쓰는 방식이라고 보면 된다.
코드 보면,
@Environment(\.colorScheme)라고 되어 있는데
이게 시스템에서 제공하는
현재 색상 모드(다크/라이트)를
가져오는 키패스다.
var colorScheme에 이 값이
자동으로 들어온다.
중요한 건,
@Environment로 받은 값은 뷰가
생성되는 순간 자동으로 초기화되고,
그 값이 바뀌면 뷰도 같이 다시 그려진다는 거다.
그래서
위 코드에선 colorScheme이
.dark면 글자를 하얗게,
아니면 검정색으로 보여주고 있다.
@EnvironmentObject
- Observableobject를 통해 구현된 타입의 인스턴스를 전역적으로 공유하여 사용하도록 도와준다.
- 앱 전역에서 공통으로 사용할 데이터를 주입 및 사용한다.
[1번 코드]
class Info: ObservableObject {
@Published var age = 10
}
[2번 코드]
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
MainView()
•environmentObject (Info())
}
}
}
[3번 코드]
struct MainView: View {
@EnvironmentObject var info: Info
var body: some View {
Button(action: {
self.info.age += 1
}) {
Text ("Click Me for plus age")
}
SubView()
}
}
@EnvironmentObject는
쉽게 말해서 ObservableObject 인스턴스를
앱 전역에서 공유해서 쓰게 해주는 도구다.
@ObservedObject처럼 특정 뷰에만
직접 주입하는 게 아니라,
아예 환경에 심어두고 필요한 뷰
어디서든 꺼내서 쓸 수 있는 구조라고 보면 된다.
먼저 1번 코드부터 보면,
Info라는 클래스가 있고,
ObservableObject를 채택했다.
age 프로퍼티에 @Published가 붙어 있어서
값이 변하면 변경 알림을 발행한다.
그 다음 2번 코드,
여기서 MainView()를 만들 때
.environmentObject(Info())를 붙였다.
이게 핵심인데,
이렇게 하면 Info 인스턴스가 환경에 주입돼서,
앱 내의 어떤 뷰에서든
@EnvironmentObject var info: Info만 선언하면
바로 접근할 수 있게 된다.
마지막 3번 코드,
여기서 info를 @EnvironmentObject로 선언했기 때문에,
앞에서 주입한 Info 인스턴스를
바로 쓸 수 있다.
버튼을 누르면 age 값이 1씩 증가하고,
이 값은 전역적으로 공유되기 때문에
다른 뷰에서도 같은 Info 인스턴스를
참조하고 있으면 값이 동시에 업데이트된다.
정리해보면 아래와 같다.
- @EnvironmentObject → 전역 공유 데이터 접근
- .environmentObject()로 루트 쪽에서 한 번 주입하면, 하위 뷰 어디서나 꺼내 쓸 수 있음
- 값이 바뀌면 연결된 모든 뷰가 자동 업데이트됨
'IOS > Swift-TIL' 카테고리의 다른 글
| [Swift-TIL] SwiftUI 그리고 MVVM (0) | 2025.08.26 |
|---|---|
| [Swift-TIL] SwiftUI의 View Layout 결정 원리 (5) | 2025.08.09 |
| [Swift-TIL] HIG 어떻게 읽어야 하며 어떻게 공부하는가? (2) | 2025.08.09 |
| [Swift-TIL] HIG는 무엇이며 왜 읽어야 하는가? (6) | 2025.08.09 |
| [Swift-TIL] NavigationController가 있는 맛보기 앱 만들어보기 (3) | 2025.08.09 |