Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import ComponentsKit
import SwiftUI
import UIKit

struct AvatarGroupPreview: View {
@State private var model = AvatarGroupVM {
$0.items = [
.init {
$0.imageSrc = .remote(URL(string: "https://i.pravatar.cc/150?img=12")!)
},
.init {
$0.imageSrc = .remote(URL(string: "https://i.pravatar.cc/150?img=14")!)
},
.init {
$0.imageSrc = .remote(URL(string: "https://i.pravatar.cc/150?img=15")!)
},
.init(),
.init(),
.init {
$0.placeholder = .text("IM")
},
.init {
$0.placeholder = .sfSymbol("person.circle")
},
]
}

var body: some View {
VStack {
PreviewWrapper(title: "SwiftUI") {
SUAvatarGroup(model: self.model)
}
Form {
Picker("Border Color", selection: self.$model.borderColor) {
Text("Background").tag(UniversalColor.background)
Text("Accent Background").tag(ComponentColor.accent.background)
Text("Success Background").tag(ComponentColor.success.background)
Text("Warning Background").tag(ComponentColor.warning.background)
Text("Danger Background").tag(ComponentColor.danger.background)
}
ComponentOptionalColorPicker(selection: self.$model.color)
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 4px").tag(ComponentRadius.custom(4))
}
Picker("Max Visible Avatars", selection: self.$model.maxVisibleAvatars) {
Text("3").tag(3)
Text("5").tag(5)
Text("7").tag(7)
}
SizePicker(selection: self.$model.size)
}
}
}
}

#Preview {
AvatarGroupPreview()
}
3 changes: 3 additions & 0 deletions Examples/DemosApp/DemosApp/Core/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ struct App: View {
NavigationLinkWithTitle("Avatar") {
AvatarPreview()
}
NavigationLinkWithTitle("Avatar Group") {
AvatarGroupPreview()
}
NavigationLinkWithTitle("Badge") {
BadgePreview()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import UIKit

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

Expand Down
45 changes: 0 additions & 45 deletions Sources/ComponentsKit/Components/Avatar/SUAvatar.swift

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftUI

struct AvatarContent: View {
// MARK: - Properties

var model: AvatarVM

@StateObject private var imageManager: AvatarImageManager
@Environment(\.colorScheme) private var colorScheme

// MARK: - Initialization

init(model: AvatarVM) {
self.model = model
self._imageManager = StateObject(
wrappedValue: AvatarImageManager(model: model)
)
}

// MARK: - Body

var body: some View {
GeometryReader { geometry in
Image(uiImage: self.imageManager.avatarImage)
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(
RoundedRectangle(cornerRadius: self.model.cornerRadius.value())
)
.onAppear {
self.imageManager.update(model: self.model, size: geometry.size)
}
.onChange(of: self.model) { newValue in
self.imageManager.update(model: newValue, size: geometry.size)
}
.onChange(of: self.colorScheme) { _ in
self.imageManager.update(model: self.model, size: geometry.size)
}
}
}
}
28 changes: 28 additions & 0 deletions Sources/ComponentsKit/Components/Avatar/SwiftUI/SUAvatar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import SwiftUI

/// A SwiftUI component that displays a profile picture, initials or fallback icon for a user.
public struct SUAvatar: View {
// MARK: - Properties

/// A model that defines the appearance properties.
public var model: AvatarVM

// MARK: - Initialization

/// Initializer.
/// - Parameters:
/// - model: A model that defines the appearance properties.
public init(model: AvatarVM) {
self.model = model
}

// MARK: - Body

public var body: some View {
AvatarContent(model: self.model)
.frame(
width: self.model.preferredSize.width,
height: self.model.preferredSize.height
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import UIKit

/// A model that defines the appearance properties for an avatar group component.
public struct AvatarGroupVM: ComponentVM {
/// The border color of avatars.
public var borderColor: UniversalColor = .background

/// The color of the placeholders.
public var color: ComponentColor?

/// The corner radius of the avatars.
///
/// Defaults to `.full`.
public var cornerRadius: ComponentRadius = .full

/// The array of avatars in the group.
public var items: [AvatarItemVM] = [] {
didSet {
self._identifiedItems = self.items.map({
return .init(id: UUID(), item: $0)
})
}
}

/// The maximum number of visible avatars.
///
/// Defaults to `5`.
public var maxVisibleAvatars: Int = 5

/// The predefined size of the component.
///
/// Defaults to `.medium`.
public var size: ComponentSize = .medium

/// The array of avatar items with an associated id value to properly display content in SwiftUI.
private var _identifiedItems: [IdentifiedAvatarItem] = []

/// Initializes a new instance of `AvatarGroupVM` with default values.
public init() {}
}

// MARK: - Helpers

fileprivate struct IdentifiedAvatarItem: Equatable {
var id: UUID
var item: AvatarItemVM
}

extension AvatarGroupVM {
var identifiedAvatarVMs: [(UUID, AvatarVM)] {
var avatars = self._identifiedItems.prefix(self.maxVisibleAvatars).map { data in
return (data.id, AvatarVM {
$0.color = self.color
$0.cornerRadius = self.cornerRadius
$0.imageSrc = data.item.imageSrc
$0.placeholder = data.item.placeholder
$0.size = self.size
})
}

if self.numberOfHiddenAvatars > 0 {
avatars.append((UUID(), AvatarVM {
$0.color = self.color
$0.cornerRadius = self.cornerRadius
$0.placeholder = .text("+\(self.numberOfHiddenAvatars)")
$0.size = self.size
}))
}

return avatars
}

var avatarSize: CGSize {
switch self.size {
case .small:
return .init(width: 36, height: 36)
case .medium:
return .init(width: 48, height: 48)
case .large:
return .init(width: 64, height: 64)
}
}

var padding: CGFloat {
switch self.size {
case .small:
return 3
case .medium:
return 4
case .large:
return 5
}
}

var numberOfHiddenAvatars: Int {
return self.items.count - self.maxVisibleAvatars
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import UIKit

/// A model that defines the appearance properties for an avatar in the group.
public struct AvatarItemVM: ComponentVM {
/// The source of the image to be displayed.
public var imageSrc: AvatarVM.ImageSource?

/// The placeholder that is displayed if the image is not provided or fails to load.
public var placeholder: AvatarVM.Placeholder = .icon("avatar_placeholder", Bundle.module)

/// Initializes a new instance of `AvatarItemVM` with default values.
public init() {}
}
37 changes: 37 additions & 0 deletions Sources/ComponentsKit/Components/AvatarGroup/SUAvatarGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import SwiftUI

/// A SwiftUI component that displays a group of avatars.
public struct SUAvatarGroup: View {
// MARK: - Properties

/// A model that defines the appearance properties.
public var model: AvatarGroupVM

// MARK: - Initialization

/// Initializer.
/// - Parameters:
/// - model: A model that defines the appearance properties.
public init(model: AvatarGroupVM) {
self.model = model
}

// MARK: - Body

public var body: some View {
HStack(spacing: -self.model.avatarSize.width / 3) {
ForEach(self.model.identifiedAvatarVMs, id: \.0) { _, avatarVM in
AvatarContent(model: avatarVM)
.padding(self.model.padding)
.background(self.model.borderColor.color)
.clipShape(
RoundedRectangle(cornerRadius: self.model.cornerRadius.value())
)
.frame(
width: self.model.avatarSize.width,
height: self.model.avatarSize.height
)
}
}
}
}
Loading