Skip to content

Commit c30888f

Browse files
implement UKAvatarGroup
1 parent b3ed278 commit c30888f

File tree

6 files changed

+244
-5
lines changed

6 files changed

+244
-5
lines changed

Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AvatarGroupPreview.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ struct AvatarGroupPreview: View {
2727

2828
var body: some View {
2929
VStack {
30+
PreviewWrapper(title: "UIKit") {
31+
UKAvatarGroup(model: self.model)
32+
.preview
33+
}
3034
PreviewWrapper(title: "SwiftUI") {
3135
SUAvatarGroup(model: self.model)
3236
}

Sources/ComponentsKit/Components/Avatar/SwiftUI/AvatarContent.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ struct AvatarContent: View {
2727
.clipShape(
2828
RoundedRectangle(cornerRadius: self.model.cornerRadius.value())
2929
)
30+
.onAppear {
31+
self.imageManager.update(model: self.model, size: geometry.size)
32+
}
3033
.onChange(of: self.model) { newValue in
3134
self.imageManager.update(model: newValue, size: geometry.size)
3235
}

Sources/ComponentsKit/Components/AvatarGroup/Models/AvatarGroupVM.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension AvatarGroupVM {
7070
return avatars
7171
}
7272

73-
var avatarSize: CGSize {
73+
var itemSize: CGSize {
7474
switch self.size {
7575
case .small:
7676
return .init(width: 36, height: 36)
@@ -92,7 +92,22 @@ extension AvatarGroupVM {
9292
}
9393
}
9494

95+
var spacing: CGFloat {
96+
return -self.itemSize.width / 3
97+
}
98+
9599
var numberOfHiddenAvatars: Int {
96-
return self.items.count - self.maxVisibleAvatars
100+
return max(0, self.items.count - self.maxVisibleAvatars)
101+
}
102+
}
103+
104+
// MARK: - UIKit Helpers
105+
106+
extension AvatarGroupVM {
107+
var avatarHeight: CGFloat {
108+
return self.itemSize.height - self.padding * 2
109+
}
110+
var avatarWidth: CGFloat {
111+
return self.itemSize.width - self.padding * 2
97112
}
98113
}

Sources/ComponentsKit/Components/AvatarGroup/SUAvatarGroup.swift renamed to Sources/ComponentsKit/Components/AvatarGroup/SwiftUI/SUAvatarGroup.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public struct SUAvatarGroup: View {
1919
// MARK: - Body
2020

2121
public var body: some View {
22-
HStack(spacing: -self.model.avatarSize.width / 3) {
22+
HStack(spacing: self.model.spacing) {
2323
ForEach(self.model.identifiedAvatarVMs, id: \.0) { _, avatarVM in
2424
AvatarContent(model: avatarVM)
2525
.padding(self.model.padding)
@@ -28,8 +28,8 @@ public struct SUAvatarGroup: View {
2828
RoundedRectangle(cornerRadius: self.model.cornerRadius.value())
2929
)
3030
.frame(
31-
width: self.model.avatarSize.width,
32-
height: self.model.avatarSize.height
31+
width: self.model.itemSize.width,
32+
height: self.model.itemSize.height
3333
)
3434
}
3535
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import AutoLayout
2+
import UIKit
3+
4+
final class AvatarContainer: UIView {
5+
// MARK: - Properties
6+
7+
let avatar: UKAvatar
8+
var groupVM: AvatarGroupVM
9+
var avatarConstraints = LayoutConstraints()
10+
11+
// MARK: - Initialization
12+
13+
init(avatarVM: AvatarVM, groupVM: AvatarGroupVM) {
14+
self.avatar = UKAvatar(model: avatarVM)
15+
self.groupVM = groupVM
16+
17+
super.init(frame: .zero)
18+
19+
self.setup()
20+
self.style()
21+
self.layout()
22+
}
23+
24+
required init?(coder: NSCoder) {
25+
fatalError("init(coder:) has not been implemented")
26+
}
27+
28+
// MARK: - Setup
29+
30+
func setup() {
31+
self.addSubview(self.avatar)
32+
}
33+
34+
// MARK: - Style
35+
36+
func style() {
37+
Self.Style.mainView(self, model: self.groupVM)
38+
}
39+
40+
// MARK: - Layout
41+
42+
func layout() {
43+
self.avatarConstraints = .merged {
44+
self.avatar.allEdges(self.groupVM.padding)
45+
self.avatar.height(self.groupVM.avatarHeight)
46+
self.avatar.width(self.groupVM.avatarWidth)
47+
}
48+
49+
self.avatarConstraints.height?.priority = .defaultHigh
50+
}
51+
52+
override func layoutSubviews() {
53+
super.layoutSubviews()
54+
55+
self.layer.cornerRadius = self.groupVM.cornerRadius.value(for: self.bounds.height)
56+
}
57+
58+
// MARK: - Update
59+
60+
func update(avatarVM: AvatarVM, groupVM: AvatarGroupVM) {
61+
let oldModel = self.groupVM
62+
self.groupVM = groupVM
63+
64+
if self.groupVM.size != oldModel.size {
65+
self.avatarConstraints.top?.constant = groupVM.padding
66+
self.avatarConstraints.leading?.constant = groupVM.padding
67+
self.avatarConstraints.bottom?.constant = -groupVM.padding
68+
self.avatarConstraints.trailing?.constant = -groupVM.padding
69+
self.avatarConstraints.height?.constant = groupVM.avatarHeight
70+
self.avatarConstraints.width?.constant = groupVM.avatarWidth
71+
72+
self.setNeedsLayout()
73+
}
74+
75+
self.avatar.model = avatarVM
76+
self.style()
77+
}
78+
}
79+
80+
// MARK: - Style Helpers
81+
82+
extension AvatarContainer {
83+
fileprivate enum Style {
84+
static func mainView(_ view: UIView, model: AvatarGroupVM) {
85+
view.backgroundColor = model.borderColor.uiColor
86+
view.layer.cornerRadius = model.cornerRadius.value(for: view.bounds.height)
87+
}
88+
}
89+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import AutoLayout
2+
import UIKit
3+
4+
/// A UIKit component that displays a group of avatars.
5+
open class UKAvatarGroup: UIView, UKComponent {
6+
// MARK: - Properties
7+
8+
/// A model that defines the appearance properties.
9+
public var model: AvatarGroupVM {
10+
didSet {
11+
self.update(oldValue)
12+
}
13+
}
14+
15+
// MARK: - Subviews
16+
17+
/// The stack view that contains avatars.
18+
public var stackView = UIStackView()
19+
20+
// MARK: - Initializers
21+
22+
/// Initializer.
23+
/// - Parameters:
24+
/// - model: A model that defines the appearance properties.
25+
public init(model: AvatarGroupVM = .init()) {
26+
self.model = model
27+
28+
super.init(frame: .zero)
29+
30+
self.setup()
31+
self.style()
32+
self.layout()
33+
}
34+
35+
public required init?(coder: NSCoder) {
36+
fatalError("init(coder:) has not been implemented")
37+
}
38+
39+
// MARK: - Setup
40+
41+
private func setup() {
42+
self.addSubview(self.stackView)
43+
self.model.identifiedAvatarVMs.forEach { _, avatarVM in
44+
self.stackView.addArrangedSubview(AvatarContainer(
45+
avatarVM: avatarVM,
46+
groupVM: self.model
47+
))
48+
}
49+
}
50+
51+
// MARK: - Style
52+
53+
private func style() {
54+
Self.Style.stackView(self.stackView, model: self.model)
55+
}
56+
57+
// MARK: - Layout
58+
59+
private func layout() {
60+
self.stackView.vertically()
61+
self.stackView.centerHorizontally()
62+
self.stackView.leadingAnchor.constraint(
63+
greaterThanOrEqualTo: self.leadingAnchor
64+
).isActive = true
65+
self.stackView.trailingAnchor.constraint(
66+
lessThanOrEqualTo: self.trailingAnchor
67+
).isActive = true
68+
}
69+
70+
// MARK: - Update
71+
72+
public func update(_ oldModel: AvatarGroupVM) {
73+
guard self.model != oldModel else { return }
74+
75+
let avatarVMs = self.model.identifiedAvatarVMs.map(\.1)
76+
self.addOrRemoveArrangedSubviews(newNumber: avatarVMs.count)
77+
78+
self.stackView.arrangedSubviews.enumerated().forEach { index, view in
79+
(view as? AvatarContainer)?.update(
80+
avatarVM: avatarVMs[index],
81+
groupVM: self.model
82+
)
83+
}
84+
self.style()
85+
86+
if self.model.size != oldModel.size {
87+
self.setNeedsLayout()
88+
self.invalidateIntrinsicContentSize()
89+
}
90+
}
91+
92+
private func addOrRemoveArrangedSubviews(newNumber: Int) {
93+
let diff = newNumber - self.stackView.arrangedSubviews.count
94+
if diff > 0 {
95+
for _ in 0 ..< diff {
96+
self.stackView.addArrangedSubview(AvatarContainer(avatarVM: .init(), groupVM: self.model))
97+
}
98+
} else if diff < 0 {
99+
for _ in 0 ..< abs(diff) {
100+
if let view = self.stackView.arrangedSubviews.first {
101+
self.stackView.removeArrangedSubview(view)
102+
view.removeFromSuperview()
103+
}
104+
}
105+
}
106+
}
107+
108+
// MARK: - Layout
109+
110+
open override func layoutSubviews() {
111+
super.layoutSubviews()
112+
113+
self.stackView.arrangedSubviews.forEach { view in
114+
view.layer.cornerRadius = self.model.cornerRadius.value(for: view.bounds.height)
115+
}
116+
}
117+
}
118+
119+
// MARK: - Style Helpers
120+
121+
extension UKAvatarGroup {
122+
fileprivate enum Style {
123+
static func stackView(_ view: UIStackView, model: Model) {
124+
view.axis = .horizontal
125+
view.spacing = model.spacing
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)