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 Syntax | Cukup deskripsikan UI, SwiftUI akan membangunnya |
| Less Code | Kode lebih sedikit dibanding UIKit untuk hasil yang sama |
| Live Preview | Langsung melihat perubahan UI di Xcode tanpa build |
| Cross-Platform | Satu kode untuk iOS, macOS, watchOS, tvOS |
| Reactive | UI otomatis update saat data berubah |
| Accessible | Aksesibilitas (VoiceOver, Dynamic Type) built-in |
SwiftUI vs UIKit
| Aspek | SwiftUI | UIKit |
|---|---|---|
| Paradigma | Declarative | Imperative |
| Lines of Code | 🟢 Sedikit | 🔴 Banyak |
| State Management | 🟢 Otomatis (@State, @Binding) | 🟡 Manual |
| Preview | 🟢 Live Preview | 🔴 Build & Run |
| Min. iOS | iOS 13+ | iOS 2+ |
| Ekosistem | 🟡 Berkembang | 🟢 Sangat matang |
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
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
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")
}
}
}
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!
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
// 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).
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.
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 |
|---|---|
@State | Data lokal di dalam satu View (value type) |
@Binding | Referensi ke @State di parent view |
@StateObject | Membuat & memiliki ObservableObject di view ini |
@ObservedObject | Mengamati ObservableObject yang dimiliki orang lain |
@EnvironmentObject | Menerima object dari environment (shared) |
@Environment | Mengakses system environment values |
@Published | Mendeklarasikan 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.
// 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()
}
7. Navigation
SwiftUI menyediakan NavigationStack (iOS 16+) dan NavigationView untuk membuat navigasi antar halaman.
// 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.
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.
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.
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
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!