Review: 31154 Forest Animals: Red Fox

LEGO Reviews

LEGO’s got some really strong Creator 3-in-1 sets out this year, and 31154 Forest Animals: Red Fox is one of the best. You can build a fox, an owl, or a squirrel, and all three models are truly excellent.

31154 Forest Animals: Red Fox doesn’t disappoint; all three models are fantastic, were a joy to build, and look great on display. Each manages to capture the unique characteristics of the woodland creatures, and the articulation makes them quite satisfying to pose in the way that you want.

31154 Forest Animals: Red Fox LEGO set

Review: 31148 Retro Roller Skate

LEGO Reviews

Once again LEGO are coming up with some very creative 3-in-1 sets for their first wave of 2024:

I’m a big fan of the 3-in-1 range. Lately LEGO have been knocking it out of the park with so many of these sets, and this one held great promise. I love the colour scheme, and the roller skate is a brilliant build. It’s a decent size, feels solid in the hand, and although it’s not a vehicle, pushing it around is quite satisfying. It’s just a shame there’s only one—a pair would have been fantastic!

LEGO 31148 Retro Roller Skate

Anatomy of a Configurable Widget

DevelopmentSwiftSwiftUI

In my previous post, Anatomy of a Widget, I outlined my basic understanding of building a simple widget in Xcode. These were the most trivial widgets possible: they provided no options, and certainly no interactivity as introduced with iOS 17. In this post, I’m going to write up my (also limited) understanding of the parts that need to be added to provide configurable options within the widget—in other words, those the user sees when they long press on a widget and hit “Edit”.

The previous post introduced the concept of the the timeline entry, the timeline provider, the widget’s view, the widget definition, and the widget bundle. We’ll be adding one more, the configuration intent, and tying it in to the rest.

The same major caveat as before continues to apply: I still do not fully understand widgets, or really know how to build them properly. Nor do I understand interactive widgets that come in with iOS 17. But, hopefully, my knowledge will increase and I can update these posts as I learn more! Please do not hesitate to let me know about anything I write here that’s misleading or factually incorrect.

The Configuration Intent

iOS apps use what Apple refers to as “intents” to tell other parts of the system what the app can do—such as Siri, or Shortcuts. The same mechanism is used by widgets to define what options are available, and we do this using a struct conforming to WidgetConfigurationIntent. This struct needs to hold all the parameters available to the user in the widget’s edit menu. For example, a very simple widget intent could look like the following:

import WidgetKit
import AppIntents

struct MyConfigurationIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Configuration"
    static var description = IntentDescription("This is an example widget.")

    @Parameter(title: "Favourite Emoji", default: "😃")
    var favouriteEmoji: String
}

This is about as basic as it gets, providing a single string parameter that will be exposed in the edit menu via a text field, with a sensible default. I’ll go through some other parameter types available later.

For now, we need to propagate the configuration intent throughout the rest of the widget’s stack.

Updating the Timeline Entry

The timeline entry is responsible for holding all the information the widget’s view needs to render for a given point in time. We need to update this to also hold the configuration intent. I will be using the example code from the previous post, and adding/amending it as necessary:

struct MyWidgetEntry: TimelineEntry {
    let date: Date
    let text: String
    let configuration: MyConfigurationIntent
}

Updating the Timeline Provider

Because the timeline entries are created by the timeline provider, we need to update this to include the configuration intent too. This time, we also need to change from conforming to the basic TimelineProvider protocol to the mode advanced AppIntentTimelineProvider protocol, which also necessitates changing the method signatures of the three methods, placeholder, getSnapshot, and getTimeline.

struct MyTimelineProvider: AppIntentTimelineProvider {
    func placeholder(in context: Context) -> MyWidgetEntry {
        MyWidgetEntry(date: Date(), configuration: MyConfigurationIntent())
    }

    func snapshot(for configuration: MyConfigurationIntent, in context: Context) async -> MyWidgetEntry {
        MyWidgetEntry(date: Date(), configuration: configuration)
    }

    func timeline(for configuration: MyConfigurationIntent, in context: Context) async -> Timeline<MyWidgetEntry> {
        var entries: [MyWidgetEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = MyWidgetEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

        return Timeline(entries: entries, policy: .atEnd)
    }
}

For the placeholder method, the timeline provider isn’t handed a configuration, so it has to create one to pass to the timeline entry. For the other two methods, however, the first argument passed is a MyConfigurationIntent struct representing the options the user has selected in the widget edit view, and we can pass this directly to the timeline entry.

Updating the Widget’s View

The only thing left to do is update the widget’s View to make use of the options provided by the configuration intent. In this case, we can use the favouriteEmoji parameter, as it’s the only one provided by our very simple intent:

struct MyWidgetView : View {
    var entry: MyWidgetEntry

    var body: some View {
        VStack {
            Text(entry.date, style: .time)
            Text(entry.text)
            Text("Favourite Emoji:")
            Text(entry.configuration.favouriteEmoji)
        }
    }
}

And that’s it! The widget now allows the user to customise its view by presenting an edit menu with a bunch of parameters. The user-chosen values for these parameters are passed into the View as a Configuration Intent parameter via the timeline entry, and the View can make use of them as it wishes.

Configuration Intent Parameters

Above, we saw a very simple configuration intent parameter of a string:

@Parameter(title: "Favourite Emoji", default: "😃")
var favouriteEmoji: String

The title is the name of the parameter as shown to the user in the edit view of the widget, and the default you provide is what the parameter is set to when the user hasn’t edited the widget and entered something else. You can view the WidgetConfigurationIntent documentation for more options that are available, such as how to control the order the parameters appear in the widget edit view or define those which depend on others.

Adding other type of data is easy, such as asking for an integer:

@Parameter(title: "Age", default: 18)
var age: Int

Or a boolean:

@Parameter(title: "Show Background", default: false)
var showBackground: Bool

You can also present the user with a choice of options by conforming to DynamicOptionsProvider:

struct IntegerOptionsProvider: DynamicOptionsProvider {
    let count: Int
    let defaultInteger: Int
    func results() async throws -> [Int] {
        Array(0...count)
    }
    func defaultResult() async -> Int? {
        defaultInteger
    }
}

...

@Parameter(title: "Hour", optionsProvider: IntegerOptionsProvider(count: 24, defaultInteger: 16))
var hour: Int

More complicated data types can be represented by conforming to various protocols, such as AppEnum to provide users with a choice based on an enum:

enum Weekday: Int, AppEnum {
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Weekday"

    case Sunday = 1
    case Monday = 2
    case Tuesday = 3
    case Wednesday = 4
    case Thursday = 5
    case Friday = 6
    case Saturday = 7

    static var caseDisplayRepresentations: [Weekday: DisplayRepresentation] = [
        .Sunday: "Sunday",
        .Monday: "Monday",
        .Tuesday: "Tuesday",
        .Wednesday: "Wednesday",
        .Thursday: "Thursday",
        .Friday: "Friday",
        .Saturday: "Saturday"
    ]
}

...

@Parameter(title: "Weekday", default: .Friday)
var weekday: Weekday

Or by conforming an struct to AppEntity and the associated EntityQuery, you can add support for arbitrary data types, which is the most powerful but complicated option, such as this example for adding a time zone choice to the widget:

struct TimeZoneQuery: EntityStringQuery {
    private func convertToWidgetTimeZone(identifiers: [String]) -> [WidgetTimeZone] {
        identifiers.compactMap { TimeZone(identifier: $0) }.map { WidgetTimeZone(timezone: $0) }
    }
    func entities(matching string: String) async throws -> [WidgetTimeZone] {
        return convertToWidgetTimeZone(identifiers: TimeZone.knownTimeZoneIdentifiers.filter { $0.localizedStandardContains(string) })
    }
    func entities(for identifiers: [String]) async throws -> [WidgetTimeZone] {
        return convertToWidgetTimeZone(identifiers: TimeZone.knownTimeZoneIdentifiers.filter { identifiers.contains($0) })
    }
    func suggestedEntities() async throws -> [WidgetTimeZone] {
        return convertToWidgetTimeZone(identifiers: TimeZone.knownTimeZoneIdentifiers)
    }
}

struct WidgetTimeZone: Equatable, Hashable, AppEntity {
    typealias DefaultQuery = TimeZoneQuery
    static var defaultQuery: TimeZoneQuery = TimeZoneQuery()
    static var typeDisplayName: LocalizedStringResource = LocalizedStringResource("TimeZone", defaultValue: "TimeZone")
    static var typeDisplayRepresentation: TypeDisplayRepresentation {
        TypeDisplayRepresentation(stringLiteral: "TimeZone")
    }
    public var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: .init(stringLiteral: id))
    }

    var id: String { timezone.identifier }
    var timezone: TimeZone
}

...

@Parameter(title: "Time Zone")
var timeZone: WidgetTimeZone?

Anatomy of a configurable widget


An extension of the Configuration Intent protocol is also what powers the interactive widgets available in iOS 17. Hopefully, once I’ve figured those out a little more, a future post will cover the basics of them too!

Anatomy of a Widget

DevelopmentSwiftSwiftUI

I have long been a little confused by how widgets work, from a development perspective, in iOS apps. There are a number of moving parts that all have to work together just so to make the widget appear how you want, with the data you want, when you want. This post is my attempt to break it down into each part, in the order they need to be defined so the app still compiles after each step, with my understanding of what they’re for and what they do.

Anatomy of a widget

The major caveat here is: I still do not understand widgets, or really know how to build them properly. Nor do I understand interactive widgets that come in with iOS 17. But, hopefully, my knowledge will increase and I can update this post as I learn more! Please do not hesitate to let me know about anything I write here that’s misleading or factually incorrect.

The Timeline Entry

Widgets are a series of static SwiftUI views, rendered on a timeline into the future. When the system reaches the end of the timeline, or at some point determined by your app or widget configuration, the app extension is asked for another timeline to render.

Each item in this timeline is a timeline entry, which is simply a struct conforming to TimelineEntry. This struct needs to hold all the data your widget needs to know in order to render correctly. The date property is mandatory (specified by the TimelineEntry protocol), but all other properties are up to you. For example, a widget that simply renders some text may need a timeline entry such as the following:

struct MyWidgetEntry: TimelineEntry {
    let date: Date
    let text: String
}

The Timeline Provider

This is the part of the widget that is responsible for providing each timeline when iOS asks for one, and needs to be a struct conforming to TimelineProvider. Apple’s documentation is pretty good here. There are three required methods that need to be implemented:

struct MyTimelineProvider: TimelineProvider {
    func placeholder(in context: Context) -> MyWidgetEntry {
        WidgetEntry(date: Date(), text: "Placeholder")
    }

    func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) -> ()) {
        let entry = MyWidgetEntry(date: Date(), text: "Snapshot")
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) -> ()) {
        var entries: [MyWidgetEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = MyWidgetEntry(date: entryDate, emoji: "In a timeline! \(hourOffset)")
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

The most common refresh policy is .atEnd, which will instruct iOS to ask for a new timeline once this one is complete. The widget will be rendered with each timeline entry at its specified date.

The Widget’s View

This is the core of the display of the widget, a SwiftUI view that takes the timeline entry as a parameter and renders the data as necessary. It doesn’t have to be a separate View (it could be rendered as part of the widget itself, see below), but it’s much neater this way.

struct MyWidgetView : View {
    var entry: MyWidgetEntry

    var body: some View {
        VStack {
            Text(entry.date, style: .time)
            Text(entry.text)
        }
    }
}

There’s nothing magic here.

The Widget Itself

Each widget is a struct that conforms to Widget, which looks similar to a SwiftUI View with a couple of extra options:

struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: MyTimelineProvider()) { entry in
            if #available(iOS 17.0, *) {
                MyWidgetView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
            } else {
                MyWidgetView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

The StaticConfiguration struct takes the widget’s kind string, an instance of your timeline provider, and a closure to call with each entry in the timeline. The closure should return the SwiftUI view configured/rendered for that particular entry.

You can also provide the supportedFamilies view modifier with a list of the different types of widget sizes that this widget supports, including Lock Screen widgets. You can use the environment variable .widgetFamily inside the view to change the layout of the view based on what size widget is currently displayed:

@Environment(\.widgetFamily) var widgetFamily

Apps built using the iOS 17 SDK require all widgets to use the new containerBackground modifier, which automatically handles padding.

Previewing Widgets

Widgets are simple to use with SwiftUI previews: you can either preview the widget View by itself, passing a static timeline entry, such as using the pre-Xcode 15 preview provider:

struct MyWidgetView_Previews: PreviewProvider {
    static var previews: some View {
        MyWidgetView(entry: MyWidgetEntry(date: .now, text: "Text"))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

Or you can use Xcode 15’s new #Preview macro, with the version specifically designed for widgets, that accepts a timeline of entries. This time you pass it the widget itself, not the view the widget renders:

#Preview(as: .systemSmall) {
    MyWidget()
} timeline: {
    MyWidgetEntry(date: .now, text: "Text 1")
    MyWidgetEntry(date: .now, text: "Text 2")
}

If you’ve reached this far, then you’ve done enough to design the widget and how it populates its timeline into the future, but we still need to tell iOS about it. This is done with one last struct.

The Widget Bundle

To tell iOS about the available widgets in your app, you need a single widget bundle defined in the widget extension, which is a struct conforming to WidgetBundle, and marked with the @main wrapper. Similar to SwiftUI views, this requires one computed parameter, body, but this time is of type some Widget:

@main
struct MyWidgets: WidgetBundle {
    var body: some Widget {
        MyWidget()
    }
}

Multiple different widgets can be returned, just put each on a new line within the body. You can also do some logic here, such as if #available checks to limit certain widgets to particular iOS versions, etc.

If you have multiple widgets that need the same data, you can reuse the same Timeline Entry and Timeline Provider in multiple Widget structs.


With that, your app should be able to provide one or more widgets to the user, and control what sizes they are available in. However, you can’t yet provide options for the user to pick from, allowing them to “edit” the widget. I’ll write up what I know about that in another post, soon!

Review: 43215 The Enchanted Treehouse

LEGO Reviews

My latest LEGO review is up on Brickset, of the Disney 100 Enchanted Treehouse.

The star of the set is clearly the impressive selection of minidolls. Their shape definitely suits the Disney Princesses more than a minifigure would; it is just a shame their articulation is significantly less.

The two halves of the treehouse look fantastic together, and there’s plenty of play value with the slide, stairs, zip wire, canoe, and various other smaller builds and interactive sections. It looks good both on display and during play.

LEGO 43215 The Enchanted Treehouse

Matching the List background colour in SwiftUI

DevelopmentSwiftSwiftUI

I recently came across a situation where I wanted to match the background colour of a a header above a SwiftUI List (using the default .insetGrouped List style) to that used by the List itself. I had done no styling to the List itself, so was relying on the system-provided background colour—this is what I wanted to match.

I tried a couple of the standard constants provided by UIColor, and landed on .secondarySystemBackground. It wasn’t until I had the build running on my phone and I was using the app later in the day that I noticed something was off slightly:

SwiftUI List background using .secondarySystemBackground in light and dark mode

In light mode, everything was fine; but in dark mode, the background of the header and navigation toolbar wasn’t dark enough! It turns out that what I actually wanted was .systemGroupedBackground:

SwiftUI List background using .systemGroupedBackground in light and dark mode

Now they match up as intended. Let this be a lesson to myself to test in both light mode and dark mode when developing anything that relies on colour!

Review: 31138 Beach Camper Van

LEGO Reviews

I am just loving the Creator 3-in-1 sets that LEGO are churning out nowadays:

31138 Beach Camper Van is a perfect example of this: an excellent camper van, some lovely little beach huts, an adorable crab—and that’s all just in the primary build! Let’s take a look at the set in detail, including the two alternative models that often make the 3-in-1 sets the success they are.

LEGO 31138 Beach Camper Van

Review: 31139 Cozy House

LEGO Reviews

I love LEGO’s Creator 3-in-1 range, where every set can be built into three different designs. 31139 Cozy House is no exception, with three excellent builds, and a handful of adorable little bugs to boot!

The primary model of the set is the titular Cozy House (or “cosy”, as we’d be more likely to write here in the UK). It’s a fantastic little two-story building with pitched roofs, a delightful garden, and is full of excellent details!

Cozy House LEGO set

Review: 60359 Dunk Stunt Ramp Challenge

LEGO Reviews

The latest batch of LEGO I’ve been provided to review included three City Stuntz sets, a range that I’ve not built or played with before. They come with flywheel-powered bikes and stunt arenas, and are all pretty creative and fun! The first review went up today, for 60359 Dunk Stunt Ramp Challenge:

Despite the theme entering its third year, this was my first experience of the line, and I was quite impressed! The flywheel-powered bike is a lot of fun, and it kept both my children (aged four and six) entertained for some considerable time (once they stopped fighting over whose turn it was).

Dunk Stunt Ramp Challenge LEGO set

Review: 71419 Peach's Garden Balloon Ride

LEGO Reviews

Princess Peach is back (in LEGO form) with another expansion set to the Super Mario theme. Huw over at Brickset.com asked me to take a look and see how it stacks up.

It is typical LEGO Super Mario fare: bright and colourful, with standard game mechanics, introducing a new character and recycling some old favourites.

Read the full review on Brickset.com.

Princess Peach Garden Balloon Ride LEGO set