Skip to content

Commit 3cc6a1d

Browse files
authored
Adds EpoxyLayoutGroups - a declarative API for creating components (#27)
* Add LayoutGroups to Epoxy * update Example with content from LayoutGroups * update README * update CHANGELOG * lint * update Rakefile with descriptions * run SwiftFormat * fix podspec * pull in latest updates * address comments * update README * run SwiftLint / SwiftFormat * address comments * run swift lint and format * fix tests
1 parent 3961357 commit 3cc6a1d

File tree

77 files changed

+6262
-86
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+6262
-86
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010
- Added an example with text field to show how can we use `avoidsKeyboard` feature
11+
- Add EpoxyLayoutGroups, a delcarative API for creating components
1112

1213
### Fixed
1314
- `AnyItemModel` is selectable when there are no `DidSelect` callbacks on the underlying model

Epoxy.podspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ Pod::Spec.new do |spec|
55
spec: spec,
66
name: 'Epoxy',
77
summary: 'Declarative UI APIs for UIKit',
8-
local_deps: ['EpoxyCore', 'EpoxyCollectionView', 'EpoxyBars', 'EpoxyNavigationController', 'EpoxyPresentations'])
8+
local_deps: ['EpoxyCore', 'EpoxyLayoutGroups', 'EpoxyCollectionView', 'EpoxyBars', 'EpoxyNavigationController', 'EpoxyPresentations'])
99
end

EpoxyLayoutGroups.podspec

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Pod::Spec.new do |spec|
2+
# Update ConfigurePodspec.rb to increment the version number.
3+
require_relative 'ConfigurePodspec'
4+
configure(
5+
spec: spec,
6+
name: 'EpoxyLayoutGroups',
7+
summary: 'Declarative API for building composable layouts in UIKit with a syntax similar to SwiftUI stack APIs',
8+
local_deps: ['EpoxyCore'])
9+
end

Example/EpoxyExample.xcodeproj/project.pbxproj

+108
Large diffs are not rendered by default.

Example/EpoxyExample/Data/Example.swift

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ enum Example: CaseIterable {
1111
case shuffle
1212
case customSelfSizing
1313
case textField
14+
case layoutGroups
1415

1516
// MARK: Internal
1617

@@ -32,6 +33,8 @@ enum Example: CaseIterable {
3233
return "Card Stack"
3334
case .textField:
3435
return "Text Field"
36+
case .layoutGroups:
37+
return "LayoutGroups"
3538
}
3639
}
3740

@@ -53,6 +56,8 @@ enum Example: CaseIterable {
5356
return "A CollectionView with BarStackView items that have a card drawn around each stack"
5457
case .textField:
5558
return "An example that combines collections, bars, and text field with keyboard avoidance"
59+
case .layoutGroups:
60+
return "A set of examples written using EpoxyLayoutGroups"
5661
}
5762
}
5863
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Created by Tyler Hedrick on 4/14/21.
2+
// Copyright © 2021 Airbnb Inc. All rights reserved.
3+
4+
import Foundation
5+
6+
enum LayoutGroupsExample: CaseIterable {
7+
case readmeExamples
8+
case textRowExample
9+
case colors
10+
case messages
11+
case messagesUIStackView
12+
case todoList
13+
case entirelyInline
14+
case complex
15+
16+
// MARK: Internal
17+
18+
var title: String {
19+
switch self {
20+
case .readmeExamples:
21+
return "Readme examples"
22+
case .textRowExample:
23+
return "Text rows"
24+
case .colors:
25+
return "Group alignments"
26+
case .messages:
27+
return "Message list"
28+
case .messagesUIStackView:
29+
return "Message list (UIStackView)"
30+
case .todoList:
31+
return "Todo List"
32+
case .entirelyInline:
33+
return "Inline components"
34+
case .complex:
35+
return "Shuffle"
36+
}
37+
}
38+
39+
var body: String {
40+
switch self {
41+
case .readmeExamples:
42+
return "All of the examples from the EpoxyLayoutGroups readme"
43+
case .textRowExample:
44+
return "Text rows with various alignments used in the titles"
45+
case .colors:
46+
return "A set of examples that show how group alignments affect subviews"
47+
case .messages:
48+
return "A list of message thread rows created using EpoxyLayoutGroups"
49+
case .messagesUIStackView:
50+
return "A list of message thread rows created using UIStackView to showcase the difference in API"
51+
case .todoList:
52+
return "A sample todo list"
53+
case .entirelyInline:
54+
return "An example showcasing creating components inline in an EpoxyCollectionView ItemModel"
55+
case .complex:
56+
return "An example showing how groups handle updates to the contained items"
57+
}
58+
}
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Created by Tyler Hedrick on 1/27/21.
2+
// Copyright © 2021 Airbnb Inc. All rights reserved.
3+
4+
import EpoxyCollectionView
5+
import UIKit
6+
7+
class ColorsViewController: CollectionViewController {
8+
9+
// MARK: Lifecycle
10+
11+
init() {
12+
super.init(layout: UICollectionViewCompositionalLayout.list)
13+
setItems(items, animated: false)
14+
}
15+
16+
// MARK: Internal
17+
18+
enum DataID {
19+
case hGroupFill
20+
case hGroupTop
21+
case hGroupCenter
22+
case hGroupBottom
23+
case vGroupFill
24+
case vGroupLeading
25+
case vGroupCenter
26+
case vGroupTrailing
27+
}
28+
29+
@ItemModelBuilder
30+
var items: [ItemModeling] {
31+
ColorsRow.itemModel(dataID: DataID.hGroupFill, style: .init(variant: .hGroup(.fill)))
32+
ColorsRow.itemModel(dataID: DataID.hGroupTop, style: .init(variant: .hGroup(.top)))
33+
ColorsRow.itemModel(dataID: DataID.hGroupCenter, style: .init(variant: .hGroup(.center)))
34+
ColorsRow.itemModel(dataID: DataID.hGroupBottom, style: .init(variant: .hGroup(.bottom)))
35+
ColorsRow.itemModel(dataID: DataID.vGroupFill, style: .init(variant: .vGroup(.fill)))
36+
ColorsRow.itemModel(dataID: DataID.vGroupLeading, style: .init(variant: .vGroup(.leading)))
37+
ColorsRow.itemModel(dataID: DataID.vGroupCenter, style: .init(variant: .vGroup(.center)))
38+
ColorsRow.itemModel(dataID: DataID.vGroupTrailing, style: .init(variant: .vGroup(.trailing)))
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Created by Tyler Hedrick on 3/17/21.
2+
// Copyright © 2021 Airbnb Inc. All rights reserved.
3+
4+
import EpoxyLayoutGroups
5+
import UIKit
6+
7+
final class ComplexDeclarativeViewController: UIViewController {
8+
9+
// MARK: Lifecycle
10+
11+
init() {
12+
super.init(nibName: nil, bundle: nil)
13+
}
14+
15+
required init?(coder: NSCoder) {
16+
fatalError("init(coder:) has not been implemented")
17+
}
18+
19+
// MARK: Internal
20+
21+
override func viewDidLoad() {
22+
super.viewDidLoad()
23+
24+
let scrollView = UIScrollView()
25+
scrollView.translatesAutoresizingMaskIntoConstraints = false
26+
scrollView.backgroundColor = .systemBackground
27+
scrollView.layoutMargins = UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
28+
view.addSubview(scrollView)
29+
scrollView.constrainToSuperview()
30+
31+
group.install(in: scrollView)
32+
NSLayoutConstraint.activate([
33+
group.leadingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.leadingAnchor),
34+
group.trailingAnchor.constraint(equalTo: scrollView.layoutMarginsGuide.trailingAnchor),
35+
group.topAnchor.constraint(greaterThanOrEqualTo: scrollView.layoutMarginsGuide.topAnchor),
36+
group.bottomAnchor.constraint(lessThanOrEqualTo: scrollView.layoutMarginsGuide.bottomAnchor),
37+
group.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
38+
])
39+
40+
updateGroup()
41+
}
42+
43+
// MARK: Private
44+
45+
private enum DataID {
46+
case button
47+
case spacer
48+
}
49+
50+
private lazy var group = VGroup(alignment: .fill, spacing: 8)
51+
52+
private func updateGroup() {
53+
group.setItems({
54+
Button.groupItem(
55+
dataID: DataID.button,
56+
content: .init(title: "Shuffle"),
57+
behaviors: .init { [weak self] _ in
58+
self?.updateGroup()
59+
},
60+
style: .init())
61+
randomColorItems()
62+
}, animated: true)
63+
}
64+
65+
private func randomColorItems() -> [GroupItemModeling] {
66+
let possibleItems = [
67+
("Red", UIColor.systemRed),
68+
("Orange", UIColor.systemOrange),
69+
("Yellow", UIColor.systemYellow),
70+
("Green", UIColor.systemGreen),
71+
("Blue", UIColor.systemBlue),
72+
("Purple", UIColor.systemPurple),
73+
]
74+
let numberOfItems = Int.random(in: 1..<possibleItems.count)
75+
let allIndicies = Array(0...numberOfItems).shuffled()
76+
77+
return allIndicies.map { index in
78+
let color = possibleItems[index]
79+
return HGroupItem(
80+
dataID: color.0,
81+
style: .init(spacing: 8))
82+
{
83+
Label.groupItem(
84+
dataID: color.0 + "label",
85+
content: color.0,
86+
style: .style(with: .title3))
87+
SpacerItem(dataID: DataID.spacer)
88+
ColorView.groupItem(
89+
dataID: color.1,
90+
style: .init(size: .init(width: 44, height: 44), color: color.1))
91+
}
92+
}
93+
}
94+
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Created by Tyler Hedrick on 3/24/21.
2+
// Copyright © 2021 Airbnb Inc. All rights reserved.
3+
4+
import EpoxyCollectionView
5+
import EpoxyLayoutGroups
6+
import Foundation
7+
import UIKit
8+
9+
/// This view controller shows how you can create entire components
10+
/// inline using VGroupView and HGroupView inside of Epoxy
11+
class EntirelyInlineViewController: CollectionViewController {
12+
13+
// MARK: Lifecycle
14+
15+
init() {
16+
super.init(layout: UICollectionViewCompositionalLayout.list)
17+
setItems(items, animated: false)
18+
}
19+
20+
// MARK: Internal
21+
22+
@ItemModelBuilder
23+
var items: [ItemModeling] {
24+
VGroupView.itemModel(
25+
dataID: RowDataID.textRow,
26+
content: .init {
27+
titleItem(title: BeloIpsum.sentence(count: 1))
28+
subtitleItem(subtitle: BeloIpsum.paragraph(count: 1))
29+
},
30+
style: .init(
31+
vGroupStyle: .init(spacing: 8),
32+
edgeInsets: .init(top: 16, leading: 24, bottom: 16, trailing: 24)))
33+
HGroupView.itemModel(
34+
dataID: RowDataID.imageRow,
35+
content: .init {
36+
IconView.groupItem(
37+
dataID: GroupDataID.image,
38+
content: UIImage(systemName: "folder"),
39+
style: .init(size: .init(width: 32, height: 32), tintColor: .systemGreen))
40+
.verticalAlignment(.top)
41+
VGroupItem(
42+
dataID: GroupDataID.verticalGroup,
43+
style: .init(spacing: 8))
44+
{
45+
titleItem(title: BeloIpsum.sentence(count: 1))
46+
subtitleItem(subtitle: BeloIpsum.paragraph(count: 1))
47+
}
48+
},
49+
style: .init(
50+
hGroupStyle: .init(spacing: 16),
51+
edgeInsets: .init(top: 16, leading: 24, bottom: 16, trailing: 24)))
52+
}
53+
54+
// MARK: Private
55+
56+
private enum RowDataID {
57+
case textRow
58+
case imageRow
59+
}
60+
61+
private enum GroupDataID {
62+
case title
63+
case subtitle
64+
case image
65+
case verticalGroup
66+
}
67+
68+
private func titleItem(title: String) -> GroupItemModeling {
69+
Label.groupItem(
70+
dataID: GroupDataID.title,
71+
content: title,
72+
style: .style(with: .title2))
73+
}
74+
75+
private func subtitleItem(subtitle: String) -> GroupItemModeling {
76+
Label.groupItem(
77+
dataID: GroupDataID.subtitle,
78+
content: subtitle,
79+
style: .style(with: .body))
80+
}
81+
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Created by Tyler Hedrick on 2/5/21.
2+
// Copyright © 2021 Airbnb Inc. All rights reserved.
3+
4+
import EpoxyCollectionView
5+
import UIKit
6+
7+
class LayoutGroupsReadmeExamplesViewController: CollectionViewController {
8+
9+
// MARK: Lifecycle
10+
11+
init() {
12+
super.init(layout: UICollectionViewCompositionalLayout.list)
13+
setItems(items, animated: false)
14+
}
15+
16+
// MARK: Internal
17+
18+
@ItemModelBuilder
19+
var items: [ItemModeling] {
20+
ActionButtonRow.itemModel(
21+
dataID: DataID.actionButtonRow,
22+
content: .init(
23+
title: "Title text",
24+
subtitle: "Subtitle text",
25+
actionText: "Perform action"))
26+
IconRow.itemModel(
27+
dataID: DataID.iconRow,
28+
content: .init(
29+
title: "This is an IconRow",
30+
icon: UIImage(systemName: "person.fill")!))
31+
}
32+
33+
// MARK: Private
34+
35+
private enum DataID {
36+
case actionButtonRow
37+
case iconRow
38+
}
39+
40+
}

0 commit comments

Comments
 (0)