📱 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()로 상태 표현
✅ 다크모드 자동 지원