Skip to content

Commit a4dad08

Browse files
authored
Merge pull request #245 from surfstudio/SPT-1522-Add-buildarable-generators
[SPT-1522] Добавление стек-генераторов
2 parents d4b27bb + 4dc4913 commit a4dad08

File tree

24 files changed

+1017
-24
lines changed

24 files changed

+1017
-24
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//
2+
// CollectionStack.swift
3+
// ReactiveDataDisplayManager
4+
//
5+
// Created by Konstantin Porokhov on 30.06.2023.
6+
//
7+
8+
import UIKit
9+
import ReactiveDataDisplayManager
10+
11+
/// Base class for collection stack (generator)
12+
open class CollectionStack: UIView, ConfigurableItem, SelectableItem {
13+
14+
// MARK: - Public properties
15+
16+
public var isNeedDeselect = true
17+
public var didSelectEvent = BaseEvent<Void>()
18+
public var didDeselectEvent = BaseEvent<Void>()
19+
20+
public var views: [UIView] = []
21+
22+
// MARK: - Private properties
23+
24+
private var cell: StackCollectionCell?
25+
private let stackView = UIStackView()
26+
27+
// MARK: - Initialization
28+
29+
/// - Parameters:
30+
/// - space: Space between items
31+
/// - insets: Insets for stack
32+
/// - axis: Axis for stack
33+
/// - items: Items for stack
34+
public init(space: CGFloat,
35+
insets: UIEdgeInsets,
36+
axis: NSLayoutConstraint.Axis,
37+
@ConfigurableItemBuilder items: () -> [any ConfigurableItem]) {
38+
39+
self.views = items().compactMap { item in
40+
return (item as? UICollectionViewCell)?.contentView
41+
?? (item as? UITableViewCell)?.contentView
42+
?? item
43+
}
44+
super.init(frame: .zero)
45+
setupStackView(with: space, insets: insets, axis: axis)
46+
configure(with: views)
47+
}
48+
49+
public required init?(coder: NSCoder) {
50+
fatalError("init(coder:) has not been implemented")
51+
}
52+
53+
// MARK: - Public methods
54+
55+
/// Update items in stack
56+
/// - Parameter items: new items
57+
public func updateItems(_ items: [any ConfigurableItem]) {
58+
self.views = items.compactMap { item in
59+
return (item as? UICollectionViewCell)?.contentView ?? (item as? UITableViewCell)?.contentView ?? item
60+
}
61+
configure(with: views)
62+
}
63+
64+
/// Remove item from stack
65+
/// - Parameter item: item for remove
66+
public func removeItem(_ item: any ConfigurableItem) {
67+
let view = (item as? UICollectionViewCell)?.contentView ?? (item as? UITableViewCell)?.contentView ?? item
68+
guard let index = views.firstIndex(where: { $0 === view }) else {
69+
return
70+
}
71+
views.remove(at: index)
72+
configure(with: views)
73+
}
74+
75+
/// Update size after update content
76+
public func updateSizeIfNeaded() {
77+
updateFrameByContent()
78+
cell?.configure(with: self)
79+
}
80+
81+
// MARK: - ConfigurableItem
82+
83+
public func configure(with model: [UIView]) {
84+
views = model
85+
86+
stackView.removeAllArrangedSubviews()
87+
model.forEach { view in
88+
stackView.addArrangedSubview(view)
89+
}
90+
updateFrameByContent()
91+
cell?.configure(with: self)
92+
}
93+
94+
// MARK: - Private methods
95+
96+
private func setupStackView(with spacing: CGFloat, insets: UIEdgeInsets, axis: NSLayoutConstraint.Axis) {
97+
stackView.axis = axis
98+
stackView.alignment = .fill
99+
stackView.spacing = spacing
100+
stackView.attach(
101+
to: self,
102+
topOffset: insets.top,
103+
bottomOffset: insets.bottom,
104+
leftOffset: insets.left,
105+
rightOffset: insets.right
106+
)
107+
}
108+
109+
}
110+
111+
// MARK: - CollectionCellGenerator
112+
113+
extension CollectionStack: CollectionCellGenerator {
114+
115+
public func generate(collectionView: UICollectionView, for indexPath: IndexPath) -> UICollectionViewCell {
116+
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as? StackCollectionCell else {
117+
return UICollectionViewCell()
118+
}
119+
self.cell = cell
120+
configure(with: views)
121+
return cell
122+
}
123+
124+
public func registerCell(in collectionView: UICollectionView) {
125+
collectionView.register(StackCollectionCell.self, forCellWithReuseIdentifier: identifier)
126+
}
127+
128+
public var identifier: String {
129+
return String(describing: StackCollectionCell.self)
130+
}
131+
132+
public static func bundle() -> Bundle? {
133+
return .main
134+
}
135+
136+
}
137+
138+
// MARK: - Events
139+
140+
public extension CollectionStack {
141+
142+
func didSelectEvent(_ closure: @escaping () -> Void) -> Self {
143+
didSelectEvent.addListner(closure)
144+
return self
145+
}
146+
147+
func didDeselectEvent(_ closure: @escaping () -> Void) -> Self {
148+
didDeselectEvent.addListner(closure)
149+
return self
150+
}
151+
152+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// HorizontalCollectionStack.swift
3+
// ReactiveDataDisplayManager
4+
//
5+
// Created by Konstantin Porokhov on 12.07.2023.
6+
//
7+
8+
import ReactiveDataDisplayManager
9+
import UIKit
10+
11+
/// Base class for horizontal collection stack (generator)
12+
open class HorizontalCollectionStack: CollectionStack {
13+
14+
// MARK: - Initialization
15+
16+
public init(space: CGFloat, insets: UIEdgeInsets = .zero, @ConfigurableItemBuilder items: () -> [any ConfigurableItem]) {
17+
super.init(space: space, insets: insets, axis: .horizontal, items: items)
18+
}
19+
20+
public init(@ConfigurableItemBuilder items: () -> [any ConfigurableItem]) {
21+
super.init(space: 0, insets: .zero, axis: .horizontal, items: items)
22+
}
23+
24+
public required init?(coder: NSCoder) {
25+
fatalError("init(coder:) has not been implemented")
26+
}
27+
28+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// StackCollectionCell.swift
3+
// ReactiveDataDisplayManager
4+
//
5+
// Created by Konstantin Porokhov on 29.06.2023.
6+
//
7+
8+
import UIKit
9+
import ReactiveDataDisplayManager
10+
11+
/// Cell - container with horizontal layout. Accepts generators of other cells, creates a single press state for them
12+
open class StackCollectionCell: UICollectionViewCell, ConfigurableItem, ExpandableItem {
13+
14+
// MARK: - ExpandableItem
15+
16+
public var onHeightChanged = ReactiveDataDisplayManager.BaseEvent<CGFloat?>()
17+
public var animatedExpandable = true
18+
19+
// MARK: - Public methods
20+
21+
public func configure(with model: UIView) {
22+
guard !contentView.contains(model) else {
23+
if contentView.frame != model.frame {
24+
onHeightChanged.invoke(with: nil)
25+
}
26+
return
27+
}
28+
model.attach(to: contentView)
29+
onHeightChanged.invoke(with: nil)
30+
}
31+
32+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// VerticalCollectionStack.swift
3+
// ReactiveDataDisplayManager
4+
//
5+
// Created by Konstantin Porokhov on 12.07.2023.
6+
//
7+
8+
import ReactiveDataDisplayManager
9+
import UIKit
10+
11+
/// Base class for vertical collection stack (generator)
12+
open class VerticalCollectionStack: CollectionStack {
13+
14+
// MARK: - Initialization
15+
16+
/// - Parameters:
17+
/// - space: Space between items
18+
/// - insets: Insets for stack
19+
/// - items: Items for stack
20+
public init(space: CGFloat, insets: UIEdgeInsets = .zero, @ConfigurableItemBuilder items: () -> [any ConfigurableItem]) {
21+
super.init(space: space, insets: insets, axis: .vertical, items: items)
22+
}
23+
24+
/// - Parameters:
25+
/// - items: Items for stack
26+
public init(@ConfigurableItemBuilder items: () -> [any ConfigurableItem]) {
27+
super.init(space: .zero, insets: .zero, axis: .vertical, items: items)
28+
}
29+
30+
public required init?(coder: NSCoder) {
31+
fatalError("init(coder:) has not been implemented")
32+
}
33+
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// BaseCollectionManager+Extension.swift
3+
// ReactiveDataDisplayManagerExample_iOS
4+
//
5+
// Created by Konstantin Porokhov on 29.06.2023.
6+
//
7+
8+
import ReactiveDataDisplayManager
9+
10+
extension BaseCollectionManager {
11+
12+
func contains(generator: CollectionCellGenerator) -> Bool {
13+
let generatorsFromAllSections = sections.flatMap { $0.generators }
14+
15+
return generatorsFromAllSections.contains { $0 === generator }
16+
}
17+
18+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// ConfigurableItem+Builder.swift
3+
// ReactiveDataDisplayManager
4+
//
5+
// Created by Konstantin Porokhov on 06.07.2023.
6+
//
7+
8+
import ReactiveDataDisplayManager
9+
10+
public extension ConfigurableItem {
11+
12+
static func buildView(with model: Self.Model, and type: CellRegisterType = .nib) -> Self {
13+
let cell: Self?
14+
switch type {
15+
case .nib:
16+
cell = Self.loadFromNib(bundle: Self.bundle() ?? .main) as? Self
17+
case .class:
18+
cell = Self()
19+
}
20+
guard let cell = cell else {
21+
fatalError("Can't load cell \(Self.self)")
22+
}
23+
cell.configure(with: model)
24+
return cell
25+
}
26+
27+
}
28+
29+
@resultBuilder
30+
public enum ConfigurableItemBuilder {
31+
32+
public static func buildExpression(_ expression: any ConfigurableItem) -> [any ConfigurableItem] {
33+
return [expression]
34+
}
35+
36+
public static func buildExpression(_ expressions: [any ConfigurableItem]) -> [any ConfigurableItem] {
37+
return expressions
38+
}
39+
40+
public static func buildExpression(_ expression: ()) -> [any ConfigurableItem] {
41+
return []
42+
}
43+
44+
public static func buildBlock(_ components: [any ConfigurableItem]...) -> [any ConfigurableItem] {
45+
return components.flatMap { $0 }
46+
}
47+
48+
public static func buildArray(_ components: [[any ConfigurableItem]]) -> [any ConfigurableItem] {
49+
Array(components.joined())
50+
}
51+
52+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// UIStackView+Extension.swift
3+
// ReactiveDataDisplayManager
4+
//
5+
// Created by Konstantin Porokhov on 29.06.2023.
6+
//
7+
8+
import UIKit
9+
10+
// MARK: - Support
11+
12+
extension UIStackView {
13+
14+
func removeAllArrangedSubviews() {
15+
arrangedSubviews.forEach { view in
16+
removeArrangedSubview(view)
17+
view.removeFromSuperview()
18+
}
19+
}
20+
21+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// UIView+Extension.swift
3+
// ReactiveDataDisplayManager
4+
//
5+
// Created by Konstantin Porokhov on 29.06.2023.
6+
//
7+
8+
import UIKit
9+
10+
extension UIView {
11+
12+
@discardableResult
13+
func updateFrameByContent() -> UIView {
14+
let newSize = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
15+
frame.size = newSize
16+
17+
return self
18+
}
19+
20+
func attach(to view: UIView,
21+
topOffset: CGFloat = 0,
22+
bottomOffset: CGFloat = 0,
23+
leftOffset: CGFloat = 0,
24+
rightOffset: CGFloat = 0) {
25+
view.addSubview(self)
26+
translatesAutoresizingMaskIntoConstraints = false
27+
28+
NSLayoutConstraint.activate([
29+
topAnchor.constraint(equalTo: view.topAnchor, constant: topOffset),
30+
bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -bottomOffset),
31+
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: leftOffset),
32+
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -rightOffset)
33+
])
34+
}
35+
36+
static func loadFromNib<T: UIView>(bundle: Bundle) -> T {
37+
let nibName = String(describing: self)
38+
let nib = UINib(nibName: nibName, bundle: bundle)
39+
40+
guard let view = nib.instantiate(withOwner: self, options: nil).first as? T else {
41+
return T()
42+
}
43+
44+
return view
45+
}
46+
47+
}

0 commit comments

Comments
 (0)