Skip to content

Commit 0149c7b

Browse files
implement avatar group, add avatar group preview
1 parent afe81fa commit 0149c7b

File tree

10 files changed

+211
-19
lines changed

10 files changed

+211
-19
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import ComponentsKit
2+
import SwiftUI
3+
import UIKit
4+
5+
struct AvatarGroupPreview: View {
6+
@State private var model = AvatarGroupVM {
7+
$0.items = [
8+
.init {
9+
$0.imageSrc = .remote(URL(string: "https://i.pravatar.cc/150?img=12")!)
10+
},
11+
.init {
12+
$0.imageSrc = .remote(URL(string: "https://i.pravatar.cc/150?img=14")!)
13+
},
14+
.init {
15+
$0.imageSrc = .remote(URL(string: "https://i.pravatar.cc/150?img=15")!)
16+
},
17+
.init(),
18+
.init(),
19+
.init {
20+
$0.placeholder = .text("IM")
21+
},
22+
.init {
23+
$0.placeholder = .sfSymbol("person.circle")
24+
},
25+
]
26+
}
27+
28+
var body: some View {
29+
VStack {
30+
PreviewWrapper(title: "SwiftUI") {
31+
SUAvatarGroup(model: self.model)
32+
}
33+
Form {
34+
Picker("Border Color", selection: self.$model.borderColor) {
35+
Text("Background").tag(UniversalColor.background)
36+
Text("Accent Background").tag(ComponentColor.accent.background)
37+
Text("Success Background").tag(ComponentColor.success.background)
38+
Text("Warning Background").tag(ComponentColor.warning.background)
39+
Text("Danger Background").tag(ComponentColor.danger.background)
40+
}
41+
ComponentOptionalColorPicker(selection: self.$model.color)
42+
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
43+
Text("Custom: 4px").tag(ComponentRadius.custom(4))
44+
}
45+
Picker("Max Visible Avatars", selection: self.$model.maxVisibleAvatars) {
46+
Text("3").tag(3)
47+
Text("5").tag(5)
48+
Text("7").tag(7)
49+
}
50+
SizePicker(selection: self.$model.size)
51+
}
52+
}
53+
}
54+
}
55+
56+
#Preview {
57+
AvatarGroupPreview()
58+
}

Examples/DemosApp/DemosApp/Core/App.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ struct App: View {
1111
NavigationLinkWithTitle("Avatar") {
1212
AvatarPreview()
1313
}
14+
NavigationLinkWithTitle("Avatar Group") {
15+
AvatarGroupPreview()
16+
}
1417
NavigationLinkWithTitle("Button") {
1518
ButtonPreview()
1619
}

Sources/ComponentsKit/Components/Avatar/Helpers/AvatarImageManager.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ final class AvatarImageManager: ObservableObject {
55
@Published var avatarImage: UIImage
66

77
private var model: AvatarVM
8-
private var remoteImagesCache = NSCache<NSString, UIImage>()
8+
private static var remoteImagesCache = NSCache<NSString, UIImage>()
99

1010
init(model: AvatarVM) {
1111
self.model = model
@@ -27,7 +27,7 @@ final class AvatarImageManager: ObservableObject {
2727

2828
switch model.imageSrc {
2929
case .remote(let url):
30-
if let image = self.remoteImagesCache.object(forKey: url.absoluteString as NSString) {
30+
if let image = Self.remoteImagesCache.object(forKey: url.absoluteString as NSString) {
3131
self.avatarImage = image
3232
} else {
3333
self.avatarImage = model.placeholderImage(for: size)
@@ -47,7 +47,7 @@ final class AvatarImageManager: ObservableObject {
4747
let image = UIImage(data: data)
4848
else { return }
4949

50-
self.remoteImagesCache.setObject(image, forKey: url.absoluteString as NSString)
50+
Self.remoteImagesCache.setObject(image, forKey: url.absoluteString as NSString)
5151

5252
if url == self.model.imageURL {
5353
self.avatarImage = image

Sources/ComponentsKit/Components/Avatar/Models/AvatarVM.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import UIKit
22

33
/// A model that defines the appearance properties for an avatar component.
4-
public struct AvatarVM: ComponentVM {
4+
public struct AvatarVM: ComponentVM, Hashable {
55
/// The color of the placeholder.
66
public var color: ComponentColor?
77

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import SwiftUI
2+
3+
struct AvatarContent: View {
4+
// MARK: - Properties
5+
6+
var model: AvatarVM
7+
8+
@StateObject private var imageManager: AvatarImageManager
9+
10+
// MARK: - Initialization
11+
12+
init(model: AvatarVM) {
13+
self.model = model
14+
self._imageManager = StateObject(
15+
wrappedValue: AvatarImageManager(model: model)
16+
)
17+
}
18+
19+
// MARK: - Body
20+
21+
var body: some View {
22+
GeometryReader { geometry in
23+
Image(uiImage: self.imageManager.avatarImage)
24+
.resizable()
25+
.aspectRatio(contentMode: .fill)
26+
.clipShape(
27+
RoundedRectangle(cornerRadius: self.model.cornerRadius.value())
28+
)
29+
.onChange(of: self.model) { newValue in
30+
self.imageManager.update(model: newValue, size: geometry.size)
31+
}
32+
}
33+
}
34+
}

Sources/ComponentsKit/Components/Avatar/SUAvatar.swift renamed to Sources/ComponentsKit/Components/Avatar/SwiftUI/SUAvatar.swift

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,22 @@ public struct SUAvatar: View {
77
/// A model that defines the appearance properties.
88
public var model: AvatarVM
99

10-
@StateObject private var imageManager: AvatarImageManager
11-
1210
// MARK: - Initialization
1311

1412
/// Initializer.
1513
/// - Parameters:
1614
/// - model: A model that defines the appearance properties.
1715
public init(model: AvatarVM) {
1816
self.model = model
19-
self._imageManager = StateObject(
20-
wrappedValue: AvatarImageManager(model: model)
21-
)
2217
}
2318

2419
// MARK: - Body
2520

2621
public var body: some View {
27-
Image(uiImage: self.imageManager.avatarImage)
28-
.resizable()
29-
.aspectRatio(contentMode: .fill)
22+
AvatarContent(model: self.model)
3023
.frame(
3124
width: self.model.preferredSize.width,
3225
height: self.model.preferredSize.height
3326
)
34-
.clipShape(
35-
RoundedRectangle(cornerRadius: self.model.cornerRadius.value())
36-
)
37-
.onChange(of: self.model) { newValue in
38-
self.imageManager.update(model: newValue, size: newValue.preferredSize)
39-
}
4027
}
4128
}

Sources/ComponentsKit/Components/Avatar/UKAvatar.swift renamed to Sources/ComponentsKit/Components/Avatar/UIKit/UKAvatar.swift

File renamed without changes.

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import UIKit
22

33
/// A model that defines the appearance properties for an avatar group component.
44
public struct AvatarGroupVM: ComponentVM {
5+
/// The border color of avatars.
6+
public var borderColor: UniversalColor = .background
7+
58
/// The color of the placeholder.
69
public var color: ComponentColor?
710

@@ -11,7 +14,13 @@ public struct AvatarGroupVM: ComponentVM {
1114
public var cornerRadius: ComponentRadius = .full
1215

1316
/// The array of avatars in the group.
14-
public var items: [AvatarItemVM] = []
17+
public var items: [AvatarItemVM] = [] {
18+
didSet {
19+
self._identifiedItems = self.items.map({
20+
return .init(id: UUID(), item: $0)
21+
})
22+
}
23+
}
1524

1625
/// The maximum number of visible avatars
1726
///
@@ -23,6 +32,67 @@ public struct AvatarGroupVM: ComponentVM {
2332
/// Defaults to `.medium`.
2433
public var size: ComponentSize = .medium
2534

35+
/// The array of avatar items with an associated id value to properly display content in SwiftUI.
36+
private var _identifiedItems: [IdentifiedAvatarItem] = []
37+
2638
/// Initializes a new instance of `AvatarVM` with default values.
2739
public init() {}
2840
}
41+
42+
// MARK: - Helpers
43+
44+
fileprivate struct IdentifiedAvatarItem: Equatable {
45+
var id: UUID
46+
var item: AvatarItemVM
47+
}
48+
49+
extension AvatarGroupVM {
50+
var identifiedAvatarVMs: [(UUID, AvatarVM)] {
51+
var avatars = self._identifiedItems.prefix(self.maxVisibleAvatars).map { data in
52+
return (data.id, AvatarVM {
53+
$0.color = self.color
54+
$0.cornerRadius = self.cornerRadius
55+
$0.imageSrc = data.item.imageSrc
56+
$0.placeholder = data.item.placeholder
57+
$0.size = self.size
58+
})
59+
}
60+
61+
if self.numberOfHiddenAvatars > 0 {
62+
avatars.append((UUID(), AvatarVM {
63+
$0.color = self.color
64+
$0.cornerRadius = self.cornerRadius
65+
$0.placeholder = .text("+\(self.numberOfHiddenAvatars)")
66+
$0.size = self.size
67+
}))
68+
}
69+
70+
return avatars
71+
}
72+
73+
var avatarSize: CGSize {
74+
switch self.size {
75+
case .small:
76+
return .init(width: 36, height: 36)
77+
case .medium:
78+
return .init(width: 48, height: 48)
79+
case .large:
80+
return .init(width: 64, height: 64)
81+
}
82+
}
83+
84+
var padding: CGFloat {
85+
switch self.size {
86+
case .small:
87+
return 3
88+
case .medium:
89+
return 4
90+
case .large:
91+
return 5
92+
}
93+
}
94+
95+
var numberOfHiddenAvatars: Int {
96+
return self.items.count - self.maxVisibleAvatars
97+
}
98+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import UIKit
22

33
/// A model that defines the appearance properties for an avatar in the group.
44
public struct AvatarItemVM: ComponentVM {
5+
/// The unique identifier for the item.
6+
public var id = UUID()
7+
58
/// The source of the image to be displayed.
69
public var imageSrc: AvatarVM.ImageSource?
710

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import SwiftUI
2+
3+
/// A SwiftUI component that displays a group of avatars.
4+
public struct SUAvatarGroup: View {
5+
// MARK: - Properties
6+
7+
/// A model that defines the appearance properties.
8+
public var model: AvatarGroupVM
9+
10+
// MARK: - Initialization
11+
12+
/// Initializer.
13+
/// - Parameters:
14+
/// - model: A model that defines the appearance properties.
15+
public init(model: AvatarGroupVM) {
16+
self.model = model
17+
}
18+
19+
// MARK: - Body
20+
21+
public var body: some View {
22+
HStack(spacing: -self.model.avatarSize.width / 3) {
23+
ForEach(self.model.identifiedAvatarVMs, id: \.0) { _, avatarVM in
24+
AvatarContent(model: avatarVM)
25+
.padding(self.model.padding)
26+
.background(self.model.borderColor.color)
27+
.clipShape(
28+
RoundedRectangle(cornerRadius: self.model.cornerRadius.value())
29+
)
30+
.frame(
31+
width: self.model.avatarSize.width,
32+
height: self.model.avatarSize.height
33+
)
34+
}
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)