개요
이 글은 SwiftUI의 EnvironmentValues를 중심으로 다룬다
프로젝트를 리팩토링하며 SwiftUI의 기능을 적극 활용하고자 고민하던 중, Reusable View 영상을 통해 재사용 가능한 뷰 설계를 하는 법에 대해 배울 수 있었다
해당 영상에서는 SwiftUI에서 뷰에 대해 리팩토링하는 여러가지 기법들을 설명하는데, 유독 EnvironmentValues가 눈에 들어왔다
왜냐하면 굳이 왜 사용하는지 잘 이해가 안갔었기 때문이다
그렇기 때문에 좀 더 사용의 필요성을 확 느낄 수 있는 예시들이 글의 대부분을 차지한다
그 글에서는 예시를 크게 두 가지로 분류했는데, 하나는 단순 값으로 이용이고, 나머지 하나는 상태를 통한 이용이다
전체적인 글은 다음 순서로 진행된다:
- Environment와 EnvironmentValues
- 예시를 통한 이해
- @Entry (WWDC24)
Environment와 EnvironmentValues
- https://developer.apple.com/documentation/swiftui/environment
- https://developer.apple.com/documentation/swiftui/environmentvalues/
@Environment는 뷰의 환경에서 값을 읽어오는 데 사용되는 프로퍼티 래퍼다
이를 뷰에서 사용하려면 .environment 수정자를 통해 전달받아야 한다
이를 통해 상위 뷰에서 설정된 데이터를 하위 뷰에서 손쉽게 접근할 수 있다
Environment는 다음 상황에서 유용하다:
- 코드 간결화: Prop Drilling을 없애고, 중복 코드를 최소화.
- 유연성 제공: 기본값을 설정하고, 필요 시 개별 뷰에서 오버라이드 가능.
- 확장성 강화: extension View로 수정자를 추가해 재사용성과 가독성 향상.
- 일관성 유지: 앱 전역에서 설정을 통일적으로 관리.
EnvironmentValues는 환경 값을 저장하는 구조체로, @Observable 클래스뿐만 아니라 일반 구조체나 클래스를 확장해 커스텀 환경 값을 정의할 수 있다
지금까지 나는 .environment 수정자로 @Observable 객체만 전달 후 사용하기만 했었지만, EnvironmentValues를 활용하면 더 체계적인 환경 값 관리가 가능하단 것을 깨달게 되었다
EnvironmentValues 커스텀 키 사용
다음과 같은 단계를 거치면 EnvironmentValues를 사용할 수 있다
- EnvironmnetKey 정의
- EnvironmentValues 확장
- View 확장(선택)
// 1. EnvironmentKey 정의
private struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .light
}
// 2. EnvironmentValues 확장
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// 3. 편의를 위한 View 확장
extension View {
func theme(_ theme: Theme) -> some View {
environment(\\.theme, theme)
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
ChildView()
.theme(.dark)
}
}
}
struct ChildView: View {
@Environment(\\.theme) var currentTheme
var body: some View {
Text("현재 테마: \\(currentTheme == .dark ? "다크" : "라이트")")
.foregroundColor(currentTheme == .dark ? .white : .black)
}
}
extension View에서 environment(_:_:)는 View에 수정자인 .environment로 환경 값을 키에 등록하는 것과 같다
// 3. 편의를 위한 View 확장
extension View {
func theme(_ theme: Theme) -> some View {
environment(\\.theme, theme)
}
}
ChildView()
.environment(\\.theme, theme)
예시를 통한 이해
WWDC23에서 @Observable이 발표되고, 나는 주로 @Observable 객체를 .environment로 뷰에 전달해 상태 변화를 관리해왔다
그래서 EnvironmentValues도 상태 관리에만 유용할 거라 생각했지만, 그게 전부가 아니었다
상태가 아닌 단순 값도 EnvironmentValues로 유용하게 활용될 수 있는데, 아래 예시를 통해 이를 확인할 수 있다
뷰 렌더링 컨텍스트 관리
예를 들어, 동일한 ContentCard 뷰를 서로 다른 스타일로 렌더링하고 싶다면 어떻게 해야 할까?
가장 간단한 방법은 ViewMode를 정의하고, viewMode 매개변수를 뷰에 추가해 조건부로 처리하는 것이다:
enum ViewMode {
case normal, preview, compact
}
struct ContentCard: View {
let viewMode: ViewMode
let title: String
let description: String
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(viewMode == .compact ? .subheadline : .headline)
if viewMode != .compact {
Text(description)
.font(.body)
.lineLimit(viewMode == .preview ? 2 : nil)
}
if viewMode == .normal {
Button("자세히 보기") { }
.padding(.top, 8)
}
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
ContentCard(viewMode: .normal, title: "일반 모드 카드", description: "...")
하지만 EnvironmentValues를 사용하면 더 효율적인 방법이 가능하다
이를 일반 매개변수 전달 방식과 비교하면 다음과 같은 장점이 있다:
계층적 오버라이드
뷰 계층의 각 수준에서 값을 유연하게 오버라이드할 수 있다
예를 들어, 앱 전체는 .normal로 설정하되 특정 섹션에서만 .compact로 변경 가능하다
ContentCard(title: "제목", description: "설명")
.environment(\\.viewMode, .compact) // 특정 뷰에만 .compact 적용
중첩된 뷰에 자동 전파
하위 뷰가 viewMode에 접근해야 할 때, 매개변수를 일일이 전달할 필요가 없다(EnvironmentValues를 사용 -> 코드 간소화)
View 확장을 수행했다면, 기본값으로 .compact가 전달된다
VStack {
// 직접 전달
ContentCard(viewMode: .normal, title: "제목", description: "설명")
// EvironmentValues 사용
ContentCard(title: "제목", description: "설명")
}
.environment(\\.viewMode, .compact)
코드 유지보수 용이성
새로운 렌더링 옵션(예: 다크 모드, 접근성 크기)이 추가될 때, 모든 뷰의 생성자를 수정할 필요 없이 EnvironmentValues에만 정의하면 된다
예시 코드
1. 환경 키, 값 정의 및 뷰 확장
// EnvironmentKey 정의
private struct ViewModeKey: EnvironmentKey {
static let defaultValue: ViewMode = .normal
}
// EnvirnmentValues 정의
extension EnvironmentValues {
var viewMode: ViewMode {
get { self[ViewModeKey.self] }
set { self[ViewModeKey.self] = newValue }
}
}
// View 확장
extension View {
func cardRender(_ viewMode: ViewMode) -> some View {
environment(\\.viewMode, viewMode)
}
}
2. 뷰에서 환경 값 사용
기존에 인자로 전달받던 값을 환경을 사용하도록 수정한다
struct ContentCard: View {
@Environment(\\.viewMode) private var viewMode
let title: String
let description: String
...
사용 예시
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
ContentCard(
title: "일반 모드 카드",
description: "이것은 일반 모드에서 표시되는 카드입니다. 모든 콘텐츠가 표시됩니다."
)
ContentCard(
title: "미리보기 모드 카드",
description: "미리보기 모드에서는 설명이 2줄로 제한됩니다. 버튼은 표시되지 않습니다."
)
.cardRender(.preview)
ContentCard(
title: "컴팩트 모드 카드",
description: "컴팩트 모드에서는 제목만 표시됩니다."
)
.cardRender(.compact)
}
.padding()
}
}
상태와 연동
SwiftUI의 List는 편집 모드(editMode)를 지원하며, 이는 EnvironmentValues에서 editMode 키로 제공되는 환경 값이다
EditMode가 활성화되면 List는 삭제(delete)와 이동(move) 기능을 기본적으로 제공한다
이를 EnvironmentValues로 관리하면 다음과 같은 장점이 있다:
- 코드 간결화: editMode를 각 뷰에 Prop으로 전달하지 않고 환경 값으로 관리.
- 유연성 제공: List와 하위 뷰에서 편집 모드를 일관되게 제어할 수 있으며, 필요 시 개별 뷰에서 추가 로직(예: 삭제 버튼 커스터마이징)을 쉽게 구현 가능.
예시 코드
@State로 editMode를 정의하고 .environment(\.editMode, $editMode)를 통해 List와 하위 뷰(ListRow)에 주입한다
Binding을 사용해 상태를 연동하므로, 상위 뷰의 버튼 클릭으로 editMode가 .active와 .inactive 간 전환될 때 List의 삭제/이동 기능과 하위 뷰의 스타일이 즉각 반영된다
이처럼 Prop 전달 없이도 상태를 환경 값으로 관리하는 장점을 얻을 수 있다
struct ContentView: View {
@State private var editMode: EditMode = .inactive
var body: some View {
VStack {
Button(action: {
editMode = (editMode == .inactive) ? .active : .inactive
}) {
Text(editMode == .inactive ? "Edit" : "Done")
}
.padding()
List {
ForEach(0..<10) { index in
ListRow(index: index)
}
.onDelete { indices in }
}
// List와 하위 뷰에 환경 값 주입
.environment(\\.editMode, editMode)
}
}
}
@Entry(WWDC24)
WWDC24에서 발표한 내용에 따르면 아래와 같이 기존에 환경 키, 값을 등록하던 방식을 매크로로 간단하게 처리할 수 있도록 @Entry가 생겨났다
enum ViewMode {
case normal, preview, compact
}
// EnvironmentKey 정의
private struct ViewModeKey: EnvironmentKey {
static let defaultValue: ViewMode = .normal
}
// EnvirnmentValues 정의
extension EnvironmentValues {
var viewMode: ViewMode {
get { self[ViewModeKey.self] }
set { self[ViewModeKey.self] = newValue }
}
}
위 같은 과정을 아래처럼 간소화시킬 수 있다
// Entry 매크로로 간소화
extension EnvironmentValues {
@Entry var viewMode: ViewMode = .normal
}
'iOS+ > SwiftUI' 카테고리의 다른 글
@Namespace와 .matchedGeometryEffect (0) | 2024.07.21 |
---|