1. Pengenalan SwiftUI
SwiftUI adalah framework UI modern dari Apple yang diperkenalkan pada WWDC 2019. SwiftUI memungkinkan developer membangun antarmuka pengguna untuk semua platform Apple β iOS, macOS, watchOS, tvOS, dan visionOS β menggunakan pendekatan declarative yang elegan dan ringkas.
Berbeda dengan UIKit yang bersifat imperative (developer harus mendeskripsikan langkah-langkah perubahan UI satu per satu), SwiftUI memungkinkan Anda mendeskripsikan apa yang ingin ditampilkan berdasarkan state saat ini, dan framework yang akan menangani pembaruan UI secara otomatis dan efisien.
Keunggulan SwiftUI
| Keunggulan | Penjelasan |
|---|---|
| Declarative Syntax | Deskripsikan UI berdasarkan state, bukan langkah-langkah perubahan β kode lebih ringkas dan mudah dibaca |
| Cross-Platform | Satu codebase bisa dijalankan di iOS, macOS, watchOS, tvOS, dan visionOS |
| Live Preview | Preview real-time di Xcode β perubahan kode langsung terlihat tanpa build ulang |
| Hot Reload | Support hot reload untuk iterasi yang sangat cepat saat development |
| Interoperable | Bisa digunakan bersama UIKit melalui UIViewRepresentable dan UIViewControllerRepresentable |
| Accessibility Built-in | Fitur accessibility sudah terintegrasi langsung di setiap view |
| Dark Mode | Support dark mode otomatis tanpa kode tambahan yang signifikan |
SwiftUI vs UIKit
| Aspek | SwiftUI | UIKit |
|---|---|---|
| Paradigma | Declarative | Imperative |
| Sintaks | Sangat ringkas | Lebih verbose |
| State Management | Property wrappers (@State, @Binding) | Manual (delegates, notifications) |
| Preview | Live Preview di Xcode | Perlu build & run |
| Minimum iOS | iOS 13+ | iOS 2+ |
| Ekosistem | Berkembang pesat | Sangat matang |
| Learning Curve | Mudah untuk pemula | Lebih curam |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β YOUR SWIFTUI CODE β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Views (Text, Image, List, NavigationStack) β β β βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ β β βΌ β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β β SwiftUI Framework β β β β ββββββββββ ββββββββββββ ββββββββββ ββββββββββ β β β β βView β βState β βLayout β βAnim β β β β β βBuilder β βEngine β βSystem β βEngine β β β β β ββββββββββ ββββββ¬ββββββ ββββββββββ ββββββββββ β β β βββββββββββββββββββββΌββββββββββββββββββββββββββββββ β β βΌ β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β β UIKit / AppKit (Rendering) β β β βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββ β β βΌ β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Core Animation / Metal (GPU Rendering) β β β βββββββββββββββββββββββββββββββββββββββββββββββββββ β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Setup & Persiapan
# SwiftUI memerlukan: # 1. Xcode 11+ (untuk SwiftUI 1.0) atau Xcode 15+ (untuk fitur terbaru) # 2. macOS Catalina 10.15+ atau lebih baru # 3. iOS 13+ sebagai deployment target # Membuat proyek baru di Xcode: # 1. Buka Xcode β File β New β Project # 2. Pilih "iOS" β "App" # 3. Pastikan "Interface" dipilih: SwiftUI # 4. Pastikan "Language" dipilih: Swift # 5. Klik "Next" dan tentukan nama proyek # Atau menggunakan Swift Package Manager untuk library: # File β Add Package Dependencies # Contoh: https://github.com/Alamofire/Alamofire
2. Views & View Protocol
Di SwiftUI, View adalah protokol fundamental yang mendeskripsikan bagian dari UI Anda. Setiap elemen UI β teks, gambar, tombol, layout β adalah sebuah View. View di SwiftUI bersifat struct-based (value type) dan menggunakan ViewBuilder untuk mendeskripsikan hierarki view secara deklaratif.
View Dasar yang Sering Digunakan
import SwiftUI
// ===== VIEW DASAR SWIFTUI =====
// Text β Menampilkan teks
Text("Halo, SwiftUI!")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
.multilineTextAlignment(.center)
// Image β Menampilkan gambar
Image(systemName: "star.fill") // SF Symbol
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.foregroundColor(.yellow)
Image("photo") // Asset catalog
.resizable()
.scaledToFit()
.cornerRadius(12)
// Button β Tombol interaktif
Button(action: {
print("Tombol ditekan!")
}) {
HStack {
Image(systemName: "hand.thumbsup.fill")
Text("Suka")
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
// Label β Ikon + Teks
Label("Beranda", systemImage: "house.fill")
.font(.headline)
// Divider β Garis pemisah
Divider()
.padding(.horizontal)
// Spacer β Mengisi ruang kosong
HStack {
Text("Kiri")
Spacer()
Text("Kanan")
}
// Color β Bidang warna
Color.red
.frame(width: 100, height: 100)
.cornerRadius(16)
Custom View
// Membuat custom view dengan protocol View
struct ProfileCard: View {
let nama: String
let jabatan: String
let imageName: String
var body: some View {
VStack(spacing: 12) {
Image(imageName)
.resizable()
.scaledToFill()
.frame(width: 80, height: 80)
.clipShape(Circle())
.overlay(
Circle().stroke(Color.blue, lineWidth: 3)
)
Text(nama)
.font(.title2)
.fontWeight(.semibold)
Text(jabatan)
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemBackground))
.shadow(color: .black.opacity(0.1), radius: 8, y: 4)
)
}
}
// Menggunakan custom view
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
ProfileCard(
nama: "Budi Santoso",
jabatan: "iOS Developer",
imageName: "profile_budi"
)
ProfileCard(
nama: "Ani Wijaya",
jabatan: "UI/UX Designer",
imageName: "profile_ani"
)
}
.padding()
}
}
Stacks β VStack, HStack, ZStack
import SwiftUI
struct StackExamples: View {
var body: some View {
ScrollView {
VStack(spacing: 24) {
// VStack β Vertikal stack
VStack(alignment: .leading, spacing: 8) {
Text("VStack Example")
.font(.headline)
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(12)
// HStack β Horizontal stack
HStack(spacing: 16) {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text("Rating: 4.8")
.font(.subheadline)
Spacer()
Text("128 ulasan")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.background(Color.green.opacity(0.1))
.cornerRadius(12)
// ZStack β Layered stack (tumpuk)
ZStack {
RoundedRectangle(cornerRadius: 16)
.fill(
LinearGradient(
colors: [.purple, .blue],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(height: 150)
VStack {
Image(systemName: "bolt.fill")
.font(.largeTitle)
.foregroundColor(.white)
Text("Premium Feature")
.font(.title3)
.fontWeight(.bold)
.foregroundColor(.white)
}
}
// Grid β LazyVGrid untuk grid layout
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
LazyVGrid(columns: columns, spacing: 16) {
ForEach(1...9, id: \.self) { index in
RoundedRectangle(cornerRadius: 12)
.fill(Color.orange.opacity(0.3))
.frame(height: 80)
.overlay(
Text("\(index)")
.font(.title2)
.fontWeight(.bold)
)
}
}
}
.padding()
}
}
}
@ViewBuilder adalah result builder yang memungkinkan Anda menulis beberapa view di dalam closure secara berurutan tanpa perlu Group atau AnyView. SwiftUI secara otomatis menggunakan TupleView untuk menggabungkan hingga 10 view dalam satu closure.
3. Modifiers & Styling
Modifiers di SwiftUI adalah method yang dipanggil pada view untuk mengubah penampilan atau perilakunya. Setiap modifier menghasilkan view baru yang membungkus view sebelumnya β ini penting untuk dipahami karena urutan modifier sangat berpengaruh pada hasil akhir.
Urutan Modifier Penting!
import SwiftUI
struct ModifierOrderExample: View {
var body: some View {
VStack(spacing: 30) {
// Contoh 1: Background SEBELUM padding
// β Background hanya selebar teks
Text("Contoh 1")
.background(Color.blue)
.padding()
// Contoh 2: Background SETELAH padding
// β Background mencakup area padding
Text("Contoh 2")
.padding()
.background(Color.green)
// Contoh 3: Corner radius sebelum shadow
Text("Contoh 3")
.padding()
.background(Color.orange)
.cornerRadius(12)
.shadow(radius: 5)
// Contoh 4: Urutan kompleks
Text("SwiftUI!")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(
LinearGradient(
colors: [.purple, .blue],
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(25)
.shadow(color: .purple.opacity(0.4), radius: 10, y: 5)
}
}
}
Tabel Modifier Umum
| Kategori | Modifier | Fungsi |
|---|---|---|
| Teks | .font() | Mengatur ukuran dan gaya font |
| Teks | .fontWeight() | Mengatur ketebalan font (bold, light, dll) |
| Warna | .foregroundColor() | Mengatur warna teks/ikon |
| Warna | .background() | Menambahkan background di belakang view |
| Warna | .opacity() | Mengatur transparansi (0.0 - 1.0) |
| Layout | .padding() | Menambahkan ruang di sekitar view |
| Layout | .frame() | Mengatur ukuran (width, height, alignment) |
| Layout | .offset() | Menggeser posisi view |
| Bentuk | .cornerRadius() | Membulatkan sudut |
| Bentuk | .clipShape() | Memotong view sesuai bentuk tertentu |
| Efek | .shadow() | Menambahkan bayangan |
| Efek | .blur() | Menambahkan efek blur |
| Gambar | .resizable() | Membuat gambar bisa di-resize |
| Gambar | .scaledToFit() | Menyesuaikan ukuran gambar (aspect fit) |
| Gambar | .scaledToFill() | Menyesuaikan ukuran gambar (aspect fill) |
ViewModifier Custom
import SwiftUI
// Membuat custom modifier
struct CardModifier: ViewModifier {
var backgroundColor: Color = Color(.systemBackground)
var cornerRadius: CGFloat = 16
func body(content: Content) -> some View {
content
.padding()
.background(backgroundColor)
.cornerRadius(cornerRadius)
.shadow(color: .black.opacity(0.1), radius: 8, y: 4)
}
}
// Modifier untuk gradient header
struct GradientHeaderModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 20)
.background(
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
}
// Extension untuk kemudahan penggunaan
extension View {
func cardStyle(
backgroundColor: Color = Color(.systemBackground),
cornerRadius: CGFloat = 16
) -> some View {
modifier(CardModifier(
backgroundColor: backgroundColor,
cornerRadius: cornerRadius
))
}
func gradientHeader() -> some View {
modifier(GradientHeaderModifier())
}
}
// Menggunakan custom modifier
struct ModifierDemoView: View {
var body: some View {
VStack(spacing: 20) {
Text("Selamat Datang!")
.gradientHeader()
Text("Ini adalah kartu dengan custom modifier")
.cardStyle()
Text("Kartu dengan warna berbeda")
.cardStyle(
backgroundColor: .blue.opacity(0.1),
cornerRadius: 20
)
}
}
}
4. State Management
State management adalah konsep kunci di SwiftUI. Karena SwiftUI menggunakan pendekatan declarative, UI akan otomatis diperbarui ketika state berubah. SwiftUI menyediakan beberapa property wrappers untuk mengelola state pada berbagai level aplikasi.
Property Wrappers untuk State
| Property Wrapper | Scope | Penggunaan |
|---|---|---|
| @State | View lokal | Menyimpan state milik view itu sendiri |
| @Binding | Referensi | Membaca & menulis state dari parent view |
| @ObservedObject | Shared | Mengamati object yang conforms ObservableObject |
| @StateObject | View lokal | Membuat & memiliki ObservableObject |
| @EnvironmentObject | Global | Mengakses object dari environment (DI) |
| @Environment | System | Mengakses system values (colorScheme, locale) |
import SwiftUI
// @State β State lokal milik view
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("Hitungan: \(count)")
.font(.title)
.fontWeight(.bold)
HStack(spacing: 20) {
Button("β") {
if count > 0 { count -= 1 }
}
.font(.title)
.frame(width: 60, height: 60)
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(30)
Button("+") {
count += 1
}
.font(.title)
.frame(width: 60, height: 60)
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(30)
}
}
}
}
// @Binding β Referensi ke state parent
struct ToggleCard: View {
@Binding var isOn: Bool
let title: String
var body: some View {
HStack {
Image(systemName: isOn ? "checkmark.circle.fill" : "circle")
.foregroundColor(isOn ? .green : .gray)
.font(.title2)
Text(title)
.font(.body)
Spacer()
Toggle("", isOn: $isOn)
.labelsHidden()
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(12)
}
}
// Parent view menggunakan @Binding
struct SettingsView: View {
@State private var notificationsOn = true
@State private var darkModeOn = false
@State private var autoSyncOn = true
var body: some View {
VStack(spacing: 12) {
Text("Pengaturan")
.font(.title)
.fontWeight(.bold)
ToggleCard(isOn: $notificationsOn, title: "Notifikasi")
ToggleCard(isOn: $darkModeOn, title: "Mode Gelap")
ToggleCard(isOn: $autoSyncOn, title: "Sinkronisasi Otomatis")
}
.padding()
}
}
import SwiftUI
import Combine
// ObservableObject β Shared state untuk beberapa view
class UserViewModel: ObservableObject {
@Published var nama: String = ""
@Published var email: String = ""
@Published var isLoggedIn: Bool = false
@Published var isLoading: Bool = false
func login(email: String, password: String) {
isLoading = true
// Simulasi API call
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.email = email
self.nama = "Budi Santoso"
self.isLoggedIn = true
self.isLoading = false
}
}
func logout() {
nama = ""
email = ""
isLoggedIn = false
}
}
// @StateObject β Membuat dan memiliki ViewModel
struct LoginView: View {
@StateObject private var viewModel = UserViewModel()
var body: some View {
Group {
if viewModel.isLoggedIn {
ProfileView()
} else {
LoginFormView()
}
}
.environmentObject(viewModel) // Inject ke environment
}
}
struct LoginFormView: View {
@EnvironmentObject var viewModel: UserViewModel
@State private var email = ""
@State private var password = ""
var body: some View {
VStack(spacing: 16) {
Text("Masuk")
.font(.largeTitle)
.fontWeight(.bold)
TextField("Email", text: $email)
.textFieldStyle(.roundedBorder)
.keyboardType(.emailAddress)
.autocapitalization(.none)
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
Button(action: {
viewModel.login(email: email, password: password)
}) {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Masuk")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(12)
.disabled(viewModel.isLoading)
}
.padding()
}
}
// @EnvironmentObject β Mengakses shared state
struct ProfileView: View {
@EnvironmentObject var viewModel: UserViewModel
var body: some View {
VStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.font(.system(size: 80))
.foregroundColor(.blue)
Text(viewModel.nama)
.font(.title)
.fontWeight(.bold)
Text(viewModel.email)
.font(.subheadline)
.foregroundColor(.secondary)
Button("Keluar") {
viewModel.logout()
}
.foregroundColor(.red)
}
.padding()
}
}
// @Environment β System values
struct ThemeAwareView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
VStack {
Text(colorScheme == .dark ? "Mode Gelap" : "Mode Terang")
Text(sizeClass == .compact ? "Compact" : "Regular")
}
}
}
Gunakan @StateObject saat view membuat dan memiliki object tersebut. Gunakan @ObservedObject saat view menerima object dari parent. Jika menggunakan @ObservedObject untuk object yang seharusnya dimiliki view, object akan di-reset setiap kali parent view di-rebuild, menyebabkan bug yang sulit dideteksi.
5. Lists & Dynamic Data
List di SwiftUI adalah view yang menampilkan sekumpulan data secara scrollable. Mirip dengan UITableView di UIKit, tetapi dengan sintaks yang jauh lebih sederhana. SwiftUI List mendukung fitur-fitur lengkap seperti selection, editing mode, swipe actions, dan section grouping.
Basic List
import SwiftUI
// Model data
struct Mahasiswa: Identifiable {
let id = UUID()
let nama: String
let jurusan: String
let angkatan: Int
let ipk: Double
}
// List dasar dengan ForEach
struct MahasiswaListView: View {
let mahasiswa: [Mahasiswa] = [
Mahasiswa(nama: "Budi Santoso", jurusan: "Teknik Informatika", angkatan: 2023, ipk: 3.8),
Mahasiswa(nama: "Ani Wijaya", jurusan: "Sistem Informasi", angkatan: 2023, ipk: 3.9),
Mahasiswa(nama: "Citra Dewi", jurusan: "Teknik Komputer", angkatan: 2024, ipk: 3.7),
Mahasiswa(nama: "Dodi Pratama", jurusan: "Teknik Informatika", angkatan: 2022, ipk: 3.5),
Mahasiswa(nama: "Eka Putri", jurusan: "Sistem Informasi", angkatan: 2024, ipk: 3.95),
]
var body: some View {
List(mahasiswa) { mhs in
HStack(spacing: 12) {
Circle()
.fill(Color.blue.gradient)
.frame(width: 44, height: 44)
.overlay(
Text(String(mhs.nama.prefix(1)))
.foregroundColor(.white)
.fontWeight(.bold)
)
VStack(alignment: .leading, spacing: 4) {
Text(mhs.nama)
.font(.headline)
Text("\(mhs.jurusan) β Angkatan \(mhs.angkatan)")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text(String(format: "%.2f", mhs.ipk))
.font(.title3)
.fontWeight(.bold)
.foregroundColor(mhs.ipk >= 3.7 ? .green : .orange)
Text("IPK")
.font(.caption2)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 4)
}
.listStyle(.insetGrouped)
.navigationTitle("Daftar Mahasiswa")
}
}
List dengan Section & Swipe Actions
import SwiftUI
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var isCompleted: Bool
var priority: Priority
enum Priority: String, CaseIterable {
case tinggi = "π΄ Tinggi"
case sedang = "π‘ Sedang"
case rendah = "π’ Rendah"
}
}
struct TodoListView: View {
@State private var todos = [
TodoItem(title: "Belajar SwiftUI", isCompleted: true, priority: .tinggi),
TodoItem(title: "Buat prototipe UI", isCompleted: false, priority: .tinggi),
TodoItem(title: "Review pull request", isCompleted: false, priority: .sedang),
TodoItem(title: "Update dokumentasi", isCompleted: true, priority: .rendah),
TodoItem(title: "Deploy ke TestFlight", isCompleted: false, priority: .sedang),
]
@State private var showAddSheet = false
var completedTodos: [TodoItem] { todos.filter { $0.isCompleted } }
var pendingTodos: [TodoItem] { todos.filter { !$0.isCompleted } }
var body: some View {
List {
// Section belum selesai
Section("Belum Selesai (\(pendingTodos.count))") {
ForEach(pendingTodos) { todo in
todoRow(todo)
}
.onDelete { indexSet in
// Handle delete
}
}
// Section sudah selesai
if !completedTodos.isEmpty {
Section("Selesai (\(completedTodos.count))") {
ForEach(completedTodos) { todo in
todoRow(todo)
}
}
}
}
.listStyle(.insetGrouped)
.navigationTitle("Todo List")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showAddSheet = true }) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddSheet) {
AddTodoSheet(todos: $todos)
}
}
func todoRow(_ todo: TodoItem) -> some View {
HStack {
Image(systemName: todo.isCompleted ?
"checkmark.circle.fill" : "circle")
.foregroundColor(todo.isCompleted ? .green : .gray)
.onTapGesture {
if let idx = todos.firstIndex(where: { $0.id == todo.id }) {
todos[idx].isCompleted.toggle()
}
}
VStack(alignment: .leading) {
Text(todo.title)
.strikethrough(todo.isCompleted)
Text(todo.priority.rawValue)
.font(.caption)
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
todos.removeAll { $0.id == todo.id }
} label: {
Label("Hapus", systemImage: "trash")
}
}
.swipeActions(edge: .leading) {
Button {
if let idx = todos.firstIndex(where: { $0.id == todo.id }) {
todos[idx].isCompleted.toggle()
}
} label: {
Label("Selesai", systemImage: "checkmark")
}
.tint(.green)
}
}
}
6. Navigation & Routing
SwiftUI menyediakan sistem navigasi yang powerful melalui NavigationStack (iOS 16+) dan NavigationLink. Dengan NavigationStack, Anda bisa melakukan programmatic navigation menggunakan navigation path yang bisa dikontrol melalui binding.
import SwiftUI
// Model untuk navigasi
struct Produk: Identifiable, Hashable {
let id = UUID()
let nama: String
let harga: Int
let deskripsi: String
let kategori: String
}
// Navigation Stack dengan programmatic navigation
struct TokoApp: View {
@State private var path = NavigationPath()
@State private var selectedKategori: String?
let produkList = [
Produk(nama: "iPhone 16 Pro", harga: 18999000, deskripsi: "Flagship Apple terbaru", kategori: "Smartphone"),
Produk(nama: "MacBook Air M3", harga: 16999000, deskripsi: "Laptop ultraportable", kategori: "Laptop"),
Produk(nama: "iPad Pro M4", harga: 14999000, deskripsi: "Tablet profesional", kategori: "Tablet"),
Produk(nama: "AirPods Pro 3", harga: 3999000, deskripsi: "Earbuds premium", kategori: "Audio"),
Produk(nama: "Apple Watch Ultra 3", harga: 12999000, deskripsi: "Smartwatch outdoor", kategori: "Wearable"),
]
var body: some View {
NavigationStack(path: $path) {
List {
// NavigationLink sederhana
Section("Kategori") {
ForEach(["Smartphone", "Laptop", "Tablet", "Audio", "Wearable"], id: \.self) { kategori in
NavigationLink(value: kategori) {
Label(kategori, systemImage: iconForKategori(kategori))
}
}
}
// NavigationLink ke detail
Section("Produk Populer") {
ForEach(produkList) { produk in
NavigationLink(value: produk) {
HStack {
VStack(alignment: .leading) {
Text(produk.nama)
.font(.headline)
Text("Rp \(produk.harga.formatted())")
.font(.subheadline)
.foregroundColor(.blue)
}
}
}
}
}
}
.navigationTitle("Toko Online")
.navigationDestination(for: String.self) { kategori in
KategoriView(
kategori: kategori,
produk: produkList.filter { $0.kategori == kategori }
)
}
.navigationDestination(for: Produk.self) { produk in
DetailProdukView(produk: produk, path: $path)
}
}
}
func iconForKategori(_ kategori: String) -> String {
switch kategori {
case "Smartphone": return "iphone"
case "Laptop": return "laptopcomputer"
case "Tablet": return "ipad"
case "Audio": return "airpodspro"
case "Wearable": return "applewatch"
default: return "bag"
}
}
}
struct KategoriView: View {
let kategori: String
let produk: [Produk]
var body: some View {
List(produk) { item in
NavigationLink(value: item) {
HStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.2))
.frame(width: 50, height: 50)
.overlay(Image(systemName: "bag.fill"))
VStack(alignment: .leading) {
Text(item.nama)
.font(.headline)
Text("Rp \(item.harga.formatted())")
.foregroundColor(.blue)
}
}
}
}
.navigationTitle(kategori)
}
}
struct DetailProdukView: View {
let produk: Produk
@Binding var path: NavigationPath
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
RoundedRectangle(cornerRadius: 16)
.fill(Color.blue.opacity(0.2))
.frame(height: 250)
.overlay(
Image(systemName: "bag.fill")
.font(.system(size: 60))
.foregroundColor(.blue)
)
Text(produk.nama)
.font(.title)
.fontWeight(.bold)
Text("Rp \(produk.harga.formatted())")
.font(.title2)
.foregroundColor(.blue)
.fontWeight(.semibold)
Text(produk.deskripsi)
.font(.body)
.foregroundColor(.secondary)
Button("Beli Sekarang") {}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.frame(maxWidth: .infinity)
Button("Kembali ke Beranda") {
path = NavigationPath() // Pop semua
}
.frame(maxWidth: .infinity)
}
.padding()
}
.navigationTitle("Detail")
.navigationBarTitleDisplayMode(.inline)
}
}
Tab Navigation
import SwiftUI
struct MainTabView: View {
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
TokoApp()
.tabItem {
Label("Beranda", systemImage: "house.fill")
}
.tag(0)
Text("Pencarian")
.tabItem {
Label("Cari", systemImage: "magnifyingglass")
}
.tag(1)
Text("Keranjang")
.tabItem {
Label("Keranjang", systemImage: "cart.fill")
}
.badge(3) // Badge notification
.tag(2)
Text("Profil")
.tabItem {
Label("Profil", systemImage: "person.fill")
}
.tag(3)
}
}
}
7. Forms & Input
import SwiftUI
struct RegistrationForm: View {
@State private var nama = ""
@State private var email = ""
@State private var password = ""
@State private var confirmPassword = ""
@State private var tanggalLahir = Date()
@State private var jenisKelamin = 0
@State private var agreedToTerms = false
@State private var selectedInterests: Set = []
let interests = ["iOS", "Android", "Web", "Machine Learning", "DevOps"]
var isFormValid: Bool {
!nama.isEmpty &&
!email.isEmpty &&
email.contains("@") &&
password.count >= 8 &&
password == confirmPassword &&
agreedToTerms
}
var body: some View {
Form {
// Section Informasi Dasar
Section("Informasi Dasar") {
TextField("Nama Lengkap", text: $nama)
.textContentType(.name)
TextField("Email", text: $email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.autocapitalization(.none)
SecureField("Password (min 8 karakter)", text: $password)
SecureField("Konfirmasi Password", text: $confirmPassword)
if !password.isEmpty && password == confirmPassword {
Label("Password cocok", systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundColor(.green)
} else if !confirmPassword.isEmpty {
Label("Password tidak cocok", systemImage: "xmark.circle.fill")
.font(.caption)
.foregroundColor(.red)
}
}
// Section Info Tambahan
Section("Informasi Tambahan") {
DatePicker("Tanggal Lahir",
selection: $tanggalLahir,
displayedComponents: .date)
Picker("Jenis Kelamin", selection: $jenisKelamin) {
Text("Laki-laki").tag(0)
Text("Perempuan").tag(1)
Text("Lainnya").tag(2)
}
.pickerStyle(.segmented)
}
// Section Minat
Section("Minat Teknologi") {
ForEach(interests, id: \.self) { interest in
Button(action: {
if selectedInterests.contains(interest) {
selectedInterests.remove(interest)
} else {
selectedInterests.insert(interest)
}
}) {
HStack {
Text(interest)
Spacer()
if selectedInterests.contains(interest) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.blue)
}
}
}
.foregroundColor(.primary)
}
}
// Section Persetujuan
Section {
Toggle("Saya menyetujui syarat & ketentuan", isOn: $agreedToTerms)
}
// Submit Button
Section {
Button(action: {
print("Form submitted!")
}) {
Text("Daftar Sekarang")
.frame(maxWidth: .infinity)
.fontWeight(.semibold)
}
.disabled(!isFormValid)
}
}
.navigationTitle("Registrasi")
}
}
8. Best Practices & Tips
Beberapa tips penting untuk menulis kode SwiftUI yang baik dan maintainable:
| Tips | Penjelasan |
|---|---|
| Ekstraksi View | Pecah view besar menjadi view-view kecil yang reusable. Idealnya satu view hanya 50-100 baris |
| @StateObject vs @ObservedObject | Gunakan @StateObject saat view membuat object, @ObservedObject saat menerima dari parent |
| Preview Provider | Selalu buat preview yang informatif dengan data sample untuk development yang cepat |
| Identifiable | Selalu gunakan protocol Identifiable untuk model data yang digunakan di List/ForEach |
| Extract Modifier | Gunakan custom ViewModifier untuk styling yang berulang |
| Avoid Massive Views | Gunakan MVVM pattern β pisahkan business logic ke ViewModel |
| Accessibility | Selalu tambahkan .accessibilityLabel() untuk elemen UI penting |
import SwiftUI
// Contoh preview yang baik
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
// Preview mode terang
MahasiswaListView()
.previewDisplayName("Light Mode")
// Preview mode gelap
MahasiswaListView()
.preferredColorScheme(.dark)
.previewDisplayName("Dark Mode")
// Preview ukuran kecil (iPhone SE)
MahasiswaListView()
.previewDevice("iPhone SE (3rd generation)")
.previewDisplayName("iPhone SE")
}
}
}
// Di SwiftUI terbaru (iOS 17+), gunakan #Preview macro
#Preview("Mahasiswa List") {
NavigationStack {
MahasiswaListView()
}
}
#Preview("Dark Mode") {
NavigationStack {
MahasiswaListView()
}
.preferredColorScheme(.dark)
}
9. Quiz Pemahaman
Uji pemahaman Anda tentang materi SwiftUI yang telah dipelajari!
1. Apa fungsi utama @State di SwiftUI?
2. Mengapa urutan modifier penting di SwiftUI?
3. Kapan menggunakan @StateObject dibanding @ObservedObject?
4. Apa perbedaan VStack, HStack, dan ZStack?
5. Apa yang dilakukan .environmentObject() di SwiftUI?
Anda telah mempelajari dasar-dasar SwiftUI β dari views, modifiers, state management, lists, hingga navigation. SwiftUI terus berkembang dengan setiap rilis iOS baru, jadi pastikan untuk selalu mengikuti update terbaru dari Apple. Lanjutkan dengan proyek praktik untuk memperdalam pemahaman Anda!