Skip to content

Commit

Permalink
feat: 🎸 [JIRA: HCPSDKFIORIUIKIT-2708] avatar stack support (#788)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyu0722 authored Sep 9, 2024
1 parent a9fd5d1 commit 910198e
Show file tree
Hide file tree
Showing 28 changed files with 1,067 additions and 371 deletions.
12 changes: 12 additions & 0 deletions Apps/Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
B1DD86512B07534D00D7EDFD /* NavigationBarFioriStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DD86502B07534D00D7EDFD /* NavigationBarFioriStyle.swift */; };
B1DD86532B0758F000D7EDFD /* NavigationBarPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DD86522B0758F000D7EDFD /* NavigationBarPopover.swift */; };
B1DD86552B0759DD00D7EDFD /* NavigationBarCustomItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DD86542B0759DD00D7EDFD /* NavigationBarCustomItem.swift */; };
B1F384322C815A540090A858 /* AvatarStackExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F384312C815A540090A858 /* AvatarStackExample.swift */; };
B1F6FC302B22BDDA005190F9 /* ToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F6FC2F2B22BDDA005190F9 /* ToolbarView.swift */; };
B80DA9BA260BBF8600C0B2E9 /* SingleActionProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80DA9B9260BBF8600C0B2E9 /* SingleActionProfiles.swift */; };
B80DA9BC260BED9400C0B2E9 /* SingleActionCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80DA9BB260BED9400C0B2E9 /* SingleActionCollectionView.swift */; };
Expand Down Expand Up @@ -338,6 +339,7 @@
B1DD86502B07534D00D7EDFD /* NavigationBarFioriStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarFioriStyle.swift; sourceTree = "<group>"; };
B1DD86522B0758F000D7EDFD /* NavigationBarPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopover.swift; sourceTree = "<group>"; };
B1DD86542B0759DD00D7EDFD /* NavigationBarCustomItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarCustomItem.swift; sourceTree = "<group>"; };
B1F384312C815A540090A858 /* AvatarStackExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackExample.swift; sourceTree = "<group>"; };
B1F6FC2F2B22BDDA005190F9 /* ToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarView.swift; sourceTree = "<group>"; };
B80DA9B9260BBF8600C0B2E9 /* SingleActionProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleActionProfiles.swift; sourceTree = "<group>"; };
B80DA9BB260BED9400C0B2E9 /* SingleActionCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleActionCollectionView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -608,6 +610,7 @@
8A5579C824C1293C0098003A /* FioriSwiftUICore */ = {
isa = PBXGroup;
children = (
B1F384302C815A410090A858 /* AvatarStack */,
8732C2C32C35092D002110E9 /* Timeline */,
B19006582C201BAC000C8B10 /* ProfileHeader */,
1F1A1FF82C0BDA42007109D8 /* MenuSelection */,
Expand Down Expand Up @@ -806,6 +809,14 @@
path = Picker;
sourceTree = "<group>";
};
B1F384302C815A410090A858 /* AvatarStack */ = {
isa = PBXGroup;
children = (
B1F384312C815A540090A858 /* AvatarStackExample.swift */,
);
path = AvatarStack;
sourceTree = "<group>";
};
B80DA9C32612A54E00C0B2E9 /* Onboarding */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1165,6 +1176,7 @@
8A557A2224C12C9B0098003A /* CoreContentView.swift in Sources */,
8A5579D224C1293C0098003A /* Color+Extensions.swift in Sources */,
6D6E866F2C539CDE00EDB6F4 /* InPlaceLoadingFlexibleButtonExample.swift in Sources */,
B1F384322C815A540090A858 /* AvatarStackExample.swift in Sources */,
B1BCB6E12C2EB362008AC070 /* ProfileHeaderStaticExample.swift in Sources */,
C1C764882A818BEC00BCB0F7 /* SortFilterExample.swift in Sources */,
64905D072C6D13E20062AAD4 /* SwitchExample.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Combine
import FioriSwiftUICore
import FioriThemeManager
import Foundation
import SwiftUI

struct AvatarStackExample: View {
@StateObject var model = AvatarStackModel()

@ViewBuilder var avatarStack: some View {
AvatarStack {
ForEach(0 ..< self.model.avatarsCount, id: \.self) { _ in
Color.random
}
} avatarsTitle: {
if self.model.title.isEmpty {
EmptyView()
} else {
Text(self.model.title)
}
}
.avatarsLayout(self.model.avatarsLayout)
.isAvatarCircular(self.model.isCircular)
.avatarsTitlePosition(self.model.titlePosition)
.avatarsSpacing(self.model.spacing)
.avatarsMaxCount(self.model.maxCount)
.avatarsBorder(self.model.borderColor, width: self.model.borderWidth)
.avatarSize(self.avatarSize)
}

var avatarSize: CGSize? {
if let sideLength = model.sideLength {
CGSize(width: sideLength, height: sideLength)
} else {
nil
}
}

var body: some View {
List {
Section {
self.avatarStack
}

Picker("Avatar Count", selection: self.$model.avatarsCount) {
ForEach(0 ... 20, id: \.self) { number in
Text("\(number)").tag(number)
}
}
TextField("Enter Title", text: self.$model.title)
Toggle("isCircle", isOn: self.$model.isCircular)

Picker("Avatars Layout", selection: self.$model.avatarsLayout) {
Text("grouped").tag(AvatarStack.Layout.grouped)
Text("horizontal").tag(AvatarStack.Layout.horizontal)
}
Picker("Title Position", selection: self.$model.titlePosition) {
Text("leading").tag(AvatarStack.TextPosition.leading)
Text("trailing").tag(AvatarStack.TextPosition.trailing)
Text("top").tag(AvatarStack.TextPosition.top)
Text("bottom").tag(AvatarStack.TextPosition.bottom)
}

Picker("Spacing (only work for horizontal avatars)", selection: self.$model.spacing) {
ForEach([-4, -1, 0, 1, 4], id: \.self) { number in
Text("\(number)").tag(CGFloat(number))
}
}

Picker("Max Count", selection: self.$model.maxCount) {
Text("None").tag(nil as Int?)
ForEach([2, 4, 8], id: \.self) { number in
Text("\(number)").tag(number as Int?)
}
}

Picker("Side Length", selection: self.$model.sideLength) {
Text("Default").tag(nil as CGFloat?)
ForEach([10, 16, 20, 30, 40], id: \.self) { number in
Text("\(number)").tag(CGFloat(number) as CGFloat?)
}
}

Picker("Border Width", selection: self.$model.borderWidth) {
ForEach([0, 1, 2, 4], id: \.self) { number in
Text("\(number)").tag(CGFloat(number))
}
}

ColorPicker(selection: self.$model.borderColor, supportsOpacity: false) {
Text("Border Color")
}
}
}
}

class AvatarStackModel: ObservableObject {
@Published var avatarsCount: Int = 2
@Published var title: String = "This is a text for avatar stack."
@Published var isCircular: Bool = true
@Published var avatarsLayout: AvatarStack.Layout = .grouped
@Published var titlePosition: AvatarStack.TextPosition = .trailing
@Published var spacing: CGFloat = -1
@Published var maxCount: Int? = nil
@Published var sideLength: CGFloat? = nil
@Published var borderColor = Color.clear
@Published var borderWidth: CGFloat = 1
}

#Preview {
AvatarStackExample()
}
7 changes: 7 additions & 0 deletions Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ struct CoreContentView: View {
var body: some View {
List {
Section(header: Text("Views")) {
NavigationLink(
destination: AvatarStackExample(),
label: {
Text("AvatarStack")
}
)

NavigationLink(
destination: FioriButtonContentView(),
label: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
Tag("103-Repair")
})
.isAvatarCircular(false)
.avatarBorderWidth(2)
.avatarBorderColor(Color.yellow)
.avatarsBorder(Color.yellow, width: 2)
.avatarSize(CGSize(width: 40, height: 40))
.splitPercent(0.5)
.footnoteIconsMaxCount(-2)
Expand Down Expand Up @@ -136,8 +135,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
Color.blue
})
.isAvatarCircular(true)
.avatarBorderWidth(1)
.avatarBorderColor(Color.black)
.avatarsBorder(Color.black)
.splitPercent(0.5)
.footnoteIconsSpacing(2)
return AnyView(oi)
Expand Down Expand Up @@ -184,8 +182,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
Color.blue
})
.isAvatarCircular(true)
.avatarBorderWidth(3)
.avatarBorderColor(Color.blue)
.avatarsBorder(Color.blue, width: 3)
.avatarSize(CGSize(width: 30, height: 30))
.footnoteIconsMaxCount(4)
.footnoteIconsSize(CGSize(width: 20, height: 20))
Expand Down Expand Up @@ -334,8 +331,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
Tag("103-Repair")
})
.isAvatarCircular(false)
.avatarBorderWidth(2)
.avatarBorderColor(Color.yellow)
.avatarsBorder(Color.yellow, width: 2)
.avatarSize(CGSize(width: 40, height: 40))
.splitPercent(0.5)
.footnoteIconsMaxCount(-2)
Expand Down Expand Up @@ -372,8 +368,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
Color.blue
})
.isAvatarCircular(true)
.avatarBorderWidth(1)
.avatarBorderColor(Color.black)
.avatarsBorder(Color.black)
.splitPercent(0.5)
.footnoteIconsSpacing(2)
return AnyView(oi)
Expand Down Expand Up @@ -420,8 +415,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
Color.blue
})
.isAvatarCircular(true)
.avatarBorderWidth(3)
.avatarBorderColor(Color.blue)
.avatarsBorder(Color.blue, width: 3)
.avatarSize(CGSize(width: 30, height: 30))
.footnoteIconsMaxCount(4)
.footnoteIconsSize(CGSize(width: 20, height: 20))
Expand Down
7 changes: 0 additions & 7 deletions Sources/FioriSwiftUICore/Models/ModelDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ import SwiftUI
// sourcery: add_env_props = "numberOfLines"
public protocol IconStackModel: IconsComponent {}

// sourcery: generated_component_not_configurable
// sourcery: add_env_props = "avatarSize"
// sourcery: add_env_props = "isAvatarCircular"
// sourcery: add_env_props = "avatarBorderWidth"
// sourcery: add_env_props = "avatarBorderColor"
public protocol AvatarStackModel: AvatarsComponent {}

// sourcery: generated_component_not_configurable
// sourcery: add_env_props = "footnoteIconsSize"
// sourcery: add_env_props = "footnoteIconsSpacing"
Expand Down
127 changes: 127 additions & 0 deletions Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarListView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import SwiftUI

struct SingleAvatar: View, AvatarList {
var count: Int {
self.isEmpty ? 0 : 1
}

func view(at index: Int) -> some View {
self
}

@Environment(\.avatarBorderColor) var borderColor
@Environment(\.avatarBorderWidth) var borderWidth
@Environment(\.isAvatarCircular) var isCircular
@Environment(\.avatarSize) var avatarSize
@Environment(\.avatarsLayout) var layout

var size: CGSize {
if let size = avatarSize {
return size
} else {
switch self.layout {
case .grouped:
return CGSize(width: 45, height: 45)
case .horizontal:
return CGSize(width: 16, height: 16)
}
}
}

let avatar: any View

var body: some View {
if self.isCircular {
self.avatar.typeErased
.frame(width: self.size.width, height: self.size.height)
.clipShape(Capsule())
.overlay {
Capsule()
.inset(by: self.borderWidth / 2.0)
.stroke(self.borderColor, lineWidth: self.borderWidth)
}
} else {
self.avatar.typeErased
.frame(width: self.size.width, height: self.size.height)
.border(self.borderColor, width: self.borderWidth)
}
}
}

struct AvatarListView<T: AvatarList>: View {
@Environment(\.avatarsLayout) var layout
@Environment(\.avatarsMaxCount) var maxCount
@Environment(\.avatarsSpacing) var spacing
@Environment(\.avatarSize) var avatarSize
let avatars: T

var size: CGSize {
if let size = avatarSize {
return size
} else {
switch self.layout {
case .grouped:
return CGSize(width: 45, height: 45)
case .horizontal:
return CGSize(width: 16, height: 16)
}
}
}

var count: Int {
self.avatars.count
}

// This condition check if for handle recursive builder issue.
private func checkIfIsNestingAvatars() -> Bool {
if self.count == 1 {
let typeString = String(describing: avatars.view(at: 0).self)
return typeString.contains("AvatarsListStack")
} else {
return false
}
}

/// :nodoc:
var body: some View {
if self.count == 0 {
EmptyView()
} else if self.count == 1, self.checkIfIsNestingAvatars() {
self.avatars.view(at: 0)
} else {
self.buildAvatars()
}
}

@ViewBuilder func buildAvatars() -> some View {
switch self.layout {
case .grouped:
// Currently group avatars support 2 avatars default.
let count = min(avatars.count, self.maxCount ?? 2)
if count > 1 {
ZStack(alignment: .topLeading) {
ForEach(0 ..< count, id: \.self) { index in
let position = CGPoint(x: CGFloat(index + 1) * self.size.width / 2,
y: CGFloat(index + 1) * self.size.height / 2)
SingleAvatar(avatar: self.avatars.view(at: index))
.position(position)
}
}
.frame(width: self.size.width * (1 + CGFloat(count - 1) * 0.5),
height: self.size.height * (1 + CGFloat(count - 1) * 0.5))
} else if count == 1 {
SingleAvatar(avatar: self.avatars.view(at: 0))
} else {
EmptyView()
}
case .horizontal:
HorizontalIconsHStack(spacing: self.spacing) {
let validMaxCount = self.maxCount ?? 0
let itemsCount = validMaxCount <= 0 ? self.count : min(self.count, validMaxCount)
ForEach(0 ..< itemsCount, id: \.self) { index in
SingleAvatar(avatar: self.avatars.view(at: index))
}
}
}
}
}
Loading

0 comments on commit 910198e

Please sign in to comment.