-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 🎸 [JIRA: HCPSDKFIORIUIKIT-2708] avatar stack support #788
Merged
billzhou0223
merged 1 commit into
SAP:main
from
xiaoyu0722:feat-avatar-stack-enhancement
Sep 9, 2024
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
Apps/Examples/Examples/FioriSwiftUICore/AvatarStack/AvatarStackExample.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarListView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty Count Violation: Prefer checking
isEmpty
over comparingcount
to zero. (empty_count)