Reputation: 1378
I'm working on a native iOS app and need to implement a layout for a presentation widget in Swift. For this I have the following design concept:
The cells should not be arranged in rows, but rather table-like with different row and column-spans.
This is the code I have so far. It's simple and far from the desired result!
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let insets = collectionView.contentInset
let collectionViewWidth = collectionView.frame.width - (insets.left + insets.right + 1)
let collectionViewHeight = collectionView.frame.height - (insets.top + insets.bottom)
switch indexPath.row {
case 0:
return CGSize(width: collectionViewWidth, height: collectionViewWidth)
case 1:
return CGSize(width: collectionViewWidth / 2, height: collectionViewWidth / 2)
default:
return CGSize(width: collectionViewWidth, height: collectionViewWidth)
}
}
Is this possible? (I come from the Android corner) .. how can I implement the desired arrangement with swift?
The arrangement may need to be rotated by 90° and vertical flipped (depending on the display orientation). The design implementation can be excluded (Borders, Margins). Later, the individual tiles should be changed one after the other using a timer.
Upvotes: 1
Views: 23
Reputation: 1378
Did it :)
MasonryViewController (createLayoput)
private func createLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
let spacing: CGFloat = 8
// Großes Quadrat
let largeItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5),
heightDimension: .fractionalHeight(0.5)
)
)
largeItem.contentInsets = NSDirectionalEdgeInsets(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
// Kleines Quadrat
let smallItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5),
heightDimension: .fractionalHeight(0.5)
)
)
smallItem.contentInsets = NSDirectionalEdgeInsets(top: spacing, leading: spacing, bottom: spacing, trailing: spacing)
// Gruppe mit zwei kleinen Quadraten
let smallGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.5),
heightDimension: .fractionalHeight(1.0)
),
subitems: [smallItem, smallItem]
)
// Horizontale Gruppe mit großem Quadrat und zwei kleinen
let mainGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)
),
subitems: [largeItem, smallGroup]
)
let section = NSCollectionLayoutSection(group: mainGroup)
return section
}
}
...
MasonryCell
import UIKit
class MasonryCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupCell()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupCell() {
contentView.layer.borderWidth = 3
contentView.layer.borderColor = UIColor.black.cgColor
contentView.layer.cornerRadius = 10
contentView.clipsToBounds = true
}
func configure(color: UIColor) {
contentView.backgroundColor = color
}
}
Upvotes: 0
Reputation: 337
You should use UICollectionViewCompositionalLayout
for complex layouts like this: here's a guide.
I was able to come up with this result:
MasonryLayoutViewController.swift
:
import UIKit
class MasonryLayoutViewController: UIViewController {
private let items = Array(1...9)
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
title = "Masonry Layout"
collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: createMasonryLayout()
)
collectionView.backgroundColor = .systemBackground
collectionView.register(MasonryCell.self, forCellWithReuseIdentifier: MasonryCell.reuseIdentifier)
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
// MARK: - layout
private func createMasonryLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
guard let self = self else { return nil }
// number of groups (each group has 3 items)
let groupCount = Int(ceil(Double(self.items.count) / 3.0))
var groups: [NSCollectionLayoutGroup] = []
// alternate groups with different layouts
for i in 0..<groupCount {
if i % 2 == 0 {
groups.append(self.makeBigLeftGroup())
} else {
groups.append(self.makeBigRightGroup())
}
}
let interGroupSpacing: CGFloat = 8
// calculate outer group height
let outerGroupHeight = CGFloat(groupCount) * 300 + CGFloat(max(0, groupCount - 1)) * interGroupSpacing
let outerGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(outerGroupHeight)
)
// create vertical outer group with all groups
let outerGroup = NSCollectionLayoutGroup.vertical(
layoutSize: outerGroupSize,
subitems: groups
)
let section = NSCollectionLayoutSection(group: outerGroup)
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
section.interGroupSpacing = interGroupSpacing
return section
}
}
private func makeBigLeftGroup() -> NSCollectionLayoutGroup {
// big item occupies full width of group
let bigItemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
let bigItem = NSCollectionLayoutItem(layoutSize: bigItemSize)
let bigGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(2.0/3.0),
heightDimension: .fractionalHeight(1.0)
)
let bigGroup = NSCollectionLayoutGroup.vertical(
layoutSize: bigGroupSize,
subitems: [bigItem]
)
let smallItemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)
)
let smallItem = NSCollectionLayoutItem(layoutSize: smallItemSize)
// group for two small items, width is 1/3 of container
let smallGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0 / 3),
heightDimension: .fractionalHeight(1.0)
)
let smallGroup = NSCollectionLayoutGroup.vertical(
layoutSize: smallGroupSize,
subitems: [smallItem, smallItem]
)
// container group with horizontal layout
let containerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(300)
)
let containerGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: containerSize,
subitems: [bigGroup, smallGroup]
)
return containerGroup
}
// group where big item is on right and small items on left
private func makeBigRightGroup() -> NSCollectionLayoutGroup {
// small items, each takes half height
let smallItemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)
)
let smallItem = NSCollectionLayoutItem(layoutSize: smallItemSize)
// group for two small items, width is 1/3 of container
let smallGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1 / 3),
heightDimension: .fractionalHeight(1)
)
let smallGroup = NSCollectionLayoutGroup.vertical(
layoutSize: smallGroupSize,
subitems: [smallItem, smallItem]
)
// big item occupies full width of group
let bigItemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let bigItem = NSCollectionLayoutItem(layoutSize: bigItemSize)
// group for big item, width is 2/3 of container
let bigGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(2/3),
heightDimension: .fractionalHeight(1)
)
let bigGroup = NSCollectionLayoutGroup.vertical(
layoutSize: bigGroupSize,
subitems: [bigItem]
)
// container group with horizontal layout (small items first, then big item)
let containerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(300)
)
let containerGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: containerSize,
subitems: [smallGroup, bigGroup]
)
return containerGroup
}
}
// MARK: - UICollectionViewDataSource
extension MasonryLayoutViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: MasonryCell.reuseIdentifier,
for: indexPath
) as! MasonryCell
cell.configure(with: items[indexPath.item])
return cell
}
}
MasonryCell.swift
:
import UIKit
class MasonryCell: UICollectionViewCell {
static let reuseIdentifier = "MasonryCell"
private let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 22)
label.textColor = .white
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
])
contentView.layer.cornerRadius = 8
contentView.layer.masksToBounds = true
contentView.backgroundColor = randomColor()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with number: Int) {
titleLabel.text = "Element \(number)"
}
private func randomColor() -> UIColor {
let r = CGFloat.random(in: 0...1)
let g = CGFloat.random(in: 0...1)
let b = CGFloat.random(in: 0...1)
return UIColor(red: r, green: g, blue: b, alpha: 1)
}
}
A short explanation:
Items:
Each cell is represented by an NSCollectionLayoutItem
. In our layout, these items are the individual cells you see.
Groups:
Items are combined into groups. In our case, each “container group” is a horizontal group with a fixed height (300 points) that holds 3 items arranged in one of two configurations:
In a “big left” group, one big item (occupying 2/3 of the width) is placed on the left, and a vertical subgroup (with two small items, each taking half the height) is on the right.
In a “big right” group, the layout is reversed: a vertical subgroup of two small items (1/3 width) is on the left, and one big item (2/3 width) is on the right.
Each of these container groups is built using NSCollectionLayoutGroup
(and can have nested groups for the small items).
Sections:
All container groups are then arranged in a vertical outer group (another NSCollectionLayoutGroup
) that forms the section. This outer group is created with a calculated height based on the number of container groups plus spacing. Finally, the section (an NSCollectionLayoutSection
) is returned by the layout provider of the UICollectionViewCompositionalLayout
.
Upvotes: 1