๐ฑ SwiftUI ๋ง์คํฐ
์ ์ธ์ UI ํ๋ ์์ํฌ๋ก ์ฑ์ ๋ง๋ญ๋๋ค. ์ฝ๋๊ฐ ๊ณง UI์ด๊ณ , UI๊ฐ ๊ณง ์ฝ๋์ ๋๋ค.
โจ SwiftUI๋?
SwiftUI๋ ์ ์ธ์ ๊ตฌ๋ฌธ์ผ๋ก UI๋ฅผ ์์ฑํ๋ Apple์ ์ต์ ํ๋ ์์ํฌ์ ๋๋ค. UIKit ๋์ ์ฌ์ฉํ๋ฉฐ, iOS 13+๋ถํฐ ์ง์๋ฉ๋๋ค.
๐ฏ ๊ธฐ๋ณธ View ๊ตฌ์กฐ
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๋ก ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ , $ ๋ฐ์ธ๋ฉ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฐ๊ฒฐํฉ๋๋ค.
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
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
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
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
// 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
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 ํํ ๋ฆฌ์ผ์ ์ฐธ๊ณ ํ์ธ์.
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)
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()๋ก ์ํ ํํ
โ
๋คํฌ๋ชจ๋ ์๋ ์ง์