A blog exploring functional programming and Swift.

Better SwiftUI navigation APIs

Monday Nov 21, 2022

Today we are releasing the biggest update to our SwiftUINavigation library since its first release one year ago. This brings support for new iOS 16 APIs, bug fixes for some of Apple’s navigation tools, better support for alerts and confirmation dialogs, and improved documentation.

Join us for a quick overview of the new features, and be sure to update to 0.4.0 to get access to all of this, and more:

Navigation stacks

iOS 16 largely reinvented the way drill-down navigations are performed by introducing a new top-level view, NavigationStack, a new view modifier, navigationDestination, and new initializers on NavigationLink. These new tools allow for greater decoupling of source and destination of navigation, and allow for better managing of deep stacks of features.

Our library brings a new tool to the table, and it’s built on the back of the navigationDestination(isPresented:) view modifier, which allows driving navigation from a boolean binding. This tool fixes one of the biggest drawbacks to NavigationLink, which is that it was difficult to use in a list view since a drill-down would only occur if the row was visible in the list. This means you could not programmatically deep link to a screen if the row was not currently visible.

The navigationDestination view modifier fixes this by allowing you to have a single place to express navigation, rather than embedding it in each row of the list:

func navigationDestination<V>(
  isPresented: Binding<Bool>,
  destination: () -> V
) -> some View where V : View

However, a boolean binding is too simplistic of a domain modeling tool to use. What if you wanted to control navigation via a piece of optional state, and further hand that state to the destination view?

This is why our library comes with an additional overload, named navigationDestination(unwrapping:), which can drive navigation from a binding to an optional:

public func navigationDestination<Value, Destination: View>(
  unwrapping value: Binding<Value?>,
  @ViewBuilder destination: (Binding<Value>) -> Destination
) -> some View {

This makes it easy to have a list of data for which you want to drill down when a row is tapped:

struct UsersListView: View {
  @State var users: [User]
  @State var editingUser: User?

  var body: some View {
    List {
      ForEach(self.users) { user in
        Button("\(user.name)") { self.editingUser = user }
      }
    }
    .navigationDestination(unwrapping: self.$editingUser) { $user in
      EditUserView(user: $user)
    }
  }
}

This works great if you only have a single destination to navigate to. But, if you need to support multiple destinations, you may be tempted to hold multiple pieces of optional state. However, that leads to an explosion of invalid states, such as when more than 1 is non-nil at the same time. SwiftUI considers that a user error, and can lead to the interface becoming non-responsive or even crash.

Subscribe to Point-Free through our Black Friday sale at a 30% discount! Learn how we motivated and built the SwiftUI Navigation library along with many other topics.

That’s why our library ships with another overload, named navigationDestination(unwrapping:case:), which allows driving multiple destinations from a single piece of enum state:

public func navigationDestination<Enum, Case, Destination: View>(
  unwrapping enum: Binding<Enum?>,
  case casePath: CasePath<Enum, Case>,
  @ViewBuilder destination: (Binding<Case>) -> Destination
) -> some View {

This allows you to model all destinations for a feature as a single enum and a single piece of optional state pointing to that enum. For example, a list with rows for users and categories for which tapping either should drill-down to the corresponding edit screen:

struct UsersListView: View {
  @State var categories: [Category]
  @State var users: [User]
  @State var destination: Destination?
  enum Destination {
    case edit(user: User)
    case edit(category: Category)
  }

  var body: some View {
    List {
      Section(header: Text("Users")) {
        ForEach(self.users) { user in
          Button("\(user.name)") { self.destination = .edit(user: user) }
        }
      }
      Section(header: Text("Categories")) {
        ForEach(self.categories) { category in
          Button("\(category.name)") { self.destination = .edit(category: category) }
        }
      }
    }
    .navigationDestination(
      unwrapping: self.$destination,
      case: /Destination.edit(user:)
    ) { $user in
      EditUserView(user: $user)
    }
    .navigationDestination(
      unwrapping: self.$destination,
      case: /Destination.edit(category:)
    ) { $category in
      EditCategoryView(user: $category)
    }
  }
}

This makes it so that the compiler can prove that two destinations are never active at the same time. After all, the destinations are modeled on an enum, and cases of an enum are mutually exclusive.

Navigation bug fixes

The navigationDestination(isPresented:)view modifier released in iOS 16 is powerful, and the above shows we can build powerful APIs on top of it, however it does have some bugs.

If you launch your application with the navigation state already hydrated, meaning you should be drilled down into the destination, the UI will be completely broken. It will not be drilled down, and worse tapping on the button to force the drill down will not work.

We filed a feedback (and we recommend you duplicate it!), and this simple example shows the problem:

struct UserView: View {
  @State var isPresented = true

  var body: some View {
    Button("Go to destination") {
      self.isPresented = true
    }
    .navigationDestination(isPresented: self.$isPresented) {
      Text("Hello!")
    }
  }
}

This is pretty disastrous. If you are using navigationDestination(isPresented:) in your code you simply will not be able to support things like URL deep linking or push notification deep linking.

However, we were able to fix that bug in our APIs. If you use navigationDestination(unwrapping:case:) then you can rest assured that deep linking will work correctly, and it will work any number of levels deep. This also fixes a long standing bug in iOS <16’s NavigationLink, which is notorious for not being able to deep link more than 2 levels deep.

Alerts and confirmation dialogs

Our SwiftUINavigation has had support for better alert and confirmation dialog APIs using optionals and enums from the very beginning, but with the 0.4.0 release we have made them even more powerful.

The library now ships data types that allow you to describe the presentation of an alert or confirmation dialog in a way that is friendlier to testing (i.e. they are Equatable). This makes it possible to store these values in your ObservableObject conformances so that you can get test coverage on any logic.

For example, suppose you have an interface with a button that can delete an inventory item, but only if it is not “locked.” We can model this in our ObservableObject as a published property of AlertState, along with an enum to describe any actions the user can take in the alert:

@Published var alert: AlertState<AlertAction>
enum AlertAction {
  case confirmDeletion
}

Then you are free to hydrate this state at anytime to represent that an alert should be displayed:

func deleteButtonTapped() {
  if item.isLocked {
    self.alert = AlertState {
      TextState("Cannot be deleted")
    } message: {
      TextState("This item is locked, and so cannot be deleted.")
    }
  } else {
    self.alert = AlertState {
      TextState("Delete?")
    } actions: {
      ButtonState(role: .destructive, action: .confirmDeletion) {
        TextState("Yes, delete")
      }
      ButtonState(role: cancel) {
        TextState("Nevermind")
      }
    } message: {
      TextState(#"Are you sure you want to delete "\(item.name)"?"#)
    }
  }
}

And the final step for the model layer is to implement a method that handles when an alert button is tapped:

func alertButtonTapped(_ action: AlertAction) {
  switch action {
  case .confirmDeletion:
    self.inventory.remove(id: item.id)
  }
}

Then, to make the view show the alert when the alert state becomes non-nil we just have to make use of the alert(unwrapping:) API that ships with our library:

struct ItemView: View {
  @ObservedObject var model: ItemModel

  var body: some View {
    Form {
      …
    }
    .alert(unwrapping: self.$model.alert) { action in
      self.model.alertButtonTapped(action)
    }
  }
}

Notice that there is no logic in the view for what kind of alert to show. All of the logic for when to display the alert and what information is displayed (title, message, buttons) has all been moved into the model, and therefore very easy to test.

To test, you can simply assert against any parts of the alert state you want. For example, if you want to verify that the message of the alert is what you expected, can just use XCTAssertEqual:

let headphones = Item(…)
let model = ItemModel(item: headphones)
model.deleteButtonTapped()

XCTAssertEqual(
  model.alert.message,
  TextState(#"Are you sure you want to delete "Headphones"?"#)
)

Get started today

Start taking advantage of all of the powerful domain modeling tools that Swift comes with (enums and optionals!) by adding SwiftUINavigation to your project today!


Non-exhaustive testing in the Composable Architecture

Monday Oct 31, 2022

Testing is by far the #1 priority of the Composable Architecture. The library provides a tool, the TestStore, that makes it possible to exhaustively prove how your features evolve over time. This not only includes how state changes with every user action, but also how effects are executed, and how data is fed back into the system.

The testing tools in the library haven’t changed much in the two and a half years since release, but thanks to close collaboration with Krzysztof Zabłocki and support from his employer, The Browser Company, the 0.45.0 release of the library brings first class support for “non-exhaustive” test stores.

Join us for an overview of the “why” and “how” of exhaustive testing, as well as when it breaks down, and how non-exhaustive testing can help.

Why exhaustive testing?

Test assertions allow you to prove that certain values in your feature are what you expect them to be, but each assertion lies on a spectrum of strength. For example, if your feature has an “Add” button that when tapped adds an item to a collection, then you can write a test for that like so:

XCTAssertEqual(model.items.count, 0)
model.addButtonTapped()
XCTAssertEqual(model.items.count, 1)

That certainly proves that something was added to the items collection, but it doesn’t prove anything about the item.

We can strengthen this assertion by further asserting on the first item in the collection:

XCTAssertEqual(model.items.count, 0)
model.addButtonTapped()
XCTAssertEqual(model.items[0], Item(name: "", quantity: 1))

This is stronger since it now proves the first item has an empty string for its name and 1 for its quantity. But, it doesn’t prove anything about what else is in the items collection. What if there was a bug that caused two items to be added? This test would happily pass.

So, we can again strengthen this by asserting that the items array consists of only a single item:

model.addButtonTapped()
XCTAssertEqual(
  model.items,
  [Item(name: "", quantity: 1)]
)

And now this assertion is much stronger.

But it’s still not as strong as it could be. We are not asserting on how anything else in the model evolves over time. What if tapping the “Add” button also makes a network request to add the item in an external database, and while that request is in flight we show a progress view somewhere in the UI.

Since we are not asserting on any of that behavior there could be bugs in that code that are not covered by the test. It is on us to be studious enough to make sure to write an explicit test for that behavior, but nothing is guiding us towards doing so.

This is exactly what exhaustive testing aims to solve. You should not be allowed to write assertions like the above without also asserting on how the rest of the system evolves. You should be forced to assert on how each piece of state changes, as well as how each side effect executes and feeds data back into the system.

How to write exhaustive tests

Now that we know why exhaustive testing can be useful, let’s see how the Composable Architecture gives us exhaustive testing right out of the box.

Continuing with the example from above, to assert what happens when the user taps the “Add” button we construct a TestStore, send it an action, and provide a trailing closure that must mutate the previous state to the current state:

func testAddItem() async {
  let store = TestStore(
    initialState: Feature.State(),
    reducer: Feature()
  )

  await store.send(.addButtonTapped) {
    $0.items = [
      Item(name: "", quantity: 1)
    ]
  }
}

If this tests passes, it proves that the only mutation made to state when sending the action was an item being added to the items collection. If anything else was changed in the feature’s state we would get a test failure.

For example, suppose the feature has additional logic, such as making a network request to add the item on the backend database and managing some state for showing a progress view, then we would get a test failure because we have not asserted on how the entire feature evolves.

In fact, we would get two test failures. The first is due to the fact that we did not fully describe how the state changes:

🛑 A state change does not match expectation: …

      Feature.State(
    −   isAdding: false,
    +   isAdding: true,
        items: […]
      )

(Expected: −, Actual: +)

The failure message is clearly showing that some state changed that we did not assert against. In particular, the isAdding boolean, which will drive the showing and hiding of a progress indicator in the view, should be true, not false.

To fix, we need to mutate that state to its actual value in the send assertion:

await store.send(.addButtonTapped) {
  $0.isAddding = true
  $0.items = [
    Item(name: "", quantity: 1)
  ]
}

This fixes one test failure, but there’s still another:

🛑 The store received 1 unexpected action after this one: …

Unhandled actions: [
  [0]: Feature.Action.addResponse(success: true)
]

This is letting us know that the test store received an action from an effect, in particular the effect that communicates to our backend server, but we did not assert on it.

This is a great test failure to have. If the effect action is expected, then we should assert on how it fed back into the system and how state changed after, otherwise we are giving opportunities for bugs to hide. And just as important, if the effect action is not expected, then there is a bug in the logic causing an effect to execute, and so that should be fixed.

To fix this test we must assert on receiving this action as well as how state changes after receiving it:

await store.receive(.addResponse(success: true)) {
  $0.isAdding = false
}

The test now passes, and we can have a lot of confidence that we are asserting on everything happening in this feature, from state changes to effect execution. If the feature changes in the future by changing more state or executing more effects, we will instantly be notified in existing tests that more work needs to be done to assert on how the feature evolved.

When exhaustive testing breaks down

While exhaustive testing can be powerful, it also has its drawbacks. In particular, for very large, complex features, or features that are composed of many other features. In such cases it can be cumbersome to assert on every little state change and effect execution inside every single feature.

For this reason, the concept of a “non-exhaustive” test store was first conceived by Krzysztof Zabłocki in a blog post and a conference talk. It allows you to be more selective over which parts of the application you want to actually assert on.

For example, suppose you have a tab-based application where the 3rd tab is a login screen. The user can fill in some data on the screen, then tap the “Submit” button, and then a series of events happens to log the user in. Once the user is logged in, the 3rd tab switches from a login screen to a profile screen, and the selected tab switches to the first tab, which is an activity screen.

When writing tests for the login feature we will want to do that in the exhaustive style so that we can prove exactly how the feature would behave in production. But, suppose we wanted to write an integration test that proves after the user taps the “Login” button that eventually the selected tab switches to the first tab.

In order to test such a complex flow we must test the integration of multiple features, which means dealing with complex, nested state and effects. We can emulate this flow in a test by sending actions that mimic the user logging in, and then eventually assert that the selected tab switched to activity:

let store = TestStore(
  initialState: App.State(),
  reducer: App()
)

// 1️⃣ Emulate user tapping on submit button.
await store.send(.login(.submitButtonTapped)) {
  // 2️⃣ Assert how all state changes in the login feature
  $0.login?.isLoading = true
  ...
}

// 3️⃣ Login feature performs API request to login, and
//    sends response back into system.
await store.receive(.login(.loginResponse(.success))) {
  // 4️⃣ Assert how all state changes in the login feature
  $0.login?.isLoading = false
  ...
}

// 5️⃣ Login feature sends a delegate action to let parent
//    feature know it has successfully logged in.
await store.receive(.login(.delegate(.didLogin))) {
// 6️⃣ Assert how all of app state changes due to that action.
  $0.authenticatedTab = .loggedIn(
    Profile.State(...)
  )
  ...
  // 7️⃣ *Finally* assert that the selected tab switches to activity.
  $0.selectedTab = .activity
}

Doing this with exhaustive testing is verbose, and there are a few problems with this:

  • We need to have intimate knowledge on how the login feature works so that we can assert on how its state changes and how its effects feed data back into the system.
  • If the login feature were to change its logic we may get test failures here even though the logic we are acutally trying to test doesn’t really care about those changes.
  • This test is very long, and so if there are other similar but slightly different flows we want to test we will be tempted to copy-and-paste the whole thing, leading to lots of duplicated, fragile tests.

So, exhaustive testing can definitely be cumbersome, and this is what led Krzysztof Zabłocki to pursue “non-exaustive” test stores.

Introducing non-exhaustive testing

Non-exhaustive testing allows us to test the integration of many complex features, such as the situation above, without needing to assert on everything in the feature. We can be selective on which pieces we want to actually assert on, and only if we make an incorrect assertion do we actually get a test failure.

Take for example the above test, which wants to confirm that the selected tab switches to the activity tab after login. In order to do that we had to assert on all of the details of the login feature, including how state changed and effects executed.

Instead, we can set the store’s exhaustivity setting to .off, and then we get to decide what we want to actually assert on:

let store = TestStore(
  initialState: App.State(),
  reducer: App()
)
store.exhaustivity = .off // ⬅️

await store.send(.login(.submitButtonTapped))
await store.receive(.login(.delegate(.didLogin))) {
  $0.selectedTab = .activity
}

This test still passes and ignores all of the superfluous details that we do not care about for this particular integration test.

In particular, it proves when the “Submit” button is tapped in the login feature that eventually a .didLogin action is sent to indicate that log in succeeded, at which point the selected tab switches to .activity. We did not assert on how the login state changed, or how login effects executed. This means the login feature is free to make any changes it wants to its logic without affecting the outcome of this test, as long as it sends the .didLogin action when log in occurs.

The style of non-exhaustivity can even be customized. Using .off causes all un-asserted changes to pass without any notification. If you would like the test to pass but also see what test assertions are being suppressed, then you can specify the showSkippedAssertions flag:

let store = TestStore(
  initialState: App.State(),
  reducer: App()
)
store.exhaustivity = .off(showSkippedAssertions: true) // ⬅️

await store.send(.login(.submitButtonTapped))
await store.receive(.login(.delegate(.didLogin))) {
  $0.selectedTab = .profile
}

When this is set, running tests in Xcode will provide grey, informational boxes on each assertion where some change wasn’t fully asserted on:

◽️ A state change does not match expectation: …

     App.State(
       authenticatedTab: .loggedOut(
         Login.State(
   −       isLoading: false
   +       isLoading: true,
           …
         )
       )
     )

   (Expected: −, Actual: +)

◽️ Skipped receiving .login(.loginResponse(.success))

◽️ A state change does not match expectation: …

     App.State(
   −   authenticatedTab: .loggedOut(…)
   +   authenticatedTab: .loggedIn(
   +     Profile.State(…)
   +   ),
       …
     )

   (Expected: −, Actual: +)

The test still passes, and none of these notifications are test failures. They just let you know what things you are not explicitly asserting against, and can be useful to see when tracking down bugs that happen in production but that aren’t currently detected in tests.

Start using non-exhaustive test stores today!

All of these tools (and more) are available in 0.45.0 of the library, which is available today. Be sure to upgrade and read the testing documentation for more information on how exhaustive and non-exhaustive test stores work.


Open Sourcing swift-clocks

Monday Oct 24, 2022

Over the past couple weeks we explored Swift 5.7’s new Clock protocol, compared it to Combine’s Scheduler protocol, and defined a whole bunch of useful conformances.

We believe these tools are highly applicable to everyone working with time-based asynchrony in Swift, and so today we are excited to release them in a new open source library.

Motivation

The Clock protocol in Swift provides a powerful abstraction for time-based asynchrony in Swift’s structured concurrency. With just a single sleep method you can express many powerful async operators, such as timers, debounce, throttle, timeout and more (see swift-async-algorithms).

However, the moment you use a concrete clock in your asynchronous code, or use Task.sleep directly, you instantly lose the ability to easily test and preview your features, forcing you to wait for real world time to pass to see how your feature works.

Tools

This library provides new Clock conformances that allow you to turn any time-based asynchronous code into something that is easier to test and debug:

TestClock

This is a clock whose time can be controlled in a deterministic manner.

This clock is useful for testing how the flow of time affects asynchronous and concurrent code. This includes any code that makes use of sleep or any time-based async operators, such as debounce, throttle, timeout, and more.

For example, suppose you have a model that encapsulates the behavior of a timer that can be started and stopped, and with each tick of the timer a count value is incremented:

@MainActor
class FeatureModel: ObservableObject {
  @Published var count = 0
  let clock: any Clock<Duration>
  var timerTask: Task<Void, Error>?

  init(clock: any Clock<Duration>) {
    self.clock = clock
  }
  func startTimerButtonTapped() {
    self.timerTask = Task {
      while true {
        try await self.clock.sleep(for: .seconds(1))
        self.count += 1
      }
    }
  }
  func stopTimerButtonTapped() {
    self.timerTask?.cancel()
    self.timerTask = nil
  }
}

Note that we have explicitly forced a clock to be provided in order to construct the FeatureModel. This makes it possible to use a real life clock, such as ContinuousClock, when running on a device or simulator, and use a more controllable clock in tests, such as the TestClock.

To write a test for this feature we can construct a FeatureModel with a TestClock, then advance the clock forward and assert on how the model changes:

func testTimer() async {
  let clock = TestClock()
  let model = FeatureModel(clock: clock)

  XCTAssertEqual(model.count, 0)
  model.startTimerButtonTapped()

  // Advance the clock 1 second and prove that the model's
  // count incremented by one.
  await clock.advance(by: .seconds(1))
  XCTAssertEqual(model.count, 1)

  // Advance the clock 4 seconds and prove that the model's
  // count incremented by 4.
  await clock.advance(by: .seconds(4))
  XCTAssertEqual(model.count, 5)

  // Stop the timer, run the clock until there is no more
  // suspensions, and prove that the count did not increment.
  model.stopTimerButtonTapped()
  await clock.run()
  XCTAssertEqual(model.count, 5)
}

This test is easy to write, passes deterministically, and takes a fraction of a second to run. If you were to use a concrete clock in your feature, such a test would be difficult to write. You would have to wait for real time to pass, slowing down your test suite, and you would have to take extra care to allow for the inherent imprecision in time-based asynchrony so that you do not have flakey tests.

ImmediateClock

A clock that does not suspend when sleeping.

This clock is useful for squashing all of time down to a single instant, forcing any sleeps to execute immediately. For example, suppose you have a feature that needs to wait 5 seconds before performing some action, like showing a welcome message:

struct Feature: View {
  @State var message: String?

  var body: some View {
    VStack {
      if let message = self.message {
        Text(self.message)
      }
    }
    .task {
      do {
        try await Task.sleep(for: .seconds(5))
        self.message = "Welcome!"
      } catch {}
    }
  }
}

This is currently using a real life clock by calling out to Task.sleep, which means that while iterating on the styling and behavior of this feature in an Xcode preview you will have to wait for 5 real life seconds to pass before you see the effect. This will severely hurt you ability to quickly iterate on the feature in an Xcode preview.

The fix is to have your view hold onto a clock so that it can be controlled from the outside:

struct Feature: View {
  @State var message: String?
  let clock: any Clock<Duration>

  var body: some View {
    VStack {
      if let message = self.message {
        Text(self.message)
      }
    }
    .task {
      do {
        try await self.clock.sleep(for: .seconds(5))
        self.message = "Welcome!"
      } catch {}
    }
  }
}

Then you can construct this view with a ContinuousClock when running on a device or simulator, and use an ImmediateClock when running in an Xcode preview:

struct Feature_Previews: PreviewProvider {
  static var previews: some View {
    Feature(clock: ImmediateClock())
  }
}

Now the welcome message will be displayed immediately with every change made to the view. No need to wait for 5 real world seconds to pass.

You can also propagate a clock to a SwiftUI view via the continuousClock and suspendingClock environment values that ship with the library:

struct Feature: View {
  @State var message: String?
  @Environment(\.continuousClock) var clock

  var body: some View {
    VStack {
      if let message = self.message {
        Text(self.message)
      }
    }
    .task {
      do {
        try await self.clock.sleep(for: .seconds(5))
        self.message = "Welcome!"
      } catch {}
    }
  }
}

struct Feature_Previews: PreviewProvider {
  static var previews: some View {
    Feature()
      .environment(\.continuousClock, ImmediateClock())
  }
}

UnimplementedClock

A clock that causes an XCTest failure when any of its endpoints are invoked.

This clock is useful when a clock dependency must be provided to test a feature, but you don’t actually expect the clock to be used in the particular execution flow you are exercising.

For example, consider the following model that encapsulates the behavior of being able to increment and decrement a count, as well as starting and stopping a timer that increments the counter every second:

@MainActor
class FeatureModel: ObservableObject {
  @Published var count = 0
  let clock: any Clock<Duration>
  var timerTask: Task<Void, Error>?

  init(clock: any Clock<Duration>) {
    self.clock = clock
  }
  func incrementButtonTapped() {
    self.count += 1
  }
  func decrementButtonTapped() {
    self.count -= 1
  }
  func startTimerButtonTapped() {
    self.timerTask = Task {
      for await _ in self.clock.timer(interval: .seconds(1)) {
        self.count += 1
      }
    }
  }
  func stopTimerButtonTapped() {
    self.timerTask?.cancel()
    self.timerTask = nil
  }
}

If we test the flow of the user incrementing and decrementing the count, there is no need for the clock. We don’t expect any time-based asynchrony to occur. To make this clear, we can use an UnimplementedClock:

func testIncrementDecrement() {
  let model = FeatureModel(clock: UnimplementedClock())

  XCTAssertEqual(model.count, 0)
  self.model.incrementButtonTapped()
  XCTAssertEqual(model.count, 1)
  self.model.decrementButtonTapped()
  XCTAssertEqual(model.count, 0)
}

If this test passes it definitively proves that the clock is not used at all in the user flow being tested, making this test stronger. If in the future the increment and decrement endpoints start making use of time-based asynchrony using the clock, we will be instantly notified by test failures. This will help us find the tests that should be updated to assert on the new behavior in the feature.

Timers

The library ships with a method defined on all clocks that allows you to create an AsyncSequence-based timer on an interval specified by a duration. This allows you to handle timers with simple for await syntax, such as this observable object that exposes the ability to start and stop a timer for incrementing a value every second:

@MainActor
class FeatureModel: ObservableObject {
  @Published var count = 0
  let clock: any Clock<Duration>
  var timerTask: Task<Void, Error>?

  init(clock: any Clock<Duration>) {
    self.clock = clock
  }
  func startTimerButtonTapped() {
    self.timerTask = Task {
      for await _ in self.clock.timer(interval: .seconds(1)) {
        self.count += 1
      }
    }
  }
  func stopTimerButtonTapped() {
    self.timerTask?.cancel()
    self.timerTask = nil
  }
}

This feature can also be easily tested by making use of the TestClock discussed above:

func testTimer() async {
  let clock = TestClock()
  let model = FeatureModel(clock: clock)

  XCTAssertEqual(model.count, 0)
  model.startTimerButtonTapped()

  await clock.advance(by: .seconds(1))
  XCTAssertEqual(model.count, 1)

  await clock.advance(by: .seconds(4))
  XCTAssertEqual(model.count, 5)

  model.stopTimerButtonTapped()
  await clock.run()
}

Try it today

swift-clocks 0.1.0 is available today! Give it a spin to introduce controllable, time-based asynchrony to your Swift applications. And if you’re a user of the Composable Architecture, try out the latest release to bring these tools to your reducers!


Older blog posts

Monday Oct 10, 2022

Announcing the Reducer Protocol

Today we are releasing the biggest update to the Composable Architecture ever, completely reimagining how features are built with the library.

Thursday Sep 8, 2022

Improving Composable Architecture performance

The latest release of the Composable Architecture brings a number of performance improvements to its tools, and best of all, most of the changes came from collaboration with people in the TCA community! 🤗

Monday Aug 8, 2022

Async Composable Architecture

Today we are releasing the biggest update to the Composable Architecture since it's first release over 2 years ago. The library has all new concurrency tools allowing you to construct complex effects using structured concurrency, tie effect lifetimes to view lifetimes, and we accomplish all of this while keeping your code 100% testable. We think it might even be the best way to test concurrent code in SwiftUI applications. 😇

Tuesday Jul 12, 2022

Reverse Engineering SwiftUI’s NavigationPath Codability

Learn how to use Swift 5.7's new existential type super powers to encode and decode type-erased values.

Wednesday Jun 29, 2022

Introducing XCTUnimplemented

We've added a new tool to our XCTest Dynamic Overlay library, which makes it easier to construct stronger dependencies for tests.

Monday Jun 6, 2022

WWDC Sale 2022

The year's biggest Apple event is here, and to celebrate we are offering a 25% discount off the first year for first-time subscribers.

Monday May 2, 2022

Open Sourcing URLRouting and VaporRouting

Introducing new routing libraries that make client-side and server-side routing easy with more type safety and less fuss.

Monday Apr 11, 2022

Parser-printer unification

A new release of swift-parsing brings printing capabilities to your parsers for transforming structured data back into unstructured data.

Monday Feb 14, 2022

Parser Errors

A new release of swift-parsing that brings delightful and informative error messaging to parser failures.

Wednesday Feb 9, 2022

Backtracking Parsers

Today we are releasing 0.6.0 of our swift-parsing library that changes its backtracking behavior.

Monday Jan 24, 2022

Introducing Parser Builders

Today we are releasing 0.5.0 of our swift-parsing library, which leverages result builders for creating complex parsers with a minimal amount of syntactic noise. Learn how in this week's blog post, and give the library a spin today!

Monday Jan 3, 2022

Unobtrusive runtime warnings for libraries

Runtime warnings in libraries are a great way to notify your users that something unexpected has happened, or that an API is being used in an incorrect manner. In this post we give an overview of some techniques that can be employed easily today, as well as discuss a technique for surfacing runtime warnings that is both very visible and unobtrusive.

Wednesday Dec 22, 2021

2021 Year-in-review

Point-Free year in review: 42 episodes, 72k visitors, 5 new open source projects, and more!

Tuesday Nov 16, 2021

Open Sourcing SwiftUI Navigation

Today we are open sourcing SwiftUI Navigation, a collection of tools for making SwiftUI navigation simpler, more ergonomic and more precise.

Tuesday Oct 26, 2021

Give the Gift of Point-Free

Today we are excited to announce that you can now gift a Point-Free subscription to your friends, colleagues and loved ones.

Wednesday Sep 15, 2021

Point Freebies: Swift Concurrency and More

We're celebrating the release of Xcode 13 by making all of our WWDC 2021 videos free! Explore SwiftUI's new refreshable and FocusState APIs, both in the context of vanilla SwiftUI and the Composable Architecture, and learn how to build a map-powered application from scratch using the new searchable API.

Monday Sep 6, 2021

The Composable Architecture ❤️ SwiftUI Bindings

Today we are improving the Composable Architecture's first-party support for SwiftUI bindings with a safer, even conciser syntax.

Monday Aug 23, 2021

Open Sourcing: Custom Dump

Today we are open sourcing Custom Dump, a collection of tools for debugging, diffing, and testing your application's data structures.

Wednesday Jul 14, 2021

Better Performance Bonanza

The past 3 weeks we've shipped updates to 3 of our libraries, focused on improving the performance of your Composable Architecture applications, and more!

Monday Jul 12, 2021

Open Sourcing Identified Collections

Today we are open sourcing Identified Collections, a library of data structures for working with collections of identifiable elements in a performant way.

Monday Jun 14, 2021

Announcing SwitchStore for the Composable Architecture

We are adding new tools for handling enum state in the Composable Architecture, with a focus on safety and performance.

Wednesday May 12, 2021

A Tour of isowords

This past month we released four completely free videos dedicated to diving into the real-world Swift code base of an iOS game we recently launched and open sourced: isowords.

Monday Mar 22, 2021

Better Testing Bonanza

We're open sourcing a library that makes it easier to be more exhaustive in writing tests.

Wednesday Mar 17, 2021

Open Sourcing isowords

We're open sourcing the entire code base to our newly released iOS word game, isowords!

Wednesday Mar 17, 2021

Announcing: isowords

We are excited to release isowords to the App Store, a new word search game for your phone. Download today!

Monday Mar 8, 2021

Composable Architecture Test Store Improvements

Composable Architecture 0.16.0 comes with significant improvements to its testing capabilities for tracking down effect-related failures.

Monday Feb 1, 2021

Composable Forms: Say "Bye" to Boilerplate!

Today we are releasing first-party support for concisely handling form data in the Composable Architecture.

Wednesday Dec 23, 2020

2020 Year-in-review

The Composable Architecture, dependency management, parsers, Combine schedulers and more! Join us for a review of everything we accomplished in 2020!

Monday Dec 21, 2020

Open Sourcing Parsing

Today we are open sourcing Parsing, a library for turning nebulous data into well-structured data, with a focus on composition, performance, and generality.

Tuesday Jun 30, 2020

The Composable Architecture and SwiftUI Alerts

Today we are releasing a new version of the Composable Architecture with helpers that make working with SwiftUI alerts and action sheets a breeze.

Monday Jun 22, 2020

Core Motion support in the Composable Architecture

We are releasing our second mini-library for the Composable Architecture, which makes it easy to use Core Motion.

Monday Jun 15, 2020

Open Sourcing CombineSchedulers

Today we are open-sourcing CombineSchedulers, a library that introduces a few schedulers that makes working with Combine more testable and more versatile.

Wednesday May 27, 2020

Instrumenting features built in the Composable Architecture

Today we are releasing first-party support for instrumenting features built in the Composable Architecture.

Wednesday May 20, 2020

Core Location support in the Composable Architecture

We are releasing a mini-library that makes it easy to use Core Location inside the Composable Architecture.

Tuesday May 12, 2020

Regional Discounts

Regional discounts takes 50% off a monthly or yearly personal subscription to anyone whose credit card is issued from a particular list of countries.

Monday May 4, 2020

Composable Architecture, the library

Today we are releasing the Composable Architecture as an open-source library. It is a way to build applications in a consistent and understandable way, with composition, testing and ergonomics in mind.

Wednesday Mar 11, 2020

Announcing Episode Collections

After over two years and nearly 100 episodes we are finally launching episode collections on Point-Free!

Friday Feb 21, 2020

Share Point-Free with friends and save!

Today we're excited to announce support for referral bonuses! When friends and colleagues of yours subscribe to Point-Free with your referral link, both of you will get one month free.

Tuesday Feb 4, 2020

Open Sourcing Case Paths

Today we are open sourcing CasePaths, a library that introduces the power and ergonomics of key paths to enums!

Monday Dec 30, 2019

2019 Year-in-review

Random number generators, parsers, SwiftUI, composable architecture and more! Join us for a review of everything we accomplished in 2019!

Monday Dec 23, 2019

Snapshot Testing SwiftUI

Snapshot testing gives us broad test coverage on our SwiftUI views with very little up front work.

Wednesday Dec 18, 2019

Free Video: Testing SwiftUI

A free video exploring how to test SwiftUI.

Wednesday Nov 20, 2019

A Crash Course in Combine

Two free videos exploring Apple's new Combine framework, its core components, and how to integrate it in your code.

Thursday Nov 7, 2019

Higher-Order Snapshot Testing

How to enrich snapshot testing strategies with additional behavior using higher-order constructions.

Tuesday Jul 30, 2019

SwiftUI and State Management Corrections

Xcode 11 beta 5 has brought lots of changes to SwiftUI, and we'd like to take a moment to provide corrections to our episodes based on these changes.

Thursday May 9, 2019

Enterprise Subscriptions

Point-Free now supports enterprise subscriptions, making it easier than ever to manage a team subscription for larger organizations! For a fixed yearly rate, everyone with an email from your company's domain will get instant access to everything Point-Free has to offer. Contact us for more info!

Monday Apr 29, 2019

Open Sourcing Enum Properties

We wanted to make Swift enum data access as ergonomic as struct data access, so today we are open sourcing a code generation tool to do just that!

Monday Mar 18, 2019

Open Sourcing Gen

Today we are open sourcing Gen: a lightweight wrapper around Swift's randomness API's that makes randomness more composable, transformable and controllable!

Tuesday Jan 8, 2019

Announcing swift-html 0.2.0

Announcing swift-html 0.2.0: support for CocoaPods, Carthage, SnapshotTesting, and more!

Wednesday Dec 19, 2018

2018 Year-in-Review

41 episodes, 19 hours of video, 25 blog posts, 8 open source libraries, 3.8K stars, 36K visitors, and we’re just getting started?

Monday Dec 3, 2018

SnapshotTesting 1.0: Delightful Swift snapshot testing

Today we are open sourcing SnapshotTesting 1.0: a modern, composable snapshot testing library built entirely in Swift!

Monday Oct 29, 2018

Some news about contramap

We've seen that contramap is a powerful operation, but the name isn't fantastic. We propose a much more intuitive name for this operation, and in doing so make our code much easier to read.

Tuesday Oct 9, 2018

How to Control the World

APIs that interact with the outside world are unpredictable and make it difficult to test and simulate code paths in our apps. Existing solutions to this problem are verbose and complicated, so let's explore a simpler solution by embracing singletons and global mutation, and rejecting protocol-oriented programming and dependency injection.

Monday Oct 8, 2018

Watch episodes in your favorite podcast app!

Follow along with the newest Point-Free episodes using your favorite podcast app. We now support podcast-friendly RSS feeds for viewing all of our videos.

Thursday Sep 20, 2018

Random Zalgo Generator

Let's create a random Zalgo text generator using the simple Gen type we defined in this week's episode!

Thursday Sep 13, 2018

Type-safe HTML with Kitura

Today we're releasing a Kitura plug-in for rendering type-safe HTML. It provides a Swift compile-time API to HTML that prevents many of the runtime errors and vulnerabilities of traditional templated HTML rendering.

Thursday Sep 13, 2018

Type-safe HTML with Vapor

Today we're releasing a Vapor plug-in for rendering type-safe HTML. It provides a Swift compile-time API to HTML that prevents many of the runtime errors and vulnerabilities of traditional templated HTML rendering.

Wednesday Sep 12, 2018

Open sourcing swift-html: A Type-Safe Alternative to Templating Languages in Swift

Today we are open sourcing a new library for building HTML documents in Swift. It's extensible, transformable, type-safe, and provides many benefits over templating languages.

Friday Aug 17, 2018

Overture 0.3.0: Now with Zip

Today we are releasing Overture 0.3.0 with a bunch of useful zip functions.

Friday Aug 17, 2018

Open Sourcing Validated

Today we are open sourcing Validated, a tiny functional Swift library for handling multiple errors: functionality that you don't get from throwing functions and the Result type.

Thursday Aug 16, 2018

Solutions to Exercises: Zip Part 3

Today we solve the exercises to the third and final part of our introductory series on zip.

Wednesday Aug 15, 2018

Solutions to Exercises: Zip Part 2

Today we solve the exercises to the second part of our introductory series on zip.

Tuesday Aug 14, 2018

Solutions to Exercises: Zip Part 1

Today we solve the exercises to the first part of our introductory series on zip.

Monday Aug 6, 2018

Announcing Student Discounts

Get 50% off your Point-Free subscription with proof of enrollment at a university or coding school.

Monday Jul 30, 2018

Celebrating 6 Months

This week marks 6 months since our launch, and we’re making one of our most popular episodes free to the public!

Monday Jul 2, 2018

Conditional Coding

What happens when we combine Swift's conditional conformance with codability?

Monday Jun 25, 2018

Open Sourcing NonEmpty

Today we are open sourcing NonEmpty, a Swift library for modeling non-empty collection types. This small library can help make your code safer and more expressive with very little work.

Monday Jun 18, 2018

Tagged Seconds and Milliseconds

Let's create a type-safe interface for dealing with seconds and milliseconds in our programs. We'll use the `Tagged` type, which allows us to construct all new types in a lightweight way.

Wednesday May 30, 2018

Styling with Functions: Free for Everyone!

We are making one of our early episodes, “UIKit Styling with Functions”, free to everyone today! It’s a seminal episode that sets the foundation for some later work in the Point-Free series.

Tuesday May 15, 2018

Overture: Now with Functional Setters

Announcing Overture 0.2.0! This release is all about setters: functions that allow us to build complex transformations out of smaller units.

Monday May 7, 2018

Solutions to Exercises: Contravariance

This week we solve the exercises from our episode on contravariance, because there were _a lot_ of them!

Monday Apr 23, 2018

Case Study: Algebraic Data Types

Let’s look at a real world use for algebraic data types. We will refactor a data type that is used in the code on this very site so that the invalid states are unrepresentable by the compiler.

Monday Apr 23, 2018

Announcing Point-Free Pointers!

Today we are excited to announcement launch of Point-Free Pointers, a blog to supplement our video series for all the content we couldn’t fit in. Expect to find regularly postings here that dive even deeper into functional programming, showing real world use cases and more!