SWIFTUI/Grammar

SwiftUI : @State, @ObservedObject / Struct를 Class로

서근 2021. 1. 25. 06:43
반응형

 

@State

왜 @State는 오직 Struct에서만 작동을 하는가?

앞전 게시물에서 수없이 나왔던 @StateStruct에 대해서 알아보겠습니다.

 

SwiftUI에서 가장 기본이 되고 많이 사용하는 것들이니 꼭 알아두시길 바랍니다. 

 

만약 사용자가 버튼을 누르거나 스크롤을 하거나 텍스트에 상자를 입력했다고 치면, 그 특정 행동은 그 State 즉 상태를 변경합니다.  그 이후에 일어날 일은 State가 변경되면 자동으로 변환시켜주는 일을 합니다. 사용자 인터페이스를 업데이트하는 것이죠.

 

그렇다면 어떻게 이렇게 할 수 있을까요? View를 사용할 때 ContentView가 실제로 View 프로토콜을 준수한다는 것을 기억해야 합니다. Body속성을 작성하죠. 이것이 View 프로토콜의 유일한 요구 사항입니다.

 

상태를 변경할 때마다 body속성이 재설정 됩니다. 뷰 자체가 다시 렌더링 되는 것이죠. 따라서 State를 변경 할 때마다 항상 새로운 View에서 렌더링 된다는 점을 기억하시면 됩니다. 그리고 사용자가 그것을 보게 되는 것이죠. 

 

사용자가 스위치를 켜고 끄는 것처럼 상태를 변경할 때 그 값을 State안에 넣으면 뷰를 렌더링 하게 됩니다.

 

이것을 사용하기 위해서는 @StateStruct를 필수적으로 사용해야 합니다. Strcut 내부의 값이 전체 Struct를 변경할 때마다 전체 구조체가 자동적으로 변경됩니다. 마치 이름이나 성에 대한 키를 입력할 때마다 새롭게 적용되게 해 주는 것처럼 말이요. 아래 예시를 보며 확인해보세요.

State 사용하기

우선, 사용자의 성과 이름을 지정하는 Struct(구조체)를 만들겠습니다.

struct User {
    var fristName = ""
    var lastName = ""
}

이제 ContentView에서 @State속성을 만들고, 아래와 같이 TextTextField를 만들어 주겠습니다.

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가 변경되면 viewreload하게 됩니다.

 

반대로 ClassStruct @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에 추가하는 등의 작업을 수행하면 속성이 변경되고 SwiftUIbody뷰의 속성을 다시 return 합니다.

 

프로퍼티를 수정하는 Struct method에 대해 mutating 키워드를 사용하는 방법을 기억하시나요? 이유는 우리가 Struct의 프로퍼티를 변수로 만들지만, Struct 자체가 상수(constant)이면 속성을 변경할 수 없기 때문입니다.

 

SwiftUI는 프로퍼티가 변경될 때 전체 Strcut를 제거하고 재생성할 수 있어야 하며, constant(상수) struct에서는 불가능합니다.

 

Classmutating 키워드가 필요하지 않습니다. 왜냐하면 Class인스턴스가 상수 Swift로 표시되어 있더라도, 변수 속성을 수정할 수 있기 때문입니다.

 

이제 UserClass이기 때문에 @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 wrapperObservableObject프로토콜에 적합한 유형에만 사용할 수 있으며 실제로 해결하기도 쉽습니다.

 

그렇기에 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에서 보여준 결괏값이랑 같습니다. 

 

 

읽어주셔서 감사합니다🤟