Creating a Functional Dribbble App Design with UIKit
Written on
Chapter 1: Introduction to App Design
In this tutorial series, we will dive into the process of converting a digital design into a working application.
Welcome back, everyone! In our last session, we started to implement the CollectionView CompositionalLayout.
Now, let's take a deeper look into CompositionalLayout! We will initiate our journey by creating a file named Layouts.swift. This file will help us manage multiple layouts for different CollectionView cells and will be structured as a Singleton to ensure only a single instance exists throughout the app.
Inside this Singleton, we will define a function called headerSection() that returns an NSCollectionLayoutSection. This function will allow us to set up the layout for TopCell, utilizing a layout group that consists of a layout item.
import Foundation
import UIKit
class Layouts {
static let shared = Layouts() // Singleton instance
func headerSection() -> NSCollectionLayoutSection {
// Create an item that fills its container
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)))
// Create a vertical group with fixed height
let group = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(150)), subitems: [item])
// Create a section using the group
let section = NSCollectionLayoutSection(group: group)
return section
}
}
Next, we will return to HomeController and update the layout configuration in the init() function to use the Layouts singleton.
init() {
let layout = UICollectionViewCompositionalLayout(section: Layouts.shared.headerSection())
super.init(collectionViewLayout: layout)
}
Chapter 2: Developing the Services Section
Having completed the TopCell, it's time to move on to the services section. We aim to achieve a specific layout, which will require some customization. First, we will create a new file named ServiceContainer.swift. This will subclass UIView and represent an individual service, containing a UIImageView for the icon and a UILabel for the title.
class ServiceContainer: UIView {
// Property to hold the icon name and trigger UI updates
var iconName: String? {
didSet {
configData()}
}
// Set up the initial view state
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
configViews()
configConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")}
// Label for the service title
lazy var bodyLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.textColor = UIColor(named: "mainColor")
label.font = .preferredFont(forTextStyle: .headline)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// Image view for the icon
lazy var icon: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFit
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
// Configure views
func configViews() {
layer.cornerRadius = 20
addSubview(bodyLabel)
addSubview(icon)
}
// Set up constraints for the views
func configConstraints() {
NSLayoutConstraint.activate([
bodyLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 4),
bodyLabel.trailingAnchor.constraint(equalToSystemSpacingAfter: icon.leadingAnchor, multiplier: 4),
bottomAnchor.constraint(equalToSystemSpacingBelow: bodyLabel.bottomAnchor, multiplier: 3),
icon.heightAnchor.constraint(equalToConstant: 28),
icon.widthAnchor.constraint(equalToConstant: 28),
icon.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 3),
icon.leadingAnchor.constraint(equalToSystemSpacingAfter: bodyLabel.trailingAnchor, multiplier: 1),
trailingAnchor.constraint(equalToSystemSpacingAfter: icon.trailingAnchor, multiplier: 3),
])
}
// Update image view with the corresponding icon
func configData() {
guard let iconName = iconName else { return }
icon.image = UIImage(systemName: iconName)?.withRenderingMode(.alwaysTemplate)
}
}
Now, let’s create ServicesCell.swift, which will subclass UICollectionViewCell to organize the individual services using UIStackViews.
class ServicesCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
configViews()
configConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")}
// Define top and bottom stack views
var topServices: UIStackView = {
let stack = UIStackView()
stack.distribution = .fillProportionally
stack.axis = .horizontal
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
var bottomServices: UIStackView = {
let stack = UIStackView()
stack.distribution = .fillProportionally
stack.axis = .horizontal
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
// Instances of ServiceContainer for different services
let cleaningService: ServiceContainer = {
let service = ServiceContainer()
service.bodyLabel.text = "CleaningnServicesn& Bundles"
service.backgroundColor = UIColor(named: "greenBG")
service.icon.tintColor = UIColor(named: "greenColor")?.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
service.iconName = "lamp.desk"
return service
}()
let errandsService: ServiceContainer = {
let service = ServiceContainer()
service.bodyLabel.text = "Errandsn& Chores"
service.backgroundColor = UIColor(named: "orangeBG")
service.icon.tintColor = UIColor(named: "orangeColor")?.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
service.iconName = "bag"
return service
}()
let requestsService: ServiceContainer = {
let service = ServiceContainer()
service.bodyLabel.text = "SpecialnRequests"
service.iconName = "bell"
service.backgroundColor = UIColor(named: "yellowBG")
service.icon.tintColor = UIColor(named: "yellowColor")?.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
return service
}()
let partnerService: ServiceContainer = {
let service = ServiceContainer()
service.bodyLabel.text = "PartnernServices"
service.iconName = "person.2"
service.backgroundColor = UIColor(named: "grayBG")
service.icon.tintColor = UIColor(named: "grayColor")?.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
return service
}()
// Set up the components of the cell
func configViews() {
addSubview(topServices)
addSubview(bottomServices)
[cleaningService, errandsService].forEach { topServices.addArrangedSubview($0) }
[requestsService, partnerService].forEach { bottomServices.addArrangedSubview($0) }
}
// Set up Auto Layout constraints
func configConstraints() {
NSLayoutConstraint.activate([
topServices.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 0),
topServices.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: topServices.trailingAnchor, multiplier: 2),
bottomServices.topAnchor.constraint(equalToSystemSpacingBelow: topServices.bottomAnchor, multiplier: 2),
bottomServices.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: bottomServices.trailingAnchor, multiplier: 2),
cleaningService.leadingAnchor.constraint(equalToSystemSpacingAfter: topServices.leadingAnchor, multiplier: 0),
cleaningService.heightAnchor.constraint(equalToConstant: 130),
errandsService.heightAnchor.constraint(equalToConstant: 130),
errandsService.centerYAnchor.constraint(equalTo: topServices.centerYAnchor),
topServices.trailingAnchor.constraint(equalToSystemSpacingAfter: errandsService.trailingAnchor, multiplier: 0),
requestsService.leadingAnchor.constraint(equalToSystemSpacingAfter: bottomServices.leadingAnchor, multiplier: 0),
requestsService.heightAnchor.constraint(equalToConstant: 130),
partnerService.heightAnchor.constraint(equalToConstant: 130),
partnerService.centerYAnchor.constraint(equalTo: requestsService.centerYAnchor),
bottomServices.trailingAnchor.constraint(equalToSystemSpacingAfter: partnerService.trailingAnchor, multiplier: 0)
])
}
}
Next, we will return to Layouts.swift and add the following function to create the services section.
func servicesSection() -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(280)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets.top = 8
return section
}
This servicesSection() function will provide a layout for ServicesCell, where items will be arranged horizontally within a group of fixed height. We can now add this cell to HomeController with two minor updates.
init() {
let layout = UICollectionViewCompositionalLayout {
(sectionNumber, _) -> NSCollectionLayoutSection? in
if sectionNumber == 0 {
return Layouts.shared.headerSection()} else {
return Layouts.shared.servicesSection()}
}
super.init(collectionViewLayout: layout)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
The first adjustment involves the init() function where the layout for each section is determined by its section number. The first section is assigned to the header layout, while the subsequent section corresponds to the services layout.
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
With this second adjustment, the number of sections in the collection view is now set to two.
Chapter 3: Conclusion and Next Steps
That's a wrap for this episode! In the next segment, we will focus on creating and implementing the recommendations cell, alongside some exciting titles.
Don't forget to check out the completed source code available in the GitHub repository below, and feel free to leave a star if you find it beneficial!
Thank you for following along, and see you in the next tutorial!