SwiftUI DataModel Property Wrapper
iOS 16 이하
@State
- Struct 내의 프로퍼티를 수정할 수 있게 만듬
- @State 로 선언된 변수를 변경 했을때, 이 변수가 포함된 body내의 모든 View를 다시 그림
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import SwiftUI
struct ContentView: View {
@State var number = 0
var body: some View {
VStack {
Text("\(number)")
Button {
number += 1
} label: {
Text("Plus")
}
}
}
}
@Binding
- 기본적으로 전달받은 곳에서 State 변수 수정 불가
- 전달 받은 곳에서 수정하고 싶다면 Binding으로 변환해서 전달해주기
- 넘겨주는 곳에서 $, 받는곳에서 @Binding
- 이를 2-way binding 이라고함
$(Projected value)
- @State, @Binding 같은 프로퍼티 래퍼는 Projected Value라는 기능이 있음
- $를 이용해서 프로퍼티 래퍼를 통해 저장된 값을 지정된 방법으로 반환 시켜줌
이때 반환되는 값은 아무타입이나 상관없는데 이 기능을 통해 @State 프로퍼티 래퍼는
1
$(Projected Value)를 통해 Binding<T> 으로 변환해서 반환시켜줌
데이터 기준으로 의존성을 표기 하기에 관계는 아래 같다.
(클린 아키텍처 기준으론 반대로 되어 있는 거라, 기분이 안좋은데 일단 기초개념만 참고하자)
1
ContentView (데이터 소유) -> SubView (데이터 참조)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView: View {
@State var number = 0
var body: some View {
VStack {
Text("ContentView")
Button("Button") {
number += 1
}
Text("number: \(number)")
Divider()
SubView(number: $number)
}
}
}
1
2
3
4
5
6
7
8
9
10
struct SubView: View {
@Binding var number: Int
var body: some View {
Text("SubView")
Button("Button") {
number += 1
}
Text("number: \(number)")
}
}
.constant
- Binding
형태로 반환하기 위해 사용, - 값 수정이 불가능 하기에 상수라는 뜻의 constant 를 사용함
- 개발중 또는 #Preview 에서 임시 바인딩 객체가 필요할때 사용
1
2
3
.constant("String")
.constant(100)
.constant(true)
ObservableObject, @Published, @ObservedObject
- @State 는 값타입(Int, String,Bool,Enum,Struct) 만 감지가능
- 참조타입 class 는 감지 불가능
- Class 참조타입을 감지하기 위해 위에 3가지 필요함
- ObserveableObject 는 프로토콜이고, @Published, @ObservedObject 는 프로퍼티래퍼다.
- ObservableObject 프로토콜 : 감지되어야 할 클래스에 붙여줌
- @Published : 클래스내 감지되어야할 프로퍼티에 붙여줌
- @ObservedObject: 사용할 ObservableObject 클래스 인스턴스 변수에 생성시 붙여줌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class NumberCounter: ObservableObject {
@Published var number: Int = 0
func increaseNumber() {
number += 1
}
}
struct ContentView: View {
@ObservedObject var numberCounter = NumberCounter()
var body: some View {
VStack {
Text("\(self.numberCounter.number)")
Button {
self.numberCounter.increaseNumber()
} label: {
Text("Plus")
}
}
}
}
@StateObject
- 데이터를 하위에 보내려면 데이터 소유주가 날라가면 안됨
- ContentView 에서 ChildView 를 생성해서 사용하고 있음
- ChildView 는 NumberCounter 라는 계산 클래스를 생성해서 가지고 있음
- @State 값 변경시 해당 변수를 포함한 body 는 리로드 된다 이때 @ObservedObject 클래스가
있다면 새로 객체가 생성되며 기존값이 사라진다. 이때 @StateObject 로 클래스 객체를 정의하면
재 갱신시 새로 생성하지 않고 기존껄 가지고 있는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class NumberCounter: ObservableObject {
@Published var number: Int = 0
func increaseNumber() {
number += 1
print("number: ", number)
}
}
struct ChildView: View {
@StateObject var numberCounter = NumberCounter()
var body: some View {
VStack {
Text("ChildView")
Text("\(self.numberCounter.number)")
Button {
self.numberCounter.increaseNumber()
} label: {
Text("Plus")
}
}
}
}
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack (alignment: .center) {
Text("ContentView")
TextField("글자를 입력해주세요", text: $text)
.frame(width: 150, height: 50)
Divider()
ChildView()
}
}
}
- ContentView 에서 NumberCounter 클래스 객체를 ObservedObject 로 생성해서
ChildView 에 프로퍼티로 전달한다. 해당 numberCounter 는 ContentView body 밖에서 생성하기 때문에 이전에 생성했던 numberCounter 값으로 ChildView를 다시 그린다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class NumberCounter: ObservableObject {
@Published var number: Int = 0
func increaseNumber() {
number += 1
print("number: ", number)
}
}
struct ChildView: View {
@ObservedObject var numberCounter : NumberCounter
var body: some View {
VStack {
Text("ChildView")
Text("\(self.numberCounter.number)")
Button {
self.numberCounter.increaseNumber()
} label: {
Text("Plus")
}
}
}
}
struct ContentView: View {
@State var text: String = ""
@ObservedObject var numberCounter = NumberCounter()
var body: some View {
VStack (alignment: .center) {
Text("ContentView")
TextField("글자를 입력해주세요", text: $text)
.frame(width: 150, height: 50)
Divider()
ChildView(numberCounter: numberCounter)
}
}
}
@EnvironmentObject
- 상속된 모든 뷰에서 공통적으로 사용할수 있게 해줌
- 필요한 차일드 뷰에서만 불러와 사용 할 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class NumberCounter: ObservableObject {
@Published var number: Int = 0
func increaseNumber() {
number += 1
}
}
struct ChildView: View {
@EnvironmentObject var numberCounter: NumberCounter
var body: some View {
VStack {
Text("\(self.numberCounter.number)")
Button {
self.numberCounter.increaseNumber()
} label: {
Text("Plus")
}
}
}
}
struct ContentView: View {
@ObservedObject var numberCounter = NumberCounter()
var body: some View {
VStack {
ChildView()
.environmentObject(numberCounter)
}
}
}
@Environment
- 시스템에 이미 세팅되어 있는 환경값을 SwiftUI 뷰에서 아무곳에서나 꺼내 쓸수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
import SwiftUI
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Text("Hello, World!")
.padding()
.background(colorScheme == .dark ? Color.black : Color.white)
.foregroundColor(colorScheme == .dark ? Color.white : Color.black)
}
}
iOS 17 이상 변경점 - XCode15 2023
변경점 요약
- @Published 프로퍼티가 변경되면 View에서 사용되는게 아니어도 전체 View가 다시 리로드 되었는데 @Observable 매크로 사용시 @Published 작성할 필요도 없고, 특히 View에 사용된 프로퍼티만 변경시 리로드 되게 감시가 효율적으로 변경됨
- ObservableObject 프로토콜 사용시에는 옵셔널과 객체들에 대한 감시가 불가능 했다.
- 기존 @StateObject와 @EnvironmentObject는 @State 와 @Environment 로 통합됨
@Observable 매크로
ObservableObject 걷어내고 @Observable 매크로로 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Observable
class NumberCounter {
var number: Int = 0
func increaseNumber() {
number += 1
print("number: ", number)
}
}
struct ContentView: View {
var numberCounter = NumberCounter()
var body: some View {
VStack {
Text("\(self.numberCounter.number)")
Button {
self.numberCounter.increaseNumber()
} label: {
Text("Plus")
}
}
}
}
@StateObject → @State 로 통합
@Observable 매크로로 변경 및 @StateObject 는 @State 로 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Observable
class NumberCounter {
var number: Int = 0
func increaseNumber() {
number += 1
print("number: ", number)
}
}
struct ChildView: View {
@State var numberCounter = NumberCounter()
var body: some View {
VStack {
Text("ChildView")
Text("\(self.numberCounter.number)")
Button {
self.numberCounter.increaseNumber()
} label: {
Text("Plus")
}
}
}
}
struct ContentView: View {
@State var text: String = ""
var body: some View {
VStack (alignment: .center) {
Text("ContentView")
TextField("글자를 입력해주세요", text: $text)
.frame(width: 150, height: 50)
Divider()
ChildView()
}
}
}
@EnvrionmentObject → @Environment 통합
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Observable
class NumberCounter {
var number: Int = 0
func increaseNumber() {
number += 1
}
}
struct ChildView: View {
@Environment(NumberCounter.self) var numberCounter
var body: some View {
VStack {
Text("ChildView")
Text("\(self.numberCounter.number)")
Button {
self.numberCounter.increaseNumber()
} label: {
Text("Plus")
}
}
}
}
struct ContentView: View {
var numberCounter = NumberCounter()
var body: some View {
VStack {
Text("ContentView")
ChildView()
.environment(numberCounter)
}
}
}
@Bindable
@ObservedObject 를 @Bindable 로 변경
데이터 소유권:
@State는 데이터를 뷰 내부에서 소유하지만, @Bindable은 외부 객체를 바인딩하여 여러 뷰와 공유할 수 있습니다.
사용 목적:
@State는 뷰 내부의 상태 관리에 적합하고, @Bindable은 외부 객체와의 데이터 동기화 및 공유에 적합합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Observable // class 에만 사용 가능
class Ramen {
var name: String
init(name: String) {
self.name = name
}
}
struct Order {
var count: Int = 10
}
struct RamenOrderView: View {
@Binding var order: Order // 상위 객체에서 받아서 사용
var body: some View {
VStack {
Text("RamenOrderView")
TextField("주문 갯수", value: $order.count, formatter: NumberFormatter())
.keyboardType(.numberPad)
.frame(maxWidth: 100)
}
}
}
struct ContentView: View {
@Bindable var ramen = Ramen(name: "신라면") // 소유권 Ramen 클래스가 가지고 있음
@State var order = Order() // 소유권 View 가 가지고 있음
var body: some View {
VStack {
Text("ContentView")
Text("상품명: \(ramen.name)")
Text("주문량: \(order.count)")
TextField("Title", text: $ramen.name)
.frame(maxWidth: 100)
Divider()
RamenOrderView(order: $order)
}
.padding()
}
}
This post is licensed under CC BY 4.0 by the author.