SwiftUI: Building iOS Apps with Declarative UI
SwiftUI is Apple’s declarative framework for building iOS, macOS, watchOS, and tvOS user interfaces using Swift code — replacing UIKit’s imperative approach with concise, reactive view definitions that automatically update when data changes.
What You’ll Learn
You’ll build layouts with VStack, HStack, and ZStack, manage state with @State, @Binding, and @ObservedObject, navigate with NavigationStack, create lists and forms, apply modifiers, use previews for rapid development, and integrate UIKit components when needed.
Why SwiftUI Matters
SwiftUI represents Apple’s future of UI development. Doda Browser for iOS could be rebuilt entirely in SwiftUI with half the code of UIKit. Apple pushes SwiftUI aggressively — new APIs and features in iOS 17+ are SwiftUI-first or SwiftUI-only. Learning SwiftUI today prepares you for the next decade of Apple platform development.
SwiftUI Learning Path
flowchart LR
A[Mobile Development Overview] --> B[iOS Development]
B --> C[SwiftUI Basics]
C --> D[Data Flow & State]
D --> E[Navigation & Lists]
E --> F[Networking & Storage]
C:::current
classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Understanding SwiftUI’s Declarative Model
UIKit is imperative — you tell the system step-by-step what to do:
// UIKit: Create a label, set text, add to view
let label = UILabel()
label.text = "Hello"
label.textColor = .blue
label.font = .systemFont(ofSize: 18)
view.addSubview(label)SwiftUI is declarative — you describe what the UI should look like, and the system figures out how to create and update it:
// SwiftUI: Describe the UI, system handles the rest
Text("Hello")
.foregroundColor(.blue)
.font(.system(size: 18))The difference: you describe what (the UI state), not how (create, configure, layout). SwiftUI automatically re-renders views when state changes.
Your First SwiftUI View
import SwiftUI
struct GreetingView: View {
@State private var name = ""
var body: some View {
VStack(spacing: 20) {
Text("Welcome to DodaTech")
.font(.largeTitle)
.fontWeight(.bold)
TextField("Enter your name", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.horizontal)
if !name.isEmpty {
Text("Hello, \(name)!")
.font(.title2)
.foregroundColor(.blue)
.transition(.opacity)
}
Button("Tap Me") {
name = ""
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}What’s happening:
@Statedeclares mutable state — whennamechanges, SwiftUI re-renders the view$nameis a binding to the state —TextFieldreads and writes it- The
ifstatement conditionally shows/hides the greeting, animated with.transition(.opacity) VStackarranges children vertically with 20pt spacing
Expected output:
- A screen with “Welcome to DodaTech” title, a text field, and a button
- As the user types, “Hello, [name]!” appears below the field
- Tapping “Tap Me” clears the field and hides the greeting
Layout Containers: VStack, HStack, ZStack
struct LayoutExample: View {
var body: some View {
VStack(spacing: 16) {
Text("Vertical Stack")
HStack(spacing: 12) {
RoundedRectangle(cornerRadius: 8)
.fill(.blue)
.frame(width: 50, height: 50)
RoundedRectangle(cornerRadius: 8)
.fill(.green)
.frame(width: 50, height: 50)
RoundedRectangle(cornerRadius: 8)
.fill(.orange)
.frame(width: 50, height: 50)
}
ZStack {
Circle()
.fill(.yellow)
.frame(width: 100, height: 100)
Text("Overlay")
.foregroundColor(.black)
.fontWeight(.bold)
}
}
}
}| Container | Direction | Use Case |
|---|---|---|
VStack | Vertical | Forms, profiles, any top-to-bottom layout |
HStack | Horizontal | Toolbars, row items, button groups |
ZStack | Depth (z-axis) | Badges, overlays, backgrounds |
@State, @Binding, and @ObservedObject
SwiftUI provides property wrappers to manage data flow:
// 1. @State — local view state (value type)
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") { count += 1 }
}
}
}
// 2. @Binding — passes state to child views (no ownership transfer)
struct ChildView: View {
@Binding var count: Int
var body: some View {
Button("Add") { count += 1 }
}
}
// 3. @ObservedObject — external reference type model
class UserSettings: ObservableObject {
@Published var username = ""
@Published var notificationsEnabled = true
}
struct SettingsView: View {
@ObservedObject var settings = UserSettings()
var body: some View {
Toggle("Notifications", isOn: $settings.notificationsEnabled)
TextField("Username", text: $settings.username)
}
}SwiftUI Data Flow Diagram
flowchart TD
subgraph Source of Truth
State["@State (local)"]
Observed["@ObservedObject / @StateObject"]
Env["@EnvironmentObject"]
end
State -->|$binding| Child1[Child View]
Observed -->|$binding| Child2[Child View]
Env -->|passed down| Child3[Any descendant]
Child1 -->|changes propagate| SwiftUI[SwiftUI re-renders]
Child2 --> SwiftUI
Child3 --> SwiftUI
NavigationStack and List
NavigationStack (iOS 16+) replaces the older NavigationView:
struct ProductListView: View {
let products = ["MacBook Pro", "iPhone 15", "AirPods", "iPad Air"]
var body: some View {
NavigationStack {
List(products, id: \.self) { product in
NavigationLink(destination: ProductDetailView(name: product)) {
HStack {
Image(systemName: "shippingbox")
.foregroundColor(.blue)
Text(product)
}
}
}
.navigationTitle("DodaTech Store")
}
}
}
struct ProductDetailView: View {
let name: String
@State private var quantity = 1
var body: some View {
VStack(spacing: 20) {
Text(name)
.font(.title)
Stepper("Quantity: \(quantity)", value: $quantity, in: 1...10)
Text("Total: $\(quantity * 99, specifier: "%.2f")")
.font(.headline)
Button("Add to Cart") {
print("Added \(quantity) x \(name)")
}
.buttonStyle(.borderedProminent)
}
.padding()
.navigationTitle("Product Details")
}
}Modifiers
Modifiers are methods that return a modified view. Order matters because each modifier wraps the previous view:
Text("Hello")
.font(.title) // 1. Apply title font
.foregroundColor(.blue) // 2. Make text blue
.padding() // 3. Add default padding
.background(.yellow) // 4. Add yellow background (behind padding)
.cornerRadius(12) // 5. Round corners
.shadow(radius: 5) // 6. Add shadowvs.
Text("Hello")
.padding()
.background(.yellow)
.cornerRadius(12)
// ✅ Padding is inside the background — the whole padded area is yellowCommon modifiers:
.padding()— adds space around the view.frame(width:height:)— sets explicit size.foregroundColor()— text/icon color.background()— background view.clipShape()— clips to a shape.opacity()— transparency (0-1)
Previews
Xcode previews show your SwiftUI code rendered in real time:
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
}
}
#Preview("Light Mode") {
ContentView()
}
#Preview("Dark Mode") {
ContentView()
.preferredColorScheme(.dark)
}
#Preview("Small Device") {
ContentView()
.previewDevice("iPhone SE (3rd generation)")
}Integrating UIKit
When SwiftUI doesn’t support something, wrap UIKit with UIViewRepresentable:
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
}
// Usage in SwiftUI
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://dodatech.com")!)
}
}Common SwiftUI Errors
1. Modifier Order Mistakes
Putting .padding() after .background() applies the background only to the unpadded view. The order of modifiers creates the view hierarchy — apply padding first for full-coverage backgrounds.
2. Forgetting $ for Bindings
TextField("Name", text: name) // ❌ Compiler error: expects Binding
TextField("Name", text: $name) // ✅ Correct binding syntax3. State Mutation in Body
var body: some View {
// ❌ Never mutate state during view rendering
count += 1
return Text("Count: \(count)")
}Mutating state in body causes infinite re-rendering. Move mutations to button actions or event handlers.
4. Using @State for Reference Types
@State works with value types (structs, strings, ints). For classes, use @StateObject or @ObservedObject.
5. Preview Crashes from Missing Dependencies
Previews that access network, Core Data, or complex services crash without mock data. Provide sample data in previews.
6. NavigationStack Inside TabView
TabView {
NavigationStack {
HomeView()
}
.tabItem { Label("Home", systemImage: "house") }
}Each tab needs its own NavigationStack — sharing one across tabs breaks navigation.
7. Ignoring Accessibility
SwiftUI provides accessibility modifiers: .accessibilityLabel(), .accessibilityHint(), .accessibilityValue(). Skipping them excludes users with disabilities.
Practice Questions
1. What’s the difference between @State and @ObservedObject?
@State is for local value-type state owned by the view. @ObservedObject is for reference-type (class) models owned externally, with @Published properties that trigger updates.
2. Why does modifier order matter in SwiftUI?
Each modifier wraps the previous view. .padding().background(.red) gives the padded area a red background. .background(.red).padding() gives only the unpadded view a red background, leaving the padding transparent.
3. How do you integrate a UIKit view in SwiftUI?
Wrap the UIKit view in a struct conforming to UIViewRepresentable, implementing makeUIView() and updateUIView().
4. What’s the purpose of NavigationStack?
It manages a stack of views for drill-down navigation, automatically adding back buttons and handling the navigation bar.
5. Challenge: Build a todo list app.
Build a SwiftUI todo app with: @State for the list, TextField for new items, List with swipe-to-delete, and UserDefaults persistence. Add a button to mark items as complete with a strikethrough effect.
FAQ
Try It Yourself
Build a simple SwiftUI app:
- Create a new iOS app in Xcode using the SwiftUI template
- Add a
Listof items with navigation to a detail view - Add a form with
TextField,Toggle, andPicker - Use
@Stateand@Bindingto pass data between views - Run on the simulator and test the interactions
What’s Next
SwiftUI is the future of Apple development. Start by rebuilding a small UIKit screen in SwiftUI to see how much cleaner the code becomes. Then explore NavigationStack, Core Data integration, and animation APIs for your next iOS project.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro