Many developers complain about SwiftUI because some of its behaviours feel counter-intuitive. For example, in the code at #1, if you comment out the .padding() modifier, the entire area above the search field turns red. That happens because the background of the search view bleeds into its parent container. If you don’t want the Search view’s background to affect the parent view, you have to break that view hierarchy connection.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
var body: some View {VStack(spacing: 24) { searchSpacer() }// .padding() // #1 .background(ThoughtStreamAsset.Colors.bgPrimary.swiftUIColor) }var search: some View {TextField("Search", text: $searchText, prompt: Text("Search your knowledge base").foregroundColor(.gray)) .foregroundColor(ThoughtStreamAsset.Colors.textPrimary.swiftUIColor) .padding() .background(Color.red) }
When the padding at #1 is not commented out, the layout renders as expected:
But when you remove the parent padding, SwiftUI changes how the backgroud is applied, causing the red search background to expand and cover areas you wouldn’t expect. These kinds of layout propagation rules are often why developers find SwiftUI unintuitive.
Before iOS 16, you could get the ScrollView’s offset using the following code. However, if the UI becomes complex, the scrollViewOffset will not update while scrolling.
I wrapped my view in a NavigationStack and used .toolbarVisibility(.hidden, for: .tabBar) to navigate while hiding the tab bar. However, this causes an issue: the transition animation becomes stiff and unnatural. I haven’t found a good solution yet.
I’m trying to research how Apple’s official apps handle this. It seems that many of them simply present a new view when they need to hide the tab bar.
The tab bar is generally always visible. When an app needs to hide it, the common pattern is to present a modal view, rather than pushing a view inside the existing navigation stack.
In Swift, Type Erasure is a key concept for understanding protocols and generics.
It explains why any exists and how Swift’s generic protocols differ from other languages.
1️⃣ Why Type Erasure Matters
1 2 3 4
protocolReReduce {}structAppReduce: ReReduce {}let a: any ReReduce = AppReduce()
any ReReduce means “a type that conforms to ReReduce,” , but the exact type is unknown at compile time.
The compiler performs type erasure, wrapping the value in an existential container. This allows storing and calling methods on multiple conforming types in one collection.
This post is about creating a confetti animation in SwiftUI with a customizable duration. I learned the technique from this Patreon tutorial.
In the original animation, the confetti pieces would change color while falling, which I considered a bug. I’ve since fixed it. Another optimization I considered was adding a fade-out effect to make the disappearance of the confetti smoother, but the result wasn’t satisfactory, so I have not included that change.
Let’s break down how to build this.
Step 1: Animate a Single Piece of Confetti
First, we’ll create the animation for a single piece of confetti. The code and the result are below:
This line of code is the key to fixing the color-changing issue:
@State private var color: Color = [.orange, .green, .blue, .red, .yellow].randomElement() ?? .green
The color used to change because the original code would assign a new random color during the animation process. By using @State to store the color, we ensure its value is preserved for the view’s lifetime, preventing the color from changing unexpectedly.
Step 2: Create the Confetti Container
Next, we’ll create a container to hold 50 confetti pieces and manage their positioning.
Sometimes, you have to explore different paths to find where you truly belong. For a developer, that path is often paved with different languages, frameworks, and platforms. My own journey has taken me through the worlds of backend development, cross-platform apps with React Native and Flutter, and of course, native iOS. And after seeing what each has to offer, I’ve come to a clear and exciting realization: my true passion lies in building for native iOS.
A Journey Across the Stack
Every technology I’ve worked with has taught me something invaluable.
Building backend systems gave me a deep appreciation for data architecture, APIs, and the logic that powers applications from behind the scenes.
Working with React Native and Flutter was a fantastic lesson in efficiency and the challenge of creating a consistent experience across different operating systems. The promise of “write once, run anywhere” is compelling, and I learned a great deal about managing a single codebase for multiple targets.
This broad perspective is something I wouldn’t trade. It gave me a holistic view of how a product comes to life, from the database all the way to the user’s screen. But it also created a point of comparison that continually highlighted what makes native development, and specifically iOS, so special to me.
The Pull of Native iOS
There’s a certain elegance and satisfaction in iOS development that I kept coming back to. The seamless integration between the Swift language, powerful frameworks like SwiftUI, and the hardware itself allows for a level of polish and performance that is simply a joy to create. The pursuit of the perfect animation, the crispness of a native UI component, and the satisfaction of building something that feels completely at home on the device—that’s what excites me as a developer.
After reflecting on this, I felt a renewed surge of energy and inspiration. To channel it into something tangible, I decided to build a small, focused project that captures the kind of delightful interaction I love: a simple “lights-out” animation.
Project: The “Lights-Out” Animation
I wanted to create more than just a toggle. I wanted to build an experience. The idea was to mimic the satisfying, physical act of pulling a cord to turn a light on and off, complete with animated light beams, a draggable cord, and a crisp sound effect.
This project, while small, was a great way to put SwiftUI’s strengths to the test, focusing on:
Declarative UI: Building complex views that react to state changes.
State Management: Using @State and @Binding to drive the entire UI from a single source of truth (isOn).
Animation: Leveraging withAnimation and animation modifiers to create fluid transitions and spring physics for the pull cord.
Gestures: Implementing a DragGesture to create an interactive and intuitive pull-cord mechanism.
A Look at the Code
For those interested in how it works, the full source code is available on my GitHub. But here are a few key pieces that bring the experience to life.
The core of the app is the ContentView, which manages the isOn state. This single boolean drives everything from the background color to the sound playback.
structContentView: View {@Statevar isOn = truevar body: some View {ZStack {// The background's gradient changes based on the 'isOn' stateLightBeamBackground(isOn: self.$isOn) .ignoresSafeArea()// The main lightbulb button that can also toggle the stateButton(action: {withAnimation {self.isOn.toggle() } }) {// ... button content } } .overlay(alignment: .topTrailing) {// The interactive pull cord is an overlayPullCord(isOn: self.$isOn) .padding(.top, -40) } .onChange(of: self.isOn) { _, newValue in// Play a sound effect whenever the state changes SoundPlayer.shared.play(newValue ? .lightOn: .lightOff) } }}
The most interactive piece is the PullCord view. It uses a DragGesture to track the user’s finger and provides physical feedback by stretching. When the drag is released, it decides whether to toggle the light based on how far it was pulled.
structPullCord: View {@Bindingvar isOn: Bool@Stateprivatevar dragOffsetY: CGFloat = 0privatelet maxDragDistance: CGFloat = 140var body: some View {ZStack(alignment: .top) {// ... visual components for the cord and handle } .gesture(DragGesture(minimumDistance: 0) .onChanged { value inlet dy = max(0, value.translation.height) dragOffsetY = min(dy, maxDragDistance) } .onEnded { value inlet shouldToggle = value.translation.height > maxDragDistance * 0.6withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { dragOffsetY = 0if shouldToggle { isOn.toggle() } } } ) }}
This project reaffirmed my belief in the power and elegance of SwiftUI for creating these kinds of delightful, polished user experiences.
What’s Next
This journey of exploration across the tech stack has been invaluable, but now I know where I want to build my future. I’m currently based in Calgary and am actively seeking my next role as an iOS Developer. I’m looking for a team where I can contribute my diverse experience, my passion for Apple’s ecosystem, and my drive to build beautiful, high-performing applications.
If you’re looking for a passionate iOS developer with a broad technical perspective, I would love to connect. You can find my LinkedIn profile here.
Good night, Calgary. Hopeful for new opportunities and interviews tomorrow!
Advanced Techniques with Binding: Transforming and Adapting State
While the standard property wrappers handle most state management needs, you will often encounter situations where the shape of your state doesn’t perfectly match the requirements of a SwiftUI view. For example, a view might need a Binding<String>, but your model provides a Binding<String?>. Or a view needs to bind to the associated value of an enum case.
SwiftUI’s Binding type is incredibly powerful, and with a few extension methods, we can transform bindings to fit our exact needs. Let’s use the following data model for our examples:
Given a state variable like @State var item: Item, a binding to its status, $item.status, would have the type Binding<Item.Status>. Let’s see how we can manipulate this and other bindings.
Drilling Down into Bindings with Key Paths
SwiftUI has built-in support for creating bindings to the properties of a bound value. When you write $item.status, Swift is transparently applying a transform to map the Binding<Item> to a Binding<Item.Status>. This is conceptually achieved through a map function that uses a WritableKeyPath:
You rarely need to call this map function directly, as the . syntax ($item.status) is convenient shorthand for the same operation. It’s the simplest way to transform a Binding<A> into a Binding<B>.
Handling Optionals: The unwrap Extension
A very common scenario is dealing with optional state. For instance, our item.color property is a Color?, making $item.color a Binding<Color?>. However, SwiftUI’s ColorPicker view requires a Binding<Color>. It cannot work with optionals.
To bridge this gap, we can create a handy unwrap extension that transforms an optional binding Binding<A?> into a non-optional binding Binding<A>?. The result is itself an optional: it will be nil if the original state is nil, or a valid, non-optional Binding if the state has a value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
extensionBinding {/// Transforms a `Binding<Wrapped?>` into a `Binding<Wrapped>?`./// If the original binding's value is `nil`, this returns `nil`.funcunwrap<Wrapped>() -> Binding<Wrapped>? whereValue == Wrapped? {// If the wrapped value is nil, we can't create a binding.guardlet value = self.wrappedValue else { returnnil }// Create a new binding that gets the non-optional value// and sets the optional value on the original source.return Binding<Wrapped>(get: { value },set: { self.wrappedValue = $0 } ) }}
Usage:
You can use this with an if let statement to conditionally show a view that requires a non-optional binding.
1 2 3 4 5 6 7 8 9 10 11 12
structItemEditorView: View {@Bindingvar item: Itemvar body: some View {// Only show the ColorPicker if a binding to a non-optional Color can be created.iflet colorBinding = $item.color.unwrap() {ColorPicker("Item Color", selection: colorBinding) } else {Text("This item has no color set.") } }}
A More General Solution for Enums: matching with CasePaths
The unwrap function is actually a specific version of a broader problem: how do we bind to the associated value of an enum case? An Optional is just an enum with two cases: .none and .some(Wrapped). unwrap effectively extracts the associated value from the .some case.
To create a more generic solution for any enum, we can leverage the excellent CasePaths library. A CasePath is like a “key path for an enum case,” allowing you to reliably extract associated values from and embed them back into an enum.
Building on this, we can create a matching function that returns a binding to an associated value if and only if the binding’s value is currently in that specific case.
// Requires the 'CasePaths' library: https://github.com/pointfreeco/swift-case-pathsimportCasePathsextensionBinding {/// Returns a new binding focused on a specific case of an enum./// If the original binding's value does not match the case, this returns `nil`.funcmatching<Case>(_casePath: CasePath<Value, Case> ) -> Binding<Case>? {// Attempt to extract the associated value from the current state.guardlet `case` = casePath.extract(from: self.wrappedValue) else { returnnil }// Create a new binding.return Binding<Case>(// The getter returns the extracted associated value.get: { `case` },// The setter embeds the new value back into the case and updates the original source.set: { `case` inself.wrappedValue = casePath.embed(`case`) } ) }}
Usage:
This powerful extension allows you to build UI that adapts to the state of an enum. For our Item.Status, we can show completely different controls for each case.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
structItemStatusView: View {@Bindingvar status: Item.Statusvar body: some View {VStack {// This UI will only appear and work if status is .inStockiflet quantityBinding = $status.matching(/Item.Status.inStock) {Stepper("Quantity: \(quantityBinding.wrappedValue)", value: quantityBinding) }// This UI will only appear and work if status is .outOfStockiflet onBackOrderBinding = $status.matching(/Item.Status.outOfStock) {Toggle("Is on backorder", isOn: onBackOrderBinding) } } }}
These techniques for transforming Binding are essential for writing clean, decoupled SwiftUI code. They allow your views to remain simple and focused on their specific data requirements, while your data models can remain complex and robust.
A Comprehensive Guide to State Management in SwiftUI
In SwiftUI, managing the state of your application—the data that drives your UI—is a fundamental concept. SwiftUI provides a set of powerful property wrappers that handle view updates automatically when your data changes. This guide explores the core tools: @State, @StateObject, @ObservedObject, and @Published.
Core Property Wrappers at a Glance
The following table provides a quick reference for the most common state management property wrappers in SwiftUI. Note that @StateObject, @ObservedObject, and @Published are all integral parts of the Combine framework, which SwiftUI uses for reactive programming.
Property Wrapper
Purpose
Data Type
Ownership & Lifecycle
Primary Use Case
@State
Manages simple, private state within a single view.
Value types (e.g., Struct, Enum, Int, String, Bool).
Owned and managed by the view. SwiftUI manages its storage. It may be re-initialized if the view’s identity changes in the view hierarchy. It is view-local.
Controlling the local state of UI components, such as a Toggle‘s on/off state, a TextField‘s input, or whether an alert is shown.
@StateObject
Manages a complex, private state object within a single view.
Reference types (Class) that must conform to ObservableObject.
Owned & Persisted by the View. SwiftUI ensures the object’s instance persists for the lifetime of the view’s identity, even across redraws. It can be passed to other views.
Creating and managing an instance of a complex data model (like a ViewModel) within the view that owns it.
@ObservedObject
Subscribes to an existing observable object from an external source.
Reference types (Class) that must conform to ObservableObject.
The view does not own the object; it merely “borrows” or “observes” it. Its lifecycle is managed externally.
Receiving and responding to a shared data model in a subview, where the model is managed by a parent view or another part of the app.
@Published
Automatically publishes notifications when a property’s value changes.
Any type.
Its lifecycle is tied to the ObservableObject instance it belongs to.
Marking properties within a ViewModel or shared data model that should trigger UI updates whenever they are modified.
A key distinction to remember is that a view’s @State can be destroyed and recreated if the view is removed and re-added to the view hierarchy. In contrast, @StateObject is designed to survive view redraws as long as the view maintains its identity.
Understanding the Nature of SwiftUI Views
Before diving deeper, it’s crucial to understand what a SwiftUI View is. The struct you define (e.g., struct MyView: View) is not the persistent object you see on screen. Instead, it’s a lightweight “blueprint” or “description” of your UI.
View Structs are Ephemeral: Every time SwiftUI needs to update the UI (perhaps because a @State variable changed), it re-creates your view struct and calls its body property to get a new blueprint. Creating and destroying these structs is extremely fast and low-cost.
“Redraw” = “Re-evaluating body“: When we say a “view redraws,” it’s more accurate to say that “the view’s body property is re-evaluated,” which often results in new view structs being created.
The Magic of @StateObject: Separating State from the View Struct
@StateObject was introduced to solve the problem of state being reset during view redraws. Its mechanism works as follows:
View Identity: SwiftUI uniquely identifies a view by its position and type within the View Tree. For example, “the first UserProfileView inside the VStack in ContentView.”
First-Time Creation & Storage: The very first time a view with this specific identity appears, SwiftUI sees the @StateObject property wrapper. It then:
Executes your initialization code (e.g., _viewModel = StateObject(wrappedValue: UserViewModel())) to create an instance of your ObservableObject.
SwiftUI then takes this newly created instance and stores it in a special, managed memory area associated with that specific view identity.
Subsequent Redraws: Later, when a parent view’s state changes and your view’s body is re-evaluated, a new view struct is created. However:
SwiftUI again sees the @StateObject property wrapper.
This time, it checks its internal storage and finds that an object is already associated with this view’s identity.
It skips your initialization code and simply connects the property to the pre-existing instance from its managed memory.
Practical Example: The Lifecycle of @State
The following code demonstrates how @State is tied to the view instance’s lifetime within the view hierarchy.
importSwiftUIstructCounterView: View {// A @State variable, initialized to 0@Stateprivatevar count = 0init() {// Log when this view is createdprint("✅ CounterView has been initialized.") }var body: some View {VStack {Text("Counter Value: \(count)") .font(.title) .padding()Button("Increment Count") { count += 1 } } .padding() .border(Color.blue, width: 2)// This is called when the view is removed from the view tree .onDisappear {print("❌ CounterView has disappeared.") } }}structContainerView: View {@Stateprivatevar showCounter = truevar body: some View {VStack(spacing: 30) {Toggle("Show/Hide Counter", isOn: $showCounter.animation()) .padding()if showCounter {// When showCounter is true, CounterView exists in the view treeCounterView() } else {// When showCounter is false, CounterView is completely removed// A placeholder Text shows the structural changeText("Counter is hidden") .foregroundColor(.gray) }Spacer() } .navigationTitle("State Lifecycle Demo") }}
How to Run and Observe
Run the ContainerView. You will see the counter, and the console will print: “✅ CounterView has been initialized.”
Click the “Increment Count” button a few times to increase the count (e.g., to 5).
Now, tap the “Show/Hide Counter”Toggle to turn it off.
The CounterView will disappear from the screen.
The console will print: “❌ CounterView has disappeared.” This confirms the view instance was destroyed.
Tap the Toggle again to turn it back on.
CounterView reappears on the screen.
The console will again print: “✅ CounterView has been initialized.” This proves that SwiftUI has created a brand new CounterView instance.
You will notice that the counter’s value has reset to 0, not the 5 you left it at.
This behavior perfectly illustrates that @State‘s storage is tied to the lifecycle of its containing view in the view hierarchy. If the view is removed, its state is lost. This is precisely the scenario where @StateObject should be used if you need the state to persist as long as the view’s identity remains the same.