SwiftUI에서 PropertyWrapper 사용해보기

marisol👩🏻‍💻·2022년 6월 23일
0

👩🏻‍💻 UIKit vs SwiftUI

PropertyWrapper에 대해서 얘기하기 전에 잠깐 UIKit과 SwiftUI를 비교해보자면,

UIKit은 사용자 인터페이스 레이아웃을 설계하고, 필요한 동작을 구현한다면
SwiftUI는 화면을 구성하는 요소에 대한 레이아웃과 모양에 대한 디테일한 내역을 직접 설계하지 않고,
단순하면서도 직관적인 구문을 이용해 화면을 구성하게 해준다고 한다.

이렇다고 한다.

예를 들어서, 어떤 버튼이 눌렸을 때 배경색을 바꾸는 프로세스를 보면 둘이 확실히 다른 것 같다.

UIKit

  • Button 눌림
  • 그 Button과 연결된 액션 (tapButton()) 호출됨
  • view의 백그라운드 컬러를 직접 변경
    ==> Event Driven (뷰 이벤트 발생에 따라 구성)

SwiftUI

  • Button 눌림
  • isButtonTapped라는 Bool 타입의 프로퍼티의 값이 바뀜
  • isButtonTapped의 값에 따라 미리 선언된대로 view가 다시 그려짐
    ==> Data Driven (뷰가 데이터 중심으로 구성됨)

SwiftUI는 선언적 구문(Declarative)을 사용하기 때문에, 사용자의 인터페이스의 기능을 명시하기만 하면 된다고 한다.
그러면 코드가 간단하고 가독성이 향상되어 시간이 절약되고 유지 관리가 용이하다 등등의 장점이 있는 것 같다..👀

애플 말로는 SwiftUI가

A modern way to "declare user interfaces" for any Apple platform.
Create beatiful, dynamic apps faster than ever before.

하다고 한다 (여기서 핵심은 "declare user interfaces" 인듯!)

그리고 무엇보다.. SwiftUI는 시뮬레이터에 빌드하고 실행하지 않아도 previewer canvas 내에서 앱을 실행하고 테스트할 수 있는 장점이 있다! 앱이 커지면 빌드 시간도 오래걸릴텐데.. 빌드하고 실행하지 않아도 프리뷰에서 실행해볼 수 있다는건 정말 큰 장점이 될 것 같다.

그리고 SwiftUI는 대부분 구조체와 프로토콜로 이루어져 있는데, 그 이유에 대해서도 공부했다.

SwiftUI는 상태 값이 변경되면 그 값에 관련된 뷰를 갱신하는데, 다시 뷰를 갱신할 때마다 값타입의 인스턴스를 연산하는 것이 참조타입의 인스턴스를 연산하는 것이 더 빠르기 때문에 구조체를 많이 쓴다고 한다.

그리고 구조체로 많이 이루어져있다보니 상속의 기능을 위해 프로토콜도 같이 많이 사용된 것 같다.

🤔 Property Wrapper in SwiftUI

위에서 SwiftUI가 Data Driven하다고 말했는데, Property Wrapper 그 Data Driven을 돕는 친구라고 한다.
(@State, @Published, @EnvironmentObject, @StateObject, @Binding... 등등)

📌 @State

: 정보가 변할 때, 관련된 뷰를 갱신한다

어떤 정보가 변할 때마다 모든 뷰를 다 갱신해버리면 🤯 정말 비효율적일 것 같다. 그래서 @State를 쓰면, @State 키워드가 붙어서 정의된 프로퍼티의 정보가 변할 때, 그 프로퍼티와 관련된 뷰만을 갱신해줄 수 있다고 한다.

근데 뷰와 관련된 변수를 항상 뷰 내부에서만 관리할까? 🧐
❌ 그건 아님. ViewModel에 저장하는 경우가 많은데,
그럼 대체 뷰 외부에 있는 프로퍼티의 값이 변경되었을 때 어떻게 그걸 추적해서 뷰를 갱신할까?

그걸 도와주는 Property Wrapper가 @ObservedObject, @StateObject, @EnvironmentObject 라고 한다!

그래서 실제로 작은 프로젝트에서 @ObservedObject@EnvironmentObject Property Wrapper를 사용해서 뷰 외부의 프로퍼티 값이 변경되었을 때 이를 관찰하고 있는 뷰가 자동으로 갱신되게 해볼거다.

📌 @EnvironmentObject

아래 화면을 SwiftUI로 만들건데
State는 각각의 ToggleView마다 따로 구성할 거고, Environment는 한꺼번에 4개의 ToggleView가 모두 작동하도록 만들거다.

먼저 @EnvironmentObject 써봐야지

"부모나 조상 뷰에서 제공하는 Observable한(관찰 가능한) 객체의 Property Wrapper"라고 정의되어 있다.

OverView를 보면,
"observable한 객체가 변경될 때마다 현재의 뷰를 무효화 한다. 프로퍼티를 environment object로 선언할 경우, environmentObject(_:) modifier를 호출해서 상위 뷰에서 해당 모델 객체를 설정해야한다."
고 되어 있다.

가장 큰 View인 ContentView 안에 ToggleView 4개가 들어가 있는 형태라서, ContentView에서 environmentObject(_:)를 호출해주었다. 그러면 ToggleView에서도 해당 뷰 외부에 있는 프로퍼티 값을 사용할 수 있다.
여기서 모델 객체는 ViewModel class이다.

그 전에 ViewModel이
1️⃣ ObservableObject 프로토콜을 채택하고
2️⃣ 외부에서 관리할 프로퍼티에 @Published 키워드를 붙여줘야 하는데,
ObservableObject는 객체가 변경되기 전에 내보내는 publisher가 있는 객체 유형이고,
@Published 키워드를 붙이면 그 프로퍼티의 Publisher가 생성된다.

==> 값이 변경되는 프로퍼티를 뷰의 외부에 선언해준 것!

class ViewModel: ObservableObject {
	@Published var isEnvironmentToggleOn: Bool = false
}

@main
struct Experiment_SwiftUIApp: App {
	var viewModel = ViewModel()
    var body: some Scene {
    	WindowGroup {
        ContentView().environmentObject(viewModel)
        }
    }
}

그리고 ToggleView에서는 ViewModel의 isEnvironmentToggleOn의 값에 따라 뷰가 변경될 수 있도록 선언해두었다.

struct ToggleView: View{
	@State private var isStateToggleOn: Bool = false // stateToggle은 view 내부에서 관리됨
    @EnvironmentObject var viewModel: ViewModel
    
    var body: some View {
    	ZStack {
        	RoundedRectangle(cornerRadius: 20)
            	.stroke(lineWidth: 2)
                .foregroundColor(.red)
            
            Vstack {
            	Text(isStateToggleOn ? "State On" : "State Off")
                
                Text(viewModel.isEnvironmentToggleOn ? "Environment On" : "Environment Off")
                
                Toggle("State", isOn: $isStateToggleOn)
                	.padding(.all)
                Toggle("Environment", isOn: $viewModel.isEnvironmentToggleOn)
                	.padding(.all)
            }
        }
        .aspectRatio(contentMode: .fit)
        .padding()
    }
}

그러면 ContentView에서는 ToggleView를 4개 넣어주면 된다

struct ContentView: View {
	var body: some View {
    	LazyVGrid(columns: [GridItem(), GridItem()]) {
        	ToggleView()
            ToggleView()
            ToggleView()
            ToggleView()
        }
    }
}

📌 @ObservedObject

"Observable한 객체를 subscribe (관찰하겠다고 등록? 하는 느낌인듯..) 하고, 그 객체가 변경될 때마다 view를 무효화하는 Property Wrapper" 라고 정의되어 있다.

Observable한 객체인 ViewModel을 관찰하고, 그 객체가 변경될 때마다 view를 무효화하고 다시 그리겠다는 의미인 것 같다.

struct ContentView: View {
	var viewModel = ViewModel()
    
    var body: some View {
    	LazyVGrid(columns: [GridItem(), GridItem()]) {
        	ToggleView(viewModel: viewModel)
            ToggleView(viewModel: viewModel)
            ToggleView(viewModel: viewModel)
            ToggleView(viewModel: viewModel)
        }
    }
}

각각의 ToggleView가 생성될 때 ContentView의 @ObservedObject를 파라미터로 받고, ToggleView 내부에서 그 값에 따라 View를 표시하는 것 같다.

그러면 State Toggle은 각각의 ToggleView에서 따로 끄고 켤 수 있고, Environment Toggle은 하나의 뷰에서 켜면 나머지 뷰에서도 다 켜지도록 잘 된다!


참고자료

0개의 댓글