Mobile

SwiftUI: UI Declarative iOS

TOKEN

Panduan lengkap membangun aplikasi iOS modern dengan SwiftUI β€” dari views, modifiers, state management, lists, hingga navigation dan best practices

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 SyntaxDeskripsikan UI berdasarkan state, bukan langkah-langkah perubahan β€” kode lebih ringkas dan mudah dibaca
Cross-PlatformSatu codebase bisa dijalankan di iOS, macOS, watchOS, tvOS, dan visionOS
Live PreviewPreview real-time di Xcode β€” perubahan kode langsung terlihat tanpa build ulang
Hot ReloadSupport hot reload untuk iterasi yang sangat cepat saat development
InteroperableBisa digunakan bersama UIKit melalui UIViewRepresentable dan UIViewControllerRepresentable
Accessibility Built-inFitur accessibility sudah terintegrasi langsung di setiap view
Dark ModeSupport dark mode otomatis tanpa kode tambahan yang signifikan

SwiftUI vs UIKit

Aspek SwiftUI UIKit
ParadigmaDeclarativeImperative
SintaksSangat ringkasLebih verbose
State ManagementProperty wrappers (@State, @Binding)Manual (delegates, notifications)
PreviewLive Preview di XcodePerlu build & run
Minimum iOSiOS 13+iOS 2+
EkosistemBerkembang pesatSangat matang
Learning CurveMudah untuk pemulaLebih curam
Diagram: Arsitektur SwiftUI
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  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

Bash
# 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

Swift β€” Views Dasar
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

Swift β€” Custom Views
// 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

Swift β€” Layout Stacks
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()
        }
    }
}
πŸ’‘ Tips ViewBuilder

@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!

Swift β€” Modifier Order
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

Swift β€” Custom ViewModifier
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
@StateView lokalMenyimpan state milik view itu sendiri
@BindingReferensiMembaca & menulis state dari parent view
@ObservedObjectSharedMengamati object yang conforms ObservableObject
@StateObjectView lokalMembuat & memiliki ObservableObject
@EnvironmentObjectGlobalMengakses object dari environment (DI)
@EnvironmentSystemMengakses system values (colorScheme, locale)
Swift β€” @State & @Binding
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()
    }
}
Swift β€” ObservableObject & Environment
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")
        }
    }
}
⚠️ Perbedaan @StateObject vs @ObservedObject

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

Swift β€” Lists
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

Swift β€” Sections & 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)
        }
    }
}

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.

Swift β€” Navigation
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

Swift β€” TabView
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

Swift β€” 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

πŸ’‘ Tips SwiftUI Best Practices

Beberapa tips penting untuk menulis kode SwiftUI yang baik dan maintainable:

Tips Penjelasan
Ekstraksi ViewPecah view besar menjadi view-view kecil yang reusable. Idealnya satu view hanya 50-100 baris
@StateObject vs @ObservedObjectGunakan @StateObject saat view membuat object, @ObservedObject saat menerima dari parent
Preview ProviderSelalu buat preview yang informatif dengan data sample untuk development yang cepat
IdentifiableSelalu gunakan protocol Identifiable untuk model data yang digunakan di List/ForEach
Extract ModifierGunakan custom ViewModifier untuk styling yang berulang
Avoid Massive ViewsGunakan MVVM pattern β€” pisahkan business logic ke ViewModel
AccessibilitySelalu tambahkan .accessibilityLabel() untuk elemen UI penting
Swift β€” Preview dengan Data Sample
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?

πŸŽ‰ Selamat!

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!

πŸ” Zoom
100%
🎨 Tema