Nil CoalescingWe are a software development company based in New Zealand passionate about iOS and macOS app development.https://nilcoalescing.comenWed, 4 Mar 2026 21:23:17 +1300Wed, 4 Mar 2026 21:23:17 +1300250https://nilcoalescing.com/blog/AdjustingLineHeightInSwiftUIOniOS26Adjusting line height in SwiftUI on iOS 26iOS 26 introduces the lineHeight(_:) modifier and AttributedString.LineHeight attribute for adjusting vertical spacing between lines of text in SwiftUI.https://nilcoalescing.com/blog/AdjustingLineHeightInSwiftUIOniOS26Wed, 4 Mar 2026 18:00:00 +1300Adjusting line height in SwiftUI on iOS 26

On iOS 26, we have a new SwiftUI modifier, lineHeight(_:), for adjusting the distance between the baselines of two subsequent lines of text. This modifier accepts a new AttributedString attribute, AttributedString.LineHeight, which can also be used separately for styling attributed strings.

There are a few options available in this new API, so I thought I would look through how they behave in practice.

# LineHeight type properties

The most direct way to adjust a line height is by using one of the built-in static properties on the AttributedString.LineHeight, such as loose or tight, for example. These presets allow for quick adjustments without the need for precise manual control.

The loose line height increases the vertical distance between lines, resulting in a more open layout for paragraph text.

Text(loremIpsum)
    .lineHeight(.loose)

Below is a comparison showing the default line height on the left versus the loose configuration on the right.

Comparison showing the default line height on the left versus the loose configuration on the right

Conversely, the tight preset reduces the baseline distance, creating a more compact vertical layout.

Text(loremIpsum)
    .lineHeight(.tight)

The left screen shows the default height, while the right screen demonstrates the increased density of the tight configuration.

Comparison showing the default line height on the left versus the tight configuration on the right

The other two options, normal and variable, don't appear significantly different from the default line height in my experimentation. Documentation defines normal as a constant line height based on a multiple of the point size perceived as normal, whereas variable uses a height based specifically on font metrics.

If no line height is specified, SwiftUI will automatically select the appropriate one based on the context.

# LineHeight type methods

To get more precise control over the text layout, we can use the static methods provided by AttributedString.LineHeight instead of just the static presets. These allow us to define the baseline distance using specific numerical values.

The multiple(factor:) method sets a constant line height based on a multiple of the font's point size. For example, we can set the line height to double the font's point size, to give the paragraph a very spaced-out feel.

Text(loremIpsum)
    .lineHeight(.multiple(factor: 2))

When the font size increases with Dynamic Type, the line height scales along with it, maintaining the same proportional spacing.

Comparison showing the multiple of 2 line height in default font size versus the larger font size

We also have leading(increase:), which keeps the line height relative to the point size and adds a fixed numerical increase.

Text(loremIpsum)
    .lineHeight(.leading(increase: 30))

Since this method is still relative to the point size, the line height will continue to grow as the font size increases with Dynamic Type, but the point increase will remain constant.

Comparison showing the leading with increase of 30 in default font size versus the larger font size

For absolute control, we can use exact(points:). This sets a constant line height based on a fixed total value in points, completely bypassing any relative calculations based on the font size.

Text(loremIpsum)
    .lineHeight(.exact(points: 30))

We should be careful when setting an exact line height value, though, as it won't adjust with Dynamic Type and can result in unreadable text. Since the baseline distance remains fixed, the lines may eventually overlap or even become cut off if a user increases their system font size.

Comparison showing the exact line height in default font size versus the larger font size where text is cut off at the bottom

lineHeight(_:) vs lineSpacing(_:)

While the lineHeight(_:) modifier is new in iOS 26, the lineSpacing(_:) modifier has been available for a while. The difference between the two is that lineHeight(_:) defines the distance between the baselines of two subsequent lines of text, whereas lineSpacing(_:) sets the amount of spacing from the bottom of one line to the top of the next.

Because lineHeight(_:) comes with a variety of configurations and presets, it seems more versatile for modern layouts. I think I'll be using it more often than line spacing in my apps, though it always depends on the specific requirements of the project.

leading(_:) font modifier

SwiftUI also provides the leading(_:) font modifier that can be applied directly to an instance of a Font to adjust its line spacing.

Text(loremIpsum)
    .font(.title.leading(.loose))

The leading(_:) font modifier has been available since iOS 14. It takes Font.Leading parameter which is an enum with only three cases standard, loose, and tight.

Applying line spacing adjustments using this modifier results in a very subtle change, which may be good in some layouts, but is quite limiting in most cases.

I covered SwiftUI font modifiers in more detail in a separate article a while back - Font modifiers in SwiftUI.


It's encouraging to see the continued evolution of text APIs in SwiftUI, and having new options like lineHeight(_:) with its various configurations is a welcome improvement for fine-tuning typography. I think it would also be great to see more detailed documentation on text APIs in general, though, to make it easier for developers to understand how new additions fit into the broader text system on Apple platforms.


If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/IsolateSwiftUIAnimationsToSpecificAttributesIsolate SwiftUI animations to specific attributesPrecisely scope animations to specific animatable attributes by using the animation(_:body:) API introduced in iOS 17.https://nilcoalescing.com/blog/IsolateSwiftUIAnimationsToSpecificAttributesWed, 18 Feb 2026 17:00:00 +1300Isolate SwiftUI animations to specific attributes

To apply animations in SwiftUI, we most commonly use either the withAnimation(_:_:) global function or the animation(value:) view modifier. These approaches work well in many cases. However, there are situations where we need more precise control over which attributes participate in the animation. This becomes especially important in reusable components that accept arbitrary child content, where unintended animations can easily occur.

First, let’s look at an example with an accidental animation.

Imagine, that we have a generic PremiumContentCard component that adjusts the opacity of its content based on availability. When premium content is enabled, the view is fully opaque. Otherwise, it becomes slightly transparent to reduce its visual prominence. To animate this change, we might reach for the familiar animation(_:value:) modifier.

struct PremiumContentCard<Content: View>: View {
    let isEnabled: Bool
    @ViewBuilder var content: Content
    
    var body: some View {
        content
            .padding()
            .background(.indigo.gradient)
            .opacity(isEnabled ? 1 : 0.4)
            .animation(.default, value: isEnabled)
    }
}

However, since our component accepts arbitrary content, we cannot guarantee that only the opacity change will be animated. For example, the content passed into PremiumContentCard may also depend on the same availability state and update its text accordingly.

PremiumContentCard(isEnabled: premiumContentEnabled) {
    VStack(alignment: .leading) {
        Text("Upper Body Workout")
            .font(.headline)
        Text("45 minutes • Intermediate")
            .font(.subheadline)
        
        if premiumContentEnabled {
            Text("Push ups, pull ups, dumbbell press, shoulder raises.")
        } else {
            Text("Upgrade to access the full workout plan.")
        }
    }
}

In that case, the text change will also be animated.

Premium workout card fades while its text content animates when toggling premium content

This may or may not be desirable. However, when designing generic container components, it's often better to be explicit about which attributes participate in the animation to avoid unexpected visual glitches. We can achieve this using the newer animation(_:body:) API introduced in iOS 17.

The animation(_:body:) modifier takes an Animation and a ViewBuilder closure. The closure receives a proxy of the modified view, allowing us to specify attributes that should participate in the animation. Anything outside that closure remains unaffected. So we can rewrite PremiumContentCard and apply the opacity modifier inside an isolated animation context.

struct PremiumContentCard<Content: View>: View {
    let isEnabled: Bool
    @ViewBuilder var content: Content
    
    var body: some View {
        content
            .padding()
            .background(.indigo.gradient)
            .clipShape(RoundedRectangle(cornerRadius: 20))
            .animation(.default) {
                $0.opacity(isEnabled ? 1 : 0.3)
            }
    }
}

Now we can guarantee that only the opacity change is animated, independent of any other changes within the content passed into our component.

Premium workout card fades smoothly while text changes instantly when toggling premium content

By isolating the animation to specific modifiers, we can make the intent of our components explicit and keep generic container views more predictable.


If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals is a great place to start. It takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/DefiningCustomStringInterpolationBehaviorInSwiftDefining custom string interpolation behavior in SwiftExtend Swift’s string interpolation to define custom behavior, such as value formatting, directly inside string literals.https://nilcoalescing.com/blog/DefiningCustomStringInterpolationBehaviorInSwiftTue, 13 Jan 2026 18:00:00 +1300Defining custom string interpolation behavior in Swift

Swift’s string interpolation system is more powerful than it first appears. Beyond simple value substitution, it can be customized to perform additional logic directly inside string literals.

Custom interpolation can be implemented by extending String.StringInterpolation. This allows us to define custom interpolation behavior tailored to specific types or formatting requirements.

The example below adds support for inline formatting using the FormatStyle protocol.

import Foundation

extension String.StringInterpolation {
    mutating func appendInterpolation<F: FormatStyle>(
        _ value: F.FormatInput,
        format: F
    ) where F.FormatInput: Equatable, F.FormatOutput == String {
        appendLiteral(format.format(value))
    }
}

With this extension in place, a Date can be formatted directly within a string literal.

let today = Date()

let formattedString = """
Today's date is \(today, format: .dateTime.year().month().day())
"""

// Today's date is 13 Jan 2026
print(formattedString)

Here, the interpolation extension applies the provided format style and inserts the formatted result into the string. This makes the formatting more expressive, avoids repetition, and keeps the interpolation logic close to where it's used.

Custom string interpolation is not limited to formatting. It can be used to validate values, apply conditional logic, or control how domain specific values are rendered. This lets us encode that behavior once and reuse it across string literals.

]]>
https://nilcoalescing.com/blog/AnimatingSFSymbolsInSwiftUIAnimating SF Symbols in SwiftUIAdd symbol effect animations and transitions to symbol images in SwiftUI to handle icon state changes without custom drawing or animation logic.https://nilcoalescing.com/blog/AnimatingSFSymbolsInSwiftUIThu, 18 Dec 2025 17:00:00 +1300Animating SF Symbols in SwiftUI

SF Symbols are a natural choice for icons in SwiftUI apps. The system provides a very large symbol catalog, and extensive customization options. Size, weight, rendering mode, and color can all be adjusted to match the surrounding UI, making symbols easy to integrate across an app.

Beyond static icons, SF Symbols also support animation. These animations are called symbol effects. They allow icons to respond to state changes and interactions, bringing more life to the interface without custom drawing or complex animation code.

Symbol effects are provided by the Symbols framework, which is implicitly available when working with SwiftUI. There is a built-in collection of effects that can be applied to any symbol, including custom symbols. Effects are configurable, and options can be chained to create very specific behavior. The SF Symbols app documents each effect and its supported options.

In this post, we'll explore the SwiftUI APIs for animating SF Symbols and use the groupings based on protocol conformance in the Symbols framework as a mental model. We'll look at four effect groups: indefinite effects (IndefiniteSymbolEffect), discrete effects (DiscreteSymbolEffect), content transitions (ContentTransitionSymbolEffect), and transitions (TransitionSymbolEffect). Each group represents a distinct animation intent and naturally aligns with the SwiftUI API patterns used to apply these animations.

# Indefinite symbol effects

Indefinite symbol effects apply a modification that remains in place for as long as the effect is active. This category includes the largest set of effects, such as ScaleSymbolEffect, RotateSymbolEffect, BreatheSymbolEffect, and many others. The full list of animations that conform to the IndefiniteSymbolEffect protocol can be found in the Apple documentation.

Indefinite effects can be configured using the symbolEffect(_:options:isActive:) modifier, where the isActive parameter is used to turn the effect on or off.

Such effects can help provide feedback for a continuous user interaction, like an active hover, for example. Here is how we can scale symbols up when the mouse is over them.

struct HoverScalingSymbol: View {
    let systemName: String
    
    @State private var isHovering = false
    
    var body: some View {
        Image(systemName: systemName)
            .symbolEffect(.scale.up, isActive: isHovering)
            .onHover { hovering in
                isHovering = hovering
            }
    }
}
Animated SF Symbols showing document actions scaling up on hover

Some animations in the indefinite effects group, such as breathe, bounce, pulse, and rotate, repeat forever while active. These are great for indicating an ongoing activity or process.

Image(systemName: "record")
    .symbolVariant(.circle)
    .symbolEffect(.breathe, isActive: isRecording)
Record symbol with a continuous breathe animation

If we omit the isActive parameter for such effects, it will default to true and the animation will be active forever.

Indefinite effects really shine with configuration options that control how the animation progresses and repeats over time. By chaining these options, we can arrive at a variant that best suits our use case. Here is an example of how the variable color animation can be customized:

Image(systemName: "waveform")
    .symbolEffect(.variableColor.iterative.reversing)
Animated waveform symbol with a looping variable color effect that progresses and reverses across layers

# Discrete symbol effects

Discrete symbol effects perform a transient, one-off animation that is triggered by a change in a specific value. These effects are great for drawing attention to an element in the UI or indicating that an action has taken place.

A few animation types that support indefinite behavior also support discrete behavior. The full list can be found in the conforming types of the DiscreteSymbolEffect protocol.

To add a discrete animation to a symbol in a SwiftUI app, we can use the symbolEffect(_:options:value:) modifier. Here is how we can trigger a one-off bounce effect every time a value is incremented:

struct BasketView: View {
    @State private var numOfItems = 0
    
    var body: some View {
        VStack(spacing: 30) {
            Image(systemName: "basket")
                .symbolEffect(.bounce, value: numOfItems)
            
            Button("Add to basket") {
                numOfItems += 1
            }
        }
    }
}
Basket SF Symbol performing a brief bounce animation when the Add to basket button is clicked

To customize how the effect is performed, how fast and how many times, we can also provide the options parameter. For example, we might want to bounce the symbol twice very quickly.

Image(systemName: "basket")
    .symbolEffect(
        .bounce,
        options: .repeat(2).speed(2),
        value: numOfItems
    )
Basket SF Symbol with a brief double bounce animation triggered by the Add to basket button

# Symbol effect content transitions

When we want to switch between symbol variants or distinct symbols, we can take advantage of built-in symbol animations with the symbol effect content transitions.

When a symbolEffect content transition is applied without any configuration options, the system will choose the most appropriate transition for the context.

In the example below, the slash will be drawn on and off as we toggle the setting.

struct NotificationButton: View {
    @State private var notificationsEnabled = true
    
    var body: some View {
        Button {
            notificationsEnabled.toggle()
        } label: {
            Image(systemName: "bell")
        }
        .buttonStyle(.borderless)
        .symbolVariant(notificationsEnabled ? .none : .slash)
        .contentTransition(.symbolEffect)
    }
}
Bell SF Symbol transitioning between regular and slashed variants using a symbol effect content transition

When switching between unrelated symbols the system will do its best to interpolate between the two.

struct WeatherSymbol: View {
    @State private var isSunny = true
    
    var body: some View {
        Image(systemName: isSunny ? "sun.max" : "cloud")
            .contentTransition(.symbolEffect)
            .onTapGesture {
                isSunny.toggle()
            }
    }
}
Full sun SF Symbol transitioning to a cloud symbol and back again

We can still customize the transition with parameters passed to the symbolEffect(_:options:) function, for example .contentTransition(.symbolEffect(.replace.upUp)), but I've found that the defaults work best for many cases.

# Symbol effect transitions

When adding a symbol to the hierarchy, or removing it, we can provide a symbol effect transition using the transition(_:) modifier.

if isSnowing {
    Image(systemName: "snowflake")
        .transition(.symbolEffect)
}
Snowflake symbol transitioning in and out of view

Note, that unlike other types of transitions, the symbolEffect transition will be activated even if we don't apply an implicit animation to the view hierarchy with the animation(_:value:) modifier or wrap the state modification into a withAnimation {} function.

Instead of relying on the default behavior of the symbol effect transition, we can provide an explicit effect type to achieve a more interesting animation. For example, on iOS and macOS 26, we can set the drawOn as a preference and it will be activated for symbols that support this new animation type.

if isWindy {
    Image(systemName: "wind")
        .transition(.symbolEffect(.drawOn))
}
Wind symbol transitioning in and out with the draw on and draw off effects

SF Symbols are very flexible, and they continue to be updated every year with new behaviors and customization options. Symbol effect animations were introduced in iOS 17, and have significantly evolved since then. It's great to see how easy it is to use these animations in our SwiftUI apps and adapt them to different interaction patterns and use cases.


If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals is a great place to start. It takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/AddAnInnerShadowToASymbolImageInSwiftUIAdd an inner shadow to a symbol image in SwiftUICreate a cut out icon effect in SwiftUI by applying a foreground style with an inner shadow to an SF Symbol image.https://nilcoalescing.com/blog/AddAnInnerShadowToASymbolImageInSwiftUITue, 9 Dec 2025 17:00:00 +1300Add an inner shadow to a symbol image in SwiftUI

SwiftUI offers a great variety of APIs to define and customize image views with SF Symbols to create beautiful icons with minimal effort. One of the lesser known techniques is the ability to apply a foreground style with an inner shadow to create an icon that looks like it's cut out of its background.

Image(systemName: "cloud.rain")
    .symbolVariant(.circle)
    .foregroundStyle(
        .indigo
            .shadow(.inner(radius: 2))
    )
Indigo rain cloud icon with a soft inner shadow

We can set different shadow radii and colors to make the effect stronger or more subtle. This works especially well with circular or rounded symbol variants when we want icons that feel slightly inset into the surface instead of sitting on top of it.

]]>
https://nilcoalescing.com/blog/InitializingObservableClassesWithinTheSwiftUIHierarchyInitializing @Observable classes within the SwiftUI hierarchyLearn the recommended ways to initialize and store @Observable classes in SwiftUI views, and see what can go wrong when observable state is managed incorrectly.https://nilcoalescing.com/blog/InitializingObservableClassesWithinTheSwiftUIHierarchyTue, 2 Dec 2025 17:00:00 +1300Initializing @Observable classes within the SwiftUI hierarchy

I've noticed that many iOS developers, even those who have been working with SwiftUI for a while, still have questions about how @Observable classes work with SwiftUI views, how they should be initialized, and whether we need to store our observable models in @State. In this post, we'll look at some examples of managing observable state in SwiftUI, explore the recommended approaches, and see what can go wrong if our observable classes are not stored correctly.

# Storing @Observable models in @State

When we initialize an @Observable class in a SwiftUI view and read its properties, SwiftUI will automatically refresh any views that depend on those properties when they change. This happens even if we simply assign the observable class to a stored property in the view struct. Because of this, it may not be immediately obvious why we need to bother with storing the model in @State.

In the following example, the text view will update as we increment the count property of DataModel.

@Observable
class DataModel {
    var count = 0
}

struct ContentView: View {
    private let dataModel: DataModel = DataModel()
    
    var body: some View {
        VStack(spacing: 30) {
            Text("Count: \(dataModel.count)")
                .font(.title)
            
            Button("Increment") {
                dataModel.count += 1
            }
            .font(.title2)
            .buttonStyle(.borderedProminent)
        }
    }
}
Side by side iPhone screens showing the same view before and after tapping Increment, with the count changing from 0 to 5

However, when an @Observable model is not stored in @State, it's not tied to the SwiftUI view lifecycle. A new instance will be created every time the view struct is initialized. In SwiftUI, view structs are ephemeral and only exist long enough to describe the view hierarchy. The actual views and their state are managed by SwiftUI in an internal data structure, so the lifetime of a view struct is not the same as the lifetime of the on screen view.

We can see the issue with not assigning the observable model to @State in the next example. Here we refactor the counter into a separate CountView and add a ColorPicker to ContentView, which controls the tint color of the app.

struct ContentView: View {
    @State private var tint: Color = Color.accentColor
    
    var body: some View {
        NavigationStack {
            CountView()
                .tint(tint)
                .toolbar {
                    ColorPicker("Tint Color", selection: $tint)
                        .labelsHidden()
                }
        }
    }
}

struct CountView: View {
    private let dataModel = DataModel()
    
    var body: some View {
        VStack(spacing: 30) {
            Text("Count: \(dataModel.count)")
                .font(.title)
            
            Button("Increment") {
                dataModel.count += 1
            }
            .font(.title2)
            .buttonStyle(.borderedProminent)
        }
    }
}

Every time the tint color changes, ContentView's body is rebuilt, the CountView initializer runs again, and DataModel is recreated, losing its previous state.

Side by side iPhone screens showing the counter before and after changing the tint color, with the count resetting from 5 to 0

Simply changing the dataModel property in CountView from a stored let on the view struct to a variable annotated with @State fixes the issue. The @State annotation tells SwiftUI that this property is tied to the view lifecycle, so SwiftUI manages its storage on our behalf and keeps it alive while the view is present in the hierarchy.

struct CountView: View {
    @State private var dataModel = DataModel()
    
    var body: some View {
        VStack(spacing: 30) {
            Text("Count: \(dataModel.count)")
                .font(.title)
            
            Button("Increment") {
                dataModel.count += 1
            }
            .font(.title2)
            .buttonStyle(.borderedProminent)
        }
    }
}

The state persists even though the view struct itself may be created and discarded many times during that period.

Side by side iPhone screens showing the counter before and after changing the tint color, with the count remaining at 5

# Deferring Observable model initialization

One caveat to remember when deciding when and where to perform the observable class initialization is that, even though the state will persist across recreations of the view struct, if the model is initialized as the default value of a state property, its initializer will still be called every time the view initializer runs, creating a new temporary instance that SwiftUI discards.

Let's add a print statement inside the DataModel initializer to observe what happens.

@Observable
class DataModel {
    var count = 0
    
    init() {
        print("DataModel initialized")
    }
}

struct ContentView: View {
    @State private var tint: Color = Color.accentColor
    
    var body: some View {
        NavigationStack {
            CountView()
                .tint(tint)
            
            ...
        }
    }
}

struct CountView: View {
    @State private var dataModel = DataModel()
    
    var body: some View {
        VStack(spacing: 30) {
            Text("Count: \(dataModel.count)")
                .font(.title)
            
            ...
        }
    }
}

When the tint color changes, ContentView’s body runs again, the CountView initializer is called, and "DataModel initialized" is printed to the console, even though we can see in the UI that the state of dataModel is preserved. SwiftUI does not overwrite the existing state with a new DataModel instance on each rebuild, but it still evaluates the initializer expression, so the print runs every time.

This aspect is important to keep in mind, because any heavy logic inside the @Observable class initializer may run multiple times and potentially degrade app performance, since SwiftUI relies on view initializers being very cheap.

In cases where our observable class initializer logic is not quick and simple, we can defer creating the model by moving that work into a task modifier.

struct CountView: View {
    @State private var dataModel: DataModel?
    
    var body: some View {
        VStack(spacing: 30) {
            Text("Count: \(dataModel?.count, default: "0")")
                .font(.title)
            
            Button("Increment") {
                dataModel?.count += 1
            }
            .font(.title2)
            .buttonStyle(.borderedProminent)
        }
        .task {
            dataModel = DataModel()
        }
    }
}

In this setup, the DataModel initializer will run only once, when CountView is added to the hierarchy.

Deferring observable model creation would also let us configure it based on a value passed into the view. But it's important to use the task() modifier with the id parameter, so that the task runs again when that value changes.

@Observable
class DataModel {
    var text: String
    
    init(id: UUID) {
        text = "ID: \(id)"
        print("DataModel initialized for id: \(id)")
    }
}

struct ContentView: View {
    @State private var id = UUID()
    
    var body: some View {
        NavigationStack {
            IDView(id: id)
                .toolbar {
                    Button(
                        "Reset ID",
                        systemImage: "arrow.trianglehead.2.clockwise"
                    ) {
                        id = UUID()
                    }
                }
        }
    }
}

struct IDView: View {
    let id: UUID
    @State private var dataModel: DataModel?
    
    var body: some View {
        Text(dataModel?.text ?? "")
            .task(id: id) {
                dataModel = DataModel(id: id)
            }
    }
}

In a real-world app, this pattern can be useful when we need to load different data based on the current selection or other changing state.

# Initializing observable state in the App struct

It's important to remember that state initialized in views inside a WindowGroup scene is scoped to that scene. On devices that support multiple scenes, such as iPad or Mac, each window gets its own fresh copy of that state. If we want shared state across all scenes, we can initialize our observable model in the App struct and then pass it down through the environment.

@Observable
class DataModel {
    var count = 0
}

@main
struct ObservableExampleApp: App {
    @State private var dataModel = DataModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(dataModel)
        }
    }
}

struct ContentView: View {
    var body: some View {
        CountView()
    }
}

struct CountView: View {
    @Environment(DataModel.self)
    private var dataModel
    
    var body: some View {
        VStack(spacing: 30) {
            Text("Count: \(dataModel.count)")
                .font(.title)
            
            Button("Increment") {
                dataModel.count += 1
            }
            .font(.title2)
            .buttonStyle(.borderedProminent)
        }
    }
}

This way, each scene will read and write to the same shared state. This is a good choice for application wide data, but not for state that should be scene specific, such as selection or navigation.

Side by side app windows on an iPad, each showing Count 5, demonstrating shared state across scenes

Even though the App struct initializer should only run once per application lifecycle, it's best to still annotate the observable model with @State or, at least make it a singleton with a static declaration, to be 100% certain that it will not get reset in any unexpected scenario.

If we want to defer the initialization of the application wide state, we cannot simply assign it in a task() modifier like we did for view state. A task attached to the root view will run once per scene, not once per app launch. To ensure the state is initialized only once, we would need an additional flag or some other guard around the initialization.


To learn more about @Observable in SwiftUI, including when to annotate it with @Bindable and how it differs from ObservableObject, you can read my other post: Using @Observable in SwiftUI views.


If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals is a great place to start. It takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/AutomaticPropertyObservationInUIKitWithObservableAutomatic property observation in UIKit with @ObservableUIKit now has native support for Swift Observation, automatically tracking reads of @Observable properties in update methods, making it easier to share data between UIKit and integrated SwiftUI components.https://nilcoalescing.com/blog/AutomaticPropertyObservationInUIKitWithObservableFri, 21 Nov 2025 17:00:00 +1300Automatic property observation in UIKit with @Observable

UIKit continues to evolve with modern patterns and better SwiftUI interoperability. This year it adds native support for Swift Observation. When we read properties of an @Observable class in update methods, UIKit records those reads and wires up the dependencies so it can invalidate and update the right views without extra manual logic on our behalf.

This automatic observation tracking is enabled by default on iOS 26, but it can also be backported to iOS 18. In this post we'll look at how automatic observation tracking works in practice on iOS 26 and iOS 18, and how it makes it easy to share data between UIKit and integrated SwiftUI components.

The example we'll use is a very basic UIKit app that shows an image and a title, and includes a settings view, the contents of which are built with SwiftUI. The SwiftUI settings view is presented in a half-sheet, and we need to make sure that as the user switches the selection in the SwiftUI view, the UIKit view below refreshes to reflect the new state.

iPhone showing a UIKit app with an image of a lake and a SwiftUI settings view in a half-sheet indicating lake selection

Here are the relevant code parts, with the UIKit update logic omitted for now:

@Observable
class SelectionState {
    var image: ImageName = .lake
    
    enum ImageName: String, CaseIterable {
        ...
    }
}

class ViewController: UIViewController {
    private let selectionState = SelectionState()
    
    private var imageView: UIImageView!
    private var imageLabel: UILabel!

    ...
    
    @objc private func showHalfSheet() {
        let settingsView = SettingsView(selectionState: selectionState)
        let settingsController = UIHostingController(rootView: settingsView)
        
        ...
        
        present(settingsController, animated: true, completion: nil)
    }
}

struct SettingsView: View {
    let selectionState: SelectionState
    
    var body: some View {
        NavigationStack {
            List(
                SelectionState.ImageName.allCases, id: \.self
            ) { image in
                Button {
                    selectionState.image = image
                } label: {
                    ...
                }
            }
        }
    }
}

# Automatic observation tracking on iOS 26

On iOS 26, we can use the new updateProperties() method to write new values to the image and label views. This method runs just before viewWillLayoutSubviews(), but is independent and is now the recommended place for populating content, applying styling, or configuring behaviors.

class ViewController: UIViewController {
    private let selectionState = SelectionState()
    
    private var imageView: UIImageView!
    private var imageLabel: UILabel!
    
    ...
    
    override func updateProperties() {
        super.updateProperties()

        imageView.image = UIImage(named: selectionState.image.rawValue)
        imageLabel.text = selectionState.image.rawValue.capitalized
    }
}

UIKit automatically tracks any @Observable we read inside updateProperties(), so we don't need to add any extra logic, like we had to do previously by calling withObservationTracking() when using the Observation framework outside of SwiftUI. The UIKit view will automatically refresh as soon as SwiftUI sets a new image value on the shared SelectionState.

UIKit view switches from the image of a lake, to a mountain, and then a forest as the selection in the SwiftUI view changes

# Automatic observation tracking on iOS 18

If we want the same automatic update behavior on iOS 18, we first need to add the UIObservationTrackingEnabled key to Info.plist and set its value to YES.

Info.plist file in Xcode that contains the UIObservationTrackingEnabled key set to YES

We also have to move the code that updates the image and label into viewWillLayoutSubviews(), since the new updateProperties() method is only available starting with iOS 26.

class ViewController: UIViewController {
    private let selectionState = SelectionState()
    
    private var imageView: UIImageView!
    private var imageLabel: UILabel!
    
    ...
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        imageView.image = UIImage(named: selectionState.image.rawValue)
        imageLabel.text = selectionState.image.rawValue.capitalized
    }
}

The viewWillLayoutSubviews() method supports observation tracking as well. When the feature is enabled, UIKit automatically establishes dependencies on any @Observable class properties read inside it and triggers the necessary updates, just like with updateProperties() on iOS 26.

Automatic observation tracking is a welcome addition to UIKit and brings a more consistent approach to state driven updates in projects that mix UIKit and SwiftUI.

If you are looking for more detailed guidance on using SwiftUI in existing UIKit projects, take a look at my book Integrating SwiftUI into UIKIt apps, which has been recently updated for iOS 26 and Xcode 26. For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/ScrollViewSnappingInSwiftUIScrollView snapping in SwiftUIExplore SwiftUI APIs for customizing scroll behavior, including paging and view-aligned snapping, and learn what to watch out for to avoid unexpected results.https://nilcoalescing.com/blog/ScrollViewSnappingInSwiftUIWed, 29 Oct 2025 17:00:00 +1300ScrollView snapping in SwiftUI

By default, a ScrollView in SwiftUI uses the system's standard deceleration rate and the scrolling velocity to determine where the scroll motion should stop. While this behavior works well in most situations, it doesn't consider the dimensions of the scroll view or its content.

In some interfaces, it can be useful to customize the scroll behavior and make the scroll view snap precisely to a page or a specific view.

I've been exploring the APIs that SwiftUI provides to customize scroll snapping, and in this post I'll share what I've learned and what to keep in mind to avoid unexpected results.

# Paging scroll behavior

When the content of each scroll view item fills the full height of the screen in a vertical scroll, or the full width of the screen in a horizontal scroll, it often makes sense to enable paging so that scrolling snaps in page-sized increments based on the visible region rather than stopping at arbitrary positions.

Let's look at a simple example with a horizontal gallery of cat images where each image matches the container visible width.

ScrollView(.horizontal) {
    LazyHStack {
        ForEach(catPhotos, id: \.self) { photo in
            Image(photo)
                .resizable()
                .scaledToFit()
                .containerRelativeFrame(.horizontal)
        }
    }
}
.scrollIndicators(.hidden)

As the user scrolls through the images, the final offset is determined by velocity and deceleration, so it can settle between pages and leave an image partially visible.

A horizontally scrolling gallery of cat photos where the scroll view stops at uneven positions, leaving each image partially visible on the screen

To customize where scroll gestures should end, SwiftUI provides the scrollTargetBehavior(_:) modifier. We can apply it to the ScrollView in our code, and pass it the paging value. This value tells SwiftUI to adjust the deceleration and target calculation so the final resting position aligns with the visible container size.

ScrollView(.horizontal) {
    LazyHStack(spacing: 0) {
        ForEach(catPhotos, id: \.self) { photo in
            Image(photo)
                .resizable()
                .scaledToFit()
                .containerRelativeFrame(.horizontal)
        }
    }
}
.scrollIndicators(.hidden)
.scrollTargetBehavior(.paging)

Note that for it to work correctly with our horizontal scroll example, we also need to set the LazyHStack spacing to zero. Without it, the default spacing assigned by SwiftUI will interfere with the scroll offset calculations and the result won't be correct.

With this setup, the scroll gesture will flip through each image, always settling on one image per page that takes up the full width of the screen.

A horizontally scrolling gallery of cat photos where the scroll view stops at one image per page

While this works well in the vertical orientation, in the horizontal one the safe area insets seem to affect the offset preventing correct alignment. If the layout allows it, as in our image gallery example, we can ignore the horizontal safe area insets so that paging works as expected.

ScrollView(.horizontal) {
    ...
}
.scrollIndicators(.hidden)
.scrollTargetBehavior(.paging)
.ignoresSafeArea(.container, edges: .horizontal)
A scrolling gallery of cat photos in horizontal device orientation where the scroll view stops at one image per page

# View aligned scroll behavior

# View-aligned snapping with full-width content

In addition to the paging scroll behavior, where the scroll snaps to the container’s visible region, SwiftUI also provides a viewAligned option that makes the scroll snap to individual views instead. When using this behavior, we also need to mark the scrollable content with the scrollTargetLayout(isEnabled:) modifier so SwiftUI knows which views should act as snap targets.

We can apply the view aligned scroll snapping to our image gallery by using the scrollTargetBehavior() modifier with the viewAligned value and marking the LazyHStack with scrollTargetLayout().

ScrollView(.horizontal) {
    LazyHStack {
        ForEach(catPhotos, id: \.self) { photo in
            Image(photo)
                .resizable()
                .scaledToFit()
                .containerRelativeFrame(.horizontal)
        }
    }
    .scrollTargetLayout()
}
.scrollIndicators(.hidden)
.scrollTargetBehavior(.viewAligned)

Since each image still takes up the full width of the container, this approach produces the same paging-like behavior as the paging option we used earlier, but it works without setting the LazyHStack spacing to zero.

A horizontally scrolling gallery of cat photos where the scroll view stops at one image per page

Another benefit is that it also behaves correctly when the horizontal safe area insets are preserved, allowing it to work as expected in horizontal phone orientation without any additional adjustments.

A scrolling gallery of cat photos in horizontal device orientation where the scroll view stops at one image per page

# View-aligned snapping with multiple items visible

The view aligned scrolling behavior is particularly useful in horizontal galleries that display several items at once, with one partially visible to hint at more content beyond the edge. When view aligned snapping is not enabled, scrolling through such a layout can require precise aiming from the user, while the view aligned behavior makes scrolling feel guided and intentional, snapping cleanly to the nearest item.

ScrollView(.horizontal) {
    LazyHStack {
        ForEach(catPhotos, id: \.self) { photo in
            Image(photo)
                .resizable()
                .scaledToFit()
                .containerRelativeFrame(
                    .horizontal, count: 5,
                    span: 2, spacing: 0
                )
        }
    }
    .scrollTargetLayout()
}
.scrollIndicators(.hidden)
.scrollTargetBehavior(.viewAligned)
A horizontally scrolling gallery of cat photos with multiple images visible at once

# View-aligned snapping and oversized items

One important consideration to keep in mind when applying the view aligned scroll behavior, is that each scroll target should fit within the visible region of the scroll view. If an item's width in a horizontal scroll or height in a vertical one exceeds the container's size, scrolling can feel broken. The oversized item will interfere with the snapping logic and make it difficult to continue scrolling once reached.

Let's say we wanted to modify our horizontal gallery by changing the containerRelativeFrame() axis from horizontal to vertical, so that each photo takes the same height while growing in width as its aspect ratio requires.

ScrollView(.horizontal) {
    LazyHStack {
        ForEach(catPhotos, id: \.self) { photo in
            Image(photo)
                .resizable()
                .scaledToFit()
                .containerRelativeFrame(
                    .vertical, count: 5,
                    span: 2, spacing: 0
                )
        }
    }
    .scrollTargetLayout()
}
.scrollIndicators(.hidden)
.scrollTargetBehavior(.viewAligned)

In this case, some cat photos can become too wide, wider than the phone screen. When such a wide photo is reached, the scrolling will feel stuck.

A horizontally scrolling gallery of cat photos where a wide image break the scrolling experience

In layouts with large items that extend beyond the visible bounds and require custom snapping, it can make sense to define a custom ScrollTargetBehavior, which provides finer control over how the content should scroll, for example by snapping at fixed distances or to specific offsets.


These are just a few ways to customize scrolling in SwiftUI, and there's still plenty more to explore. Since iOS 17, SwiftUI has gained a much richer scrolling system, not just for snapping, but also for defining scroll targets, margins, and positions, giving us finer control over how content moves, aligns, and interacts with its container.


If you are looking to build a strong foundation in SwiftUI, my book SwiftUI Fundamentals is a great place to start. It takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/AddACloseButtonToSwiftUIModalsOnIOS26Add a Close button to SwiftUI modals on iOS 26In iOS 26, SwiftUI introduces a new close button role for dismissing informational views, automatically showing a standard close icon without needing a custom label.https://nilcoalescing.com/blog/AddACloseButtonToSwiftUIModalsOnIOS26Mon, 20 Oct 2025 18:00:00 +1300Add a Close button to SwiftUI modals on iOS 26

In iOS 26, SwiftUI introduces a new close button role designed for buttons that dismiss informational views or modals, for example, sheets showing read-only content or contextual details.

Unlike the existing cancel role, which indicates discarding edits or abandoning progress, the close role doesn't imply data loss. It doesn't perform any automatic dismissal by itself but provides internal semantics that help SwiftUI and the system understand the intent of the button, such as for accessibility and platform-consistent presentation.

Here's how we might use it in a modal sheet:

struct BirdDescriptionSheet: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        NavigationStack {
            BirdDescription()
                .toolbar {
                    Button(role: .close) {
                        dismiss()
                    }
                }
        }
    }
}

This adds a system-standard close button with an xmark symbol without the need to define a custom label or icon.

iPhone showing a SwiftUI sheet with a fantail bird description and a small X close button at the top right corner of the sheet

It's a small but useful addition for aligning SwiftUI modals with platform conventions.

]]>
https://nilcoalescing.com/blog/SwiftUIFundamentalsUpdateOctober2025"SwiftUI Fundamentals" book update: refreshed for iOS 26 and the Liquid Glass design"SwiftUI Fundamentals" by Natalia Panferova has been updated for iOS 26 with refreshed visuals and examples reflecting the new Liquid Glass design.https://nilcoalescing.com/blog/SwiftUIFundamentalsUpdateOctober2025Mon, 20 Oct 2025 18:00:00 +1300"SwiftUI Fundamentals" book update: refreshed for iOS 26 and the Liquid Glass design

I'm happy to share that I've updated SwiftUI Fundamentals for iOS 26, bringing refreshed visuals and refinements throughout the book. This new edition aligns with the Liquid Glass design introduced across Apple platforms, giving every example and screenshot an updated look that matches the latest system appearance.

While the visual style has evolved, the fundamental workings of the framework remain the same. The core principles of view composition, state flow, and layout behavior continue to define how SwiftUI operates. This update focuses on clarity and accuracy rather than new concepts, keeping the material fully relevant for developers building with SwiftUI in 2025.

I've verified all examples against the current SwiftUI APIs and refined explanations where needed to reflect minor behavioral changes on iOS 26.

If you already own the book, you can download the updated edition here: Download the October 2025 Edition.

If you haven't checked out the book yet and want to deepen your understanding of SwiftUI, this is a great place to start. I wrote "SwiftUI Fundamentals" to help developers build a clear mental model of how SwiftUI works, from layout and state flow to the principles that guide its design, drawing on my experience contributing to the framework at Apple.

You can learn more or get your copy here: SwiftUI Fundamentals.

]]>
https://nilcoalescing.com/blog/ShowIconsOnlyInSwiftUISwipeActionsOnIOS26Show icons only in SwiftUI swipe actions on iOS 26Starting with iOS 26, SwiftUI shows both title and icon in swipe action buttons by default, but the previous icon-only appearance can be restored using the labelStyle() modifier.https://nilcoalescing.com/blog/ShowIconsOnlyInSwiftUISwipeActionsOnIOS26Wed, 15 Oct 2025 18:00:00 +1300Show icons only in SwiftUI swipe actions on iOS 26

When declaring a simple button in SwiftUI, we usually provide a title and an image name, and rely on SwiftUI to adapt its display based on the context the button is used in. This adaptable behavior has changed for buttons in swipe actions on iOS 26.

Previously, a button with a title and a symbol would only display the symbol when placed inside a swipe action.

RecipeRow(recipe: recipe)
    .swipeActions {
        Button(
            "Delete", systemImage: "trash",
            role: .destructive
        ) {
            delete(recipe: recipe)
        }
    }
iPhone screen showing a list with a row swiped to the left to reveal the delete action displaying a trash symbol


On iOS 26, however, the same button includes both the title and the symbol either stacked vertically, if the row height allows it, or placed side by side.

Two iPhone screens showing a delete swipe action: one with icon and title stacked vertically and one horizontally


This new behavior can be useful for actions that don't have obvious icons to depict them, but it may be overkill for something simple like "Delete", where the trash icon by itself is clear enough.

To return to the previous button look in swipe actions and show only the icons, we can apply the labelStyle() modifier to the button and pass in the iconOnly option.

RecipeRow(recipe: recipe)
    .swipeActions {
        Button(
            "Delete", systemImage: "trash",
            role: .destructive
        ) {
            delete(recipe: recipe)
        }
        .labelStyle(.iconOnly)
    }
iPhone screen showing a list with a row swiped to the left to reveal the delete action displaying a trash symbol


This works because a SwiftUI button constructed with a title and an image uses a label under the hood, so setting an explicit label style in the environment makes it adjust its appearance based on the provided style, rather than follow an automatic behavior that comes from SwiftUI's internal logic.


If you are looking to build a strong foundation in SwiftUI, make sure to check out my book: SwiftUI Fundamentals. It takes a deep dive into the framework's core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, take a look at my other books and book bundles.

]]>
https://nilcoalescing.com/blog/AvoidingTextTruncationInSwiftUIAvoiding text truncation in SwiftUI with Dynamic TypePrevent unnecessary text truncation at larger text sizes with the fixedSize(horizontal:vertical:) modifier, forcing the text to expand vertically as needed.https://nilcoalescing.com/blog/AvoidingTextTruncationInSwiftUIMon, 6 Oct 2025 18:00:00 +1300Avoiding text truncation in SwiftUI with Dynamic Type

When testing Breve with Dynamic Type, I noticed that in some layouts, text would get truncated at larger text sizes instead of wrapping onto the next line, even when there is no line limit and no vertical size constraint on the element. Text truncation can still occur even when the UI element containing the text is inside a scroll view and can grow infinitely.

iPhone screen showing a scroll view with coffee drink cards where the drink description is truncated

I had run into similar truncation issues before, but they seem to have become more frequent in iOS 26.

To ensure that text doesn't truncate, I had to apply the fixedSize(horizontal:vertical:) modifier to the text view, passing false for the horizontal parameter and true for the vertical one. This tells the text to respect horizontal size constraints so it wraps normally, while at the same time forcing it to ignore vertical constraints and expand as needed.

Text(drink.description)
    .fixedSize(
        horizontal: false,
        vertical: true
    )
iPhone screen showing a scroll view with coffee drink cards where the drink description is fully visible

When using this workaround, make sure the layout can grow vertically as much as required, even at the largest accessibility sizes, so the text doesn't get cut off.

]]>
https://nilcoalescing.com/blog/IntroducingBreveIntroducing Breve: an arty coffee app built for iOS 26Take a closer look at my new app Breve, exploring the technical insights around Liquid Glass design, iOS 26 SwiftUI APIs, and system integrations.https://nilcoalescing.com/blog/IntroducingBreveSat, 4 Oct 2025 15:00:00 +1300Introducing Breve: an arty coffee app built for iOS 26

I'm really excited to finally introduce my new app, Breve: Barista Recipes, released just a few days ago. Breve is available on iPhone and iPad and comes with a large collections of coffee recipes and guides that adapt to whatever equipment users have on hand, from espresso machines, to simple hands-on tools.

Coffee drink collections in Breve on iPad and a Cortado recipe on iPhone

Breve is built for iOS 26 and combines the new Liquid Glass design with soft colorful background gradients and watercolor-style artworks. If you want to check it out, you can find it on the App Store. If you feel like leaving a rating or review, it would mean a lot and help more people come across it.

I've been working on Breve for the last few months, and it gave me a great platform to experiment with the new iOS 26 APIs, and system integrations. While I kept the app itself secret until the launch date, I've been documenting many of my technical learnings on the blog. Now that Breve is out, I'd like to revisit those key learnings and share insights that may be useful for others working on their iOS 26 projects too.

# Background extension effect

One of the interesting design solutions in Breve is the recipe detail header with the watercolor illustration blending into the background. I achieved this look using a combination of background extension effects, blurs and gradient overlays.

Espresso recipe in light mode and Bicerin recipe in dark mode on iPhone

The new backgroundExtensionEffect() modifier in iOS 26 lets us extend and blur visual content beyond a view's bounds, creating continuous backgrounds behind elements like sidebars, inspectors, and overlay controls. But we can also use it more creatively, for example, by extending and blurring an image into the surrounding safe area.

If you want to learn more about the new backgroundExtensionEffect() modifier in SwiftUI and see some examples of how it can be used, take a look at my post: Create immersive backgrounds in SwiftUI with backgroundExtensionEffect().

# Stretchy header

It's a common design pattern in modern iOS apps to have a large image at the top of a scroll view, extending all the way into the top safe area, like I have in the recipe view in Breve. To avoid revealing empty space above the image, when the user pulls down to overscroll, I added a stretchy header effect.

Scrollable recipe detail screen where a full-width coffee image at the top stretches smoothly when overscrolled

There are a few ways to achieve this effect in SwiftUI and I most commonly see developers using onScrollGeometryChange() where they get the scroll offset, write it to a shared app state, and then use it to compute the frame of the image. But we can also do it in a slightly simpler way using the visualEffect() API.

If you'd like to add something similar in your apps, feel free to use my strechy() modifier built with the visualEffect(). You can get the code for it from my post: Stretchy header in SwiftUI with visualEffect().

# Search functionality

For the search functionality in Breve I decided to use a separate tab. This pattern appears in many Apple apps such as Health, Music, and Books and works nicely with the Liquid Glass tab view in iOS 26. To adopt this pattern in SwiftUI, we need to create a Tab with the search role and apply the searchable() modifier to the TabView. You can see the example code for it, and other search placements in iOS 26 in my article: SwiftUI Search Enhancements in iOS and iPadOS 26.

On iPhone the search tab appears visually separated from the others and transforms into a glassy search field when selected.

Tab view and search tab in Breve on iPhone

On iPad, the search field is placed in the navigation bar by default, and it works well with a sidebar adaptable tab view.

Sidebar adaptable tab view and search tab in Breve on iPad

Breve is a very content-rich app, so making sure users can quickly find what they are looking for is important. To make the content searchable not only within the app but also through system-wide Spotlight search, I indexed it with Core Spotlight. It's possible to use the same search index for both, and I wrote a detailed post on how this can be done: Core Spotlight integration for Spotlight and internal app search.

# Liquid glass sheets

In addition to the Liquid Glass tab view with search, Breve also makes use of the new Liquid Glass sheet appearance for preferences, paired with a morphing transition from the settings button.

Preferences sheet morphing from the settings toolbar button

To take advantage of the new iOS 26 Liquid Glass sheet design, we need to make sure that our presentation detents include at least one partial-height option. And to implement the morphing transition, we can use the matchedTransitionSource() modifier on the ToolbarItem, paired with navigationTransition() on the view inside the sheet. I walk through all the necessary setup in Presenting Liquid Glass sheets in SwiftUI on iOS 26.

Like in many other apps, the Preferences sheet in Breve contains a form and also pushes views onto a navigation stack within the sheet content. In such cases, the default Liquid Glass background in partial-height gets obscured by the form and the navigation destination background, unless we add a few extra configurations. I described how the glassy sheet appearance can be achieved in more complex sheets in my post on SwiftUI Liquid Glass sheets with NavigationStack and Form. I use the techniques covered in my article, such as reading the current sheet detent and adjusting the scroll content background, to make sure that my Preferences sheet presents the form appropriately in both medium and large detents.

Preferences sheet in Breve in medium and large detents

# AlarmKit integration

In terms of system integration, I thought the most appropriate iOS 26 framework to bring into the first release of Breve would be AlarmKit.

AlarmKit lets us schedule one-time alarms, weekly repeating alarms, and countdown timers that start immediately, and in Breve it's used for brewing timers. The framework provides APIs to request authorization from users, customize alerts that cut through silent mode and Focus, and display a live countdown in a Live Activity. I walk through the setup, which is quite involved, in my blog post: Schedule a countdown timer with AlarmKit.

Unfortunately, there are no APIs for displaying a live countdown inside the app, so I had to write some custom logic for it in Breve, placing the countdown either in a tab view accessory or in the bottom toolbar, depending on where in the app the user is. I'll try to cover that in an upcoming post.

Timer running in a tab view accessory and in a Live Activity


Breve gave me plenty of chances to try out new APIs and design ideas, and I've got a backlog of learnings that I haven't had a chance to turn into blog posts yet. I'm going to do my best to share as much as I can in the near future.

]]>
https://nilcoalescing.com/blog/LiquidGlassSheetsWithNavigationStackAndFormSwiftUI Liquid Glass sheets with NavigationStack and FormConfigure the NavigationStack and Form background in SwiftUI so partial height sheets keep the translucent Liquid Glass appearance on iOS 26.https://nilcoalescing.com/blog/LiquidGlassSheetsWithNavigationStackAndFormTue, 9 Sep 2025 18:00:00 +1200SwiftUI Liquid Glass sheets with NavigationStack and Form

A few weeks ago I wrote a post on Presenting Liquid Glass sheets in SwiftUI on iOS 26, including setting up presentation detents and configuring morphing transitions from toolbar buttons. But if sheets include forms or push views onto a NavigationStack, which is common for preferences sheets in many apps, they won't get the Liquid Glass appearance by default. In this post I'm going to walk through the extra steps required in such cases to keep the Liquid Glass effect visible throughout.

# Liquid Glass sheet with a Form

Form views, like lists, provide their own opaque background that covers the glassy sheet surface. To give a sheet with a form the Liquid Glass appearance, in addition to specifying at least one partial-height detent in its presentation detents, the default form background needs to be hidden. This can be done by applying .scrollContentBackground(.hidden) directly to the Form.

struct SettingsSheet: View {
    var body: some View {
        NavigationStack {
            Form {
                SettingsToggles()
                NotificationsSection()
            }
            .settingsToolbar()
            .scrollContentBackground(.hidden)
        }
        .presentationDetents([.medium, .large])
    }
}

With this configuration the main form background will be removed, leaving only the row backgrounds visible, which results in a glassy sheet appearance in the medium detent.

Two iPhone screens showing partial height settings sheet with Liquid Glass appearance in light and dark mode

As the sheet transitions to the large detent and takes the full height of the screen, the Liquid Glass background will get replaced by a regular opaque one. In dark color scheme the form will still look as expected, but in light the row backgrounds will not be visible anymore. This is slightly inconsistent and might not be what you want in your design. To fix this issue we can dynamically hide and show the default form background based on the current sheet detent. We can keep the glassy appearance in half-sheet by hiding the scroll content background and set it back to default in full screen sheet.

struct SettingsSheet: View {
    @State
    private var currentDetent: PresentationDetent = .medium
    
    var body: some View {
        NavigationStack {
            Form {
                SettingsToggles()
                NotificationsSection(currentDetent: currentDetent)
            }
            .settingsToolbar()
            .scrollContentBackground(
                currentDetent == .medium ? .hidden : .automatic
            )
        }
        .presentationDetents(
            [.medium, .large], selection: $currentDetent
        )
    }
}

This keeps the form appearance consistent in both light and dark color schemes.

Two iPhone screens showing full height settings sheet in light and dark mode

In case we do want to remove the row backgrounds altogether to maximize the glassy look of the sheet, we can apply the listRowBackground() modifier to the form sections or to individual rows and set their backgrounds to Color.clear.

# Liquid Glass sheet with navigation destinations

While the root view of the NavigationStack doesn't set an opaque background for its content by default, navigation destinations pushed onto the stack have an opaque extra layer behind, which obscures the Liquid Glass in the sheet. To maintain the translucency of the sheet as users navigate inside it, we need to remove that default background from navigation destinations.

For example, we might have a screen with multiple notification settings that has its own form and is pushed onto the root view of the settings sheet. To make it translucent, apart from removing the form scroll content background like we saw earlier, we also have to remove its container background by adding .containerBackground(.clear, for: .navigation).

struct NotificationsSection: View {
    let currentDetent: PresentationDetent
    
    var body: some View {
        Section {
            NavigationLink("Notifications") {
                Form {
                    NotificationSettingsForm(currentDetent: currentDetent)
                }
                .scrollContentBackground(
                    currentDetent == .medium ? .hidden : .automatic
                )
                .containerBackground(.clear, for: .navigation)
                .navigationTitle("Notifications")
            }
        }
    }
}
Two iPhone screens showing notifications settings destination in the settings sheet in light and dark mode

It's also common to include a picker with a navigationLink style in the settings, which pushes the list of options onto the navigation stack. The setup to ensure that the sheet maintains its glassy appearance with such pickers is similar to the one with plain navigation destinations. We have to remove the scroll content background first by applying the scrollContentBackground() modifier to the Picker view, and then remove the navigation container background by adding containerBackground(.clear, for: .navigation) to the picker content.

struct WeatherAlertsSections: View {
    let currentDetent: PresentationDetent
    
    @State private var selection = "Off"
    let options = ["Off", "Severe only", "Daily forecast", "All updates"]
    
    var body: some View {
        Section {
            Picker("Weather alerts", selection: $selection) {
                ForEach(options, id: \.self) { option in
                    Text(option)
                }
                .containerBackground(.clear, for: .navigation)
            }
            .pickerStyle(.navigationLink)
            .scrollContentBackground(
                currentDetent == .medium ? .hidden : .automatic
            )
        }
    }
}
Two iPhone screens showing notifications settings and weather alerts options

With these few extra steps, our sheets can take full advantage of the Liquid Glass design and preserve their translucent appearance at partial height in all contexts.


If you're looking to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/ConcentricRectangleInSwiftUICorner concentricity in SwiftUI on iOS 26Make your views and controls fit perfectly within their containers using new SwiftUI APIs in iOS 26 such as the ConcentricRectangle shape and the containerShape() view modifier.https://nilcoalescing.com/blog/ConcentricRectangleInSwiftUIThu, 21 Aug 2025 18:00:00 +1200Corner concentricity in SwiftUI on iOS 26

At WWDC 25, Apple talked a lot about corner concentricity in their new design system. The idea of concentric corners, rounded corners where the curved portions of the inner and outer shapes share the same center and create a visually consistent and nested appearance, was highlighted multiple times. They also showed how to create concentric shapes in SwiftUI in one of the WWDC sessions, but the API was missing from the early betas.

Now that we are approaching the iOS 26 launch and the ConcentricRectangle API is finally available in SwiftUI, I thought it would be a good time to take a closer look at how concentric shapes work in practice, where in our apps they can be applied, and the caveats to be aware of along the way.

# Concentric shape definition

To define a concentric shape in SwiftUI we can either use one of the ConcentricRectangle initializers or call the static rect(corners:) function defined on the Shape protocol passing it a concentric corner style.

ConcentricRectangle()
    .fill(Color.indigo.gradient)
    .padding(8)
    .ignoresSafeArea()
Rectangle()
    .fill(Color.blue.gradient)
    .clipShape(.rect(corners: .concentric))
    .padding(8)
    .ignoresSafeArea()

Both of these versions will produce the same visual result, which is a concentric version of the current container shape. If it's the root view of the app, or a full screen cover, then the container shape comes from the device.

Two iPhone screens showing full-screen rounded shapes: the left filled with indigo, and the right filled with blue, both matching the device’s corner radii

The corner radii for the concentric shape are derived from the container corner radii and the distance between the container and the inner shape corners. If the distance between the inner shape's corners and the container corners is larger than the container’s corner radii, the radii of the inner corners will be set to 0. That is why I applied ignoresSafeArea() in my examples to remove the default distance between my shape and the device's edge for demo purposes.

# Default container shapes

Depending on where in the app hierarchy we need a concentric shape, its container shape will either be provided by the system or defined by us. The default container shape could be the device’s bezel, or the shape of a sheet or a popover, for example.

Two iPhone screens showing a blue concentric rectangle inside a sheet on the left and a green concentric rectangle inside a popover on the right

I noticed that to get a uniform concentric shape inside a sheet, I just needed to ignore the safe area, similar to a shape concentric to the device bezel. But for a shape inside a popover, I had to add the isUniform parameter set to true, otherwise the two corners closer to the popover arrow wouldn’t be rounded enough for visual balance.

.sheet(isPresented: .constant(true)) {
    ConcentricRectangle()
        .fill(.blue.gradient)
        .padding()
        .ignoresSafeArea()
        .presentationDetents([.medium])
}
.popover(isPresented: .constant(true)) {
    ConcentricRectangle(
        corners: .concentric, isUniform: true
    )
    .fill(.mint.gradient)
    .frame(width: 200, height: 200)
    .padding()
    .presentationDetents([.medium])
    .presentationCompactAdaptation(.popover)
}

# Setting a custom container shape

For many custom UI elements, such as cards, tags, custom controls and containers, we'll need to set a custom container shape if we want the corners of nested views to be correctly rounded. We can do it by applying the new containerShape(_:) modifier to the parent container.

struct Card: View {
    let color: Color
    let text: String
    
    var body: some View {
        ZStack {
            ConcentricRectangle()
                .fill(.thickMaterial)
            
            VStack {
                ConcentricRectangle(
                    corners: .concentric,
                    isUniform: true
                )
                .fill(color.gradient)
                
                Text(text)
                    .font(.headline)
            }
            .padding(12)
        }
        .containerShape(
            .rect(cornerRadius: 24)
        )
    }
}

In this example, applying .containerShape(.rect(cornerRadius: 24)) to the ZStack in Card ensures that the background ConcentricRectangle matches the 24‑point corner radius exactly, while the foreground ConcentricRectangle becomes a concentric version of the container shape where the corner radius is computed as 24 minus the padding. The top corners of the foreground ConcentricRectangle will be rounded by default, but the bottom corners would get a radius of 0 because of their greater distance to the parent container corners unless we set isUniform to true.

iPhone screen showing a grid of cards, each consisting of a dark rounded rectangle in the background and a smaller inset colorful rectangle in the foreground with concentric corners

The container shape that we use must conform to the RoundedRectangularShape protocol, otherwise, ConcentricRectangle won't be able to resolve its corner radius based on concentricity and will revert to a ContainerRelativeShape. Standard SwiftUI shapes such as RoundedRectangle, Capsule, and Circle all conform to RoundedRectangularShape, so they can be used as container shapes for concentric rectangles.

# Corner radius resolution

The resolved corner radius for each corner of a concentric shape can vary based on how the shape is positioned within its container. If it’s not centered, the corners closer to the edge will get a larger radius than those farther away. Corners that are too far from the container’s edge will default to a radius of 0 points.

ZStack(alignment: .topTrailing) {
    ConcentricRectangle()
        .fill(.thickMaterial)
    
    ConcentricRectangle()
        .fill(.indigo.gradient)
        .frame(width: 300, height: 300)
        .padding()
}
.containerShape(
    .rect(cornerRadius: 60)
)
.frame(width: 360, height: 500)
iPhone screen showing a dark rounded rectangle with a smaller indigo rectangle inside it, where the top corners are rounded but the bottom corners have a radius of 0

If we want a uniform shape instead, we need to use the init(corners:isUniform:) initializer and pass true to isUniform, which is false by default.

ConcentricRectangle(
    corners: .concentric,
    isUniform: true
)
.fill(.indigo.gradient)
.frame(width: 300, height: 300)
.padding()

Corners of a uniform concentric shape will all have the same corner radius, equal to the largest resolved radius among the corners.

iPhone screen showing a dark rounded rectangle and a smaller indigo rectangle inside it with uniform rounded corners

# Minimum corner radius

To make sure a concentric shape never gets corners that are not rounded, we can provide a minimum corner radius.

Text("Hello, world!")
    .padding()
    .background(
        .teal.gradient,
        in: .rect(
            corners: .concentric(minimum: 24),
            isUniform: true
        )
    )

When the shape is too far from the edge of its container to resolve rounded concentric corners, it will use the minimum radius. If it moves closer to the edge or grows, for example as dynamic text size changes, it will switch to concentric corners that match the container.

Two iPhone screens showing 'Hello, world!' in teal rounded rectangles with different text sizes and corner radii


There are a few more customizations we can do with concentric rectangles, such as setting a corner style for each corner individually, or applying styles to groups of corners like the top or bottom ones, in case we need more control over how our shapes adapt to different layouts and containers.

Overall, I think that concentric rectangles are a welcome addition to SwiftUI that give us new ways to make our interfaces more consistent, adaptive, and polished for iOS 26.


If you're looking to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/CoreSpotlightIntegrationCore Spotlight integration for Spotlight and internal app searchUse a shared Core Spotlight search index to make content discoverable in system Spotlight and support internal search within the app.https://nilcoalescing.com/blog/CoreSpotlightIntegrationMon, 11 Aug 2025 20:00:00 +1200Core Spotlight integration for Spotlight and internal app search

We can increase the visibility of our app’s content by making it available to the system so it appears in Spotlight search results on the device. This can be done by indexing the content with Core Spotlight APIs. The same index can also be used to power search inside the app, allowing us to avoid writing custom search logic.

In this post, I’ll walk through how we can leverage Core Spotlight for both system-wide and in-app search. Core Spotlight indexing APIs have been available since iOS 9, but they’ve evolved over the years. I’ll show the approach I’ve found to work best with the latest updates.

# Adding data to Spotlight index

There are several ways to integrate app data with Spotlight, including CSSearchableItem, NSUserActivity, and IndexedEntity. I’ve been using CSSearchableItem because it’s a simple, lightweight solution that also allows linking data to NSUserActivity via relatedUniqueIdentifier, or to IndexedEntity with associateAppEntity(_:priority:) when we need the additional functionality they provide.

We can start indexing data as soon as it’s available in the app, for example after a network call retrieves it, after the user creates an item, or on app launch if the data is bundled with the app. In our simple example, the data is hardcoded, so we can index it immediately. The recent Core Spotlight APIs provide async versions, which allows us to place the indexing logic inside the SwiftUI task() modifier.

import SwiftUI
import CoreSpotlight

@main
struct CoreSpotlightExampleApp: App {
    var body: some Scene {
        WindowGroup {
            SmoothiesView()
                .task {
                    await indexAppContent()
                }
        }
    }
    
    private func indexAppContent() async {
        // index app content
    }
}

The first step in indexing content is to create CSSearchableItem instances. For each item, we define an attribute set that specifies the properties to index and display in Spotlight search. We then create the searchable item with this attribute set and a unique identifier.

private func indexAppContent() async {
    let items = RecipeProvider.shared.recipes.map { recipe in
        let attributeSet = CSSearchableItemAttributeSet(
            contentType: .content
        )
        attributeSet.title = recipe.title
        attributeSet.thumbnailData = recipe.imageData
        
        return CSSearchableItem(
            uniqueIdentifier: recipe.id,
            domainIdentifier: nil,
            attributeSet: attributeSet
        )
    }
    
    ...
}

Once the searchable items are prepared, the next step is to define a CSSearchableIndex. This index can be created using the shared default instance or by initializing a custom index with a specific name. Using a custom index provides additional control, such as specifying a protection class and performing batch updates, which we will cover later. In this example, we create a named custom index and call indexSearchableItems() to add the items to it.

private func indexAppContent() async {
    ...
    
    let index = CSSearchableIndex(name: "SpotlightSearchExample")
    
    do {
        try await index.indexSearchableItems(items)
    } catch {
        print("Error indexing content: \(error.localizedDescription)")
    }
}

After completing this setup and running the app, we can open Spotlight search and look for a term that matches our content. My example project contains smoothie recipes, so when I search for a term like "berry", I will see matching berry smoothie entries.

iPhone showing Spotlight search for 'berry' with results that include three berry smoothie recipes

# Deep linking from Spotlight results

By default, when the user taps a search result in Spotlight, the system launches the app on its initial page. We can improve this experience by taking them directly to the detail page of the selected item.

When the app is opened from a Spotlight result, we can use the onContinueUserActivity() modifier in SwiftUI to retrieve the tapped item’s identifier and navigate to it. The activity type is CSSearchableItemActionType, and the identifier is available under the CSSearchableItemActivityIdentifier key in the activity’s userInfo dictionary.

NavigationStack(path: $navigationPath) {
    ...
}
.onContinueUserActivity(
    CSSearchableItemActionType
) { activity in
    if let uniqueIdentifier = activity.userInfo?[
        CSSearchableItemActivityIdentifier
    ] as? String {
        navigateToRecipe(withID: uniqueIdentifier)
    }
}

Here’s an example of deep linking from a Spotlight search result directly to an item in the app.

Two iPhones showing a Spotlight search and the smoothie detail view in the app

# Avoiding unnecessary re-indexing

With this navigation in place, we can turn our attention back to indexing. There are a few improvements we can make to the basic setup to make it smarter and more efficient. Firstly, we can utilize the batch update APIs, which also allow us to attach some custom metadata to the updates called client state. By reading and writing the client state, we can ensure that we only index content when necessary, avoiding re-indexing the same items every time the app starts.

The client state type that Core Spotlight accepts is Data, so it can contain anything we need to include. For my example, I defined a simple Codable struct with a date.

struct SearchIndexData: Codable {
    let date: Date
}

We can update our previous indexing logic to check if client data already exists, and depending on that, proceed with indexing or skip it. When indexing is required, we should create a new index data instance, perform the indexing within a batch update, then end the update and attach the new data. This ensures that the next time the app starts, fetching client data returns the value we stored. Since there is no way to add client data without using batch updates, this API is still useful even when the content is small enough not to require batching.

private func indexAppContent() async {
    ...
    
    let index = CSSearchableIndex(name: "SpotlightSearchExample")
    
    if
        let clientState = try? await index.fetchLastClientState(),
        let indexData = try? JSONDecoder().decode(
            SearchIndexData.self, from: clientState
        )
    {
        print("Search index exists, created on: \(indexData.date)")
    } else if
        let newData = try? JSONEncoder().encode(
            SearchIndexData(date: Date())
        )
    {
        do {
            index.beginBatch()
            try await index.indexSearchableItems(items)
            try await index.endBatch(withClientState: newData)
        } catch {
            print("Error indexing content: \(error.localizedDescription)")
        }
    }
}

# Customizing result ranking

Another improvement we can make is to inform Core Spotlight when the user interacts with indexed content in the app. This helps Spotlight prioritize search results, making it more likely that items the user engages with frequently will appear first.

We can achieve this by setting the lastUsedDate in the attribute set for the CSSearchableItem, which requires re-indexing the last used item. Depending on the application flow, we can choose the most suitable place to perform this re-indexing. In my example, I’ll do it in the onAppear() modifier of the recipe detail view.

struct RecipeView: View {
    let recipe: Recipe
    
    var body: some View {
        ScrollView {
            ...
        }
        .onAppear {
            let attributeSet = CSSearchableItemAttributeSet(
                contentType: .content
            )
            attributeSet.title = recipe.title
            attributeSet.thumbnailData = recipe.imageData
            attributeSet.lastUsedDate = Date()
            
            let item = CSSearchableItem(
                uniqueIdentifier: recipe.id,
                domainIdentifier: nil,
                attributeSet: attributeSet
            )
            
            item.isUpdate = true
            let index = CSSearchableIndex(
                name: "SpotlightSearchExample"
            )
            index.indexSearchableItems([item])
        }
    }
}

Note that we should set isUpdate property on the item to true in this case. This allows the system to update an existing item with the same identifier instead of deleting and reinserting it, which is more efficient.

To test whether this prioritization works in practice, we can open the same item several times in the app and then search in Spotlight for a term that matches it along with a few other items. In my case, after interacting with the Summer Berry Blend smoothie a few times, it appears at the top of the Spotlight results for the app.

iPhone showing Spotlight search for 'berry' with Summer Berry Blend smoothie at the top of the results

# Provide a reindexing app extension

Apple recommends providing a reindexing app extension to keep the index up to date even when the app isn’t running. This extension can be used to update the app’s index if it appears to be incomplete, corrupted or lost.

To add the extension, we need to create a new target using the CoreSpotlight Delegate template.

Xcode showing the dialog for creating a new target with the CoreSpotlight Delegate extension selected

This extension requires implementing two methods: one for reindexing all searchable items and another for reindexing specific items by their identifiers. It’s important to call the acknowledgementHandler once indexing is completed.

class SearchReindexing: CSIndexExtensionRequestHandler {
    
    private func searchableItems(for recipes: [Recipe]) -> [CSSearchableItem] {
        recipes.map { recipe in
            let attributeSet = CSSearchableItemAttributeSet(
                contentType: .content
            )
            attributeSet.title = recipe.title
            attributeSet.thumbnailData = recipe.imageData
            
            return CSSearchableItem(
                uniqueIdentifier: recipe.id,
                domainIdentifier: nil,
                attributeSet: attributeSet
            )
        }
    }

    override func searchableIndex(
        _ searchableIndex: CSSearchableIndex,
        reindexAllSearchableItemsWithAcknowledgementHandler acknowledgementHandler: @escaping () -> Void
    ) {
        let items = searchableItems(for: RecipeProvider.shared.recipes)
        
        searchableIndex.indexSearchableItems(items) { error in
            // handle possible errors
            
            acknowledgementHandler()
        }
    }

    override func searchableIndex(
        _ searchableIndex: CSSearchableIndex,
        reindexSearchableItemsWithIdentifiers identifiers: [String],
        acknowledgementHandler: @escaping () -> Void
    ) {
        let items = searchableItems(
            for: RecipeProvider.shared.recipes
                .filter { identifiers.contains($0.id) }
        )
        
        searchableIndex.indexSearchableItems(items) { error in
            // handle possible errors
            
            acknowledgementHandler()
        }
    }
    
    ...
}

To reindex your data inside the extension, make sure the files where it’s stored are included in both the main app target and the extension target.

# Power in-app search with Core Spotlight

Exposing our app data in Spotlight search is an effective way to increase its visibility outside the app, but the Core Spotlight index can also provide value within the app itself. We can leverage this index to power in-app search without building and maintaining separate search logic.

I’m going to show how we can integrate Core Spotlight search capability, continuing from my SwiftUI sample project. To let users perform searches inside the app, I’ll add a searchable() modifier to the SmoothiesView.

import SwiftUI
import CoreSpotlight

struct SmoothiesView: View {
    ...
    
    @State private var searchText = ""
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            ...
        }
        .searchable(text: $searchText)
    }
}

# Querying Core Spotlight index inside the app

To query Core Spotlight for search results, we first need to prepare Spotlight by calling the prepare() class method on CSUserQuery. This only needs to be called once per app lifecycle and should be done only when necessary, for example, when the view with the search interface first appears.

NavigationStack(path: $navigationPath) {
    ...
}
.searchable(text: $searchText)
.onAppear {
    CSUserQuery.prepare()
}

The Core Spotlight APIs for querying search results have async versions, so we can call them within a task() modifier. It’s recommended to wait at least 0.3 seconds after the user’s input to allow them to finish typing and avoid unnecessary search queries, which can be expensive. We can then initialize a Core Spotlight query by passing the search text to the userQueryString parameter, and read the results as they arrive by iterating over the responses async sequence. This sequence can return an enum with two cases: a search result or a search suggestion. However, I’ve never seen it return any suggestions in practice. It’s possible this functionality is incomplete, or that it depends on factors I’m not aware of.

NavigationStack(path: $navigationPath) {
    ...
}
.searchable(text: $searchText)
.onAppear {
    CSUserQuery.prepare()
}
.task(id: searchText) {
    guard !searchText.isEmpty else { return }
    
    do {
        try await Task.sleep(for: .seconds(0.3))
    
        let query = CSUserQuery(
            userQueryString: searchText,
            userQueryContext: nil
        )
        
        for try await element in query.responses {
            switch(element) {
            case .item(let item):
                print("Got a result: \(item.item.uniqueIdentifier)")
            case .suggestion(let suggestion):
                let str = suggestion
                    .suggestion.localizedAttributedSuggestion
                print("Got a suggestion: \(str)")
            @unknown default: break
            }
        }
    } catch {
        // handle possible errors
        return
    }
}

When constructing a search query, it’s important to use the correct initializer: init(userQueryString:userQueryContext:), not init(queryString:queryContext:). Using the latter results in an invalid query error (CSSearchQueryErrorDomain/-2002), but it’s easy to select it by accident when using Xcode autocomplete, so I thought it was worth mentioning.

# Displaying search results

Depending on the desired search experience in our app, we can either display a dedicated search interface while a search is in progress or filter the content in place. I’ll filter the list of smoothies as the user types a search query. To do this, I’ll assemble all the search results into an array and then transform this array of query items into recipe objects. If you are doing the same, don’t forget to reset the results when a new query starts.

struct SmoothiesView: View {
    @State private var navigationPath: [Recipe] = []
    
    private let recipes = RecipeProvider.shared.recipes
    
    @State private var searchText = ""
    @State private var filteredRecipes: [Recipe] = []
    @State private var queryItems: [CSUserQuery.Item] = []
    
    var recipesToDisplay: [Recipe] {
        searchText.isEmpty ? recipes : filteredRecipes
    }
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            List(recipesToDisplay) { recipe in
               ...
            }
        }
        ...
        
        .task(id: searchText) {
            ...
            
            do {
                ...
            
                queryItems = []
            
                for try await element in query.responses {
                    switch(element) {
                    case .item(let item):
                        queryItems.append(item)
                    case .suggestion(let suggestion):
                        ...
                    @unknown default: break
                    }
                }
            } catch {
                // handle possible errors
                return
            }
            
            filteredRecipes = queryItems.compactMap { item in
                recipes.first(
                    where: { $0.id == item.item.uniqueIdentifier }
                )
            }
        }
    }
}

Now, when the search text is not empty, the list of smoothies will consist of the recipes that match the search.

Two iPhones showing a full list of smoothies and a filtered list with a search query

# Personalizing results based on engagement

Similar to how we informed Spotlight about the items the user engages with for improved result ranking in the "Customizing result ranking" section for Spotlight search, we can also personalize the results in internal app search, although the APIs to do this are a bit different. First, we need to detect when the user engages with a search result and call the userEngaged() method on the query instance. This method requires the engaged item as a CSUserQuery.Item and the currently visible items as an array of the same type.

struct SmoothiesView: View {
    @State private var navigationPath: [Recipe] = []
    
    @State private var searchText = ""
    @State private var queryItems: [CSUserQuery.Item] = []
    
    ...
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            ...
        }

        ...
        .onChange(of: navigationPath) { oldValue, newValue in
            guard
                !searchText.isEmpty,
                let selection = newValue.first,
                let item = queryItems.first(
                    where: {
                        $0.item.uniqueIdentifier == selection.id
                    }
                )
            else { return }
            
            let query = CSUserQuery(
                userQueryString: searchText,
                userQueryContext: nil
            )
            
            query.userEngaged(
                item, visibleItems: queryItems,
                interaction: .select
            )
        }
    }
}

We also need to enable ranked results for the Core Spotlight query used to power the search, and sort the results based on rank before preparing them for display. Even though the documentation claims that enableRankedResults is true by default, I found that without defining the query context, the ranking won't work.

struct SmoothiesView: View {
    ...
    
    @State private var queryItems: [CSUserQuery.Item] = []
    
    ...
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            ...
        }
        .task(id: searchText) {
            ...
            
            do {
                ...
                
                let context = CSUserQueryContext()
                context.enableRankedResults = true
                
                let query = CSUserQuery(
                    userQueryString: searchText,
                    userQueryContext: context
                )
                
                ...
                
            } catch {
                // handle possible errors
                return
            }
            
            queryItems.sort { first, second in
                first.item.compare(
                    byRank: second.item
                ) == .orderedAscending
            }
            
            filteredRecipes = queryItems.compactMap { item in
                recipes.first(
                    where: {
                        $0.id == item.item.uniqueIdentifier
                    }
                )
            }
        }
    }
}

Now, if I search for "milk" in my sample smoothie app and interact with the "Chocolate Almond Milk" result, the next time it appears in search it will be moved to the top of the list.

iPhone showing in-app search for 'milk' with the Chocolate Almond Milk smoothie at the top


As we’ve seen, Core Spotlight provides powerful APIs for exposing our app’s data on the user’s device, even when the app isn’t running, and for driving search functionality inside the app. It’s not perfect, and some promised features like search suggestions and semantic search announced by Apple last year don’t appear to be working properly, but it’s still a useful framework to understand and integrate.

If you’d like to try it out yourself, you can download the sample code I used to demo the examples in this post from our GitHub repository. Note that I created the project using Xcode 26 beta 5.


If you’re looking for more Swift and SwiftUI reading, take a look at my books and book bundles. They offer detailed guides and practical tips to deepen your understanding of both the Swift language and the SwiftUI framework.

]]>
https://nilcoalescing.com/blog/SwiftUISearchEnhancementsIniOSAndiPadOS26SwiftUI Search Enhancements in iOS and iPadOS 26Take advantage of the updated search placement and behavior in iOS 26, and implement toolbar and tab bar search patterns that adapt across devices and integrate with the new Liquid Glass design.https://nilcoalescing.com/blog/SwiftUISearchEnhancementsIniOSAndiPadOS26Mon, 28 Jul 2025 18:00:00 +1200SwiftUI Search Enhancements in iOS and iPadOS 26

In iOS and iPadOS 26, the search experience has been updated with new placement behaviors and visual styling, and we also have new SwiftUI APIs to support the changes. In this post, we'll explore how we can take advantage of these enhancements to build search interfaces that feel modern and consistent with the system.

We'll focus on two common search patterns in iOS apps: search in the toolbar and search in the tab bar, and review how each works, what has changed, and how to implement them in SwiftUI.

In apps that use hierarchical navigation with NavigationStack or NavigationSplitView, it's common to enable global search by applying the searchable() modifier to the entire navigation view.

NavigationSplitView {
    List(notes, selection: $selectedNote) { note in
        NavigationLink(note.title, value: note)
    }
    .navigationTitle("Notes")
} detail: {
    NoteDetailView(note: selectedNote)
}
.searchable(text: $searchText)

Applying searchable() to the navigation container instead of an individual column allows the system to determine the appropriate placement for the search field.

In earlier versions of iOS and iPadOS, this setup would place the search field at the top of the sidebar on iPad and at the top of the root view on iPhone. Starting with iOS 26, the same configuration results in a search field presented in a Liquid Glass container, positioned in the top trailing corner of the window on iPad, and at the bottom of the screen on iPhone for easier reach.

iPad and iPhone side-by-side showing a notes app using NavigationSplitView, with the search field appearing in the top trailing corner on iPad and at the bottom of the screen on iPhone

We can still explicitly position the search field in the sidebar on iPad if that better suits our app by specifying the sidebar placement in the searchable() modifier.

NavigationSplitView {
    List(filteredNotes, selection: $selectedNote) { note in
        NavigationLink(note.title, value: note)
    }
    .navigationTitle("Notes")
} detail: {
    NoteDetailView(note: selectedNote)
}
.searchable(text: $searchText, placement: .sidebar)

This restores the previous behavior, placing the search field at the top of the sidebar column instead of using the new floating style.

iPad showing a notes app with the search field expanded in the sidebar and a search query entered

For applications where search is scoped to the detail content rather than applied globally, we should attach the searchable() modifier directly to the detail view.

NavigationSplitView {
    List(notes, selection: $selectedNote) { note in
        NavigationLink(note.title, value: note)
    }
    .navigationTitle("Notes")
} detail: {
    NoteDetailView(note: selectedNote)
        .searchable(text: $searchText)
}

This placement has also been updated with the new design in iOS and iPadOS 26.

iPad and iPhone showing the same selected note with the search field in the top toolbar on iPad and at the bottom of the screen on iPhone

When the detail view includes other bottom toolbar items on iPhone, and we want the search field to still appear at the bottom, we can use two new toolbar content types introduced in iOS 26: DefaultToolbarItem with the search kind, and ToolbarSpacer.

NoteDetailView(note: selectedNote)
    .toolbar {
        if #available(iOS 26.0, *) {
            DefaultToolbarItem(kind: .search, placement: .bottomBar)
            ToolbarSpacer(.flexible, placement: .bottomBar)
        }
        ToolbarItem(placement: .bottomBar) {
            NewNoteButton()
        }
    }
    .searchable(text: $searchText)

This ensures that the search field is allocated space in the bottom toolbar layout and remains visually separated from the other toolbar items.

iPhone displaying a note with the search field in the bottom toolbar and a compose button on the right

If search isn't the primary experience we want to promote in our app, we can use the new SwiftUI modifier searchToolbarBehavior() to minimize the search field into a toolbar button. The system may also apply this behavior automatically based on factors such as device size or the number of toolbar items.

extension View {
    @ViewBuilder func minimizedSearch() -> some View {
        if #available(iOS 26.0, *) {
            self.searchToolbarBehavior(.minimize)
        } else { self }
    }
}

struct NotesView: View {
    ...
    
    var body: some View {
        NavigationSplitView {
            ...
        } detail: {
            NoteDetailView(note: selectedNote)
                .toolbar {
                    if #available(iOS 26.0, *) {
                        DefaultToolbarItem(kind: .search, placement: .bottomBar)
                        ToolbarSpacer(.flexible, placement: .bottomBar)
                    }
                    ToolbarItem(placement: .bottomBar) {
                        NewNoteButton()
                    }
                }
                .searchable(text: $searchText)
                .minimizedSearch()
        }
    }
}
iPhone displaying a note with the search button in the bottom leading corner and the compose button in the bottom trailing corner

In apps that use tab-based navigation, it’s common to place search in a dedicated tab where users can browse suggestions or enter queries directly. This pattern appears in many Apple apps such as Health, Music, and Books. To adopt it, we can create a Tab with the search role and apply the searchable() modifier to the view inside the tab. For the search field to appear and function correctly, the contents of the search tab should be wrapped in a NavigationStack.

TabView {
    Tab("Library", systemImage: "books.vertical") {
        LibraryView()
    }
    Tab("Book Store", systemImage: "bag") {
        StoreView()
    }
    
    Tab(role: .search) {
        NavigationStack {
            SearchView()
                .navigationTitle("Search")
        }
        .searchable(text: $searchText)
    }
}

With the new Liquid Glass tab bar design in iOS 26, the search tab appears visually separated from the others and transforms into a search field when selected.

Two iPhones showing a books app, the Library tab displays a grid of book titles, while the Search tab shows categorized book lists with a bottom-aligned search field

Just like with other standard system components, adopting the new Liquid Glass design for search doesn’t require much effort. By taking advantage of the updates to standard search patterns and adjusting behavior to suit our app’s needs, we can ensure that our search experience feels at home on iOS and iPadOS 26.


If you're looking to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/PresentingLiquidGlassSheetsInSwiftUIPresenting Liquid Glass sheets in SwiftUI on iOS 26Learn how to leverage the new glass appearance for partial sheets in iOS 26, and set up morphing transitions for sheets presented from toolbar buttons using SwiftUI APIs.https://nilcoalescing.com/blog/PresentingLiquidGlassSheetsInSwiftUIFri, 18 Jul 2025 20:00:00 +1200Presenting Liquid Glass sheets in SwiftUI on iOS 26

iOS 26 introduces the new Liquid Glass design, which brings updates to the appearance and behavior of sheets in SwiftUI. In this post, we'll explore these changes and how we can adopt them in our apps.

# Liquid Glass background

In iOS 26, partial height sheets appear to float above the interface, with rounded corners that follow the shape of the device and edges that don’t touch the screen. They also use the new Liquid Glass background by default.

To take advantage of this new design, we need to specify presentation detents in the sheet content and include at least one partial height option, such as medium or a custom height detent.

BirdImage()
    .sheet(isPresented: $showInfo) {
        InfoView()
            .presentationDetents([.medium, .large])
    }

When the sheet is expanded to the large detent, its background transitions to an opaque appearance, and it becomes attached to the sides and bottom of the screen.

Two iPhones showing a partial and full-height SwiftUI sheet with Liquid Glass styling

In earlier versions of iOS, we often customized sheet backgrounds using the presentationBackground() modifier, typically with a translucent material. On iOS 26, this is no longer needed and should be avoided if we want the system to apply the new Liquid Glass appearance automatically.

# Morphing sheet transitions

Another update in iOS 26 is that sheets can now morph from the toolbar button that presents them. This creates a fluid transition that fits with the Liquid Glass design and makes the sheet feel visually connected to its source.

To implement this behavior in our apps, we first need to indicate that the ToolbarItem presenting the sheet is the source by applying the matchedTransitionSource() modifier to it. We also need to apply a zoom navigation transition to the sheet content, using a matching sourceID to link it to the button.

struct ContentView: View {
    @Namespace private var transition
    @State private var showInfo = false
    
    var body: some View {
        NavigationStack {
            BirdImage()
                .toolbar {
                    ToolbarSpacer(placement: .bottomBar)
                    
                    ToolbarItem(placement: .bottomBar) {
                        Button("Info", systemImage: "info") {
                            showInfo = true
                        }
                    }
                    .matchedTransitionSource(
                        id: "info", in: transition
                    )
                }
                .sheet(isPresented: $showInfo) {
                    InfoView()
                        .presentationDetents([.medium, .large])
                        .navigationTransition(
                            .zoom(sourceID: "info", in: transition)
                        )
                }
        }
    }
}

With this setup, the toolbar button will transition into the sheet when pressed, and the sheet will return to the button when dismissed.

Toolbar button triggering a morphing transition into a partial-height sheet in SwiftUI

Note that for the transition to work correctly, the view containing the toolbar must be inside a NavigationStack or NavigationSplitView, even when using a bottom bar that would otherwise work without navigation. As of beta 3, the transition can still be glitchy at times, but this will hopefully be improved in future betas.

By adopting both the new visual appearance and morphing transitions for our sheets, we can align our apps with the updated system design in iOS 26 and use the tools SwiftUI now provides to create presentations that fit naturally within the new interface style.


If you're looking to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/CountdownTimerWithAlarmKitSchedule a countdown timer with AlarmKitStep through the essential setup for AlarmKit timers in iOS 26, from requesting authorization and scheduling a countdown to presenting the Live Activity and an in-app list of active timers.https://nilcoalescing.com/blog/CountdownTimerWithAlarmKitThu, 3 Jul 2025 18:00:00 +1200Schedule a countdown timer with AlarmKit

At WWDC 2025 Apple introduced AlarmKit, a framework that lets us schedule one-time alarms, weekly repeating alarms, and countdown timers that start immediately. Unlike notifications we schedule with UserNotifications, which stay quiet in silent mode or during a Focus unless the app holds the rarely granted Critical Alerts entitlement, AlarmKit alerts cut through silent mode and focus with a permanent banner and sound. This makes AlarmKit the first widely available way to ensure that everyday timers and alarms always surface at the right moment.

I've been experimenting with integrating countdown timer scheduling with AlarmKit into one of my projects, and in this post I’ll share how it can be done.

# Check and request authorization

Because an AlarmKit timer ends with an alert that plays sound and appears even when a Focus is active, we must ask the user for explicit permission. The first step is to add the NSAlarmKitUsageDescription key to Info.plist and write a short explanation of why our app needs to schedule alarms or timers. This text will be shown in a system prompt that we'll trigger when the user schedules a timer in our app for the first time.

Xcode Info tab displaying the Privacy – Alarm Kit Usage Description entry set to 'We’ll schedule alerts for timers you create within our app.'

The next step is to check the current authorization status and, if it’s still undetermined, prompt the user. We can do that with the AlarmManager APIs. The shared AlarmManager instance exposes the authorizationState property for reading the status and the requestAuthorization() method for showing the system prompt.

import SwiftUI
import AlarmKit

struct TimerButton: View {
    private let manager = AlarmManager.shared
    
    var body: some View {
        Button("Start a timer", systemImage: "timer") {
            Task {
                if await checkForAuthorization() {
                    // Schedule the countdown timer
                } else {
                    // Handle unauthorized status
                }
            }
        }
    }
    
    private func checkForAuthorization() async -> Bool {
        switch manager.authorizationState {
        case .notDetermined:
            do {
                let state = try await manager.requestAuthorization()
                return state == .authorized
            } catch {
                print("Authorization error: \(error)")
                return false
            }
        case .authorized: return true
        case .denied: return false
        @unknown default: return false
        }
    }
}

Calling requestAuthorization() in the .notDetermined branch triggers a system alert that shows Apple’s standard explanation of alarms and timers, followed by the custom text from NSAlarmKitUsageDescription.

iPhone displaying system prompt to allow TimerExample to schedule alarms and timers with 'Don’t Allow' and 'Allow' buttons

If the user denies permission, we can’t schedule AlarmKit timers. Depending on the app, we can disable timer-related features and direct users to Settings to enable them, or fall back to another timing method. If the user grants permission, we can go ahead and schedule a timer with AlarmKit.

# Schedule a timer

Scheduling a timer is unfortunately not as straightforward as checking for authorization, because even the simplest configuration needs a bit of setup. Let’s go over it step by step.

First we need to describe the alert that appears when the timer finishes by creating an AlarmPresentation.Alert. It requires a title and a stopButton, and can optionally have a secondary button as well.

import SwiftUI
import AlarmKit

struct TimerButton: View {
    private let manager = AlarmManager.shared
    
    var body: some View {
        Button("Start a timer", systemImage: "timer") {
            Task {
                if await checkForAuthorization() {
                    await scheduleTimer()
                } else {
                    // Handle unauthorized status
                }
            }
        }
    }
    
    private func scheduleTimer() async {
        let alert = AlarmPresentation.Alert(
            title: "Ready!",
            stopButton: AlarmButton(
                text: "Done",
                textColor: .pink,
                systemImageName: "checkmark"
            )
        )
    }
    
    ...
}

The alert appears in two sizes: a small banner when the timer ends while the device is unlocked and a larger one on the lock screen. We should keep the title short so it doesn't get truncated in the compact version.

Compact banner and full-screen lock-screen versions of the timer alert

Once the alert is in place, we can create AlarmAttributes, which requires a presentation and a tintColor. The metadata parameter is optional, but AlarmAttributes is generic over Metadata, so we still need to provide a type that conforms to AlarmMetadata, even if it’s empty. Without that type the code won’t compile and we'll see the error Generic parameter 'Metadata' could not be inferred. In projects created with Xcode 26, where types are MainActor-isolated by default, we should mark our type as nonisolated to satisfy the protocol conformance.

import SwiftUI
import AlarmKit

nonisolated struct TimerData: AlarmMetadata {}

struct TimerButton: View {
    ...
    
    private func scheduleTimer() async {
        let alert = AlarmPresentation.Alert(
            title: "Ready!",
            stopButton: AlarmButton(
                text: "Done",
                textColor: .pink,
                systemImageName: "checkmark"
            )
        )
        
        let attributes = AlarmAttributes<TimerData>(
            presentation: AlarmPresentation(alert: alert),
            tintColor: .pink
        )
    }
}

Now we can finally start a timer by calling the schedule() method of AlarmManager and passing a configuration that includes the AlarmAttributes we defined earlier together with a duration. In this example we schedule a 30-second timer, but in a real app we would set the duration from context or let the user choose it.

import SwiftUI
import AlarmKit

nonisolated struct TimerData: AlarmMetadata {}

struct TimerButton: View {
    private let manager = AlarmManager.shared
    
    ...
    
    private func scheduleTimer() async {
        let alert = AlarmPresentation.Alert(
            title: "Ready!",
            stopButton: AlarmButton(
                text: "Done",
                textColor: .pink,
                systemImageName: "checkmark"
            )
        )
        
        let attributes = AlarmAttributes<TimerData>(
            presentation: AlarmPresentation(alert: alert),
            tintColor: .pink
        )
        
        do {
            let timerAlarm = try await manager.schedule(
                id: UUID(),
                configuration: .timer(
                    duration: 30,
                    attributes: attributes
                )
            )
        } catch {
            print("Scheduling error: \(error)")
        }
    }
}

# Configure a countdown Live Activity

So far we've scheduled a timer and set up the alert presentation that appears when the timer ends. To display a continuous countdown while the timer is running, we need to set up a Live Activity that shows on the Lock Screen and in the Dynamic Island.

We'll need to add a Widget Extension target to our project and remove all the widget-related code, as we just need a Live Activity. The most basic starter configuration only requires a WidgetBundle declaration marked with @main and the Live Activity definition itself with an ActivityConfiguration using AlarmAttributes.

import WidgetKit
import SwiftUI
import AlarmKit

@main
struct CountdownTimerBundle: WidgetBundle {
    var body: some Widget {
        CountdownTimerLiveActivity()
    }
}

struct CountdownTimerLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: AlarmAttributes<TimerData>.self) { context in

        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    
                }
                DynamicIslandExpandedRegion(.trailing) {

                }
                DynamicIslandExpandedRegion(.bottom) {

                }
            } compactLeading: {
                
            } compactTrailing: {
                
            } minimal: {
                
            }
        }
    }
}

We also need to ensure that the TimerData declaration defined in the main application target is compiled into the Widget Extension. The simplest approach is to place it in a dedicated Swift file and assign that file to both targets.

Xcode showing TimerData.swift with its Target Membership set for both the app and the widget extension

The context provided inside the Live Activity contains the AlarmPresentationState, from which we can extract the countdown, create a timer-interval range, and pass it to a Text view. This text will be updating automatically as the seconds pass.

struct CountdownTimerLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: AlarmAttributes<TimerData>.self) { context in
            CountdownTextView(state: context.state)
                .font(.largeTitle)
                .padding()
        } dynamicIsland: { context in
            ...
        }
    }
}

struct CountdownTextView: View {
    let state: AlarmPresentationState
    
    var body: some View {
        if case let .countdown(countdown) = state.mode {
            Text(
                timerInterval: Date.now ... countdown.fireDate
            )
            .monospacedDigit()
            .lineLimit(1)
        }
    }
}
iPhone lock screen with a Live Activity banner counting down from 22 seconds

For the Dynamic Island presentation, we can either reuse the text countdown or choose a different approach. In this example we’ll define an additional progress view to explore a more compact alternative.

struct CountdownTimerLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: AlarmAttributes<TimerData>.self) { context in
            ...
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.bottom) {
                    HStack {
                        CountdownTextView(state: context.state)
                            .font(.headline)
                        CountdownProgressView(state: context.state)
                            .frame(maxHeight: 30)
                    }
                }
            } compactLeading: {
                CountdownTextView(state: context.state)
            } compactTrailing: {
                CountdownProgressView(state: context.state)
            } minimal: {
                CountdownProgressView(state: context.state)
            }
        }
    }
}

struct CountdownProgressView: View {
    let state: AlarmPresentationState
    
    var body: some View {
        if case let .countdown(countdown) = state.mode {
            ProgressView(
                timerInterval: Date.now ... countdown.fireDate,
                label: { EmptyView() },
                currentValueLabel: { Text("") }
            )
            .progressViewStyle(.circular)
        }
    }
}
Two iPhone screens showing a compact Dynamic Island with countdown and circular progress and an expanded Dynamic Island with larger countdown and progress

# Show active timers in the app

With scheduling and Live Activity presentation complete, the next logical step is to surface any timer that is currently running inside the foreground app itself.

AlarmKit provides a dedicated asynchronous sequence that emits the full set of active alarms every time the system adds, removes, or mutates one. By iterating over that sequence, we can keep local state in sync and render an up-to-date list.

struct TimerList: View {
    @State private var timerAlarms: [Alarm] = []
    
    var body: some View {
        List(timerAlarms) { alarm in
            TimerAlarmRow(alarm: alarm)
        }
        .task {
            for await alarms in AlarmManager.shared.alarmUpdates {
                timerAlarms.removeAll { local in
                    alarms.allSatisfy { $0.id != local.id }
                }
                
                for alarm in alarms {
                    if let index = timerAlarms.firstIndex(
                        where: { $0.id == alarm.id }
                    ) {
                        timerAlarms[index] = alarm
                    } else {
                        timerAlarms.insert(alarm, at: 0)
                    }
                }
            }
        }
    }
}

Every Alarm instance exposes just a few properties we can work with, such as its identifier, the countdown duration it was originally configured with, and its current state, such as countdown, alerting, or paused, if we support pausing. It doesn't report the live time-remaining value, that comes through the presentation state we used in the Live Activity, so the list can only display the static duration and the state label. The identifier, lets us cancel the alarm programmatically, so we can surface a cancel action in the app’s UI.

struct TimerAlarmRow: View {
    let alarm: Alarm
    
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 12) {
                Text("\(alarm.id)")
                    .font(.caption)
                
                if let countdown = alarm.countdownDuration?.preAlert {
                    let duration = Duration.seconds(countdown)
                    Text("\(duration, format: .units(width: .wide))")
                        .font(.title)
                }
                
                switch alarm.state {
                case .countdown: Text("running")
                case .alerting: Text("alerting")
                default: EmptyView()
                }
            }
            
            Spacer()
            
            Button(role: .cancel) {
                try? AlarmManager.shared.cancel(id: alarm.id)
            }
            .labelStyle(.iconOnly)
        }
    }
}
iPhone screen displaying a timers list with three 30-second alarms


AlarmKit is a welcome addition, yet even the simplest end-to-end timer flow requires setting up several components: authorization, scheduling, Live Activity configuration, Dynamic Island presentation, and an in-app overview. In this post we walked through that baseline flow to see every step in context. The sample project I used is available on GitHub, so feel free to download it and experiment. There is still plenty to explore, for example adding secondary buttons to alerts or surfacing extra details carried in the metadata.


If you are after more Swift and SwiftUI reading, take a look at my books and book bundles, they provide detailed guides and practical tips to deepen your understanding of both the Swift language and the SwiftUI framework.

]]>
https://nilcoalescing.com/blog/UsingEnumeratedWithListAndForEachUsing enumerated() with SwiftUI List and ForEach to show item numbersStarting with Swift 6.2 and iOS 26, EnumeratedSequence conforms to RandomAccessCollection, allowing enumerated() to be used directly in ForEach and List views.https://nilcoalescing.com/blog/UsingEnumeratedWithListAndForEachTue, 24 Jun 2025 18:00:00 +1200Using enumerated() with SwiftUI List and ForEach to show item numbers

When displaying items in SwiftUI, we sometimes want to include their position in the sequence, for example, to show a list of instructions or ranked results. A common way to achieve this is by calling enumerated() on the collection. This returns an EnumeratedSequence, which is a sequence of (offset, element) pairs, where offset is a counter starting from zero and element is the corresponding value from the original collection.

Until recently, enumerated() wasn’t directly compatible with ForEach or List in SwiftUI, because the result did not conform to RandomAccessCollection. As a workaround, we had to wrap the sequence in an array.

RecipeStepsView: View {
    let steps = [
        "Chop lettuce, tomatoes, and cucumber.",
        "Drizzle with olive oil and lemon juice.",
        "Toss gently and serve."
    ]
    
    var body: some View {
        VStack(alignment: .leading) {
            ForEach(
                Array(steps.enumerated()), id: \.element
            ) { offset, step in
                Text("\(offset + 1). \(step)")
            }
        }
    }
}

As of Swift 6.2 and iOS 26, this extra step is no longer required. Proposal SE-0459 adds Collection, RandomAccessCollection, and BidirectionalCollection conformance to EnumeratedSequence when the base collection supports those protocols.

ForEach(
    steps.enumerated(), id: \.element
) { offset, step in
    Text("\(offset + 1). \(step)")
}

It’s important to note that the offset in EnumeratedSequence is a counter and should not be used as an index to access elements from the original collection. While it may appear to work when the base collection is zero-based and uses integer indices, this assumption breaks down in other scenarios, for example, when iterating over a SubSequence or any collection that does not start at zero. For more details and recommended approaches, see my other post: Iterate over items and indices in Swift collections.

Another important point to consider is that neither offset nor an index should be used as the id in ForEach unless the collection is immutable and the ordering is guaranteed to remain stable. In dynamic collections where elements can be added, removed, or reordered, such as in user-editable lists, using a counter or index as the identifier can result in incorrect view updates and animations. In these cases, a stable and unique identifier should be used instead, either by conforming the element type to Identifiable or by supplying a unique key path.

struct Task: Identifiable {
    var id = UUID()
    var title: String
}

struct TaskListView: View {
    @State private var tasks = [
        Task(title: "Buy vegetables"),
        Task(title: "Prep dressing"),
        Task(title: "Set the table")
    ]
    
    var body: some View {
        List {
            ForEach(
                tasks.enumerated(),
                id: \.element.id
            ) { offset, task in
                Text("\(offset + 1). \(task.title)")
            }
            .onDelete { indices in
                tasks.remove(atOffsets: indices)
            }
        }
    }
}

This pattern keeps the display numbering from enumerated() while maintaining stable identity through the task’s id. By decoupling numbering from identity, it ensures correct behavior as the data changes.

If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/StretchyHeaderInSwiftUIStretchy header in SwiftUI with visualEffect()Build a stretchy image header in SwiftUI using the visualEffect() modifier, scaling the image on pull-down without tracking scroll offset or modifying its frame.https://nilcoalescing.com/blog/StretchyHeaderInSwiftUIMon, 16 Jun 2025 18:00:00 +1200Stretchy header in SwiftUI with visualEffect()

It’s a common design pattern in modern iOS apps to have a large image at the top of a scroll view, extending all the way into the top safe area. When the user pulls down to overscroll, instead of revealing empty space above the image, the image expands, growing in size and creating a dynamic visual effect.

There are a few ways to achieve this effect in SwiftUI and I most commonly see developers using onScrollGeometryChange() where they get the scroll offset, write it to a shared app state, and then use it to compute the frame of the image. But we can also do it in a slightly simpler way using the visualEffect() modifier.

This modifier takes a closure with two arguments: an EmptyVisualEffect that we can apply additional effects to, and a GeometryProxy that gives us access to the size and coordinates of the view, which we can use to get its frame in scroll view coordinate space. Given we have everything we need for our stretchy effect inside this closure, we can create a simple, self-contained view modifier that doesn’t need any additional state tracking.

extension View {
    func stretchy() -> some View {
        visualEffect { effect, geometry in
            let currentHeight = geometry.size.height
            let scrollOffset = geometry.frame(in: .scrollView).minY
            let positiveOffset = max(0, scrollOffset)
            
            let newHeight = currentHeight + positiveOffset
            let scaleFactor = newHeight / currentHeight
            
            return effect.scaleEffect(
                x: scaleFactor, y: scaleFactor,
                anchor: .bottom
            )
        }
    }
}

We get the view’s current height and its scroll offset from the GeometryProxy, and we make sure to only account for the positive offset, since we just want the image to grow when the user overscrolls and don’t want it to shrink when the user scrolls up to reveal more content below the image. We then compute the view’s new height and the scale factor, and apply a scale effect to the EmptyVisualEffect with the bottom anchor, so that the image stretches up without affecting the rest of the layout underneath it.

Now all that’s left is to apply our new stretchy() modifier to the image we want to grow when the user pulls down.

struct FlowerView: View {
    let flower: Flower
    
    var body: some View {
        ScrollView {
            VStack {
                Image(flower.name)
                    .resizable()
                    .scaledToFill()
                    .stretchy()
                
                FlowerInfo(flower: flower)
            }
        }
        .ignoresSafeArea(edges: .top)
    }
}
Scrollable flower detail screen in a SwiftUI app where a full-width peony image at the top stretches smoothly when overscrolled

This gives us a smooth, self-contained stretchy header effect with no extra state or offset tracking needed, and it can be easily reused throughout the app.

If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/BackgroundExtensionEffectInSwiftUICreate immersive backgrounds in SwiftUI with backgroundExtensionEffect()The new backgroundExtensionEffect() modifier in iOS 26 lets us extend and blur visual content beyond a view’s bounds, creating continuous backgrounds behind elements like sidebars, inspectors, and overlay controls.https://nilcoalescing.com/blog/BackgroundExtensionEffectInSwiftUIWed, 11 Jun 2025 18:00:00 +1200Create immersive backgrounds in SwiftUI with backgroundExtensionEffect()

WWDC25 introduced a wave of visual and functional updates, from the new Liquid Glass design language to redesigned system apps and deeper SwiftUI integration across platforms. With the first iOS 26 betas now available, I’ve been exploring some of the new APIs, and one that immediately caught my eye is backgroundExtensionEffect().

This modifier is mentioned in the updated Human Interface Guidelines and WWDC sessions as a way to extend content behind the control layer. It’s designed for cases like showing content beneath sidebars, inspectors, toolbars, or custom controls, and it can add an immersive effect to SwiftUI interfaces.

# Extend the detail column image behind the sidebar

The most obvious use case for backgroundExtensionEffect() is to apply it to a view in the detail column of a NavigationSplitView. We can use it on the graphic placed at the top of the detail view, and it will mirror and blur that content to continue its colors underneath the sidebar. This creates a sense of visual continuity, similar to what we see in some system apps, like Podcasts, for example.

In the code sample below, we are using this technique in a recipe detail view. The header image extends behind the sidebar using backgroundExtensionEffect(), making the layout feel more connected across the interface.

struct RecipeDetailView: View {
    let recipe: Recipe
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                Image(recipe.imageName)
                    .resizable()
                    .scaledToFill()
                    .backgroundExtensionEffect()
                
                RecipeNameAndDescription(recipe: recipe)
            }
        }
        .ignoresSafeArea(edges: .top)
    }
}
iPad showing a dessert list with Tiramisu selected and its image displayed demonstrating background extension effect extending blurred content behind the sidebar

# Extend and blur images to create continuous backgrounds in cards

Another interesting effect we can achieve with a creative use of the new backgroundExtensionEffect() modifier is the kind of continuous background in cards sometimes seen in system apps. Instead of treating the image as a standalone element, we can extend and blur it to serve as a visual backdrop for overlay content like text or controls.

In the example below, we're building a horizontal scroll of recipe cards. Each card displays an image at the top, and by applying backgroundExtensionEffect(), we extend and blur that image into the surrounding safe area. To make use of this extended region, we place the text content inside a safeAreaInset() attached to the image itself. This lets us position labels or controls on top of the extended background without overlapping the original image bounds. To ensure readability across a variety of images, we also add a subtle linear gradient behind the text.

struct RecipeCards: View {
    let recipes: [Recipe]
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack(spacing: 24) {
                ForEach(recipes) { recipe in
                    RecipeCard(recipe: recipe)
                }
            }
            .padding()
        }
    }
}

struct RecipeCard: View {
    let recipe: Recipe
    
    var body: some View {
        Image(recipe.imageName)
            .resizable()
            .scaledToFill()
            .frame(
                minWidth: 0, maxWidth: .infinity,
                minHeight: 0, maxHeight: .infinity
            )
        
            .backgroundExtensionEffect()
            .safeAreaInset(edge: .bottom) {
                RecipeNameAndDescription(recipe: recipe)
                    .foregroundStyle(.white)
                    .padding()
                    .background(
                        LinearGradient(
                            colors: [
                                .black.opacity(0.6),
                                .clear
                            ],
                            startPoint: .bottom,
                            endPoint: .top
                        )
                    )
            }
            .clipShape(
                RoundedRectangle(
                    cornerRadius: 26,
                    style: .continuous
                )
            )
        
            .frame(width: 400)
    }
}
iPad displaying a horizontal scroll of dessert cards with blurred extended backgrounds, showing chocolate chip cookies, berry parfait, and linzer tart

This approach can work well in layouts where we layer text or controls over images, such as titles, labels, or playback indicators. The blurred extension softens the transition between the image and interface elements, helping the layout feel more unified and balanced. But as with any visual effect, it’s important to test that the content remains clear and legible across different images, themes, and contexts.

If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/EnableScrollingBasedOnContentSizeInSwiftUIEnable scrolling based on content size in SwiftUISupport large accessibility text by wrapping content in a scroll view, and prevent unnecessary bounce by enabling scrolling only when the content doesn’t exceed the screen size.https://nilcoalescing.com/blog/EnableScrollingBasedOnContentSizeInSwiftUIMon, 26 May 2025 18:00:00 +1200Enable scrolling based on content size in SwiftUI

When designing interfaces in SwiftUI, it’s often a good idea to wrap our app’s content in a scroll view, even if the content usually fits on screen. This helps ensure that users who enable larger text sizes in accessibility settings can still access all the content without layout issues, clipped views and truncated text. However, doing this introduces an unintended side effect. By default, ScrollView adds a bouncy scrolling behavior, even when the content fits entirely within the available space. This can make the interface feel oddly springy when no scrolling is actually needed.

iOS screen with default-sized text in a scroll view that bounces slightly despite content fitting the screen

To address this, we can use the scrollBounceBehavior(_:axes:) modifier introduced in iOS 16.4. When we apply this modifier to a ScrollView or to a view hierarchy that contains one, and pass the basedOnSize value, SwiftUI automatically disables bounce behavior when the content fits and enables it only when scrolling is actually required.

ScrollView {
    WalkDetailView()
}
.scrollBounceBehavior(.basedOnSize)
iOS screen with default text size showing static layout without any scrolling or bounce

This small adjustment helps create a more polished experience, adapting gracefully to both default and accessibility text sizes.

]]>
https://nilcoalescing.com/blog/MeshGradientsInSwiftUIMesh gradients in SwiftUIExplore ways to create and customize mesh gradients in SwiftUI, including color adjustments, finer control with Bezier points, and color position animations to add variety and emphasis to your design.https://nilcoalescing.com/blog/MeshGradientsInSwiftUIMon, 19 May 2025 18:00:00 +1200Mesh gradients in SwiftUI

A mesh gradient is a technique for rendering smooth, multi-directional color transitions across a surface using a structured network of control points. Unlike linear or radial gradients, which interpolate color along fixed axes, a mesh gradient defines colors at specific positions within a two-dimensional grid. These positions act as anchors, and the rendering engine computes smooth interpolations between them across the surface.

This makes mesh gradients particularly well-suited for complex, organic color transitions that cannot be achieved with simpler gradient types. They have been part of vector illustration tools for years and are now available in SwiftUI starting with iOS 18 and macOS 15, where they can be used for both static designs and animated effects.

# Defining a mesh gradient

To define a mesh gradient in SwiftUI, we can use the MeshGradient struct and choose one of its available initializers. We always have to provide the width and height of the mesh, which define how many points are arranged horizontally and vertically in the grid.

The simplest mesh gradient we can make is a 2×2 grid. This gives us four corner points, each with its own position and color, and defines a single patch, which is a rectangular area where SwiftUI blends the colors between the four corners.

MeshGradient(
    width: 2,
    height: 2,
    points: [
        [0, 0], [1, 0],
        [0, 1], [1, 1]
    ],
    colors: [
        .purple, .mint,
        .orange, .blue
    ]
)

Mesh points are expressed as SIMD2<Float>, but we can also use array literals to create each point, as in our example. The points array defines the position of each vertex in the mesh, laid out row by row. The colors array assigns a color to each point in the same order. SwiftUI takes care of interpolating the color transitions between these points to produce a smooth blend across the surface.

Mesh gradient using purple, mint, orange, and blue, blended smoothly across a rectangular surface

# Gradient geometry

The points we use to define a mesh gradient are in the coordinate space of the gradient view. A point like [0, 0] refers to the top-left corner, while [1, 1] refers to the bottom-right. If we want the gradient to fit exactly within its container, the outermost points in the mesh must span the full range from 0 to 1 along both axes. This means the top row of points should have a y-coordinate of 0, the bottom row a y-coordinate of 1, and the left and right columns x-coordinates of 0 and 1 respectively.

But we can also assign coordinates outside that range. For example, if we move the top-left point to [-0.4, -0.4] instead of [0, 0], the most intense purple color will begin outside the visible area, resulting in a more subtle appearance within the view.

MeshGradient(
    width: 2,
    height: 2,
    points: [
        [-0.4, -0.4], [1, 0],
        [0, 1], [1.0, 1.0]
    ],
    colors: [
        .purple, .mint,
        .orange, .blue
    ],
)
Mesh gradient with the top-left point offset outside the view, creating a softer purple blend into mint, orange, and blue

In contrast, if a point that lies along the outer edge of the mesh doesn’t reach the corresponding edge of the view, the gradient will stop short in that area. Setting the bottom-right point to [0.8, 0.9], for instance, leaves part of the view uncovered, since the mesh no longer fully reaches the bottom and trailing edges.

MeshGradient(
    width: 2,
    height: 2,
    points: [
        [0, 0], [1, 0],
        [0, 1], [0.8, 0.9]
    ],
    colors: [
        .purple, .mint,
        .orange, .blue
    ]
)
Mesh gradient with the bottom-right point inset, causing the mesh to stop short in that corner and reveal the view’s background

# Background fill

By default, the background of a mesh gradient is set to Color.clear. This means that any part of the view not covered by the mesh, such as areas where the points don’t extend all the way to the edges, will show whatever is behind the gradient. That might be the background of the containing view or the window itself if nothing else is drawn underneath. If we want to fill those uncovered areas with a specific color instead, we can pass a background parameter to the initializer.

MeshGradient(
    width: 2,
    height: 2,
    points: [
        [0, 0], [1, 0],
        [0, 1], [0.8, 0.9]
    ],
    colors: [
        .purple, .mint,
        .orange, .blue
    ],
    background: .indigo
)
Mesh gradient with the bottom-right point inset, revealing the indigo background in the uncovered corner

# Color adjustments

Each color in the colors array we provide to the gradient initializer can be modified using any method on Color that returns a Color. For example, we can adjust the opacity of individual segments to create a more muted appearance.

MeshGradient(
    width: 2,
    height: 2,
    points: [
        [0, 0], [1, 0],
        [0, 1], [1.0, 1.0]
    ],
    colors: [
        .purple.opacity(0.6), .mint.opacity(0.7),
        .orange.opacity(0.5), .blue.opacity(0.8)
    ],
    background: .indigo
)
.background(.white)

In this case, the color passed in the background parameter of the initializer does not show through the semi-transparent colors. It only fills areas outside the bounds of the mesh if the points don’t fully span the view, as we saw earlier. The transparency of the colors themselves allows the view behind the gradient to show through, which in this example is the white background applied to the gradient view.

Mesh gradient with semi-transparent colors over a white background, producing a soft blend of purple, mint, orange, and blue

# Bezier points

While creating a mesh gradient from SIMD2<Float> points is the most straightforward approach, we can also declare it using MeshGradient.BezierPoint instead. This gives us more control over how the colors stretch and blend. Even if we stick to a simple 2×2 grid, we can shape the color transitions by adjusting the control points of each Bezier point. For example, we could make the orange color in the bottom-leading corner take up more space by positioning its top control point above the center of the mesh and its trailing control point farther toward the trailing edge.

MeshGradient(
    width: 2,
    height: 2,
    bezierPoints: [
        // Top-left: [0, 0]
        MeshGradient.BezierPoint(
            position: [0.0, 0.0],
            leadingControlPoint: [0.0, 0.0],
            topControlPoint: [0.0, 0.0],
            trailingControlPoint: [0.0, 0.0],
            bottomControlPoint: [0.0, 0.0]
        ),
        // Top-right: [1, 0]
        MeshGradient.BezierPoint(
            position: [1.0, 0.0],
            leadingControlPoint: [1.0, 0.0],
            topControlPoint: [1.0, 0.0],
            trailingControlPoint: [1.0, 0.0],
            bottomControlPoint: [1.0, 0.0]
        ),
        // Bottom-left: [0, 1]
        MeshGradient.BezierPoint(
            position: [0.0, 1.0],
            leadingControlPoint: [0.0, 1.0],
            topControlPoint: [0.0, 0.4],
            trailingControlPoint: [0.9, 1.0],
            bottomControlPoint: [0.0, 1.0],
        ),
        // Bottom-right: [1, 1]
        MeshGradient.BezierPoint(
            position: [1.0, 1.0],
            leadingControlPoint: [1.0, 1.0],
            topControlPoint: [1.0, 1.0],
            trailingControlPoint: [1.0, 1.0],
            bottomControlPoint: [1.0, 1.0]
        )
    ],
    colors: [
        .purple, .mint,
        .orange, .blue
    ]
)
Mesh gradient with adjusted control points on the bottom-left to stretch the orange color upward and toward the center

# Advanced grids

Whether we are using SIMD2<Float> or BezierPoint to construct the grid, we can experiment by adding more points to create unique and expressive gradients. Below is an example of a 4×4 mesh that uses bright colors and an interesting distribution of points across the surface. The slight shifts in interior point positions introduce gentle curves and directionality.

MeshGradient(
    width: 4,
    height: 4,
    points: [
        [0.0, 0.0], [0.3, 0.0], [0.7, 0.0], [1.0, 0.0],
        [0.0, 0.3], [0.2, 0.4], [0.7, 0.2], [1.0, 0.3],
        [0.0, 0.7], [0.3, 0.8], [0.7, 0.6], [1.0, 0.7],
        [0.0, 1.0], [0.3, 1.0], [0.7, 1.0], [1.0, 1.0]
    ],
    colors: [
        .purple, .indigo, .purple, .yellow,
        .pink, .purple, .pink, .yellow,
        .orange, .pink, .yellow, .orange,
        .yellow, .orange, .pink, .purple
    ]
)
Mesh gradient with vibrant pink, purple, orange, and yellow colors, creating curved transitions and saturated highlights across the surface

As we add more points, though, it becomes increasingly important to pay attention to how they are arranged. We should place the points in a consistent progression from lower to higher values, both across rows and down columns, for the automatic blending of colors to work correctly. Specifically, the x-values should increase from left to right within each row, and the y-values should increase from top to bottom within each column.

For instance, in the second row of our earlier example, the points were ordered as [0.0, 0.3], [0.3, 0.4], [0.7, 0.2], [1.0, 0.3]. If we swap the x-values of the two middle points so the row becomes [0.0, 0.3], [0.7, 0.4], [0.2, 0.2], [1.0, 0.3], the horizontal progression is no longer valid. As a result, the color blending breaks down. Instead of transitioning gradually, we'll get a clean-edged wave running through the middle. This might be the intended effect in some designs, but it's important to understand that point ordering affects how colors blend across the surface.

MeshGradient(
    width: 4,
    height: 4,
    points: [
        [0.0, 0.0], [0.3, 0.0], [0.7, 0.0], [1.0, 0.0],
        [0.0, 0.3], [0.7, 0.4], [0.2, 0.2], [1.0, 0.3],
        [0.0, 0.7], [0.3, 0.8], [0.7, 0.6], [1.0, 0.7],
        [0.0, 1.0], [0.3, 1.0], [0.7, 1.0], [1.0, 1.0]
    ],
    colors: [
        .purple, .indigo, .purple, .yellow,
        .pink, .purple, .pink, .yellow,
        .orange, .pink, .yellow, .orange,
        .yellow, .orange, .pink, .purple
    ]
)
Mesh gradient with pink and purple tones forming a wave-like shape that cuts through a bright yellow-orange background

# Animated gradients

If we want to take mesh gradient design even further, we can animate the positions of individual points. This creates a dynamic, flowing visual effect as the gradient continuously shifts and reshapes itself. Since the mesh is defined by precise point positions, even small changes can produce interesting motion and fluid transitions in how the colors blend.

In the example below, we use a TimelineView with the animation schedule to update the mesh over time. A sine and cosine wave are used to offset a few of the interior points, which results in a smooth oscillating effect across the gradient.

TimelineView(.animation) { context in
    let time = context.date.timeIntervalSince1970
    let offsetX = Float(sin(time)) * 0.1
    let offsetY = Float(cos(time)) * 0.1
    
    MeshGradient(
        width: 4,
        height: 4,
        points: [
            [0.0, 0.0],
            [0.3, 0.0],
            [0.7, 0.0],
            [1.0, 0.0],
            [0.0, 0.3],
            [0.2 + offsetX, 0.4 + offsetY],
            [0.7 + offsetX, 0.2 + offsetY],
            [1.0, 0.3],
            [0.0, 0.7],
            [0.3 + offsetX, 0.8],
            [0.7 + offsetX, 0.6],
            [1.0, 0.7],
            [0.0, 1.0],
            [0.3, 1.0],
            [0.7, 1.0],
            [1.0, 1.0]
        ],
        colors: [
            .purple, .indigo, .purple, .yellow,
            .pink, .purple, .pink, .yellow,
            .orange, .pink, .yellow, .orange,
            .yellow, .orange, .pink, .purple
        ]
    )
}
Animated mesh gradient with flowing color shifts across purple, pink, orange, and yellow tones

By animating only a few internal points and keeping the outer edges fixed, the gradient maintains its overall frame while producing natural movement within. This technique is especially effective for backgrounds, loading states, or interactive visualizations where a subtle sense of motion adds depth and interest.

Mesh gradients are an interesting component to explore. They can be used in a variety of ways, such as dynamic backgrounds, decorative fills for shapes, or to draw attention to specific elements like buttons. When used thoughtfully and in moderation, they can add visual variety without overwhelming the interface.

If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/FormattingDataInsideSwiftUITextViewsFormatting data inside SwiftUI Text viewsFormat interpolated values like arrays of strings, measurements, and dates directly inside SwiftUI Text views using FormatStyle, and display dynamic dates using Text.DateStyle.https://nilcoalescing.com/blog/FormattingDataInsideSwiftUITextViewsThu, 8 May 2025 18:00:00 +1200Formatting data inside SwiftUI Text views

I’ve just started a new "Nil Coalescing" YouTube channel, and in the first video I take a closer look at how we can use FormatStyle and Text.DateStyle directly inside SwiftUI Text views to format values inline using string interpolation. This approach allows us to display arrays of strings, measurements, dates and other types of data in a clean, declarative way without writing additional formatting code.

If you enjoy video content, make sure to check out the video on YouTube and subscribe to our channel.

▶️ Formatting Data inside SwiftUI Text Views

In this post, I’ll go through the same examples I cover in the video, with written explanations and code you can easily reference or copy as needed.

# Interpolation and formatting in Text

Text is one of my favorite components in SwiftUI. It looks simple at first, but it handles a significant amount of logic internally. It has some really useful capabilities that are not immediately obvious, and one of my favorites is the ability to format values directly inside string interpolation.

When we initialize a Text view from a string literal, SwiftUI treats it as a LocalizedStringKey. In addition to enabling automatic localization, LocalizedStringKey supports interpolation of values and lets us provide an optional format parameter, which is intended for formatting dates, numbers, measurements, and other types of data directly inside the text view.

let date = Date()
        
Text("Today's date is \(date, format: .dateTime.day().month()).")

# Formatting arrays of strings

When interpolating an array of strings inside a Text view, we can apply the list format style and specify the list type. SwiftUI will turn the array into a grammatically correct list and automatically include a conjunction — like "and" or "or" — based on the list type we choose.

let activities = ["hiking", "swimming", "cycling"]
           
// Weekend activities: hiking, swimming, and cycling
Text("Weekend activities: \(activities, format: .list(type: .and))")

The format including the conjunction, will be adapted to the user’s locale, so we don’t need to add any extra logic to display list data in a natural way.

let activités = ["randonnée", "natation", "vélo"]

// Activités du week-end: randonnée, natation et vélo
Text("Activités du week-end: \(activités, format: .list(type: .and))")
    .environment(\.locale, Locale(identifier: "fr"))

# Displaying measurements inline

Displaying measurements inline is another case where interpolation with format styles is especially useful. Instead of converting units ourselves or building strings manually, we can just interpolate a Measurement value and use the measurement format. SwiftUI will take care of converting and displaying measurements using the most appropriate unit.

We can set the value in whatever unit we prefer, like meters in our example, and SwiftUI will automatically display it in miles for users in the US, or kilometers for users in New Zealand, without any extra logic on our part.

let distance = Measurement(value: 3200, unit: UnitLength.meters)

// You walked 2 mi.
Text("You walked \(distance, format: .measurement(width: .abbreviated)).")
    .environment(\.locale, Locale(identifier: "en_US"))

// You walked 3.2 km.
Text("You walked \(distance, format: .measurement(width: .abbreviated)).")
    .environment(\.locale, Locale(identifier: "en_NZ"))

We can also control how the unit is shown - with a short abbreviation, or a full name. It keeps the code clean, and the result is always localized and consistent.

# Formatting dates

Dates can be formatted in two different ways. To show a static date, like the time of an event, the format parameter can be used just like with arrays and measurements.

let eventDate = Calendar.current.date(
    from: DateComponents(year: 2025, month: 5, day: 5, hour: 13)
)!

// Event time: 1:00 PM
Text("Event time: \(eventDate, format: .dateTime.hour().minute())")

But if we want to display dynamic information, like the time left until the event, we can use the style parameter instead. Applying styles like relative, offset and timer, will result in an autoupdating date without any additional code.

Text("\(eventDate, style: .relative) left until the event")
    .monospacedDigit()

The relative style, that we are using in our example, displays the date as relative to now, and the Text view automatically refreshes. When displaying dynamic times like this it’s also a good idea to apply the monospacedDigit() modifier to the Text. It makes sure that all numeric characters take up the same width, so the UI doesn’t jitter as the value changes.

Animated countdown displaying '8 min, 44 secs left until the event' with the time decreasing in real time

These are just a few examples of how we can format data directly inside a Text view using interpolation. It keeps the code simple, handles localization automatically, and works great for displaying structured or time-sensitive information without much effort.

If you’re interested in learning more about how Text and other core components work in SwiftUI, and how to use the framework effectively in your projects, check out my book SwiftUI Fundamentals. It combines what I’ve learned from using SwiftUI since its release and working on its source code at Apple, to give you a solid understanding of the most important aspects of the framework.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/FormatLongParameterListsIntoSeparateLinesInXcodeFormat long parameter lists into separate lines in XcodeQuickly split long function calls or initializers into separate lines using a built-in Xcode shortcut.https://nilcoalescing.com/blog/FormatLongParameterListsIntoSeparateLinesInXcodeFri, 2 May 2025 18:00:00 +1200Format long parameter lists into separate lines in Xcode

When working with functions or initializers that take many parameters, especially when some values are closures or long arrays, the code can become difficult to scan and maintain. Xcode includes a useful keyboard shortcut for improving the readability of these calls.

To reformat them quickly, place the cursor inside the parentheses and press Control (⌃) + M. Xcode will automatically place each parameter on its own line.

If the entire expression is selected before using the shortcut, Xcode will also format the values, making closures and collections easier to read at a glance.

LinearGradient initializer with multiple parameters being reformatted onto separate lines using the Control-M shortcut]]>
https://nilcoalescing.com/blog/EmbeddingACustomFontIntoAMacOSAppBundleEmbedding a custom font into a macOS app bundleAdd a custom font to your Mac app by embedding it in the app bundle and making sure it loads correctly for use in your UI.https://nilcoalescing.com/blog/EmbeddingACustomFontIntoAMacOSAppBundleWed, 30 Apr 2025 18:00:00 +1200Embedding a custom font into a macOS app bundle

We commonly use Apple system fonts in our apps, but sometimes we may need something custom for company branding or a specific design. For example, in my macOS word game LexiMorph, I needed a cleanly outlined typeface for the glowing base word, and the default system font didn’t work for what I had in mind.

To use a custom font in our UI, we need to embed it directly into the app bundle so we are not relying on that particular typeface being installed on the user's machine. This involves copying the font file into the project in Xcode, making sure it’s included in the built product, and then referencing it in code when styling our text.

The process of embedding a custom font in a macOS app is a little different from how things work on iOS, and it’s not particularly well documented. I decided to write up the exact steps that worked for me, both as a reference for myself and in case it helps other developers trying to add custom fonts to their Mac apps.

# Add custom font file to Xcode project

Imagine we want to embed a Google font named "Indie Flower" and use it in our app. We’ll download the font file and the accompanying license from Google Fonts, then drag both into our Xcode project and place them in a folder named "Fonts".

Xcode project showing a custom font file selected with preview and no target membership assigned

Notice that we’ll leave their target membership unchecked, as we’ll be adding them to the build using a separate copy step.

# Copy font files to application resources

To make sure the custom font is loaded and accessible to our code, we need to copy it into the Resources folder of the app bundle. To do that, we’ll define a custom Copy Files build phase with the destination set to Resources and the subpath set to Fonts. Then we’ll add the font files from the "Fonts" folder in Xcode to that phase.

Xcode project showing a Copy Files build phase configured to copy a custom font to the Resources/Fonts directory

# Provide font path in Info.plist

The final step is to tell the system where to find the font. We do this by adding the ATSApplicationFontsPath key to the Info.plist file and setting its value to the path of the font files relative to the Resources folder. In our case, the value will simply be Fonts.

Xcode project Info tab showing the ATSApplicationFontsPath key set to Fonts in the app's Info.plist

# Load custom font and apply it to text

At this point, the font is embedded in the app bundle and registered with the system, so we can start using it in code. The way we reference it is by name, the same name that’s defined inside the font file itself, not necessarily the file name.

To load the custom font in AppKit, we can use the NSFont(name:size:) initializer:

let font = NSFont(name: "IndieFlower", size: 28)!

In SwiftUI, we can initialize a custom font using the Font.custom(_:size:) method and apply it to a Text view using the font(_:) modifier:

Text("Hello, world!")
    .font(.custom("IndieFlower", size: 28))
macOS app window displaying Hello, world! using a custom font

If you're using SwiftUI in your projects and want to get a more thorough grasp of the framework, my new book SwiftUI Fundamentals takes an in-depth look at the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/FullyCustomAboutWindowForAMacAppInSwiftUICreate a fully custom About window for a Mac app in SwiftUIDesign a custom About window for your SwiftUI macOS app with a personalized layout, detailed app information, and a styled background that fits your app’s look and feel.https://nilcoalescing.com/blog/FullyCustomAboutWindowForAMacAppInSwiftUIWed, 23 Apr 2025 18:00:00 +1200Create a fully custom About window for a Mac app in SwiftUI

Mac apps typically include an About window that displays basic app information such as the version number and other details. Every macOS app project comes with a default About window accessible from the main app menu. This built-in panel shows the app icon, name, version, and build number.

Menu bar of a macOS app named PhotoTune with the About window open, showing the app icon, name, and version number against a dark background

Back in 2020, I wrote a post about how we can enhance this default panel using a mix of SwiftUI and AppKit APIs, along with credits files and Info.plist entries — Customize About panel on macOS. But starting with macOS 13, thanks to the introduction of the Window scene and the openWindow environment value, we can now create and present a fully custom About window using just standard SwiftUI APIs.

# Define a custom About window view

We can define the layout of the About window using SwiftUI, just like we would for any other window in the app. This gives us full control over the appearance and content, so we can make the About window match our app’s style and include whatever information we want.

struct AboutView: View {
    private var appVersionAndBuild: String {
        let version = Bundle.main
            .infoDictionary?["CFBundleShortVersionString"] as? String ?? "N/A"
        let build = Bundle.main
            .infoDictionary?["CFBundleVersion"] as? String ?? "N/A"
        return "Version \(version) (\(build))"
    }
    
    private var copyright: String {
        let calendar = Calendar.current
        let year = calendar.component(.year, from: Date())
        return  \(year) Nil Coalescing Limited"
    }
    
    private var developerWebsite: URL {
        URL(string: "https://nilcoalescing.com/")!
    }
    
    var body: some View {
        VStack(spacing: 14) {
            Image(.mainIcon)
                .resizable().scaledToFit()
                .frame(width: 80)
            Text("PhotoTune")
                .font(.title)
            VStack(spacing: 6) {
                Text(appVersionAndBuild)
                Text(copyright)
            }
            .font(.callout)
            Link(
                "Developer Website",
                destination: developerWebsite
            )
            .foregroundStyle(.accent)
        }
        .padding()
        .frame(minWidth: 400, minHeight: 260)
    }
}

We are free to lay this out however we like, and include any details we find useful, such as version and build number, copyright, or a link to the developer’s website.

# Present the custom About window

To present our custom AboutView in place of the default About panel, we first need to wrap it in a Window scene. This is the right scene type to use because it creates a unique window, unlike WindowGroup, which allows users to open multiple instances of the same window template.

Once the scene is defined, we can replace the default About button in the app menu with a custom one. We'll do this by overriding the appInfo command placement with a new CommandGroup. Inside the custom button, we'll call the openWindow action from the environment to present our window.

@main
struct PhotoTuneApp: App {
    @Environment(\.openWindow) private var openWindow
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            CommandGroup(replacing: CommandGroupPlacement.appInfo) {
                Button {
                    openWindow(id: "about")
                } label: {
                    Text("About PhotoTune")
                }
            }
        }
        
        Window("About PhotoTune", id: "about") {
            AboutView()
        }
    }
}

Now, when selecting "About PhotoTune" from the app menu, our custom AboutView will be presented in a standalone window.

Menu bar of a macOS app named PhotoTune with a custom About window open, showing the app icon, name, version, copyright, and a link to the developer website

# Fine-tune window appearance and behavior

By presenting a custom window with the Window scene, we gain full control over not just the content of the About window, but also its appearance and behavior. This includes styling the background, adjusting window chrome, modifying drag and resize options, and more.

Let’s look at a few examples of what we can do, but note that while the Window scene and openWindow environment value have been available since macOS 13, many of the APIs for customizing window appearance and behavior, such as containerBackground(), windowBackgroundDragBehavior(), windowMinimizeBehavior(), and others, were introduced in later versions, so these features will require a newer deployment target.

One example is applying a translucent material background to the window and removing the title and toolbar, allowing the material effect to extend all the way to the top. When doing this, it’s a good idea to let users drag the window by its background area, which we can enable using the windowBackgroundDragBehavior() scene modifier.

Window("About PhotoTune", id: "about") {
    AboutView()
        .containerBackground(.regularMaterial, for: .window)
        .toolbar(removing: .title)
        .toolbarBackground(.hidden, for: .windowToolbar)
}
.windowBackgroundDragBehavior(.enabled)
Custom About window for the macOS app PhotoTune with a translucent material background

Another example is controlling how the window can be resized. Since the contents of our AboutView are static, we may want to prevent resizing altogether. We can do that by applying windowResizability(.contentSize) to the window.

We can also customize the window controls in the top-left corner. Because our window isn’t resizable, the green zoom control is already disabled by default, but we can additionally disable the yellow minimize control. This control isn't needed here, since the About window only shows static content and can always be reopened from the app menu.

Window("About PhotoTune", id: "about") {
    AboutView()
        .toolbar(removing: .title)
        .toolbarBackground(.hidden, for: .windowToolbar)
        .containerBackground(.regularMaterial, for: .window)
        .windowMinimizeBehavior(.disabled)
}
.windowBackgroundDragBehavior(.enabled)
.windowResizability(.contentSize)
Custom macOS About window for the PhotoTune app with only the close button visible

By default, windows in SwiftUI support state restoration, they remember whether they were open when the app last quit and reopen automatically. This behavior isn’t necessary for an About window, so we can turn it off using the restorationBehavior() modifier.

Window("About PhotoTune", id: "about") {
    ...
}
.windowBackgroundDragBehavior(.enabled)
.windowResizability(.contentSize)
.restorationBehavior(.disabled)

Creating a fully custom About window in SwiftUI gives us the flexibility to match the design and behavior of the rest of the app, while still integrating cleanly with macOS conventions. With just a few modifiers, we can control layout, appearance, and window behavior — all using familiar SwiftUI tools.

If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/ImageAccessibilityLabelsFromLocalizableStringsFilesAutomatic image accessibility labels from localized strings in SwiftUISwiftUI uses a localized string matching the image name as the accessibility label, without needing to apply the accessibilityLabel() modifier.https://nilcoalescing.com/blog/ImageAccessibilityLabelsFromLocalizableStringsFilesWed, 16 Apr 2025 18:00:00 +1200Automatic image accessibility labels from localized strings in SwiftUI

When an image is loaded by name using the Image(_:) initializer in SwiftUI, VoiceOver uses the image name itself as the default accessibility label.

For example, if we have a custom image in the asset catalog named person.bicycle, VoiceOver will read "person bicycle" unless a custom label is explicitly provided.

// VoiceOver reads "person bicycle" by default
Image("person.bicycle")

However, if a localized string is defined for the image name, either in a Localizable.strings file or a string catalog, SwiftUI will automatically use the localized value as the accessibility label, without requiring the accessibilityLabel() modifier.

String catalog entry in Xcode showing a localized value for the image name person.bicycle

In our example, using "person.bicycle" = "Person on a bicycle" for the English localization causes VoiceOver to read "Person on a bicycle" when the image is focused, with no additional code changes needed.

]]>
https://nilcoalescing.com/blog/ForegroundColorStyleAndTintInSwiftUIWays to customize text color in SwiftUISwiftUI offers several methods to change the color of text, including foregroundStyle() and tint() modifiers, AttributedString attributes, and the textRenderer() API for advanced styling.https://nilcoalescing.com/blog/ForegroundColorStyleAndTintInSwiftUITue, 15 Apr 2025 18:00:00 +1200Ways to customize text color in SwiftUI

SwiftUI provides several ways to change the color of text, including the foregroundStyle(_:) modifier, the older (now deprecated) foregroundColor(_), AttributedString attributes, the tint(_:) modifier, and, for advanced use cases, the textRenderer(_:) modifier. While some of these can produce similar visual results, they differ in behavior, styling capabilities, and how they interact with system themes and view hierarchies. In this post, we’ll look at common use cases and consider which approach works best in each context.

# Foreground style

In earlier versions of SwiftUI, foregroundColor(_:) was the standard way to set a custom color for text in SwiftUI. It accepted a Color and applied it in the environment, affecting text and other foreground elements within the view hierarchy. This modifier is now deprecated, and the recommended approach is to use foregroundStyle(_:) instead.

Introduced in iOS 15, foregroundStyle(_:) has since become the preferred way to control foreground appearance. It works with any value that conforms to ShapeStyle, which makes it more versatile than foregroundColor(_:).

In its simplest form, foregroundStyle(_:) can be used to apply a solid color to all foreground elements within a view hierarchy:

VStack {
    Image(systemName: "globe")
    Text("Hello, world!")
}
.foregroundStyle(.cyan)
Globe symbol above the text 'Hello, world!' styled in cyan

Unlike foregroundColor(_:), the foregroundStyle(_:) modifier supports more advanced styling. For example, it allows applying gradients:

VStack {
    Image(systemName: "globe")
    Text("Hello, world!")
}
.foregroundStyle(
    LinearGradient(
        colors: [.mint, .blue],
        startPoint: .leading,
        endPoint: .trailing
    )
)
Globe symbol above the text 'Hello, world!' styled with a mint-to-blue gradient

The modifier also supports hierarchical styling. We can use levels like primary, secondary, tertiary, and quaternary to control how elements are rendered in the current foreground style:

VStack {
    Image(systemName: "globe")
    Text("Hello, world!")
        .foregroundStyle(.secondary)
}
.foregroundStyle(.green)

In this example, the base style is green. The image uses the default primary level, while the text uses secondary. Both share the same base style but differ in intensity.

Globe symbol above the text 'Hello, world!' styled in green using a secondary foreground style

Starting with iOS 17, foregroundStyle(_:) can be used to style individual fragments of text inside a larger string. When applied directly to a Text instance, the modifier returns a Text instead of some View, which allows us to use it inside text interpolation:

Text("Hello, \(Text("world").foregroundStyle(.mint))!")
Text reading 'Hello, world!' with the word 'world' styled in mint

# Foreground color attribute in an AttributedString

Another way to customize text color in SwiftUI is by using AttributedString. This API offers a modern, strictly typed approach to styling specific portions of text, and it works well when we want to apply styles dynamically based on the content.

For example, we can search for a substring, set its foreground color, and pass the resulting attributed string to a Text view:

struct MessageView: View {
    var attrString: AttributedString {
        var attrString = AttributedString("Don't miss the event today!")
        
        if let range = attrString.range(of: "today") {
            attrString[range].foregroundColor = .yellow
        }
        
        return attrString
    }
    
    var body: some View {
        Text(attrString)
    }
}
Text reading 'Don't miss the event today!' with the word 'today' styled in yellow

This gives us full control over which parts of the text are styled, and can be especially useful when the text content isn’t known ahead of time. However, the foreground color attribute only supports solid colors. We can’t use gradients or hierarchical styles like secondary when styling text via AttributedString.

# Tint

The tint(_:) modifier was introduced in iOS 16. Its purpose is to override the accent color for a view rather than to set the color of all foreground elements. When applied to a view hierarchy, tint(_:) affects the color of text inside controls such as buttons and links, but it doesn't change the color of plain Text views.

VStack {
    Text("Welcome!")
    Button("Log in") {
        // perform login
    }
}
.tint(.teal)
iPhone screen showing the text 'Welcome!' with a teal 'Log in' button below

The tint(_:) modifier can also be used to change the color of links embedded inside Text views, whether they're defined using Markdown or an AttributedString. In this case, the modifier only affects the portion of text with the link attribute and leaves the rest of the string unchanged.

Text("Visit our [website](https://example.com).")
    .tint(.cyan)
iPhone screen showing the text 'Visit our website' with the word 'website' styled as a cyan link

# Text renderer

If we need more advanced control over how text is colored, SwiftUI offers the textRenderer(_:) modifier, introduced in iOS 18. This modifier lets us replace the system’s default text rendering with a fully custom renderer that conforms to the TextRenderer protocol.

This is a lower-level API intended for advanced visual effects and animations, but it can also be used when we want to apply complex coloring that isn't possible or is difficult to achieve with modifiers like foregroundStyle(_:).

For example, we can use hue rotation to apply a varying color across glyphs:

struct HueRotationRenderer: TextRenderer {
    func draw(layout: Text.Layout, in context: inout GraphicsContext) {
        for line in layout {
            for run in line {
                let glyphCount = run.count
                for (index, glyph) in run.enumerated() {
                    var glyphContext = context
                    let hueAngle = (Double(index) / Double(glyphCount)) * 360.0
                    glyphContext.addFilter(.hueRotation(.degrees(hueAngle)))
                    glyphContext.draw(glyph)
                }
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .foregroundStyle(.blue)
            .textRenderer(HueRotationRenderer())
    }
}
Text reading 'Hello, world!' with hue rotation based on glyph index

This approach gives us full flexibility in how we draw each glyph. While it’s not necessary for most text styling needs, it opens the door to rich, animated, or highly customized effects when needed.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/SwiftGemsUpdateApril2025"Swift Gems" book update: new techniques from recent and past Swift releases"Swift Gems" by Natalia Panferova has been updated with tips from Swift 6, along with new techniques based on earlier Swift versions, covering optionals, noncopyable types, async programming, and more.https://nilcoalescing.com/blog/SwiftGemsUpdateApril2025Tue, 8 Apr 2025 18:00:00 +1200"Swift Gems" book update: new techniques from recent and past Swift releases

I’m excited to share that my book Swift Gems has just received a major update, bringing a fresh collection of practical tips and techniques for writing better Swift code. The new edition includes insights from the latest language advancements in Swift 6.0 and 6.1, as well as new examples and refinements based on features introduced in earlier versions of Swift.

This book is designed for experienced Swift developers looking to sharpen their skills and make the most of what the language has to offer. From working with optionals and generics to mastering async programming and improving collection performance, each tip is focused, actionable, and grounded in real-world usage.

The latest update introduces new techniques for working with noncopyable types, static dispatch, metatype key paths, and more. You’ll also find new content on using AsyncStream and TaskGroup, enhancing error handling with typed throws, and structuring your code for better runtime performance and clarity.

This update also refreshes existing tips with updated examples.

If you already own a copy of Swift Gems, you can now read the updated version online or download it from the book page. You can also view the release notes for a detailed list of what’s new and changed.

If you haven’t picked up a copy yet, this is a great time to dive in. The book continues to grow with new tips as Swift evolves, and all future updates are included with your purchase.

]]>
https://nilcoalescing.com/blog/TextConcatenationVsTextInterpolationInSwiftUIText concatenation vs Text interpolation in SwiftUICombining multiple SwiftUI Text views into a single text with the + operator to apply different styles can cause localization issues, making text interpolation the preferred technique for accurate translations.https://nilcoalescing.com/blog/TextConcatenationVsTextInterpolationInSwiftUITue, 1 Apr 2025 18:00:00 +1300Text concatenation vs Text interpolation in SwiftUI

SwiftUI allows us to combine multiple Text views into a single view using the plus (+) operator. This enables us to apply different styles to individual parts of the text.

Here's a simple example that colors the word "spicy" in red:

Text("Tortilla chips with ") +
Text("spicy 🌶️🌶️🌶️").foregroundStyle(.red) +
Text(" dip")
Text reading 'Tortilla chips with spicy 🌶️🌶️🌶️ dip', with 'spicy' styled in red

I often see developers using this technique, but it's important to understand its limitations, especially regarding localization.

When we concatenate text like in the example above, each segment will be extracted and translated separately. Here's how it might look in a String Catalog file for French:

Xcode String Catalog showing three separately extracted localization keys, each with a French translation

Firstly, some segments have trailing and preceding spaces, which can be quite difficult to spot. Translators must carefully preserve these spaces to ensure that when the translated segments are combined, words still have spaces between them. But there's also a larger issue: concatenation doesn't allow translators to reorder the segments for languages with different grammatical structures. For example, in French, adjectives usually follow the noun, unlike English. The correct translation would be "Chips tortilla avec sauce épicée 🌶️🌶️🌶️", but with simple concatenation, the app would incorrectly display "Chips tortilla avec épicée 🌶️🌶️🌶️ sauce", reflecting a direct translation from English that sounds unnatural.

French text rendering as 'Chips tortilla avec épicée 🌶️🌶️🌶️ sauce', with 'épicée' styled in red

To handle localization properly, we should use text interpolation rather than concatenation. Text interpolation lets us insert variables directly inside a single localizable string, even if these variables are styled Text views. This means we can apply text modifiers exactly like we do with concatenated segments.

Here's how the previous example looks using interpolation:

Text("Tortilla chips with \(Text("spicy 🌶️🌶️🌶️").foregroundStyle(.red)) dip")

Visually, this renders exactly the same in English:

Text reading 'Tortilla chips with spicy 🌶️🌶️🌶️ dip', with 'spicy' styled in red

However, the extracted localizable strings in the String Catalog will be very different. While the styled segment is still extracted separately, the main localized string includes a format specifier, allowing translators to reposition the interpolated segment freely within the sentence structure. Additionally, translators no longer need to manage leading and trailing spaces for segments manually.

Xcode String Catalog showing interpolated text with reordered segments in the French translation

Now, when the app runs in French, SwiftUI correctly inserts the styled adjective into the appropriate place:

French text rendering as 'Chips tortilla avec sauce épicée 🌶️🌶️🌶️', with 'épicée' styled in red

In summary, while text concatenation works well for simple styling scenarios, it's best practice to always prefer interpolation for localized text to ensure grammatically correct and natural translations. In real projects, it's also recommended to include comments, especially for strings with format specifiers, so translators understand what each placeholder represents.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/MethodDispatchMechanismsInSwiftMethod dispatch mechanisms in Swift: static and dynamic dispatchDive into how static and dynamic dispatch work in Swift, how they affect performance, and how to control method resolution to write faster, more efficient code.https://nilcoalescing.com/blog/MethodDispatchMechanismsInSwiftFri, 28 Mar 2025 18:00:00 +1300Method dispatch mechanisms in Swift: static and dynamic dispatch

Method dispatch refers to the process of determining which method implementation to execute when a method is called. In Swift, this can be either dynamic or static, each with distinct implications for performance and flexibility.

Dynamic dispatch is resolved at runtime based on the actual type of the object, enabling features like polymorphism but introducing runtime overhead due to method lookup. At a low level, this is implemented using virtual tables (V-Tables) for classes, which store pointers to method implementations. When a method is called through a base class reference, the V-Table of the actual instance type is used to determine the correct implementation at runtime. For protocols, Swift uses witness tables, which map protocol requirements to the implementations provided by conforming types. When a method is called through a protocol-typed value, the witness table for the underlying type is used to locate and invoke the appropriate implementation.

Static dispatch, on the other hand, is resolved at compile time based on the declared type of the variable. This allows the compiler to determine exactly which method to call before the program runs, avoiding the overhead of runtime lookup. At a low level, static dispatch, used by value types (structs and enums) and in non-overridable contexts like final classes, involves direct addressing: the compiler embeds the method’s memory address directly into the compiled code. Since there are no inheritance hierarchies in value types and no overriding in final classes, the call target is guaranteed to be fixed. This enables further optimizations such as inlining, where the method call is replaced with its body for improved performance.

# Dynamic dispatch: cases and examples

In Swift, only specific types of method calls use dynamic dispatch, such as those involving overridable class methods, protocol requirements, and Objective-C or virtual C++ methods. These scenarios leverage runtime resolution to provide the flexibility needed for dynamic behavior.

# Overridable class methods

Methods in classes not marked as final can be overridden by subclasses, enabling polymorphic behavior where different types respond uniquely to the same method call. When such a method is called through a base class reference, dynamic dispatch resolves the call at runtime based on the actual object type, using the virtual table of the instance to locate the correct implementation.

class Vehicle {
    func startEngine() {
        print("Vehicle engine initiated")
    }
}

class Car: Vehicle {
    override func startEngine() {
        print("Car engine initiated")
    }
}

class Truck: Vehicle {
    override func startEngine() {
        print("Truck engine initiated")
    }
}

let vehicles: [Vehicle] = [Car(), Truck()]

for vehicle in vehicles {
    // Dynamic dispatch via V-Table at runtime
    vehicle.startEngine()
}

In this example, the vehicles array contains a Car and a Truck, both referenced as Vehicle. Iterating over the array and calling startEngine() on each triggers dynamic dispatch: the V-Table for each instance is consulted at runtime, ensuring Car’s startEngine() prints "Car engine initiated" and Truck’s prints "Truck engine initiated," despite the reference type being Vehicle.

# Protocol requirements

Methods declared in a protocol’s main body are requirements that must be implemented by any conforming type, enabling polymorphic behavior across different implementations. When called through a value of the protocol type, these methods use dynamic dispatch, relying on a witness table to map each requirement to the correct implementation. This runtime resolution is necessary because the concrete type is not known until runtime when the protocol type is used as an abstraction.

protocol Drivable {
    func drive()
}

struct Truck: Drivable {
    func drive() {
        print("Truck drive initiated")
    }
}

struct Car: Drivable {
    func drive() {
        print("Car drive initiated")
    }
}

let drivables: [Drivable] = [Truck(), Car()]

for drivable in drivables {
    // Dynamic dispatch via witness table
    drivable.drive()
}

In this scenario, each call to drivable.drive() uses dynamic dispatch to invoke the correct implementation based on the underlying type, either Truck or Car. At runtime, Swift consults the witness table for each instance to resolve the method, ensuring the appropriate implementation is executed for the actual type.

# Objective-C or virtual C++ methods

When interacting with methods from Objective-C or C++ that inherently use dynamic dispatch, such as Objective-C message passing or virtual C++ methods, Swift adopts dynamic dispatch to handle these calls appropriately, ensuring compatibility with these languages’ runtime systems.

@objc protocol LegacyVehicleProtocol {
    func logMaintenance()
}
class LegacyVehicleSystem: NSObject, LegacyVehicleProtocol {
    @objc func logMaintenance() {
        print("Logging maintenance for vehicle in legacy system")
    }
}
let legacySystem: LegacyVehicleProtocol = LegacyVehicleSystem()

// Dynamic dispatch via Objective-C message passing
legacySystem.logMaintenance()

In this case, legacySystem.logMaintenance() is dispatched dynamically using Objective-C’s message passing system, which resolves the method at runtime.

# Static dispatch: cases and examples

Static method dispatch in Swift is used for all cases not listed in the dynamic dispatch section, for example with value types, final classes, private methods, and non-requirement methods in protocol extensions. These scenarios leverage compile-time resolution to ensure efficient execution without runtime overhead.

# Methods on value types

Since value types like structs and enums cannot be subclassed, their methods are always statically dispatched based on their known types at compile-time, eliminating the need for runtime lookups due to the absence of inheritance hierarchies.

struct VehicleRecord {
    let id: Int
    let mileage: Double
    
    func calculateFuelEfficiency() -> Double {
        // Simplified calculation
        return mileage / 30.0
    }
}

let record = VehicleRecord(id: 101, mileage: 300.0)

// Static dispatch - method resolved at compile time
print(record.calculateFuelEfficiency())

Here, record.calculateFuelEfficiency() employs static dispatch to call VehicleRecord’s implementation. Since VehicleRecord is a struct, its type is known at compile-time, allowing the compiler to directly embed the method’s address into the compiled code for efficient execution.

# Final classes and methods

Marking a class or method as final prevents overriding, ensuring that method calls are statically dispatched since the compiler can guarantee no subclass will provide a different implementation, eliminating the need for runtime resolution.

final class ElectricCar {
    let batteryLevel: Double
    
    init(batteryLevel: Double) {
        self.batteryLevel = batteryLevel
    }
    
    func checkBattery() {
        print("Battery level is \(batteryLevel)%")
    }
}

let electricCar = ElectricCar(batteryLevel: 85.0)

// Static dispatch - final class prevents overriding
electricCar.checkBattery()

With this setup, electricCar.checkBattery() uses static dispatch to call ElectricCar’s implementation. Since ElectricCar is marked as final, the compiler knows no subclass can override checkBattery(), allowing it to resolve the call at compile-time and embed the method’s address directly into the compiled code.

# Private class declarations

Applying the private or fileprivate keywords to a declaration restricts its visibility to the scope or file in which it is defined. This allows the compiler to determine all other potentially overriding declarations. In the absence of such declarations within the file, the compiler can treat method calls as non-overridable and eliminate indirect dispatch.

private class VehicleManager {
    private var vehicleCount = 0
    
    func addVehicle() {
        vehicleCount += 1
    }
}

private let manager = VehicleManager()

// Static dispatch - no visible subclass in file
manager.addVehicle()

Since VehicleManager is a private class and no subclass is declared in the same file, the compiler can guarantee that addVehicle() is not overridden. As a result, the call to addVehicle() can be devirtualized and replaced with a direct call, avoiding the overhead of dynamic dispatch.

# Protocol extension methods (non-requirements)

Methods defined only in protocol extensions and not declared as requirements are always resolved using static dispatch. Even if a conforming type provides its own implementation, calling these methods through a value of the protocol type will consistently use the version from the extension, as they do not participate in dynamic dispatch through the witness table.

protocol Vehicle {
    func startEngine()
}

extension Vehicle {
    func stopEngine() {
        print("Vehicle engine stopped")
    }
}

struct Sedan: Vehicle {
    func startEngine() {
        print("Sedan engine started")
    }
    
    func stopEngine() {
        print("Sedan engine stopped")
    }
}

struct Van: Vehicle {
    func startEngine() {
        print("Van engine started")
    }
}

let vehicles: [Vehicle] = [Sedan(), Van()]

for vehicle in vehicles {
    // Dynamic dispatch via witness table
    vehicle.startEngine()
    
    // Static dispatch to protocol extension
    vehicle.stopEngine()
}

Here, the vehicles array contains a Sedan and a Van, both conforming to Vehicle. Iterating over the array, each vehicle.startEngine() call uses dynamic dispatch to invoke the type’s specific implementation, while each vehicle.stopEngine() call uses static dispatch to the extension’s implementation, printing "Vehicle engine stopped" in both cases, even though Sedan defines its own version.

# Performance considerations

The performance differences between static and dynamic dispatch aren't always obvious, but understanding how these mechanisms work helps us make targeted optimizations when needed. To encourage static dispatch and reduce runtime overhead, we can mark classes as final when inheritance isn’t required. This allows the compiler to safely resolve method calls at compile time without relying on V-Table lookups. Similarly, declaring a class as private or fileprivate limits its visibility to the current file, giving the compiler full knowledge of any subclassing or overriding that may occur. If no subclass is present in the file, the compiler can treat method calls as non-overridable and emit direct calls, avoiding dynamic dispatch.

Enabling Whole Module Optimization (WMO) may improve performance further. By analyzing the entire module as a unit, the compiler can replace dynamic dispatch with static calls for internal declarations that aren't overridden elsewhere, eliminating unnecessary indirection.

When working with protocols, we can reduce dynamic dispatch by limiting method requirements to those essential for conformance-based polymorphism. Methods declared in the protocol body become requirements and are dispatched via witness tables at runtime, enabling dynamic behavior across conforming types. In contrast, methods defined only in protocol extensions are not included in the witness table and are resolved through static dispatch at compile-time, resulting in more efficient calls.

In performance-sensitive code, it also helps to avoid unnecessary Objective-C interoperability, as its message-passing system is slower than Swift’s native dispatch model.

Finally, profiling with Instruments can reveal dispatch-related overhead, helping us focus our optimization efforts where they have the most impact.


If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/AdaptingImagesAndSymbolsToDynamicTypeSizesInSwiftUIAdapting images and symbols to Dynamic Type sizes in SwiftUIMake SF Symbols and custom images adapt to different font sizes while maintaining layout balance, keeping key icons clear, and providing more space for higher-priority content when needed.https://nilcoalescing.com/blog/AdaptingImagesAndSymbolsToDynamicTypeSizesInSwiftUIFri, 21 Mar 2025 18:00:00 +1300Adapting images and symbols to Dynamic Type sizes in SwiftUI

Dynamic Type allows users to adjust text size across the system, improving readability and accessibility. When we use built-in fonts in SwiftUI, text scales automatically to match the user’s preferred size. However, text is only part of the interface. Other elements, such as images and symbols, also need to adapt to maintain a balanced layout.

In this post, we'll look at how to handle images in a Dynamic Type environment, covering both SF Symbols and custom images. We'll explore techniques to ensure meaningful icons remain clear at larger text sizes, scale custom images appropriately, and exclude decorative images when necessary to optimize space.

# Using SF Symbols with Dynamic Type

If we use icons to communicate important information, they need to stay clear and legible at larger font sizes. SF Symbols make this easy by scaling automatically with Dynamic Type, ensuring they adjust as text size changes.

These symbols integrate with the San Francisco system font, aligning naturally with different weights and sizes. They work well for conveying concepts or representing objects, especially alongside text.

Even though SF Symbols are placed inside an Image view, they behave more like text. They inherit font attributes from the environment to match surrounding content.

VStack {
    Image(systemName: "cloud.sun")
    Text("Partly Cloudy")
}
.font(.title)
Comparison of a weather icon and text at Large (Default) and XXX Large Dynamic Type sizes, showing how both elements scale proportionally

An important detail to keep in mind is that it's generally not recommended to use the resizable() modifier with SF Symbols. Applying resizable() to a symbol causes it to lose its symbol properties, meaning it will be treated more like a regular image. This prevents it from scaling automatically with Dynamic Type.

VStack {
    Image(systemName: "cloud.sun")
        .resizable()
        .scaledToFit()
        .frame(height: 28)
    Text("Partly Cloudy")
}
.font(.title)
Comparison of a weather icon and text at Large (Default) and XXX Large Dynamic Type sizes, showing that the text scales up while the icon remains the same size

There may be cases where converting a symbol into an image and manually scaling it, as we would with custom images, is appropriate. However, in most situations, it's best to use SF Symbols as intended to ensure they remain consistent with surrounding text and provide the best user experience.

# Scaling custom images

Custom images from an asset catalog don't automatically resize with text. If an image is not purely decorative but conveys meaning, we should manually ensure it scales appropriately when the user adjusts their preferred font size.

SwiftUI provides the ScaledMetric API to make this easier to handle. Instead of defining multiple sizes for an image, we can specify a base value and store it in a property annotated with the @ScaledMetric property wrapper. By default, ScaledMetric adjusts relative to the body text style, but we can customize it to match a different text style.

In the example below, the image height will scale relative to the title font size.

struct ContentView: View {
    @ScaledMetric(relativeTo: .title)
    private var imageHeight = 28
    
    var body: some View {
        VStack {
            Image(.partlyCloudy)
                .resizable()
                .scaledToFit()
                .frame(height: imageHeight)
            Text("Partly Cloudy")
        }
        .font(.title)
    }
}
Comparison of a custom weather icon and text at Large (Default) and XXX Large Dynamic Type sizes, showing that both the image and text scale proportionally

When designing with scaled metrics, it's important to start with a size that works well for the default Dynamic Type setting and ensure that images have a high enough resolution to scale up without losing quality.

# Handling decorative images

Decorative images that don't convey important meaning shouldn't be scaled, as they can take up valuable screen space that could be used for more important content. In some cases, it may even be best to remove decorative images altogether at larger text sizes.

We can determine the current Dynamic Type size by reading the dynamicTypeSize environment value. In the example below, the image remains visible for standard text sizes but is removed entirely when an accessibility text size is enabled, ensuring that text has as much space as possible.

struct ContentView: View {
    @Environment(\.dynamicTypeSize)
    private var typeSize
    
    var body: some View {
        VStack {
            if !typeSize.isAccessibilitySize {
                Image(decorative: "forecast")
            }
            Text("Weather Forecast")
        }
        .font(.title)
    }
}
Weather icon visible at Large text size and removed at Accessibility 1

Adapting images and symbols to Dynamic Type ensures that interfaces remain clear, accessible, and visually balanced across different text sizes. By making thoughtful adjustments to how images scale, or whether they appear at all, we can create layouts that adapt seamlessly to user preferences while maintaining clarity and usability.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/CustomEnvironmentValuesInSwiftUICustom environment values in SwiftUILearn how to define custom environment values in SwiftUI, eliminate boilerplate with the @Entry macro in Xcode 16, and pass data through the view hierarchy efficiently.https://nilcoalescing.com/blog/CustomEnvironmentValuesInSwiftUIThu, 13 Mar 2025 18:00:00 +1300Custom environment values in SwiftUI

SwiftUI environment provides a way to pass values through the view hierarchy without manually forwarding them. While SwiftUI includes built-in environment values for system settings, we can also define custom environment values to share app-specific data. This allows us to store and propagate values such as user preferences, feature flags, or custom styling configurations in a structured and reusable way.

To use a custom environment value, we need to define it, provide a default value, and inject it into the environment so that any child view can access it.

# Defining custom environment values before Xcode 16

Before Xcode 16, defining a custom environment value required writing a fair amount of boilerplate. We had to create a key conforming to EnvironmentKey, provide a default value, and extend EnvironmentValues to add a computed property for accessing and modifying the value.

For example, to define a custom environment value for storing a user's preferred color, we had to write the following code:

private struct FavoriteColorKey: EnvironmentKey {
    static let defaultValue: Color = .blue
}

extension EnvironmentValues {
    var favoriteColor: Color {
        get { self[FavoriteColorKey.self] }
        set { self[FavoriteColorKey.self] = newValue }
    }
}

This setup allowed us to store and retrieve the value through the environment, but it required multiple declarations for a single property.

# Defining custom environment values with the @Entry macro

With the introduction of the @Entry macro in Xcode 16, manually defining environment keys is no longer necessary. Now, we can declare an environment value directly inside the EnvironmentValues struct and assign it a default value at the same time.

extension EnvironmentValues {
    @Entry var favoriteColor: Color = .blue
}

The @Entry macro automatically generates the necessary environment key and computed property, eliminating the need for additional boilerplate. If you're curious, you can expand the macro by right-clicking on @Entry in Xcode and selecting "Expand Macro" to view the generated code.

Even though the @Entry macro is new in Xcode 16, it works with older iOS and macOS versions as long as the app is built with Xcode 16. This means we can use it while supporting iOS 13 and later, as well as macOS 10.15 and later. There’s no need to drop compatibility with older systems to take advantage of this improved syntax.

# Setting and reading custom environment values

To pass a value into the environment, we can use the environment(_:_:) modifier, which sets the environment value for a specific key path.

For built-in values, SwiftUI provides dedicated view modifiers that encapsulate this logic, and the same approach is recommended when defining custom values. To achieve this, we can extend View and create a method that writes the value to the environment.

extension View {
    func favoriteColor(_ color: Color) -> some View {
        environment(\.favoriteColor, color)
    }
}

By defining a dedicated modifier, we make setting the value more intuitive and consistent with SwiftUI’s built-in environment APIs.

In the following example, we let the user select their favorite color using a ColorPicker. The selected color is then stored in a @State property and injected into the environment using our custom favoriteColor(_:) modifier, making it available to all child views.

struct ContentView: View {
    @State private var favoriteColor: Color = .blue
    
    var body: some View {
        NavigationStack {
            TodoList()
                .toolbar {
                    ColorPicker(
                        "Favorite color",
                        selection: $favoriteColor
                    )
                    .labelsHidden()
                }
        }
        .favoriteColor(favoriteColor)
    }
}
Color picker interface on an iPhone showing a grid of colors with a selected pink shade

Once the value is set in the environment, any child view, no matter how deeply nested, can read it using the @Environment property wrapper.

For example, suppose we have a TaskCheckmark view that displays a checkmark icon next to a completed task in the TodoList. Instead of passing the color manually from ContentView through TodoList and its subviews, TaskCheckmark can directly retrieve favoriteColor from the environment and apply it to the checkmark’s foreground style.

struct TaskCheckmark: View {
    @Environment(\.favoriteColor)
    private var favoriteColor
    
    var body: some View {
        Image(systemName: "checkmark")
            .foregroundStyle(favoriteColor)
    }
}
A to-do list showing three tasks, two of which are marked as completed with pink checkmarks

Because SwiftUI’s environment propagates changes, any updates to favoriteColor in ContentView will instantly reflect in all views that use it. If the user selects a new color, the checkmark (and any other dependent views) will update automatically without requiring any additional code.

You can find the sample code for this post on GitHub.

If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/ModalPresentationBackgroundAndColorSchemeInSwiftUICustomizing modal presentation background and color scheme in SwiftUISet a custom background, like an image or a translucent material, for SwiftUI sheets, popovers, and full-screen covers, and explicitly control the presentation color scheme.https://nilcoalescing.com/blog/ModalPresentationBackgroundAndColorSchemeInSwiftUIFri, 7 Mar 2025 19:00:00 +1300Customizing modal presentation background and color scheme in SwiftUI

SwiftUI provides built-in modifiers to customize the background appearance of modal presentations. These modifiers allow us to apply a solid color, a gradient, a translucent material, or even a custom view as the background of sheets, popovers, and full-screen covers.

# Setting a background style

The presentationBackground(_:) modifier lets us define a background for a modal presentation using any ShapeStyle. This includes colors, gradients, and materials.

For example, we can make a sheet’s background translucent by applying a thin material:

.sheet(isPresented: $showSheet) {
    Text("Sheet content...")
        .presentationBackground(.thinMaterial)
}

This gives the sheet a frosted glass effect, allowing the content behind it to subtly show through, as seen in the screenshot below.

SwiftUI sheet with a translucent background, allowing a cyan color to show through behind it

# Using a custom background view

For more flexibility, SwiftUI provides the presentationBackground(alignment:content:) modifier, which allows us to set a custom SwiftUI view as the background. This is particularly useful when we want to use an image or a complex visual design as the modal background.

.sheet(isPresented: $showSheet) {
    Text("Sheet content...")
        .presentationBackground {
            Image(.clouds)
                .resizable()
                .scaledToFill()
        }
}
SwiftUI sheet with a cloud image as its background

These background customization options work consistently across all modal presentation types. However, one interesting detail is that translucency appears more pronounced in sheets and full-screen covers than in popovers.

# Removing default backgrounds from container views

Some SwiftUI container views, such as List and Form, come with a default opaque background. When placed inside a modal, they obscure the presentation background, preventing customizations from taking effect. To make the presentation background visible, we need to disable the default styling of these container views.

Consider the following example, where a List is placed inside a sheet with a material background applied:

.sheet(isPresented: $showSheet) {
    List(1...10, id: \.self) {
        Text("Item \($0)")
    }
    .presentationBackground(.thinMaterial)
}

Even though we have applied a translucent material, the sheet appears fully opaque.

SwiftUI sheet with a dark opaque background containing a list of items

To allow the presentation background to show through, we first need to disable the List view's background using the scrollContentBackground(_:) modifier.

.sheet(isPresented: $showSheet) {
    List(1...10, id: \.self) {
        Text("Item \($0)")
    }
    .scrollContentBackground(.hidden)
    .presentationBackground(.thinMaterial)
}

This removes the opaque background from the empty areas of the list, making them transparent. However, the default row backgrounds remain visible.

SwiftUI sheet with a translucent blue background and opaque list rows

To ensure a fully translucent appearance, we also need to remove the background from list rows by setting it to a clear color.

.sheet(isPresented: $showSheet) {
    List(1...10, id: \.self) {
        Text("Item \($0)")
            .listRowBackground(Color.clear)
    }
    .scrollContentBackground(.hidden)
    .presentationBackground(.thinMaterial)
}

Now, both the list’s empty areas and its rows become translucent, creating a consistent material effect throughout the modal.

SwiftUI sheet with a fully translucent blue background and a list of items without row backgrounds

# Controlling the color scheme

In addition to customizing the background, we can also specify a preferred color scheme for modal presentations. This ensures that the text, controls, and other UI elements remain legible regardless of the system-wide appearance settings.

Setting a color scheme can be particularly useful when using a custom background. For example, if our modal uses a dark image, we may want the text and buttons to appear in a light color for contrast.

To enforce a dark appearance inside a full-screen cover, we can apply the preferredColorScheme(_:) modifier:

.fullScreenCover(isPresented: $showCover) {
    PresentationContent()
        .preferredColorScheme(.dark)
}

With this in place, even if the rest of the app adapts dynamically to light and dark mode, the full-screen cover will always be displayed in a dark color scheme with light text elements, ensuring proper contrast between foreground and background.

SwiftUI full-screen cover with a dark starry sky background and light-colored text

The preferred color scheme value flows up the view hierarchy and is intercepted at the nearest presentation. However, this modifier can also be used outside of modal content, in which case it will affect the entire application.

For a more detailed discussion on working with color schemes in SwiftUI, including how to detect the current mode and respond to system changes, you can read my post: Reading and setting color scheme in SwiftUI.

If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/CustomLazyListInSwiftUIDesigning a custom lazy list in SwiftUI with better performanceImplement a high-performance lazy scrolling list in SwiftUI by efficiently reusing views for smooth scrolling with large datasets.https://nilcoalescing.com/blog/CustomLazyListInSwiftUIMon, 3 Mar 2025 18:00:00 +1300Designing a custom lazy list in SwiftUI with better performance

Mac apps often need to handle large datasets efficiently, but SwiftUI’s standard List can struggle with performance on macOS as the number of items grows. Scrolling may become sluggish, and memory usage can increase significantly.

For example, an app that enumerates files in a folder can easily generate a list with over 10,000 rows. While List is the obvious choice, its performance degrades at scale. A common alternative is wrapping a LazyHStack in a ScrollView, but this approach also struggles with large datasets.

So what’s the solution? We can build a custom layout that aggressively recycles rows, repositioning them just in time as the user scrolls while reusing the same view identity as a row fragment. This works particularly well with a fixed row height, which is common in macOS applications, since it allows us to determine visible rows based on the scroll offset. While this solution was designed for macOS, the same technique can also be applied to iOS.

The first step is to create a view that calculates the visible range of rows within the ScrollView. This allows us to determine which rows should be displayed at any given time.

struct RecyclingScrollingLazyView: View {
    let numberOfRows: Int
    let rowHeight: CGFloat
    @State var visibleRange: Range<Int> = 0..<1
    
    var body: some View {
        ScrollView(.vertical) {
            Color.red.frame(
                height: rowHeight * CGFloat(numberOfRows)
            )
        }
        .onScrollGeometryChange(
            for: Range<Int>.self,
            of: { geo in
                self.computeVisibleRange(in: geo.visibleRect)
            },
            action: { oldValue, newValue in
                self.visibleRange = newValue
            }
        )
    }
    
    func computeVisibleRange(in rect: CGRect) -> Range<Int> {
        let lowerBound = Int(
            max(0, floor(rect.minY / rowHeight))
        )
        let rowsThatFitInRange = Int(
            ceil(rect.height / rowHeight)
        )
        
        let upperBound = max(
            lowerBound + rowsThatFitInRange, 
            lowerBound + 1
        )
        
        return lowerBound..<upperBound
    }
}

Here, we create a vertical ScrollView and use the onScrollGeometryChange() modifier. This modifier is powerful because it calls the transform method every time the scroll offset updates but only triggers the action block if the result of the transform changes.

Next, we need to create the rows. Our RecyclingScrollingLazyView will take a list of row IDs and a ViewBuilder closure to create each row given its ID.

struct RecyclingScrollingLazyView<
    ID: Hashable, Content: View
>: View {
    let rowIDs: [ID]
    let rowHeight: CGFloat
    
    @ViewBuilder
    var content: (ID) -> Content
    
    var numberOfRows: Int { rowIDs.count }
    
    ...
}

The updated view is generic in both the row ID and the row content.

We can now use it as follows:

RecyclingScrollingLazyView(
    rowIDs: [1, 2, 3], rowHeight: 42
) { id in
    HStack {
        Text("Row \(id)")
        Spacer()
        Text("Some other row content")
    }
}

So far, our view only renders a large red rectangle without displaying any rows. To fix this, we need to map the visible range of rows to actual row content.

Since we want SwiftUI to reuse views efficiently, we’ll introduce a reusable identifier for each row, which we'll call a fragment. To achieve this, we'll define a nested struct called RowData inside our view. This struct will help manage row identity and ensure proper recycling of views as the user scrolls.

struct RecyclingScrollingLazyView<
    ID: Hashable, Content: View
>: View {
    ... 
    
    struct RowData: Identifiable {
        let fragmentID: Int
        let index: Int
        let value: ID
        
        var id: Int { fragmentID }
    }
    
    ...
}

Next, we’ll define a computed property that returns the visible range of rows as RowData. This ensures that only the necessary rows are displayed while efficiently reusing fragments.

struct RecyclingScrollingLazyView<
    ID: Hashable, Content: View
>: View {
    ... 

    var visibleRows: [RowData] {
        if rowIDs.isEmpty { return [] }
        
        let lowerBound = min(
            max(0, visibleRange.lowerBound),
            rowIDs.count - 1
        )
        let upperBound = max(
            min(rowIDs.count, visibleRange.upperBound),
            lowerBound + 1
        )
        
        let range = lowerBound..<upperBound
        let rowSlice = rowIDs[lowerBound..<upperBound]
        
        let rowData = zip(rowSlice, range).map { row in
            RowData(
                fragmentID: row.1 % range.count,
                index: row.1, value: row.0
            )
        }
        return rowData
    }
}

We determine the visible rows by slicing rowIDs based on the computed visible range. To maximize reuse, we assign each row a fragment ID, calculated as the row index modulo the total number of visible rows.

We can now replace the large empty rectangle with actual rows inside the scroll view.

struct RecyclingScrollingLazyView<
    ID: Hashable, Content: View
>: View {
    ...
    
    var body: some View {
        ScrollView(.vertical) {
            ForEach(visibleRows) { row in
                content(row.value)
            }
        }
        .onScrollGeometryChange(
            ...
        )
    }
}

The key to this approach is ensuring that our nested struct conforms to Identifiable and uses the fragment ID as the row identifier. This allows SwiftUI to reuse rows as we scroll, rather than creating new leaf nodes in the SwiftUI view hierarchy. By maintaining consistent view identities, we ensure that off-screen rows are efficiently recycled instead of being recreated.

However, if we run this now, we'll notice that only a small number of rows appear, always positioned at the top of the ScrollView. This happens because we're not explicitly positioning the rows or defining the total height of the content.

To fix this, we need a custom layout that places each row at the correct vertical position while ensuring the total height matches the full dataset.

We'll define an OffsetLayout struct that conforms to the Layout protocol, allowing precise control over row positioning.

struct OffsetLayout: Layout {
    let totalRowCount: Int
    let rowHeight: CGFloat
    
    func sizeThatFits(
        proposal: ProposedViewSize, 
        subviews: Subviews, 
        cache: inout ()
    ) -> CGSize {
        CGSize(
            width: proposal.width ?? 0,
            height: rowHeight * CGFloat(totalRowCount)
        )
    }
    
    func placeSubviews(
        in bounds: CGRect,
        proposal: ProposedViewSize, 
        subviews: Subviews, 
        cache: inout ()
    ) {
        for subview in subviews {
            let index = subview[LayoutIndex.self]
            subview.place(
                at: CGPoint(
                    x: bounds.midX,
                    y: bounds.minY + rowHeight * CGFloat(index)
                ),
                anchor: .top,
                proposal: .init(
                    width: proposal.width, height: rowHeight
                )
            )
        }
    }
}

struct LayoutIndex: LayoutValueKey {
    static var defaultValue: Int = 0
    
    typealias Value = Int
}

The sizeThatFits() method calculates the total height by multiplying the number of rows by the height of each row. This ensures the scrollable area reflects the full dataset.

In placeSubviews(), each visible row is positioned at its correct vertical offset. To determine its position, we need to know its index within the full list. We achieve this by defining a LayoutValueKey, which allows us to pass the index to the layout.

struct RecyclingScrollingLazyView<
    ID: Hashable, Content: View
>: View {
    ...
    
    var body: some View {
        ScrollView(.vertical) {
            OffsetLayout(
                totalRowCount: rowIDs.count,
                rowHeight: rowHeight
            ) {
                // The fragment ID is used instead of the row ID
                ForEach(visibleRows) { row in
                    content(row.value)
                        .layoutValue(
                            key: LayoutIndex.self, value: row.index
                        )
                }
            }
        }
        .onScrollGeometryChange(
            ...
        )
    }
}

Using the custom OffsetLayout, we can position each visible row precisely in the scroll view according to its index. The LayoutIndex value is set on each row and read back within our custom layout.

Since our approach reuses a limited number of rows, local state can persist when a row is recycled. For example, storing user input in a @State property may cause it to appear in a different row after scrolling.

To avoid this issue, we need to reset or clear any local state whenever the row ID changes.

struct MyRow: View {
    let id: Int
    @State private var text: String = ""
    
    var body: some View {
        TextField("Enter something", text: $text)
            .onChange(of: id) { newID in
                self.text = ""
            }
    }
}
RecyclingScrollingLazyView(
    rowIDs: [1, 2, 3], rowHeight: 42
) { id in
    MyRow(id: id)
}

Our custom approach is faster because we reuse a limited set of view identities instead of creating a new view for every row in a large list. We achieve this by calculating a fragment ID, which is the row’s index modulo the maximum number of visible rows. This allows SwiftUI to recycle view identities as rows move off-screen, rather than instantiating new views each time. By reusing existing views, we significantly improve performance and reduce memory usage.

In contrast, built-in components like List and LazyHStack cannot reuse views as aggressively. They assign each row a unique identity (provided in the ForEach statement) to properly maintain per-row state. As a result, SwiftUI’s view graph must create a separate leaf for each row and compute its height individually—an expensive process as the number of rows grows.

The performance difference becomes even more pronounced when using AppKit-backed controls such as text views, sliders, or buttons. By reusing view identities, previously instantiated AppKit views remain attached and are efficiently recycled, much like how a native NSTableView optimizes performance at scale.

If you're looking to gain a deeper understanding of SwiftUI state management, view identity, layout system, and rendering internals, make sure to check out SwiftUI Fundamentals by Natalia Panferova. It explores these core concepts in depth, helping you write more efficient and scalable SwiftUI apps.

]]>
https://nilcoalescing.com/blog/DetectFocusedWindowOnMacOSDetecting the focused window on macOS in SwiftUIDetect window focus with the appearsActive environment value to adjust UI and handle focus changes.https://nilcoalescing.com/blog/DetectFocusedWindowOnMacOSSat, 1 Mar 2025 18:00:00 +1300Detecting the focused window on macOS in SwiftUI

Previously, I relied on the controlActiveState environment value to check whether a window was focused in SwiftUI on macOS. However, this API has been deprecated since macOS 15.

The replacement is the appearsActive environment value, which is true when the window is focused and false when it's not.

struct ContentView: View {
    @Environment(\.appearsActive) var appearsActive
    
    var body: some View {
        Text("Window is focused: \(appearsActive)")
            .onChange(of: appearsActive) { oldValue, newValue in
                let oldStatus = oldValue ? "active" : "inactive"
                let newStatus = newValue ? "active" : "inactive"
                print("Window focus changed from \(oldStatus) to \(newStatus).")
            }
    }
}

This value is useful for adjusting the appearance of controls or other UI elements when the window becomes inactive. We may also want to trigger actions when focus changes.

I used appearsActive in a settings window to update the state of a control. You can find an example in my previous post: Add launch at login setting to a macOS app.

]]>
https://nilcoalescing.com/blog/SwiftUIFundamentalsReleaseAnnouncementSwiftUI Fundamentals: a deeper look into the framework"SwiftUI Fundamentals" is a new book by Natalia Panferova that explores SwiftUI’s core principles and APIs in depth. It will help you build a solid foundation for using SwiftUI effectively in your projects.https://nilcoalescing.com/blog/SwiftUIFundamentalsReleaseAnnouncementTue, 18 Feb 2025 18:00:00 +1300SwiftUI Fundamentals: a deeper look into the framework

I'm excited to announce the release of my new book – SwiftUI Fundamentals. This book takes a deep dive into the core principles and APIs that power SwiftUI, the modern UI framework for building apps across Apple platforms.

Unlike step-by-step tutorials, "SwiftUI Fundamentals" focuses on helping you go beyond surface-level knowledge. The book provides a solid foundation in how SwiftUI works under the hood, equipping you with the insights you need to write more efficient, maintainable, and expressive code.

You'll learn how SwiftUI handles view updates, data flow, and layout calculations. We'll look at how views negotiate size and position, how navigation is driven by state and data, and how animations and gestures are built into the system. The book also covers the most important APIs and patterns that developers encounter when building apps for iOS, macOS and other platforms.

Having contributed to SwiftUI's development as part of the team at Apple, I wrote this book to share what I’ve learned along the way. My goal is to help developers deepen their understanding of the framework, so they can confidently solve real-world problems without guesswork.

If you've used SwiftUI before and want to strengthen your understanding of how it really works, "SwiftUI Fundamentals" is for you.

Get your copy today and take your SwiftUI knowledge to the next level.

I hope you find it helpful as you continue building with SwiftUI.

]]>
https://nilcoalescing.com/blog/BuildAMacOSMenuBarUtilityInSwiftUIBuild a macOS menu bar utility in SwiftUILearn how to build a macOS menu bar app in SwiftUI using MenuBarExtra, customize its icon, hide it from the Dock, and add essential features like a quit option.https://nilcoalescing.com/blog/BuildAMacOSMenuBarUtilityInSwiftUIThu, 13 Feb 2025 18:00:00 +1300Build a macOS menu bar utility in SwiftUI

SwiftUI’s MenuBarExtra scene provides a simple way to integrate menu bar functionality into macOS apps. It can complement a traditional app by offering quick access to frequently used features or serve as the foundation for a standalone menu bar utility.

In this post, I’ll walk through the process of building a menu bar–only app using MenuBarExtra. I recently developed EncodeDecode, a small tool for encoding and decoding strings with percent encoding (URL encoding), and I’ll share what I learned along the way.

# Create a menu bar app

Once we have created a macOS app project in Xcode, we can modify the app declaration by replacing the default WindowGroup with a MenuBarExtra scene. This scene can be initialized with just a title or both a title and an image. If an image is provided, only the image will be visible in the menu bar, otherwise, the title text will be displayed.

@main
struct MenuBarExampleApp: App {
    var body: some Scene {
        MenuBarExtra(
            "Menu Bar Example",
            systemImage: "characters.uppercase"
        ) {
            ContentView()
                .frame(width: 300, height: 180)
        }
        .menuBarExtraStyle(.window)
    }
}

By default, MenuBarExtra uses the menu style, which behaves like a standard dropdown menu. However, for a standalone app, we may want to use the window style instead, as it allows for greater flexibility in presenting content. The window can either resize dynamically based on its content or have a fixed frame set on the root view.

The ContentView serves as the root view of the utility, defining the app's interface within the window. For this example, we can create a simple app that allows users to paste a string, convert it to uppercase, and copy the result to the clipboard.

Here’s an example of how ContentView might look:

struct ContentView: View {
    @State private var textInput: String = ""
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Add your text below:")
                .foregroundStyle(.secondary)
            TextEditor(text: $textInput)
                .padding(.vertical, 4)
                .scrollContentBackground(.hidden)
                .background(.thinMaterial)
            Button(
                "Copy uppercased result",
                systemImage: "square.on.square"
            ) {
                let pasteboard = NSPasteboard.general
                pasteboard.clearContents()
                pasteboard.setString(
                    textInput.uppercased(),
                    forType: .string
                )
            }
            .buttonStyle(.plain)
            .foregroundStyle(.blue)
            .bold()
        }
        .padding()
    }
}
Menu bar app with an 'ABC' icon, displaying a floating window with a text input field and a 'Copy uppercased result' button

# Hide the app from the Dock and application switcher

For apps that only appear in the menu bar, it's common to hide them from both the Dock and the application switcher. This can be achieved by setting the LSUIElement flag to true in the Info.plist file. This tells macOS that the app is an agent that runs in the background.

Xcode Info tab showing the Application is agent (UIElement) setting set to YES to hide the app from the Dock

# Providing a way to quit the app

After removing the app from the Dock and the app switcher, it’s important to provide a way for users to quit it, as they can no longer do so from the Dock. Unfortunately, SwiftUI doesn’t offer a built-in way to add a right-click menu to a menu bar item, so the simplest approach is to include a Quit button somewhere in the app’s interface.

For this example, we can place a small close button in the top-right corner of the window:

MenuBarExtra(
    "Menu Bar Example",
    systemImage: "characters.uppercase"
) {
    ContentView()
        .overlay(alignment: .topTrailing) {
            Button(
                "Quit",
                systemImage: "xmark.circle.fill"
            ) {
                NSApp.terminate(nil)
            }
            .labelStyle(.iconOnly)
            .buttonStyle(.plain)
            .padding(6)
        }
        .frame(width: 300, height: 180)
}
Menu bar app with an 'ABC' icon, displaying a floating window with a text input field, a 'Copy uppercased result' button, and a close button in the top-right corner

# Use a custom icon for the menu bar item

Before releasing the app, replacing the system symbol with a custom icon can make the menu item easier to recognize. A simplified version of the app icon, similar to the one used in the App Store, helps users associate the menu bar button with the app they downloaded.

To use a custom image, we need a different MenuBarExtra initializer that takes a label instead of a text and system image. If the image asset is already sized correctly for the menu bar button, we can load it directly, as shown in the example below.

MenuBarExtra {
    ContentView()
        .overlay(alignment: .topTrailing) {
            ...
        }
        .frame(width: 300, height: 180)
} label: {
    Label(
        "Menu Bar Example",
        image: .menuBarIcon
    )
}

However, when using a PNG that was larger than needed, I ran into sizing issues when creating a SwiftUI image directly. A workaround that worked for me was to first load the image as an NSImage, adjust its size, and then convert it to a SwiftUI Image.

MenuBarExtra {
    ContentView()
        .overlay(alignment: .topTrailing) {
            ...
        }
        .frame(width: 300, height: 180)
} label: {
    Label {
        Text("Menu Bar Example")
    } icon: {
        let image: NSImage = {
            let ratio = $0.size.height / $0.size.width
            $0.size.height = 18
            $0.size.width = 18 / ratio
            return $0
        }(NSImage(named: "MenuBarIcon")!)
        
        Image(nsImage: image)
    }
}
Menu bar app with a stylized 'A' icon, a text input field, a 'Copy uppercased result' button, and a close button in the top-right corner

To take your macOS app further, consider integrating it with system features, such as adding a launch-at-login option or providing system-wide services. You can check out my other blog posts for instructions on how to do that: Add launch at login setting to a macOS app and Provide macOS system-wide services from your app.

If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/TestingSceneStorageStatePersistenceInXcodeTesting SceneStorage state persistence in XcodeVerify SwiftUI app state restoration using the Xcode simulator by following the correct steps, including backgrounding the app and relaunching it.https://nilcoalescing.com/blog/TestingSceneStorageStatePersistenceInXcodeWed, 5 Feb 2025 16:00:00 +1300Testing SceneStorage state persistence in Xcode

When we need to ensure that an iOS app preserves its state, such as tab selection or navigation, across launches, we can use the SceneStorage property wrapper in SwiftUI. This allows us to retain small pieces of UI-related state automatically.

struct ContentView: View {
    @SceneStorage("selectedTab")
    private var selectedTab: Int = 0
    
    var body: some View {
        TabView(selection: $selectedTab) {
            ...
        }
    }
}

We can test whether state preservation works as expected using the Xcode simulator, but it's crucial to follow the correct sequence of steps. Otherwise, the state might not be restored, leading us to mistakenly think that our implementation is incorrect.

To properly test state preservation with SceneStorage in Xcode:

  1. Run the app in the Xcode simulator.
  2. Change the state (e.g., switch tabs or navigate within the app).
  3. Press the Home button in the simulator to send the app to the background.
  4. Press the Stop button in Xcode to terminate the app.
  5. Run the app again in Xcode and check if the state is preserved.

SwiftUI automatically clears SceneStorage values if the user explicitly quits the app from the app switcher. So when testing state preservation, avoid force-quitting the app and follow the steps above instead.

]]>
https://nilcoalescing.com/blog/CaptureUUIDValuesWithRegexCapture UUID values with regex in SwiftBuild reusable, type-safe components for extracting and validating UUID values using Swift's RegexBuilder framework.https://nilcoalescing.com/blog/CaptureUUIDValuesWithRegexSun, 2 Feb 2025 18:00:00 +1300Capture UUID values with regex in Swift

When working with UUIDs in Swift, we may need to match and extract them using regular expressions. To achieve this in a type-safe and reusable way, we can define a custom RegexComponent leveraging the Capture component.

# Defining a reusable RegexComponent for UUIDs

The following implementation creates a CaptureUUID struct that conforms to RegexComponent. This struct defines a regular expression pattern for matching UUIDs while ensuring type safety through a transformation function.

import RegexBuilder

struct CaptureUUID: RegexComponent {
    let reference: Reference<UUID>
    
    init(as reference: Reference<UUID>) {
        self.reference = reference
    }
    
    @RegexComponentBuilder
    var regex: Regex<(Substring, UUID)> {
        Capture(as: reference) {
            Repeat(count: 8) { .hexDigit }
            "-"
            Repeat(count: 4) { .hexDigit }
            "-"
            Repeat(count: 4) { .hexDigit }
            "-"
            Repeat(count: 4) { .hexDigit }
            "-"
            Repeat(count: 12) { .hexDigit }
        } transform: { substring in
            try Self.transform(substring)
        }
    }
    
    private static func transform(
        _ substring: Substring
    ) throws -> UUID {
        guard
            let uuid = UUID(uuidString: String(substring))
        else {
            throw RegexTransformError
                .unableToDecode(UUID.self, from: substring)
        }
        return uuid
    }
}

enum RegexTransformError: Error {
    case unableToDecode(Any.Type, from: Substring)
}

# Using CaptureUUID in a regex expression

This component can be integrated into a larger regex pattern to extract UUID values from a given string. For example, the following regex captures a UUID from a URL path:

let accountID = Reference<UUID>()

let regex = Regex {
    "accounts/"
    CaptureUUID(as: accountID)
    "/"
    Optionally {
        "index.json"
    }
    Anchor.endOfSubject
}

let match = url
    .path(percentEncoded: false)
    .firstMatch(of: regex)
    
if let match {
    // Returns the account id as a UUID
    return match[accountID]
}

By using CaptureUUID, we ensure type-safe access to extracted values while improving the clarity and maintainability of our regex-based parsing logic.

]]>
https://nilcoalescing.com/blog/DetachedActionsInDjangoChannelsRestFrameworkManaging WebSocket messages concurrently with detached actionsHandle multiple concurrent WebSocket messages per connection with Django Channels.https://nilcoalescing.com/blog/DetachedActionsInDjangoChannelsRestFrameworkWed, 29 Jan 2025 18:00:00 +1300Managing WebSocket messages concurrently with detached actions

When using Django Channels, WebSocket consumers handle one message at a time for each connection. This means long-running async tasks block the consumer from processing other messages.

With Django Channels Rest Framework, you can run actions in a detached manner, allowing your consumer to handle other messages concurrently.

To enable this, add detached=True to your @action method decorator:

from djangochannelsrestframework.decorators import action
from djangochannelsrestframework.consumers import AsyncAPIConsumer

class MyConsumer(AsyncAPIConsumer):
    @action(detached=True)
    async def send_email(self, pk=None, to=None, **kwargs):
        # Perform long-running async tasks like making network requests

Detached actions are essential for handling long-running async operations, such as upstream network requests. Without them, your consumer will queue all incoming messages until the current action finishes.

]]>
https://nilcoalescing.com/blog/macOSSystemWideServicesProvide macOS system-wide services from your appExtend your app’s functionality to the entire macOS system by implementing services that users can access from the context menu or the Services menu in other apps.https://nilcoalescing.com/blog/macOSSystemWideServicesTue, 28 Jan 2025 18:00:00 +1300Provide macOS system-wide services from your app

Services on macOS allow us to extend our app’s functionality to the entire system, enabling users to interact with our app’s features while working in other contexts without explicitly opening it. These services are accessible via the context menu or from an application's Services menu in the macOS menu bar.

I recently implemented services for URL encoding and decoding strings in my macOS utility, EncodeDecode, and in this post, I’ll explain how I did it.

macOS context menu with 'URL-Decode' and 'URL-Encode' services for text selection

For my app, I implemented two services: URL-Encode and URL-Decode. These services become available when users select text anywhere on the system, such as in TextEdit or Xcode. When a user selects text and invokes one of the services, macOS launches EncodeDecode, passes the selected string to my app for processing, and replaces the original text with the processed result.

# Define service methods in code

To implement this functionality, I needed to define the required methods in code. This involved creating a service provider class that inherits from NSObject and implementing methods that process the text from the pasteboard.

import AppKit

class EncodeDecodeServiceProvider: NSObject {
    @objc func encode(
        _ pasteboard: NSPasteboard,
        userData: String?,
        error: AutoreleasingUnsafeMutablePointer<NSString>
    ) {
        guard let string = pasteboard.string(
            forType: NSPasteboard.PasteboardType.string
        ) else {
            return
        }
        pasteboard.clearContents()
        pasteboard.setString(
            EncodingManager.encodeUrl(string),
            forType: .string
        )
    }
    
    @objc func decode(
        _ pasteboard: NSPasteboard,
        userData: String?,
        error: AutoreleasingUnsafeMutablePointer<NSString>
    ) {
        guard let string = pasteboard.string(
            forType: NSPasteboard.PasteboardType.string
        ) else {
            return
        }
        pasteboard.clearContents()
        pasteboard.setString(
            EncodingManager.decode(urlString: string),
            forType: .string
        )
    }
}

# Register the service provider

After defining the service methods, the next step was to register the service provider. I did it in the applicationDidFinishLaunching(_:) method of my application delegate.

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(
        _ aNotification: Notification
    ) {
        NSApplication.shared
            .servicesProvider = EncodeDecodeServiceProvider()
    }
}

Only one service provider can be registered per application. If our app provides multiple services, a single object must handle all of them, as I did in EncodeDecode.

# Update Info.plist

Finally, I needed to declare the services in the Info.plist file. Here is how the NSServices key is configured for EncodeDecode:

<key>NSServices</key>
<array>
    <dict>
        <key>NSKeyEquivalent</key>
        <dict>
            <key>default</key>
            <string>E</string>
        </dict>
        <key>NSMenuItem</key>
        <dict>
            <key>default</key>
            <string>URL-Encode</string>
        </dict>
        <key>NSMessage</key>
        <string>encode</string>
        <key>NSPortName</key>
        <string>EncodeDecode</string>
        <key>NSRestricted</key>
        <false/>
        <key>NSRequiredContext</key>
        <dict/>
        <key>NSReturnTypes</key>
        <array>
            <string>NSStringPboardType</string>
        </array>
        <key>NSSendTypes</key>
        <array>
            <string>NSStringPboardType</string>
        </array>
    </dict>
    <dict>
        <key>NSKeyEquivalent</key>
        <dict>
            <key>default</key>
            <string>D</string>
        </dict>
        <key>NSMenuItem</key>
        <dict>
            <key>default</key>
            <string>URL-Decode</string>
        </dict>
        <key>NSMessage</key>
        <string>decode</string>
        <key>NSPortName</key>
        <string>EncodeDecode</string>
        <key>NSRestricted</key>
        <false/>
        <key>NSRequiredContext</key>
        <dict/>
        <key>NSReturnTypes</key>
        <array>
            <string>NSStringPboardType</string>
        </array>
        <key>NSSendTypes</key>
        <array>
            <string>NSStringPboardType</string>
        </array>
    </dict>
</array>

The NSServices key contains an array of dictionaries, each defining a service. For EncodeDecode, there are two services: URL-Encode and URL-Decode. Each service specifies the name as it appears in the Services menu, along with a keyboard shortcut for quick access. The NSMessage key maps to the selector method handling the service, and NSPortName identifies the app providing the service. Both services define NSSendTypes and NSReturnTypes to specify that they work with text data from the pasteboard.

To simplify the process of adding and editing these configurations, I used the XML editor in Xcode. You can access it by right-clicking on the Info.plist file and selecting Open As > Source Code. This approach provides full control and makes it easier to define the necessary keys and values.

# Testing and debugging

During development and testing, I found that the most reliable way to see updated services was to log out of my macOS user account and log back in.

To ensure that the system has recognized your service, you can also use the pbs tool to list the registered services. Run the following command in the terminal with the dump_pboard option:

/System/Library/CoreServices/pbs -dump_pboard

This command will display a list of registered services, allowing you to verify that your service has been correctly registered.


Users can manage available services in System Settings under Keyboard > Keyboard Shortcuts > Services. They can enable or disable services, assign shortcuts for quick access, and ensure only relevant services appear in an app’s Services menu.

System Settings showing the Services section under Keyboard Shortcuts, with options like URL-Encode and URL-Decode enabled


If you're an experienced Swift developer looking to learn advanced techniques, check out my book Swift Gems. It’s packed with tips and tricks focused solely on the Swift language and Swift Standard Library. From optimizing collections and handling strings to mastering asynchronous programming and debugging, "Swift Gems" provides practical advice that will elevate your Swift development skills to the next level. Grab your copy and let's explore these advanced techniques together.

]]>
https://nilcoalescing.com/blog/PopoverOniPhoneInSwiftUIShow a popover on iPhone in SwiftUIStarting with iOS 16.4, we can use the presentationCompactAdaptation(_:) modifier to tell SwiftUI that we prefer popover presentation even in compact size classes.https://nilcoalescing.com/blog/PopoverOniPhoneInSwiftUIFri, 24 Jan 2025 18:00:00 +1300Show a popover on iPhone in SwiftUI

In SwiftUI, we can use the popover() modifier to present a popover when a given condition is true. By default, it shows a popover on iPad in a regular horizontal size class but turns into a sheet on iPhone.

Starting with iOS 16.4, we can use the presentationCompactAdaptation(_:) modifier to tell SwiftUI that we prefer popover presentation even in compact size classes. This modifier is applied to the view inside the popover's content.

Here’s an example of how we can show a popover for a list of ingredients in a recipe app when the user clicks on the list button:

IngredientsListButton(
    showIngredients: $showIngredients
)
.popover(isPresented: $showIngredients) {
    IngredientsList(
        ingredients: recipe.ingredients
    )
    .presentationCompactAdaptation(.popover)
}

Since we indicated that we prefer a popover for compact adaptation, SwiftUI will show a popover instead of a sheet, even on a phone.

An iPhone screen shows a recipe app with a popover listing cookie ingredients


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/LaunchAtLoginSettingAdd launch at login setting to a macOS appRegister your macOS app as a login item using SMAppService.https://nilcoalescing.com/blog/LaunchAtLoginSettingFri, 17 Jan 2025 18:00:00 +1300Add launch at login setting to a macOS app

I recently built a macOS menu bar utility for URL encoding and decoding strings called EncodeDecode, and I thought it would be nice to include a setting to launch the app at login for users who might need frequent access to it. In this post, I'll show how this can be easily implemented using SMAppService.

It's important to note that it's best to have an explicit setting for this functionality in the app, set to false by default, to avoid any issues with App Store review. According to Apple's App Review Guidelines, Mac apps may not auto-launch or execute code at startup or login without user consent.

I included a toggle to control this setting within the Settings panel of my app.

Screenshot of the Settings panel of EncodeDecode

Here's the UI code for that section:

struct LaunchAtLoginSettingsSection: View {
    @Environment(AppState.self) var appState
    
    var body: some View {
        @Bindable var appState = appState
        
        Section {
            VStack(alignment: .leading, spacing: 8) {
                Toggle("Launch at login", isOn: $appState.launchAtLogin)
                Text("""
                Add EncodeDecode app to the menu bar automatically \
                when you log in on your Mac.
                """)
                    .font(.callout)
                    .foregroundStyle(.secondary)
            }
        }
    }
}


@Observable
class AppState {
    var launchAtLogin = false
}

To use the SMAppService APIs, we need to import the ServiceManagement framework. Once imported, we can register the app service object corresponding to the main application as a login item when the user enables the setting, and unregister it when the setting is disabled.

import ServiceManagement

struct LaunchAtLoginSettingsSection: View {
    @Environment(AppState.self) var appState
    
    var body: some View {
        ...
        
        Section {
            ...
        }
        .onChange(of: appState.launchAtLogin) { _, newValue in
            if newValue == true {
                try? SMAppService.mainApp.register()
            } else {
                try? SMAppService.mainApp.unregister()
            }
        }
    }
}

When the user enables the setting and we register the app with SMAppService, they will see a notification informing them that a login item was added.

Screenshot of the Login Item Added notification

Users can view and manage their login items in the System Settings. They can also remove our app from the login items list if they wish.

Screenshot of the Login Items and Extensions settings


The SMAppService API allows us to check the current status of our app, so we can use it to ensure the UI is up to date.

import ServiceManagement

struct LaunchAtLoginSettingsSection: View {
    @Environment(AppState.self) var appState
    
    var body: some View {
        ...
        
        Section {
            ...
        }
        .onAppear {
            if SMAppService.mainApp.status == .enabled {
                appState.launchAtLogin = true
            } else {
                appState.launchAtLogin = false
            }
        }
    }
}

In most cases, checking the status in onAppear() should be sufficient, as it’s unlikely that the user would modify this setting in the System Settings while the app's settings window is open. However, if that does happen, we can update the UI state when the settings window regains focus by using the appearsActive environment value. This ensures that when the user refocuses on the settings window, the correct system settings status is reflected.

import ServiceManagement

struct LaunchAtLoginSettingsSection: View {
    @Environment(AppState.self) var appState
    @Environment(\.appearsActive) var appearsActive
    
    var body: some View {
        ...
        
        Section {
            ...
        }
        .onChange(of: appearsActive) { _, newValue in
            guard newValue else { return }
            if SMAppService.mainApp.status == .enabled {
                appState.launchAtLogin = true
            } else {
                appState.launchAtLogin = false
            }
        }
    }
}

Since users can remove our app from the login items at any time, it’s important to read the status from SMAppService rather than storing this setting locally in the app, to ensure that our UI reflects the most recent state.


If you're an experienced Swift developer looking to learn advanced techniques, check out my book Swift Gems. It’s packed with tips and tricks focused solely on the Swift language and Swift Standard Library. From optimizing collections and handling strings to mastering asynchronous programming and debugging, "Swift Gems" provides practical advice that will elevate your Swift development skills to the next level.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/HandlePluralsInSwiftUITextViewsWithInflectionHandle plurals in SwiftUI Text views with inflectionDisplay grammatically correct text effortlessly with Foundation's automatic grammar agreement, handling pluralization without extra logic.https://nilcoalescing.com/blog/HandlePluralsInSwiftUITextViewsWithInflectionMon, 13 Jan 2025 18:00:00 +1300Handle plurals in SwiftUI Text views with inflection

The Foundation framework has a feature called automatic grammar agreement that reduces the number of localized strings we need to provide while making our code simpler. It ensures text follows grammatical rules like pluralization and gender. It works seamlessly with SwiftUI, allowing us to handle plurals directly in Text views without any extra manual logic.

To make text automatically adjust to plural values, we can specify that we want it to use the inflection rule and define the scope for the inflection using the following syntax:

Text("You read ^[\(bookCount) book](inflect: true) this year!")

SwiftUI recognizes this syntax as a custom Markdown attribute and processes it accordingly. When a Text view is created with a string literal, as shown in the example above, SwiftUI treats the string as a LocalizedStringKey and parses the Markdown it contains. It identifies inflection attributes within the string and uses the automatic grammar agreement feature from Foundation to apply the necessary adjustments during rendering.

Here’s how that would look in a complete SwiftUI view:

struct BookReadingTracker: View {
    @State private var bookCount: Int = 0
    
    var body: some View {
        VStack(spacing: 16) {
            Text("""
            You read ^[\(bookCount) book](inflect: true) this year!
            """)

            Button("Add a book") {
                bookCount += 1
            }
        }
    }
}

In this example, tapping the button increases the bookCount value, and the text updates automatically. When bookCount is 1, it uses the singular form book. As the value increases, such as to 2 or higher, the text dynamically switches to the plural form, ensuring grammatical correctness.

Two views showing 'You read 1 book this year!' and 'You read 2 books this year!' with 'Add a book' button below

This integration makes handling plurals in SwiftUI both simple and efficient. The automatic grammar agreement feature and the inflection attribute were introduced in iOS 15 with initial support for English and Spanish. Over the years, the grammar engine was expanded to include additional languages, and as of iOS 18, it also supports German, French, Italian, Portuguese, Hindi, and Korean, making it even more versatile for multilingual apps.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/CodableConformanceForSwiftEnumsCodable conformance for Swift enumsLearn how to add Codable conformance to Swift enums, including automatic synthesis, customizations, and fully manual implementations for complex cases.https://nilcoalescing.com/blog/CodableConformanceForSwiftEnumsTue, 7 Jan 2025 18:00:00 +1300Codable conformance for Swift enums

Enums in Swift are a fundamental way to model a fixed set of choices or states in a type-safe manner. Adding Codable conformance allows enums to be serialized and deserialized, enabling them to work seamlessly with data formats like JSON or property lists. This is particularly useful when interacting with APIs, saving app data, or exchanging information between systems.

# Automatic Codable conformance

In many cases, all we need to do is mark an enum as Codable, and the compiler handles the rest. Before Swift 5.5, only enums conforming to RawRepresentable could automatically conform to Codable. Now, enums without raw values, including those with or without associated values, can also benefit from automatic Codable synthesis if all associated types are themselves Codable.

Let’s look at how automatic conformance works for different kinds of enums.

# Raw value enums

For enums with raw values, the encoded value corresponds to the raw value that represents it.

enum Direction: String, Codable {
    case north
    case south
    case east
    case west
}

let direction: Direction = .north
let jsonData = try JSONEncoder().encode(direction)

// Output: "north"
let jsonString = String(data: jsonData, encoding: .utf8)!

Decoding works the same way, the raw value is mapped back to the appropriate case.

# Enums without raw or associated values

Enums without associated values are encoded as an empty container, preserving the case name as the key.

enum Status: Codable {
    case success
    case failure
}

let status: Status = .success
let jsonData = try JSONEncoder().encode(status)

// Output: {"success":{}}
let jsonString = String(data: jsonData, encoding: .utf8)!

Decoding restores the enum case from the JSON key.

# Enums with associated values

For enums with associated values, each case is treated as a container with a key matching the case name. The associated values are encoded as nested key-value pairs within that container.

enum Command: Codable {
    case load(key: String)
    case store(key: String, value: Int)
}

let command: Command = .store(key: "exampleKey", value: 42)
let jsonData = try JSONEncoder().encode(command)

// Output: {"store":{"value":42,"key":"exampleKey"}}
let jsonString = String(data: jsonData, encoding: .utf8)!

For cases with unlabeled associated values, the compiler generates underscore-prefixed numeric keys, such as _0, _1, etc., based on their position.

enum Role: Codable {
    case vipMember(String, Int)
}


let role: Role = .vipMember("1234", 5)
let jsonData = try JSONEncoder().encode(role)

// Output: {"vipMember":{"_0":"1234","_1":5}}
let jsonString = String(data: jsonData, encoding: .utf8)!

Optional associated values are treated as nil if they are missing, which results in them being excluded from the encoded data.

# Customizing automatic conformance

# Customizing case names

By defining a CodingKeys enum, we can map case names to custom keys during encoding and decoding.

enum Status: Codable {
    case success
    case failure(reason: String)
    
    enum CodingKeys: String, CodingKey {
        case success
        case failure = "error"
    }
}

This customization alters the JSON structure:

let status: Status = .failure(reason: "Invalid request")
let jsonData = try JSONEncoder().encode(status)

// Output: {"error":{"reason":"Invalid request"}}
let jsonString = String(data: jsonData, encoding: .utf8)!

# Customizing associated value keys

We can also customize the keys used for associated values by defining separate coding keys enums for each case. These coding keys enums have to be prefixed with the capitalized case name.

enum Command: Codable {
    case load(key: String)
    case store(key: String, value: Int)
    
    enum StoreCodingKeys: String, CodingKey {
        case key
        case value = "data"
    }
}

let command: Command = .store(key: "code", value: 123)
let jsonData = try JSONEncoder().encode(command)

// Output: {"store":{"key":"code","data":123}}
let jsonString = String(data: jsonData, encoding: .utf8)!

This approach is particularly useful when we need to align the encoded structure with external data formats.

# Excluding cases or values

Certain cases or associated values can be excluded by omitting them from the CodingKeys declaration.

enum Event: Codable {
    case start
    case end(description: String, metadata: String = "")

    enum EndCodingKeys: String, CodingKey {
        case description
    }
}

Values that are excluded must have a default value defined, if a Decodable conformance should be synthesized.

# Custom Codable conformance

Automatic conformance doesn’t fit all use cases. For example, enums with overloaded case identifiers or complex encoding requirements might require custom implementations of encode(to:) and init(from:).

enum Response: Codable {
    case success(data: String)
    case error(reason: String)
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .success(let data):
            try container.encode(["status": "success", "data": data])
        case .error(let reason):
            try container.encode(["status": "error", "reason": reason])
        }
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawData = try container.decode([String: String].self)
        if let data = rawData["data"] {
            self = .success(data: data)
        } else if let reason = rawData["reason"] {
            self = .error(reason: reason)
        } else {
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: decoder.codingPath,
                    debugDescription: "Invalid data"
                )
            )
        }
    }
}

This allows a custom structure that doesn’t follow the default nested encoding format.

Codable conformance makes enums in Swift even more powerful and versatile. While automatic synthesis covers many scenarios, customization and fully manual implementations offer the flexibility to handle complex requirements. With these tools, we can work confidently with serialized data, ensuring that our enums integrate seamlessly into real-world applications.


As someone who has worked extensively with Swift, I've gathered many insights over the years and compiled them in my book, Swift Gems. The book focuses exclusively on the Swift language and Swift Standard Library, offering over 100 advanced tips and techniques on topics such as optimizing collections, leveraging generics, asynchronous programming, and debugging. Each tip is designed to help intermediate and advanced Swift developers write clearer, faster, and more maintainable code by fully utilizing Swift's built-in capabilities.

]]>
https://nilcoalescing.com/blog/CustomizingMacOSWindowBackgroundInSwiftUICustomizing macOS window background in SwiftUIApply a translucent background to macOS windows using SwiftUI APIs, and adjust the toolbar background and title visibility to customize the window's appearance.https://nilcoalescing.com/blog/CustomizingMacOSWindowBackgroundInSwiftUIMon, 30 Dec 2024 19:00:00 +1300Customizing macOS window background in SwiftUI

Customizing the appearance of macOS windows can help create a unique and polished user experience. One of the key aspects we can modify is the window's background. In this post, we’ll explore how to customize the background of macOS windows in SwiftUI, adjust the toolbar appearance, and remove the window title. These simple changes can make our apps look more modern and clean.

By default, macOS windows have an opaque background and a toolbar with its own background on top. Here's how a basic default window looks when we create a fresh SwiftUI app.

Screenshot of a macOS app window with the default opaque background in dark mode

To customize the background of our window, we can use the containerBackground(_:for:) modifier. This allows us to pass a style for the background, setting the container parameter to .window. The style can be any type that conforms to ShapeStyle, such as a color, gradient, or material. For example, we can apply a translucent material to our window, which allows the desktop color to shine through.

  @main
struct MyAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .containerBackground(
                    .thinMaterial, for: .window
                )
        }
    }
}
Screenshot of a macOS app window with a translucent material background in dark mode

If we also want to remove the toolbar background, we can apply the toolbarBackgroundVisibility(_:for:) modifier to the contents of the window. We pass .hidden for the visibility parameter and .windowToolbar for the bars parameter, which will hide the toolbar's background.

@main
struct MyAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .containerBackground(
                    .thinMaterial, for: .window
                )
                .toolbarBackgroundVisibility(
                    .hidden, for: .windowToolbar
                )
        }
    }
}
Screenshot of a macOS app window with a translucent material background without any toolbar background in dark mode

If we want to go further, we can remove the window title as well. This can be done either by adding toolbar(removing: .title) to the contents of the window or by applying windowStyle(.hiddenTitleBar) to the WindowGroup. The latter removes both the title and the toolbar background in one step.

@main
struct MyAppApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .containerBackground(
                    .thinMaterial, for: .window
                )
        }
        .windowStyle(.hiddenTitleBar)
    }
}
Screenshot of a macOS app window with a translucent material background without any toolbar background or window title in dark mode

When there is no visible toolbar, it might be helpful to allow users to drag the window by clicking anywhere on the background. We can achieve this by applying windowBackgroundDragBehavior(.enabled) to the WindowGroup.

@main
struct MyAppApp: App {
    var body: some Scene {
        WindowGroup {
            ...
        }
        .windowStyle(.hiddenTitleBar)
        .windowBackgroundDragBehavior(.enabled)
    }
}

With just a few SwiftUI modifiers, we can adjust the appearance of macOS windows to better fit our needs. Customizing the background, toolbar, and title can help us create the design we're looking for in our apps.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/AdjustTheIntensityOfColorsInSwiftUIViewsAdjust the intensity of colors in SwiftUI viewsLighten or darken colors in SwiftUI views using the brightness(_:) modifier.https://nilcoalescing.com/blog/AdjustTheIntensityOfColorsInSwiftUIViewsThu, 19 Dec 2024 18:00:00 +1300Adjust the intensity of colors in SwiftUI views

In SwiftUI, we can use the convenient brightness(_:) modifier to adjust the brightness of colors in our views. It accepts a Double value, where passing 0 keeps the original color, and a value of 1 makes the color fully white.

Here’s an example of how we can use it to lighten a color:

ForEach(0..<8) { num in
    Color.purple
        .brightness(Double(num) * 0.1)
}

This creates a gradient effect, transitioning the purple color from its original shade to almost white.

iPhone screen displaying a gradient of color purple from original to almost white

We can also use negative values with the brightness(_:) modifier to darken a color as it approaches -1:

ForEach(0..<8) { num in
    Color.purple
        .brightness(Double(num) * -0.1)
}

This results in a gradient effect where the purple transitions from its original shade to almost black.

iPhone screen displaying a gradient of color purple from original to almost black


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/NoncopyableTypesInSwiftNoncopyable types in SwiftExplore noncopyable types in Swift and learn how they enforce stricter ownership rules to avoid unintended errors and resource conflicts.https://nilcoalescing.com/blog/NoncopyableTypesInSwiftMon, 16 Dec 2024 18:00:00 +1300Noncopyable types in Swift

In Swift, types are copyable by default. This design simplifies development by allowing values to be easily duplicated when assigned to new variables or passed to functions. While convenient, this behavior can sometimes lead to unintended issues. For instance, copying a single-use ticket or duplicating a database connection could result in invalid states or resource conflicts.

To address these challenges, Swift 5.9 introduced noncopyable types. By marking a type as ~Copyable, we explicitly prevent Swift from duplicating it. This guarantees unique ownership of the value and enforces stricter constraints, reducing the risk of errors.

Here’s a simple example of a noncopyable type:

struct SingleUseTicket: ~Copyable {
    let ticketID: String
}

In contrast to the regular behavior of value types, when we assign an instance of a noncopyable type to a new variable, the value gets moved instead of being copied. If we attempt to use the original variable later, we'll get a compile-time error.

let originalTicket = SingleUseTicket(ticketID: "S645")
let newTicket = originalTicket

// Error: 'originalTicket' used after consume
// print(originalTicket.ticketID)

Classes can't be declared noncopyable. All class types remain copyable by retaining and releasing references to the object.

# Methods in noncopyable types

Methods in noncopyable types can read, mutate, or consume self.

# Borrowing methods

Methods inside a noncopyable type are borrowing by default. This means they only have read access to the instance, allowing safe inspection of the instance without affecting its validity.

struct SingleUseTicket: ~Copyable {
    let ticketID: String
    
    func describe() {
        print("This ticket is \(ticketID).")
    }
}

let ticket = SingleUseTicket(ticketID: "A123")

// Prints `This ticket is A123.`
ticket.describe()

# Mutating methods

A mutating method provides temporary write access to self, allowing modifications without invalidating the instance.

struct SingleUseTicket: ~Copyable {
    var ticketID: String

    mutating func updateID(newID: String) {
        ticketID = newID
        print("Ticket ID updated to \(ticketID).")
    }
}

var ticket = SingleUseTicket(ticketID: "A123")

// Prints `Ticket ID updated to B456.`
ticket.updateID(newID: "B456")

# Consuming methods

A consuming method takes ownership of self, invalidating the instance once the method completes. This is useful for tasks that finalize or dispose of a resource. After the method is called, any attempt to access the instance results in a compile-time error.

struct SingleUseTicket: ~Copyable {
    let ticketID: String
    
    consuming func use() {
        print("Ticket \(ticketID) used.")
    }
}

func useTicket() {
    let ticket = SingleUseTicket(ticketID: "A123")
    ticket.use()
    
    // Error: 'ticket' consumed more than once
    // ticket.use()
}

useTicket()

Note that we can't consume noncopyable types stored in global variables, that's why we wrapped the code into the useTicket() function in our example.

# Noncopyable types in function arguments

When passing noncopyable types as arguments to functions, Swift requires us to specify the ownership model for that function. We can mark parameters as borrowing, inout, or consuming, each offering different levels of access, similar to methods inside the types.

# Borrowing parameters

Borrowing ownership allows the function to temporarily read the value without consuming or mutating it.

func inspectTicket(_ ticket: borrowing SingleUseTicket) {
    print("Inspecting ticket \(ticket.ticketID).")
}

# Inout parameters

The inout modifier provides temporary write access to a value, allowing the function to modify it while returning ownership to the caller.

func updateTicketID(_ ticket: inout SingleUseTicket, to newID: String) {
    ticket.ticketID = newID
    print("Ticket ID updated to \(ticket.ticketID).")
}

# Consuming parameters

When a parameter is marked as consuming, the function takes full ownership of the value, invalidating it for the caller. This is ideal for tasks where the value is no longer needed after the function.

func processTicket(_ ticket: consuming SingleUseTicket) {
    ticket.use()
}

# Deinitializers and the discard operator

Noncopyable structs and enums can have deinitializers, like classes, which run automatically at the end of the instance's lifetime.

struct SingleUseTicket: ~Copyable {
    let ticketID: Int
    
    deinit {
        print("Ticket deinitialized.")
        
        // cleanup logic
    }
}

However, when both a consuming method and a deinitializer perform cleanup, there is a risk of redundant operations. To address this, Swift introduced the discard operator.

By using discard self in a consuming method, we explicitly stop the deinitializer from being called, avoiding duplicate logic:

struct SingleUseTicket: ~Copyable {
    let ticketID: Int
    
    consuming func invalidate() {
        print("Ticket \(ticketID) invalidated.")
        
        // cleanup logic
        
        discard self
    }
    
    deinit {
        print("Ticket deinitialized.")
        
        // cleanup logic
    }
}

Note that we can only use discard if our type contains trivially-destroyed stored properties. It can't have reference-counted, generic, or existential fields.


Noncopyable types are invaluable in scenarios where unique ownership is essential. They prevent duplication of resources like single-use tokens, cryptographic keys, or database connections, reducing the risk of errors. By enforcing ownership rules at compile time, Swift enables developers to write safer, more efficient code.

While noncopyable types might not be required in every project, they provide a powerful tool for ensuring safety and clarity in critical systems. As Swift continues to evolve, these types represent a significant step forward in the language’s focus on performance and correctness.


As someone who has worked extensively with Swift, I've gathered many insights over the years and compiled them in my book, Swift Gems. The book focuses exclusively on the Swift language and Swift Standard Library, offering over 100 advanced tips and techniques on topics such as optimizing collections, leveraging generics, asynchronous programming, and debugging. Each tip is designed to help intermediate and advanced Swift developers write clearer, faster, and more maintainable code by fully utilizing Swift's built-in capabilities.

]]>
https://nilcoalescing.com/blog/CopyStringToClipboardInSwiftOnMacOSCopy a string to the clipboard in Swift on macOSTo copy a string to the clipboard on macOS, we need to first clear it using clearContents(), unlike on iOS, where UIPasteboard automatically overwrites the existing data.https://nilcoalescing.com/blog/CopyStringToClipboardInSwiftOnMacOSSun, 8 Dec 2024 11:00:00 +1300Copy a string to the clipboard in Swift on macOS

I’ve been implementing copy-to-clipboard functionality in a Mac app and realized that it’s a bit different from iOS. With UIPasteboard, assigning a string automatically replaces the previous contents of the clipboard with the new string. But on macOS, we need to clear the previous contents manually, otherwise, it won’t work.

So essentially, to copy a string to the clipboard on macOS, our code will look like this:

func copyToClipboard(string: String) {
    let pasteboard = NSPasteboard.general
    pasteboard.clearContents()
    pasteboard.setString(string, forType: .string)
}

We should use clearContents() as the first step when providing data to the pasteboard.

I found a few solutions online suggesting the use of declareTypes(_:owner:) as the initial step for setting up the pasteboard. However, according to Apple’s documentation for NSPasteboard, this approach was meant for writing data to the pasteboard on macOS versions 10.5 and earlier.


If you're an experienced Swift developer looking to deepen your skills, my book Swift Gems covers advanced techniques like optimizing collections, handling strings, and mastering asynchronous programming, perfect for taking your Swift expertise to the next level.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/SetAShapeAsBackgroundInSwiftUISet a shape as background in SwiftUISwiftUI provides a simple way to set a view's background to a shape, like a capsule or rounded rectangle, using the background(_:in:fillStyle:) modifier.https://nilcoalescing.com/blog/SetAShapeAsBackgroundInSwiftUIFri, 6 Dec 2024 11:00:00 +1300Set a shape as background in SwiftUI

SwiftUI provides a simple way to set a view's background to a shape, like a capsule or rounded rectangle, using the background(_:in:fillStyle:) modifier. This avoids the need to clip the background or separately define and fill a shape.

Here's an example:

Text("Hello, world!")
    .font(.title)
    .fontWeight(.semibold)
    .padding(22)
    .background(
        Color.yellow.gradient,
        in: Capsule()
    )

In this case, the capsule filled with a subtle yellow gradient is layered behind the text using background(Color.yellow.gradient, in: Capsule()).

iPhone screen displaying text with a yellow capsule in the background

This convenience method works with shapes that conform to the InsettableShape protocol, like Capsule, Rectangle, Circle, and RoundedRectangle.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/CustomSegmentedControlWithMatchedGeometryEffectSwiftUI matched geometry effect in a custom segmented controlLearn how to use matchedGeometryEffect() in SwiftUI to animate a capsule background that highlights the selected option in a custom segmented control.https://nilcoalescing.com/blog/CustomSegmentedControlWithMatchedGeometryEffectSun, 1 Dec 2024 19:00:00 +1300SwiftUI matched geometry effect in a custom segmented control

I'm working on a small macOS utility app and using it as an opportunity to experiment with some SwiftUI features. As part of this project, I decided to build a custom segmented control and looked for a simple, clean way to animate a capsule shape that highlights the selected option as it transitions between choices when the user interacts with the control. I found that using matchedGeometryEffect() was the most straightforward solution.

While many examples of the matchedGeometryEffect() modifier focus on hero animations, it can also be applied in other contexts, like my custom segmented control. Here, it's not used to transition geometry when one view disappears and another appears. Instead, it matches the geometry of a non-source view to that of the source view while both remain present in the view hierarchy simultaneously.

Let me show you how I set it up.

First, I defined the foundation of my segmented control: an HStack of buttons. Tapping a button updates the selection state. I also added a background around all the options to form the control. At this stage, tapping a button changes the selection, but there’s no visual indication of the currently selected option.

enum SegmentedControlState: String, CaseIterable, Identifiable {
    var id: Self { self }
    
    case option1 = "Option 1"
    case option2 = "Option 2"
    case option3 = "Option 3"
}

struct SegmentedControl: View {
    @State private var state: SegmentedControlState = .option1
    
    var body: some View {
        HStack {
            ForEach(SegmentedControlState.allCases) { state in
                Button {
                    self.state = state
                } label: {
                    Text(state.rawValue)
                        .padding(10)
                }
            }
        }
        
        .background(.indigo)
        .clipShape(
            Capsule()
        )
        .buttonStyle(.plain)
    }
}
Xcode preview showing a HStack with three plain buttons with indigo background

Next, I added another capsule in the background of the HStack containing the buttons. This capsule will eventually highlight the selected option once everything is set up. For now, however, it simply fills the entire available space within the control, minus some padding.

struct SegmentedControl: View {
    @State private var state: SegmentedControlState = .option1
    
    var body: some View {
        HStack {
            ...
        }
        
        .background(
            Capsule()
                .fill(.background.tertiary)
        )
        .padding(6)
        
        .background(.indigo)
        .clipShape(
            Capsule()
        )
        .buttonStyle(.plain)
    }
}
Xcode preview showing a HStack with three plain buttons with indigo background and inner dark background capsule

To make the inner capsule dynamically match the size and position of the selected option, I used the matchedGeometryEffect(). First, I defined a namespace with the @Namespace property wrapper. Then, I applied the matchedGeometryEffect() modifier to the inner capsule, linking it to the namespace and using the selection state as the identifier. This identifier ensures that the capsule matches the geometry of the source view with the same ID. Additionally, it’s important to set the capsule as a non-source view by specifying isSource: false.

struct SegmentedControl: View {
    @State private var state: SegmentedControlState = .option1
    @Namespace private var segmentedControl
    
    var body: some View {
        HStack {
            ...
        }
        
        .background(
            Capsule()
                .fill(.background.tertiary)
                .matchedGeometryEffect(
                    id: state,
                    in: segmentedControl,
                    isSource: false
                )
        )
        .padding(6)
        
        ...
    }
}

For now, nothing will visibly change since it's not specified which view should act as the source for the geometry. That’s the next step.

I applied the matchedGeometryEffect() modifier to each button in the HStack, assigning each button a unique ID corresponding to the state it represents. Note that I didn’t specify the isSource parameter for the buttons, as it defaults to true. This means the currently selected button automatically acts as the source for the matched geometry effect, allowing the capsule to align with the button whose ID matches the selection state.

struct SegmentedControl: View {
    @State private var state: SegmentedControlState = .option1
    @Namespace private var segmentedControl
    
    var body: some View {
        HStack {
            ForEach(SegmentedControlState.allCases) { state in
                Button {
                    self.state = state
                } label: {
                    Text(state.rawValue)
                        .padding(10)
                }
                .matchedGeometryEffect(
                    id: state,
                    in: segmentedControl
                )
            }
        }
        
        .background(
            Capsule()
                .fill(.background.tertiary)
                .matchedGeometryEffect(
                    id: state,
                    in: segmentedControl,
                    isSource: false
                )
        )
        .padding(6)
        
        ...
    }
}

If we check the preview now, we’ll see the capsule positioned just behind the selected option, matching its size and position. By default, the matched geometry effect aligns both size and position, but this behavior can be customized using the properties parameter. In this case, however, the default behavior works perfectly.

Xcode preview showing a HStack with three plain buttons where only option one button has dark capsule background

To animate the capsule when switching options, I wrapped the state change in a withAnimation block.

struct SegmentedControl: View {
    @State private var state: SegmentedControlState = .option1
    @Namespace private var segmentedControl
    
    var body: some View {
        HStack {
            ForEach(SegmentedControlState.allCases) { state in
                Button {
                    withAnimation {
                        self.state = state
                    }
                } label: {
                    Text(state.rawValue)
                        .padding(10)
                }
                .matchedGeometryEffect(
                    id: state,
                    in: segmentedControl
                )
            }
        }
        
        ...
    }
}

Now, the capsule slides smoothly between options as the selection changes.

A gif of a custom segmented control with the capsule shape sliding to indicate selected option as it is clicked

With just a few lines of code, I got a custom segmented control with a sliding animation. Using the matched geometry effect makes this approach much cleaner and simpler compared to alternative methods.

Keep in mind that when creating custom UI components, it's essential to test for accessibility features like Dynamic Type and VoiceOver. This example focuses on using the matched geometry effect for the background animation, so be sure to add any required accessibility functionality if you use it in your app. Here is a good accessibility solution using accessibilityRepresentation() from Bas Broek.

Since SwiftUI doesn’t yet allow custom styles for pickers, creating a custom segmented control like this requires building it as a separate component.

You can find the code example for this post on GitHub.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/AnimateUIKitViewsWithSwiftUIAnimationsAnimate UIKit views with SwiftUI animations in iOS 18With iOS 18, we can now use SwiftUI animations to animate UIKit views, making it easier to bring SwiftUI’s expressive and flexible animations into UIKit projects.https://nilcoalescing.com/blog/AnimateUIKitViewsWithSwiftUIAnimationsThu, 28 Nov 2024 19:00:00 +1300Animate UIKit views with SwiftUI animations in iOS 18

iOS 18 introduced a powerful new feature: the ability to animate UIKit views using SwiftUI animation types. This bridges the gap between the two frameworks even further, allowing us to bring the flexibility and expressiveness of SwiftUI animation system into UIKit-based projects.

Let’s take a look at a simple example to see how this works in practice. We’ll animate a UIImageView that scales up and down continuously.

Here is the initial setup:

class ViewController: UIViewController {
    private var animatingView: UIView?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        animatingView = UIImageView(
            image: UIImage(systemName: "volleyball.fill")
        )
        animatingView?.tintColor = .systemPink
        animatingView?.contentMode = .scaleAspectFit
        animatingView?.frame = CGRect(
            origin: .init(x: 0, y: 0),
            size: .init(width: 80, height: 80)
        )
        view.addSubview(animatingView!)
        animatingView?.center = view.center
    }
}

Next, we'll define the animation logic. The animation will start when the view appears on the screen, so we’ll implement it in the viewDidAppear() method. We'll use SwiftUI Animation API to create a linear animation lasting 1.3 seconds that repeats indefinitely. We'll apply this animation to the image view using the new UIView.animate() method, which accepts a SwiftUI Animation as an argument.

class ViewController: UIViewController {
    ...

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.startAnimating()
    }

    private func startAnimating() {
        let animation = SwiftUI.Animation
            .linear(duration: 1.3)
            .repeatForever()
        
        UIView.animate(animation) { [weak self] in
            self?.animatingView?.transform = .init(scaleX: 2, y: 2)
        }
    }
}

Finally, we can preview the animation directly in Xcode to ensure everything is working as expected:

#Preview {
    ViewController()
}

When the app runs, the volleyball icon smoothly scales up to twice its size and continuously repeats this effect.

A gif of a smooth animation of a volleyball growing and shrinking in the middle of the phone screen

SwiftUI animation API makes it simple to define animations and manage their timing and repetition. By using SwiftUI animations in UIKit, we can create smoother, more cohesive animations across our entire app, improving the overall experience for users.


If you have older iOS apps and want to enhance them with modern SwiftUI features, check out my book Integrating SwiftUI into UIKit Apps. It provides detailed guidance on gradually adopting SwiftUI in your UIKit projects. Additionally, if you're eager to enhance your Swift programming skills, my book Swift Gems offers over a hundred advanced tips and techniques, including optimizing collections, handling strings, mastering asynchronous programming, and debugging, to take your Swift code to the next level.

]]>
https://nilcoalescing.com/blog/SetSupportedPlatformsInFileTargetMembershipOptionsInXcodeSet supported platforms in file target membership options in XcodeConfigure a file target membership in Xcode to restrict it to specific platforms, avoiding the need for conditional compilation checks when the file contains platform-specific code.https://nilcoalescing.com/blog/SetSupportedPlatformsInFileTargetMembershipOptionsInXcodeMon, 25 Nov 2024 19:00:00 +1300Set supported platforms in file target membership options in Xcode

When we create a new file in Xcode, it's set to build for all platforms that the app supports by default. If the code in the file uses platform-specific APIs, such as NSDocumentController for macOS, the app won't compile for other platforms like iOS unless we wrap the platform-specific code in #if os(macOS) checks:

import SwiftUI

#if os(macOS)
struct MacDocumentList: View {
    
    var body: some View {
        List(
            NSDocumentController.shared.recentDocumentURLs,
            id: \.self
        ) { url in
            Text(url.absoluteString)
        }
    }
}
#endif

Alternatively, we can edit the file’s target membership options in Xcode and restrict the file to a specific platform. For instance, we can configure the file to only build for macOS. To do this, we open the file inspector panel in Xcode, select the target membership, and click the pencil icon to edit. From there, we uncheck the "Any Supported Platform" checkbox, choose the platform the file should build for, and click save.

Screenshot of Xcode shoing file target membership options

This avoids wrapping the entire file in conditional compilation checks when its contents are specific to one platform.

Note that this feature is only available for files managed by Xcode and it won't work in a Swift package.


If you're an experienced Swift developer looking to learn advanced techniques, check out my book Swift Gems. It dives into topics like optimizing collections, handling strings, and mastering asynchronous programming - perfect for taking your Swift skills to the next level.

]]>
https://nilcoalescing.com/blog/LazyVarsInObservableClassesLazy vars in @Observable classes in SwiftLearn how to resolve the issue of using lazy variables in @Observable classes by annotating them with @ObservationIgnored.https://nilcoalescing.com/blog/LazyVarsInObservableClassesTue, 19 Nov 2024 19:00:00 +1300Lazy vars in @Observable classes in Swift

When migrating from the ObservableObject protocol to the @Observable macro, I encountered an issue with using a lazy variable in my observable class. In this post, I’ll explain the problem and show how I resolved it.

Here’s a simplified version of my class using ObservableObject:

class WalksViewModel: ObservableObject {
    let walks = [
        Walk(title: "Central Park Loop", difficulty: .easy),
        Walk(title: "Mountain Ridge Trail", difficulty: .hard),
        Walk(title: "Riverbank Stroll", difficulty: .medium)
    ]
    
    @Published var sortingIsOn = false
    
    lazy var sortedWalks: [Walk] = {
        walks.sorted(
            using: KeyPathComparator(\Walk.difficulty)
        )
    }()
}

This works as expected. However, after updating the class to use the @Observable macro, the following error appeared: 'lazy' cannot be used on a computed property.

@Observable
class WalksViewModel {
    let walks = [
        Walk(title: "Central Park Loop", difficulty: .easy),
        Walk(title: "Mountain Ridge Trail", difficulty: .hard),
        Walk(title: "Riverbank Stroll", difficulty: .medium)
    ]
    
    var sortingIsOn = false
    
    // Error: 'lazy' cannot be used on a computed property
    lazy var sortedWalks: [Walk] = {
        walks.sorted(
            using: KeyPathComparator(\Walk.difficulty)
        )
    }()
}

This happens because the @Observable macro automatically generates observation logic for all variables in the class, but it cannot synthesize the necessary observation code for a lazy property.

While searching for a solution, I found this discussion on Swift Forums. I learned that to use lazy variables in observable classes, we can annotate them with @ObservationIgnored. This tells the observation system to skip synthesizing observation logic for the property.

Here’s how my final solution looks:

@Observable
class WalksViewModel {
    let walks = [
        Walk(title: "Central Park Loop", difficulty: .easy),
        Walk(title: "Mountain Ridge Trail", difficulty: .hard),
        Walk(title: "Riverbank Stroll", difficulty: .medium)
    ]
    
    var sortingIsOn = false
    
    @ObservationIgnored
    lazy var sortedWalks: [Walk] = {
        walks.sorted(
            using: KeyPathComparator(\Walk.difficulty)
        )
    }()
}


If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/PreviewSwiftUIViewsWithBindingsPreview SwiftUI views with bindings using @PreviewableXcode 16 introduced the @Previewable macro, making it easier to preview SwiftUI views with bindings.https://nilcoalescing.com/blog/PreviewSwiftUIViewsWithBindingsWed, 13 Nov 2024 18:00:00 +1300Preview SwiftUI views with bindings using @Previewable

Xcode 16 introduced the Previewable macro, making it easier to preview SwiftUI views with bindings. By annotating dynamic properties like @State in a #Preview body with @Previewable, we can pass them as bindings to views directly.

Here is an example on how we can easily setup a fully interactive preview for a basic counter view that accepts a binding:

struct CounterView: View {
    @Binding var count: Int
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment count") {
                count += 1
            }
        }
    }
}

#Preview {
    @Previewable @State var count = 0
    CounterView(count: $count)
}

This example creates an interactive preview where the count value can be updated in real-time using the provided button.

Xcode preview showing count 3 and increment button

@Previewable is supported on iOS 17, macOS 14, and other platforms.

]]>
https://nilcoalescing.com/blog/IntegratingSwiftUIUpdate2024"Integrating SwiftUI into UIKit Apps" book updated for iOS 18 and Xcode 16"Integrating SwiftUI into UIKit Apps" by Natalia Panferova has been updated, featuring the latest tools like Observable, the Preview macro, and a new section on animating UIKit views with SwiftUI animations in iOS 18.https://nilcoalescing.com/blog/IntegratingSwiftUIUpdate2024Sat, 9 Nov 2024 14:00:00 +1300"Integrating SwiftUI into UIKit Apps" book updated for iOS 18 and Xcode 16

I’m excited to announce that my book Integrating SwiftUI into UIKit Apps has been updated to include the latest features from iOS 18 and Xcode 16. This guide is designed to help UIKit developers seamlessly integrate SwiftUI into their existing projects, blending the declarative power of SwiftUI with the flexibility of UIKit.

The book covers everything you need to know to bridge the two frameworks effectively, from embedding SwiftUI views and managing data flow to migrating entire projects to the SwiftUI app lifecycle. It also includes techniques for enhancing apps with modern SwiftUI components like widgets and Swift Charts while maintaining compatibility with your existing UIKit code.

The new edition brings exciting updates. Projects now use the @Observable macro, simplifying data sharing between UIKit and SwiftUI. The book also shows how to work with Xcode’s updated preview system, leveraging the #Preview macro to accelerate UI development and testing. Additionally, there’s a brand new subchapter on using SwiftUI animations to animate UIKit views, a capability introduced with iOS 18.

This book is ideal for developers who want to incrementally adopt SwiftUI without rewriting their entire app. Each chapter includes practical examples and sample projects to help you follow along and implement the techniques in your own work. Whether you’re adding a single SwiftUI view to a UIKit app or planning a full transition, this book has you covered.

If you’re ready to enhance your projects with the latest advancements in SwiftUI, the updated Integrating SwiftUI into UIKit Apps is here to guide you every step of the way. Grab your copy and start building today!

]]>
https://nilcoalescing.com/blog/FontModifiersInSwiftUIFont modifiers in SwiftUIFont modifiers in SwiftUI let us style text directly at the font level, offering precise control over typography in our apps.https://nilcoalescing.com/blog/FontModifiersInSwiftUIMon, 28 Oct 2024 18:00:00 +1300Font modifiers in SwiftUI

We are all familiar with view modifiers in SwiftUI, such as font(), background(), frame(), etc. We use them all the time to style and customize views in our apps. They are methods defined on the View type that return a different version of the original view. I previously wrote about text modifiers, which are methods applied directly to Text, returning a modified Text, that can be used inside text interpolation. In this post I'd like to explore font modifiers, which work similarly to view and text modifiers but are applied to the Font type, returning a modified font.

Here is an example of font modifiers in action. We apply bold(), monospaced() and smallCaps() to the largeTitle font to customize the way it looks.

Text("Hello, world!")
    .font(
        .largeTitle
            .bold()
            .monospaced()
            .smallCaps()
    )
iPhone screen displaying text in bold, monospaced and small caps

We can view the full list of font modifiers in SwiftUI Font documentation in the "Styling a font" section. We can see that they are not exactly the same as modifiers available on the Text type or methods for styling text on the View type.

For example, there is a font modifier for customizing leading, which lets us adjust the line spacing of a font and takes a Font.Leading enum as an argument. This modifier can't be applied to a Text or any other view, only to a Font.

VStack(spacing: 20) {
    Text(exampleText)
        .font(
            .largeTitle
                .leading(.tight)
        )
    Text(exampleText)
        .font(
            .largeTitle
                .leading(.loose)
        )
}
iPhone screen displaying text with tight leading and loose leading

Font modifiers are a great way to manage typography in SwiftUI, letting us style text directly at the font level. Using these modifiers thoughtfully can help us create a polished look that improves our app’s user experience.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/UserLevelStringSearchInSwiftUser-level string search in SwiftUse localizedStandardRange(of:) in Swift for flexible, locale-aware, case and accent-insensitive string searches, providing a user experience similar to system-wide searches.https://nilcoalescing.com/blog/UserLevelStringSearchInSwiftTue, 22 Oct 2024 16:00:00 +1300User-level string search in Swift

When implementing search functionality in Swift apps, it’s important to make sure it behaves in a way users expect. Typically, when users search within apps or the system, they expect loose matches that ignore case and accents. For this reason, instead of using strict checks like range(of:) or contains(), it’s better to use flexible, locale-aware methods.

In Swift, localizedStandardRange(of:) is the most appropriate method for performing user-level string searches, as it mirrors how searches are done generally in the system. It’s case-insensitive, diacritic-insensitive, and adjusts to the user’s locale settings, ensuring that results meet user expectations.

Here’s an example with German strings:

import Foundation

let germanStrings = ["STRASSE", "Straße", "straße"]
let searchStr = "strasse"

for str in germanStrings {
    if let rangeOfMatch = str.localizedStandardRange(of: searchStr) {
        print(str[rangeOfMatch])
    }
}

This will print STRASSE, Straße, and straße, as they are all considered equivalent in German.

Note that localizedStandardRange(of:) comes from the Foundation framework, so we'll need to import Foundation when using it.


To illustrate diacritic insensitivity, here’s a French example:

import Foundation

let frenchStrings = ["fête", "Fete", "FÊTE"]
let searchStr = "fete"

for str in frenchStrings {
    if let rangeOfMatch = str.localizedStandardRange(of: searchStr) {
        print(str[rangeOfMatch])
    }
}

This example will print all variants of "fête", illustrating that the search is accent-insensitive.

Use localizedStandardRange(of:) to provide users with a search experience that feels familiar and intuitive, just like in other system-wide searches.


If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/SwiftUIEnvironmentSwiftUI EnvironmentExplore different ways to work with the SwiftUI environment, including reading and setting values, creating custom environment keys, and using it to pass down actions and observable classes.https://nilcoalescing.com/blog/SwiftUIEnvironmentFri, 4 Oct 2024 16:00:00 +1300SwiftUI Environment

SwiftUI provides a powerful mechanism called the environment, allowing data to be shared across views in a view hierarchy. This is particularly useful when we need to pass data or configuration information from a parent view down to its children, even if they’re many levels deep. It enhances the flexibility of SwiftUI views and decouples the data source from its destination.

SwiftUI includes a predefined list of values stored in the EnvironmentValues struct. These values are populated by the framework based on system state and characteristics, user settings or sensible defaults. We can access these predefined values to adjust the appearance and behavior of custom SwiftUI views, and we can also override some of them to influence the behavior of built-in views. Additionally, we can define our own custom values by extending the EnvironmentValues struct.

In this post, we'll explore various ways to work with the SwiftUI environment, including reading and setting predefined values, creating custom environment keys, and using the environment to pass down actions and observable classes.

# Reading environment values

SwiftUI provides a convenient way to read values stored in the EnvironmentValues structure using the @Environment property wrapper.

For example, we can get the vertical size class of the user interface by specifying the verticalSizeClass key path. The value stored in this property is of type UserInterfaceSizeClass, and it informs our view about the amount of available vertical space based on the device type and orientation. SwiftUI automatically sets this value by considering factors like whether the device is an iPhone, iPad, or another screen size, and whether the device is in portrait or landscape mode.

struct ContentView: View {
    @Environment(\.verticalSizeClass)
    private var verticalSizeClass
    
    var body: some View {
        VStack {
            Text("Good morning!")
                .font(.largeTitle)
            if verticalSizeClass == .regular {
                Image("sunrise")
            }
        }
        .padding()
    }
}

In this example, we check the verticalSizeClass to determine whether the view has enough vertical space to display an additional image. If the size class is regular, the image will appear.

iPhone screen displaying a Good morning! message with a sunrise image in portrait orientation and a black background in landscape orientation

Reading environment values provided by SwiftUI helps us build flexible and adaptive UIs without hardcoding assumptions about the device or user preferences.

# Setting environment values

We can set or override some environment values using the environment() view modifier. This modifier allows us to define settings or configurations that automatically apply to a group of views. For example, we might use it to change text formatting, line limits, or layout behavior across multiple views. Note that not all environment values can be written to, some values are read-only and are managed by the system.

When we set an environment value, it affects all child views of the view where the modifier is applied, unless overridden later in the hierarchy.

struct ContentView: View {
    var body: some View {
        VStack(spacing: 30) {
            Text("Welcome to SwiftUI")
                .font(.title)
            Text("SwiftUI is a powerful UI framework.")
                .font(.body)
        }
        .padding()
        .multilineTextAlignment(.center)
        .environment(\.textCase, .uppercase)
    }
}

In this example, the textCase environment value is set to uppercase for all views within the VStack. As a result, both text views will display their content in uppercase letters. If needed, this setting can be overridden for specific child views by applying a different value at a lower level in the hierarchy.

iPhone screen displaying WELCOME TO SWIFTUI and SWIFTUI IS A POWERFUL UI FRAMEWORK in uppercase

# Custom environment values

SwiftUI lets us define custom environment values to share our own data across views in a flexible way. This allows us to inject values into the view hierarchy, so child views can access them without needing to pass properties manually. We can do this by extending the EnvironmentValues structure and defining a new property using the @Entry macro. Note that to be able to use @Entry, you'll need to build your apps with Xcode 16 or newer.

Imagine we are building an app where different views behave differently based on the user's role. We can create a custom environment value that holds the user role.

enum UserRole {
    case admin, regular, guest
}

extension EnvironmentValues {
    @Entry var userRole: UserRole = .guest
}

We can then set the user role on the application level and every view can read it from the environment.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.userRole, .admin)
        }
    }
}

struct ContentView: View {    
    var body: some View {
        UserRoleView()
    }
}

struct UserRoleView: View {
    @Environment(\.userRole)
    private var userRole
    
    var body: some View {
        Text("Current role: \(userRole)")
            .font(.title)
    }
}

Using custom environment values makes it easy to manage context-specific information without cluttering our view with manually passed properties.

# Environment-based view modifiers

SwiftUI offers dedicated methods on the View struct to set environment values, which make our code more readable and concise. Instead of using the environment() modifier directly, these dedicated methods provide a cleaner and more expressive way to set environment values.

For instance, rather than setting the textCase environment value manually by applying environment(\.textCase, .uppercase) to a view, we can use the built-in textCase() modifier. Under the hood, it still works exactly the same way and passes the value down the view hierarchy and sets it for all the Text views, unless it's overridden further down the stack.

struct ContentView: View {
    var body: some View {
        VStack(spacing: 30) {
            Text("Welcome to SwiftUI")
                .font(.title)
            Text("SwiftUI is a powerful UI framework.")
                .font(.body)
        }
        .padding()
        .multilineTextAlignment(.center)
        .textCase(.uppercase)
    }
}
iPhone screen displaying WELCOME TO SWIFTUI and SWIFTUI IS A POWERFUL UI FRAMEWORK in uppercase

To follow the same API design, we can define such view modifiers to set custom environment values as well.

extension View {
    func userRole(_ role: UserRole) -> some View {
        environment(\.userRole, role)
    }
}

This custom method encapsulates the process of setting the userRole environment value, making it easier to use in different parts of the app.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .userRole(.admin)
        }
    }
}

# Actions in SwiftUI environment

In addition to environment values, SwiftUI also injects certain actions into the environment, allowing views to perform specific tasks, such as opening a URL or dismissing a modal. These actions are defined as structs that behave like functions because they use Swift’s callAsFunction() feature. This makes them simple to use, as you can interact with them like regular functions, even though they are structs. You can learn more about this Swift feature in my book Swift Gems.

One commonly used action is the OpenURLAction, which allows us to open a URL in the default browser or another app. We can access it via the openURL environment value and call it with the URL we want to open.

struct ContentView: View {
    @Environment(\.openURL) private var openURL
    
    private let url = URL(string: "https://www.example.com")!
    
    var body: some View {
        Button("Check out our website") {
            openURL(url)
        }
    }
}

We can also override the default OpenURLAction in the environment if we need to customize its behavior. For example, we can create an action that processes the URL before it's opened, logs the link click, or navigates to a different screen in the app, instead of opening the link in a browser. This is particularly useful when we want to define custom behavior for links embedded within text views.

struct ContentView: View {
    var body: some View {
        Text("Check out [our policy](https://example.com) for more details.")
            .environment(\.openURL, OpenURLAction { url in
                handleURL(url)
                return .handled
            })
    }
    
    func handleURL(_ url: URL) {
        // handle URL here
        print("Link clicked: \(url.absoluteString)")
    }
}

In this example, we customize the openURL environment to call handleURL() whenever the link is clicked, allowing us to handle the URL in a specific way.

# Using environment for observable classes

With iOS 17 and the introduction of the Observation framework, SwiftUI’s environment has become even more versatile, allowing us to pass reference types marked with @Observable through the environment. Before iOS 17, the environment was primarily used for value types. If we needed to pass a class that conformed to ObservableObject, we had to use the dedicated environmentObject() modifier and access it with the @EnvironmentObject property wrapper. Now, with the enhanced environment support in iOS 17, we can use the environment() modifier to pass a reference type directly and read it using the @Environment property wrapper.

Here’s an example of how to pass an observable class through the environment:

import SwiftUI
import Observation

@Observable
class DataModel {
    var count = 0
}

@main
struct MyApp: App {
    @State
    private var dataModel = DataModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(dataModel)
        }
    }
}

struct ContentView: View {
    var body: some View {
        IncrementCountView()
            .padding()
    }
}

struct IncrementCountView: View {
    @Environment(DataModel.self)
    private var dataModel
    
    var body: some View {
        VStack(spacing: 10) {
            Button("Increment") {
                dataModel.count += 1
            }
            Text("Count: \(dataModel.count)")
        }
    }
}

In this example, we define a DataModel class that will be used across multiple views. We initialize our data model at the top level of the app and make it available to all child views using the environment() modifier. In IncrementCountView, we then access the shared DataModel instance using the @Environment property wrapper with the object type, instead of a key path.

This new approach simplifies passing reference types through the environment, making it easier to share data models across views in a clean and consistent way.


The SwiftUI environment is a powerful way to share data and configurations across our views. We can read predefined values from the EnvironmentValues struct to adjust our views or override them to customize built-in components. We can also define our custom values to avoid cluttering our code with extra properties and create additional view methods to make it easier to set those values in the hierarchy. The environment also gives us access to system actions, like opening URLs, and allows us to customize their behavior. Finally, starting from iOS 17, we can use these same mechanisms to pass down reference types, simplifying state management throughout the app.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/SortingArraysInSwiftUsingComparisonOperatorsSorting arrays in Swift using comparison operators as closuresSorting arrays in Swift can be made more concise and readable by using comparison operators as closures in the sorted(by:) method.https://nilcoalescing.com/blog/SortingArraysInSwiftUsingComparisonOperatorsFri, 30 Aug 2024 22:00:00 +1200Sorting arrays in Swift using comparison operators as closures

Sorting arrays in Swift can be made more concise and readable by using comparison operators as closures. Swift’s sorted(by:) method allows us to pass operators like < or > directly, leveraging their ability to compare elements.

For example:

let numbers = [3, 1, 4, 1, 5, 9, 2]

// [1, 1, 2, 3, 4, 5, 9]
let ascending = numbers.sorted(by: <)

// [9, 5, 4, 3, 2, 1, 1]
let descending = numbers.sorted(by: >)

Here, < and > act as shorthand for closures, making the code more expressive and easier to understand. This technique elegantly utilizes Swift’s functional programming capabilities.


You can learn more about Sorting arrays using comparison operators as closures and many other Swift techniques in my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/ObservableInSwiftUIUsing @Observable in SwiftUI viewsDiscover how to use the @Observable macro in SwiftUI and its advantages over ObservableObject, such as more efficient view updates and simplified code management.https://nilcoalescing.com/blog/ObservableInSwiftUIFri, 23 Aug 2024 19:00:00 +1200Using @Observable in SwiftUI views

With the release of iOS 17 last year, Apple introduced the Observation framework, providing a Swift-specific implementation of the observer design pattern. This framework allows us to use the @Observable macro in SwiftUI as a way to provide observable data to our views.

Switching from the older ObservableObject to the new @Observable macro can be a good choice. This new approach lets us use familiar tools like State and Environment, instead of their object-based equivalents, making our code simpler and easier to work with. It can also help improve performance by only updating views when the properties they rely on change.

As iOS 18 approaches and we might consider dropping support for versions below iOS 17 soon, I thought that now is a great time to explore how we can use Observation in our SwiftUI projects.

# Declare and initialize an Observable

To start using @Observable, we apply the macro to a class. This allows SwiftUI to observe changes in the class's properties and update the UI accordingly. Here's a simple example:

import SwiftUI
import Observation

@Observable
class DataModel {
    var count = 0
}

struct ContentView: View {
    @State private var dataModel = DataModel()
    
    var body: some View {
        VStack(spacing: 10) {
            Button("Increment") {
                dataModel.count += 1
            }
            Text("Count: \(dataModel.count)")
        }
        .padding()
    }
}

In this example, the count property updates whenever the button is pressed, and the text view immediately reflects the new count. This approach makes it easy to keep our UI in sync with our data.

A mobile screen displaying a button labeled Increment and a text below it showing Count: 3

# Share an Observable between a parent and a child view

If we have a more complex view hierarchy, we can define our observable data higher up and pass it down through the views. This allows different parts of the app to share the same data model without needing to manually synchronize changes.

struct ContentView: View {
    @State private var dataModel = DataModel()
    
    var body: some View {
        VStack(spacing: 10) {
            IncrementButton(dataModel: dataModel)
            Text("Count: \(dataModel.count)")
        }
        .padding()
    }
}

struct IncrementButton: View {
    var dataModel: DataModel
    
    var body: some View {
        Button("Increment") {
            dataModel.count += 1
        }
    }
}

# Pass an Observable through the environment

We can also initialize our data model at the top level of the app and make it available to all child views using the environment() modifier. This way, we can easily access our data model by its type anywhere in the app.

import SwiftUI
import Observation

@main
struct MyApp: App {
    @State private var dataModel = DataModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(dataModel)
        }
    }
}

struct ContentView: View {
    @Environment(DataModel.self) private var dataModel
    
    var body: some View {
        VStack(spacing: 10) {
            IncrementButton()
            Text("Count: \(dataModel.count)")
        }
        .padding()
    }
}

struct IncrementButton: View {
    @Environment(DataModel.self) private var dataModel
    
    var body: some View {
        Button("Increment") {
            dataModel.count += 1
        }
    }
}

# Create bindings to properties of an Observable

To create bindings to the mutable properties of an observable class, we need to mark it as bindable using the @Bindable property wrapper. This allows us to create bindings directly from the observable properties, which can then be passed down to child views.

struct ContentView: View {
    @Environment(DataModel.self) private var dataModel
    
    var body: some View {
        @Bindable var dataModel = dataModel
        
        VStack(spacing: 10) {
            IncrementButton(count: $dataModel.count)
            Text("Count: \(dataModel.count)")
        }
        .padding()
    }
}

struct IncrementButton: View {
    @Binding var count: Int
    
    var body: some View {
        Button("Increment") {
            count += 1
        }
    }
}

Note that in this example, it's important to place the @Bindable declaration at the top of the body. Placing it inside the VStack will cause a linker error Undefined symbol: unsafeMutableAddressor of self #1 : Observable.ContentView in Observable.ContentView.body.getter : some.

# @Observable vs ObservableObject

One of the biggest advantages of the new @Observable compared to the old ObservableObject is that views depending on it will only be re-rendered when the properties they read in their body change.

Here is an example of an @Observable with two mutable properties, count1 and count2:

import SwiftUI
import Observation

@Observable
class DataModel {
    var count1 = 0
    var count2 = 0
}

@main
struct MyApp: App {
    @State private var dataModel = DataModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(dataModel)
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack(spacing: 40) {
            Count1()
            Count2()
        }
        .padding()
    }
}

struct Count1: View {
    @Environment(DataModel.self) private var dataModel
    
    var body: some View {
        let _ = Self._printChanges()
        @Bindable var dataModel = dataModel
        VStack(spacing: 10) {
            IncrementButton(count: $dataModel.count1)
            Text("Count 1: \(dataModel.count1)")
        }
    }
}

struct Count2: View {
    @Environment(DataModel.self) private var dataModel
    
    var body: some View {
        let _ = Self._printChanges()
        @Bindable var dataModel = dataModel
        VStack(spacing: 10) {
            IncrementButton(count: $dataModel.count2)
            Text("Count 2: \(dataModel.count2)")
        }
    }
}

struct IncrementButton: View {
    @Binding var count: Int
    
    var body: some View {
        Button("Increment") {
            count += 1
        }
    }
}

If we have two different views to display the two count values, and each of the views only reads one value in the body, the view body will only be recalled when that particular count value changes. We put Self._printChanges() here for debugging purposes to see which view body is called. If we run the app and only increment the first count, we will only see Count1: @dependencies changed. printed in the Xcode console each time we increment it. The body of the Count2 view will not be called.

Mobile screen displaying two Increment buttons with Count 1: 5 and Count 2: 0 beneath them

Comparing this to using ObservableObject with @Published properties:

import SwiftUI

class DataModel: ObservableObject {
    @Published var count1 = 0
    @Published var count2 = 0
}

@main
struct MyApp: App {
    @StateObject private var dataModel = DataModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(dataModel)
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack(spacing: 40) {
            Count1()
            Count2()
        }
        .padding()
    }
}

struct Count1: View {
    @EnvironmentObject private var dataModel: DataModel
    
    var body: some View {
        let _ = Self._printChanges()
        VStack(spacing: 10) {
            IncrementButton(count: $dataModel.count1)
            Text("Count 1: \(dataModel.count1)")
        }
    }
}

struct Count2: View {
    @EnvironmentObject private var dataModel: DataModel
    
    var body: some View {
        let _ = Self._printChanges()
        VStack(spacing: 10) {
            IncrementButton(count: $dataModel.count2)
            Text("Count 2: \(dataModel.count2)")
        }
    }
}

struct IncrementButton: View {
    @Binding var count: Int
    
    var body: some View {
        Button("Increment") {
            count += 1
        }
    }
}

If we run this version of the app, we'll see both Count2: _dataModel changed. and Count1: _dataModel changed. printed in the console every time we increment a count. That's because with ObservableObject, if a view reads even one published property, it will be re-rendered when any published property changes. This often led to unnecessary re-renders in the past when using ObservableObject, causing views to update for no reason. With the new @Observable, we don't have this risk.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/RecursiveEnumsInSwiftRecursive enums in SwiftThis post explains how to use recursive enums in Swift, including the indirect keyword, to effectively model and manage complex, hierarchical data structures.https://nilcoalescing.com/blog/RecursiveEnumsInSwiftThu, 15 Aug 2024 19:00:00 +1200Recursive enums in Swift

In Swift, an enumeration (enum) is a type that groups related values together. Recursive enums take this concept further by allowing an enum to include instances of itself within its cases. This feature is particularly useful when modeling data that is nested or recursive in nature. By enabling a type to reference itself, recursive enums provide a straightforward and type-safe way to represent complex relationships, like those found in file systems or organizational structures.

To allow recursion within an enum, Swift uses the indirect keyword. This keyword instructs Swift to handle the enum's memory in a way that supports safe recursion, preventing issues that could arise from circular references.

Consider the task of modeling a file system where folders can contain both files and other folders. This hierarchical structure is a perfect example of where a recursive enum can be effectively used. Here’s how we can define such a system in Swift:

enum FileSystemItem {
    case file(name: String)
    case folder(name: String, items: [FileSystemItem])
    indirect case alias(name: String, to: FileSystemItem)
}

In this example, FileSystemItem has three cases. The file case represents a single file with a name. The folder case represents a directory that can contain an array of FileSystemItem instances, allowing it to hold files and other folders. The alias case, marked with indirect, represents a symbolic link or shortcut that references another FileSystemItem. The indirect keyword is necessary here because the alias case directly references another instance of the FileSystemItem enum.

Using this recursive enum, we can easily create a simple file system. Imagine we have two files that we want to place inside a "Documents" folder. Additionally, we want to create an alias to one of these files within a "Desktop" folder:

let imageFile = FileSystemItem.file(name: "photo.png")
let textFile = FileSystemItem.file(name: "notes.txt")

let documentsFolder = FileSystemItem.folder(
    name: "Documents",
    items: [imageFile, textFile]
)

let profileImageAlias = FileSystemItem.alias(name: "ProfileImage", to: imageFile)

let desktopFolder = FileSystemItem.folder(
    name: "Desktop",
    items: [documentsFolder, profileImageAlias]
)

In this setup, imageFile and textFile are individual files, while documentsFolder is a folder containing these files. The profileImageAlias is an alias pointing to the imageFile, and desktopFolder contains both the documentsFolder and the alias. This structure illustrates how recursive enums can be used to create a hierarchical, yet easily manageable file system with the added flexibility of aliases.

To count the total number of items (files, folders, and aliases) in a folder, including those in subfolders and through aliases, we can write a recursive function:

func countItems(in item: FileSystemItem) -> Int {
    switch item {
    case .file:
        return 1
    case .folder(_, let items):
        return items.map(countItems).reduce(0, +)
    case .alias(_, let to):
        return countItems(in: to)
    }
}

let totalItems = countItems(in: desktopFolder)
print("Total items: \(totalItems)")

This function works by returning 1 for each file, recursively counting the items within each folder, and handling aliases by counting the items they reference. The reduce() function sums these counts to provide the total number of items, regardless of how deeply nested they are or how many aliases exist.

The use of the indirect keyword in the alias case is essential for enabling recursion in enums like this. The indirect keyword is required when a case directly references the enum itself, as seen in the alias case (alias(name: String, to: FileSystemItem)). However, when the reference is through a collection type, such as an array in items: [FileSystemItem], the compiler does not require indirect because the array itself introduces the necessary level of indirection.

Recursive enums are a powerful feature in Swift that allow us to model complex, hierarchical data structures like file systems with clarity and precision. By understanding how and when to use the indirect keyword, we can leverage recursive enums to create robust and maintainable models for a variety of applications.


If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/RenderingQuadraticBezierCurvesWithMetalRendering quadratic Bézier curves with MetalA simple method for rendering quadratic Bézier curves on the GPU in Metal without pre-processing geometry.https://nilcoalescing.com/blog/RenderingQuadraticBezierCurvesWithMetalMon, 5 Aug 2024 20:00:00 +1200Rendering quadratic Bézier curves with Metal

When developing our app Exsto, we needed to add GPU rendering to the app. As the curves are drawn by users with their fingers or pencil, the strokes end up with many hundreds, if not thousands, of points with multiple self-intersecting sections. What we settled on is by no means the most optimal method, nor have we implemented it in the most optimal way. However, we do believe it is one of the simplest solutions we have come across while researching the topic.

The method we present is a partial implementation of the algorithm in Kokojima et al. 2006 paper. The reason it is not a full implementation of Kokojima's solution is that we have not adopted their more optimal multi-sampling method and have rather followed a simpler, more costly, brute-force approach, as the performance difference on Apple GPUs is likely minimal.

Since these curves can (and commonly do) have many self-intersecting loops with a mixture of convex and concave curve sections, most solutions end up with a complex pre-processing stage that builds a detailed (non-overlapping) geometry. Kokojima et al. trades off this complexity for extra GPU rendering cost.

The Kokojima et al. method consists of three distinct steps. The initial two steps are dedicated to setting up a stencil buffer, while the final step is responsible for shading the stenciled area.

# Rendering your first curve

This method can only be used to render a CGPath that is made of moveToPoint, addQuadCurveToPoint and addLineToPoint CGPathElements.

For this post we will be re-creating the steps needed to render this curve.

Final result

The curve has multiple self-intersecting segments and is a good example of the types of curves created in our app Exsto.

# The triangle fan

The first step requires creating a triangle fan about a pivot including all of the points on the curve (but not the control points). We can use any pivot point, so for simplicity, we use the first point of the curve.

Triangle fan geometry

This is rendered into the stencil buffer using the invert operation. The stencil starts out with a value of 0, so only areas with an odd number of overlapping triangles end up set within the stencil.

// Invert for back facing primitives
depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = .invert
depthStencilDescriptor.backFaceStencil.depthStencilPassOperation = .invert

// Do not skip any primitives
depthStencilDescriptor.backFaceStencil.stencilCompareFunction = .always
depthStencilDescriptor.frontFaceStencil.stencilCompareFunction = .always
depthStencilDescriptor.frontFaceStencil.readMask = 0
depthStencilDescriptor.backFaceStencil.readMask = 0

We do not need to render any color values, so we do not attach a fragment function.

Stencil after drawing triangle fan

Notice how the upper left region of the stencil map has a chunk cut away. Looking back at the triangles in the geometry image, you can see the overlapping section that resulted in an even number of inversions on the stencil.

# Convex and concave curves

We render the curves by collecting all curve segments, including the control point, and creating a sequence of triangles with the control point as the second point in each triangle.

Curve geometry

We apply a fragment shader to discard pixels outside the quadratic curve. Here we further update the stencil using the same invert function. The fragment function does not set any color values, so it has a void return type.

For maximum GPU compatibility, we use UV coordinates set in the vertex stage. Metal interpolates these for each pixel of the fragment shader. To set these coordinates within your vertex function, set the start point on each triangle as [0, 0], the control point as [0.5, 0], and the final point on the curve as [1, 1].

vertex CurvedVertexOut curveVertexFunction(
    uint vertexId [[vertex_id]],
    uint vertexOffset [[base_vertex]],
    constant simd_float2 *vertices [[buffer(0)]],
    constant MTLSceneState *sceneStatePointer [[buffer(1)]]

) {
    CurvedVertexOut out;
    float2 pixelSpacePosition = vertices[vertexId].xy;
    MTLSceneState sceneState = MTLSceneState(*sceneStatePointer);
    
    out.position = position(pixelSpacePosition, sceneState);
    
    // Compute UV values for interpolation
    uint adjustedIndex = vertexId - vertexOffset;
    uint vertexNumber = adjustedIndex % 3;
    out.uv.x = half(vertexNumber) * 0.5h;
    out.uv.y = floor(out.uv.x);
    return out;
}

Within GPU shaders, it is of course important to minimize logic branches as much as possible. In our shader, we are indexing the points from a shared buffer using a vertexOffset. As each triangle has 3 vertices, taking the adjusted index modulo 3 will number the vertices for each triangle {0, 1, 2}. The first value in our UV vector can be computed by multiplying the vertex number by 0.5. To compute the second value, we round the first value down to the closest integer, resulting in our {0, 0}, {0.5, 0}, {1, 1} set of values without any branching.

fragment void curveFragment(CurvedVertexOut in [[ stage_in ]])
{
    float threshold = (in.uv.x * in.uv.x) - in.uv.y;
    if (threshold >= 0.0h) {
        discard_fragment();
    }
    return;
}

Here we are using the interpolated UV within the fragment shader to compute the quadratic u^2 - v for each pixel of each triangle. Only the non-discarded pixels invert the stencil. For a convex curve, this sets the stencil value. For a concave curve, where the triangle fan would have already set the stencil, this inverts the value back to 0.

Stencil after drawing curves

# Shading the path

With the stencil buffer now set, we render the two triangles that form the bounding box of the path, only rendering where the stencil is active. Additionally, we reset the stencil to 0 to prepare for the next path.

// Reset the stencil for any location drawn
depthStencilDescriptor.frontFaceStencil.depthStencilPassOperation = .zero
depthStencilDescriptor.backFaceStencil.depthStencilPassOperation = .zero

// Only pixels within the stencil
descriptor.backFaceStencil.stencilCompareFunction = .equal
descriptor.frontFaceStencil.stencilCompareFunction = .equal

To ensure that the GPU performs the stencil test before evaluating the final shading shader, we can prefix the fragment function with [[early_fragment_tests]].

# Multi sampling

For smooth edges, we use the native multi-sampling provided by Metal on Apple GPUs. This approach is simpler than the more complex solution proposed by Kokojima et al.

For the triangle fan the GPU automatically interpolates the edges of the triangles. For curves, we need to ensure that the fragment function is evaluated for each multi-sampled point. To achieve this, we set the UV coordinates that are being interpolated as [[sample_no_perspective]], implicitly forcing the fragment function to be run once for each sample rather than ones per pixel.

struct CurvedVertexOut
{
    float4 position [[position]];
    half2 uv [[sample_no_perspective]];
};

In the future, we plan on looking into using the fragment function return [[sample_mask]] value so that the quadratic is only evaluated once per pixel rather than once per sample, using the proximity to 0 as the indicator of the number of intersected samples.

]]>
https://nilcoalescing.com/blog/CountTheNumberOfObjectsThatPassATestInSwiftCount the number of objects that pass a test in Swift using count(where:)Efficiently count the number of elements in a sequence that satisfy the given condition with the new count(where:) method introduced in Swift 6.https://nilcoalescing.com/blog/CountTheNumberOfObjectsThatPassATestInSwiftFri, 2 Aug 2024 19:00:00 +1200Count the number of objects that pass a test in Swift using count(where:)

Swift continues to evolve, introducing new features that enhance both performance and code readability. One such addition is the count(where:) method, introduced in SE-0220. This method allows us to count elements in a sequence that satisfy a given condition, providing a more efficient and expressive way to achieve what previously required a combination of filter() and count.

The count(where:) method combines filtering a sequence and counting elements in a single step. It eliminates the need to create an intermediate array that would be discarded, enhancing performance and resulting in cleaner, more concise code.

Here's a simple example. Suppose we have an array of temperatures in Celsius and we want to count how many of them are above freezing (0°C):

let temperatures = [-5, 10, -2, 20, 25, -1]
let aboveFreezingCount = temperatures.count { $0 > 0 }

// Prints `3`
print(aboveFreezingCount)

In this case, aboveFreezingCount will be 3 since there are three temperatures (10, 20, and 25) that meet the condition.

# Counting elements with a specific prefix

Consider we have a list of products and we want to count how many of them start with "Apple":

let products = [
    "Apple", "Banana", "Apple Pie",
    "Cherry", "Apple Juice", "Blueberry"
]
let appleCount = products.count { $0.hasPrefix("Apple") }

// Prints `3`
print(appleCount)

Here, appleCount will be 3 because "Apple", "Apple Pie", and "Apple Juice" all start with "Apple".

# Counting elements based on length

Another common use case is counting elements based on their length. For instance, we might want to find out how many names in an array are shorter than six characters:

let names = ["Natalia", "Liam", "Emma", "Olivia", "Noah", "Ava"]
let shortNameCount = names.count { $0.count < 6 }

// Prints `4`
print(shortNameCount)

In this example, shortNameCount will be 4, as "Liam", "Emma", "Noah" and "Ava" are all shorter than six characters.

# Counting specific elements

If we need to count how many times a specific element appears in a sequence, we can use the equality operator (==) within the closure. For example:

let animals = ["cat", "dog", "cat", "bird", "cat", "dog"]
let catCount = animals.count { $0 == "cat" }

// Prints `3`
print(catCount)

Here, catCount will be 3 because "cat" appears three times in the array.


The count(where:) method is available to all types that conform to the Sequence protocol. This means we can use it not only with arrays but also with sets, dictionaries, and other sequence types. The sequence must be finite, ensuring that the method can complete in a reasonable amount of time.

The count(where:) method was introduced in Swift 6, which means you'll need Xcode 16 to use this feature. It's supported across various platforms and OS versions, including iOS 8.0+, macOS 10.10+, visionOS 1.0+ etc.


If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/CustomizingTheAppearanceOfSymbolImagesInSwiftUICustomizing the appearance of symbol images in SwiftUILearn how to adjust size, color, rendering modes, variable values, and design variants of SF Symbols in SwiftUI apps.https://nilcoalescing.com/blog/CustomizingTheAppearanceOfSymbolImagesInSwiftUIMon, 22 Jul 2024 19:00:00 +1200Customizing the appearance of symbol images in SwiftUI

Symbol images are vector-based icons from Apple's SF Symbols library, designed for use across Apple platforms. These scalable images adapt to different sizes and weights, ensuring consistent, high-quality icons throughout our apps. Using symbol images in SwiftUI is straightforward with the Image view and the system name of the desired symbol. Here's a quick example:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Image(systemName: "star")
    }
}
Screenshot of the star system image

There are various ways to customize the appearance of symbol images. Let's explore how to do that in SwiftUI.

# Size

Even though a symbol is placed inside an Image view, it should be treated more like text. To adjust the size of a symbol, we can apply the font() modifier, just like with a Text view. This allows us to align the size of the symbol with different text styles, ensuring visual consistency in our UI.

HStack {
    Image(systemName: "star")
        .font(.title)
    
    Image(systemName: "star")
        .font(.body)
    
    Image(systemName: "star")
        .font(.caption)
}
Screenshot of the star system image in three different fonts: title, body and caption

We can adjust the weight of the symbol using the fontWeight() modifier. This modifier changes the thickness of the symbol's strokes, allowing us to match or contrast the symbol with the surrounding text.

HStack {
    Image(systemName: "star")
        .fontWeight(.light)
    
    Image(systemName: "star")
        .fontWeight(.bold)
    
    Image(systemName: "star")
        .fontWeight(.black)
}
Screenshot of the star system image in three different weights: light, bold and black

To scale the image relative to its font size, we should use the imageScale() modifier. There are three options small, medium, and large, which scale the symbol proportionally based on its font size. If the font is not set explicitly, the symbol will inherit the font from the current environment.

HStack {
    Image(systemName: "star")
        .imageScale(.small)
    
    Image(systemName: "star")
        .imageScale(.medium)
    
    Image(systemName: "star")
        .imageScale(.large)
}
.font(.headline)
Screenshot of the star system image in three different scales: small, medium and large

It's not recommended to resize symbol images by applying the resizable() modifier and setting a frame, as we would with other images. When resizable() is used, the image stops being a symbol image, which can affect its layout and alignment with text.

# Color

Customizing the color of symbol images in SwiftUI is straightforward using the foregroundStyle() view modifier. This modifier allows us to set the color of the symbol image directly.

Image(systemName: "star")
    .foregroundStyle(.orange)
Screenshot of the star system image in orange color

The foregroundStyle() modifier can take any ShapeStyle, including gradients, which opens up a wide range of customization possibilities for our symbol images. In this example, the star symbol uses a LinearGradient with yellow and red colors, transitioning from the top to the bottom.

Image(systemName: "star")
    .foregroundStyle(
        LinearGradient(
            colors: [.yellow, .red],
            startPoint: .top,
            endPoint: .bottom
        )
    )
A glowing neon star icon with a gradient from yellow to red

# Rendering mode

We can customize the appearance of symbol images further by using different rendering modes. SF Symbols have four different rendering modes that change the symbol’s colors and appearance. Some rendering modes keep the entire icon the same color, while others allow for multiple colors.

To set the preferred rendering mode for a symbol image in SwiftUI, we use the symbolRenderingMode() modifier.

# Monochrome

Monochrome is the default rendering mode. In this mode, each layer of the symbol is the same color.

Image(systemName: "thermometer.snowflake")
    .symbolRenderingMode(.monochrome)
An icon depicting a thermometer with a snowflake symbol next to it

# Hierarchical

Hierarchical mode renders symbols as multiple layers, with different opacities applied to the foreground style. The hierarchy of the layers and their opacities are predefined within each symbol, but we can still customize the color of the layers using the foregroundStyle() modifier.

HStack {
    Image(systemName: "thermometer.snowflake")
    Image(systemName: "thermometer.snowflake")
        .foregroundStyle(.indigo)
}
.symbolRenderingMode(.hierarchical)

The symbolRenderingMode() modifier can be applied either directly to an Image view or set in the environment by applying it to a parent view containing multiple symbol images. This way, all symbol images inside the parent will be affected.

Two icons depicting thermometers with snowflakes next to them. The left icon has a white thermometer with a gray snowflake, while the right icon has a blue thermometer with a light blue snowflake.

# Palette

Palette mode allows symbols to be rendered with multiple layers, each layer having a different color. This mode is ideal for creating colorful, multi-layered icons.

Image(systemName: "thermometer.snowflake")
    .symbolRenderingMode(.palette)
    .foregroundStyle(.blue, .teal, .gray)

Interestingly, we don't need to explicitly specify the palette rendering mode. If we apply more than one style inside the foregroundStyle() modifier, palette mode will be activated automatically.

Image(systemName: "thermometer.snowflake")
    .foregroundStyle(.blue, .teal, .gray)
An icon depicting a thermometer with a blue bulb and a cyan snowflake next to it

If we specify only two colors for a symbol that defines three levels of hierarchy, the secondary and tertiary layers will use the same color.

Image(systemName: "thermometer.snowflake")
    .foregroundStyle(.blue, .gray)
An icon depicting a thermometer with a blue bulb and a gray snowflake next to it

# Multicolor

Multicolor mode renders symbols with their inherent styles, using a fixed set of colors defined by Apple. When using multicolor rendering, we cannot customize the colors of the symbol, it will use the predefined colors, which we can preview in the SF Symbols app. Even if we set a foregroundStyle() while the multicolor rendering mode is activated, the foreground style customization will be ignored.

HStack {
    Image(systemName: "thermometer.snowflake")
    Image(systemName: "thermometer.sun.fill")
        
}
.symbolRenderingMode(.multicolor)
Two icons depicting thermometers with weather symbols next to them. The left icon shows a thermometer with a blue bulb and a white snowflake. The right icon displays a thermometer with a red bulb and a yellow sun.

It's worth noting that since these colors are fixed, they don't adapt to light and dark mode. For example, our thermometer symbol has white outlines that will be invisible on a white background.

Not all symbols support every rendering mode. Symbols with fewer layers may look the same across modes, with hierarchical and palette modes appearing similar to monochrome.

# Variable value

When displaying a symbol image in SwiftUI, we can provide an optional value between 0.0 and 1.0 that the rendered image can use to customize its appearance. If the symbol doesn’t support variable values, this parameter has no effect. We should check in the SF Symbols app to determine which symbols support variable values.

HStack {
    Image(systemName: "speaker.wave.3", variableValue: 0)
    Image(systemName: "speaker.wave.3", variableValue: 0.3)
    Image(systemName: "speaker.wave.3", variableValue: 0.6)
    Image(systemName: "speaker.wave.3", variableValue: 0.9)
}
A series of five speaker icons with sound wave symbols next to them, representing increasing volume levels

With variable values, we can represent a characteristic that can change over time, like capacity or strength. This allows the symbol's appearance to change dynamically based on the state of our application.

struct ContentView: View {
    @State private var value = 0.5
    
    var body: some View {
        VStack {
            Image(
                systemName: "speaker.wave.3",
                variableValue: value
            )
            Slider(value: $value, in: 0...1)
                .padding()
        }
        .padding()
    }
}

In this example, the symbol speaker.wave.3 changes its appearance based on the value provided by the Slider.

A volume control interface showing a speaker icon with sound waves and a horizontal slider below it

We should use variable values to communicate changes in status, such as volume, battery level, or signal strength, providing users with a clear visual representation of dynamic states. To convey depth and visual hierarchy, we should use the hierarchical rendering mode, which elevates certain layers and distinguishes foreground and background elements within a symbol.

# Design variants

Symbols can come in different design variants, such as fill and slash, for example, to help communicate specific states and actions. The slash variant can indicate that an item or action is unavailable, while the fill variant can signify selection.

In SwiftUI, we can use the symbolVariant() modifier to apply these variants.

HStack {
    Image(systemName: "heart")
    
    Image(systemName: "heart")
        .symbolVariant(.slash)
    
    Image(systemName: "heart")
        .symbolVariant(.fill)
}
Three heart icons representing different states. The first icon is an outlined heart, the second icon is an outlined heart with a diagonal slash through it, and the third icon is a solid heart.

Additionally, some symbols can be enclosed within shapes such as circles, squares, or rectangles to enhance their visual context

HStack {
    Image(systemName: "heart")
        .symbolVariant(.circle)
    
    Image(systemName: "heart")
        .symbolVariant(.square)
    
    Image(systemName: "heart")
        .symbolVariant(.rectangle)
}
Three icons depicting hearts inside different shapes. The first icon shows a solid heart inside a circle, the second icon shows a solid heart inside a square, and the third icon shows a solid heart inside a rounded rectangle.

Different symbol variants serve various design purposes. The outline variant is effective in toolbars, navigation bars, and lists, where symbols are often displayed alongside text. Enclosing symbols in shapes like circles or squares can enhance legibility, especially at smaller sizes. The fill variant, with its solid areas, gives symbols more visual emphasis, making it suitable for iOS tab bars, swipe actions, and scenarios where an accent color indicates selection.

In many cases, the view displaying the symbol automatically chooses the appropriate variant. For example, an iOS tab bar typically uses the fill variant, while a navigation bar prefers the outline variant. This automatic selection ensures that symbols are used effectively in different contexts without needing explicit specification.


Enhancing symbol images in SwiftUI can significantly improve our app's look and feel. By adjusting size, color, rendering modes, variable values, and design variants, we can create icons that make our app more intuitive and visually appealing. SwiftUI makes these adjustments straightforward, enabling us to easily implement and refine these customizations for a better user experience.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/IntroducingStrollyIntroducing Strolly - our new app for fresh daily walksDiscover Strolly, our new free app that generates unique daily walking routes, providing variety and adventure while keeping user privacy in mind.https://nilcoalescing.com/blog/IntroducingStrollyWed, 3 Jul 2024 17:00:00 +1200Introducing Strolly - our new app for fresh daily walks

I'm really excited to share that Matthaus and I have just released our new app, Strolly: Generated Daily Walks. Strolly generates unique, random looping walks in your area, adding a fresh twist to your daily routine.


We built Strolly because we were tired of repeating the same walking routes with our dog. We wanted variety but didn’t have time to plan new routes daily. Strolly is perfect for people like us who live in walkable neighborhoods and enjoy daily strolls. It provides three random walk suggestions every day, eliminating the need for planning. Simply open the app, pick a walk you like, and enjoy a small adventure.

Screenshot of Strolly showing a generated path

Privacy was a top priority for us, especially since users often set their home as the starting point for walks. To ensure privacy, we opted to generate all paths locally on the user's device using Apple's MapKit framework. Our path generation process involves retrieving local points of interest with MKLocalSearch and building routes using MKDirections. We then create looping walks by combining these segments, carefully cleaning the routes to avoid overlapping and encourage loops.

Screenshot of Strolly showing the process of building paths


Our goal was to reduce cognitive load and let users quickly select a route. Standard system components didn’t meet our needs, so we created custom components. For example, our route cards respond to user gestures, allowing users to flick them away or pull a background card forward, making it more natural to navigate.

We also designed a custom control for configuring walk lengths, offering three options: short, medium, and long. Users can select "short", "medium", and "long" individually, or combinations like "short and medium", "medium and long", or all three. However, combinations like "short and long" are not allowed.

Screenshot of Strolly showing the settings sheet with a custom walk durations picker

Additionally, we developed minimal "settings" and "flag issues" buttons to keep the map content clear. Labels pop out for new users, then hide after a few seconds, adding a fun, elastic feel.


Strolly captures a large amount of data points for possible path segments. Initially, we used Apple's SwiftData framework, but it wasn’t optimal for large segments. So, we created a binary Codable format to efficiently encode data into files saved to disk. This format allows us to store, merge, and retrieve data efficiently, ensuring smooth performance.

We implemented our own UnsafeCodable protocol, which uses custom pointer types to manage memory efficiently. This approach ensures our data writes are stable and predictable, avoiding segmentation faults.


Strolly is totally free and doesn’t have any paid features at the moment. We wanted to have as many real users as possible to gather feedback and ensure it works well in different locations. So please try it out and share your feedback with us. We will work on improving the app and consider adding more advanced paid features later.

]]>
https://nilcoalescing.com/blog/WrappingTextWithinAnotherViewInSwiftUIWrapping text within another view in SwiftUIUsing the overlay() modifier in SwiftUI, we can elegantly wrap text within another view, ensuring the text is positioned and sized relative to the primary content.https://nilcoalescing.com/blog/WrappingTextWithinAnotherViewInSwiftUIThu, 27 Jun 2024 19:00:00 +1200Wrapping text within another view in SwiftUI

When designing user interfaces in SwiftUI, we might encounter situations where we need to wrap text within another view, such as an image or a shape. This can be effectively achieved using the overlay() modifier. Let's explore this concept with an example and compare it with using a ZStack.

Consider the following code snippet where we use the overlay() modifier to wrap the text "Hello, world!" within a clipboard image:

struct ContentView: View {
    @ScaledMetric private var imageSize = 140
    @ScaledMetric private var fontSize = 40
    
    var body: some View {
        Image(systemName: "clipboard")
            .imageScale(.large)
            .font(.system(size: imageSize))
            .overlay {
                Text("Hello, world!")
                    .font(.system(size: fontSize))
                    .multilineTextAlignment(.center)
            }
    }
}

In this example, the overlay() modifier places the Text view directly over the Image view, allowing the text to wrap within the image's bounds. The multilineTextAlignment() modifier ensures that the text is centered within the overlay. We use @ScaledMetric for image and font sizes to scale the UI based on user's preferred accessibility size.

Screenshot showing a clipboard image with Hello, world! text inside it

You might wonder why we don't use a ZStack for this purpose. Here's the same example using a ZStack:

struct ContentView: View {
    @ScaledMetric private var imageSize = 140
    @ScaledMetric private var fontSize = 40
    
    var body: some View {
        ZStack {
            Image(systemName: "clipboard")
                .imageScale(.large)
                .font(.system(size: imageSize))
            
            Text("Hello, world!")
                .font(.system(size: fontSize))
                .multilineTextAlignment(.center)
        }
    }
}

While the ZStack also places the text over the image, it positions the views independently based on the available space. This often leads to unwanted results, especially if the size of one view needs to depend on the size of another.

Screenshot showing a clipboard image with Hello, world! text on top of it but not wrapped


Using the overlay() modifier in SwiftUI allows us to create more cohesive and responsive designs where the size and position of one view can depend on another. This method is particularly useful when wrapping text within other views, ensuring that our layout behaves as expected.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/CustomScenesInSwiftUIDefining custom scenes in SwiftUISwiftUI custom scenes enable the creation of modular, maintainable code, allowing for precise management of complex user interfaces and behavior across different platforms.https://nilcoalescing.com/blog/CustomScenesInSwiftUIWed, 19 Jun 2024 19:00:00 +1200Defining custom scenes in SwiftUI

Scenes are a core concept in SwiftUI, representing containers for our app's user interface and behavior. They manage views, handle lifecycle events, and coordinate with the operating system. Understanding and leveraging scenes is crucial for organizing and maintaining complex applications.

SwiftUI provides several built-in scene types designed to cater to different needs and platforms. The most commonly used scene type is the WindowGroup, which defines the main window of an app and supports multiple windows on macOS and iPadOS. The DocumentGroup is ideal for document-based apps, providing built-in support for opening, editing, and saving documents. The Settings scene type is used to manage app settings, presenting a separate window on macOS. The Window scene creates a single unique window and is ideal for tools or utility panels.

One of the powerful features of SwiftUI is the ability to define custom scenes. Custom scenes allow us to create modular, maintainable code, making it easier to manage complex user interfaces.

To create a custom scene, we need to define a new struct that conforms to the Scene protocol. Here's an example of a custom scene that uses the Window type, which is available only on macOS:

#if os(macOS)
struct PanelScene: Scene {
    var body: some Scene {
        Window("Panel", id: "panel") {
            PanelView()
        }
    }
}
#endif

In this example, the PanelScene struct defines a scene that creates a window titled "Panel" and displays the PanelView within it. The #if os(macOS) compiler directive ensures that this code only compiles for macOS, making it suitable for multi-platform apps.

We can further customize the scene by observing the current phase using the scenePhase value from the environment. This allows us to perform actions based on the scene's state changes:

#if os(macOS)
struct PanelScene: Scene {
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        Window("Panel", id: "panel") {
            PanelView()
        }
        .onChange(of: scenePhase) { _, newPhase in
            switch newPhase {
            case .active:
                print("Panel window is active")
            case .inactive:
                print("Panel window is inactive")
            default:
                break
            }
        }
    }
}
#endif

Once we have defined our custom scene, we can integrate it into the App struct's body. SwiftUI allows composing multiple scenes to create rich and versatile applications. Here’s how we can add the custom scene to the app:

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        
        #if os(macOS)
        PanelScene()
        #endif
    }
}

Scenes in SwiftUI offer a flexible and powerful way to structure and manage an app's user interface and behavior. By defining custom scenes, we can create modular and maintainable code tailored to specific use cases and platforms. This approach enhances our ability to build complex, multi-platform applications efficiently.

]]>
https://nilcoalescing.com/blog/EnhancedReplaceTransitionForSFSymbolsInIOS18Enhanced replace transition for SF Symbols in iOS 18Leverage the new magic replace symbol effect in iOS 18 for smooth transitions of slashes and badges in SF Symbols.https://nilcoalescing.com/blog/EnhancedReplaceTransitionForSFSymbolsInIOS18Thu, 13 Jun 2024 13:00:00 +1200Enhanced replace transition for SF Symbols in iOS 18

iOS 18 introduces a new option for the ReplaceSymbolEffect called MagicReplace. This feature allows for smooth animations of slashes and badges in SF Symbols, enhancing the visual transitions in our apps.

The MagicReplace option is automatically applied to the replace symbol effect when possible, and it works specifically with related SF Symbols. This feature is particularly useful for tappable elements in our apps, such as various toggles.

Let's see how it works with an example of a notification toggle.

Button {
    withAnimation {
        notificationsEnabled.toggle()
    }
} label: {
    Label(
        "Toggle notifications",
        systemImage: notificationsEnabled ? "bell" : "bell.slash"
    )
}
.contentTransition(
    .symbolEffect(.replace)
)

In iOS 18 beta, this results in a smooth magic replace animation on the bell icon when the slash is added or removed.

A gif a smooth animation for adding and removing a slash on a bell icon when it's tapped

While the new magic option is applied by default, we can also specify it explicitly along with a preferred fallback option.

.contentTransition(
    .symbolEffect(.replace.magic(fallback: .replace))
)

This new feature allows for more polished and visually appealing interactions in our apps, enhancing the overall user experience.

]]>
https://nilcoalescing.com/blog/FormSheetInSwiftUISwiftUI sheet sizing updates on iPadOS 18Sheet sizing became more flexible in SwiftUI on iPadOS 18, with the default size now matching a form sheet and presentationSizing() allowing explicit size customizations.https://nilcoalescing.com/blog/FormSheetInSwiftUIWed, 12 Jun 2024 13:00:00 +1200SwiftUI sheet sizing updates on iPadOS 18

For a while, presenting a form sheet in SwiftUI, equivalent to the UIModalPresentationStyle.formSheet, was a challenge. With iPadOS 18, SwiftUI introduced more flexibility, making it easier to control sheet sizing on iPad.

On iPadOS 17 and earlier, sheets in a regular size class were always presented as large sheets (also known as page sheets), with no built-in way to adjust their size in SwiftUI, unless we wrapped UIKit or implemented a fully custom modal presentation.

Starting with iPadOS 18, the default sheet size became smaller, aligning with the traditional form sheet style. Additionally, the presentationSizing(_:) modifier was introduced, allowing us to explicitly define a sheet’s size.

For example, specifying .form ensures a compact, centered appearance, independent of future default behavior changes:

.sheet(isPresented: $showSheet) {
    Text("Sheet content...")
        .presentationSizing(.form)
}
Screenshot of a form sheet on iPad simulator

We can also enforce a page sheet size for informational content, fit the sheet to its content, or define fully custom sizing logic.

These updates make sheet presentation more adaptable, giving us greater control over how modals appear in SwiftUI. For more details, see the PresentationSizing documentation.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/RespondingToModifierKeysResponding to keyboard modifiers on macOS in SwiftUITake advantage of the new macOS 15 API to update SwiftUI views when modifier keys are pressed.https://nilcoalescing.com/blog/RespondingToModifierKeysTue, 11 Jun 2024 18:00:00 +1200Responding to keyboard modifiers on macOS in SwiftUI

New in macOS 15, we can now use onModifierKeysChanged(mask:initial:_:) to update our views when keyboard modifiers are held down.

It's common to reveal additional details and advanced settings when the user holds down the option key in a Mac app. Here is how we can use the new API to show a button only while the option modifier is pressed.

struct ContentView: View {
    @State var optionPressed = false
    
    var body: some View {
        VStack(alignment: .trailing) {
            Toggle("Show emoji", isOn: .constant(false))
            Spacer()
            if optionPressed {
                Button("Advanced") {
                    /// TODO: open advance settings sheet
                }
            }
        }
        .onModifierKeysChanged(mask: .option) { old, new in
            optionPressed = new.contains(.option)
        }
    }
}

The new onModifierKeysChanged() method is only available on the latest macOS, so if you need to respond to modifier keys on iPadOS or older macOS versions, check out my other post Reading keyboard modifiers on iPad in SwiftUI.

]]>
https://nilcoalescing.com/blog/GradientOnPolylinesInSwiftUIMapKitCreating gradient on polylines in SwiftUI MapKitLearn how to use MapKit and SwiftUI to apply a gradient that follows a polyline, enhancing the visual appeal of your maps.https://nilcoalescing.com/blog/GradientOnPolylinesInSwiftUIMapKitWed, 5 Jun 2024 18:00:00 +1200Creating gradient on polylines in SwiftUI MapKit

When using MapKit and SwiftUI, Xcode will autocomplete to suggest you use linearGradient(_:startPoint:endPoint:options:) to style a MapPolyline. The linearGradient() method requires you to provide a start and end point in the screen space. However, most of the time, if you are reaching for a gradient, you want it to follow the line along the map.

By passing a Gradient directly to the stroke() modifier, you can avoid needing to provide a screen space startPoint and endPoint, allowing the gradient to follow the line on the map.

MapPolyline(
    coordinates: coordinates
)
.stroke(
    Gradient(colors: [.red, .indigo])
)

This approach simplifies the implementation and creates a more visually appealing effect.

]]>
https://nilcoalescing.com/blog/CompareArraysBasedOnCustomCriteriaCompare arrays based on custom criteriaWhen we need to compare arrays based on custom criteria in Swift, we can use elementsEqual(_:by:) method. It allows us to define custom comparison logic with a closure, offering more flexibility than using == operator.https://nilcoalescing.com/blog/CompareArraysBasedOnCustomCriteriaMon, 3 Jun 2024 18:00:00 +1200Compare arrays based on custom criteria

When we need to compare arrays based on custom criteria in Swift, we can use elementsEqual(_:by:) method. This method allows us to define how two arrays should be compared, offering more flexibility than using == operator with arrays of Equatable elements.

Here's a practical example:

struct Employee {
    let name: String
    let age: Int
    let role: String
}

let employees1 = [
    Employee(name: "Alice", age: 30, role: "Engineer"),
    Employee(name: "Bob", age: 25, role: "Designer")
]

let employees2 = [
    Employee(name: "Charlie", age: 28, role: "Engineer"),
    Employee(name: "Dave", age: 32, role: "Designer")
]

// Custom comparison logic: compare based on role only
let areRolesEqual = employees1.elementsEqual(employees2) {
    $0.role == $1.role
}
print(areRolesEqual) // true

In this example, we compare two lists of employees based solely on their roles, ignoring names and ages. This is particularly useful for scenarios where specific comparison logic is required temporarily or for a particular use case.

Using elementsEqual(_:by:), we can define custom comparison logic with a closure, making it easy to switch criteria without modifying the Employee struct itself. This method is ideal for single-use scenarios where conforming array elements to Equatable might be unnecessary or inappropriate.

For more advanced Swift tips and techniques, check out my book, Swift Gems. It covers a wide range of topics, including optimizing collections, handling strings, mastering asynchronous programming, and debugging - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/ScenesTypesInASwiftUIMacAppScenes types in a SwiftUI Mac appDiscover how to leverage SwiftUI's versatile scene types, like WindowGroup, DocumentGroup, Settings, Window, and MenuBarExtra, to create efficient and dynamic macOS applications.https://nilcoalescing.com/blog/ScenesTypesInASwiftUIMacAppTue, 28 May 2024 19:00:00 +1200Scenes types in a SwiftUI Mac app

Creating versatile and efficient macOS applications with SwiftUI involves understanding how to use various scene types. Scenes in SwiftUI manage and display their contents through windows, with different scene types offering distinct behaviors and capabilities. In this post, we'll explore the different scene types available in SwiftUI for macOS, including WindowGroup, DocumentGroup, Settings, Window, and MenuBarExtra.

# WindowGroup

The WindowGroup scene is a key tool for developing data-driven applications in SwiftUI. It allows for the creation of multiple instances of a scene, with each instance represented by its own window. This capability is especially useful for applications that need to present different views independently.

On macOS, WindowGroup offers several unique advantages. Users can organize open windows into a tabbed interface, enhancing usability by allowing better organization and quick access to multiple windows within the same application. Additionally, WindowGroup on macOS includes built-in commands for standard window management tasks, such as minimizing and closing windows.

Each window in a WindowGroup maintains its own state. The system allocates separate storage for any State variables instantiated by the scene’s view hierarchy for each window. This ensures that each window operates with its own unique data and behavior, allowing for highly dynamic and interactive applications.

Here’s an example of how to use WindowGroup in a SwiftUI macOS app:

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Screenshot of a Mac app that shows two windows with a different navigation state

# DocumentGroup

The DocumentGroup scene in SwiftUI is specifically designed for applications that work with document-based data. It manages the lifecycle of documents, including their creation, opening, and saving, making it well-suited for file-centric apps.

To set up a DocumentGroup scene, we need to provide a document model and a view that can display and edit the document. SwiftUI leverages this model to integrate document handling features into our app. On macOS, this integration includes menu support for managing documents, such as opening recent files, creating new ones, and handling multiple documents at the same time.

The DocumentGroup scene dynamically updates its contents whenever the document's configuration changes. This behavior ensures that the app remains consistent with the current document data.

Here’s an example of how to implement DocumentGroup in SwiftUI:

@main
struct MyDocumentApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: MyDocument()) { file in
            DocumentView(document: file.$document)
        }
    }
}

struct MyDocument: FileDocument {
    ...
}

struct DocumentView: View {
    @Binding var document: MyDocument
    
    ...
}
Screenshot of a Mac app that shows two text documents

# Settings

The Settings scene provides an interface for representing settings values on macOS. This scene is ideal for creating a dedicated settings window that users can access to configure the application’s preferences.

When we pass a view to the Settings scene in the App declaration, SwiftUI automatically enables the app’s "Settings" menu item. This integration ensures that SwiftUI manages the display and removal of the settings view when the user selects the "Settings" option from the application menu or uses the corresponding keyboard shortcut.

Here's an example usage of the Settings scene:

@main
struct SettingsExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        Settings {
            SettingsView()
        }
    }
}

struct SettingsView: View {
    @AppStorage("username")
    private var username: String = "User"
    
    @AppStorage("notificationsEnabled")
    private var notificationsEnabled: Bool = true

    ...
}
Screenshot of a Mac app that shows an example Settings window

# Window

The Window scene type creates a single unique window. This is particularly useful when we need a specific window that doesn’t follow the typical grouping behavior of WindowGroup.

Unlike WindowGroup, which manages multiple windows of the same type and automatically handles their creation and presentation, Window is intended for scenarios where a single, distinct window is required. This is ideal for tools or utility panels that need to be accessible from multiple places within our application but should only have one instance open at any given time. If an app uses a single window as its main scene, the app quits when the window is closed.

Here’s an example of how to define a unique window in a macOS SwiftUI app using the Window scene type:

@main
struct SingleWindowExample: App {
    var body: some Scene {
        Window("My Unique Window", id: "uniqueWindow") {
            UniqueWindowView()
        }
    }
}
Screenshot of a Mac app that shows a single window with sample text

The MenuBarExtra scene renders as a persistent control in the macOS system menu bar, making it ideal for utilities or controls that need to be readily available. It provides users with quick and easy access to essential app functionality without opening the main app window.

MenuBarExtra offers a seamless way to integrate additional features directly into the menu bar, perfect for apps that deliver system-level utilities, frequently used functions, or status monitoring.

In addition to its basic functionality, MenuBarExtra offers customization through styles, allowing developers to tailor the behavior and appearance of the menu bar item to better fit their application's needs.

Here’s an example of how to define a MenuBarExtra scene with the window style:

@main
struct MenuBarExampleApp: App {
    var body: some Scene {
        MenuBarExtra("My Menu Bar Extra", systemImage: "gear") {
            MenuBarView()
        }
        .menuBarExtraStyle(.window)
    }
}
Screenshot of a Mac app that shows a menu bar extra window

# Composing scene types

One of the powerful features of SwiftUI is the ability to compose different scene types together within an app. This allows for creating complex and feature-rich applications by leveraging multiple scenes.

@main
struct MyCompositeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        Settings {
            SettingsView()
        }
        MenuBarExtra("My Menu Bar Extra", systemImage: "gear") {
            MenuBarView()
        }
    }
}

In this example, we combine WindowGroup, Settings, and MenuBarExtra to build a comprehensive application with multiple windows, a settings panel, and a menu bar utility.

SwiftUI provides a variety of scene types to suit different application needs on macOS. Whether we are building a data-driven app, a document-based app, or a utility accessible from the menu bar, understanding and utilizing these scene types effectively can help us create powerful and flexible applications.


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/AutoConvertJsonSnakeCaseToSwiftCamelCasePropertiesAuto-convert JSON snake case to Swift camel case propertiesUse convertFromSnakeCase in JSONDecoder to automatically map JSON snake case keys to Swift camel case properties, simplifying model definitions and ensuring naming consistency.https://nilcoalescing.com/blog/AutoConvertJsonSnakeCaseToSwiftCamelCasePropertiesMon, 20 May 2024 16:00:00 +1200Auto-convert JSON snake case to Swift camel case properties

Snake case is a naming convention where words are separated by underscores and presented in lowercase, such as user_name or account_id. This format is commonly found in JSON responses from APIs. In contrast, Swift typically uses camel case for naming properties, where the first word is lowercase and each subsequent word starts with an uppercase letter, like userName or accountId. When working with JSON data, we often encounter a mismatch between the JSON's snake case keys and Swift's camel case property names.

Fortunately, we can address this challenge with the convertFromSnakeCase decoding strategy, a feature of the JSONDecoder class from the Foundation framework. This strategy automatically converts snake case names from JSON into camel case properties in our Swift models, eliminating the need for manual mapping of JSON keys to model properties.

Consider the following JSON object and corresponding Swift model:

{
  "user_id": 123,
  "user_name": "JohnDoe"
}
struct User: Codable {
    var userId: Int
    var userName: String
}

Here is how we can use the convertFromSnakeCase strategy to decode the data:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

// User(userId: 123, userName: "JohnDoe")
let user = try? decoder.decode(User.self, from: data)

By setting the keyDecodingStrategy of JSONDecoder to .convertFromSnakeCase, we instruct the decoder to automatically map user_id to userId and user_name to userName during decoding. We don’t need to write custom decoding logic or use coding keys for each property. This keeps our model definitions cleaner and more consistent with Swift's naming conventions.

Using this strategy simplifies the process of working with JSON data in Swift and ensures our code remains clean and maintainable.

If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/StartingAndGrowingYourOwnTechnicalBlogStarting and growing your own technical blogI recently gave a talk at Code Camp Wellington on how starting a technical blog can transform your career development. Since it wasn't recorded, I've summarized my insights in this post to inspire others to share their technical knowledge online.https://nilcoalescing.com/blog/StartingAndGrowingYourOwnTechnicalBlogSat, 18 May 2024 18:00:00 +1200Starting and growing your own technical blog

Starting and growing your own technical blog can be a transformative experience for both personal and professional development. In my recent talk at the Code Camp Wellington conference, I discussed how launching a technical blog can help you expand your expertise, contribute to the tech community, and create new career opportunities. Since my talk wasn't recorded, I wanted to share my insights here to inspire more people to start sharing their technical knowledge online.

My blog on nilcoalescing.com began four years ago and has grown into a popular platform where I share my expertise in iOS development and other tech topics. With over 100 posts and an average of 3,000 daily readers, it's become a significant part of my career.

Starting my blog in early 2020 opened many doors, from podcast invitations to workshops and even a job at Apple on the SwiftUI team. Although I paused blogging during my time at Apple, I resumed in 2022 and continued to grow my audience. The positive feedback from the community motivated me to publish my books, Swift Gems and Integrating SwiftUI into UIKit Apps, aimed at helping developers enhance their Swift skills and smoothly integrate SwiftUI into existing UIKit projects.

Blogging has brought me numerous opportunities and personal growth. I hope to encourage you to start or enhance your own technical blog with the insights and tips I'll share.

# Benefits of sharing knowledge online

One of the most significant benefits of maintaining a technical blog is the improvement of your writing and communication skills. As software developers, we often communicate through written mediums such as Slack messages, emails, code comments, and documentation. By writing blog posts, you learn to convey complex ideas clearly and concisely, which is a valuable skill in any professional setting. This practice can help you become more effective in your daily work, making your communication with colleagues and stakeholders more precise and understandable.

Additionally, blogging forces you to deepen your understanding of the topics you write about. The research and experimentation required to produce high-quality content solidify your knowledge and make you more proficient in your field.

Blogging offers more than just personal gains, it allows you to contribute to the broader tech community. By sharing your insights and solutions, you enrich the wealth of free resources available to other developers. We are fortunate to have many excellent free resources in the iOS space, and I am proud to contribute to this collective knowledge.

Compilation of the blogs I read regularly, such as Hacking with Swift, SwiftLee, Swift with Majid and others

The sense of giving back is incredibly rewarding. I often receive messages from readers thanking me for my posts and sharing how my content has helped them solve problems or learn something new. These interactions make me feel connected to a global community of developers. Knowing that my work has a positive impact keeps me motivated to continue blogging.

Writing a blog also opens up career opportunities that you might not have anticipated. When you make your work visible online, you attract attention from potential employers and collaborators. For example, my blog led to invitations to speak at conferences, lead workshops, and even join the SwiftUI team at Apple. These opportunities might not have come my way if I hadn't shared my knowledge through my blog.

# Choosing the right blogging platform

To get started with your own blog, the first step is to choose a blogging platform that suits your needs and preferences. Platforms like Medium offer a straightforward setup with a built-in audience but limited customization options. Content management systems like WordPress or Squarespace provide more customization but may involve additional costs. For those comfortable with coding, static site generators like Publish, which I use for my blog, offer high performance and flexibility.

When choosing a platform, consider its accessibility support. Ensure that your content is accessible to as many people as possible by adding image descriptions, enabling keyboard navigation, and testing the contrast and readability of your text. Additionally, make sure your website adapts to different screen sizes, particularly for mobile devices, and supports both dark and light modes.

For a technical blog, displaying code snippets with proper syntax highlighting is crucial. Check if the platform supports your preferred programming languages or allows you to integrate additional libraries for syntax highlighting.

Also, consider how your target audience will find your blog and whether the platform provides tools for analytics to monitor your blog’s performance.

# Generating content ideas

Once you are ready to start writing, you may find it difficult to come up with ideas, at least initially. One effective strategy is to write about something you’ve recently learned. This could be a specific topic like “Exporting and importing localizations in a SwiftUI app” or a more general one like “Lessons from my first year in cybersecurity.” Documenting recent projects, the challenges you faced, and how you overcame them can also provide valuable content. For example, you might write about “How to build a chat app with WebSocket.”

Staying up-to-date with recent developments in your field and covering new technologies and APIs as they are released can attract a lot of readers. My blog posts about new Apple APIs during the WWDC season always bring a lot of traffic to my blog. People are eager to see new features in action, even if they can’t immediately use them.

Another way to generate content ideas is to address common problems faced by others. When I was starting my blog, I would browse Stack Overflow for highly upvoted questions without detailed answers and then write blog posts providing solutions. I would then share a description and a link to my post in the Stack Overflow replies. This strategy was highly effective in attracting readers.

You can also write reviews and comparisons of tools and technologies you use, such as comparing AWS Lambda and Microsoft Azure Functions. Don’t feel pressured to always write long articles; sometimes, short tips with code snippets can be just as valuable.

# Structuring your blog posts

From my personal experience, there are three types of content that work great for a technical blog: short tips, regular articles, and series of articles. Each of these types can be effective depending on the topic you are trying to cover and the level of detail your audience expects. Having all of these types represented on your blog can attract readers with different preferences.

Short tips are a great way to get started and are usually very popular. When I write a short tip, I typically provide a few sentences describing the problem and a solution in the form of a code snippet that others can easily copy and integrate. It can be helpful to also provide a link to documentation or another post covering the subject in more detail for those who are interested. This type of content is popular among developers because we often want a quick solution to a problem we are having at work and don’t always have lots of time to read long explanations. You can see some examples of short tips under the tips section of my blog.

If you have an idea and the time to explore it in detail, then writing a traditional article could be a good choice. There is a specific structure that works well for articles, such as an introduction, subsections, and a summary. It’s important that the introduction describes the purpose of the article well so that readers can understand immediately if the article is going to be useful for them. Subsections with headers make it easier to quickly scan the page and jump to the right part of the blog post if needed. The summary can include the key points covered in the post and maybe a link to a sample project that readers can download and run if applicable. I recommend putting as many code samples and screenshots throughout your blog post as possible to make it easier for people to understand what you are explaining.

If you would like to cover a complex topic with a detailed explanation, then you might consider writing a series of articles. Breaking the topic down into digestible pieces of information can be more effective than writing a single, extra-long blog post. When writing a series of posts, I recommend structuring them in a similar way and providing links to all the posts in the series in one place. Having internal links in your blog can also be good for search engine optimization.

# Promoting your content

After publishing your posts, promoting them effectively is crucial for attracting readers. Social media can be a powerful tool for this. Set up your profile with a picture and a brief description of what you do, making you appear more approachable and engaging to potential readers. Although it can be intimidating to put yourself out there, the positive feedback you receive can boost your confidence. I started sharing my posts anonymously but eventually switched to using my real name after receiving encouraging feedback. You can check out my X (Twitter), Mastodon and LinkedIn profiles to see how I share my new blog posts.

Posting on multiple platforms like X, Mastodon, and LinkedIn can help you reach a diverse audience. Sharing your articles in relevant LinkedIn groups can also increase visibility. The timing of your posts can impact engagement, so experiment with different times to see what works best for your audience.

Ensure that your social media posts display correctly by using appropriate meta tags for social media banners. Visual elements like code samples or screenshots can make your posts more attractive. Using relevant hashtags can also help your posts get discovered by a broader audience. Engaging with comments and answering questions can provide valuable feedback and foster a sense of community around your blog. While it's important to interact with your readers, try to maintain a sustainable level of engagement to avoid burnout. Focus on meaningful interactions and prioritize comments that add value to the discussion or require clarification, ensuring you stay motivated and connected without feeling overwhelmed.

Another effective strategy is to get your content included in technical newsletters. Subscribe to as many newsletters in your field as possible to understand what kind of content is typically included. Reach out to newsletter authors and share a link to your blog and RSS feed. Some newsletters accept submissions through GitHub, so look for open-source newsletters where you can submit your links.

After publishing your posts and promoting them, consider using tools like Google Analytics to monitor their performance. Analyzing page views, and traffic sources can help you tailor your content to better meet your readers’ needs and interests.

# Exploring monetization strategies

As your blog grows in popularity, you can start thinking about monetization strategies. One straightforward way to earn some income is by asking your readers for donations. Setting up a sponsorship page on GitHub and including a link to it in your articles can generate some contributions. For instance, I have a sponsorship page and at the end of each post on my blog I mention, “If you like our blog, please consider supporting us on GitHub.” While this approach doesn't bring in a significant amount of money, it does result in some contributions and even a few monthly subscribers, which motivates me to keep publishing articles.

Promoting your own products, such as books or apps, within your articles can be highly effective. Including banners or links to your products can drive sales, especially when your articles attract significant traffic. You can also advertise freelance or consulting services you offer, turning your blog into a platform for showcasing your expertise.

Another viable option is offering sponsorship slots on your website. You can charge a flat rate to promote relevant products or services to your audience. This method works well if your blog has a dedicated readership and can attract companies looking to reach your specific audience. By carefully selecting sponsors that align with your content, you can maintain the trust of your readers while generating income.

As your blog continues to grow, these monetization strategies can help you turn your passion for writing into a sustainable source of income, allowing you to invest more time and resources into creating valuable content for your readers.

# Recap and final thoughts

Having your own technical blog can bring great personal benefits. Sharing your knowledge online can help you improve your writing skills, force you to understand the topics you write about on a deeper level, and serve as a repository of knowledge you can refer to in the future.

Blogging can also benefit the technical community by increasing the number of freely available resources and providing unique perspectives that can help others learn. It can help you foster connections within the industry and stay in touch with developers worldwide.

Making your work visible can attract exciting career opportunities and get you headhunted for roles you might not have considered.

To get started, choose a blogging platform you’re comfortable with that provides a good user experience for your target audience.

Share your content in various formats, including short tips, traditional articles, and series of posts. Promote your content on social media and reach out to newsletter authors to increase visibility. When you’re ready, explore monetization strategies to turn your blog into a potential source of side income.

I hope my story and insights inspire you to start or enhance your own technical blog and share your knowledge with the world.

]]>
https://nilcoalescing.com/blog/ReactToNetworkStatusUpdatesInSwiftUIReact to network status updates in SwiftUILearn how to use NWPathMonitor as an async sequence for real-time network status updates in your SwiftUI views.https://nilcoalescing.com/blog/ReactToNetworkStatusUpdatesInSwiftUIThu, 16 May 2024 18:00:00 +1200React to network status updates in SwiftUI

In this short post we'll look into how to easily manage network status updates in SwiftUI by using the NWPathMonitor as an async sequence. This method integrates seamlessly with your views for efficient updates.

import Network

struct ContentView: View {
    @State private var isNetworkAvailable: Bool = false

    var body: some View {
        VStack {
            if isNetworkAvailable {
                Text("Network is Available")
            } else {
                Text("Network is Unavailable")
            }
        }
        .task {
            for await path in NWPathMonitor() {
                isNetworkAvailable = path.status == .satisfied
            }
        }
    }
}

Here we are enumerating over the async sequence of network path updates provided by NWPathMonitor(). The view then displays the current network status based on this state. Additionally, the view’s task() modifier ensures that the async sequence is automatically canceled when the view disappears, safely cleaning up resources.

]]>
https://nilcoalescing.com/blog/MultiCriteriaDataSortingWithTuplesStreamline multi-criteria data sorting and organization with tuplesSwift's tuples are a convenient way to group multiple values, and their true utility shines with the language's built-in ability to compare tuples.https://nilcoalescing.com/blog/MultiCriteriaDataSortingWithTuplesWed, 15 May 2024 13:00:00 +1200Streamline multi-criteria data sorting and organization with tuples

Swift's tuples are a convenient way to group multiple values, and their true utility shines with the language's built-in ability to compare tuples. Swift allows tuples to be compared directly, right out of the box, under two conditions: the tuples must contain the same number of elements, and the corresponding elements must be of comparable types. This built-in comparison feature evaluates tuples element by element, in order.

The comparison starts with the first elements of each tuple. If these are unequal, the comparison is determined based on these elements. If the first elements are equal, the comparison proceeds to the second elements, and so on. This continues until a pair of unequal elements is found. The comparison stops at the first pair of unequal elements, determining the lesser or greater tuple. If all elements are equal, the tuples themselves are considered equal.

This feature is invaluable for sorting and organizing data based on multiple criteria, offering a clean and efficient approach.

Imagine an inventory where each product is represented by a tuple containing the product's name and price. Using Swift's tuple comparison, we can sort this inventory by price and, in case of a tie, by product name.

var inventory = [
    ("Apple", 1.20),
    ("Banana", 0.75),
    ("Carrot", 1.20),
    ("Date", 0.75)
]

inventory.sort {
    ($0.1, $0.0) < ($1.1, $1.0)
}

$0 and $1 represent any two tuples from the inventory array. The tuples are first compared by price (.1), and if the prices are equal, the comparison moves to the product names (.0).

The sorted inventory array is now neatly organized. Products are sorted by price, and in the case of a tie, alphabetically by name.

[
    ("Banana", 0.75),
    ("Date", 0.75),
    ("Apple", 1.20),
    ("Carrot", 1.20)
]

Swift's built-in tuple comparison feature is a straightforward and effective tool for data organization, particularly useful in multi-criteria sorting scenarios. As demonstrated with the inventory example, it allows for clean and efficient sorting logic, making code more readable and maintainable.

As someone who has worked extensively with Swift, I've gathered many insights over the years and compiled them in my book, Swift Gems. The book focuses exclusively on the Swift language and Swift Standard Library, offering over 100 advanced tips and techniques on topics such as optimizing collections, leveraging generics, asynchronous programming, and debugging. Each tip is designed to help intermediate and advanced Swift developers write clearer, faster, and more maintainable code by fully utilizing Swift's built-in capabilities.

]]>
https://nilcoalescing.com/blog/SwiftGemsReleaseAnnouncementExplore advanced Swift techniques in the latest book "Swift Gems""Swift Gems" is a new book by Natalia Panferova for seasoned Swift developers. It offers over 100 tips and techniques to improve code efficiency and robustness, ideal for those looking to advance their Swift expertise.https://nilcoalescing.com/blog/SwiftGemsReleaseAnnouncementSat, 11 May 2024 14:00:00 +1200Explore advanced Swift techniques in the latest book "Swift Gems"

I'm excited to share that I have just released my new book - Swift Gems. This book is a deep dive into the advanced realms of Swift programming, tailored for developers who are ready to elevate their skills beyond the basics.

"Swift Gems" is packed with over 100 tips and techniques that I've gathered and tested over the years. The book is designed to help you tackle complex programming challenges, enhance the readability and efficiency of your code, and master sophisticated programming paradigms such as asynchronous operations and error handling.

You'll discover new ways to utilize protocols and generics for creating flexible and reusable components, delve into expert strategies for string manipulation and collection optimization, and gain insights into advanced debugging methods that keep your code clean and maintainable.

If you're passionate about Swift and looking to push your coding capabilities further, "Swift Gems" is crafted just for you. I hope it inspires you to create even more powerful and elegant applications.

Grab your copy of Swift Gems and let's explore the advanced techniques together.

Looking forward to hearing how it helps you in your projects!

]]>
https://nilcoalescing.com/blog/TrialNotificationsWithProvisionalAuthorizationOnIOSSending trial notifications with provisional authorization on iOSBy taking advantage of provisional authorization for notifications, we can provide a gentle introduction to our app's notifications without upfront permission from the user.https://nilcoalescing.com/blog/TrialNotificationsWithProvisionalAuthorizationOnIOSTue, 19 Mar 2024 19:00:00 +1300Sending trial notifications with provisional authorization on iOS

In the world of iOS app development, engaging users effectively while respecting their preferences is crucial. Notifications can be a powerful tool for keeping users informed and engaged, but the challenge often lies in obtaining permission to send these notifications. Apple's User Notifications framework offers a solution that strikes a balance between engagement and user consent: provisional notifications. This feature allows apps to send notifications without upfront permission, providing a gentle introduction to our app's notifications.

Provisional authorization allows apps to send notifications silently to the Notification Center, bypassing the lock screen, banners, and sounds. This is an excellent way to showcase the value of our notifications without being intrusive.

# Requesting provisional authorization

To request provisional authorization, we'll need to use the same method as we'd use for the full authorization requestAuthorization(options:completionHandler:) on UNUserNotificationCenter, but we'll need to add the provisional option.

let center = UNUserNotificationCenter.current()
do {
    try await center.requestAuthorization(
        options: [.alert, .sound, .badge, .provisional]
    )
} catch {
    print("Error requesting notification authorization: \(error)")
}

This code won't trigger a dialog prompting the user to allow notifications like it would do when requesting full authorization. It will silently grant our app notification permissions when first called. Since it won't be disruptive for the user, we don't have to wait for the right time to request authorization, and can do it right on app launch.

import UIKit
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [
            UIApplication.LaunchOptionsKey: Any
        ]?
    ) -> Bool {
        Task {
            let center = UNUserNotificationCenter.current()
            let authorizationStatus = await center
                .notificationSettings().authorizationStatus
                
            if authorizationStatus != .authorized ||
                authorizationStatus != .provisional {
                do {
                    try await center.requestAuthorization(
                        options: [.alert, .sound, .badge, .provisional]
                    )
                } catch {
                    print("Error requesting notification authorization: \(error)")
                }
            }
        }
        return true
    }
}

# Scheduling a notification

To demonstrate the value of our app's notifications, we can start targeting the user with local or remote notifications. We will schedule a local one as an example here, but you can check out my previous post iOS app setup for remote push notifications if you want to trial remote push notifications instead.

Here's an example of scheduling a local notification that will trigger 10 seconds after being set up, perfect for testing the provisional notifications flow:

class AppDelegate: NSObject, UIApplicationDelegate {
    func scheduleTestNotification() {
        let content = UNMutableNotificationContent()
        content.title = "Discover something new!"
        content.body = "Tap to explore a feature you haven't tried yet."
        
        let trigger = UNTimeIntervalNotificationTrigger(
            timeInterval: 10,
            repeats: false
        )
        let request = UNNotificationRequest(
            identifier: UUID().uuidString,
            content: content,
            trigger: trigger
        )
        
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Error scheduling notification: \(error)")
            }
        }
    }

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [
            UIApplication.LaunchOptionsKey: Any
        ]?
    ) -> Bool {
        Task {
            ...
                
            if authorizationStatus != .authorized ||
                settings.authorizationStatus != .provisional {
                ...
            }
            
            scheduleTestNotification()
        }
        return true
    }
}

Since we just have provisional authorization at this point, we will only see our notification in the Notification Center's history.

Screenshot showing a notification in the Notification Center

# Encouraging full authorization

After users have experienced the value of our notifications through the provisional ones, we might want to encourage them to opt for full notification permissions. This can be done by explaining the benefits of enabling notifications through an in-app message or alert, ideally at a moment when the user is most likely to appreciate the value of full notifications.

struct EnableNotificationsView: View {
    var body: some View {
        VStack {
            Text("Get the most out of our app!")
            Text("Enable notification banners and sounds to stay up-to-date with everything our app has to offer.")
            Button("Go to settings") {
                openAppSettings()
            }
        }
        .padding()
        .multilineTextAlignment(.center)
    }
    
    func openAppSettings() {
        guard let url = URL(
            string: UIApplication.openSettingsURLString
        ) else {
            return
        }
        
        UIApplication.shared.open(url)
    }
}

This view serves as a gentle reminder of the benefits of full notifications, making it easy for users to act immediately by taking them to the settings page. By explaining the value and making the process straightforward, we are more likely to convert them, ensuring our users remain active and informed about our app's offerings.

Implementing provisional notifications in our app is a great way to engage users. By following the guidelines provided by Apple, we can create a non-intrusive notification experience that respects user preferences while showcasing the value of staying connected. We should also remember, that with all types of notifications it's important to be informative, timely, and respectful of the user's choices.

]]>
https://nilcoalescing.com/blog/UserFriendlyDescriptionsAndRecoverySuggestionsForCustomErrorsInSwiftUser-friendly descriptions and recovery suggestions for custom errors in SwiftGuide users through resolving issues and make your Swift applications more intuitive and supportive by defining user-friendly descriptions and recovery suggestions for custom errors.https://nilcoalescing.com/blog/UserFriendlyDescriptionsAndRecoverySuggestionsForCustomErrorsInSwiftSun, 18 Feb 2024 22:00:00 +1300User-friendly descriptions and recovery suggestions for custom errors in Swift

Creating a seamless user experience in Swift involves not just managing errors but also communicating them effectively. By defining user-friendly descriptions and recovery suggestions for custom errors, we can guide users through resolving issues, making our applications more intuitive and supportive.

We can begin by categorizing our custom errors using an enum. Then we should make the enum conform to the LocalizedError protocol to enrich the errors with detailed, localized descriptions and actionable recovery suggestions.

import Foundation

enum NetworkError: Error {
    case disconnected
    case timeout
}

extension NetworkError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .disconnected:
            return "Unable to connect to the network."
        case .timeout:
            return "The request timed out."
        }
    }

    var recoverySuggestion: String? {
        switch self {
        case .disconnected:
            return "Please check your internet connection and try again."
        case .timeout:
            return "Please check your network speed or try again later."
        }
    }
}

When our application encounters an error, we can handle it gracefully by providing the user with a clear understanding of what went wrong and how they can fix it.

do {
    // Some network operation that might throw
} catch let error as NetworkError {
    print(error.localizedDescription)
    if let suggestion = error.recoverySuggestion {
        print("Suggestion: \(suggestion)")
    }
} catch {
    print("An unexpected error occurred: \(error.localizedDescription)")
}

By defining user-friendly descriptions and recovery suggestions for custom errors, we can transform technical setbacks into helpful guidance. This approach not only enhances the user experience but also empowers users to resolve issues proactively, making our Swift applications more robust and user-centric.

]]>
https://nilcoalescing.com/blog/UseCasesForSelfInSwiftUse cases for self, Self and Self.self in SwiftExplore the practical applications and distinctions of self, Self, and Self.self in Swift, clarifying their roles in instance referencing, protocol conformance, and metatype access.https://nilcoalescing.com/blog/UseCasesForSelfInSwiftSun, 28 Jan 2024 15:00:00 +1300Use cases for self, Self and Self.self in Swift

The Swift language constructs self, Self, and Self.self can sometimes be a source of confusion, even for experienced developers. It's not uncommon to pause and recall what each is referring to in different contexts. In this post I aim to provide some straightforward examples that clarify the distinct roles and uses of these three constructs. Whether it's managing instance references, adhering to protocol conformance, or accessing metatype information, understanding these concepts is key to harnessing the full potential of Swift in our projects.

# self - instance reference

self refers to the instance of the type within its own instance methods. It's a way to access the instance's properties and methods from within its own scope. This is particularly useful for differentiating between instance properties and method parameters when they share the same name.

class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
}

# Self - type reference in protocols

Self with a capital "S" refers to the type that conforms to a protocol, allowing for polymorphic behavior. It enables protocols to specify requirements that are then tailored to the conforming type, a feature that's especially powerful in the context of protocol-oriented design.

protocol Duplicatable {
    func duplicate() -> Self
}

In the Duplicatable protocol, Self is used to specify that the duplicate() method should return an instance of the conforming type. The exact type is not specified in the protocol itself, but will be determined by the type that conforms to the protocol. It allows each conforming type to have a clear contract: if you conform to Duplicatable, you must implement a duplicate() method that returns an instance of your own type.

# Self.self - metatype reference

Self.self is used to refer to the metatype of the type, essentially the type of the type itself. It's commonly used in static methods or when we need to access type-level properties or pass the type itself as a parameter.

protocol Registrable {
    static func register()
}

extension Registrable {
    static func register() {
        print("Registering \(Self.self)")
    }
}

class Service: Registrable {}

// Prints `Registering Service`
Service.register()

In this example, Self.self is used to access the metatype of the conforming type (Service) within a static method, allowing for type-level operations.

Understanding and correctly utilizing self, Self, and Self.self are fundamental in harnessing the full potential of Swift's type system and protocol-oriented programming capabilities. Each serves a specific purpose, enabling more expressive, flexible, and maintainable code.

As someone who has worked extensively with Swift, I've gathered many insights over the years and compiled them in my book, Swift Gems. The book focuses exclusively on the Swift language and Swift Standard Library, offering over 100 advanced tips and techniques on topics such as optimizing collections, leveraging generics, asynchronous programming, and debugging. Each tip is designed to help intermediate and advanced Swift developers write clearer, faster, and more maintainable code by fully utilizing Swift's built-in capabilities.

]]>
https://nilcoalescing.com/blog/CaseInsensitiveStringComparisonInSwiftCase insensitive string comparison in SwiftDiscover how string comparison methods from Foundation outperform basic case conversion, ensuring precise, efficient, and culturally aware comparisons in our applications.https://nilcoalescing.com/blog/CaseInsensitiveStringComparisonInSwiftSun, 21 Jan 2024 15:00:00 +1300Case insensitive string comparison in Swift

String comparison is a fundamental aspect of many programming tasks, from sorting and searching to data validation. The simplest method to compare two strings ignoring case we might think of is converting them to a common case and using ==. However, this approach has significant limitations.

Languages and locales have specific rules for case mappings, making simple case conversion unreliable. Let's look at an example in German.

let string1 = "Fußball"
let string2 = "FUSSBALL"

// Prints `false`
print(string1.lowercased() == string2.lowercased())

The lowercase of ß in German is not equivalent to ss, leading to a false negative in comparison.

Also note that using lowercased() creates new string instances, which can be a performance bottleneck. And simple case conversion does not respect locale-specific sorting rules, leading to lists that might appear incorrectly ordered to users.

Fortunately, the Foundation framework offers more nuanced methods for string comparison.

For non-user-facing comparisons we can use the compare(_:options:) method or any of its overloads. It allows us to request specific comparison behaviors like case-insensitive, diacritic-insensitive, or numeric comparison.

let result = string1.compare(string2, options: [.caseInsensitive, .diacriticInsensitive])
if result == .orderedSame {
    print("The strings are considered equal.")
}

For user-facing, locale-aware comparison, we should use compare(_:options:range:locale:) or localizedStandardCompare(_:).

localizedStandardCompare(_:) can come in handy when sorting strings that will be displayed to users, like filenames, titles, or any list that requires natural sorting order. It will compare strings in a way that aligns with human expectations, even considering numeric values embedded in strings.

let filenames = ["File10.txt", "file2.txt", "FILE1.txt"]
let sortedFilenames = filenames.sorted(
    by: { $0.localizedStandardCompare($1) == .orderedAscending }
)

// Prints `["FILE1.txt", "file2.txt", "File10.txt"]`
print(sortedFilenames)

By selecting the most suitable method for our specific situation, we can ensure that our string comparisons are not only correct but also efficient and considerate of the user's cultural context. This careful approach can greatly improve the user experience and the overall reliability of our applications.

If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/PatternMatchingForCustomTypesInSwiftPattern matching for custom types in SwiftDefine custom logic for matching different types of data in switch cases by overloading the Swift pattern matching operator (~=).https://nilcoalescing.com/blog/PatternMatchingForCustomTypesInSwiftFri, 22 Dec 2023 19:00:00 +1300Pattern matching for custom types in Swift

Pattern matching in Swift is a technique that allows us to check and de-structure data in a concise way. It's most often seen in switch statements, where it can match against a variety of patterns.

An expression pattern is used in a switch case to represent the value of an expression. The magic behind matching these patterns lies in the ~= operator, which Swift uses under the hood to determine if the pattern matches the value. By default, ~= compares two values of the same type using the == operator, but we can overload it to provide custom behavior.

Let's consider a custom type Circle and demonstrate how to implement custom pattern matching for it.

First, we define a simple Circle struct with a radius.

struct Circle {
    var radius: Double
}

let myCircle = Circle(radius: 5)

Now, let's overload the ~= operator to match a Circle with a specific radius.

func ~= (pattern: Double, value: Circle) -> Bool {
    return value.radius == pattern
}

This overload allows us to use a Double in a switch statement case to match against a Circle's radius.

switch myCircle {
case 5:
    print("Circle with a radius of 5")
case 10:
    print("Circle with a radius of 10")
default:
    print("Circle with a different radius")
}

We can add as many overloads as we need. For example, we can define custom logic to check whether the Circle's radius falls within a specified range.

func ~= (pattern: ClosedRange<Double>, value: Circle) -> Bool {
    return pattern.contains(value.radius)
}

The switch statement can now match myCircle against Double values and ranges, thanks to our custom implementations of the ~= operator.

switch myCircle {
case 0:
    print("Radius is 0, it's a point!")
case 1...10:
    print("Small circle with a radius between 1 and 10")
default:
    print("Circle with a different radius")
}

Custom pattern matching in Swift opens up a lot of possibilities for handling complex types more elegantly. By overloading the ~= operator, we can tailor the pattern matching process to suit our custom types. As with any powerful tool, we should use it wisely to enhance our code without compromising on readability.

As someone who has worked extensively with Swift, I've gathered many insights over the years and compiled them in my book, Swift Gems. The book focuses exclusively on the Swift language and Swift Standard Library, offering over 100 advanced tips and techniques on topics such as optimizing collections, leveraging generics, asynchronous programming, and debugging. Each tip is designed to help intermediate and advanced Swift developers write clearer, faster, and more maintainable code by fully utilizing Swift's built-in capabilities.

]]>
https://nilcoalescing.com/blog/TriggerPropertyObserversFromInitializersInSwiftTrigger property observers from initializers in SwiftProperty observers like willSet and didSet aren't triggered during initialization in Swift, but if we need to execute logic from property observers at this stage, we can use some workarounds.https://nilcoalescing.com/blog/TriggerPropertyObserversFromInitializersInSwiftSun, 17 Dec 2023 18:00:00 +1300Trigger property observers from initializers in Swift

In Swift, property observers such as willSet and didSet are not called when a property is set in an initializer. This is by design, as the initializer's purpose is to set up the initial state of an object, and during this phase, the object is not yet fully initialized. However, if we need to perform some actions similar to what we'd do in property observers during initialization, there are some workarounds.

# Set properties after initialization

One approach is to set the properties after the object has been initialized.

class MyClass {
    var myProperty: String {
        willSet {
            print("Will set myProperty to \(newValue)")
        }
        didSet {
            print("Did set myProperty to \(myProperty), previously \(oldValue)")
        }
    }

    init() {
        myProperty = "Initial value"
    }
}

let myObject = MyClass()
myObject.myProperty = "New value"

In this example, the property observers will not trigger during the initial assignment in the initializer but will trigger on subsequent property changes.

# Separate property setup method

Another approach is to use a separate method to set up the property.

class MyClass {
    var myProperty: String {
        willSet {
            print("Will set myProperty to \(newValue)")
        }
        didSet {
            print("Did set myProperty to \(myProperty), previously \(oldValue)")
        }
    }
    
    init(value: String) {
        myProperty = "Initial value"
        setupPropertyValue(value: value)
    }
    
    private func setupPropertyValue(value: String) {
        myProperty = value
    }
}

let myObject = MyClass(value: "New value")

This approach ensures that the property observers are triggered during the setup phase after the initialization of the object is completed.

# Create a defer closure

An alternative approach involves using a defer block within the initializer.

class MyClass {
    var myProperty: String {
        willSet {
            print("Will set myProperty to \(newValue)")
        }
        didSet {
            print("Did set myProperty to \(myProperty), previously \(oldValue)")
        }
    }

    init(value: String) {
        defer { myProperty = value } 
        myProperty = "Initial value"
    }
}

let myObject = MyClass(value: "New value")

The defer block ensures that the didSet logic is called after the initial value is set.

# Manually trigger side effects

If we need to perform some specific actions during initialization, we can also manually call the same methods or code blocks that we would have called in our observers.

class MyClass {
    var myProperty: String {
        willSet {
            propertyWillChange(newValue)
        }
        didSet {
            propertyDidChange(oldValue)
        }
    }

    init(value: String) {
        myProperty = value
        propertyDidChange(nil)
    }

    private func propertyWillChange(_ newValue: String) {
        print("Will set myProperty to \(newValue)")
    }

    private func propertyDidChange(_ oldValue: String?) {
        print("Did set myProperty to \(myProperty), previously \(oldValue)")
    }
}

let myObject = MyClass(value: "New value")

In this example, the propertyDidChange() method is manually called within the initializer to simulate the didSet observer.


While property observers don't fire during initialization, these workarounds offer alternative ways to achieve similar behavior. We choose the approach that best fits our use case and coding style.

As someone who has worked extensively with Swift, I've gathered many insights over the years and compiled them in my book, Swift Gems. The book focuses exclusively on the Swift language and Swift Standard Library, offering over 100 advanced tips and techniques on topics such as optimizing collections, leveraging generics, asynchronous programming, and debugging. Each tip is designed to help intermediate and advanced Swift developers write clearer, faster, and more maintainable code by fully utilizing Swift's built-in capabilities.

]]>
https://nilcoalescing.com/blog/AsyncStreamFromWithObservationTrackingFuncCreate an AsyncStream from withObservationTracking() functionLearn how to wrap withObservationTracking() into an AsyncStream to iterate over changes with an async for loop.https://nilcoalescing.com/blog/AsyncStreamFromWithObservationTrackingFuncSat, 25 Nov 2023 18:00:00 +1300Create an AsyncStream from withObservationTracking() function

After Natalia wrote her previous post on how to implement the observer pattern in Swift in iOS 17 Using Observation framework outside of SwiftUI, I've been wondering if it would be possible to wrap observation into an AsyncStream. That would let me use an asynchronous for loop to iterate over changes. In this post I will share how I implemented it.

I used the same User class from Natalia's post marked with @Observable macro.

@Observable class User {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

Then I defined a function that returns an AsyncStream. The stream is created from the results of the apply closure of withObservationTracking() function.

func observationTrackingStream<T>(
    _ apply: @escaping () -> T
) -> AsyncStream<T> {
    AsyncStream { continuation in
        @Sendable func observe() {
            let result = withObservationTracking {
                apply()
            } onChange: {
                DispatchQueue.main.async {
                    observe()
                }
            }
            continuation.yield(result)
        }
        observe()
    }
}

I wanted the first iteration of the loop to include the current value, so I called the yield() method on the result returned from withObservationTracking(). When the observed values change, I schedule a new observe() function call that will read the value and yield it to the AsyncStream.

Note that since the onChange callback of withObservationTracking() is called before the property changes on the thread that is making the change, I use the async dispatch to ensure that we read the value after the property has changed. However, if changes are being made to these properties from other dispatch queues, I would need to adjust my code here to ensure that the observe() function is called after the property value changes.

To use my observationTrackingStream() function, I can create a new stream and then iterate over it.

let user = User(name: "Jane", age: 21)

let changes = observationTrackingStream {
    user.age
}

for await age in changes {
    print("User's age is \(age)")
}

It's worth noting that this method only works in Swift 5 language mode, which is still the default for new projects in Xcode.

In Swift 6 language mode, this code won't compile due to changes in how observation and closure capture work. One workaround is to replace AsyncStream with AsyncChannel from swift-async-algorithms, and make the observe() function asynchronous, running it inside a detached task. However, it would introduce some overhead and may not be suitable for high-frequency updates, since each change would create a new detached task.

]]>
https://nilcoalescing.com/blog/ObservationFrameworkOutsideOfSwiftUIUsing Observation framework outside of SwiftUIMonitor changes to specific properties of an observable class using withObservationTracking() function from Observation framework in iOS 17.https://nilcoalescing.com/blog/ObservationFrameworkOutsideOfSwiftUIWed, 22 Nov 2023 18:00:00 +1300Using Observation framework outside of SwiftUI

This year at WWDC 2023 Apple introduced the new Observation framework which provides an implementation of the observer design pattern in Swift. Classes marked with @Observable macro provided by the framework are now a better alternative to the older ObservableObject when combined with SwiftUI. The new observable classes can be used with existing data flow primitives like State and Environment and trigger view updates only when properties read in the view's body change. But we can also leverage the power of the Observation framework outside of SwiftUI, in plain Swift code and even in UIKit.

In this post we are going to see how to use withObservationTracking(_:onChange:) function included in the new Observation framework to track changes to properties of an observable class.

Let's say we have a User class that we marked with the @Observable macro. It has two properties: name and age.

@Observable class User {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

We'll make an example user called Jane and wish her a happy birthday when her age changes next time. To track changes to the age property, we need to read it inside the apply closure of withObservationTracking() function. The onChange parameter provides a closure to run when the value of age changes next time. Changes to other user properties, such as name will not trigger onChange code.

import Foundation
import Observation

let user = User(name: "Jane", age: 21)

_ = withObservationTracking {
    user.age
} onChange: {
    DispatchQueue.main.async {
        // Prints "Happy birthday! You are now 22!"
        print("Happy birthday! You are now \(user.age)!")
    }
}

user.age += 1

Note that I wrapped the print statement inside the onChange closure in DispatchQueue.main.async {}. I did this to print the new value of age which is 22. If we omit DispatchQueue.main.async {} and simply print the age, we will get the old value of age which is 21. So the onChange closure is called before new value is actually set.

let user = User(name: "Jane", age: 21)

_ = withObservationTracking {
    user.age
} onChange: {
    // Prints "Happy birthday! You are now 21!"
    print("Happy birthday! You are now \(user.age)!")
}

user.age += 1

Another thing to note is that onChange is only called once for the next update. If Jane's age keeps changing, with the current implementation we have, we'll still only congratulate her once.

let user = User(name: "Jane", age: 21)

_ = withObservationTracking {
    user.age
} onChange: {
    DispatchQueue.main.async {
        // Prints "Happy birthday! You are now 22!"
        print("Happy birthday! You are now \(user.age)!")
    }
}

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
    user.age += 1
}

This type of functionality means that we don't have to worry about manually cancelling updates. But it also means that we have to add a recursion if we do want to track subsequent changes.

We can do so by wrapping the code that implements the tracking into a separate function and then calling that function inside the onChange closure.

let user = User(name: "Jane", age: 21)

func confirmAge() {
    _ = withObservationTracking {
        user.age
    } onChange: {
        DispatchQueue.main.async {
            // Prints "Happy birthday! You are now 22!",
            // then "Happy birthday! You are now 23!" etc.
            print("Happy birthday! You are now \(user.age)!")
            confirmAge()
        }
    }
}

confirmAge()

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
    user.age += 1
}

We can use a similar implementation to track changes to an observable class and trigger UI updates in a UIKit view controller. That's really useful when bridging data between UIKit and SwiftUI parts of the same app. I wrote more about it with some examples in my book Integrating SwiftUI into UIKit Apps in the new chapter SwiftUI integration in iOS 17 that focuses on new iOS 17 APIs.

Note that we may experience some potential issues when using withObservationTracking() function that we should be aware of. Firstly, since there is no easy way to cancel the tracking, we have to make sure that we don't capture any strong references inside the onChange closure. Another point to keep in mind is that if we use asynchronous observation, there is no guarantee on the order of the actual executions, so we could be dealing with potentially un-ordered callbacks.

]]>
https://nilcoalescing.com/blog/HierarchicalBackgroundStylesHierarchical background styles in SwiftUIUse the new instance properties of the ShapeStyle in iOS 17 to access hierarchical system background styles, such as secondary and tertiary background.https://nilcoalescing.com/blog/HierarchicalBackgroundStylesSun, 5 Nov 2023 18:00:00 +1300Hierarchical background styles in SwiftUI

Before iOS 17 to get hierarchical system background colors in SwiftUI we usually had to convert them from UIColor. For example, to get the secondary system background we would write the following code: Color(uiColor: .secondarySystemBackground).

Starting from iOS 17 we now have new properties, such as secondary, tertiary, quaternary and quinary that are defined on an instance of a ShapeStyle. To get hierarchical background colors we simply have to access these properties on the current background style: BackgroundStyle().secondary. BackgroundStyle in SwiftUI conforms to ShapeStyle protocol, so accessing the secondary property on an instance of a BackgroundStyle will return the second level of the background in the current context that depends on the operating system and color scheme (light or dark mode enabled).

We can also get the current background style from the static background property defined on ShapeStyle.

We can see that if we put both the old and the new ways to get hierarchical system background colors side by side, they will be the same. UIKit system background colors go up to the tertiary one.

HStack {
    Image(systemName: "cat")
        .padding()
        .background(Color(uiColor: .secondarySystemBackground))
    
    Image(systemName: "cat")
        .padding()
        .background(.background.secondary)
}

...

Screenshot showing an image of a cat with secondary system background

Unfortunately, the new instance properties are only available on iOS 17, so if you are supporting older OS versions, you would need to wrap their use in if #available(iOS 17.0, *) and fallback to the previous method for older targets.

if #available(iOS 17.0, *) {
    Image(systemName: "cat")
        .padding()
        .background(.background.secondary)
} else {
    Image(systemName: "cat")
        .padding()
        .background(Color(uiColor: .secondarySystemBackground))
}

Note, that the new instance properties for getting hierarchical shape styles that return some ShapeStyle are different from the static properties available on older iOS versions, such as primary, secondary, tertiary, quaternary and quinary that return HierarchicalShapeStyle. The new instance properties provide a hierarchical level of the shape style that they are accessed on, for example a hierarchical level of the current background style. The static properties always provide a hierarchical level of the current foreground style.

The new instance properties can be accessed on other shape styles too, not just the background style. For example, we can get hierarchical levels of a TintShapeStyle or any SwiftUI Color.

Image(systemName: "cat")
    .padding()
    .background(.tint.secondary)

...


Image(systemName: "cat")
    .padding()
    .background(.green.tertiary)
Screenshot showing levels of the blue tint color and green color


If you want to build a strong foundation in SwiftUI, my new book SwiftUI Fundamentals takes a deep dive into the framework’s core principles and APIs to help you understand how it works under the hood and how to use it effectively in your projects.

For more resources on Swift and SwiftUI, check out my other books and book bundles.

]]>
https://nilcoalescing.com/blog/FilteringLogsInXcode15Filtering logs in Xcode 15Take advantage of the improvements in the debug console in Xcode and learn how to filter logs by type, category or message, show and hide similar items and view log metadata.https://nilcoalescing.com/blog/FilteringLogsInXcode15Mon, 23 Oct 2023 19:00:00 +1300Filtering logs in Xcode 15

We have some great new features in the debug console in Xcode 15. I'm particularly excited about improved filtering functionality that makes it much easier to view console logs. In this post I'm going to highlight the main changes and show how we can take advantage of them.

To benefit from all the nice new features, we have to make sure that we are using unified logging and not print statements in our projects. For the demo purposes in this post, let's imagine that we have the following simple logs already in place.

import OSLog

...

let welcomeScreenLogger = Logger(subsystem: "MyTestApp", category: "WelcomeScreen")
let accountLogger = Logger(subsystem: "MyTestApp", category: "Account")
    
...

welcomeScreenLogger.info("Loading welcome screen data...")

...

welcomeScreenLogger.error("Loading error occurred: \(error)")

...

accountLogger.info("Logged in successfully")

...

accountLogger.fault("Something bad happened")

# Viewing metadata

Firstly, let's see how we can make Xcode show different kinds of metadata for logs to make it easier to find and read the messages that we are looking for.

In the case when all of our demo logs appear in the console, we are going to see them in the following way. The metadata is hidden by default and different log levels have different background color. Error logs are colored in yellow while fault logs are colored in red.

Screenshot showing console logs in Xcode 15

We can now choose what kind of metadata we want to see by pressing the "Metadata Options" button at the bottom of the console. For example, we can show type, timestamp and category of the logs by checking these checkboxes. The selected metadata will appear below the message for each log.

Screenshot showing console logs with metadata

If we want to see all of the metadata available for a singe log, we can click on that log first, and then press space on the keyboard. Xcode will show a popover with all the relevant information including the call site.

Screenshot showing info for an error log in Xcode

# Showing or hiding similar items

If we are interested in a particular log and want to see only items that are similar to it and hide the rest, we can do it by right-clicking on the log, then choosing the "Show Similar Items" option. There we can choose what information should be used as a filter. For example, if we only want to see the logs from the same category, we can select "Category 'WelcomeScreen'" as the filter.

Screenshot showing filtering option for similar logs

Once selected, the category filter will be automatically added in the filter bar at the bottom of the console. There, we can clear the filter if we want to see all the logs again.

Screenshot showing the category filter applied

If we want to hide only one type of logs and show everything else, we can right-click on a log and choose the "Hide Similar Items" option. For example, we can hide all of the info logs, to reduce the amount of noise in the console while we are focusing on errors and faults.

Screenshot showing filtering option for hiding similar logs

# Filtering by type

We can quickly filter logs by type from the quick filters option on the left of the filter bar. We can choose to only show errors or faults, for example.

Screenshot showing quick filtering options

In the same popover we can also access the filters that we recently applied.

# Entering filters in the filter bar

When entering a filter into the filter bar, we can make use of Xcode filtering suggestions. If we want to only show logs from a certain category, we can start typing the name in the filter bar and check the autocomplete options. For example, to quickly see all the logs for the Account component, I start typing "acc" and select Category == Account from Xcode suggestions.

Screenshot showing filter suggestions

If we are looking for a particular message, we can simply type a porting of the string into the filter bar. Only the logs that contain the string we typed will be shown.

Screenshot showing filtering by a string


If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/RemotePushSetupiOS app setup for remote push notificationsThis post will walk you through all the necessary setup so that you can enable remote push notification functionality in your iOS project.https://nilcoalescing.com/blog/RemotePushSetupWed, 16 Aug 2023 19:00:00 +1200iOS app setup for remote push notifications

Remote push notifications are messages that app developers can send to users directly on their devices from a remote server. These notifications can appear even if the app is not open, making them a powerful tool for re-engaging users or delivering timely information. They are different from local notifications, which are scheduled and triggered by the app itself on the device.

Adding remote notifications capability to an iOS app is a quite involved process that includes several steps and components. This post will walk you through all the necessary setup so that you can enable remote push notification functionality in your iOS project.

Note that to be able to fully configure and test remote push notifications, you will need an active Apple developer account.

# App ID configuration

To configure your app ID with push capabilities, go to Certificates, Identifiers & Profiles in your Apple developer account, create a new app ID or choose an existing one that corresponds to your project where you are adding push notifications support.

Make sure that the "Push Notifications" capability is selected.

Screenshot of app ID setup with push notifications enabled

# Push certificate

If you don't have a push notifications key associated with your developer account yet under Certificates, Identifiers & Profiles, then you will need to create one.

Name it "Push Notification Key" and make sure to check the "Apple Push Notifications service (APNs)" checkbox.

Screenshot of push notifications key setup

Once created, download the key and keep it somewhere safe. You will need this key to connect to APNs from your server when you are ready to send remote push notifications to your app users.

# Xcode project configuration

Open your Xcode project and go to the project settings. In "Signing and Capabilities" settings add the "Push Notifications" capability.

Screenshot of Push Notifications capability in Xcode

# Requesting user permission

This step is only necessary if you are going to send alert push notifications, sometimes called foreground notifications. These display an alert, sound, or a badge on the app's icon and used to inform users of new content, updates, or other important information that requires immediate attention. Background push notifications or sometimes called silent notifications don't require user permission as they are not visible to the user and are not disruptive. They are great for updating content, syncing data, or performing maintenance tasks without interrupting the user.

To be able to show alerts on the user’s device we first need to ask for permission. We should do it in response to a user action so that they are not surprised by the shown dialog. It’s important that the user has the context to understand why the app needs authorization for notifications.

We can request permission with the following code.

Task {
    let center = UNUserNotificationCenter.current()
    let success = try await center.requestAuthorization(options: [.alert])
    
    if success {
        // proceed with registration
    }
}
Screenshot of prompt asking user permission for notifications

The system prompts the user to grant or deny the request the first time, subsequent requests don't prompt the user. Once we obtain the user authorization we can proceed to register our app to receive remote push notifications.

# Registering for push notifications

To get the device token from APNs we need to register for remote push notifications. This can be done after getting user permission for alerts. It should also be done on every app start, because old tokens can be no longer valid.

If your app uses the SwiftUI app lifecycle you'll need to add an application delegate to your project. With the UIKit lifecycle you can just use your existing UIApplicationDelegate.

In a SwiftUI app create a new file called AppDelegate.swift, declare a class conforming to UIApplicationDelegate and add application(_:didFinishLaunchingWithOptions:) method. If you are planning to send alerts rather than background push notifications, then make sure to check for notifications authorization status on the user's device before registering.

import UIKit
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [
            UIApplication.LaunchOptionsKey: Any
        ]?
    ) -> Bool {
        Task {
            let center = UNUserNotificationCenter.current()
            let authorizationStatus = await center
                .notificationSettings().authorizationStatus
                
            if authorizationStatus == .authorized {
                await MainActor.run {
                    application.registerForRemoteNotifications()
                }
            }
        }
        return true
    }
}

To connect the app delegate to the SwiftUI app lifecycle we need to use the UIApplicationDelegateAdaptor property wrapper. Add your delegate like in the following code to your app struct to insure that the application launch code is called.

@main
struct PushSetupApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self)
    private var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

# Handling the device token

Once registered, the app will receive a device token which needs to be sent to your backend server. This token will be used by your server to send notifications to this specific device.

To retrieve the token add the application(_:didRegisterForRemoteNotificationsWithDeviceToken:) method to your app delegate. Depending on what your server expects, you might need to convert the token to a string before sending it. Either way, it will be useful to have it in a string format for testing.

class AppDelegate: NSObject, UIApplicationDelegate {    
    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        let token = deviceToken.reduce("") { $0 + String(format: "%02x", $1) }
        print("device token: \(token)")
        
        // send the token to your server
    }
}

# Testing remote push notifications

At this point your app is configured to receive basic push notifications. You will need to go through additional setup steps if you want to support rich push notifications with images, actions buttons, deep links or custom UI. I may cover these later in future posts.

To test that the basic setup is working properly we can use the new Push Notifications Console from Apple to send ourselves a test push.

Once you open the console, select your app ID in the top left corner and create a test push notification. To target your test device or simulator, you can copy the printed device token and paste it in the "Device Token" field of the notification form.

Screenshot of Push Notifications console with the test push

If everything goes well you should receive your test push notification on your target device.

Screenshot of the test push notification on iOS simulator

Note that by default push notifications will only be shown if your app is not running in the foreground. If you do want to receive notifications while in the foreground, you should look into implementing userNotificationCenter(_:willPresent:withCompletionHandler:) method.

]]>
https://nilcoalescing.com/blog/NotificationActionButtonsWithImagesNotification action buttons with images in iOSAdd icons to buttons in actionable push notifications using the UNNotificationActionIcon class to make notifications more intuitive and visually appealing.https://nilcoalescing.com/blog/NotificationActionButtonsWithImagesMon, 7 Aug 2023 13:00:00 +1200Notification action buttons with images in iOS

Action buttons are a vital part of the iOS notification system, allowing users to interact with notifications directly from the home screen. According to Apple documentation, these buttons can be used to trigger specific actions within an app without needing to open the app itself.

The User Notifications framework provides several types of actions, including:

  • Generic Actions: These can be used for any purpose and are often used to open the app or perform a task.
  • Text Input Actions: These allow the user to respond with text directly from the notification.
  • Destructive Actions: These are used for actions that might change or delete data and are usually styled to warn the user.

Starting from iOS 15, Apple has taken notification actions a step further by allowing developers to include images in these buttons.

Screenshot of a push notification with like and dislike action buttons

# Adding images to notification actions

To learn more about notification actions, check out Declaring your actionable notification types.

To add an image to a notification action we can first declare it using the UNNotificationActionIcon class. Icons can be created using either a system symbol or a an image in your app’s bundle.

let likeActionIcon = UNNotificationActionIcon(systemImageName: "hand.thumbsup")

To assign the image to an action button, we'll use the UNNotificationAction initializer that accepts an icon.

let likeAction = UNNotificationAction(
    identifier: "new_comment_category.like_action",
    title: "Like",
    options: UNNotificationActionOptions.foreground,
    icon: likeActionIcon
)

Finally, we'll assign the action to a notification category.

let newCommentCategory = UNNotificationCategory(
      identifier: "new_comment_category",
      actions: [likeAction, dislikeAction],
      intentIdentifiers: []
)

Don't forget to register your category on application launch.

let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.setNotificationCategories([
    newCommentCategory,
    ...
])

And make sure to handle all of the notification actions that you declared inside userNotificationCenter(_:didReceive:withCompletionHandler:) delegate method.

# Testing notifications on the simulator

We can easily test if our action buttons appear as expected when the notification arrives locally on an iOS simulator or by sending a remote push with the new Push Notifications Console from Apple.

To quickly test the notification appearance locally, create a test.apn file with the payload. Make sure to include the notification category that contains the buttons with icons.

{
   "aps": {
      "category" : "new_comment_category",
      "alert" : {
         "title" : "New comment on your post",
         "body" : "Great work!"
      }
   }
}

Run your app on the iOS simulator. In the terminal go to the directory where you saved the test.apn file and run xcrun simctl push booted com.nilcoalescing.MyApp test.apn replacing com.nilcoalescing.MyApp with your app bundle ID.

Long press on the notification that appears in the simulator and check if the action buttons show the text and the icons that you provided.

Screenshot of a push notification with like and dislike action buttons


If you're an experienced Swift developer looking to level up your skills, take a look at my book Swift Gems. It offers 100+ advanced Swift tips, from optimizing collections and leveraging generics, to async patterns, powerful debugging techniques, and more - each designed to make your code clearer, faster, and easier to maintain.

]]>
https://nilcoalescing.com/blog/ForegroundStyleInsideTextInSwiftUIInterpolate text with custom foreground style in SwiftUIAdd custom foreground styles, such as gradients, to words inside Text views in SwiftUI.https://nilcoalescing.com/blog/ForegroundStyleInsideTextInSwiftUITue, 13 Jun 2023 13:00:00 +1200Interpolate text with custom foreground style in SwiftUI

SwiftUI lets us style portions of text by interpolating Text inside another Text and applying available text modifiers, such as underline() or font().

Starting from iOS 17 we can apply more intricate styling to ranges within a Text view with foregroundStyle().

For example, we can color a word with a gradient.

Screenshot of hello world text where world is colored with linear gradient
struct ContentView: View {
    let gradient = LinearGradient(
        colors: [.blue, .green],
        startPoint: .leading,
        endPoint: .trailing
    )
    
    var body: some View {
        Text("Hello, \(Text("world").foregroundStyle(gradient))!")
            .bold()
            .font(.title)
            .textCase(.uppercase)
    }
}

And if you are interested in more advanced text styling, you can take a look at the new ShaderLibrary. In iOS 17 Metal shaders get automatically converted to ShapeStyles that then can be passed to foregroundStyle().

]]>
https://nilcoalescing.com/blog/ParameterPermutationsInXcode15AutocompleteSee all parameter permutations in code completion in Xcode 15In Xcode 15 code completion we can view all the possible permutations of function parameters by pressing the right arrow key.https://nilcoalescing.com/blog/ParameterPermutationsInXcode15AutocompleteSun, 11 Jun 2023 13:00:00 +1200See all parameter permutations in code completion in Xcode 15

Xcode 15 beta comes with some great improvements to code completion. One of my favorite is the ability to view all possible permutations of function parameters.

Screenshot of Xcode showing all the possible permutations for the SwiftUI frame modifier in code completion window

If a function contains more than one default parameter, we can see all the possible cases by pressing the right arrow keyboard key. This lets us choose the right permutation without any extra typing.

]]>
https://nilcoalescing.com/blog/KeyboardNavigationWithFocusableKeyboard driven navigation with focusable() on iPad in SwiftUIThe focusable() modifier available since iPadOS 17 allows us to provide full keyboard navigation, even including custom views not focusable by default.https://nilcoalescing.com/blog/KeyboardNavigationWithFocusableThu, 8 Jun 2023 16:00:00 +1200Keyboard driven navigation with focusable() on iPad in SwiftUI

The recent updates to iOS 17 and iPadOS 17 have brought about exciting changes, one of which is the introduction of the focusable() modifier. This allows us to let users focus on views that are not focusable by default, significantly improving keyboard based interaction.

Here's a simple example of how we can use the focusable() modifier in SwiftUI.

VStack {
    FirstView()
        .focusable()

    SecondView()
        .focusable()
}
Screenshot showing a focused SwiftUI view.

# Customize focus effect style

To customize the style of the focus effect we need to remove the default styling with focusEffectDisabled() and then apply our own.

To know that a view is focused we need to use the focused() modifier with a @FocusState to track and respond to changes.

struct ContentView: View {
    var body: some View {
        VStack {
            FirstViewWrapped()
                .focusEffectDisabled()

            SecondView()
                .focusable()
        }
    }
}

struct FirstViewWrapped: View {
    @FocusState var isFocused
        
    var body: some View {
        FirstView()

            .focusable()             // Enable focuses
            .focused($isFocused)     // Detect focuses
            
            .background {
                if isFocused {
                    Capsule()
                        .fill(.indigo)
                        .opacity(0.3)
                }
            }
    }
}
Screenshot showing a focused SwiftUI view with custom background style.

The order of the focusable() and focused() modifiers is important. The focused() modifier needs to be placed after the focusable() modifier.

]]>
https://nilcoalescing.com/blog/ControlGroupInContextMenusControlGroup in context menus in SwiftUIStarting from iOS 17 and iPadOS 17 we can use ControlGroup to display a horizontal collection of actions in a context menu.https://nilcoalescing.com/blog/ControlGroupInContextMenusWed, 7 Jun 2023 16:00:00 +1200ControlGroup in context menus in SwiftUI

Starting from iOS 17 and iPadOS 17 we can now use a ControlGroup inside a contextMenu(), enabling more compact access to common actions.

Screenshot of a context menu open showing cut, copy and past all in a horizontal stack.
ContentView()
    .contextMenu {
        ControlGroup {
            Button {
                // cut action
            } label: {
                Label("Cut", systemImage: "scissors")
            }
            
            Button {
                // copy action
            } label: {
                Label("Copy", systemImage: "doc.on.doc")
            }
            
            Button {
                // paste action
            } label: {
                Label("Paste", systemImage: "doc.on.clipboard")
            }
        }
        
        Button(role: .destructive) {
            // delete action
        } label: {
            Label("Delete", systemImage: "trash")
        }
    }

On macOS this code creates a nested sub-menu with cut, copy and paste as items.

]]>
https://nilcoalescing.com/blog/IfElseExpressionsInSwiftif/else statements as expressions in SwiftSwift 5.9 introduced the use of if/else statements as expressions, simplifying value returns, variable assignments, and enhancing code readability.https://nilcoalescing.com/blog/IfElseExpressionsInSwiftWed, 7 Jun 2023 13:00:00 +1200if/else statements as expressions in Swift

Swift 5.9 introduced a nice feature that lets us use if/else statements as expressions. We can return values from functions, assign values to variables, and declare variables using if/else, where previously we needed to use a ternary operator, Swift's definite initialization with assignment on each branch or even closures.

Let's say we have a game where a player can score points, and based on the number of points, we assign them a rank.

func playerRank(points: Int) -> String {
    let rank = 
        if points >= 1000 { "Gold" }
        else if points >= 500 { "Silver" }
        else if points >= 100 { "Bronze" }
        else { "Unranked" }
    return rank
}

let playerPoints = 650
print(playerRank(points: playerPoints))  // Prints "Silver"

In this example, the playerRank() function takes the number of points a player has scored as input and returns a string that represents the player's rank. The rank is determined by an if/else expression, and the value of the chosen branch becomes the value of the overall expression.

Note, that each branch of if/else must be a single expression, and each expression, when type checked independently, must produce the same type. This ensures easy type checking and clear reasoning about the code.

This feature applies to switch statements as well. You can find out more in the related Swift proposal on if and switch expressions.

]]>
https://nilcoalescing.com/blog/Xcode15AssetsAccess colors and images from asset catalog via static properties in Xcode 15Xcode 15 automatically generates static properties on ColorResource and ImageResource types for colors and images that we store in asset catalog. This lets us access them in SwiftUI, UIKit or AppKit code without using string literals.https://nilcoalescing.com/blog/Xcode15AssetsTue, 6 Jun 2023 18:00:00 +1200Access colors and images from asset catalog via static properties in Xcode 15

Xcode 15 came with a great new way to access colors and images stored in asset catalog. It automatically generates static properties corresponding to our assets on new ColorResource and ImageResource types. We can then use those properties to initialize colors and images in SwiftUI, UIKit and AppKit where we previously had to use string literals.

We are going to look at some SwiftUI examples more closely, but keep in mind that equivalent APIs are available in UIKit and AppKit as well.

Let's imagine that we have a custom color called "MyGreen" stored in the asset catalog.

Screenshot of Xcode showing a custom green color in asset catalog

To reference this color in our SwiftUI code, we simply need to call the new Color initializer that accepts a ColorResource and pass it .myGreen.

struct ContentView: View {
    var body: some View {
        Color(.myGreen)
    }
}

The static property myGreen was created automatically by Xcode, all we needed to do is to define the color in the asset catalog. And it works with autocomplete too. No more string literals to reference the resources 🎉

Screenshot of Xcode showing myGreen color autocompleted in SwiftUI

The same applies to image assets too. Let's say we are storing an image called "fern" in the asset catalog.

Screenshot of Xcode showing a fern image in asset catalog

To create a SwiftUI image that shows the fern from the asset catalog, we'll call the new Image init that accepts an ImageResource and pass it .fern.

struct ContentView: View {
    var body: some View {
        Image(.fern)
    }
}

# Generate Swift Asset Symbol Extensions setting

Xcode 15 takes this feature even further with the “Generate Swift Asset Symbol Extensions” setting that is enabled by default, but can be disabled by us if needed.

We can access our assets via static properties generated directly on system types, such as Color. So we could reference our custom green color from the asset catalog as follows.

struct ContentView: View {
    var body: some View {
        Color.myGreen
    }
}

Note, that as of Xcode 15 beta 1, this doesn't work on the SwiftUI Image type, referencing Image.fern will not compile.

If you would like to check out how this setting is enabled or need to disable it in your project, you can search for ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS in build settings in Xcode.

Screenshot of Xcode shwoing build settings


I think this new Xcode 15 behavior is a huge improvement over accessing our asset catalog resources via string literals, like we had to do previously. Our code will now be safer and cleaner.

Screenshot of Xcode showing a fern image between two green color rectangles in a VStack in a SwiftUI preview on the side of the code

And the greatest thing about this feature is that it works on older deployment targets too, not just iOS 17!

If you're an experienced Swift developer looking to learn advanced techniques, check out my book Swift Gems. It’s packed with tips and tricks focused solely on the Swift language and Swift Standard Library. From optimizing collections and handling strings to mastering asynchronous programming and debugging, "Swift Gems" provides practical advice that will elevate your Swift development skills to the next level. Grab your copy and let's explore these advanced techniques together.

]]>