Revitalizing the Getir Food HomePage: A Modern Collection View Approach

Arif Okuyucu
Getir
Published in
12 min readJun 19, 2023
Image generated with Midjourney

Over a year ago, when we decided to upgrade our minimum iOS version to 13, our designers came up with a very exciting design for the filter and sort feature for restaurants. After carefully evaluating this fresh design of the filter and sort feature, we made a pivotal choice to implement it using Compositional Layout and Diffable Data Source. This decision was influenced by two critical factors: the cost-effectiveness of updating the UI with the diffable data source and the convenience it provided in implementing self-sized cells, particularly for the width, using the compositional layout. Furthermore, adopting this new layout system enabled us to eliminate the need for inner collection views within sections.

Once we released this feature, the advantages of combining diffable data source and compositional layout became immediately apparent. Our users experienced seamless scrolling, while our team delighted in the simplicity of extending section layouts and the improved maintainability and testability of our codebase.

Refined Getir Food Filter and Sort Feature Demo: Efficient, No Nested Collection View, Automatic Cell Sizing, and Optimized Cell Selection Updates

Building on the success of implementing Compositional Layout and Diffable Data Source and gaining valuable experience with this new tech stack, we confidently decided as well. Our goal was to revamp the Getir Food HomePage, leveraging the power of the modern collection view approach.

Design of Food Home

Before diving into the implementation of the new design, we recognized the importance of building a robust and scalable structure. Drawing from our experience in implementing the filter and sort feature, we aimed to develop a system that would facilitate easier testing, reduce errors, and allow for future enhancements. As a result, we devised an extendable presentation model for the Diffable Data Source, coupled with a loosely coupled architecture for the data source and layout components. This approach ensured that our implementation was not only efficient but also adaptable to evolving requirements.

Managing the Source of Truth for Diffable Data Sources: Ensuring Consistency and Accuracy

When working with diffable data sources, it is crucial to establish a reliable and accurate source of truth. This involves specifying the types of sections and items when declaring the diffable data source.

/// When declaring the diffable data source, 
/// we used type aliases to simplify the naming and
/// improve code readability. Our typealias definitions
/// for the data source and snapshot reflected the structure
/// of our Home Feature, encompassing the specific section and item types.
typealias DataSource = UICollectionViewDiffableDataSource<
Section,
Item
>

typealias Snapshot = NSDiffableDataSourceSnapshot<
Section,
Item
>

However, managing the source of truth for diffable data sources presented an intriguing challenge. One challenge we face is the potential for undesired animation of snapshot changes in case the data is not properly organized. This can lead to increased costs in updating the user interface. To address this, we harnessed the power of Swift enums. Leveraging the dynamic nature of enums, we were able to declare section types and item types within the diffable data source, ensuring flexibility and adaptability in our data management approach.

Efficient Section Declaration in Diffable Data Source: Simplifying Data Management

In the following code snippet, we define HomeSection the type of DiffableDataSourceSection. This model encompasses various header and section types. The compositional layout utilizes HomeSectionType to determine the layout for each section while HomeSectionHeader determining the layout for section headers. Although we currently don't use a footer, you have the option to extend the implementation by declaring an HomeSectionFooter enum and adding it as a property within HomeSection.

struct HomeSection: Hashable {
var header: HomeSectionHeader
let sectionType: HomeSectionType
}

enum HomeSectionType: Hashable {
case banner
case slidingRestaurants
case restaurant
...
}

enum HomeSectionHeader: Hashable {
case tappableHeader(title: String)
case empty // Empty case is used when there is no header for section
}

By employing this structured approach, we streamline the management of sections and headers, facilitating a more efficient implementation of the diffable data source.

Enhancing Item Declaration in Diffable Data Source

In the code snippet provided, we have the declaration of the HomeSectionItem, which represents the type of item used in the DiffableDataSource. Just like in the previous explanation, we leverage the power of Swift enums here as well.

enum HomeSectionItem: Hashable {
case banner(BannerEntity)
case restaurantSliding(RestaurantEntity)
case restaurant(RestaurantEntity)
}

The HomeSectionItem enum contains various cases, each representing a specific type of item. These cases include banner, infoCard, cuisine, restaurantSliding, and restaurant, each associated with a corresponding entity type.

To define our diffable data source, we use the following type alias:

typealias DataSource = UICollectionViewDiffableDataSource<
HomeSection,
HomeSectionItem
>

This type alias specifies that our data source operates on a HomeSection as the section type and HomeSectionItem as the item type.

Additionally, we define another type alias:

typealias Snapshot = NSDiffableDataSourceSnapshot<
HomeSection,
HomeSectionItem
>

By utilizing these type aliases, we establish a clear and concise definition for our diffable data source, facilitating its usage and management within the codebase.

The Explanation for Separating Section and Item Structures in the Diffable Data Source:

It’s important to note that we maintain separate structures for sections and items in this implementation. The rationale behind this decision is rooted in the animation mechanism provided by the UIKit framework, which relies on checking the hash value of properties.

By keeping sections and items in distinct structures, we prevent unintended animations that could occur if they were combined. For instance, if both sections and items were part of the same structure, modifying an item could trigger an animation in the corresponding section header. Similarly, changes to the section header could inadvertently affect the animations of the items.

By segregating sections and items, we ensure that updates and animations are precisely targeted, enhancing the overall stability and predictability of the diffable data source implementation.

Efficient Utilization of Models for Snapshot and Data Source Conversion

To efficiently apply models to snapshots and data sources, we employ a structured approach. This involves converting response data into models suitable for creating snapshot objects. In our implementation, we utilize a presentation model structure to transfer data to data source objects. These presentation models focus on preserving section and item information and are specifically used during snapshot creation.

struct HomePresentationModel {
var section: HomeSection
var items: [HomeSectionItem]
}

Based on the model examples we’ve examined, it’s clear that we have chosen to use value type over reference type when creating models for our diffable data source. The main reason here is that when the models we will use in diffable data source are reference types, data changes also had unwanted animations and it was very difficult to manage shared instances due to the nature of the reference type. Unlike reference types, which can introduce complexities in these areas, value types offer a more straightforward and reliable approach. Therefore, it is crucial to exercise caution and prioritize the use of value types when working with diffable data sources.
Furthermore, it’s worth highlighting that our adoption of value types has led to a significant reduction in code complexity. This is made possible by leveraging the automatic conformance provided for the Hashable and Equatable protocols. By taking advantage of this automatic conformance, we’ve been able to streamline our code and eliminate the need for manual implementation of these protocols, resulting in cleaner and more efficient definitions.

func applyPresentationModel(
models: [HomePresentationModel],
completion: (() -> Void)?
) {
var snapshot = Snapshot()
models.forEach { model in
snapshot.appendSections([model.section])
snapshot.appendItems(model.items, toSection: model.section)
}
dataSource?.apply(snapshot, completion: completion)
}

Note: I’d like to highlight another important aspect here. When applying the snapshot to the Diffable Data Source, we deliberately did not utilize the animatingDifferences parameter, as its default value is false. I strongly advise against setting this value to true. Enabling it can introduce unexpected animations that may not be desirable. Moreover, setting animatingDifferences to true causes the diffable data source to behave similarly to the reloadData function in iOS 14, which can result in increased costs associated with UI updates. To maintain a smoother and more efficient user experience, it is recommended to stick with the default value of false for this parameter. For more information, I recommend reading the link here.

Here’s how the process unfolds:

  1. Presentation Model Structure: We define a HomePresentationModel the structure contains two properties: section and items. This structure holds the necessary information for creating snapshot objects.
  2. Applying Presentation Models: The applyPresentationModel function takes an array of HomePresentationModel instances as input, along with an optional completion closure(In Getir we are showing loading animation without any skeleton cells so we are using completion for dismissing loading after data is applied.). Within this function, we create a mutable snapshot object and iterate through the models. For each model, we append the corresponding section and items to the snapshot.
  3. Applying Snapshot: Finally, we apply the created snapshot to the data source by invoking the applySnapshot function, and passing the snapshot as a parameter. This step effectively updates the data source with the new snapshot, reflecting the changes made.

By following these steps, we effectively utilize the Diffable Data Source pattern in our Home View. Before delving into the management of Compositional Layout with multiple sections, it is essential to understand how we leverage Diffable Data Source and Compositional Layout within the Viper architecture.

Exploring the Architectural Diagrams for the Home Feature

Architectural Diagrams of Home Feature

In the diagram above, you will notice three additional layers accompanying the usual VIPER architecture structure. These extra layers have been introduced to handle the complexity of the homepage’s structure. Before diving into the details of these layers, it’s important to note that we maintain our general logic within the presenter. While opinions may differ on this matter, we have found it suitable to centralize the general logic within the presenter for better unit testing capabilities.

Let’s take a closer look at each of these extra layers:

  1. Mapper: The Mapper layer is responsible for transforming the response data into the presentation model. It facilitates the conversion process, ensuring that the data is appropriately mapped to the desired format.
  2. Data Source Layer: The Data Source Layer takes charge of managing all the data related to the collection view. In addition, we leverage the new cell registrations introduced in iOS 14 within this layer. It efficiently handles data management, including cell registration and configuration.
  3. Compositional Layout Layer: The Compositional Layout Layer focuses on managing the layout of the sections. It receives section information from the presenter through a delegate and generates layouts accordingly. This layer dynamically adapts the layout based on the provided information, offering flexibility and customization.

To abstract these additional layers, we have utilized protocols. This approach ensures that all layers are easily testable and maintainable. Furthermore, the separation of layout and data source implementation allows for future changes without impacting the view implementation.

Moving forward, let’s delve into the Compositional Layout section. In this section, we will explore how we achieve data synchronization without relying on local properties, solely utilizing the current snapshot values.

Elevating Collection View Layouts to the Next Level

In our previous implementation of the homepage, each section of the collection view was represented by a separate collection view. Unfortunately, this approach introduced code fragility and increased the chances of errors. Moreover, it resulted in writing repetitive boilerplate code for each section. However, with our new layout system, these problems are now a thing of the past! Let’s dive into the details.

To begin with, let’s explore how we leverage the compositional layout delegate to manage sections. The presenter holds the compositional layout delegate and retrieves the current snapshot of the data source via the view protocol. The view, in turn, obtains the current snapshot from the data source layer. By using the current snapshot values, we ensure that the layout and data source remain synchronized. This eliminates common issues such as index out-of-range errors and data synchronization crashes, which were often encountered when using the previous UICollectionViewDataSource approach.

Here’s an example of the protocols and their implementation:

protocol HomeCompositionalLayoutDelegate {
func getSectionType(with sectionIndex: Int) -> HomeSectionType
func getHeaderType(with sectionIndex: Int) -> HomeSectionHeader
}
extension HomePresenter: HomeCompositionalLayoutDelegate {
func getSectionType(with sectionIndex: Int) -> HomeSectionType? {
guard let view,
let snapshot = view.currentSnapshot,
// We are using safe array extension for safety :)
let sectionType = snapshot.sectionIdentifiers[safe: sectionIndex]?.sectionType else {
return nil
}
return sectionType
}

func getHeaderType(with sectionIndex: Int) -> HomeSectionHeader {
guard let view,
let snapshot = view.currentSnapshot,
// We are using safe array extension for safety :)
let header = snapshot.sectionIdentifiers[safe: sectionIndex]?.header else {
return .empty
}
return header
}
}

In the compositional layout class, we define the makeCompositionalLayout function, which is responsible for creating the layout of the collection view. This function is called when the collection view is created on the view side.

private func makeCollectionView() -> UICollectionView {
let collectionView = UICollectionView(
frame: .zero,
// compositionalLayout is an instance of FoodHomeCompositionalLayoutMaking
collectionViewLayout: compositionalLayout.makeCompositionalLayout()
)
...
return collectionView
}

The makeCompositionalLayout the function returns a UICollectionViewCompositionalLayout with a section provider closure. Inside the closure, we call makeLayoutSection to create a section based on the cell type. The same process is applied to the section headers.

To provide a complete example, here’s an outline of the makeLayoutSection and setUpSupplementaryItems functions:

func makeCompositionalLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout
{ [weak self] sectionIndex, layoutEnvironment -> NSCollectionLayoutSection? in
guard let self,
let delegate = self.delegate
else { return nil }
let section = self.makeLayoutSection(
sectionIndex: getSectionType,
cellType: delegate.getCellType(with: sectionIndex),
layoutEnvironment: layoutEnvironment
)

if let section {
section.boundarySupplementaryItems = self.setUpSupplementaryItems(
headerType: delegate.getHeaderType(with: sectionIndex)
)
}

return section
}
return layout
}

// Just an example it does not include every case of HomeSectionType
private func makeLayoutSection(
sectionIndex: Int,
cellType: HomeSectionType,
layoutEnvironment: NSCollectionLayoutEnvironment
) -> NSCollectionLayoutSection? {
switch cellType {
case .banner:
return makeBannerSectionLayout()
case .restaurantList:
return makeRestaurantListLayout(
sectionIndex: sectionIndex,
layoutEnvironment: layoutEnvironment
)
}
}

// Just an example it does not include every case of HomeSectionHeader
private func setUpSupplementaryItems(
headerType: HomeSectionHeader
) -> [NSCollectionLayoutBoundarySupplementaryItem] {
switch headerType {
case .restaurantList:
let header = makeHeader()
return [header]
case .empty:
return []
}
}

By adopting this approach, adding a new section to the collection view becomes incredibly straightforward, requiring only a few lines of code. We owe a debt of gratitude to Apple for providing these valuable tools and APIs. :)

Benefits of the modern approach:

As we reach the end of this article, let’s summarize the benefits of adopting this modern collection view structure:

  1. Significant Reduction in Update Cost: By leveraging Diffable Data Source, we have greatly minimized the overhead associated with updating the collection view.
  2. Simplified Section Implementation: The Compositional layout has streamlined the process of adding new sections, reducing the need for repetitive boilerplate code.
  3. Synchronization Between UI and Data: Utilizing the snapshot value of the Diffable Data Source ensures seamless synchronization between the user interface and the underlying data.
  4. Improved Cell Registration: The new cell registration structure introduced in iOS 14 has eliminated issues related to reuse identifiers and subsequent crashes.
  5. Enhanced Performance: By moving away from nested collection views, we have eliminated performance bottlenecks, resulting in a smoother scrolling experience for the users.

Challenges:

Of course, we encountered some challenges during the development process. For instance, implementing the sliding restaurants section without using an inner collection view required extensive research and custom group implementation, which took more time than anticipated. Additionally, there were instances where we faced unintended change animations due to improper management of the data source snapshot. However, the final outcome demonstrated that the time invested in overcoming these challenges was worthwhile.

It’s worth noting that, except for the sliding restaurants section, which contains multiple cell types, we utilize horizontal or vertical groups for sections . However, for the sliding restaurants section, we employ a custom group due to its diverse cell types. If you’re interested in the custom group implementation, we may consider writing a separate article about it in the future, as there isn’t much information available on this topic.

Sliding Restaurants Section includes two types of UICollectionViewCell

Conclusion

Refined Getir Food Home Feature Demo: Efficient, No Nested Collection View, Automatic Cell Sizing, and Optimized Cell Selection Updates

In this article, we have provided an in-depth explanation of how we implemented the modern collection view structure at Getir. We hope you found it informative and valuable. We eagerly await your feedback and look forward to sharing more articles with you in the future. Until then, take care! 👋

--

--