SwiftUI : @State, @ObservedObject / Struct를 Class로
@State
왜 @State는 오직 Struct에서만 작동을 하는가?
앞전 게시물에서 수없이 나왔던 @State
와 Struct
에 대해서 알아보겠습니다.
SwiftUI
에서 가장 기본이 되고 많이 사용하는 것들이니 꼭 알아두시길 바랍니다.
만약 사용자가 버튼을 누르거나 스크롤을 하거나 텍스트에 상자를 입력했다고 치면, 그 특정 행동은 그 State
즉 상태를 변경합니다. 그 이후에 일어날 일은 State
가 변경되면 자동으로 변환시켜주는 일을 합니다. 사용자 인터페이스를 업데이트하는 것이죠.
그렇다면 어떻게 이렇게 할 수 있을까요? View
를 사용할 때 ContentView
가 실제로 View
프로토콜을 준수한다는 것을 기억해야 합니다. Body
속성을 작성하죠. 이것이 View 프로토콜의 유일한 요구 사항입니다.
상태를 변경할 때마다 body
속성이 재설정 됩니다. 뷰 자체가 다시 렌더링 되는 것이죠. 따라서 State
를 변경 할 때마다 항상 새로운 View에서 렌더링 된다는 점을 기억하시면 됩니다. 그리고 사용자가 그것을 보게 되는 것이죠.
사용자가 스위치를 켜고 끄는 것처럼 상태를 변경할 때 그 값을 State
안에 넣으면 뷰를 렌더링 하게 됩니다.
이것을 사용하기 위해서는 @State
와 Struct
를 필수적으로 사용해야 합니다. Strcut
내부의 값이 전체 Struct
를 변경할 때마다 전체 구조체가 자동적으로 변경됩니다. 마치 이름이나 성에 대한 키를 입력할 때마다 새롭게 적용되게 해 주는 것처럼 말이요. 아래 예시를 보며 확인해보세요.
State 사용하기
우선, 사용자의 성과 이름을 지정하는 Struct
(구조체)를 만들겠습니다.
struct User {
var fristName = ""
var lastName = ""
}
이제 ContentView
에서 @State
속성을 만들고, 아래와 같이 Text
와 TextField
를 만들어 주겠습니다.
struct ContentView: View {
@State private var user = User()
var body: some View {
VStack {
Text("당신의 이름은 \(user.fristName)\(user.lastName) 입니다")
.font(.title)
.fontWeight(.bold)
.padding(30)
List {
Section(header: Text("이름을 입력하세요").font(.headline)) {
TextField("성", text: $user.fristName)
TextField("이름", text: $user.lastName)
}
}
}
}
}
@ObservedObject / @Published
@State
특정 view에서만 사용하는 프로퍼티
@ObservedObject
복잡한 프로퍼티(여러 프로퍼티나 메서드가 있거나, 여러 view에서 공유할 수 있는 커스텀 타입이 있는 경우)
- String이나 integer 같은 간단한 로컬 프로퍼티 대신 외부 참조 타입을 사용한다는 점을 제외하면 @State와 매우 유사.
- @ObservedObject와 함께 사용하는 타입은 ObservableObject프로토콜을 따라야 함.
- @Observed object가 데이터가 변경되었음을 view에 알리는 방법은 여러 가지가 있지만 가장 쉬운 방법은 @Published 프로퍼티 래퍼를 사용하는 것. = SwiftUI에 view reload를 트리거.
class UserSettings: ObservableObject {
//@ObervedObjet를 사용하기위해 @Published를 할당
@Published var score = 0
}
struct ContentView: View {
//@state를 지우고 @ObervedObject로 바꿔줌
@ObservedObject var settings = UserSettings()
var body: some View {
VStack {
Text("나의 점수는 \(settings.score)점 입니다.")
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
}
}
}
score
에 @Published
가 붙었기 때문에 이 score
가 변경되면 view
를 reload
하게 됩니다.
반대로 Class
를 Struct
@State
로 해주고 싶다면,
struct UserSettings {
var score = 0
}
struct ContentView: View {
@State var settings = UserSettings()
var body: some View {
VStack {
Text("나의 점수는 \(settings.score)점 입니다.")
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
}
}
}
더 자세한 사용법은 아래에서 확인해 보도록 하겠습니다.
SwiftUI에서 Struct를 Class로 바꾸게 되면?
이전에 class와 struct의 차이점을 살펴봤는데, 두 가지의 중요한 차이점이 있었습니다.
첫 번째
struct
는 항상 고유한 소유자를 갖는 반면, class
는 여러 가지가 동일한 값을 가리킬 수 있다.
두 번째
상수 클래스의 프로퍼티를 변경할 수 있기 때문에, 프로퍼티를 변경하는 메서드보다 먼저 해당 클래스에 mutating
키워드가 필요하지 않다.
위에서 사용한 struct User
를 class User
로 바꿔주고 싶습니다. Xcode
에서 class
로 코드를 수정하면 작동은 되지만, 텍스트 필드에 입력값을 수정할 때마다 값이 변하지 않는다는 것을 확인할 수 있습니다.
@State
를 사용할 때, 사용자는 SwiftUI
에게 속성 변경을 감시하도록 요청합니다.
따라서 문자열을 변경하고 Bool
값을 뒤집고 Array
에 추가하는 등의 작업을 수행하면 속성이 변경되고 SwiftUI
가 body
뷰의 속성을 다시 return
합니다.
프로퍼티를 수정하는 Struct method에 대해 mutating 키워드를 사용하는 방법을 기억하시나요? 이유는 우리가 Struct
의 프로퍼티를 변수로 만들지만, Struct
자체가 상수(constant)이면 속성을 변경할 수 없기 때문입니다.
SwiftUI는 프로퍼티가 변경될 때 전체 Strcut를 제거하고 재생성할 수 있어야 하며, constant(상수) struct
에서는 불가능합니다.
Class
는 mutating
키워드가 필요하지 않습니다. 왜냐하면 Class
인스턴스가 상수 Swift
로 표시되어 있더라도, 변수 속성을 수정할 수 있기 때문입니다.
이제 User
가 Class
이기 때문에 @State
는 아무것도 인식하지 못하고 보기를 다시 로드할 수 없습니다. 클래스 내부의 값은 변화하고 있지만 @State
는 이러한 값을 모니터링하지 않습니다.
즉, 클래스 내부의 값은 변화하고 있지만 이러한 변화를 반영하기 위해 뷰가 다시 로드되지 않는다는 것입니다.
그래서 SwiftUI에서 Class를 그대로 사용하고 싶을 땐?
@Published
/ @ObservedObject
를 사용해야 합니다. class User
내부에 @Published
를 각각 할당해주겠습니다.
class User {
@Published var firstName = ""
@Published var lastName = ""
}
@Published
는 @State
의 절반 정도이라고 보면 됩니다. (그럼 나머지 절반이 있어야 작동하겠죠?)
SwiftUI
는 두 가지 속성 중 하나가 변경될 때마다 리로드 해야 하는 것을 감시한다고 했었습니다. 이러한 View
는 어떤 클래스가 이러한 보고를 보낼 수 있는지 어떻게 알 수 있을까요?
그것은 @State
의 나머지 절반인 @ObservedObject
라는 또 다른 property wrapper
입니다.
만약 ContentView
내에 @State private var user = User()
의 @State
를 지우고 @ObervedObject
를 삽입한다면 컴파일이 되지 않습니다.
@ObservedObject property wrapper
는 ObservableObject
프로토콜에 적합한 유형에만 사용할 수 있으며 실제로 해결하기도 쉽습니다.
그렇기에 Class User
에 콜론(:
) 후 , ObservableObject
를 추가해줘야 합니다.
import SwiftUI
class User: ObservableObject {
@Published var fristName = ""
@Published var lastName = ""
}
struct ContentView: View {
//@state를 지우고 @ObservedObject으로 수정
@ObservedObject private var user = User()
var body: some View {
VStack {
Text("당신의 이름은 \(user.fristName)\(user.lastName) 입니다")
.font(.title)
.fontWeight(.bold)
.padding(30)
List {
Section(header: Text("이름을 입력하세요").font(.headline)) {
TextField("성", text: $user.fristName)
TextField("이름", text: $user.lastName)
}
}
}
}
}
최종 화면은 Struct
에서 보여준 결괏값이랑 같습니다.
읽어주셔서 감사합니다🤟