Handling deeplinks in iOS 14 with onOpenURL

Starting with iOS 14, we can write apps that are fully built using SwiftUI, dropping the need to have AppDelegate and SceneDelegate files entirely. For as long as I remember, I've handled deeplinks in my AppDelegate and for the past year in the SceneDelegate. So when Apple introduced developers to this new @main annotated App struct style of building apps, I'm sure we all had the same question on our mind. How does the new App struct work with deeplinks and other tasks that are normally performed in the AppDelegate?

Luckily, Apple engineers made sure that handling deeplinks in our apps is still possible with the new onOpenURL(perform:) view modifier.

Handling deeplinks with onOpenURL

The new onOpenURL(perform:) view modifier is new in 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.

This is vastly different from how we're used to dealing with URLs in UIKit and the SceneDelegate flow.

The old way of handling deeplinks requires you to handle each link in the SceneDelegate (or AppDelegate). You would have to manipulate the selected tab in a UITabBarViewController, or present a UIViewController by inspecting the current view controller hierarchy and pushing the needed UIViewController from right inside of the SceneDelegate.

In SwiftUI, you can use the onOpenURL(perform:) on the root of your scene as follows:

@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

This week, you saw how you can use iOS 14's new 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.

I was rather surprised when I learned that deeplink handling on iOS 14 with SwiftUI is this powerful and flexible. Since we can specify onOpenURL handlers wherever they are needed, it's much easier to decouple and compose your app using small parts. You no longer need a SceneDelegate that knows exactly how your entire app is structured because it needs to manipulate your app's entire navigation state from a single place. It feels much cleaner to handle URLs on a local level where the side-effects are limited to the view that's handling the URL.

If you have any comments, questions, or feedback about this post please reach out on Twitter. 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.

What’s the difference between @StateObject and @ObservedObject?

Views in SwiftUI are thrown away and recreated regularly. When this happens, the entire view struct is initialized all over again. Because of this, any values that you create in a SwiftUI view are reset to their default values unless you've marked these values using @State.

This means that if you declare a view that creates its own @ObservedObject instance, that instance is replaced every time SwiftUI decides that it needs to discard and redraw that view.

If you want to see what I mean, try running the following SwiftUI view:

class DataSource: ObservableObject {
  @Published var counter = 0
}

struct Counter: View {
  @ObservedObject var dataSource = DataSource()

  var body: some View {
    VStack {
      Button("Increment counter") {
        dataSource.counter += 1
      }

      Text("Count is \(dataSource.counter)")
    }
  }
}

struct ItemList: View {
  @State private var items = ["hello", "world"]

  var body: some View {
    VStack {
      Button("Append item to list") {
        items.append("test")
      }

      List(items, id: \.self) { name in
        Text(name)
      }

      Counter()
    }
  }
}

While the views in this example might not be super useful, it does a good job of demonstrating how Counter creates its own @ObservedObject. If you'd tap the Increment counter button defined in Counter a couple of times, you'd see that its Count is ... label updates everytime. If you then tap the Append item to list button that's defined in ItemList, the Count is ... label in Counter resets back to 0. The reason for this is that Counter got recreated which means that we now have a fresh instance of DataSource.

To fix this, we could either create the DataSource in ItemList, keep that instance around as a property on ItemList and pass that instance to Counter, or we can use @StateObject instead of @ObservedObject:

struct Counter: View {
  @StateObject var dataSource = DataSource()

  var body: some View {
    VStack {
      Button("Increment counter") {
        dataSource.counter += 1
      }

      Text("Count is \(dataSource.counter)")
    }
  }
}

By making DataSource a @StateObject, the instance we create is kept around and used whenever the Counter view is recreated. This is extremely useful because ItemList doesn't have to retain the DataSource on behalf of the Counter, which makes the DataSource that much cleaner.

You should use @StateObject for any ObservableObject properties that you create yourself in the object that holds on to that object. So in this case, Counter creates its own DataSource which means that if we want to keep it around, we must mark it as an @StateObject.

If a view receives an ObservableObject in its initializer you can use @ObservedObject because the view does not create that instance on its own:

struct Counter: View {
  // the DataSource must now be passed to Counter's initializer
  @ObservedObject var dataSource: DataSource

  var body: some View {
    VStack {
      Button("Increment counter") {
        dataSource.counter += 1
      }

      Text("Count is \(dataSource.counter)")
    }
  }
}

Keep in mind though that this does not solve the problem in all cases. If the object that creates Counter (or your view that has an @ObservedObject) does not retain the ObservableObject, a new instance is created every time that view redraws its body:

struct ItemList: View {
  @State private var items = ["hello", "world"]

  var body: some View {
    VStack {
      Button("Append item to list") {
        items.append("test")
      }

      List(items, id: \.self) { name in
        Text(name)
      }

      // a new data source is created for every redraw
      Counter(dataSource: DataSource())
    }
  }
}

However, this does not mean that you should mark all of your @ObservedObject properties as @StateObject. In this last case, it might be the intent of the ItemList to create a fresh DataSource every time the view is redrawn. If you'd have marked Counter.dataSource as @StateObject the new instance would be ignored and your app might now have a new hidden bug.

A not completely unimportant implication of @StateObject is performance. If you're using an @ObservedObject that's recreated often that might harm your view's rendering performance. Since @StateObject is not recreated for every view re-render, it has a far smaller impact on your view's drawing cycle. Of course, the impact might be minimal for a small object, but could grow rapidly if your @ObservedObject is more complex.

So in short, you should use @StateObject for any observable properties that you initialize in the view that uses it. If the ObservableObject instance is created externally and passed to the view that uses it mark your property with @ObservedObject.

For a quick reference to SwiftUI's property wrappers, take a look at swiftuipropertywrappers.com.