SwiftUI - Integrating UIKit

SwiftUI doesn’t provide neither a UICollectionView nor a UIActivityIndicator equivalence so we need to integrate the components directly from UIKit. Apple’s built-in solution to address this issue is the UIViewRepresentable protocol.

When conforming to UIViewRepresentable, our ReusableCollectionView object must implement the following required methods:

func makeUIView(context: UIViewRepresentableContext<ReusableCollectionView>) -> UICollectionView
func updateUIView(_ uiView: UICollectionView, context: UIViewRepresentableContext<ReusableCollectionView>)

The first method will setup your representable view and will be called only once during the app lifetime. The second method will be called every time a view is being updated.

The UICollectionViewDiffableDataSource class is used to provide our view with the latest data. If you’re not familiar with this new API, check out my article here.

In the Headlines app, we need to update our datasource object with the latest data fetched from the web-service.

final class ReusableCollectionViewDelegate: NSObject, UICollectionViewDelegate {
    let section: HeadlinesSection
    let viewModel: HeadlinesViewModel
    let showDetails: (Bool) -> ()
    init(
        section: HeadlinesSection, 
        viewModel: HeadlinesViewModel, 
        handler: @escaping (Bool) -> ()) {
        self.section = section
        self.viewModel = viewModel
        self.showDetails = handler
     }
     func collectionView(
         _ collectionView: UICollectionView, 
         didSelectItemAt indexPath: IndexPath) {
         guard let category = self.viewModel
             .headlines
             .first(where: { $0.name == self.section }) else { 
                 return 
         }
         viewModel.selectedArticle = category.articles[indexPath.item]
         showDetails(true)
      }
}
struct ReusableCollectionView: UIViewRepresentable {
    var viewModel: HeadlinesViewModel
    let section: HeadlinesSection
    let delegate: ReusableCollectionViewDelegate
    let reloadData: Bool
    init(
        viewModel: HeadlinesViewModel, 
        section: HeadlinesSection, 
        shouldReloadData: Bool = true, 
        handler: @escaping (Bool) -> ()) {
     self.viewModel = viewModel
         self.section = section
         self.reloadData = shouldReloadData
         self.delegate = ReusableCollectionViewDelegate(
             section: section,
             viewModel: viewModel,
             handler: handler
         )
     }
     func makeUIView(context: UIViewRepresentableContext<ReusableCollectionView>) -> UICollectionView {
         let collectionView = UICollectionView(
             frame: .zero,
             collectionViewLayout: CollectionViewFlowLayout()
         )
         collectionView.showsHorizontalScrollIndicator = false
         collectionView.delegate = delegate
         let articleNib = UINib(
             nibName: "\(ArticleCell.self)", 
             bundle: .main
         )
         collectionView.register(
             articleNib, 
             forCellWithReuseIdentifier: "\(ArticleCell.self)"
         )
         let dataSource = UICollectionViewDiffableDataSource<HeadlinesSection, HeadlinesContainer>(collectionView: collectionView) { collectionView, indexPath, container in
             let cell = collectionView.dequeueReusableCell(
                 withReuseIdentifier: "\(ArticleCell.self)", 
                 for: indexPath) as? ArticleCell
             guard let category = self.viewModel
                 .headlines
                 .first(where: { $0.name == self.section }) else {
                     return cell
             }
             cell?.configure(
                 article: category.articles[indexPath.item]
             )
             return cell
         }
         populate(dataSource: dataSource)
         context.coordinator.dataSource = dataSource
         return collectionView
     }
     func updateUIView(_ uiView: UICollectionView, context: UIViewRepresentableContext<ReusableCollectionView>) {
         guard let dataSource = context
             .coordinator
             .dataSource else {
                 return
         }
       
        populate(dataSource: dataSource)
     }
     func makeCoordinator() -> MainCoordinator {
         MainCoordinator()
     }
     func populate(dataSource: UICollectionViewDiffableDataSource<HeadlinesSection, HeadlinesContainer>) {
         var snapshot = NSDiffableDataSourceSnapshot<HeadlinesSection, HeadlinesContainer>()
         guard let category = self.viewModel
             .headlines
             .first(where: { $0.name == self.section }), 
             reloadData else {
                 return
          }
         let containers = category.articles.map(HeadlinesContainer.init)
         snapshot.appendSections([category.name])
         snapshot.appendItems(containers)
         dataSource.apply(snapshot)
     }
}
enum HeadlinesSection: String, CaseIterable {
    case sports
    case technology
    case business
    case general
    case science
    case health
    case entertainment
    case filtered
}
final class HeadlinesContainer: Hashable {
    let id = UUID()
    var article: Article
    init(article: Article) {
        self.article = article
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    static func == (
        lhs: HeadlinesContainer, 
        rhs: HeadlinesContainer) -> Bool {
        return lhs.id == rhs.id
    }
}

When a SwiftUI view is rendered for the first time, collection views are initialized through the makeUI method and the system keep these instances in memory.

func content(
    forMode mode: Mode, 
    headlines: Headlines) -> some View {
    Group {
        if mode == .image {
            VStack(alignment: .leading) {
                HeaderView(headlines: headlines)
                CategoryRow(
                    model: self.viewModel,
                    section: headlines.name,
                    shouldReloadData: !self.navigator.showSheet,
                    handler: { _ in 
                        self.navigator.presenting = .details 
                    }
                 )
            }.frame(height: headlines.isFavorite ? 400: 300)
         } else {
             HeaderView(headlines: headlines)
             ForEach(headlines.articles, id: \.title) { article in
                 ArticleRow(
                     article: article, 
                     viewModel: self.viewModel
                 ) { _ in 
                     self.navigator.presenting = .details 
                   }
             }
          }
      }
}
struct CategoryRow: View {
    let model: HeadlinesViewModel
    let section: HeadlinesSection
    let shouldReloadData: Bool
    let handler: (Bool) -> ()
    var body: some View {
        ReusableCollectionView(
        viewModel: model,
        section: section,
        shouldReloadData: shouldReloadData,
        handler: handler
    ).edgesIgnoringSafeArea(
         .init(arrayLiteral: .leading, .trailing)
     )
    }
}

These ReusableCollectionViewobjects will then be updated through the updateUI method each time a SwiftUI view is being rendered.

The issue here is that we don’t get to see which of our UICollectionView instances is being updated.

The key is to keep track of which category is being updated by storing a local section variable, then using self.viewModel.headlines.first(where: { $0.name == self.section }) which enable us to update the right section with the right data.

Using UIKit APIs directly into a SwiftUI app looks a bit counter intuitive (and error prone), hopefully Apple will release SwiftUI built-in solutions for such components in the next years.