From c30888f42fa8037502c31ec5abaaec8a33cb3235 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 17 Jan 2025 16:29:01 +0100 Subject: [PATCH 1/6] implement UKAvatarGroup --- .../PreviewPages/AvatarGroupPreview.swift | 4 + .../Avatar/SwiftUI/AvatarContent.swift | 3 + .../AvatarGroup/Models/AvatarGroupVM.swift | 19 ++- .../{ => SwiftUI}/SUAvatarGroup.swift | 6 +- .../AvatarGroup/UIKit/AvatarContainer.swift | 89 ++++++++++++ .../AvatarGroup/UIKit/UKAvatarGroup.swift | 128 ++++++++++++++++++ 6 files changed, 244 insertions(+), 5 deletions(-) rename Sources/ComponentsKit/Components/AvatarGroup/{ => SwiftUI}/SUAvatarGroup.swift (84%) create mode 100644 Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift create mode 100644 Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift 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/Avatar/SwiftUI/AvatarContent.swift b/Sources/ComponentsKit/Components/Avatar/SwiftUI/AvatarContent.swift index 0f462a0d..f4fed34f 100644 --- a/Sources/ComponentsKit/Components/Avatar/SwiftUI/AvatarContent.swift +++ b/Sources/ComponentsKit/Components/Avatar/SwiftUI/AvatarContent.swift @@ -27,6 +27,9 @@ struct AvatarContent: View { .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) } 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..aec05701 --- /dev/null +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift @@ -0,0 +1,89 @@ +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 + } + + 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..6502df89 --- /dev/null +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift @@ -0,0 +1,128 @@ +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.vertically() + self.stackView.centerHorizontally() + 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() + + if self.model.size != oldModel.size { + self.setNeedsLayout() + self.invalidateIntrinsicContentSize() + } + } + + 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: - Layout + + open override func layoutSubviews() { + super.layoutSubviews() + + self.stackView.arrangedSubviews.forEach { view in + view.layer.cornerRadius = self.model.cornerRadius.value(for: view.bounds.height) + } + } +} + +// MARK: - Style Helpers + +extension UKAvatarGroup { + fileprivate enum Style { + static func stackView(_ view: UIStackView, model: Model) { + view.axis = .horizontal + view.spacing = model.spacing + } + } +} From 12abeb57eb23819942a90caa8fa7afbfe4ca5734 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 17 Jan 2025 16:32:30 +0100 Subject: [PATCH 2/6] set priority of avatar width in the container to `defaultHigh` --- .../Components/AvatarGroup/UIKit/AvatarContainer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift index aec05701..c37c9d9c 100644 --- a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/AvatarContainer.swift @@ -47,6 +47,7 @@ final class AvatarContainer: UIView { } self.avatarConstraints.height?.priority = .defaultHigh + self.avatarConstraints.width?.priority = .defaultHigh } override func layoutSubviews() { From 9777cdc641534eeeb1ba47d6010e9968a48b0e02 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 17 Jan 2025 16:47:29 +0100 Subject: [PATCH 3/6] remove unused code --- .../Components/AvatarGroup/UIKit/UKAvatarGroup.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift index 6502df89..8b075860 100644 --- a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift @@ -82,11 +82,6 @@ open class UKAvatarGroup: UIView, UKComponent { ) } self.style() - - if self.model.size != oldModel.size { - self.setNeedsLayout() - self.invalidateIntrinsicContentSize() - } } private func addOrRemoveArrangedSubviews(newNumber: Int) { From 9eb04ed5404e800c3b0e9479ffc2f28dc54b3710 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 17 Jan 2025 16:56:17 +0100 Subject: [PATCH 4/6] improve layout in UKAlertGroup --- .../Components/AvatarGroup/UIKit/UKAvatarGroup.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift index 8b075860..23a83b88 100644 --- a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift @@ -57,8 +57,15 @@ open class UKAvatarGroup: UIView, UKComponent { // MARK: - Layout private func layout() { - self.stackView.vertically() + 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 From 93d0d6a91c7ab463859762c049b882daa152e114 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 17 Jan 2025 16:58:39 +0100 Subject: [PATCH 5/6] remove unnecessary code --- .../Components/AvatarGroup/UIKit/UKAvatarGroup.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift index 23a83b88..0762bc7f 100644 --- a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift @@ -106,16 +106,6 @@ open class UKAvatarGroup: UIView, UKComponent { } } } - - // MARK: - Layout - - open override func layoutSubviews() { - super.layoutSubviews() - - self.stackView.arrangedSubviews.forEach { view in - view.layer.cornerRadius = self.model.cornerRadius.value(for: view.bounds.height) - } - } } // MARK: - Style Helpers From e80ac2298808ca97c18e27b1f6dc7d75e0cee180 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 21 Jan 2025 12:48:52 +0100 Subject: [PATCH 6/6] fix: set avatars distribution --- .../Components/AvatarGroup/UIKit/UKAvatarGroup.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift index 0762bc7f..52fed579 100644 --- a/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift +++ b/Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift @@ -115,6 +115,7 @@ extension UKAvatarGroup { static func stackView(_ view: UIStackView, model: Model) { view.axis = .horizontal view.spacing = model.spacing + view.distribution = .equalCentering } } }