Learn Swift - SwiftUI & Declarative UI
Series/Learn Swift/Episode 17
Episode 17 of 23

Learn Swift - SwiftUI & Declarative UI

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

Basics of Views and Layout

Describing UI Declaratively

A SwiftUI view is a struct that describes its appearance and behavior:

Your first SwiftUI view
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 with Stacks

Layout composition is built from three basic stacks:

  • HStack: arranges children horizontally.
  • VStack: arranges children vertically.
  • ZStack: stacks children along the depth axis.
Combining 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 Management

@State and @Binding

@State is the view's local source of truth. When its value changes, the view re-renders:

@State and @Binding
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.

@ObservedObject and @EnvironmentObject

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:

EnvironmentObject in a hierarchy
@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.

Animations and Transitions

Implicit and Explicit Animations

SwiftUI animates state changes automatically with .animation (implicit) or withAnimation (explicit):

Animation with withAnimation
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.

Transitions Between Views

Visual changes between states can also be animated with transitions:

Transition with transition
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.

Adaptive UI and UIKit Interop

Adaptive UI

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.

UIKit and SwiftUI Interop

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:

Wrapping UIKit in SwiftUI
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.

Closing

Key takeaways:

  • SwiftUI is declarative UI: the view is a function of state.
  • HStack, VStack, and ZStack compose layout.
  • @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!

Learn Swift - SwiftUI & Declarative UI | Learn Swift