What’s the difference between Float and Double in Swift

A Double and Float are both used to represent decimal numbers, but they do so in slightly different ways.

If you initialize a decimal number in Swift using as shown below, the Swift compiler will assume that you meant to create a Double:

let val = 3.123 // val is inferred to be Double

The reason for this is that Double is the more precise type when comparing it to Float. A Float holds a total of 8 positions, or 32 bits. Since Double is more precise, it can hold more positions. It uses 64 bits to do this. In practice, that means the following:

print(Double.pi) // 3.141592653589793
print(Float.pi) // 3.1415925

As you can see, Double can represent pi far more accurately than Float. We can make the difference more obvious if we multiply pi by 1000:

print(Double.pi * 1000) // 3141.592653589793
print(Float.pi * 1000) // 3141.5925

Both Double and Float sacrifice some after the comma precision when you multiply pi by 1000. For Float this means that it only has four decimal places while Double still has twelve.

How Do Swift Property Wrappers Work?

Property wrappers are a feature that was introduced in Swift 5.1 and they play a huge role in SwiftUI and Combine which are two frameworks that shipped alongside Swift 5.1 in iOS 13. The community was quick to create some useful examples that were embraced by folks relatively quickly.

As a user of property wrappers, you don't need to be concerned about what they are exactly, or how they work. All that you need to know is how you can use them. However, if you're curious how property wrappers work on the inside, this is just the post for you.

This week I would like to take a deep dive into property wrappers and take you on a journey to see how they work exactly.

Property wrappers look a lot like macros, but they're not the same thing. If you'd like to learn more about their differences, take a look at my comparison post here.

Why do we need property wrappers?

The reason that the Swift team wanted to add property wrappers to the Swift language is to help facilitate common patterns that are applied to properties all the time. If you've ever marked a property as lazy, you've used such a pattern. The Swift compiler knows how to handle lazy, and all the code needed to expand your lazy keyword into code that actually makes your property lazy is hardcoded into the compiler.

Since there are many more of these patterns that can be applied to properties, hardcoding all of them wouldn't make sense. Especially since one of the goals of property wrappers is to allow developers to provide their own patterns in the form of property wrappers.

Let's back up and zoom in on the lazy keyword. I just mentioned that the compiler expands your code into other code that makes your property lazy. Let's take a look at what this would look like if we didn't have the lazy keyword, and we'd have to write a lazy property ourselves.

This example is taken directly from the Swift evolution proposal and slightly modified for readability:

struct MyObject {
  private var _myProperty: Int?

  var myProperty: Int {
    get {
      if let value = _myProperty { return value }
      let initialValue = 1738
      _myProperty = initialValue
      return initialValue
    }
    set {
      _myProperty = newValue
    }
  }
}

Notice that this code is far more verbose than just writing lazy var myProperty: Int?.

Capturing a large number of these patterns in the compiler isn't desirable, and it's also not very extensible. The Swift team wanted to allow themselves, and developers to use keywords to define their own patterns that are similar to lazy to help them clean up their code, and make their code more expressive.

Note that property wrappers do not allow developers to do otherwise impossible things. They merely allow developers to express patterns and intent using a more expressive syntax. Let's move on and look at an example.

Picking apart a property wrapper

A property wrapper that I use a lot is the @Published property wrapper from Combine. This property wrapper converts the property that it's applied to into a publisher that will notify subscribers when you change that property's value. This property wrapper is used like this:

class MyObject {
  @Published var myProperty = 10
}

Fairly straightforward, right?

To access the created publisher I need to use a $ prefix on myProperty. So $myProperty. The myProperty property points to the underlying value which is an Int with a value of 10 by default in this case. There's also a second prefix that can be applied to a property wrapper which is _, so _myProperty. This is a private property so it can only be accessed from within MyObject in this case but it tells us a lot about how property wrappers work. In the case of the MyObject example above, _myProperty is a Published<Int>. $myProperty is a Published.Publisher and myProperty is an Int. So that single line of code results in three different kinds of properties we can access. Let's define a custom property wrapper and find out what each of these three properties is, and what it does.

@propertyWrapper
struct ExampleWrapper<Value> {
  var wrappedValue: Value
}

This property wrapper is very minimal, and not very useful at all. However, it's good enough for us to explore the anatomy of a property wrapper.

First, notice that the ExampleWrapper struct has an annotation on the line before its definition: @propertyWrapper. This annotation means that the struct that's defined after it is a property wrapper. Also note that the ExampleWrapper is generic over Value. This Value is the type of the wrappedValue property.

Property wrappers don't have to be generic. You can hardcode the type of wrappedValue if needed. You could hardcode the wrappedValue if you want your property wrapper to only work for a specific type. Alternatively, you can constrain the type of Value if needed.

The wrappedValue property is required for a property wrapper. All property wrappers must have a non-static property called wrappedValue.

Let's put this ExampleWrapper to work:

class MyObject {
  @ExampleWrapper var myProperty = 10

  func allVariations() {
    print(myProperty)
    //print($myProperty)
    print(_myProperty)
  }
}

let object = MyObject()
object.allVariations()

Notice that I have commented out $myProperty. I will explain why in a moment.

When you run this code, you would see the following printed in Xcode's console:

10
ExampleWrapper<Int>(wrappedValue: 10)

myProperty still prints as 10. Accessing the property that's marked with a property wrapper directly will print the wrappedValue property of the property wrapper. When you print _myProperty, you access the property wrapper object itself. Notice that _myProperty is a member of MyObject. You can type self._myProperty and Swift will know what to do, even though you never explicitly defined _myProperty yourself. I mentioned earlier that _myProperty is private so you can't access it from outside of MyObject but it's there.

The reason is that the Swift compiler will take that @ExampleWrapper var myProperty = 10 line and convert in something else behind the curtain:

private var _myProperty: ExampleWrapper<Int> = ExampleWrapper<Int>(wrappedValue: 10)

var myProperty: Int {
  get { return _myProperty.wrappedValue }
  set { _myProperty.wrappedValue = newValue }
}

There are two things that we can learn from this example. First, you can see that property wrappers really aren't magic. They are actually relatively straightforward. This doesn't make them simple, or easy, but once you know that this conversion from a single definition is exploded into two separate definitions it suddenly becomes a lot easier to reason about.s

The _myProperty isn't some kind of magic value. It's a real member of MyObject that's created by the compiler. And myProperty returns the value of wrappedValue because it's hardcoded that way. Not by us, but by the compiler.

The _myProperty property is called the synthesized storage property. It's where the property wrapper which provides the storage for its wrapped value exists.

So where's $myProperty?

Not all property wrappers come with a $ prefixed flavor. The $ prefixed version of a property wrapped property is called the projected value. The projected value can be useful to provide a special, or different interface for a specific property wrapper like @Published does for example. To add a projected value to a property wrapper you must implement a projectedValue property on the property wrapper definition.

In MyExampleWrapper this would look as follows:

@propertyWrapper
struct ExampleWrapper<Value> {
  var wrappedValue: Value

  var projectedValue: Value {
    get { wrappedValue }
    set { wrappedValue = newValue }
  }
}

This example isn't useful at all, I will show you a more useful example in the next section. For now, I want to show you the anatomy of a property wrapper without any bells and whistles.

If you'd use this property wrapper like before, Swift will generate the following code for you:

private var _myProperty: ExampleWrapper<Int> = ExampleWrapper<Int>(wrappedValue: 10)

var myProperty: Int {
  get { return _myProperty.wrappedValue }
  set { _myProperty.wrappedValue = newValue }
}

var $myProperty: Int {
  get { return _myProperty.projectedValue }
  set { _myProperty.projectedValue = newValue }
}

An extra property is created that uses the private _myProperty's projectedValue for it's get and set implementations.

Since _myProperty is private, your projected value might provide direct access to the property wrapper which is one of the examples shown in the original property wrapper proposals. Alternatively, you could expose a completely different object as your property wrapper's projected value. It's up to you to make this choice. The @Published property wrapper uses its projectedValue to expose a publisher.

Implementing a property wrapper

I have already shown you how to define a simple property wrapper, but let's be honest. That example was boring and kind of bad. In this section, we'll look at implementing a custom property wrapper that mimics the behavior of Combine's @Published property wrapper. If you want to learn about Combine I have several posts about it in the Combine section of this blog.

Let's define the basics first:

@propertyWrapper
struct DWPublished<Value> {
  var wrappedValue: Value
}

This defines a property wrapped that wraps any kind of value. That's good. The goal here is to implement a projected value that exposes some kind of publisher. I will use a CurrentValueSubject for this. Whenever wrappedValue gets a new value, the CurrentValueSubject should emit a new value to its subscribers. A basic implementation might look like this:

@propertyWrapper
class DWPublished<Value> {
  var wrappedValue: Value {
    get { subject.value }
    set { subject.value = newValue }
  }

  private let subject: CurrentValueSubject<Value, Never>

  var projectedValue: CurrentValueSubject<Value, Never> {
    get { subject }
  }

  init(wrappedValue: Value) {
    self.subject = CurrentValueSubject(wrappedValue)
  }
}

Warning:
This implementation is very basic and should not be used as a reference for how @Published is actually implemented. I'm sure there might be bugs with this code. My goal is to help you understand how property wrappers work. Not to show you a perfect custom @Published property wrapper.

This code is vastly different from what you've seen before. The wrappedValue used the private subject to implement its get and set. This means that the wrapped value is always in sync with the subject's current value.

The projectedValue only has it's get specified. We don't want users of this property wrapper to assign anything to projectedValue; it's read-only.

When a property wrapper is initialized in its simplest form, it receives its wrapped value. The wrapped value passed to DWPublished is used to set up the subject with the value we're supposed to wrap as its initial value.

Using this property wrapper would look like this:

class MyObject {
  @DWPublished var myValue = 1
}

let obj = MyObject()
obj.$myValue.sink(receiveValue: { int in
  print("received int: \(int)")
})

obj.myValue = 2

The printed output for this example would be:

received int: 1
received int: 2

Pretty neat, right?

Since the property wrapper's projected value is a CurrentValueSubject, it has a value property that we can assign values to. If I'd do this, the property wrapper's wrappedValue is also updated because the CurrentValueSubject is used to drive the wrappedValue of my property wrapper.

obj.$myValue.value = 3
print(obj.myValue) // 3

This is something that's not possible with the @Published property wrapped because Apple exposes the projectedValue for @Published as a custom type called Published.Publisher instead of a CurrentValueSubject.

A more complicated property wrapper might take some kind of configuration, like a maximum or minimum value. Let's say I want to expand my @DWPublished property wrapper to limit its output by debouncing it. I would like to write the following code in MyObject to configure this:

class MyObject {
  @DWPublished(debounce: 0.3) var myValue = 1
}

This would debounce my published values with 300 milliseconds. We can update the initializer for DWPublished to accept this argument, and refactor the code a little bit:

@propertyWrapper
class DWPublished<Value> {
  var wrappedValue: Value {
    get { subject.value }
    set { subject.value = newValue }
  }

  private let subject: CurrentValueSubject<Value, Never>
  private let publisher: AnyPublisher<Value, Never>

  var projectedValue: AnyPublisher<Value, Never> {
    get { publisher }
  }

  init(wrappedValue: Value, debounce: DispatchQueue.SchedulerTimeType.Stride) {
    self.subject = CurrentValueSubject(wrappedValue)
    self.publisher = self.subject
      .debounce(for: debounce, scheduler: DispatchQueue.global())
      .eraseToAnyPublisher()
  }
}

The initializer for my property wrapper now accepts the debounce interval and uses this interval to create an all-new publisher that debounces my CurrentValueSubject. I erase this publisher to AnyPublisher so I have a nice type for my publisher instead of Publishers.Debounce<CurrentValueSubject<Value, Never>, S> where S : Scheduler which would be the type of my publisher if I didn't erase it.

My property wrapper's wrappedValue still shadows subject.value. The projectedValue now uses the debounced publisher for its get instead of the CurrentValueSubject.

Using this property wrapper now looks as follows:

class MyObject {
  @DWPublished(debounce: 0.3) var myValue = 1
}

var cancellables = Set<AnyCancellable>()

let obj = MyObject()
obj.$myValue
  .sink(receiveValue: { int in
    print("received int: \(int)")
  })
  .store(in: &cancellables)

obj.myValue = 2
obj.myValue = 3
obj.myValue = 4

If you would run this in a playground, only received int: 4 should be printed. That's the debouncer at work and it's exactly what I wanted to happen.

Note that because the property wrapper's projected value is now an AnyPublisher, it's no longer possible to assign new values using $myValue.value like we could before.

In summary

In this week's post, you saw how property wrappers work internally, and what happens when you use a property wrapper. I showed you that the Swift compiler generates code on your behalf and that a property wrapper is far from magic. Swift generates a _ prefixed private property that's an instance of your property wrapper, a $ prefixed property that shadows the private property's projectedValue property, and that the original property shadows the wrappedValue property of the _ prefixed private property.

Once you understand this, you can quickly see how property wrappers work and how they might be implemented. I demonstrated this by implementing my own version of Combine's @Published property wrapper. After that, I showed you how to create a property wrapper that can be configured with extra arguments by expanding your property wrapper's initializer.

I hope that you have a much clearer picture of how property wrappers work now and that using them feels less like magic. For questions or feedback, I would love to hear from you on Twitter.

Five tips to help you become a well-rounded developer

This week I wanted to write about something non-technical. And while the topic of this week's post isn't a technical one, I think it's an important topic for developers who want to expand their knowledge, and deepen their skills.

I have been a developer professionally for more than ten years at this point and in these ten years there are some fundamental lessons I have learned that I believe have helped me get where I am today.

In this week's post, I will share five tips with you that have made into the developer I am today, and I strongly believe that these tips can help you become a more well-rounded developer.

Tip 1: Read books

Some of my fundamental knowledge and programming principles come from books like The Pragmatic Programmer, and Design Patterns. None of these books are about Swift, or iOS Development. In fact, they predate iOS and Swift by many years. However, these books contain fundamental knowledge and experiences that apply to virtually every project you will work on.

The books I have mentioned specifically, will not teach you how to do X or Y. Instead, they contain tons of lessons, examples, principles and information that can help you see programming in the bigger picture. These books can help you put what we're doing today in Swift in a much broader perspective.

And even if you pick up a book about Swift development, like the ones Paul Hudson writes, or one of the books from objc.io or Ray Wenderlich, you will grow as a developer. By reading a complete book from an author you don't just learn the skills they teach. A good book about programming will also give you a glimpse inside of the author's brain. You will learn how they think about programming, and why they teach the code the way they do. Daniel Steinberg is an author who, in my opinion, is exceptionally good at this.

Over the years I have read dozens of programming books and the one thing that they all had in common is that I learned something more than just the concepts I read about. I learned new ways of thinking. I learned new ways to approach problems. And, equally important, my knowledge on the topic I read about got a huge boost.

2. Don't rely on tutorials all the time

Tutorials are a fantastic way to explore something new. Or to learn if you're just getting started. But there comes a point where following tutorial after tutorial won't teach you a lot anymore.

Where books will often help you understand something in depth, or help you see the bigger picture, tutorials are often much shorter publications that simply show you how to get the job done.

Tutorials often don't go too deep into any trade-offs, the reasoning behind certain decisions or how to apply the skills learned in a different context. The tutorial starts with a cool example, the code needed to implement the example is shown, and in the end you have replicated the example 1:1.

This is great because it gives you a sense of accomplishment, but how often do you come out of a tutorial having actually learned something new? Of course, if you're just starting out you will gain tons from tutorials. But there comes a moment where tutorials don't introduce you to any new tools, or where they don't introduce you to new language constructs anymore.

If you feel that this is the case for you, it's time to stop relying on tutorials. It's time that you move on learn how to figure out how you can build those cool tutorial demos on your own. And just follow the occasional tutorial for fun, or just to figure out how something specific was done before you move and and explore the how and why on your own.

This segues nicely into my next tip:

3. Learn to navigate the documentation

Once you stop relying on tutorials all the time, you will need a new way to learn. Picking up a book every time you want to learn something isn't very beneficial so luckily there is an alternative; documentation.

Granted, Apple's documentation for iOS hasn't been great the past couple of years. But regardless, learning how to navigate the documentation, or even learning how to browse the headers for Apple's frameworks can be a huge boost to discovering and learning new frameworks and technologies.

Once you understand how you can learn and read the documentation for the frameworks you use, you will feel far more confident about the products you're building. Instead of just replicating what you saw in a tutorial, you will be able to look up the frameworks and APIs used in a tutorial, and you will be able to reason about the choices that were made.

4. Participate in the community

There's a community around virtually every framework, programming language or tool. Participating in these communities can be scary, but from participating you can gain a ton.

Simply reading along on a community forum like the Swift forum, or being a member of a Slack group like the iOS Developers Slack and reading some of the conversations that go on in there can be eye opening. You don't have to actively participate immediately, but from just soaking up the knowledge that flows writhing these communities you can grow so much.

When I first started reading along with the Swift mailinglist, which later moved on to the Swift forums, I didn't understand half of the conversations that went on. And to be quite frank, there's still a lot of comments and explanations I don't fully understand all of the time. Either way, seeing what goes on and learning from it is a growing experience for me.

Slowly but surely my confidence has built over the years to the point where I enjoy participating in communities all the time. It's why I write this blog, and it's why I'm a workspace admin for the iOS developers group. Helping people figure things out isn't just fun. It's also a great way for me to learn.

So by passively reading conversations I don't understand well enough to participate in, and by actively helping community members where I can I have learned more than I could have in any other way. I can only say that I strongly recommend to find the community for your favorite framework, programming language or tool and start participating. Maybe just read along at first, and at some point you will have the confidence to start contributing. I know you will.

5. Step outside of your comfort zone

The last tip on my list flows nicely from the previous tip. Participating in a community might mean you have to step outside of your comfort zone. But that's not why I put this tip on the list.

What I mean by stepping outside of your comfort zone is that I think it's very beneficial for you as a developer to try new things. If you're an iOS developer, try building something that runs on the server. Maybe use Swift, or maybe learn a different language to do it, like Python. If you're a JavaScript developer, try working on an app sometimes.

By taking a look at new programming languages and platforms, you will learn more about the worlds you exist in as a developer. And more importantly, you will understand your (future) coworkers much better if you have an idea of what they work with.

If you've learned to step outside of your comfort zone, and learned to develop in more than a single language and/or platform, you will feel much more confident. And questions like "Will iOS development still be in demand in a couple of years" suddenly aren't as scary. You'll know how to adapt. You've learned to be flexible.

Bonus tip: Go beyond development

Once you're proficient at writing code and building apps, it's time to starting focussing on everything that happens around development. Try to gain a deeper understanding of the company you work for, it will help you understand the work you do much better which in turn means that you'll be able to make much better choices while coding. Maybe you can try mentoring or supporting junior developers around you or within your company. Helping others grow and learn is a fantastic way to solidify your own understanding of topics, as well as think about the way you work in a different light.

Just to clarify, I'm not saying go into management. Not even close. Of course, management is a viable career path but if you want to grow beyond senior levels of development you'll need to do more than just work on tickets you're assigned.

Some of the best developers I've worked with hardly stood out due to their coding skills (even when those skills were amazing). What stood out about these developers is that they had tons of connections in the workplace. They know loads of people, and they knew exactly who they needed to go to for which questions. And people also know exactly what to come to these skilled developers for. Whenever tickets were shown during a backlog refinement session, they would be able to discuss these tickets in far greater detail than just the scope of the ticket. They know what's going on in the company, where it's headed, and where it's coming from.

Knowing a lot about the context of your work, and having a solid network can really be a superpower as a developer.

In summary

Everybody's journey towards becoming a developer is different. For some is a long and hard journey, while other are lucky enough to come from a place where learning how to program was made easy. But there's one thing we all have in common. We're all constantly learning, growing and evolving.

In this week's post I have shared five tips with you. These tips are what I believe to be important because I believe that following these tips has helped me grow, learn, and evolve into the developer I am today.

The reason I say this is that I don't believe these five tips are the magical five tips that will help everybody. These are just my five tips. And I hope that they are useful to you. And if they're not, I hope you can find your own five tips. We're all different, and that means that different things work for everybody.

What’s the difference between catch and replaceError in Combine?

There are several ways to handle errors in Combine. Most commonly you will either use catch or replaceError if you want to implement a mechanism that allows you to recover from an error. For example, catch is useful if you want to retry a network operation with a delay.

The catch and replaceError operators look very similar at first glance. They are both executed when an error occurs in your pipeline, and they allow you to recover from an error. However, their purposes are very different.

When to use catch

The catch operator is used if you want to inspect the error that was emitted by an upstream publisher, and replace the upstream publisher with a new publisher. For example:

let practicalCombine = URL(string: "https://practicalcombine.com")!
let donnywals = URL(string: "https://donnywals.com")!

var cancellables = Set<AnyCancellable>()

URLSession.shared.dataTaskPublisher(for: practicalCombine)
  .catch({ urlError in
    return URLSession.shared.dataTaskPublisher(for: donnywals)
  })
  .sink(receiveCompletion: { completion in
    // handle completion
  }, receiveValue: { value in
    // handle response
  })
  .store(in: &cancellables)

In this example I replace any errors emitted by my initial data task publisher with a new data task publisher. Depending on your needs and the emitted error, you can return any kind of publisher you want from your catch. The only thing you need to keep in mind is that the publisher you create must have the same Output and Failure as the publisher that the catch is applied to. So in this case it needs to be a publisher that matches a data task publisher's Output and Failure.

Note that you cannot throw errors in catch. You must always return a valid publisher. If you only want to create a new publisher for a specific error, and otherwise forward the thrown error, you can use tryCatch which allows you to throw errors.

When to use replaceError

The replaceError operator is slightly simpler than the catch operator. With replaceError you can provide a default value that's used to replace any thrown error from upstream publishers. Note that this operator changes your Failure type to Never because with this operator in place, it will become impossible for your pipeline to fail. This is different from catch because the publisher you create in the catch operator might still fail.

Let's look at an example of replaceError:

enum MyError: Error {
  case failed
}

var cancellables = Set<AnyCancellable>()
var subject = PassthroughSubject<Int, Error>()

subject
  .replaceError(with: 42)
  .sink(receiveCompletion: { completion in
    print(completion)
  }, receiveValue: { int in
    print(int)
  })
  .store(in: &cancellables)

subject.send(1)
subject.send(2)
subject.send(completion: .failure(MyError.failed))

If you execute this code in a Playground you'll find that the console will contain the following output:

1
2
42
finished

The first two values are sent explicitly by calling send. The third value is the result of replacing the error I sent as the last value. Note that the publisher completes successfully immediately after. The upstream publisher completes as soon as it emits an error, it's one of the rules of Combine that publishers can only complete once, and they do so through an error event or a completion event.

Note that in catch, the publisher that emitted the error that triggered the catch completes when it emits an error. The publisher you return from catch does not have to complete immediately and it replaces the failed publisher completely. So your sink could receive several values after the source publisher failed because the replacement publisher is still active.

Retrying a network request with a delay in Combine

Combine comes with a handy retry operator that allows developers to retry an operation that failed. This is most typically used to retry a failed network request. As soon as the network request fails, the retry operator will resubscribe to the DataTaskPublisher, kicking off a new request hoping that the request will succeed this time. When you use retry, you can specify the number of times you want to retry the operation to avoid endlessly retrying a network request that will never succeed.

While this is great in some scenarios, there are also cases where this behavior is not what you want.

For example, if your network request failed due to being rate limited or the server being too busy, you should probably wait a little while before retrying your network call since retrying immediately is unlikely to succeed anyway.

In this week's post you will explore some options you have to implement this behavior using nothing but operators and publishers that are available in Combine out of the box.

Implementing a simple retry

Before I show you the simplest dretry mechanism with a delay I could come up with, I want to show you what an immediate retry looks like since I'll be using that as the starting point for this post:

var cancellables = Set<AnyCancellable>()

let url = URL(string: "https://practicalcombine.com")!
let dataTaskPublisher = URLSession.shared.dataTaskPublisher(for: url)

dataTaskPublisher
  .retry(3)
  .sink(receiveCompletion: { completion in
    // handle errors and completion
  }, receiveValue: { response in
    // handle response
  })
  .store(in: &cancellables)

This code will fire a network request, and if the request fails it will be retried three times. That means that at most we'd make this request 4 times in total (once for the initial request and then three more times for the retries).

Note that a 404, 501 or any other error status code does not count as a failed request in Combine. The request made it to the server and the server responded. A failed request typically means that the request wasn't executed because the device making the request is offline, the server failed to respond in a timely manner, or any other reason where we never received a response from the server.

For all of these cases it probably makes sense to retry the request immediately. But how should an HTTP status code of 429 (Too Many Requests / Rate Limit) or 503 (Server Busy) be handled? These will be seen as successful outcomes by Combine so we'll need to inspect the server's response, raise an error and retry the request with a couple of seconds delay since we don't want to make the server even busier than it already is (or continue hitting our rate limit).

The first step doesn't really have anything to do with our simple retry yet but it's an important prerequisite. We'll need to extract the HTTP status code from the response we received and see if we should retry the request. For simplicity I will only check for 429 and 503 codes. In your code you'll probably want to check which status codes can be returned by your API and adapt accordingly.

enum DataTaskError: Error {
  case invalidResponse, rateLimitted, serverBusy
}

let dataTaskPublisher = URLSession.shared.dataTaskPublisher(for: url)
  .tryMap({ response -> (data: Data, response: URLResponse) in
    // just so we can debug later in the post
    print("Received a response, checking status code")

    guard let httpResponse = response.response as? HTTPURLResponse else {
      throw DataTaskError.invalidResponse
    }

    if httpResponse.statusCode == 429 {
      throw DataTaskError.rateLimitted
    }

    if httpResponse.statusCode == 503 {
      throw DataTaskError.serverBusy
    }

    return response
  })

By applying a tryMap on the dataTaskPublisher we can get the response from the data task and check its HTTP status code. Depending on the status code I throw different errors. If the status code is not 429 or 503 it's up to the subscriber of dataTaskPublisher to handle any errors. Since this tryMap is fairly lengthy I will omit the definition of the let dataTaskPublisher and the DataTaskError enum in the rest of this post and instead just refer to dataTaskPublisher:

Now that we have a publisher that fails when we want it to fail we can implement a delayed retry mechanism.

Implementing a delayed retry

Since retry doesn't allow us to specify a delay we'll need to come up with a clever solution. Luckily, I am not the first person to try and come up with something because Joseph Heck and Matt Neuburg both wrote some information and their approaches on Stackoverflow.

So why am I writing this if there's already something on Stackoverflow?

Well, neither of the solutions there is the solution. At least not in Xcode 11.5. Maybe they worked in an older Xcode version but I didn't check.

The general idea of their suggestions still stands though. Use a catch to capture any errors, and return the initial publisher with a delay from the catch. Then place a retry after the catch operator. That could would look a bit like this:

// This code is not the final solution
dataTaskPublisher
  .tryCatch({ error -> AnyPublisher<(data: Data, response: URLResponse), Error> in
    print("In the tryCatch")

    switch error {
    case DataTaskError.rateLimitted, DataTaskError.serverBusy:
      return dataTaskPublisher
        .delay(for: 3, scheduler: DispatchQueue.global())
        .eraseToAnyPublisher()
    default:
      throw error
    }
  })
  .retry(2)
  .sink(receiveCompletion: { completion in
    print(completion)
  }, receiveValue: { value in
    print(value)
  })
  .store(in: &cancellables)

The solution above uses the dataTaskPublisher from the earlier code snippet. I use a tryCatch to inspect any errors coming from the data task publisher. If the error matches one of the errors where I want to perform a delayed retry, I return the dataTaskPublisher with a delay applied to it. This will delay the delivery of values from the data task publisher that I return from tryCatch. I also erase the resulting publisher to AnyPublisher because it looks nicer.

Note that any errors emitted by dataTaskPublisher are now replaced by a new publisher that's based on dataTaskPublisher. These publishers are not the same publisher. The new publisher will begin running immediately and emit its output with a delay of 3 seconds.

This means that the publisher that has the delay applied will delay the delivery of both its success and failure values by three seconds.

When this second publisher emits an error, the retry will re-subscribe to the initial data task publisher immediately, kicking off a new network request. And this dance continues until retry was hit twice. With the code as-is, the output looks a bit like this:

Received a response, checking status code # 0 seconds in from the initial dataTaskPublisher
In the tryCatch # 0 seconds in from the initial dataTaskPublisher
Received a response, checking status code # 0 seconds in from the publisher returned by tryCatch
Received a response, checking status code # 3 seconds in from the initial dataTaskPublisher
In the tryCatch # 3 seconds in from the publisher returned by tryCatch
Received a response, checking status code # 3 seconds in from the publisher returned by tryCatch
Received a response, checking status code # 6 seconds in from the initial dataTaskPublisher
In the tryCatch # 6 seconds in from the publisher returned by tryCatch
Received a response, checking status code # 6 seconds in from the publisher returned by tryCatch

That's not exactly what we expected, right? This shows a total of 6 responses being received. Double what we wanted. And more importantly, we make requests in pairs of two. So the publisher that's created in the tryCatch executes immediately, but just doesn't emit its values until 3 seconds later which is why it takes three seconds for the initial dataTaskPublisher to fire again.

Let's see how we can fix this. First, I'll show you an interesting yet incorrect approach at implementing this.

An incorrect approach to a delayed retry

We can bring down this number to a more sensible number of requesting by applying the share() operator to the initial publisher. This will make it so that we only execute the first data task only once:

dataTaskPublisher.share()
  .tryCatch({ error -> AnyPublisher<(data: Data, response: URLResponse), Error> in
    print("In the tryCatch")
    switch error {
    case DataTaskError.rateLimitted, DataTaskError.serverBusy:
      return dataTaskPublisher
        .delay(for: 3, scheduler: DispatchQueue.global())
        .eraseToAnyPublisher()
    default:
      throw error
    }
  })
  .retry(2)
  .sink(receiveCompletion: { completion in
    print(completion)
  }, receiveValue: { value in
    print(value)
  })
  .store(in: &cancellables)

By applying share() to the dataTaskPublisher a new publisher is created that will execute when it receives its initial subscriber and replays its results for any subsequent subscribers. In our case, this results in the following output:

Received a response, checking status code # 0 seconds in from the initial dataTaskPublisher
In the tryCatch # 0 seconds in from the initial dataTaskPublisher
Received a response, checking status code # 0 seconds in from the publisher returned by tryCatch
In the tryCatch # 3 seconds in from the publisher returned by tryCatch
Received a response, checking status code # 3 seconds in from the publisher returned by tryCatch
In the tryCatch # 6 seconds in from the publisher returned by tryCatch
Received a response, checking status code # 6 seconds in from the publisher returned by tryCatch

We're closer to the desired outcome but not quite there yet. Now that we use a shared publisher as the initial publisher, it will no longer execute its data task and the tryMap that we defined on the dataTaskPublisher earlier is no longer called. The result of the tryMap is cached in the share() and this cached result is immediately emitted when retry resubscribes. This means that share() will re-emit whatever error we received the first time it made its request.

This behavior will make it look like we're correctly retrying our request but there's actually a problem. Or rather, there are a couple of problems with this approach.

The retry operator in Combine will catch any errors that occur upstream and resubscribe to the pipeline so far. This means that any errors that occur above the retry will make it so we resubsribe to dataTaskPublisher.share(). In other words, the tryCatch that we have after dataTaskPublisher.share() will always receive the same error. So if the initial request failed due to being rate limitted and our retried request fails because we couldn't make a request, the tryCatch will still think we ran into a rate limit error and retry the request even though the logic in the tryCatch says we want to throw an error if we encountered something other than DataTaskError.rateLimitted or DataTaskError.serverBusy.

And on top of that, when we encounter something other than DataTaskError.rateLimitted or DataTaskError.serverBusy we still hit our retry with an error. This means that we'll resubscribe to dataTaskPublisher.share(), hit the tryCatch, throw an error, and retry again until we've retried the specified amount of times (2 in this example).

We should fix so that:

  1. We always receive the current / latest error in the tryCatch.
  2. We don't retry when we caught a non-retryable error.

This means that we should get rid of the share() and actually run the network request when the retry resubscribes to dataTaskPublisher while making sure we don't get the extra requests that we wanted to get rid of in the previous section.

A correct way to retry a network request with a delay

The first thing we should do in order to fix our retry mechanism is redefine how the dataTaskPublisher property is created. The changes we need to make are fairly small but they have a large impact on our final result. As I mentioned in the previous section, retry will resubscribe to the upstream publisher whenever it encounters an error. This means that a failing network call would trigger our retry even though we only want to retry when we enounter an error that we consider worth retrying the call for. In this post I assume that we should retry for "rate limitted" and "server busy" status codes. Any other failure should not be retried.

To achieve this, we need to make the retry operator think that our network call always succeeds unless we encounter one of our retryable errors. We can do this by converting the network call's output to a Result object that has the data task publisher's output as it's Output and Error as its failure. If the network call comes back with a retryable error, we'll throw an error from tryMap to trigger the retry. Otherwise, we'll return a Swift Result that can hold an error, or our output. This will make it look like everything went well so the retry doesn't trigger, but we'll be able to extract errors later if needed.

Let's take a look at what this means for how the dataTaskPublisher is defined:

let dataTaskPublisher = networkCall
  .tryMap({ dataTaskOutput -> Result<URLSession.DataTaskPublisher.Output, Error> in
    print("Received a response, checking status code")

    guard let response = dataTaskOutput.response as? HTTPURLResponse else {
      return .failure(DataTaskError.invalidResponse)
    }

    if response.statusCode == 429 {
      throw DataTaskError.rateLimitted
    }

    if response.statusCode == 503 {
      throw DataTaskError.serverBusy
    }

    return .success(dataTaskOutput)
  })

If we would erase this pipeline to AnyPublisher, we'd have the following type for our publisher: AnyPublisher<Result<URLSession.DataTaskPublisher.Output, Error>, Error>. The Error in the Result is what we'll use to send non-retryable errors down the pipeline. The publisher's error is what we'll use for retryable errors.

For example, I don't want to retry my network request when I receive an invalid response so I map the data task output to .failure(DataTaskError.invalidResponse) which means the request shouldn't be retried but we can still extract and use the invalid response error after the retry.

When the request succeeded and we're happy I return .success(dataTaskOutput) so I can extract and use the data task output later.

If a retryable error occured I throw an error so we can catch that error later to setup our delayed retry in a similar fashion as what you've seen in the previous section:

dataTaskPublisher
  .catch({ (error: Error) -> AnyPublisher<Result<URLSession.DataTaskPublisher.Output, Error>, Error> in
    print("In the catch")
    switch error {
    case DataTaskError.rateLimitted,
         DataTaskError.serverBusy:
      print("Received a retryable error")
      return Fail(error: error)
        .delay(for: 3, scheduler: DispatchQueue.main)
        .eraseToAnyPublisher()
    default:
      print("Received a non-retryable error")
      return Just(.failure(error))
        .setFailureType(to: Error.self)
        .eraseToAnyPublisher()
    }
  })
  .retry(2)

Instead of a tryCatch I use catch in this example. We want to catch any errors that originated from making the network request (for example if the request couldn't be made) or the tryMap (if we encountered a retryable error).

In the catch I check whether we encountered one of the retryable errors. If we did, I create a publisher that will immediately fail with the received error. I delay the delivery of this error by three seconds and I erase to any publisher so I can have a consistent return type for my catch. This code path will trigger the retry after three seconds and will make it so we resubscribe to dataTaskPublisher and execute the network call again because we don't use the share() anymore.

If we encounter a non-retryable error, I return a Just publisher that will immediately emit a single value. Similar to the tryMap, I wrap this error in a Swift Result to make the retry think everything is fine because we don't emit an error from the Just publisher.

At this point, our pipeline will only emit an error if we encounter a retryable error. Any other errors are wrapped in a Result and sent down the pipeline as Output.

We'll want to transform our Result back to an Error event after the tryMap so we'll receive errors in our sink's receiveCompletion and the receiveValue only receives succesful output.

Here's how we can achieve this:

dataTaskPublisher
  .catch({ (error: Error) -> AnyPublisher<DataTaskResult, Error> in
    print("In the catch")
    switch error {
    case DataTaskError.rateLimitted,
         DataTaskError.serverBusy:
      print("Received a retryable error")
      return Fail(error: error)
        .delay(for: 3, scheduler: DispatchQueue.main)
        .eraseToAnyPublisher()
    default:
      print("Received a non-retryable error")
      return Just(.failure(error))
        .setFailureType(to: Error.self)
        .eraseToAnyPublisher()
    }
  })
  .retry(2)
  .tryMap({ result in
    // Result -> Result.Success or emit Result.Failure
    return try result.get()
  })
  .sink(receiveCompletion: { completion in
    print(completion)
  }, receiveValue: { value in
    print(value)
  })
  .store(in: &cancellables)

By placing a tryMap after the retry we can grab our Result<URLSession.DataTaskPublisher.Output, Error> value and call try result.get() to either return the success case of our result, or throw the error in our failure case.

By doing this, we'll receive errors in receiveCompletion and receiveValue only receives succesful values. This means we won't have to deal with the Result in our receiveValue.

The output for this example would look like this:

Received a response, checking status code # 0 seconds after the initial data task
In the catch # 0 seconds after the initial data task
Received a retryable error # 0 seconds after the initial data task
Received a response, checking status code # 3 seconds after the initial data task
In the catch # 3 seconds after the initial data task
Received a retryable error # 3 seconds after the initial data task
Received a response, checking status code # 6 seconds after the initial data task
In the catch # 6 seconds after the initial data task
Received a retryable error # 6 seconds after the initial data task
failure(__lldb_expr_5.DataTaskError.rateLimitted) # 9 seconds after the initial data task

By delaying the delivery of certain errors, we can manipulate the start of the retried request. One downside is that if every request fails, we'll also delay the delivery of the final failure by the specified interval.

One thing I really like about this approach is that you can use different intervals for different errors, and you can even have the server tell you how long you should wait before retrying a request if the server includes this information in an HTTP header or as part of the response body. The delay could be configured in the tryMap where you have access to the HTTP response and you could associate the delay with your custom error case as an associated value.

In summary

What started as a simple question "How do I implement a delayed retry in Combine" turned out to be quite an adventure for me. Every time I thought I had found a solution, like the one I linked to from Stackoverflow there was always something about each solution I didn't like. As it turns out, there is no quick and easy way in Combine to implement a delayed retry that only applies to specific errors. I even had to update this post months after writing it because Alex Grebenyuk pointed out some interesting issues with the initial solution proposed in this post.

In this post you saw the various solutions I've tried, and why they were not to my liking. In the last section I showed you a tailor-made solution that works by delaying the delivery of specific errors rather than attempting to delay the start of the next request. Ultimately that mechanism delays delivery of all results, including success if the initial request failed. My final solution does not have this drawback which, in my opinion, is much nicer than delaying everything.

I have made the code used in this post available as a GitHub gist here. You can paste it in a Playground and it should work immediately. The code is sllightly modified to proof that network calls get re-executed and I have replaced the network with a Future so you have full control over the fake network call. To learn more about Combine's Future, you might want to read this post.

If you have your own solution for this problem and think it's more elegant, shorter or better than please, do reach out to me on Twitter so I can update this post. I secretly hope that this post is obsolete by the time WWDC 2020 comes along but who knows. For now I think this is the best we have.

Reclaim disk space by deleting old iOS simulators and Device Support files

After using a MacBook that runs Xcode for a few years it's likely that your disk space is starting to fill up good. A large part of this disk space can be occupied by Device Support files that are used by Xcode for older iOS versions, or by iOS simulators that are no longer available on your machine.

To clean these files up you can do the following:

  • Go to your Terminal and type open ~/Library/Developer/Xcode/iOS\ DeviceSupport
  • Delete folders for iOS versions that you no longer need to support.
  • Do the same with open ~/Library/Developer/Xcode/watchOS\ DeviceSupport
  • Clean up unavailable simulators using by typing xcrun simctl delete unavailable in your Terminal

When I ran these commands on a machine that got a clean install when macOS Catalina came out I was able to free up 15Gb of disk space. So that's 15Gb of space after about 8 months of use. Pretty good, right?

Xcode 12 automatically helps cleaning up Device Support files

A very cool feature of Xcode 12 is that Xcode will track the device / iOS version combinations that you use and update the mtime for each item in the Device Support directory accordingly. Any Device Support files that haven't been used for 180 days or more are automatically made elligible for deletion by the system.

This means that macOS can automatically clean up old Device Support files after they haven't been used for 180 days. The system has full control over when exactly this cleanup takes place and the exact cleanup times are dependent on variables like system activity, available disk space, whether you're connected to power and more.

I think it's really cool that Xcode and macOS can now actively help you reclaim disk space by removing unused Device Support files. Of course, if you need space now you'll still need to go in and manually delete Device Support files that you no longer need but this feature should certainly put a cap on the number of old Device Support files that are kept around.

(Thanks to Olivier Halligon for telling me about this feature. And also thanks to Russ Bishop for telling Olivier about this.)

Throttle network speeds for a specific host in Charles

Sometimes you'll want to test whether your app works properly under poor networking conditions. One way to test this is Apple's Network Link Conditioner. Unfortunately, this will slow internet speeds for your entire machine to a crawl which can be counterproductive. Especially if you want to throttle your app for a longer period of time.

If you have Charles installed to debug your app's network traffic, you can use it to throttle network speeds for the entire system, or for a selection of hosts which is exactly what we're looking for.

To enable throttling in Charles you can either go to Proxy -> Start Throttling or press cmd + T. This will turn on global throttling by default.

You can configure how Charles throttles, and for which hosts through Proxy -> Throttle Settings or by pressing cmd + shift + T. Make sure to check the Only for selected hosts checkbox if you want to configure which hosts should be throttled.

Throttle Settings window

Click the Add button to add a new host that should be throttled.

Use the bottom section of the Throttle Settings to configure how the network connection should be throttled. This configuration is applied to all throttled hosts.

How to have more than one type of cell in a Collection View

Collection views in iOS are awesome. You can use them to build complex custom layouts and since iOS 13 you can use Compositional Layouts to quickly build good looking layouts that would take forever to accomplish on iOS 12 and below.

But what if you want to use more than one type of cell in your layout?

If you're building your app without storyboard you register collection view cells using the register method on UICollectionView. If you want to use more than one cell type, all you need to do is call register multiple times:

collectionView.register(CellTypeOne.self, forCellWithReuseIdentifier: "CellTypeOne")
collectionView.register(CellTypeTwo.self, forCellWithReuseIdentifier: "CellTypeTwo")

Each cell type has its own class and its own reuse identifier. In the collection view's cellForItemAt delegate method you can return either of your registered cells like this:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  if indexPath.section == 0 {
    let dequeuedCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellTypeOne", for: indexPath)

    guard let cellOne = dequeuedCell as? CellTypeOne else {
      fatalError("Wrong cell type for section 0. Expected CellTypeOne")
    }

    // configure your CellTypeOne

    return cellOne
  } else {
    let dequeuedCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CellTypeTwo", for: indexPath)

    guard let cellTwo = dequeuedCell as? CellTypeTwo else {
      fatalError("Wrong cell type for section 0. Expected CellTypeTwo")
    }

    // configure your CellTypeTwo

    return cellTwo
  }
}

In my example, I want to use CellTypeOne for the first section in my collection view and CellTypeTwo for all other sections. You can use any logic you want to determine which cell should be used. Notice that using multiple cell types ultimately isn't too different from using one cell type. The only difference is that you'll have an if somewhere to determine which cell type you want to dequeue.

If you're using Storyboards to build your app the process of dequeuing cells is the same as it is when you're doing everything in code. The only difference is how you register the cells on your collection view. It's perfectly fine to mix Storyboards and programmatic so you could use register to register new cells on the collection view that you created in a Storyboard.

Alternatively, you can drag more than one cell from the Object Library into your collection view, assign a subclass and reuse identifier for the cell and design it as needed. Just like you would for a single cell. Dragging multiple cells into your collection view in a Storyboard automatically registers these cells on your collection view. You can use and dequeue them just like I showed you in the code above.

If you have any questions about this tip, or if you have feedback for me, don't hesitate to reach out on Twitter.

What is type erasure in Swift? An explanation with code samples

Swift's type system is (mostly) fantastic. Its tight constraints and flexible generics allow developers to express complicated concepts in an extremely safe manner because the Swift compiler will detect and flag any inconsistencies within the types in your program.

While this is great most of the time, there are times where Swift's strict typing gets in the way of what we're trying to build. This is especially true if you're working on code that involves protocols and generics.

With protocols and generics, you can express ideas that are insanely complex and flexible. But sometimes you're coding along happily and the Swift compiler starts yelling at you. You've hit one of those scenarios where your code is so flexible and dynamic that Swift isn't having it.

Let's say you want to write a function that returns an object that conforms to a protocol that has an associated type? Not going to happen unless you use an opaque result type.

But what if you don't want to return the exact same concrete type from your function all the time? Unfortunately, opaque result types won't help you there. Luckily, Swift 5.7 which came out in 2022 allows us to define so-called primary assocated types which allow us to specialize our opaque return types where needed.

It's important to notw that primary associated types remove many of the reasons the use type erasure in your app, but they don't make type erasure completely obsolete.

So when the Swift compiler keeps yelling at you and you have no idea how to make it stop, it might be time to apply some type erasure.

In this week's blog post I will explain what type erasure is and show an example of how type erasure can be used to craft highly flexible code that the Swift compiler will be happy to compile.

There are multiple scenarios where type erasure makes sense and I want to cover two of them.

Using type erasure to hide implementation details

The most straightforward way to think of type erasure is to consider it a way to hide an object's "real" type. Some examples that come to mind immediately are Combine's AnyCancellable and AnyPublisher. An AnyPublisher in Combine is generic over an Output and a Failure. If you're not familiar with Combine, you can read up in the Combine category on this blog. All you really need to know about AnyPublisher is that it conforms to the Publisher protocol and wraps another publisher. Combine comes with tons of built-in publishers like Publishers.Map, Publishers.FlatMap, Future, Publishers.Filter, and many, many more.

Often when you're working with Combine, you will write functions that set up a chain of publishers. You usually don't want to expose the publishers you used to callers of your function. In essence, all you want to expose is that you're creating a publisher that emits values of a certain type (Output) or fails with a specific error (Failure). So instead of writing this:

func fetchData() -> URLSession.DataTaskPublisher<(data: Data, response: URLResponse), URLError> {
  return URLSession.shared.dataTaskPublisher(for: someURL)
}

You will usually want to write this:

func fetchData() -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
  return URLSession.shared.dataTaskPublisher(for: someURL)
    .eraseToAnyPublisher()
}

By applying type erasure to the publisher created in fetchData we are now free to change its implementation as needed, and callers of fetchData don't need to care about the exact publisher that's used under the hood.

When you think about how you can refactor this code, you might be tempted to try and use a protocol instead of an AnyPublisher. And you'd be right to wonder why we wouldn't.

Since a Publisher has an Output and Failure that we want to be able to use, using some Publisher wouldn't work. We wouldn't be able to return Publisher due to its associated type constraints, so returning some Publisher would allow the code to compile but it would be pretty useless:

func fetchData() -> some Publisher {
  return URLSession.shared.dataTaskPublisher(for: someURL)
}

fetchData().sink(receiveCompletion: { completion in
  print(completion)
}, receiveValue: { output in
  print(output.data) // Value of type '(some Publisher).Output' has no member 'data'
})

Because some Publisher hides the true type of the generics used by Publisher, there is no way to do anything useful with the output or completion in this example. An AnyPublisher hides the underlying type just like some Publisher does, except you can still define what the Output and Failure types are for the publisher by writing AnyPublisher<Output, Failure>.

With primary associated types in Swift 5.7, you can write the following code:

func fetchData() -> any Publisher<(Data, URLResponse), URLError> {
  return URLSession.shared.dataTaskPublisher(for: someURL)
}

The only problem with using primary associated types on a Publisher is that not all methods that exist on publishers like an AnyPublisher are added to the Publisher protocol. This means that we might lose some functionality by not using AnyPublisher.

I will show you how type erasure works in the next section. But first I want to show you a slightly different application of type erasure from the Combine framework. In Combine, you'll find an object called AnyCancellable. If you use Combine, you will encounter AnyCancellable when you subscribe to a publisher using one of Combine's built-in subscription methods.

Without going into too much detail, Combine has a protocol called Cancellable. This protocol requires that conforming objects implement a cancel method that can be called to cancel a subscription to a publisher's output. Combine provides three objects that conform to Cancellable:

  1. AnyCancellable
  2. Subscribers.Assign
  3. Subscribers.Sink

The Assign and Sink subscribers match up with two of Publisher's methods:

  1. assign(to:on:)
  2. sink(receiveCompletion:receiveValue)

These two methods both return AnyCancellable instances rather than Subscribers.Assign and Subscribers.Sink. Apple could have chosen to make both of these methods return Cancellable instead of AnyCancellable.

But they didn't.

The reason Apple applies type erasure in this example is that they don't want users of assign(to:on:) and sink(receiveCompletion:receiveValue) to know which type is returned exactly. It simply doesn't matter. All you need to know is that it's an AnyCancellable. Not just that it's Cancellable, but that it could be _any MARKDOWN_HASH9fba8e737f748904c9dc7415d4876e4aMARKDOWN<em>HASH.

Because AnyCancellable erases the type of the original Cancellable by wrapping it, you don't know if the AnyCancellable wraps a Subscribers.Sink or some other kind of internal, private Cancellable that we're not supposed to know about.

If you have a need to hide implementation details in your code, or if you run into a case where you want to return an object that conforms to a protocol that has an associated type that you need to access without returning the actual type of object you wanted to return, type erasure just might be what you're looking for.

Applying type erasure in your codebase

To apply type erasure to an object, you need to define a wrapper. Let's look at an example:

protocol DataStore {
  associatedtype StoredType

  func store(_ object: StoredType, forKey: String)
  func fetchObject(forKey key: String) -> StoredType?
}

class AnyDataStore<StoredType>: DataStore {
  private let storeObject: (StoredType, String) -> Void
  private let fetchObject: (String) -> StoredType?

  init<Store: DataStore>(wrappedStore: Store) where Store.StoredType == StoredType {
    self.storeObject = wrappedStore.store
    self.fetchObject = wrappedStore.fetchObject
  }

  func store(_ object: StoredType, forKey key: String) {
    storeObject(object, key)
  }

  func fetchObject(forKey key: String) -> StoredType? {
    return fetchObject(key)
  }
}

This example defines a DataStore protocol and a type erasing wrapper called AnyDataStore. The purpose of the AnyDataStore is to provide an abstraction that hides the underlying data store entirely. Much like Combine's AnyPublisher. The AnyDataStore object makes extensive use of generics and if you're not too familiar with them this object probably looks a little bit confusing.

The AnyDataStore itself is generic over StoredType. This is the type of object that the underlying DataStore stores. The initializer for AnyDataStore is generic over Store where Store conforms to DataStore and the objects that are stored in the Store must match the objects stored by the AnyDataStore. Due to the way this wrapper is set up that should always be the case but Swift requires us to be explicit.

We want to forward any calls on AnyDataStore to the wrapped store, but we can't hold on to the wrapped store since that would require making AnyDataStore generic over the underlying data store, which would expose the underlying datastore. Instead, we capture references to the method we need in the storeObject and fetchObject properties and forward any calls to store(_:forKey:) and fetchObject(forKey:) to their respective stored references.

It's quite a generics feast and again, if you're not too familiar with them this can look confusing. I wrote about generics a while ago so make sure to click through to that post if you want to learn more.

Let's see how this AnyDataStore can be used in an example:

class InMemoryImageStore: DataStore {
  var images = [String: UIImage]()

  func store(_ object: UIImage, forKey key: String) {
    images[key] = object
  }

  func fetchObject(forKey key: String) -> UIImage? {
    return images[key]
  }
}

struct FileManagerImageStore: DataStore {
  typealias StoredType = UIImage

  func store(_ object: UIImage, forKey key: String) {
    // write image to file system
  }

  func fetchObject(forKey key: String) -> UIImage? {
    return nil // grab image from file system
  }
}

class StorageManager {
  func preferredImageStore() -> AnyDataStore<UIImage> {
    if Bool.random() {
      let fileManagerStore = FileManagerImageStore()
      return AnyDataStore(wrappedStore: fileManagerStore)
    } else {
      let memoryStore = InMemoryImageStore()
      return AnyDataStore(wrappedStore: memoryStore)
    }
  }
}

In the code snippet above I create two different data stores and a StorageManager that is responsible for providing a preferred storage solution. Since the StorageManager decides which storage we want to use it returns an AnyDataStore that's generic over UIImage. So when you call preferredImageStore() all you know is that you'll receive an object that conforms to DataStore and provides UIImage object.

Of course, the StorageManager I wrote is pretty terrible. When you're working with data and storing it you need a lot more control over what happens and whether data is persisted. And more importantly, a StorageManager that will randomly switch between stores is not that useful. However, the important part here is not whether or not my DataStore is good. It's that you can use type erasure to hide what's happening under the hood while making your code more flexible in the process.

The example of AnyDataStore I just showed you is very similar to the AnyPublisher scenario that I described in the previous section. It's pretty complex but I think it's good to know this exists and how it (possibly) looks under the hood.

In the previous section, I also mentioned AnyCancellable. An object like that is much simpler to recreate because it doesn't involve any generics or associated types. Let's try to create something similar except my version will be called AnyPersistable:

protocol Persistable {
  func persist()
}

class AnyPersistable: Persistable {
  private let wrapped: Persistable

  init(wrapped: Persistable) {
    self.wrapped = wrapped
  }

  func persist() {
    wrapped.persist()
  }
}

An abstraction like the one I showed could be useful if you're dealing with a whole bunch of objects that need to be persisted but you want to hide what these objects really are. Since there are no complicated generics involved in this example it's okay to hold on to the Persistable object that's wrapped by AnyPersistable.

In summary

In this post, you learned about type erasure. I showed you what type erasing is, and why it's used. You saw how Apple's Combine framework uses type erasure to abstract Publisher and Cancellable objects and hide their implementation details. This can be really useful, especially if you're working on a framework or library where you don't want others to know which objects you are using internally to prevent users from making any assumptions about how your API works internally.

After explaining how type erasure is used, I showed you two examples. First, you saw a complicated example that uses generics and stores references to functions as closures. It's pretty complex if you haven't seen anything like it before so don't feel bad if it looks a little crazy to you. I know that with time and experience, a construction like the one I showed you will start to make more sense. Type erasure can be a pretty complicated topic.

The second example I showed you was simpler because it doesn't involve any generics. It mimics what Apple does with Combine's AnyCancellable to hide the underlying Cancellable objects from developers.

If you have any questions about this post or if you have feedback for me, reach out to me on Twitter

Getting started with testing your Combine code

A question that often comes up when folks get into learning Combine is "how do I test code that uses Combine?". In this week's post, I will briefly explain the basics of testing Combine code. I will assume that you already know the basics of testing and Combine. If you're just getting started with both topics or would like a refresher I can recommend that you take a look at the following resources:

By the end of this post you will understand the essentials of testing code that uses Combine.

Effectively writing tests for code that uses Combine

If you know how you can test asynchronous code, you know how to test Combine code. That's the short answer I like to give to people who ask me about testing their Combine code. The essence of testing asynchronous code is that you use XCTest's XCTestExpectation objects to make sure your test sits idle until your asynchronous code has produced a result. You can then assert than this result is the result that you expected and your test will have succeeded (or failed). This same idea applies to Combine code.

Before you write your tests you will need to apply the same abstractions that you would apply for other code, and you'll want to make sure that you can mock or stub any dependencies from the object you're testing. The major difference between testing code that uses Combine, and code that runs asynchronously is that your asynchronous code will typically produce a single result. Combine code can publish a stream of values which means you'll want to make sure that your publisher has published all of the values you expected.

Let's look at a simple model and view model that we can write a test for:

public class Car {
  @Published public var kwhInBattery = 50.0
  let kwhPerKilometer = 0.14
}

public struct CarViewModel {
  var car: Car

  public lazy var batterySubject: AnyPublisher<String?, Never> = {
    return car.$kwhInBattery.map({ newCharge in
      return "The car now has \(newCharge)kwh in its battery"
    }).eraseToAnyPublisher()
  }()

  public mutating func drive(kilometers: Double) {
    let kwhNeeded = kilometers * car.kwhPerKilometer

    assert(kwhNeeded <= car.kwhInBattery, "Can't make trip, not enough charge in battery")

    car.kwhInBattery -= kwhNeeded
  }
}

The CarViewModel in this example provides batterySubject publisher and we'll test that it publishes a new value every time we call drive(kilometers:). The drive(kilometers:) method is used to update the Car's kwhInBattery which means that $kwhInBattery should emit a new value, which is then transformed and the transformed value is them emitted by batterySubject.

Keep in mind that we have no business testing whether the map that's used for the batterySubject works properly. We shouldn't care what Combine operators are used under the hood, and we also should test that Combine's built-in operators work properly. That's Apple's job. Your job is to test the code you write and own.

To test CarViewModel you could write the following unit test:

class CarViewModelTest: XCTestCase {
  var car: Car!
  var carViewModel: CarViewModel!
  var cancellables: Set<AnyCancellable>!

  override func setUp() {
    car = Car()
    carViewModel = CarViewModel(car: car)
    cancellables = []
  }

  func testCarViewModelEmitsCorrectStrings() {
    // determine what kwhInBattery would be after driving 10km
    let newValue: Double = car.kwhInBattery - car.kwhPerKilometer * 10

    // configure an array of expected output
    var expectedValues = [car.kwhInBattery, newValue].map { doubleValue in
      return "The car now has \(doubleValue)kwh in its battery"
    }

    // expectation to be fulfilled when we've received all expected values
    let receivedAllValues = expectation(description: "all values received")

    // subscribe to the batterySubject to run the test
    carViewModel.batterySubject.sink(receiveValue: { value in
      guard  let expectedValue = expectedValues.first else {
        XCTFail("Received more values than expected.")
        return
      }

      guard expectedValue == value else {
        XCTFail("Expected received value \(value) to match first expected value \(expectedValue)")
        return
      }

      // remove the first value from the expected values because we no longer need it
      expectedValues = Array(expectedValues.dropFirst())

      if expectedValues.isEmpty {
        // the  test is completed when we've received all expected values
        receivedAllValues.fulfill()
      }
    }).store(in: &cancellables)

    // call drive to trigger a second value
    carViewModel.drive(kilometers: 10)

    // wait for receivedAllValues to be fulfilled
    waitForExpectations(timeout: 1, handler: nil)
  }
}

I have added several comments to the unit test code. I am testing my Combine code by comparing an array of expected values to the values that are emitted by batterySubject. The first element in the expectedValues array is always the element that I expect to receive from batterySubject. After receiving a value, I use dropFirst() to create a new expectedValues array with all elements from the old expectedValues array, except for the first value. I drop the first value because I just received that value.

In essence, this code isn't too different from any other asynchronous test code you may have written. The most important difference is in the fact that you now need to compare an array of expected values to the values that were actually emitted by your publisher.

Note that I am not using any Combine operators other than sink because I need to subscribe to the publisher I want to test. Using a Combine operator in your test is often a sign that you're not really testing the logic that you should be testing.

If you consider this test to be a little bit wordy, or if you think writing your tests like I just showed you is a bit repetitive when you're testing a whole bunch of publishers, I agree with you. In my Practical Combine book I demonstrate how you can improve a test like this using a simple extension on Publisher that allows you to compare a publisher's output to an array of expected values using just a few lines of code. You will also learn how you can test a publisher that needs to explicitly complete or throw an error before you consider it to be completed, including a nifty helper that allows you to write your tests in a way that almost makes them look like your logic isn't asynchronously tested at all.

In summary

In this week's post, I gave you some insight into testing code that uses Combine. You learned how you can set up an array of expected values, compare them with the values that are emitted by a publisher, and ultimately complete your test when all expected values have been emitted. If you want to learn more about Combine, testing or both make sure to take a look at the testing and Combine categories on this blog. And if you want to gain some deeper knowledge about testing your Combine code make sure to pick up my book on Combine. It includes an entire chapter dedicated to testing with several examples and two convenient helpers to clean up your test code.