๐Ÿ“ฑ SwiftUI ๋งˆ์Šคํ„ฐ

์„ ์–ธ์  UI ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ์•ฑ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ฝ”๋“œ๊ฐ€ ๊ณง UI์ด๊ณ , UI๊ฐ€ ๊ณง ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

โœจ 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 ๊ณต์‹ ํŠœํ† ๋ฆฌ์–ผ