Understanding the importance of abstractions

As developers, we constantly deal with layers of abstractions that make our lives easier. We have abstractions over low level networking operations that allow us to make network calls with URLSession. Core Data provides an abstraction over data persistence that can be used to store information in an sqlite database. And there are many, many more abstractions that we all use every day.

Over the past few weeks I have seen many people ask about using Core Data in pure SwiftUI projects created in Xcode 12. These projects no longer require an App- and SceneDelegate, and the checkbox to add Core Data is disabled for these projects. Some folks immediately thought that this meant Core Data can't be used with these projects since Xcode's template always initialized Core Data in the AppDelegate, and since that no longer exists it seems to make sense that Core Data is incompatible with apps that don't have an AppDelegate. How else would you initialize Core Data?

Fortunately, this isn't true. It's still possible to use Core Data in projects, even if they don't have an AppDelegate. In fact, the only thing that AppDelegate has to do with Core Data is that Apple decided that they wanted to setup Core Data in the AppDelegate.

They didn't have to make that choice. Core Data can be initialized from anywhere in your app.

However, this got me thinking about abstractions. Folks who have built a layer of abstraction between their app and Core Data probably already know that you don't need Xcode to generate a Core Data stack for you. They probably also already know that you can initialize Core Data anywhere.

While thinking about this, I started thinking more about abstractions. Adding the right abstractions to your app at the right time can help you build a more modular, portable and flexible code base that can quickly adapt to changes and new paradigms.

That why in this week's post, I would like to talk about abstractions.

Understanding what abstractions are

Abstractions provide a seperation between the interface you program against and the underlying implementation that performs work. In essence you can think of most, if not all, frameworks you use every day on iOS as abstractions that make working with something complex easier.

In programming, we often work with abstractions on top of abstractions on top of more abstractions. And yet, there is value in adding more abstractions yourself. A good abstraction does not only hide complexity and implementation details. It should also be reusable. When your abstraction is reusable it can be used in multiple projects with similar needs.

I could try to make the explanation more wordy, fancy or impressive but that wouldn't help anybody. Abstractions wrap a complex interface and provide an (often simpler) inferface while hiding the wrapped, complex interface as an implementation detail. Good abstractions can be reused.

Knowing when to write an abstraction

Earlier I wrote that adding your own abstractions has value. That said, it's not always obvious to know when you should write an abstraction. Especially since there are no hard or clear rules.

A good starting point for me is to determine whether I will write a certain block of tedious code more than once. Or rather, whether I will write similar blocks of tedious code multiple times. If the answer is yes, it makes sense to try and create a lightweight abstraction to wrap the tedious code and make it less annoying to work with.

Another method I often use to determine whether I should write an abstraction is to ask myself how easily I want to be able to swap a certain mechanism in my app out for testing or to replace it entirely.

Usually the answer to this question is that I want to be able to swap things out as easily as possible. And more often than not this means that I should add an abstraction.

For instance, when I write code that uses Core Data I always wrap it in a small abstraction layer. I don't want my entire app to depend directly on Core Data. Instead, my app uses the abstraction to interface with a persistence layer. The code in my app doesn't know how the persistence layer works. It just knows that such a layer exists, and that it can fetch and save objects of certain types.

Creating an abstraction like this allows me to easily change the underlying storage mechanism in my persistence layer. I could switch to Realm, use sqlite directly, or even move from local persitence to persisting data on a server or in iCloud. The app shouldn't know, and the app shouldn't care. That's the beauty of abstractions.

Designing an abstraction

Once you've decided that you want to write an abstraction, you need to design it. The first thing I always do is make sure that I decide which properties and methods should be publicly available. I then define a protocol that captures this public API for my abstraction. For example:

protocol TodoItemPersisting {
  func getAllTodoItems() -> Future<[TodoItem]>
  func getTodoItem(withId id: UUID) -> Future<TodoItem?>
  func updateItem(_ item: TodoItem)
  func newTodoItem() -> Future<TodoItem>
}

This is a very simple protocol that exposes nothing about the underlying persistence layer. In the rest of my code I will always refer to TodoItemPersisting when I want to use my persistence abstraction:

struct TodoListViewModel {
  private let itemStore: TodoItemPersisting
}

In this example I defined a ViewModel that has an itemStore property. This property conforms to TodoItemPersisting and the object that creates an instance of TodoListViewModel gets to decide which concrete implementation of TodoItemPersisting is injected. And since the protocol for TodoItemPersisting uses Combine Futures, we know that the persistence layer does work asynchronously. The ViewModel doesn't know whether the persistence layer goes to the network, file system, Core Data, Realm, Firebase, iCloud or anywhere else for persistence.

It just knows that items are fetched and created asynchronously.

At this point you're free to create objects that implement TodoItemPersisting as needed. Usually you'll have one or two. One for the app to use, and a second version to use while testing. But you might have more in your app. It depends on the abstraction and what it's used for.

For instance, if your app uses In-App Purchases to provide syncing data to a server you might have a local persistence abstraction, and a premium local + remote persistence abstraction that you can swap out depending on whether the user bought your premium IAP.

By desiginig abstractions as protocols you gain a lot of flexibility and power. So whenever possible I always recommend to design and define your abstractions as protocols.

Things to watch out for when writing abstractions

Once you get the hang of abstracting code, it's very tempting to go overboard. While abstractions provide a lot of power, they also add a layer of indirection. New members of your team might understand the things you've abstracted really well, but if you added to many layers your code will be really hard to understand and your abstractions will be in the way of understanding the code base.

It's also possible that you didn't design your abstractions properly. When this happens, you will find that your abstractions are holding you back rather than helping you write code that does exactly what you want it to do. When you find you're fighting your abstractions it's time to revise your design and make improvements where needed.

And the last word of warning I want to give you is that it's important to limit the levels of abstractions you add. No matter how good your abstractions are, there will come a point where it'll get harder and harder to understand and debug your app when something is wrong. There's no hard cutoff point but eventually you'll develop a sense for when you're going too far. For now it's good to know that you can abstract too much.

In Summary

In this week's post you learned about abstractions in programming. You learned what an abstraction is, what abstractions are used for and how you can determine whether you should write an abstraction of your own.

You learned that abstractions can be extremely useful when you want to write code that's testible, flexible, and maintainable. Good abstractions make difficult work easier, and allow you to hide all implementation details of the thing or process you've written your abstraction for. You also learned that protocols are a fantastic tool to help you define and design your abstraction. Lastly, I gave you some things to watch out for when writing abstractions to make sure you don't overcomplicate matters or abstract too much.

If you have any questions for me, or if you have feedback about this week's post make sure to reach out to me on Twitter.

Expand your learning with my books

Practical Swift Concurrency header image

Learn everything you need to know about Swift Concurrency and how you can use it in your projects with Practical Swift Concurrency. It contains:

  • Eleven chapters worth of content.
  • Sample projects that use the code shown in the chapters.
  • Free updates for future iOS versions.

The book is available as a digital download for just $39.99!

Learn more

Handling deeplinks in a SwiftUI app

The quickest way to handle deeplinks in your app is through the onOpenURL(perform:) view modifier which has been available since iOS 14. It allows developers to register a URL handler on their views so they can respond to URLs by modifying state for their views as needed.

In this post, we'll cover all the steps involved in setting up your app to handle deeplinks. We'll start with explaining what deeplinks are, and then we'll configure an Xcode project to allow your app to handle incoming deeplinks.

Note that we won't cover setting up Universal Links in this post; we'll solely focus on handling incoming links. These can be deeplinks or Universal Links, but the setup process for Universal Links is slightly more involved than adding the ability to handle deeplinks in your app.

To learn more about Universal Links, you can read my post about adding support for Universal Links to your app.

Setting your app up to handle deeplinks

To set your app up for handling incoming deeplinks, you need to register which url schemes your app should respond to. A URL scheme usually something like yourapp://, or in the case of the screenshot below maxine://. This means that whenever somebody tries to open a URL that looks like maxine://workouts/20250601 my app can be asked to handle this URL. Other examples of URL Schemes are http://, https://, ws:// etc.

You'll want to make sure that your scheme is somewhat unique so that you're not competing with other apps for that scheme. It's good to know that iOS will use the first app that the user installed to handle a scheme if multiple apps claim the same scheme. So for example, if you register myapp:// and the user installs your app, then your app will be used to handle myapp:// links if your app was the first to claim support for that scheme.

To claim a scheme, you go to the Info editor for your project and you can configure your URL Type as follows:

Note that you can ignore every field except for the identifier and the schemes. For macOS apps you can specify a "role" that's assigned to the link. This will allow you to enable read-only access for specific URL schemes for example.

In this post, we'll not explore the specifics of macOS so I'm going to leave this setting as Editor.

Handing incoming deeplinks

To have a central point for handling incoming links, you can use the onOpenURL(perform:) view modifier. on the root of your scene. The perform closure that you pass to this view modifier is called either when your app is asked to handle a URL that uses your URL scheme, or when your app is asked to handle an incoming Universal Link. So really, onOpenURL(perform:) becomes the central entry point for your app when it comes to handling incoming links.

Here's what applying onOpenURL(perform:) looks like when it's applied to your scene.

@main
struct MyApplication: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onOpenURL { url in
          // handle the URL that must be opened
        }
    }
  }
}

I will cover what it means exactly to handle the url in the next section of this article, but usually it will involve mutating some state to load and display the view associated with the URL that must be opened.

What's really neat is that you can specify multiple onOpenURL handlers throughout your app. This means that you can make multiple, smaller changes to your app state which means that you no longer have one place where all of your deeplink handling and view manipulation takes place.

Furthermore, onOpenURL is called when your app is in the foreground, background or not running at all. This means that there is now a single entry point for your app to handle URLs. Even if your app is relaunched after being force-closed.

In the next section, I will show you an example of how you can select a tab in a TabView depending on the URL that your app is requested to open. After that, I will show you how to navigate to a list item in a view that's embedded in a TabView by adding a second onOpenURL view modifier on a child View that contains a List.

Activating a tab in a TabView when opening a URL

In SwiftUI, views are a function of their state. This means that virtually everything in a SwiftUI application can be represented and manipulated as a data model. This means that we can represent the currently selected tab in a SwiftUI TabView as a property on an App struct.

The following code shows how:

struct MyApplication: App {
  @State var activeTab = 0

  var body: some Scene {
    WindowGroup {
      TabView(selection: $activeTab) {
        HomeView()
          .tabItem {
            VStack {
              Image(systemName: "house")
              Text("Home")
            }
          }
          .tag(0)

        SettingsView()
          .tabItem {
            VStack {
              Image(systemName: "gearshape")
              Text("Settings")
            }
          }
          .tag(1)
      }
      .onOpenURL { url in
        // determine which tab should be selected and update activeTab
      }
    }
  }
}

What's important to notice here is the activeTab property. This property is marked as @State and represents the selected tab in the TabView. When creating the TabView, I pass a binding to activeTab to the TabView's initializer. Setting the TabView up like this means that updating activeTab will cause the TabView to update its selected tab as well.

Notice that I set a tag on the views that are added to the TabView. This tag is used to identify the TabView's items. When activeTab matches one of the tags associated with your views, the TabView will activate the matching tab.

In this case that means setting activeTab to 1 would activate the tab that displays SettingsView.

Let's see how you can implement onOpenURL to figure out and activate the correct tab. To do this, I'm going to introduce an extension on URL, and a new type called TabIdentifier:

enum TabIdentifier: Hashable {
  case home, settings
}

extension URL {
  var isDeeplink: Bool {
    return scheme == "my-url-scheme" // matches my-url-scheme://<rest-of-the-url>
  }

  var tabIdentifier: TabIdentifier? {
    guard isDeeplink else { return nil }

    switch host {
    case "home": return .home // matches my-url-scheme://home/
    case "settings": return .settings // matches my-url-scheme://settings/
    default: return nil
    }
  }
}

The code above is just a convient way to figure out which tab belongs to a URL without having to duplicate logic all over the app. If you decide to implement a similar object, the isDeeplink computed property should be updated according to the URLs you want to support. If you're implementing Universal Links, you'll want to check whether the URL's host property matches your hostname. I've set up a very minimal check here for demonstration purposes where I only care about the URL scheme.

The tabIdentifier property is a computed property that uses the host property to determine which tab should be selected. For a Universal Link you'll probably want to use the pathComponents property and compare using the second entry in that array, depending on your mapping strategy. Again, I set this up to be very basic.

You can use this basic setup in the App struct as follows:

struct MyApplication: App {
  @State var activeTab = TabIdentifier.home

  var body: some Scene {
    WindowGroup {
      TabView(selection: $activeTab) {
        HomeView()
          .tabItem {
            VStack {
              Image(systemName: "house")
              Text("Home")
            }
          }
          .tag(TabIdentifier.home) // use enum case as tag

        SettingsView()
          .tabItem {
            VStack {
              Image(systemName: "gearshape")
              Text("Settings")
            }
          }
          .tag(TabIdentifier.settings) // use enum case as tag
      }
      .onOpenURL { url in
        guard let tabIdentifier = url.tabIdentifier else {
          return
        }

        activeTab = tabIdentifier
      }
    }
  }
}

Because I made TabIdentifier Hashable, it can be used as the activeTab identifier. Each tab in the TabView is associated with a TabIdentifier through their tags, and by reading the new tabIdentifier that I added to URL in my extension, I can easily extract the appropriate tab identifier associated with the URL that I need to open.

As soon as I assign the acquired tabIdentifier to activeTab, the TabView is updated marking the appropriate tab as selected along with displaying the appropriate View.

Of course this is only half of what you'll want to typically do when opening a deeplink. Let's take a look at activating a NavigationLink in a different view next.

Handling a URL by activating the correct NavigationLink in a List

You already know how to activate a tab in a TabView when your app needs to handle a URL. Often you'll also need to navigate to a specific detail page in the view that's shown for the selected tab item. The cleanest way I have found to do this, is by adding a second onOpenURL handler that's defined within the detail view that should activate your navigation link.

When you define multiple onOpenURL handlers, the system will call them all, allowing you to make small, local changes to your view's data model. Like selecting a tab in the App struct, and activating a NavigationLink in a child view. Before I show you how to do this, We'll need another extension on URL to extract the information we need to activate the appropriate NavigationLink in a List:

enum PageIdentifier: Hashable {
  case todoItem(id: UUID)
}

extension URL {
  var detailPage: PageIdentifier? {
    guard let tabIdentifier = tabIdentifier,
          pathComponents.count > 1,
          let uuid = UUID(uuidString: pathComponents[1]) else {
      return nil
    }

    switch tabIdentifier {
    case .home: return .todoItem(id: uuid) // matches my-url-scheme://home/<item-uuid-here>/
    default: return nil
    }
  }
}

I've added a new enum called PageIdentifier. This enum has a single case with an associated value. This associated value represents the identifier of the object that I want to deeplink to. My app is a to-do app, and each to-do item uses a UUID as its unique identifier. This is also the identifier that's used in the deeplink. The approach above is similar to what I've shown in the previous section and if you decide you like my URL parsing approach, you'll have to make some modifications to adapt it in your app.

The next step is to implement the HomeView, and select the appropriate item from its list of items:

struct HomeView: View {
  @StateObject var todoItems = TodoItem.defaultList // this is just a placeholder.  
  @State var activeUUID: UUID?

  var body: some View {
    NavigationView {
      List(todoItems) { todoItem in
        NavigationLink(destination: TodoDetailView(item: todoItem), tag: todoItem.id, selection: $activeUUID) {
          TaskListItem(task: todoItem)
        }
      }
      .navigationTitle(Text("Home"))
      .onOpenURL { url in
        if case .todoItem(let id) = url.detailPage {
          activeUUID = id
        }
      }
    }
  }
}

Notice that my HomeView has a property called activeUUID. This property serves the exact same purpose that activeTab fulfilled in the previous section. It represents the identifier for the item that should be selected.

When creating my NavigationLink, I pass a tag and a binding to activeUUID to each NavigationLink object. When SwiftUI notices that the tag for one of my navigation links matches the activeUUID property, that item is selected and pushed onto the NavigationView's navigation stack. If you already have a different item selected, SwitUI will first go back to the root of your NavigationView (deactivating that link) and then navigate to the selected page.

In onOpenURL I check whether url.detailPage points to a todoItem, and if it does I extract its UUID and set it as the active UUID to navigate to that item.

By definiing two onOpenURL handlers like I just did, I can make small changes to local state and SwiftUI takes care of the rest. Both of these onOpenURL handlers are called when the app is expected to handle a link. This means that it's important for each View to check whether it can (and should) handle a certain link, and to make small changes to the view it belongs to rather than making big app-wide state changes like you would in a UIKit app.

In Summary

In this post, you saw how you can use the onOpenURL view modifier to handle open URL requests for your app. You learned that you can define more than one handler, and that onOpenURL is called for all scenarios where your app needs to open a URL. Even if your app is launched after being force closed.

First, I showed you how you can parse a URL to determine which tab in a TabView should be selected. Then I showed you how you can change the active tab in a TabView by tagging your views and passing a selection biding to the TabView's initializer. After that, you saw how you can navigate to a detail view by doing more URL parsing, and passing a tag and binding to your NavigationLink.

If you have any comments, questions, or feedback about this post please reach out on X. I love hearing from you.

Implementing an infinite scrolling list with SwiftUI and Combine

Tons of apps that we build feature lists. Sometimes we build lists of settings, lists of todo items, lists of our favorite pictures, lists of tweets, and many other things. Some of these lists could scroll almost endlessly. Think of a Twitter timeline, a Facebook feed or a list of posts on Reddit.

You might argue that knowing how to build a list that scrolls infinitely and fetches new content whenever a user reaches the end of the list is an essential skill of any iOS developer. That's why as one of my first posts that covers SwiftUI I wanted to explore building a list that can scroll forever.

And to be honest, I was surprised with how simple SwiftUI makes implementing this feature on iOS 14, even though we can't read the current scroll offset of a list like we can in UIKit. Instead of reading a scroll offset we can use a list item's onAppear modifier to trigger a new page load.

Let's find out how.

Implementing the SwiftUI portion of an infinite scrolling list

Before I explain, let's look at some code:

struct EndlessList: View {
  @StateObject var dataSource = ContentDataSource()

  var body: some View {
    List(dataSource.items) { item in
      Text(item.label)
        .onAppear {
          dataSource.loadMoreContentIfNeeded(currentItem: item)
        }
        .padding(.all, 30)
    }
  }
}

This code uses @StateObject which is new in iOS 14. Read more about @StateObject and how it compares to @ObservedObject here.

I'll show you the data source in a moment, but let's talk about the couple of lines of code in this snippet first. Surely this can't be all we need to support infinite scrolling, right? Well... it turns out it is all we need.

In SwiftUI, onAppear is called when a view is rendered by the system. This doesn't mean that the view will be rendered within the user's view, or that it ever makes it on screen so we're relying on List's performance optimizations here and trust that it doesn't render all of its views at once.

A List will only keep a certain number of views around while rendering so we can use onAppear to hook into List's rendering. Since we have access to the item that's being rendered, we can ask the data source to load more data if needed depending on the item that's being rendered. If this is one of the last items in the data source, we can kick off a page load and add more items to the data source.

Implementing the data source

Let's look at the data source for this example:

class ContentDataSource: ObservableObject {
  @Published var items = [ListItem]()
  @Published var isLoadingPage = false
  private var currentPage = 1
  private var canLoadMorePages = true

  init() {
    loadMoreContent()
  }

  func loadMoreContentIfNeeded(currentItem item: ListItem?) {
    guard let item = item else {
      loadMoreContent()
      return
    }

    let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
    if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
      loadMoreContent()
    }
  }

  private func loadMoreContent() {
    guard !isLoadingPage && canLoadMorePages else {
      return
    }

    isLoadingPage = true

    let url = URL(string: "https://s3.eu-west-2.amazonaws.com/com.donnywals.misc/feed-\(currentPage).json")!
    URLSession.shared.dataTaskPublisher(for: url)
      .map(\.data)
      .decode(type: ListResponse.self, decoder: JSONDecoder())
      .receive(on: DispatchQueue.main)
      .handleEvents(receiveOutput: { response in
        self.canLoadMorePages = response.hasMorePages
        self.isLoadingPage = false
        self.currentPage += 1
      })
      .map({ response in
        return self.items + response.items
      })
      .catch({ _ in Just(self.items) })
      .assign(to: $items)
  }
}

This code uses Combine's new assign(to:) function. Read more about it here.

There's a lot to unpack in that snippet but I think the most interesting bit is loadMoreContent. The rest of the code kind of speaks for itself.

In loadMoreContent I check whether I'm already loading a page, and whether there are more pages to load. I set isLoadingPage to true, and I construct a URL for a page which points to a feed file that I've uploaded to Amazon S3. This would normally be a URL that points to the page that you want to load in your list. I create a dataTaskPublisher so I can load the URL and I use Combine's handleEvents operator to apply side-effects to my data source when a response was loaded.

Next, I update the canLoadMorePages boolean, set isLoadingPage back to false because the load is complete and increment the currentPage. I prefixed handleEvents with receive(on: DispatchQueue.main) because I modify an @Published property in the handleEvents operator which might change my view and that must be done on the main thread. I don't do this in my map that's applied after handleEvents because map is supposed to be pure and not apply side-effects.

In the map I return a value that merges the current list of items with the newly loaded items. Lastly, I catch any erros that might have occured during the page load and replace them with a publisher that re-emits the current list of items. To update the items property I use Combine's new assign(to:) operator. This operator can pipe the output from a publisher directly into an @Published property without needing to manually subscribe to it.

While it's a lot of code, I think this pipeline is relatively straightforward once you understand all of the operators that are used.

Since I made the ContentDataSource's isLoadingPage property @Published, we can use it to add a loading indicator to the bottom of the list to show the user we're loading a new page in case the page isn't loaded by the time the user reaches the end of the list:

struct EndlessList: View {
  @StateObject var dataSource = ContentDataSource()

  var body: some View {
    List {
      ForEach(dataSource.items) { item in
        Text(item.label)
          .onAppear {
            dataSource.loadMoreContentIfNeeded(currentItem: item)
          }
          .padding(.all, 30)
      }

      if dataSource.isLoadingPage {
        ProgressView()
      }
    }
  }
}

This if statement will conditionally show and hide a ProgressView depending on whether we're fetching a new page.

We can modify this example to build in infinite scrolling list using a ScrollView and ForEach through a LazyVStack on iOS 14.

Building an endless scrolling LazyVStack

On iOS 13 it's possible to build scrolling lists using ForEach and VStack. Unfortunately, these components don't work well with the technique for building an infinite list that I just demonstrated. A VStack combined with ForEach builds its entire view hierarchy at once rather than lazily like a List does. This would mean that we'd immediately begin loading items from the server and continue to load more until all pages are loaded without any action from the user. This happens because onAppear is called when a view is added to the view hierarchy rather than when the view actually becomes visible.

Luckily, iOS 14 introduces a LazyVStack that builds its view hierarchy lazily, which means that new items are added to its layout as the user scrolls. This means that the onAppear method for items created in ForEach is called at a similar time as it is for items inside a List, and that we can use it to build our infinite scrolling list without using a List:

struct EndlessList: View {
  @StateObject var dataSource = ContentDataSource()

  var body: some View {
    ScrollView {
      LazyVStack {
        ForEach(dataSource.items) { item in
          Text(item.label)
            .onAppear {
              dataSource.loadMoreContentIfNeeded(currentItem: item)
            }
            .padding(.all, 30)
        }

        if dataSource.isLoadingPage {
          ProgressView()
        }
      }
    }
  }
}

Pretty nifty, right?

In Summary

In this week's post I finally went all-in on SwiftUI. With iOS 14 I think it has reached a level of maturity that makes it attractive to learn, and since all SwiftUI code from iOS 13 also works on iOS 14 I think it's unlikely that Apple will make huge breaking changes to SwiftUI in the near future.

You saw how you can use SwiftUI to build an infinite scrolling list using the onAppear modifier, and how you can back this up with a data source object that's implemented in Combine. I also showed you the new LazyVStack that was added to SwiftUI in iOS 14 which allows you to apply the technique we used to build an inifite scrolling list to a VStack.

If you have questions about this post, or if you have feedback to me I would love to hear from you on Twitter.

Using multi-colored icons in iOS 14 with SF Symbols 2

Apple introduced SF Symbols in iOS 13. SF Symbols allow developers to easily integrate icons in their apps. The SF Symbols icons integrate really well with the default system font, and provide a consistent look throughout the system.

In iOS 14, Apple added over 750 new icons to the SF Symbols library for developers to use in their apps. Additionally, Apple has expanded SF Symbols to include multi-colored icons. For a full overview of the available SF Symbols that are available, including the newly added and multicolor symbols, download the SF Symbols 2 app from Apple's SF Symbols page.

grid of new symbols

To use a multicolored symbol in your app, all you need to do is set the correct rendering mode for your image.

To use a multi-colored icon in SwiftUI you can use the following code:

Image(systemName: "thermometer.sun.fill")
  .font(.largeTitle)
  .renderingMode(.original)

In a UIKit based app, you can set the icon's tint color as follows:

let image = UIImage(systemName: "star.fill")?
  .withRenderingMode(.alwaysOriginal)

Note that at the time of writing I have not managed to get multi-colored SF Symbols to actually work with UIKit and that only a handful of SF Symbols properly show up with multiple colors when used in SwiftUI depending on the device you're using. The iPhone 11 simulator appears to render all icons correctly but the iOS 14 beta on an iPhone 7 doesn't. There's also a bug currently where setting an icon's font-size can cause it to not be colored correctly.

The ability to use multi-colored symbols in your app is a very welcome addition to the SF Symbols feature and I think it can add a really vibrant touch to your apps.

How to change a UICollectionViewListCell’s separator inset

In WWDC2020's session Lists in UICollectionView a slide is shown where a UICollectionViewListCell's separator inset is updated by assigning a new leading anchor to separatorLayoutGuide.leadingAnchor.

Unfortunately, this doesn't work in when you try to do it.

To set the separator inset for a UICollectionViewListCell you can update the leading anchor constraint by overriding updateConstraints in a UICollectionViewListCell subclass. Setting the anchor in init will cause the system to override your custom anchor leaving you with the default inset.

override func updateConstraints() {
  super.updateConstraints()

  separatorLayoutGuide.leadingAnchor.constraint(equalTo: someOtherView.leadingAnchor, constant: 10).isActive = true
}

You can set the leadingAnchor constraint just like you would set any other constraint. In fact, you can even set the separator's trailingAnchor using the same method I just showed if you want to offset the seperator from the trailing edge of your content view.

What’s new with UICollectionView in iOS 14

Last year, the team that works on UICollectionView shipped massive improvements like compositional layout and diffable data sources.

This year, the team went all out and shipped even more amazing improvements to UICollectionView, making UITableView obsolete through the new UICollectionViewCompositionalLayout.list and UICollectionLayoutListConfiguration. This new list layout allows you to create collection views that look and function identical to UITableView. When paired with UICollectionViewListCell your collection view can support features that used to only be available to UITableView.

For instance, you can now add swipe actions to a cell and set its accessories to add certain affordances on a cell like a disclosure indicator.

In addition adding features that make collection views look more like table views when needed, the team also made huge improvements to data sources.

Diffable data sources now have first class support for features like reordering and deleting cells. All you have to do is assign a couple of handlers to your data source and the system handles the rest.

As if that's not enough, you can now build collapsable lists in collection views with hardly any effort at all by setting up your diffable data sources with hierarchical data. This is going to save plenty of folks some serious headaches.

If you've worked with diffable data sources before you probably know that they are super convenient. However, every time you want to change a snapshot for your diffable data source in iOS 13 you must recreate or update the entire snapshot. In iOS 14 you can use section snapshots which allow you to only update a specific section in your data source rather than rebuiding the entire snapshot every time.

Apple also made a whole bunch of changes to how we configure collection view cells. Cells can now adopt a new feature that lets developers apply states and configurations to cells to set up their appearance and state. This means that you no longer directly assign values to labels, images or other components of a cell but instead the cell takes a configuration object and updates its UI accordingly. The best part of this feature in my opinion is that these configurations are not tied to collection view cells per se. Any view that can work with these configurations can accept and apply a configuration making this a highly portably and flexible feature.

Last but not least I would like to mention that in iOS 14 there's a new way for developers to register and dequeue their collection view cells. In iOS 13 and earlier you would use a string identifier to register and dequeue cells. In iOS 14 you can do this through the new UICollectionView.CellRegistration and it's truly awesome.

I'm super happy with all of these new collection view features and I can't wait to take them for a spin.

Learn how to use new UICollectionView features

How to add a custom accessory to a UICollectionViewListCell?

Apple provides several accessory types that you can use to apply certain affordances to a UICollectionViewListCell. However, sometimes these options don't suit your needs and you're looking for something more customizable.

To add a custom accessory to a list cell instead of a standard one, you use the .custom accessory type. The initializer for this accessory takes a UICellAccessory.CustomViewConfiguration that describes how your accessory should look and where it's positioned. Let's dive right in with an example:

// create the accessory configuration
let customAccessory = UICellAccessory.CustomViewConfiguration(
  customView: UIImageView(image: UIImage(systemName: "paperplane")),
  placement: .leading(displayed: .always))

// add the accessory to the cell
cell.accessories = [.customView(configuration: customAccessory)]

To create a custom accessory all you really need to provide is a view that you want to display, and you need to specify where and when the accessory should be displayed. In this case, I created a simple UIImageView that shows a paperplane SF Symbol that's always visible on the leading edge of the cell. It's also possible to pass .trailing to make the accessory appear on the trailing edge of the cell. For the displayed argument of placement, you can pass .whenEditing or .whenNotEditing instead of .always to control whether your accessory should or shouldn't be visible when the collection view is in edit mode.

By default your custom accessory will be shown as close to the content as possible if there are multiple accessories shown on the same side of the cell. You can customize this by passing a closure to the placement object:

let customAccessory = UICellAccessory.CustomViewConfiguration(
  customView: UIImageView(image: UIImage(systemName: "paperplane")),
  placement: .trailing(displayed: .always, at: { accessories in
    return 1
  }))

In the closure you receive the other accessories that are shown on the same side as your accessory and you can return the preferred position for your accessory. A lower value makes your accessory appear closer to the content.

While you can already build a pretty nice accessory with this, there are more arguments that you can pass to UICellAccessory.CustomViewConfiguration's initializer:

let customAccessory = UICellAccessory.CustomViewConfiguration(
  customView: UIImageView(image: UIImage(systemName: "paperplane")),
  placement: .leading(displayed: .always, at: { accessories in
    return 1
  }),
  reservedLayoutWidth: .standard,
  tintColor: .darkGray,
  maintainsFixedSize: false)

This is an example of a fully customized accessory. In addition to the required two arguments you can also pass a reservedLayoutWidth to tell iOS how much space should be used for your accessory. This will help iOS build your layout and space the cell's content appropriately. The tintColor argument is used to set a color for your accessory. The default color is a blue color, in this example I changed this color to .darkGray. Lastly you can determine if the accessory maintains a fixed size, or if it can scale if needed.

How to add accessories to a UICollectionViewListCell?

In iOS 14 Apple added the ability for developers to create collection views that look and feel like table views, except they are far, far more powerful. To do this, Apple introduced a new UICollectionViewCell subclass called UICollectionViewListCell. This new cell class allows us to implement several tableviewcell-like principles, including accessories.

Adding accessories to a cell is done by assigning an array of UICellAccessory items to a UICollectionViewListCell's accessories property. For example, to make a UICollectionViewListCell show a disclosure indicator that makes it clear to a user that they will see more content if they tapp a cell, you would use the following code:

listCell.accessories = [.disclosureIndicator()]

You set a cell's accessories in either the cellForItemAt UICollectionViewDataSource method or in your CellRegistration closure.

Apple has added a whole bunch of accessories that you can add to your cells. For example checkmark to to show the checkmark symbol that you might know from UITableView, .delete to indicate that a user can delete a cell, .reorder to show a reorder control, and more.

You have full control over when certain accessories are displayed and their color. For example, a reorder control is normally only visible when a collection view is in edit mode. To make it always visible, and make it orange instead of gray you'd use the following code:

cell.accessories = [.reorder(displayed: .always, options: .init(tintColor: .orange, showsVerticalSeparator: false))]

Every accessory has its own options object with different parameters. Refer to the documentation to see the configuration options for accessories you're interested in.

How to add custom swipe actions to a UICollectionViewListCell?

In iOS 14 Apple added the ability for developers to create collection views that look and feel like table views, except they are far, far more powerful. To do this, Apple introduced a new UICollectionViewCell subclass called UICollectionViewListCell. This new cell class allows us to implement several tableviewcell-like principles, including swipe actions.

You can add both leading and trailing swipe actions to a cell by assigning a UISwipeActionsConfigurationProvider instance to the collection view's UICollectionLayoutListConfiguration object's leadingSwipeActionsConfigurationProvider and trailingSwipeActionsConfigurationProvider properties. This swipe actions provider is expected to return an instance of UISwipeActionsConfiguration. A UISwipeActionsConfiguration is created using an array of one or more UIContextualAction instances.

You can customize the icon, title, and background color of a contextual icon as needed. Setting an image and background is optional but you are required to pass a title to the UIContextualAction initializer.

Let's look at an example of how you can add a simple trailing swipe action to a list configuration instance:

let listConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)

listConfig.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in 
  guard let self = self else { return nil }

  let model = self.dataSource[indexPath.row] // get the model for the given index path from your data source

  let actionHandler: UIContextualAction.Handler = { action, view, completion in
    model.isDone = true
    completion(true)
    self.collectionView.reloadItems(at: [indexPath])
  }

  let action = UIContextualAction(style: .normal, title: "Done!", handler: actionHandler)
  action.image = UIImage(systemName: "checkmark")
  action.backgroundColor = .systemGreen

  return UISwipeActionsConfiguration(actions: [action])
}

let listLayout = UICollectionViewCompositionalLayout.list(using: listConfig)

collectionView = UICollectionView(frame: .zero, collectionViewLayout: listLayout)

This is a very simple configuration that adds a single trailing action to every cell in the list. The action is configured to have a green background and has a checkmark icon. Note that when you set an image on the action, the tite is hidden from view on iOS. It's still used for accessibillity so make sure to set a good title, even if you plan on using an image in the UI.

Important
Notice that I assign listConfig.trailingSwipeActionsConfigurationProvider before creating my listLayout property. Since UICollectionLayoutListConfiguration is a struct you need to make sure it's fully configured before using it to create a list layout. If you create the list layout first, it will use its own copy of your list configuration and updates you make to your initial instance won't carry over to the UICollectionViewCompositionalLayout because it uses its own copy of the configuration object.

You can return different configurations depending on the index path that you're supplying the swipe actions for. If you don't want to have any swipe actions for a certain index path, you can return nil from the closure that's used to configure your swipe actions.

Note that I created a .normal contextual action. The other option here is .desctructive to indicate that an action might destruct data.

The action handler that's passed to UIContextualAction receives a reference to the action that triggered it, the view that this occured in and a completion handler that you must call.

You also receive a completion handler in the action handler. You must call this handler when you're done handling the action and pass true if you handled the action successfully. If you failed to successfully handle the action, you must pass false. This allows the system to perform any tasks (if needed) related to whether you were able to handle the action or not.

Configure collection view cells with UICollectionView.CellRegistration

In iOS 14 you can use the new UICollectionView.CellRegistration class to register and configure your UICollectionViewCell instances. So no more let cellIdentifier = "MyCell", no more collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) and best of all, you no longer need to cast the cell returned by dequeueReusableCell(withReuseIdentifier:for:) to your custom cell class.

Adopting UICollectionView.CellRegistration in your project is surprisingly straightforward. For demo purposes I created the following UICollectionViewCell subclass:

class MyCollectionViewCell: UICollectionViewCell { 
  let label = UILabel()

  required init?(coder: NSCoder) {
    fatalError("nope!")
  }

  override init(frame: CGRect) {
    super.init(frame: frame)

    contentView.addSubview(label)
    label.translatesAutoresizingMaskIntoConstraints = false
    label.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
    label.leadingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.leadingAnchor, constant: 8).isActive = true
    label.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor, constant:  -8).isActive = true
    label.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -8).isActive = true
  }
}

To register this cell on a collection view using UICollectionView.CellRegistration all you need is an instance of UICollectionView.CellRegistration that's specialized for your cell class and data model. My case I'm going to use a String as my data model since the cell only has a single label. You can use any object you want for your cells. Usually it will be the model that you retrieve in the cellForItemAt delegate method.

// defined as an instance property on my view controller
let simpleConfig = UICollectionView.CellRegistration<MyCollectionViewCell, String> { (cell, indexPath, model) in
  cell.label.text = model

}

Notice that UICollectionView.CellRegistration is generic over two types. The UICollectionViewCell that I want to use, and the model type which is a String in my case. The initializer for UICollectionView.CellRegistration takes a closure that's used to set up the cell. This closure receives a cell, an index path and the model that's used to configure the cell.

In my implementation I simply assign my String model to cell.label.text.

You don't have to register your UICollectionView.CellRegistration on your UICollectionView. Instead, you can use it when you ask your collection view to dequeue a reusable cell in cellForItemAt as follows:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

  let model = "Cell \(indexPath.row)"

  return collectionView.dequeueConfiguredReusableCell(using: simpleConfig,
                                                      for: indexPath,
                                                      item: model)
}

When you call dequeueConfiguredReusableCell(using:for:item:) on a UICollectionView, it dequeues a cell that has the correct class and the closure that you passed to your UICollectionView.CellRegistration initializer is called so you can configure your cell.

All you need to do is make sure that you grab the correct model from your data source, pass it to dequeueConfiguredReusableCell(using:for:item:) and return the freshly obtained cell. That's all there is to it! Pretty nifty right? There's no other special setup involved. No secret tricks. Nothing. Just a much nicer way to obtain and configure collection view cells.