🇺🇸 EN

📱 SwiftUI 마스터

선언적 UI 프레임워크로 앱을 만듭니다. 코드가 곧 UI이고, UI가 곧 코드입니다.

⭐ 난이도: ⭐⭐ ⏱️ 예상 시간: 2-3h 📂 App Frameworks

✨ SwiftUI란?

SwiftUI는 선언적 구문으로 UI를 작성하는 Apple의 최신 프레임워크입니다. UIKit 대신 사용하며, iOS 13+부터 지원됩니다.

🎯 기본 View 구조

ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 16) {
            Text("Hello, SwiftUI!")
                .font(.largeTitle)
                .fontWeight(.bold)

            Image(systemName: "star.fill")
                .font(.system(size: 48))
                .foregroundStyle(.yellow)

            Button("탭하기") {
                print("Button tapped!")
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

📊 State & Binding

SwiftUI는 @State로 상태를 관리하고, $ 바인딩으로 데이터를 연결합니다.

StateExample.swift
struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("카운트: \(count)")
                .font(.title)

            HStack(spacing: 16) {
                Button("-") { count -= 1 }
                    .buttonStyle(.bordered)

                Button("+") { count += 1 }
                    .buttonStyle(.borderedProminent)
            }
        }
        .padding()
    }
}

📝 TextField & Binding

FormExample.swift
struct LoginView: View {
    @State private var username = ""
    @State private var password = ""

    var body: some View {
        VStack(spacing: 16) {
            TextField("아이디", text: $username)
                .textFieldStyle(.roundedBorder)
                .textInputAutocapitalization(.never)

            SecureField("비밀번호", text: $password)
                .textFieldStyle(.roundedBorder)

            Button("로그인") {
                login(username: username, password: password)
            }
            .buttonStyle(.borderedProminent)
            .disabled(username.isEmpty || password.isEmpty)
        }
        .padding()
    }

    func login(username: String, password: String) {
        // 로그인 처리
    }
}

📋 List & ForEach

ListView.swift
struct Todo: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool = false
}

struct TodoListView: View {
    @State private var todos = [
        Todo(title: "SwiftUI 공부"),
        Todo(title: "앱 만들기"),
        Todo(title: "출시하기")
    ]

    var body: some View {
        List {
            ForEach($todos) { $todo in
                HStack {
                    Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                        .foregroundStyle(todo.isCompleted ? .green : .gray)

                    Text(todo.title)
                        .strikethrough(todo.isCompleted)
                }
                .contentShape(Rectangle())
                .onTapGesture {
                    todo.isCompleted.toggle()
                }
            }
            .onDelete(perform: deleteTodos)
        }
    }

    func deleteTodos(at offsets: IndexSet) {
        todos.remove(atOffsets: offsets)
    }
}

🧭 Navigation

NavigationExample.swift
struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(1..<11, id: \.self) { number in
                NavigationLink("아이템 \(number)") {
                    DetailView(number: number)
                }
            }
            .navigationTitle("목록")
        }
    }
}

struct DetailView: View {
    let number: Int

    var body: some View {
        VStack {
            Text("아이템 #\(number)")
                .font(.largeTitle)
        }
        .navigationTitle("상세")
        .navigationBarTitleDisplayMode(.inline)
    }
}

🎨 Modifier & Custom Style

CustomModifier.swift
// Custom Modifier
struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.white)
            .cornerRadius(12)
            .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardModifier())
    }
}

// 사용 예시
struct ContentView: View {
    var body: some View {
        VStack {
            Text("카드 1")
                .cardStyle()

            Text("카드 2")
                .cardStyle()
        }
        .padding()
    }
}

⏱️ Animation

AnimationExample.swift
struct AnimationView: View {
    @State private var isExpanded = false
    @State private var rotation = 0.0

    var body: some View {
        VStack(spacing: 32) {
            // 크기 애니메이션
            RoundedRectangle(cornerRadius: 12)
                .fill(Color.blue)
                .frame(
                    width: isExpanded ? 200 : 100,
                    height: isExpanded ? 200 : 100
                )
                .animation(.spring(response: 0.6), value: isExpanded)

            // 회전 애니메이션
            Image(systemName: "star.fill")
                .font(.system(size: 48))
                .foregroundStyle(.yellow)
                .rotationEffect(.degrees(rotation))
                .animation(.linear(duration: 1).repeatForever(autoreverses: false), value: rotation)

            Button("토글") {
                isExpanded.toggle()
                rotation += 360
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

🔄 상태 관리: @Observable (iOS 17+) vs ObservableObject

💡 iOS 17+에서는 @Observable 권장!
ObservableObject는 레거시 방식입니다. 새 프로젝트에서는 @Observable 매크로를 사용하세요. 자세한 내용은 Observation 튜토리얼을 참고하세요.

ViewModel.swift (iOS 17+ 권장)
import Observation

@Observable
class TodoViewModel {
    var todos: [Todo] = []  // @Published 불필요!
    var isLoading = false

    func addTodo(title: String) {
        let newTodo = Todo(title: title)
        todos.append(newTodo)
    }
}

struct ContentView: View {
    var viewModel = TodoViewModel()  // @StateObject 불필요!

    var body: some View {
        List(viewModel.todos) { todo in
            Text(todo.title)
        }
    }
}
📜 레거시: ObservableObject (iOS 13-16)
ViewModel.swift (레거시)
class TodoViewModel: ObservableObject {
    @Published var todos: [Todo] = []
    @Published var isLoading = false
}

struct ContentView: View {
    @StateObject private var viewModel = TodoViewModel()
    // ...
}

💡 HIG 체크리스트
✅ SF Symbols를 활용한 아이콘
✅ .padding()으로 충분한 여백
✅ .buttonStyle()로 버튼 스타일 지정
✅ .disabled()로 상태 표현
✅ 다크모드 자동 지원

📦 학습 자료

💻
GitHub 프로젝트
🍎
Apple HIG 원문
📖
Apple 공식 튜토리얼

📎 Apple 공식 자료

📘 공식 문서 💻 샘플 코드 🎬 WWDC 세션