This episode covers SwiftUI: the basics of views and layout in a declarative style, state management with @State, @Binding, @ObservedObject, and @EnvironmentObject, animations and transitions for adaptive UI, and interoperability between UIKit and SwiftUI.

SwiftUI changes how you build interfaces: from imperative ("do this, then that") to declarative ("here's what you want"). Episode 17 covers SwiftUI and declarative UI — from the basic concepts of views and layout, to state management with property wrappers, to animations and interoperability with UIKit.
With SwiftUI, you describe the UI as a function of state: when state changes, SwiftUI recomputes the view and updates what changed. Understanding this state model is the key to mastering SwiftUI.
A SwiftUI view is a struct that describes its appearance and behavior:
import SwiftUI
struct GreetingView: View {
var body: some View {
VStack(spacing: 12) {
Text("Halo, SwiftUI")
.font(.largeTitle)
Text("Belajar deklaratif")
}
.padding()
}
}struct GreetingView: View declares that the view is data — a combination of components with modifiers like .font and .padding. VStack arranges its children vertically. This UI renders identically on iPhone, iPad, Mac, Apple Watch, and Apple TV.
Layout composition is built from three basic stacks:
HStack {
Image(systemName: "heart.fill")
Text("Menyukai Swift")
}
.padding(12)HStack { ... } places an icon and text side by side. Stacks combine freely to build complex layouts, with Spacer and frame controlling space distribution.
@State is the view's local source of truth. When its value changes, the view re-renders:
struct CounterView: View {
@State private var jumlah = 0
var body: some View {
VStack {
Text("Jumlah: \(jumlah)")
TombolTambah(jumlah: $jumlah)
}
}
}
struct TombolTambah: View {
@Binding var jumlah: Int
var body: some View {
Button("Tambah") {
jumlah += 1
}
}
}@State private var jumlah = 0 stores the view's local state, and @Binding var jumlah: Int lets a child view read and modify state owned by the parent. Bindings keep a single source of truth without moving state into the child.
For more widely shared state, episode 16 introduced @StateObject. When a state object is owned elsewhere, use @ObservedObject; and to inject it throughout the whole hierarchy, use @EnvironmentObject:
@main
struct AppUtama: App {
@StateObject var sesi = SesiPengguna()
var body: some Scene {
WindowGroup {
KontenView()
.environmentObject(sesi)
}
}
}.environmentObject(sesi) makes sesi available to every view below it without passing it down one by one. Views that need it simply declare @EnvironmentObject var sesi: SesiPengguna. This is the most practical way to share global state like login sessions and themes.
SwiftUI animates state changes automatically with .animation (implicit) or withAnimation (explicit):
struct AnimasiView: View {
@State private var diperbesar = false
var body: some View {
Button("Tekan") {
withAnimation(.spring(response: 0.4, dampingFraction: 0.6)) {
diperbesar.toggle()
}
}
.scaleEffect(diperbesar ? 1.4 : 1.0)
}
}withAnimation(.spring(...)) { diperbesar.toggle() } animates the changes inside its block — here, the button's scale change. Choose durations and curves that fit; spring gives a natural feel for user interactions.
Visual changes between states can also be animated with transitions:
if ditampilkan {
DetailView()
.transition(.opacity.combined(with: .slide))
}.transition(.opacity.combined(with: .slide)) combines fade and slide effects as the view appears or disappears. Transitions require an animation around them — without withAnimation or .animation, the change happens instantly.
SwiftUI adapts to screen size through responsive capabilities: an HStack can switch to a VStack depending on the horizontal size class. Use @Environment to read size, and ViewThatFits to pick a layout that fits the available space.
SwiftUI and UIKit coexist in one project. UIViewRepresentable wraps a UIKit view so it can be used in SwiftUI, and UIViewControllerRepresentable wraps a View Controller:
import UIKit
import SwiftUI
struct MapViewWrapper: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
return MKMapView()
}
func updateUIView(_ uiView: MKMapView, context: Context) {}
}makeUIView(context:) creates the UIKit view and updateUIView(_:context:) updates it when SwiftUI changes. This interop means you can adopt SwiftUI gradually without discarding existing UIKit — and the reverse direction, embedding SwiftUI inside UIKit, uses UIHostingController.
Info
Property wrappers are the core of SwiftUI's state model. Start with @State for local state, @Binding for passing it down, then @StateObject and @EnvironmentObject for shared state. This progression keeps your architecture simple.
Key takeaways:
@State for local state, @Binding for propagating changes.@ObservedObject and @EnvironmentObject share state within a hierarchy.withAnimation and .transition bring state changes to life.UIViewRepresentable bridges UIKit and SwiftUI in one project.In the next episode, episode 18, we'll cover modern tooling and build automation — Xcode project management and build settings, continuous integration with Xcode Cloud, GitHub Actions, and Bitrise, code signing with provisioning profiles, and Fastlane for build and deployment automation. Your pipeline runs automatically!