Building a SwiftUI app using MVVM architecture — Part 6: 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 `ReusableCollectionView`objects 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.
Follow me on Twitter for articles, posts, tips.