Skip to content

Commit a14ab1d

Browse files
Merge pull request #63 from componentskit/SUAvatarGroup
SUAvatarGroup
2 parents 4a0c6bc + 35611b2 commit a14ab1d

File tree

10 files changed

+279
-46
lines changed

10 files changed

+279
-46
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("Badge") {
1518
BadgePreview()
1619
}

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

Sources/ComponentsKit/Components/Avatar/SUAvatar.swift

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import SwiftUI
2+
3+
struct AvatarContent: View {
4+
// MARK: - Properties
5+
6+
var model: AvatarVM
7+
8+
@StateObject private var imageManager: AvatarImageManager
9+
@Environment(\.colorScheme) private var colorScheme
10+
11+
// MARK: - Initialization
12+
13+
init(model: AvatarVM) {
14+
self.model = model
15+
self._imageManager = StateObject(
16+
wrappedValue: AvatarImageManager(model: model)
17+
)
18+
}
19+
20+
// MARK: - Body
21+
22+
var body: some View {
23+
GeometryReader { geometry in
24+
Image(uiImage: self.imageManager.avatarImage)
25+
.resizable()
26+
.aspectRatio(contentMode: .fill)
27+
.clipShape(
28+
RoundedRectangle(cornerRadius: self.model.cornerRadius.value())
29+
)
30+
.onAppear {
31+
self.imageManager.update(model: self.model, size: geometry.size)
32+
}
33+
.onChange(of: self.model) { newValue in
34+
self.imageManager.update(model: newValue, size: geometry.size)
35+
}
36+
.onChange(of: self.colorScheme) { _ in
37+
self.imageManager.update(model: self.model, size: geometry.size)
38+
}
39+
}
40+
}
41+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import SwiftUI
2+
3+
/// A SwiftUI component that displays a profile picture, initials or fallback icon for a user.
4+
public struct SUAvatar: View {
5+
// MARK: - Properties
6+
7+
/// A model that defines the appearance properties.
8+
public var model: AvatarVM
9+
10+
// MARK: - Initialization
11+
12+
/// Initializer.
13+
/// - Parameters:
14+
/// - model: A model that defines the appearance properties.
15+
public init(model: AvatarVM) {
16+
self.model = model
17+
}
18+
19+
// MARK: - Body
20+
21+
public var body: some View {
22+
AvatarContent(model: self.model)
23+
.frame(
24+
width: self.model.preferredSize.width,
25+
height: self.model.preferredSize.height
26+
)
27+
}
28+
}

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

File renamed without changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import UIKit
2+
3+
/// A model that defines the appearance properties for an avatar group component.
4+
public struct AvatarGroupVM: ComponentVM {
5+
/// The border color of avatars.
6+
public var borderColor: UniversalColor = .background
7+
8+
/// The color of the placeholders.
9+
public var color: ComponentColor?
10+
11+
/// The corner radius of the avatars.
12+
///
13+
/// Defaults to `.full`.
14+
public var cornerRadius: ComponentRadius = .full
15+
16+
/// The array of avatars in the group.
17+
public var items: [AvatarItemVM] = [] {
18+
didSet {
19+
self._identifiedItems = self.items.map({
20+
return .init(id: UUID(), item: $0)
21+
})
22+
}
23+
}
24+
25+
/// The maximum number of visible avatars.
26+
///
27+
/// Defaults to `5`.
28+
public var maxVisibleAvatars: Int = 5
29+
30+
/// The predefined size of the component.
31+
///
32+
/// Defaults to `.medium`.
33+
public var size: ComponentSize = .medium
34+
35+
/// The array of avatar items with an associated id value to properly display content in SwiftUI.
36+
private var _identifiedItems: [IdentifiedAvatarItem] = []
37+
38+
/// Initializes a new instance of `AvatarGroupVM` with default values.
39+
public init() {}
40+
}
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+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import UIKit
2+
3+
/// A model that defines the appearance properties for an avatar in the group.
4+
public struct AvatarItemVM: ComponentVM {
5+
/// The source of the image to be displayed.
6+
public var imageSrc: AvatarVM.ImageSource?
7+
8+
/// The placeholder that is displayed if the image is not provided or fails to load.
9+
public var placeholder: AvatarVM.Placeholder = .icon("avatar_placeholder", Bundle.module)
10+
11+
/// Initializes a new instance of `AvatarItemVM` with default values.
12+
public init() {}
13+
}
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)