diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AvatarGroupPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AvatarGroupPreview.swift index 787c563d..948ecbe2 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AvatarGroupPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AvatarGroupPreview.swift @@ -27,6 +27,10 @@ struct AvatarGroupPreview: View { var body: some View { VStack { + PreviewWrapper(title: "UIKit") { + UKAvatarGroup(model: self.model) + .preview + } PreviewWrapper(title: "SwiftUI") { SUAvatarGroup(model: self.model) } diff --git a/Sources/ComponentsKit/Components/AvatarGroup/Models/AvatarGroupVM.swift b/Sources/ComponentsKit/Components/AvatarGroup/Models/AvatarGroupVM.swift index 9d4dadbd..1d271844 100644 --- a/Sources/ComponentsKit/Components/AvatarGroup/Models/AvatarGroupVM.swift +++ b/Sources/ComponentsKit/Components/AvatarGroup/Models/AvatarGroupVM.swift @@ -70,7 +70,7 @@ extension AvatarGroupVM { return avatars } - var avatarSize: CGSize { + var itemSize: CGSize { switch self.size { case .small: return .init(width: 36, height: 36) @@ -92,7 +92,22 @@ extension AvatarGroupVM { } } + var spacing: CGFloat { + return -self.itemSize.width / 3 + } + var numberOfHiddenAvatars: Int { - return self.items.count - self.maxVisibleAvatars + return max(0, self.items.count - self.maxVisibleAvatars) + } +} + +// MARK: - UIKit Helpers + +extension AvatarGroupVM { + var avatarHeight: CGFloat { + return self.itemSize.height - self.padding * 2 + } + var avatarWidth: CGFloat { + return self.itemSize.width - self.padding * 2 } } diff --git a/Sources/ComponentsKit/Components/AvatarGroup/SUAvatarGroup.swift b/Sources/ComponentsKit/Components/AvatarGroup/SwiftUI/SUAvatarGroup.swift similarity index 84% rename from Sources/ComponentsKit/Components/AvatarGroup/SUAvatarGroup.swift rename to Sources/ComponentsKit/Components/AvatarGroup/SwiftUI/SUAvatarGroup.swift index db72d163..a089b174 100644 --- a/Sources/ComponentsKit/Components/AvatarGroup/SUAvatarGroup.swift +++ b/Sources/ComponentsKit/Components/AvatarGroup/SwiftUI/SUAvatarGroup.swift @@ -19,7 +19,7 @@ public struct SUAvatarGroup: View { // MARK: - Body public var body: some View { - HStack(spacing: -self.model.avatarSize.width / 3) { + HStack(spacing: self.model.spacing) { ForEach(self.model.identifiedAvatarVMs, id: \.0) { _, avatarVM in AvatarContent(model: avatarVM) .padding(self.model.padding) @@ -28,8 +28,8 @@ public struct SUAvatarGroup: View { RoundedRectangle(cornerRadius: self.model.cornerRadius.value()) ) .frame( - width: self.model.avatarSize.width, - height: self.model.avatarSize.height + width: self.model.itemSize.width, + height: self.model.itemSize.height ) } } diff --git a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift new file mode 100644 index 00000000..c37c9d9c --- /dev/null +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift @@ -0,0 +1,90 @@ +import AutoLayout +import UIKit + +final class AvatarContainer: UIView { + // MARK: - Properties + + let avatar: UKAvatar + var groupVM: AvatarGroupVM + var avatarConstraints = LayoutConstraints() + + // MARK: - Initialization + + init(avatarVM: AvatarVM, groupVM: AvatarGroupVM) { + self.avatar = UKAvatar(model: avatarVM) + self.groupVM = groupVM + + super.init(frame: .zero) + + self.setup() + self.style() + self.layout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + func setup() { + self.addSubview(self.avatar) + } + + // MARK: - Style + + func style() { + Self.Style.mainView(self, model: self.groupVM) + } + + // MARK: - Layout + + func layout() { + self.avatarConstraints = .merged { + self.avatar.allEdges(self.groupVM.padding) + self.avatar.height(self.groupVM.avatarHeight) + self.avatar.width(self.groupVM.avatarWidth) + } + + self.avatarConstraints.height?.priority = .defaultHigh + self.avatarConstraints.width?.priority = .defaultHigh + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.layer.cornerRadius = self.groupVM.cornerRadius.value(for: self.bounds.height) + } + + // MARK: - Update + + func update(avatarVM: AvatarVM, groupVM: AvatarGroupVM) { + let oldModel = self.groupVM + self.groupVM = groupVM + + if self.groupVM.size != oldModel.size { + self.avatarConstraints.top?.constant = groupVM.padding + self.avatarConstraints.leading?.constant = groupVM.padding + self.avatarConstraints.bottom?.constant = -groupVM.padding + self.avatarConstraints.trailing?.constant = -groupVM.padding + self.avatarConstraints.height?.constant = groupVM.avatarHeight + self.avatarConstraints.width?.constant = groupVM.avatarWidth + + self.setNeedsLayout() + } + + self.avatar.model = avatarVM + self.style() + } +} + +// MARK: - Style Helpers + +extension AvatarContainer { + fileprivate enum Style { + static func mainView(_ view: UIView, model: AvatarGroupVM) { + view.backgroundColor = model.borderColor.uiColor + view.layer.cornerRadius = model.cornerRadius.value(for: view.bounds.height) + } + } +} diff --git a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift new file mode 100644 index 00000000..52fed579 --- /dev/null +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift @@ -0,0 +1,121 @@ +import AutoLayout +import UIKit + +/// A UIKit component that displays a group of avatars. +open class UKAvatarGroup: UIView, UKComponent { + // MARK: - Properties + + /// A model that defines the appearance properties. + public var model: AvatarGroupVM { + didSet { + self.update(oldValue) + } + } + + // MARK: - Subviews + + /// The stack view that contains avatars. + public var stackView = UIStackView() + + // MARK: - Initializers + + /// Initializer. + /// - Parameters: + /// - model: A model that defines the appearance properties. + public init(model: AvatarGroupVM = .init()) { + self.model = model + + super.init(frame: .zero) + + self.setup() + self.style() + self.layout() + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + private func setup() { + self.addSubview(self.stackView) + self.model.identifiedAvatarVMs.forEach { _, avatarVM in + self.stackView.addArrangedSubview(AvatarContainer( + avatarVM: avatarVM, + groupVM: self.model + )) + } + } + + // MARK: - Style + + private func style() { + Self.Style.stackView(self.stackView, model: self.model) + } + + // MARK: - Layout + + private func layout() { + self.stackView.centerVertically() + self.stackView.centerHorizontally() + + self.stackView.topAnchor.constraint( + greaterThanOrEqualTo: self.topAnchor + ).isActive = true + self.stackView.bottomAnchor.constraint( + lessThanOrEqualTo: self.bottomAnchor + ).isActive = true + self.stackView.leadingAnchor.constraint( + greaterThanOrEqualTo: self.leadingAnchor + ).isActive = true + self.stackView.trailingAnchor.constraint( + lessThanOrEqualTo: self.trailingAnchor + ).isActive = true + } + + // MARK: - Update + + public func update(_ oldModel: AvatarGroupVM) { + guard self.model != oldModel else { return } + + let avatarVMs = self.model.identifiedAvatarVMs.map(\.1) + self.addOrRemoveArrangedSubviews(newNumber: avatarVMs.count) + + self.stackView.arrangedSubviews.enumerated().forEach { index, view in + (view as? AvatarContainer)?.update( + avatarVM: avatarVMs[index], + groupVM: self.model + ) + } + self.style() + } + + private func addOrRemoveArrangedSubviews(newNumber: Int) { + let diff = newNumber - self.stackView.arrangedSubviews.count + if diff > 0 { + for _ in 0 ..< diff { + self.stackView.addArrangedSubview(AvatarContainer(avatarVM: .init(), groupVM: self.model)) + } + } else if diff < 0 { + for _ in 0 ..< abs(diff) { + if let view = self.stackView.arrangedSubviews.first { + self.stackView.removeArrangedSubview(view) + view.removeFromSuperview() + } + } + } + } +} + +// MARK: - Style Helpers + +extension UKAvatarGroup { + fileprivate enum Style { + static func stackView(_ view: UIStackView, model: Model) { + view.axis = .horizontal + view.spacing = model.spacing + view.distribution = .equalCentering + } + } +}