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
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
121 changes: 121 additions & 0 deletions Sources/ComponentsKit/Components/AvatarGroup/UIKit/UKAvatarGroup.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading