Skip to content

Latest commit

 

History

History
186 lines (128 loc) · 9 KB

SwiftUI.md

File metadata and controls

186 lines (128 loc) · 9 KB

SwiftUI in UITableViewCells

DTTableViewManager introduces support for rendering SwiftUI views in UITableViewCells starting with 11.x release. Registering SwiftUI view is done similarly to registering usual cells:

manager.registerHostingCell(for: Post.self) { model, indexPath in
    PostSwiftUICell(model: model)
}

This functionality is supported on iOS 13 + / tvOS 13+ / macCatalyst 13+. It's important to understand, that this method of showing SwiftUI views in table / collection view cells is not supported by Apple, and has some hacks implemented to make it work.

Implementation for iOS 16+ method of showing SwiftUI views via hosting configuration (https://developer.apple.com/documentation/SwiftUI/UIHostingConfiguration) is hopefully coming a bit later.

Registration of SwiftUI views follows the same pattern as registering other table view cells, however there are some important distinctions:

  • SwiftUI lifecycle management is done by special subclass of UITableViewCell - HostingTableViewCell, provided by DTTableViewManager.
  • SwiftUI views need to be hosted in UIHostingController, which needs to be added as a child to view controller hierarchy, or appearance and sizing methods will not work in SwiftUI view. This is done automatically by HostingTableViewCell, but may have some unintended consequences, which you can read about below.
  • Because SwiftUI views are generally self-sizing, it's recommended to use this approach with self-sizing UITableView.

Let's dive into those topics, as they are important to understand how to use this approach correctly.

UIHostingController hacks

When SwiftUI view (it's UIHostingController) is added to view controller hierarchy, it tries to control several things that may be surprizing in context of UITableViewCell content:

  • Navigation bar appearance
  • Keyboard avoidance / safe area insets
  • Other view controller behaviors I did not encounter yet

For example, in the app I'm working on, adding such hosted cell in view controller that had navigation bar hidden, immediately forced navigation bar to appear. In order to fix this problem, DTTableViewManager provides a way to customize UIHostingController used to host table view cells.

To always hide navigation bar in my view hierarchy, I implemented following subclass of UIHostingController (full credit to this answer on StackOverflow answer:

class ControlledNavigationHostingController<Content: View>: UIHostingController<Content> {

    public override init(rootView: Content) {
        super.init(rootView: rootView)
    }

    @objc dynamic required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.isNavigationBarHidden = true
    }
}

Using this subclass with DTTableViewManager requires modifiying hosting configuration, available via mapping closure:

manager.registerHostingCell(for: Post.self) { model, _ in
    PostSwiftUICell(model: model)
} mapping: { mapping in
    mapping.configuration.hostingControllerMaker = {
        ControlledNavigationHostingController(rootView: $0) 
    }
}

I'm assuming other potential issues, like keyboard avoidance, can also be solved by custom UIHostingController subclass, or swizzling UIHostingController methods. For example, here is great article by Peter Steinberger, that shows how to disable keyboard avoidance for SwiftUI views embedded in table view or collection view cells.

Parent view controller

HostingTableViewCell requires parent view controller to add SwiftUI to view controller hierarchy. DTTableViewManager provides default parent view controller by typecasting DTTableViewManageable instance to UIViewController type. If your class implementing DTTableViewManageable is a view controller, you don't need to do anything.

However, if DTTableViewManageable instance is not a view controller, you would need to specify parent view controller explicitly in mapping closure:

mapping.configuration.parentController = customParentViewController

Determining cell size

Because SwiftUI views are generally self-sized, it's recommended to use self-sizing table view with them. To do that, use automatic height and provide estimated height for cell:

tableView.rowHeight = UITableView.automaticDimension

manager.registerHostingCell(for: Post.self) { model, _ in
    PostSwiftUICell(model: model)
} mapping: { mapping in
    mapping.estimatedHeightForCell { model, indexPath in
     100 // pick appropriate cell height 
    }
}

If you can't or don't want to use automatic cell sizing, make sure SwiftUI view and cells have equal heights, otherwise SwiftUI and autolayout system may fight and produce unexpected results.

Cell state and tap events

While HostingTableViewCell hosts SwiftUI view, it does not communicate to UITableView with any special information on doing so. So, if for example, you simultaneously implement SwiftUI.Button in a cell, and .didSelect event (tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)), they will not play together nicely.

Instead, consider implementing .onTapGesture modifier on SwiftUI view and passing events through it's data model (view model would probably fit better here).

HostingTableViewCell also supresses selection state by default to prevent clashing of different cell states SwiftUI is not aware of (UITableViewCell.SelectionStyle = .none).

Customizing hosting cell

HostingTableViewCell is designed to be just a container for SwiftUI view, so all it's views have background of UIColor.clear by default, and cell is not selectable. If you need, you can customize colors and selection state of the cell using configuration:

mapping.configuration.backgroundColor = customColor
mapping.configuration.contentViewBackgroundColor = customColor
mapping.configuration.hostingViewBackgroundColor = customColor

mapping.configuration.selectionStyle = .default

If you need any other changes on HostingTableViewCell, you can also provide a closure, that is run after all cell updates:

mapping.configuration.configureCell = { cell in
   // Customize cell
}

Perfomance

Each cell creates only one hosting controller, that is reused when cell is updated with new data.

In order to preserve perfomance, background colors and selection state is set only once when cell is first created. When cell is being reused, only configureCell closure is called on each cell update.

Is it worth it?

I leave answer to this question for your consideration, since Apple does not support this, and some hacks may be required to work with hosted cells.

For me, however, it was 100% worth it. After applying navigation bar hack, I've encountered no problems, and live previewing table view cells in different view states is super helpful in implementing complex views, and is overall much simpler and efficient than doing it in UIKit.

What about delegate methods for hosted cells?

SwiftUI hosted cells support all delegate methods implemenented for non-hosted cells, for example:

manager.registerHostingCell(for: Post.self) { model, _ in
    PostSwiftUICell(model: model)
} mapping: { mapping in
    mapping.willDisplay { cell, model, indexPath in
    
    }
}

Can I use SwiftUI in table view headers / footers?

It seems possible, and code infrastructure is prepared to implement SwiftUI views in table view headers and footers, but I'm not rushing there yet. It's possible there might be some more hacks there, and I'm not sure at this point, if it's worth doing that, since Apple only introduced support for cells in iOS 16.

However, I might reconsider this, if there's demand for this feature.

Is UIHostingConfiguration supported on iOS 16 and higher?

UIHostingConfiguration on iOS 16 is supported by additional registration method:

manager.registerHostingConfiguration(for: Post.self) { cell, post, indexPath in
    UIHostingConfiguration {
        PostView(post: post)
    }
}

Because this is officially supported way of integrating SwiftUI with table and collection view cells, I highly recommend watching WWDC 2022 session video on this topic.

All customization options for UIHostingConfiguration is fully supported, for example you can customize margins for cells content:

manager.registerHostingConfiguration(for: Post.self) { cell, post, indexPath in
    UIHostingConfiguration {
        PostView(post: post)
    }.margins(.horizontal, 16)
}

Additionally, you can also use UICellConfigurationState of a cell by simply adding one additional parameter:

manager.registerHostingConfiguration(for: Post.self) { state, cell, post, indexPath in
    UIHostingConfiguration {
        PostView(post: post, isSelected: state.isSelected)
    }
}