@@ -16,6 +16,9 @@ import SwiftUI
1616 /// items than just the remainder of the avatars that could not be displayed due to the maxDisplayedAvatars property.
1717 var overflowCount : Int { get set }
1818
19+ /// Show a top-trailing aligned unread dot when set to true.
20+ var isUnread : Bool { get set }
21+
1922 /// Style of the AvatarGroup.
2023 var style : MSFAvatarGroupStyle { get set }
2124
@@ -190,13 +193,11 @@ public struct AvatarGroup: View, TokenizedControlView {
190193 style: FillStyle ( eoFill: true ) )
191194 } )
192195 }
193- . padding ( . trailing, isStackStyle ? stackPadding : interspace)
196+ . padding ( . trailing, ( isLastDisplayed && !hasOverflow ) ? 0 : isStackStyle ? stackPadding : interspace)
194197 }
195198
196199 @ViewBuilder
197- var avatarGroupContent : some View {
198- let animation = Animation . linear ( duration: animationDuration)
199-
200+ var avatarGroup : some View {
200201 HStack ( spacing: 0 ) {
201202 ForEach ( enumeratedAvatars. prefix ( avatarsToDisplay) , id: \. 1 ) { index, avatar in
202203 avatarView ( at: index, for: avatar)
@@ -210,19 +211,52 @@ public struct AvatarGroup: View, TokenizedControlView {
210211 . transition ( AnyTransition . opacity)
211212 }
212213 }
213- . animation ( animation, value: state. avatars)
214- . animation ( animation, value: [ state. maxDisplayedAvatars, state. overflowCount] )
215- . animation ( animation, value: state. style)
216- . animation ( animation, value: state. size)
217- . frame ( maxWidth: . infinity,
218- minHeight: groupHeight,
219- maxHeight: . infinity,
220- alignment: . leading)
221- . accessibilityElement ( children: . combine)
222- . accessibilityLabel ( groupLabel)
223214 }
224215
225- return avatarGroupContent
216+ @ViewBuilder
217+ var unreadGroup : some View {
218+ if state. isUnread {
219+ let strokeWidth = AvatarGroupTokenSet . unreadDotStrokeWidth
220+ let dotSize = AvatarGroupTokenSet . unreadDotSize
221+ avatarGroup
222+ . overlay ( alignment: . topTrailing) {
223+ Circle ( )
224+ . foregroundColor ( Color ( tokenSet [ . unreadDotColor] . uiColor) )
225+ . frame ( width: dotSize, height: dotSize)
226+ // Add half the strokeWidth as padding to get the stroke drawn around the outside of the
227+ // dot instead of having the stroke centered on the edge of the dot, but it needs to be
228+ // inset slightly to not have a gap.
229+ . padding ( strokeWidth / 2 - strokeInset)
230+ . overlay {
231+ Circle ( )
232+ . stroke ( Color ( tokenSet [ . backgroundColor] . uiColor) ,
233+ lineWidth: strokeWidth)
234+ }
235+ . offset ( x: AvatarGroupTokenSet . unreadDotHorizontalOffset,
236+ y: AvatarGroupTokenSet . unreadDotVerticalOffset)
237+ }
238+ } else {
239+ avatarGroup
240+ }
241+ }
242+
243+ @ViewBuilder
244+ var animatedGroup : some View {
245+ let animation = Animation . linear ( duration: animationDuration)
246+ unreadGroup
247+ . animation ( animation, value: state. avatars)
248+ . animation ( animation, value: [ state. maxDisplayedAvatars, state. overflowCount] )
249+ . animation ( animation, value: state. style)
250+ . animation ( animation, value: state. size)
251+ . frame ( maxWidth: . infinity,
252+ minHeight: groupHeight,
253+ maxHeight: . infinity,
254+ alignment: . leading)
255+ . accessibilityElement ( children: . combine)
256+ . accessibilityLabel ( groupLabel)
257+ }
258+
259+ return animatedGroup
226260 }
227261
228262 var avatarsToDisplay : Int {
@@ -253,6 +287,11 @@ public struct AvatarGroup: View, TokenizedControlView {
253287 str += String ( format: " Accessibility.AvatarGroup.AvatarList " . localized, displayedAvatarAccessibilityLabels [ i] )
254288 }
255289 str += String ( format: " Accessibility.AvatarGroup.AvatarListLast " . localized, displayedAvatarAccessibilityLabels. last ?? " " )
290+
291+ if state. isUnread {
292+ str = String ( format: " Accessibility.TabBarItemView.UnreadFormat " . localized, str)
293+ }
294+
256295 return str
257296 }
258297
@@ -261,6 +300,7 @@ public struct AvatarGroup: View, TokenizedControlView {
261300 @ObservedObject var state : MSFAvatarGroupStateImpl
262301
263302 private let animationDuration : CGFloat = 0.1
303+ private let strokeInset : CGFloat = 0.1
264304
265305 private func createOverflow( count: Int ) -> Avatar {
266306 let state = MSFAvatarStateImpl ( style: . overflow, size: state. size)
@@ -301,6 +341,7 @@ class MSFAvatarGroupStateImpl: ControlState, MSFAvatarGroupState {
301341 @Published var avatars : [ MSFAvatarGroupAvatarStateImpl ] = [ ]
302342 @Published var maxDisplayedAvatars : Int = Int . max
303343 @Published var overflowCount : Int = 0
344+ @Published var isUnread : Bool = false
304345
305346 @Published var style : MSFAvatarGroupStyle
306347 @Published var size : MSFAvatarSize
0 commit comments