챌린지 1-3 · 5 Topics

🗂 여러 화면과 공유 상태

화면이 2개인데 데이터를 어떻게 공유하는가

NavigationPath sheet vs fullScreenCover @StateObject @ObservedObject @Observable (iOS 17+) @EnvironmentObject @Published TabView
시작 전에 던져볼 질문
01
화면이 2개 이상인데, 한 화면에서 바꾼 데이터를 다른 화면에서 어떻게 보는가?
검색 → SwiftUI shared state ObservableObject
02
화면을 "밀어서 이동"하는 것과 "아래에서 올라오는 모달"의 차이는 무엇인가?
검색 → SwiftUI NavigationStack vs sheet
03
앱 하단에 탭 바를 만들어서 여러 화면을 전환하려면 어떻게 하는가?
검색 → SwiftUI TabView tabItem
Topic 01

NavigationStack + NavigationPath

스택 기반 이동과 프로그래밍 방식 제어
Swift
struct RootView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List {
                NavigationLink("프로필",
                    value: "profile")
                NavigationLink("설정",
                    value: "settings")
            }
            .navigationTitle("메뉴")
            .navigationDestination(
                for: String.self
            ) { value in
                Text("\(value) 화면")
            }
        }
    }
}
생각해보기
  • NavigationLink(destination:).navigationDestination(for:)의 차이는 무엇인가?
  • NavigationPath@State로 관리하면 어떤 것이 가능해지는가?
  • 코드로 특정 화면으로 바로 이동하려면 NavigationPath를 어떻게 조작하는가?
NavigationLink(destination:)
탭하면 화면 push. 뒤로 가기는 자동. .navigationDestination(for:) 패턴 (iOS 16+)
NavigationPath
@State로 path를 관리해 코드로 화면 이동. path.append() / path.removeLast() / path.removeAll()
딥링크 처리
onOpenURL에서 NavigationPath를 조작해 특정 화면으로 바로 이동
Topic 02

sheet vs fullScreenCover

모달 종류와 선택 기준
Swift
struct ModalDemo: View {
    @State private var showSheet = false
    @State private var showFull = false

    var body: some View {
        VStack(spacing: 16) {
            Button("Sheet 열기") {
                showSheet = true
            }
            Button("FullScreen 열기") {
                showFull = true
            }
        }
        .sheet(isPresented: $showSheet) {
            Text("Sheet!")
                .presentationDetents(
                    [.medium, .large])
        }
        .fullScreenCover(
            isPresented: $showFull
        ) {
            Text("Full Screen!")
        }
    }
}
생각해보기
  • .sheet().fullScreenCover()는 시각적으로 어떻게 다른가?
  • sheet를 띄운 화면에서 dismiss하려면 어떤 환경 변수를 사용하는가?
  • .presentationDetents([.medium, .large])는 무엇을 조절하는가?
.sheet(isPresented: $show)
아래에서 올라오는 카드 형태. 뒤 화면이 살짝 보임. 가장 일반적인 모달
.fullScreenCover(isPresented: $show)
전체 화면 덮음. 카메라, 온보딩 등 완전한 새 컨텍스트를 줄 때
dismiss 환경 변수
@Environment(\.dismiss) var dismiss — 자식 View에서 모달 닫기
Topic 03

@StateObject vs @ObservedObject

소유 vs 구독의 차이
Swift
class TodoStore: ObservableObject {
    @Published var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }
}

struct ParentView: View {
    @StateObject var store = TodoStore()

    var body: some View {
        ChildView(store: store)
    }
}

struct ChildView: View {
    @ObservedObject var store: TodoStore

    var body: some View {
        List(store.items, id: \.self) {
            Text($0)
        }
    }
}
생각해보기
  • @StateObject@ObservedObject는 언제 각각 사용하는가?
  • 자식 View에서 @StateObject를 쓰면 왜 위험한가?
  • ObservableObject@Published 프로퍼티가 변경되면 어떤 일이 일어나는가?
  • class를 쓰는 이유는 무엇인가? struct로는 왜 안 되는가?
ObservableObject + @Published
class에 채택. @Published 프로퍼티가 변경되면 구독 중인 View 재렌더
@StateObject — 소유자
객체를 처음 생성하고 소유. View가 재생성되어도 객체는 유지. 최상위 View에서 사용
@ObservedObject — 구독자
외부에서 주입받아 구독. View가 재생성되면 새로운 객체로 교체될 수 있음
실수 패턴
자식 View에서 @StateObject를 사용하면 부모 재렌더 시 객체가 리셋된다
Topic 04

@Observable · @EnvironmentObject

iOS 17+ 새 방법과 환경 주입
Swift
// iOS 17+ 방식
@Observable
class UserSettings {
    var username = "개발자리"
    var isDarkMode = false
}

struct AppRoot: View {
    @State var settings = UserSettings()

    var body: some View {
        SettingsView()
            .environment(settings)
    }
}

struct SettingsView: View {
    @Environment(UserSettings.self)
    var settings

    var body: some View {
        Text(settings.username)
    }
}
생각해보기
  • @Observable 매크로(iOS 17+)를 쓰면 @Published가 왜 필요 없어지는가?
  • @EnvironmentObject는 어떤 문제를 해결하기 위해 존재하는가? (prop drilling이란?)
  • @Environment(\.dismiss)에서 \.dismiss는 어디에 정의되어 있는가?
@Observable 매크로 (iOS 17+)
class에 붙이면 ObservableObject + @Published 없이도 동작. 훨씬 단순
@EnvironmentObject
부모에서 .environmentObject() 주입. 깊은 자식이 바로 꺼내 씀. prop drilling 해결
@Environment(\.store)
@Observable + @Environment 조합 (iOS 17+). 더 명시적이고 타입 안전한 방법
Topic 05

TabView 기본 구성

tabItem · 탭별 NavigationStack
Swift
struct MainTabView: View {
    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Label("홈",
                        systemImage: "house")
                }

            SearchView()
                .tabItem {
                    Label("검색",
                        systemImage:
                            "magnifyingglass")
                }

            ProfileView()
                .tabItem {
                    Label("프로필",
                        systemImage: "person")
                }
        }
    }
}
생각해보기
  • TabView 안의 각 탭에 NavigationStack을 넣는 이유는 무엇인가?
  • TabView(selection: $selectedTab)으로 프로그래밍 방식 탭 전환이 필요한 경우는 언제인가?
  • .tabItem { }에 넣을 수 있는 View의 종류에 제한이 있는가?
.tabItem { Label("홈", systemImage: "house") }
각 탭의 아이콘과 레이블. SF Symbols 이름 사용
탭별 독립 NavigationStack
각 탭 안에 NavigationStack을 넣으면 탭 전환 시 navigation 상태가 각자 유지
Programmatic 탭 전환
TabView(selection: $selectedTab). 푸시 알림 등에서 특정 탭으로 이동할 때
Preview
메뉴
프로필
설정
Preview
Sheet 열기
FullScreen 열기
Preview
장보기
SwiftUI 공부
운동하기
Preview
개발자리
Preview
홈 화면
🏠
🔍 검색
👤 프로필
Homework

챌린지 1-3을 마쳤다면

필수
3탭 앱 만들기
TabView에 홈/검색/프로필 3개 탭 구성. 각 탭 안에 NavigationStack 배치. 홈에서 상세 화면으로 push 이동
필수
공유 데이터 모델 만들기
@Observable class로 앱 전체 상태 관리. 하나의 탭에서 데이터를 바꾸면 다른 탭에서도 반영되는지 확인
도전
메모 앱 — sheet + NavigationStack 조합
메모 목록(List) → 메모 상세(push) → 새 메모 작성(sheet). @StateObject로 메모 배열 관리, 자식에게 @ObservedObject로 전달
완료 체크리스트
NavigationStack과 sheet의 사용 시점 차이를 안다
@StateObject vs @ObservedObject를 올바르게 쓸 수 있다
@Observable (iOS 17+) 방식을 이해하고 쓸 수 있다
TabView 안에 독립적인 NavigationStack을 넣을 수 있다
Solution — 답안 보기
1. 3탭 앱
Swift
struct MainTabView: View {
    var body: some View {
        TabView {
            NavigationStack {
                List {
                    NavigationLink("공지사항") {
                        Text("공지 상세")
                    }
                    NavigationLink("이벤트") {
                        Text("이벤트 상세")
                    }
                }
                .navigationTitle("홈")
            }
            .tabItem {
                Label("홈",
                    systemImage: "house")
            }

            NavigationStack {
                Text("검색 화면")
                    .navigationTitle("검색")
            }
            .tabItem {
                Label("검색",
                    systemImage: "magnifyingglass")
            }

            NavigationStack {
                Text("내 프로필")
                    .navigationTitle("프로필")
            }
            .tabItem {
                Label("프로필",
                    systemImage: "person")
            }
        }
    }
}
2. 공유 데이터 모델 (@Observable)
Swift
@Observable
class AppData {
    var favorites: [String] = []

    func toggle(_ item: String) {
        if favorites.contains(item) {
            favorites.removeAll { $0 == item }
        } else {
            favorites.append(item)
        }
    }
}

struct AppRoot: View {
    @State var data = AppData()

    var body: some View {
        TabView {
            ItemListView()
                .tabItem { Label("목록",
                    systemImage: "list.bullet") }
            FavoritesView()
                .tabItem { Label("즐겨찾기",
                    systemImage: "heart") }
        }
        .environment(data)
    }
}

struct ItemListView: View {
    @Environment(AppData.self) var data
    let items = ["사과", "바나나", "포도"]

    var body: some View {
        List(items, id: \.self) { item in
            HStack {
                Text(item)
                Spacer()
                Image(systemName:
                    data.favorites.contains(item)
                    ? "heart.fill" : "heart")
                    .foregroundStyle(.red)
                    .onTapGesture {
                        data.toggle(item)
                    }
            }
        }
    }
}

struct FavoritesView: View {
    @Environment(AppData.self) var data

    var body: some View {
        List(data.favorites, id: \.self) {
            Text($0)
        }
    }
}
3. 메모 앱
Swift
struct Memo: Identifiable {
    let id = UUID()
    var title: String
    var body: String
    var date = Date()
}

class MemoStore: ObservableObject {
    @Published var memos: [Memo] = [
        Memo(title: "첫 번째 메모",
             body: "SwiftUI 공부 시작!")
    ]
}

struct MemoListView: View {
    @StateObject var store = MemoStore()
    @State private var showNew = false

    var body: some View {
        NavigationStack {
            List {
                ForEach(store.memos) { memo in
                    NavigationLink {
                        MemoDetailView(memo: memo)
                    } label: {
                        VStack(alignment: .leading) {
                            Text(memo.title)
                                .font(.headline)
                            Text(memo.body)
                                .font(.caption)
                                .foregroundStyle(
                                    .secondary)
                                .lineLimit(1)
                        }
                    }
                }
            }
            .navigationTitle("메모")
            .toolbar {
                Button { showNew = true } label: {
                    Image(systemName: "plus")
                }
            }
            .sheet(isPresented: $showNew) {
                NewMemoView(store: store)
            }
        }
    }
}

struct MemoDetailView: View {
    let memo: Memo
    var body: some View {
        ScrollView {
            Text(memo.body)
                .padding()
        }
        .navigationTitle(memo.title)
    }
}

struct NewMemoView: View {
    @ObservedObject var store: MemoStore
    @Environment(\.dismiss) var dismiss
    @State private var title = ""
    @State private var body = ""

    var body_view: some View {
        NavigationStack {
            Form {
                TextField("제목", text: $title)
                TextEditor(text: $body)
                    .frame(minHeight: 120)
            }
            .navigationTitle("새 메모")
            .toolbar {
                Button("저장") {
                    store.memos.append(
                        Memo(title: title,
                             body: body))
                    dismiss()
                }
            }
        }
    }
}
← 이전 챌린지 1-2 버튼 누르면 화면이 바뀌게
코멘트
후원하기
콘텐츠가 도움이 됐다면
커피 한 잔의 후원을 부탁드려요 :)
카카오페이 QR
이현호
카카오페이
카카오톡 QR
카카오톡 오픈채팅 💬
학습 질문 · 코드 리뷰 · 링크로 참여