diff --git a/Sources/Playbook/Components/Avatar/PBAvatar.swift b/Sources/Playbook/Components/Avatar/PBAvatar.swift index 76c0aca3a..ffcf1d4d1 100644 --- a/Sources/Playbook/Components/Avatar/PBAvatar.swift +++ b/Sources/Playbook/Components/Avatar/PBAvatar.swift @@ -84,7 +84,10 @@ public extension PBAvatar { return nil } - enum Size: CaseIterable { + enum Size: CaseIterable, Hashable { + public static var allCases: [PBAvatar.Size] = [] + + case xxSmall case xSmall case small @@ -95,7 +98,7 @@ public extension PBAvatar { case defaultStackedIndicator case smallStacked case smallStackedIndicator - + case custom(_ size: CGFloat) var diameter: CGFloat { switch self { case .xxSmall: return 20 @@ -108,6 +111,7 @@ public extension PBAvatar { case .defaultStackedIndicator: return 18 case .smallStacked: return 22 case .smallStackedIndicator: return 16 + case .custom(let size): return size } } diff --git a/Sources/Playbook/Components/User/Multiple Users/MultipleUsersCatalog.swift b/Sources/Playbook/Components/User/Multiple Users/MultipleUsersCatalog.swift index 059363dd0..dcb04d11b 100644 --- a/Sources/Playbook/Components/User/Multiple Users/MultipleUsersCatalog.swift +++ b/Sources/Playbook/Components/User/Multiple Users/MultipleUsersCatalog.swift @@ -15,7 +15,7 @@ public struct MultipleUsersCatalog: View { PBDoc(title: "xSmall") { xsmallView } - + PBDoc(title: "Small") { smallView } @@ -23,6 +23,10 @@ public struct MultipleUsersCatalog: View { PBDoc(title: "Small Reverse") { smallReverseView } + + PBDoc(title: "Small Bubble") { + smallUserBubbleView + } } } } @@ -39,4 +43,31 @@ extension MultipleUsersCatalog { PBMultipleUsers(users: Mocks.twoUsers, size: .small, reversed: true) } } + var smallUserBubbleView: some View { + HStack(spacing: Spacing.small) { + PBMultipleUsers( + users: Mocks.twoUsers, + variant: .bubble, + bubbleSize: .small, + bubbleCount: .two + ) + PBMultipleUsers( + users: Mocks.threeUsers, + variant: .bubble, + bubbleSize: .small, + bubbleCount: .three + ) + PBMultipleUsers( + users: Mocks.multipleUsers, + variant: .bubble, + bubbleSize: .small, + bubbleCount: .four + ) + } + } +} + +#Preview { + registerFonts() + return MultipleUsersCatalog() } diff --git a/Sources/Playbook/Components/User/Multiple Users/PBMultipleUsers.swift b/Sources/Playbook/Components/User/Multiple Users/PBMultipleUsers.swift index 85c49e695..cce757ac2 100644 --- a/Sources/Playbook/Components/User/Multiple Users/PBMultipleUsers.swift +++ b/Sources/Playbook/Components/User/Multiple Users/PBMultipleUsers.swift @@ -11,73 +11,92 @@ import SwiftUI public struct PBMultipleUsers: View { var users: [PBUser] - var size: AvatarSize + var size: PBAvatar.Size + var variant: Variant var reversed: Bool + var bubbleSize: BubbleSize + var bubbleCount: BubbleCount var maxDisplayedUsers: Int + @State private var avSize: CGFloat = 20 public init( users: [PBUser] = [], - size: AvatarSize = .small, + size: PBAvatar.Size = .small, + variant: Variant = .linear, reversed: Bool = false, + bubbleSize: BubbleSize = .small, + bubbleCount: BubbleCount = .two, maxDisplayedUsers: Int = 4 ) { self.users = users self.size = size + self.variant = variant self.reversed = reversed + self.bubbleSize = bubbleSize + self.bubbleCount = bubbleCount self.maxDisplayedUsers = maxDisplayedUsers } + public var body: some View { + variantView + } +} + +public extension PBMultipleUsers { + enum Variant { + case linear, bubble + } + @ViewBuilder + var variantView: some View { + switch variant { + case .linear: multipleUsersView + case .bubble: multipleUsersBubbleView + } + } var filteredUsers: ([PBUser], Int?) { var displayedUsers = users var additionalUsers = 0 - if users.count > maxDisplayedUsers { displayedUsers = Array(users.prefix(maxDisplayedUsers - 1)) additionalUsers = users.count - maxDisplayedUsers + 1 } - return (displayedUsers, additionalUsers) } - + func xOffset(index: Int) -> CGFloat { - let offset = size.avatarSize.diameter / 1.5 * CGFloat(index) - + let offset = size.diameter / 1.5 * CGFloat(index) return reversed ? -offset : offset } var leadingPadding: CGFloat { - let padding = size.avatarSize.diameter / 1.5 * CGFloat(filteredUsers.0.count - (filteredUsers.1 == 0 ? 1 : 0)) - + let padding = size.diameter / 1.5 * CGFloat(filteredUsers.0.count - (filteredUsers.1 == 0 ? 1 : 0)) return reversed ? padding : 0 } - + var totalWidth: CGFloat { - var width = size.avatarSize.diameter - + var width = size.diameter if filteredUsers.0.count > 1 { - width = size.avatarSize.diameter / 1.5 * CGFloat(filteredUsers.0.count - 1) + size.avatarSize.diameter + width = size.diameter / 1.5 * CGFloat(filteredUsers.0.count - 1) + size.diameter } - if users.count > maxDisplayedUsers { - width = size.avatarSize.diameter / 1.5 * CGFloat(maxDisplayedUsers - 1) + size.avatarSize.diameter + width = size.diameter / 1.5 * CGFloat(maxDisplayedUsers - 1) + size.diameter } - return width } - - public var body: some View { + + var multipleUsersView: some View { ZStack { ForEach(filteredUsers.0.indices, id: \.self) { index in PBAvatar( image: filteredUsers.0[index].image, name: filteredUsers.0[index].name, - size: size.avatarSize, + size: size, wrapped: true ) .offset(x: xOffset(index: index), y: 0) } - - PBMultipleUsersIndicator(usersCount: filteredUsers.1, size: size.avatarSize) + + PBMultipleUsersIndicator(usersCount: filteredUsers.1, size: size) .offset(x: xOffset(index: filteredUsers.0.endIndex), y: 0) } .padding(.leading, leadingPadding) @@ -86,22 +105,120 @@ public struct PBMultipleUsers: View { } public extension PBMultipleUsers { - enum AvatarSize { - case xSmall - case small - - var avatarSize: PBAvatar.Size { - switch self { - case .xSmall: return .xSmall - case .small: return .small - } + enum BubbleSize { + case small, medium, large, xLarge + } + enum BubbleCount { + case two, three, four + } + + var multipleUsersBubbleView: some View { + CircularLayout { + userBubbleView + } + .padding(5) + .background(Color.background(.light)) + .clipShape(/*@START_MENU_TOKEN@*/Circle()/*@END_MENU_TOKEN@*/) + } + var userBubbleView: some View { + ForEach(filteredUsers.0.indices, id: \.self) { index in + let avatarSize = avatarSize(for: index, total: filteredUsers.0.count) + PBAvatar( + image: filteredUsers.0[index].image, + name: filteredUsers.0[index].name, + size: .custom(avatarSize), + wrapped: true + ) + .padding(index <= 1 ? -1 : -4) + .offset(x: avatarXPosition(for: index, total: filteredUsers.0.count), y: avatarYPosition(for: index, total: filteredUsers.0.count)) + } + } + + func avatarSize(for index: Int, total: Int) -> CGFloat { + let sizes: [BubbleSize: [Int: [CGFloat]]] = [ + .small: [ + 1: [20], + 2: [20, 12], + 3: [16, 12, 10], + 4: [16, 12, 10, 8] + ], + .medium: [ + 1: [32], + 2: [32, 16], + 3: [24, 20, 16], + 4: [28, 20, 16, 12] + ], + .large: [ + 1: [44], + 2: [44, 20], + 3: [32, 24, 20], + 4: [36, 28, 24, 16] + ], + .xLarge: [ + 1: [56], + 2: [56, 24], + 3: [44, 32, 24], + 4: [44, 32, 24, 16] + ] + ] + return sizes[bubbleSize]?[total]?[index] ?? 0 + } + + func avatarXPosition(for index: Int, total: Int) -> CGFloat { + switch (total, index) { + case (2, 0): + return -6 + case (2, 1): + return 8 + case (3, 0): + return -6 + case (3, 1): + return 3 + case (3, 2): + return 8 + case (4, 0): + return -6 + case (4, 1): + return 1 + case (4, 2): + return -4 + case (4, 3): + return 24 + default: + return 0 + } + } + func avatarYPosition(for index: Int, total: Int) -> CGFloat { + switch (total, index) { + case (2, 0): + return 0 + case (2, 1): + return -2 + case (3, 0): + return 0 + case (3, 1): + return -4 + case (3, 2): + return 4 + case (4, 0): + return 0 + case (4, 1): + return 4 + case (4, 2): + return -4 + case (4, 3): + return -10 + default: + return 0 } } } -public struct PBMultipleUsers_Previews: PreviewProvider { - public static var previews: some View { +#Preview { registerFonts() - return MultipleUsersCatalog() - } + return VStack { + PBMultipleUsers(users: Mocks.multipleUsers, variant: .linear) + PBMultipleUsers(users: Mocks.multipleUsers, variant: .bubble) + } + .padding() } diff --git a/Sources/Playbook/Resources/Helper Files/Mocks.swift b/Sources/Playbook/Resources/Helper Files/Mocks.swift index 7457ed5d7..c88391857 100644 --- a/Sources/Playbook/Resources/Helper Files/Mocks.swift +++ b/Sources/Playbook/Resources/Helper Files/Mocks.swift @@ -17,6 +17,7 @@ enum Mocks { static let luccile = PBUser(name: "Luccile Black", image: Image("Lu", bundle: .module), size: .small, title: "Senior User Experience Engineer") static let oneUser = [andrew] static let twoUsers = [andrew, ana] + static let threeUsers = [andrew, ana, patric] static let multipleUsers = [andrew, ana, patric, luccile] static let multipleUsersDictionary: [(String, PBUser)] = [(andrew.name, andrew), (ana.name, ana), (patric.name, patric), (luccile.name, luccile)] static let avatarXSmall = PBAvatar(image: Image("andrew", bundle: .module), size: .xSmall)