☀️Siang
SwiftUI

📱 SwiftUI: Membangun UI Declarative

Tutorial lengkap SwiftUI — views, modifiers, state, binding, navigation, list, dan quiz interaktif untuk membangun aplikasi iOS dengan UI modern

Artikel: Swiftui Basics Artikel: Swiftui Basics


1. Pengenalan SwiftUI

SwiftUI adalah framework UI yang diperkenalkan oleh Apple pada WWDC 2019. SwiftUI menggunakan pendekatan declarative — Anda mendeskripsikan "apa" yang ingin ditampilkan, bukan "bagaimana" menampilkannya. Framework ini tersedia untuk iOS, macOS, watchOS, tvOS, dan visionOS.

SwiftUI menggantikan UIKit (yang imperatif) dengan sintaks yang jauh lebih ringkas, modern, dan menyenangkan untuk ditulis. Dengan SwiftUI, Anda cukup mendeskripsikan UI dalam kode Swift biasa.

Mengapa SwiftUI?

Keunggulan Penjelasan
Declarative SyntaxCukup deskripsikan UI, SwiftUI akan membangunnya
Less CodeKode lebih sedikit dibanding UIKit untuk hasil yang sama
Live PreviewLangsung melihat perubahan UI di Xcode tanpa build
Cross-PlatformSatu kode untuk iOS, macOS, watchOS, tvOS
ReactiveUI otomatis update saat data berubah
AccessibleAksesibilitas (VoiceOver, Dynamic Type) built-in

SwiftUI vs UIKit

Aspek SwiftUI UIKit
ParadigmaDeclarativeImperative
Lines of Code🟢 Sedikit🔴 Banyak
State Management🟢 Otomatis (@State, @Binding)🟡 Manual
Preview🟢 Live Preview🔴 Build & Run
Min. iOSiOS 13+iOS 2+
Ekosistem🟡 Berkembang🟢 Sangat matang
Diagram: SwiftUI vs UIKit
PERBANDINGAN UI FRAMEWORK
SwiftUI UIKit
Text("Hello") let label = UILabel()
.font(.title) label.text = "Hello"
.padding() label.font = .title
view.addSubview(label)
Declarative ✨ Imperative 📝
5 baris kode 15+ baris kode


2. View Dasar

View adalah blok bangunan utama SwiftUI. Semua elemen UI adalah View — teks, gambar, tombol, dan container semua berupa View.

View Dasar: Text, Image, Button

Swift — Views
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {

            // Text View
            Text("Halo, SwiftUI! 🎉")
                .font(.largeTitle)
                .fontWeight(.bold)
                .foregroundColor(.blue)

            // Text dengan multi-line
            Text("Ini adalah tutorial SwiftUI yang sangat panjang dan akan otomatis wrap ke baris berikutnya.")
                .font(.body)
                .multilineTextAlignment(.center)
                .lineLimit(3)

            // Image View
            Image(systemName: "star.fill")
                .font(.system(size: 50))
                .foregroundColor(.yellow)

            // Async Image (load dari URL)
            AsyncImage(url: URL(string: "https://picsum.photos/200")) { image in
                image
                    .resizable()
                    .frame(width: 200, height: 150)
                    .clipShape(RoundedRectangle(cornerRadius: 10))
            } placeholder: {
                ProgressView()
            }

            // Button
            Button("Klik Saya!") {
                print("Tombol diklik!")
            }
            .buttonStyle(.borderedProminent)
            .tint(.green)

            // Button dengan custom label
            Button(action: {
                print("Aksi custom")
            }) {
                HStack {
                    Image(systemName: "heart.fill")
                    Text("Favorit")
                }
                .padding()
                .background(Color.red)
                .foregroundColor(.white)
                .cornerRadius(10)
            }

            // SF Symbol
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

Divider, Spacer, Group

Swift — Container Views
struct ContainerViews: View {
    var body: some View {
        VStack(spacing: 10) {
            Text("Atas")

            Divider()  // Garis pemisah

            Text("Tengah")

            Spacer()  // Mengisi ruang kosong

            Text("Bawah")
        }
        .padding()

        // Group — mengelompokkan view tanpa mempengaruhi layout
        Group {
            Text("Item 1")
            Text("Item 2")
            Text("Item 3")
        }

        // Section — untuk pembagian logis (bagus di List)
        Section("Header Section") {
            Text("Konten di dalam section")
        }
    }
}
💡 Tips

Semua elemen SwiftUI adalah View (struct yang mengimplementasi View protocol). body computed property adalah satu-satunya requirement. SwiftUI menggunakan ViewBuilder result builder untuk menulis views secara deklaratif.



3. Modifiers

Modifiers digunakan untuk mengubah tampilan atau perilaku view. Setiap modifier menghasilkan view baru (immutable pattern), sehingga urutan modifier penting!

Swift — Modifiers
struct ModifierContoh: View {
    var body: some View {
        VStack(spacing: 20) {

            // Urutan modifier PENTING!
            // Background dulu baru padding vs padding dulu baru background
            Text("Hello A")
                .background(Color.yellow)  // Background di belakang teks
                .padding()
                // Result: padding di LUAR background kuning

            Text("Hello B")
                .padding()
                .background(Color.green)  // Background di belakang+padding
                // Result: background hijau MENGELILINGI teks+padding

            // Font & Color
            Text("Styled Text")
                .font(.title)
                .fontWeight(.semibold)
                .foregroundColor(.white)
                .padding()
                .background(Color.blue)
                .cornerRadius(10)

            // Frame
            Text("Frame Modifier")
                .font(.headline)
                .frame(width: 300, height: 100)
                .background(Color.gray.opacity(0.2))
                .border(Color.red, width: 2)

            // Shadow & Opacity
            Text("Shadow Text")
                .font(.title2)
                .padding()
                .background(Color.purple)
                .foregroundColor(.white)
                .cornerRadius(10)
                .shadow(color: .purple.opacity(0.5), radius: 10, x: 0, y: 5)

            // Clip Shape
            Image(systemName: "person.circle.fill")
                .font(.system(size: 80))
                .clipShape(Circle())
                .overlay(Circle().stroke(Color.white, lineWidth: 4))
                .shadow(radius: 10)

            // Overlay & Background
            Text("Overlay Example")
                .font(.headline)
                .padding()
                .background(
                    RoundedRectangle(cornerRadius: 12)
                        .fill(Color.blue)
                )
                .overlay(
                    RoundedRectangle(cornerRadius: 12)
                        .stroke(Color.white, lineWidth: 2)
                )
        }
        .padding()
    }
}

Custom Modifier

Swift — Custom Modifier
// Custom modifier menggunakan ViewModifier protocol
struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.white)
            .cornerRadius(12)
            .shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: 2)
    }
}

// Extension untuk kemudahan penggunaan
extension View {
    func cardStyle() -> some View {
        self.modifier(CardModifier())
    }
}

// Penggunaan
struct CardContoh: View {
    var body: some View {
        VStack {
            Text("Kartu Saya")
                .font(.title2)
            Text("Ini menggunakan custom modifier")
                .font(.body)
        }
        .cardStyle()  // Sangat mudah digunakan!
    }
}

4. Layout System

SwiftUI menggunakan tiga container utama untuk menyusun layout: VStack (vertikal), HStack (horizontal), dan ZStack (lapisan/tumpukan).

Swift — Layout System
struct LayoutContoh: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {

                // VStack — vertical stack
                VStack(alignment: .leading, spacing: 8) {
                    Text("VStack — Vertikal")
                        .font(.headline)
                    Text("Item 1")
                    Text("Item 2")
                    Text("Item 3")
                }
                .padding()
                .background(Color.blue.opacity(0.1))
                .cornerRadius(10)

                // HStack — horizontal stack
                HStack(spacing: 15) {
                    Text("HStack —")
                    Text("Horizontal")
                    Image(systemName: "arrow.right")
                }
                .padding()
                .background(Color.green.opacity(0.1))
                .cornerRadius(10)

                // ZStack — z-axis (tumpukan)
                ZStack {
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 100, height: 100)
                    Circle()
                        .fill(Color.white)
                        .frame(width: 60, height: 60)
                    Text("🎯")
                        .font(.title)
                }

                // Grid Layout (iOS 16+)
                Grid(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 10) {
                    GridRow {
                        Text("Nama:")
                        Text("Andi")
                        Text("Kelas:")
                        Text("XII")
                    }
                    GridRow {
                        Text("NIM:")
                        Text("12345")
                        Text("Jurusan:")
                        Text("TKJ")
                    }
                }
                .padding()
                .background(Color.orange.opacity(0.1))
                .cornerRadius(10)

                // LazyVGrid — grid dinamis
                let columns = [
                    GridItem(.flexible()),
                    GridItem(.flexible()),
                    GridItem(.flexible())
                ]

                LazyVGrid(columns: columns, spacing: 16) {
                    ForEach(0..<12) { index in
                        RoundedRectangle(cornerRadius: 10)
                            .fill(Color.blue.opacity(0.3))
                            .frame(height: 80)
                            .overlay(Text("\(index + 1)"))
                    }
                }

                // GeometryReader — responsive layout
                GeometryReader { geometry in
                    HStack(spacing: 0) {
                        Rectangle()
                            .fill(Color.blue)
                            .frame(width: geometry.size.width * 0.3)
                        Rectangle()
                            .fill(Color.green)
                            .frame(width: geometry.size.width * 0.7)
                    }
                }
                .frame(height: 100)
                .cornerRadius(10)
            }
            .padding()
        }
    }
}


5. State

State adalah mekanisme SwiftUI untuk mengelola data. Ketika state berubah, SwiftUI secara otomatis membangun ulang view yang terpengaruh.

Swift — @State
struct CounterView: View {
    // @State — menyimpan data lokal yang bisa berubah
    @State private var count = 0
    @State private var isOn = false
    @State private var nama = ""
    @State private var showAlert = false

    var body: some View {
        VStack(spacing: 20) {
            // Counter
            Text("Count: \(count)")
                .font(.largeTitle)

            HStack(spacing: 20) {
                Button("➖ Kurangi") {
                    count -= 1
                }
                .buttonStyle(.bordered)

                Button("➕ Tambah") {
                    count += 1
                }
                .buttonStyle(.borderedProminent)
            }

            Divider()

            // Toggle
            Toggle("Mode Gelap", isOn: $isOn)
                .padding()
                .background(isOn ? Color.black : Color.white)
                .foregroundColor(isOn ? .white : .black)
                .cornerRadius(10)

            // Text Field
            TextField("Masukkan nama...", text: $nama)
                .textFieldStyle(.roundedBorder)
                .padding(.horizontal)

            Text("Halo, \(nama.isEmpty ? "Pengguna" : nama)!")
                .font(.title2)

            // Alert
            Button("Tampilkan Alert") {
                showAlert = true
            }
            .alert("Perhatian!", isPresented: $showAlert) {
                Button("OK") { }
            } message: {
                Text("Ini adalah alert dari SwiftUI")
            }
        }
        .padding()
    }
}

// @StateObject — untuk object yang lebih kompleks
class DataModel: ObservableObject {
    @Published var items: [String] = ["Apel", "Mangga", "Jeruk"]
    @Published var searchText = ""

    var filteredItems: [String] {
        if searchText.isEmpty {
            return items
        }
        return items.filter { $0.localizedCaseInsensitiveContains(searchText) }
    }

    func tambahItem(_ item: String) {
        items.append(item)
    }

    func hapusItem(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
}

struct DataListView: View {
    @StateObject private var model = DataModel()

    var body: some View {
        VStack {
            TextField("Cari buah...", text: $model.searchText)
                .textFieldStyle(.roundedBorder)
                .padding()

            List {
                ForEach(model.filteredItems, id: \.self) { item in
                    Text(item)
                }
                .onDelete(perform: model.hapusItem)
            }

            Button("Tambah Nanas") {
                model.tambahItem("Nanas")
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

Property Wrappers SwiftUI

Wrapper Kegunaan
@StateData lokal di dalam satu View (value type)
@BindingReferensi ke @State di parent view
@StateObjectMembuat & memiliki ObservableObject di view ini
@ObservedObjectMengamati ObservableObject yang dimiliki orang lain
@EnvironmentObjectMenerima object dari environment (shared)
@EnvironmentMengakses system environment values
@PublishedMendeklarasikan property yang bisa diamati di ObservableObject


6. Binding

Binding memungkinkan child view mengakses dan mengubah state yang dimiliki parent view. Binding menggunakan prefix $ di depan nama state.

Swift — @Binding
// Child view yang menerima binding
struct StepperView: View {
    @Binding var value: Int  // Binding ke state parent
    var label: String

    var body: some View {
        HStack {
            Text(label)
                .font(.headline)

            Spacer()

            Button("−") {
                if value > 0 { value -= 1 }
            }
            .buttonStyle(.bordered)
            .disabled(value <= 0)

            Text("\(value)")
                .font(.title2)
                .frame(width: 50)

            Button("+") {
                value += 1
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(10)
    }
}

// Toggle component dengan binding
struct CustomToggle: View {
    @Binding var isOn: Bool
    var title: String

    var body: some View {
        HStack {
            Text(title)
            Spacer()
            Toggle("", isOn: $isOn)
                .labelsHidden()
        }
        .padding()
    }
}

// Parent view menggunakan child dengan binding
struct OrderView: View {
    @State private var nasi = 0
    @State private var ayam = 0
    @State private var sayur = 0
    @State private var pesanAntar = false

    var totalItem: Int {
        nasi + ayam + sayur
    }

    var body: some View {
        VStack(spacing: 15) {
            Text("🛒 Pesanan Makanan")
                .font(.title2)
                .fontWeight(.bold)

            StepperView(value: $nasi, label: "🍚 Nasi")
            StepperView(value: $ayam, label: "🍗 Ayam")
            StepperView(value: $sayur, label: "🥬 Sayur")
            CustomToggle(isOn: $pesanAntar, title: "🏍️ Pesan Antar")

            Divider()

            HStack {
                Text("Total Item: \(totalItem)")
                    .font(.headline)
                Spacer()
                Text(totalItem > 0 ? "Siap Dipesan ✅" : "Belum ada pesanan")
                    .foregroundColor(totalItem > 0 ? .green : .gray)
            }
            .padding()

            if pesanAntar && totalItem > 0 {
                Text("Ongkir: Rp 10.000")
                    .font(.caption)
                    .foregroundColor(.orange)
            }
        }
        .padding()
    }
}

#Preview {
    OrderView()
}

SwiftUI menyediakan NavigationStack (iOS 16+) dan NavigationView untuk membuat navigasi antar halaman.

Swift — Navigation
// Model data
struct Mahasiswa: Identifiable {
    let id = UUID()
    let nama: String
    let nim: String
    let jurusan: String
}

// Detail View
struct MahasiswaDetailView: View {
    let mahasiswa: Mahasiswa

    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "person.circle.fill")
                .font(.system(size: 100))
                .foregroundColor(.blue)

            Text(mahasiswa.nama)
                .font(.title)
                .fontWeight(.bold)

            VStack(alignment: .leading, spacing: 10) {
                DetailRow(label: "NIM", value: mahasiswa.nim)
                DetailRow(label: "Jurusan", value: mahasiswa.jurusan)
            }
            .padding()
            .background(Color.gray.opacity(0.1))
            .cornerRadius(12)
        }
        .padding()
        .navigationTitle("Detail Mahasiswa")
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct DetailRow: View {
    let label: String
    let value: String

    var body: some View {
        HStack {
            Text(label)
                .fontWeight(.semibold)
                .frame(width: 80, alignment: .leading)
            Text(value)
            Spacer()
        }
    }
}

// Main View dengan NavigationStack
struct DaftarMahasiswaView: View {
    let data = [
        Mahasiswa(nama: "Andi Pratama", nim: "12345", jurusan: "Teknik Informatika"),
        Mahasiswa(nama: "Siti Rahayu", nim: "12346", jurusan: "Sistem Informasi"),
        Mahasiswa(nama: "Budi Santoso", nim: "12347", jurusan: "Teknik Komputer"),
        Mahasiswa(nama: "Dewi Lestari", nim: "12348", jurusan: "Teknik Informatika")
    ]

    var body: some View {
        NavigationStack {
            List(data) { mhs in
                NavigationLink(value: mhs) {
                    HStack {
                        Image(systemName: "person.circle")
                            .font(.title2)
                            .foregroundColor(.blue)
                        VStack(alignment: .leading) {
                            Text(mhs.nama)
                                .font(.headline)
                            Text(mhs.jurusan)
                                .font(.caption)
                                .foregroundColor(.gray)
                        }
                    }
                }
            }
            .navigationTitle("Daftar Mahasiswa")
            .navigationDestination(for: Mahasiswa.self) { mhs in
                MahasiswaDetailView(mahasiswa: mhs)
            }
        }
    }
}

// Navigation dengan Sheet (modal)
struct SheetContoh: View {
    @State private var showSheet = false
    @State private var showFullScreen = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Tampilkan Sheet") {
                showSheet = true
            }
            .buttonStyle(.borderedProminent)

            Button("Full Screen Cover") {
                showFullScreen = true
            }
        }
        .sheet(isPresented: $showSheet) {
            VStack(spacing: 20) {
                Text("Ini Sheet Modal 📋")
                    .font(.title)
                Button("Tutup") {
                    showSheet = false
                }
            }
        }
        .fullScreenCover(isPresented: $showFullScreen) {
            VStack(spacing: 20) {
                Text("Full Screen Cover 🖥️")
                    .font(.title)
                Button("Tutup") {
                    showFullScreen = false
                }
            }
        }
    }
}


8. List & ForEach

List adalah komponen untuk menampilkan data dalam bentuk daftar scrollable. ForEach digunakan untuk mengulang elemen di dalam List.

Swift — List & ForEach
struct ListContoh: View {
    @State private var buah = ["Apel 🍎", "Mangga 🥭", "Jeruk 🍊", "Pisang 🍌", "Anggur 🍇"]
    @State private var favorit: Set<String> = []

    var body: some View {
        NavigationStack {
            List {
                // Section 1: Daftar Buah
                Section("Buah-buahan 🍓") {
                    ForEach(buah, id: \.self) { item in
                        HStack {
                            Text(item)
                            Spacer()
                            if favorit.contains(item) {
                                Image(systemName: "heart.fill")
                                    .foregroundColor(.red)
                            }
                        }
                        .onTapGesture {
                            if favorit.contains(item) {
                                favorit.remove(item)
                            } else {
                                favorit.insert(item)
                            }
                        }
                    }
                    .onDelete { offsets in
                        buah.remove(atOffsets: offsets)
                    }
                    .onMove { from, to in
                        buah.move(fromOffsets: from, toOffset: to)
                    }
                }

                // Section 2: Info
                Section("Info") {
                    HStack {
                        Text("Total Buah")
                        Spacer()
                        Text("\(buah.count)")
                            .foregroundColor(.blue)
                    }
                    HStack {
                        Text("Favorit")
                        Spacer()
                        Text("\(favorit.count)")
                            .foregroundColor(.red)
                    }
                }
            }
            .navigationTitle("Daftar Buah")
            .toolbar {
                EditButton()  // Edit button untuk delete & move
            }
        }
    }
}

// List dengan Identifiable struct
struct Produk: Identifiable {
    let id = UUID()
    let nama: String
    let harga: Int
    let kategori: String
}

struct ProdukListView: View {
    let produkList = [
        Produk(nama: "Laptop", harga: 15000000, kategori: "Elektronik"),
        Produk(nama: "Mouse", harga: 150000, kategori: "Aksesoris"),
        Produk(nama: "Keyboard", harga: 500000, kategori: "Aksesoris"),
        Produk(nama: "Monitor", harga: 3000000, kategori: "Elektronik")
    ]

    var body: some View {
        List(produkList) { produk in
            HStack {
                VStack(alignment: .leading) {
                    Text(produk.nama)
                        .font(.headline)
                    Text(produk.kategori)
                        .font(.caption)
                        .foregroundColor(.gray)
                }
                Spacer()
                Text("Rp \(produk.harga)")
                    .fontWeight(.semibold)
                    .foregroundColor(.blue)
            }
            .padding(.vertical, 4)
        }
        .navigationTitle("Produk")
    }
}

9. Form & Input

Form menyediakan wrapper yang memformat input dengan cara standar iOS — sangat cocok untuk pengaturan dan pengisian data.

Swift — Form
struct FormContoh: View {
    @State private var nama = ""
    @State private var email = ""
    @State private var umur = 25.0
    @State private var isMahasiswa = false
    @State private var jurusan = "Teknik Informatika"
    @State private var tanggal = Date()
    @State private var rating = 3

    let jurusanList = [
        "Teknik Informatika",
        "Sistem Informasi",
        "Teknik Komputer",
        "Manajemen Informatika"
    ]

    var body: some View {
        NavigationStack {
            Form {
                // Section: Data Pribadi
                Section("Data Pribadi") {
                    TextField("Nama Lengkap", text: $nama)
                    TextField("Email", text: $email)
                        .keyboardType(.emailAddress)
                        .autocapitalization(.none)

                    VStack(alignment: .leading) {
                        Text("Umur: \(Int(umur)) tahun")
                        Slider(value: $umur, in: 15...65, step: 1)
                    }

                    DatePicker("Tanggal Lahir", selection: $tanggal, displayedComponents: .date)
                }

                // Section: Akademik
                Section("Akademik") {
                    Toggle("Mahasiswa", isOn: $isMahasiswa)

                    if isMahasiswa {
                        Picker("Jurusan", selection: $jurusan) {
                            ForEach(jurusanList, id: \.self) { j in
                                Text(j)
                            }
                        }
                    }
                }

                // Section: Rating
                Section("Rating Aplikasi") {
                    HStack {
                        ForEach(1...5, id: \.self) { star in
                            Image(systemName: star <= rating ? "star.fill" : "star")
                                .foregroundColor(.yellow)
                                .onTapGesture {
                                    rating = star
                                }
                        }
                    }
                }

                // Section: Submit
                Section {
                    Button("Simpan Data") {
                        print("Nama: \(nama)")
                        print("Email: \(email)")
                        print("Rating: \(rating)")
                    }
                    .frame(maxWidth: .infinity)
                    .disabled(nama.isEmpty || email.isEmpty)
                }
            }
            .navigationTitle("Formulir Pendaftaran")
        }
    }
}


10. Animasi & Transisi

SwiftUI memiliki animasi bawaan yang sangat mudah digunakan — cukup dengan modifier .animation() atau withAnimation block.

Swift — Animasi
struct AnimasiContoh: View {
    @State private var isScaled = false
    @State private var isRotated = false
    @State private var showDetail = false
    @State private var opacity = 1.0

    var body: some View {
        VStack(spacing: 30) {
            // Scale Animation
            Image(systemName: "heart.fill")
                .font(.system(size: 60))
                .foregroundColor(.red)
                .scaleEffect(isScaled ? 1.5 : 1.0)
                .animation(.spring(response: 0.3, dampingFraction: 0.5), value: isScaled)
                .onTapGesture {
                    isScaled.toggle()
                }

            // Rotation Animation
            Image(systemName: "gear")
                .font(.system(size: 50))
                .rotationEffect(.degrees(isRotated ? 360 : 0))
                .animation(.linear(duration: 1.0).repeatForever(autoreverses: false), value: isRotated)
                .onAppear {
                    isRotated = true
                }

            // Transition
            Button("Toggle Detail") {
                withAnimation(.easeInOut(duration: 0.3)) {
                    showDetail.toggle()
                }
            }

            if showDetail {
                VStack {
                    Text("Detail View 📝")
                        .font(.title2)
                    Text("Ini menggunakan transition")
                }
                .padding()
                .background(Color.blue.opacity(0.1))
                .cornerRadius(10)
                .transition(.slide)
            }

            // Opacity Animation
            Rectangle()
                .fill(Color.purple)
                .frame(width: 200, height: 100)
                .opacity(opacity)
                .cornerRadius(10)
                .animation(.easeInOut(duration: 0.5), value: opacity)

            Button("Fade In/Out") {
                opacity = opacity == 1.0 ? 0.0 : 1.0
            }
        }
        .padding()
    }
}

11. Contoh App Lengkap: Todo List

Swift — TodoApp
import SwiftUI

// Model
struct TodoItem: Identifiable {
    let id = UUID()
    var title: String
    var isCompleted: Bool = false
}

// ViewModel
class TodoViewModel: ObservableObject {
    @Published var items: [TodoItem] = []
    @Published var newTitle = ""

    func tambahItem() {
        guard !newTitle.isEmpty else { return }
        items.append(TodoItem(title: newTitle))
        newTitle = ""
    }

    func toggleComplete(_ item: TodoItem) {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index].isCompleted.toggle()
        }
    }

    func hapusItem(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }

    var completedCount: Int {
        items.filter { $0.isCompleted }.count
    }
}

// Main View
struct TodoAppView: View {
    @StateObject private var viewModel = TodoViewModel()

    var body: some View {
        NavigationStack {
            VStack {
                // Input Section
                HStack {
                    TextField("Tambah tugas baru...", text: $viewModel.newTitle)
                        .textFieldStyle(.roundedBorder)
                        .onSubmit { viewModel.tambahItem() }

                    Button(action: viewModel.tambahItem) {
                        Image(systemName: "plus.circle.fill")
                            .font(.title2)
                            .foregroundColor(.blue)
                    }
                }
                .padding()

                // Progress
                if !viewModel.items.isEmpty {
                    ProgressView(value: Double(viewModel.completedCount),
                                 total: Double(viewModel.items.count))
                    .padding(.horizontal)
                    .tint(.green)

                    Text("\(viewModel.completedCount)/\(viewModel.items.count) selesai")
                        .font(.caption)
                        .foregroundColor(.gray)
                }

                // List
                List {
                    ForEach(viewModel.items) { item in
                        HStack {
                            Image(systemName: item.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundColor(item.isCompleted ? .green : .gray)
                                .onTapGesture { viewModel.toggleComplete(item) }

                            Text(item.title)
                                .strikethrough(item.isCompleted)
                                .foregroundColor(item.isCompleted ? .gray : .primary)
                        }
                    }
                    .onDelete(perform: viewModel.hapusItem)
                }
            }
            .navigationTitle("📝 Todo List")
            .toolbar {
                EditButton()
            }
        }
    }
}

#Preview {
    TodoAppView()
}


12. Quiz Pemahaman

Uji pemahaman Anda tentang SwiftUI dengan quiz singkat berikut!

1. Apa perbedaan antara @State dan @Binding?

a) @State untuk data global, @Binding untuk data lokal
b) @State menyimpan data lokal yang dimiliki view, @Binding mereferensikan @State dari parent
c) Tidak ada perbedaan
d) @State untuk class, @Binding untuk struct

2. Container apa yang menyusun elemen secara horizontal di SwiftUI?

a) VStack
b) ZStack
c) HStack
d) Grid

3. Mengapa urutan modifier penting di SwiftUI?

a) Karena modifier diterapkan dari bawah ke atas
b) Karena setiap modifier menghasilkan view baru, sehingga urutan mempengaruhi hasil akhir
c) Urutan modifier tidak penting
d) Karena SwiftUI mengacak urutan modifier

4. Apa fungsi @StateObject dibanding @ObservedObject?

a) @StateObject membuat dan memiliki object, @ObservedObject hanya mengamati
b) Keduanya sama saja
c) @StateObject lebih cepat dari @ObservedObject
d) @ObservedObject deprecated, harus pakai @StateObject

5. Bagaimana cara memicu animasi di SwiftUI saat data berubah?

a) Panggil UIView.animate()
b) Gunakan modifier .animation() atau withAnimation { }
c) Animasi otomatis tanpa kode tambahan
d) Gunakan Timer.scheduledTimer

🔍 Zoom
100%
🎨 Tema