Skip to content
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] avatars enhancement #773

Merged
merged 5 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -11,7 +11,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
}

func numberOfRowsInSection(_ section: Int) -> Int {
4
self.isNewObjectItem ? 8 : 4
}

func titleForHeaderInSection(_ section: Int) -> String {
Expand Down Expand Up @@ -168,7 +168,7 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
Image(systemName: "person")
.resizable()
Text("XY")
.frame(width: 40, height: 40)
.frame(width: 30, height: 30)
.background(Color.red)
.foregroundColor(Color.white)
}, footnoteIcons: {
Expand All @@ -191,6 +191,68 @@ struct ObjectItemAvatarsExample: ObjectItemListDataProtocol {
.footnoteIconsSize(CGSize(width: 20, height: 20))
.isFootnoteIconsCircular(false)
return AnyView(oi)
case (0, 4):
let oi = ObjectItem {
Text("Title: This is a case for for long text with icons")
} subtitle: {
Text("Subtitle: this is a subtitle")
} footnoteIcons: {
Color.random
Color.random
Color.random
Color.random
Color.random
Color.random
}.footnoteIconsText("This is a very very very very very very very very very very very very long text layout with footnote icons")
return AnyView(oi)
case (0, 5):
let oi = ObjectItem {
Text("This is a case for for short text with icons")
} subtitle: {
Text("Subtitle: this is a subtitle")
} footnoteIcons: {
Color.random
Color.random
Color.random
Color.random
Color.random
Color.random
}.footnoteIconsText("This is a short one.")
return AnyView(oi)
case (0, 6):
let oi = ObjectItem {
Text("This is a case for for long leading text with icons")
} subtitle: {
Text("Subtitle: this is a subtitle")
} footnoteIcons: {
Color.random
Color.random
Color.random
Color.random
Color.random
Color.random
}.footnoteIconsText("This is a very very very very very very very very very very very very long text layout with footnote icons")
.footnoteIconsTextPosition(.leading)
return AnyView(oi)
case (0, 7):
let oi = ObjectItem {
Text("This is a case for for short leading text with icons")
} subtitle: {
Text("Subtitle: this is a subtitle")
} footnoteIcons: {
Color.random
Color.random
Color.random
Color.random
Color.random
Color.random
}.footnoteIconsText {
Text("This is text with custom style.")
.font(.fiori(forTextStyle: .headline))
.foregroundStyle(Color.random)
}
.footnoteIconsTextPosition(.leading)
return AnyView(oi)
default:
return AnyView(_ObjectItem(title: "Lorem ipseum dolor"))
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/FioriSwiftUICore/Models/ModelDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public protocol AvatarStackModel: AvatarsComponent {}
// sourcery: add_env_props = "footnoteIconsSpacing"
// sourcery: add_env_props = "isFootnoteIconsCircular"
// sourcery: add_env_props = "footnoteIconsMaxCount"
// sourcery: add_env_props = "footnoteIconsTextPosition"
// sourcery: add_env_props = "footnoteIconsText"
public protocol FootnoteIconStackModel: FootnoteIconsComponent {}

// sourcery: add_env_props = "horizontalSizeClass"
Expand Down
40 changes: 24 additions & 16 deletions Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,37 @@ public protocol AvatarList: View, _ViewEmptyChecking {
public extension AvatarList {
/// :nodoc:
@ViewBuilder func buildAvatar(_ avatar: V) -> some View {
Group {
if isCircular {
avatar
.frame(width: size.width, height: size.height)
.clipShape(Capsule())
.overlay {
Capsule()
.inset(by: borderWidth / 2.0)
.stroke(borderColor, lineWidth: borderWidth)
}
} else {
avatar
.frame(width: size.width, height: size.height)
.border(borderColor, width: borderWidth)
}
if isCircular {
avatar
.frame(width: size.width, height: size.height)
.clipShape(Capsule())
.overlay {
Capsule()
.inset(by: borderWidth / 2.0)
.stroke(borderColor, lineWidth: borderWidth)
}
} else {
avatar
.frame(width: size.width, height: size.height)
.border(borderColor, width: borderWidth)
}
}

// This condition check if for handle recursive builder issue.
private func checkIsNestingAvatars() -> Bool {
let typeString = String(describing: V.self)
return typeString.contains("SingleAvatar<SingleAvatar") || typeString.contains("SingleAvatar<PairAvatar") || typeString.contains("PairAvatar<")
}

/// :nodoc:
var body: some View {
Group {
if count == 1 {
self.buildAvatar(view(at: 0))
if self.checkIsNestingAvatars() {
self.view(at: 0)
} else {
self.buildAvatar(view(at: 0))
}
} else if count >= 2 {
ZStack(alignment: .topLeading) {
self.buildAvatar(view(at: 0))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,14 @@ public protocol FootnoteIconList: View, _ViewEmptyChecking {
var size: CGSize { get }
var isCircular: Bool { get }
var spacing: CGFloat { get }
var text: (any View)? { get }
var textPosition: TextPosition { get }
}

public extension FootnoteIconList {
/// :nodoc:
var body: some View {
HStack(spacing: spacing) {
let itemsCount = maxCount <= 0 ? count : min(count, maxCount)
ForEach(0 ..< itemsCount, id: \.self) { index in
view(at: index)
.frame(width: size.width, height: size.height)
.ifApply(isCircular) {
$0.clipShape(Capsule())
}
.overlay {
Group {
if isCircular {
Capsule()
.inset(by: 0.33 / 2.0)
.stroke(Color.preferredColor(.separator), lineWidth: 0.33)
} else {
Rectangle()
.inset(by: 0.33 / 2.0)
.stroke(Color.preferredColor(.separator), lineWidth: 0.33)
}
}
}
}
}
.clipped()
FootnoteIconsListView(icons: self)
}
}

Expand All @@ -63,6 +42,8 @@ public struct SingleFootnoteIcon<Content: View>: FootnoteIconList {
@Environment(\.isFootnoteIconsCircular) var isFootnoteIconsCircular
@Environment(\.footnoteIconsSpacing) var footnoteIconsSpacing
@Environment(\.footnoteIconsSize) var footnoteIconsSize
@Environment(\.footnoteIconsText) var footnoteIconsText
@Environment(\.footnoteIconsTextPosition) var footnoteIconsTextPosition
public var maxCount: Int {
self.footnoteIconsMaxCount
}
Expand All @@ -78,6 +59,14 @@ public struct SingleFootnoteIcon<Content: View>: FootnoteIconList {
public var spacing: CGFloat {
self.footnoteIconsSpacing
}

public var text: (any View)? {
self.footnoteIconsText
}

public var textPosition: TextPosition {
self.footnoteIconsTextPosition
}
}

public struct ConditionalSingleFootnoteIcon<TrueContent: View, FalseContent: View>: FootnoteIconList {
Expand Down Expand Up @@ -110,6 +99,8 @@ public struct ConditionalSingleFootnoteIcon<TrueContent: View, FalseContent: Vie
@Environment(\.isFootnoteIconsCircular) var isFootnoteIconsCircular
@Environment(\.footnoteIconsSpacing) var footnoteIconsSpacing
@Environment(\.footnoteIconsSize) var footnoteIconsSize
@Environment(\.footnoteIconsText) var footnoteIconsText
@Environment(\.footnoteIconsTextPosition) var footnoteIconsTextPosition
public var maxCount: Int {
self.footnoteIconsMaxCount
}
Expand All @@ -125,6 +116,14 @@ public struct ConditionalSingleFootnoteIcon<TrueContent: View, FalseContent: Vie
public var spacing: CGFloat {
self.footnoteIconsSpacing
}

public var text: (any View)? {
self.footnoteIconsText
}

public var textPosition: TextPosition {
self.footnoteIconsTextPosition
}
}

public struct PairFootnoteIcon<First: View, Second: FootnoteIconList>: FootnoteIconList {
Expand All @@ -150,6 +149,9 @@ public struct PairFootnoteIcon<First: View, Second: FootnoteIconList>: FootnoteI
@Environment(\.isFootnoteIconsCircular) var isFootnoteIconsCircular
@Environment(\.footnoteIconsSpacing) var footnoteIconsSpacing
@Environment(\.footnoteIconsSize) var footnoteIconsSize
@Environment(\.footnoteIconsText) var footnoteIconsText
@Environment(\.footnoteIconsTextPosition) var footnoteIconsTextPosition

public var maxCount: Int {
self.footnoteIconsMaxCount
}
Expand All @@ -165,6 +167,14 @@ public struct PairFootnoteIcon<First: View, Second: FootnoteIconList>: FootnoteI
public var spacing: CGFloat {
self.footnoteIconsSpacing
}

public var text: (any View)? {
self.footnoteIconsText
}

public var textPosition: TextPosition {
self.footnoteIconsTextPosition
}
}

/// A custom parameter attribute that constructs views from closures.
Expand Down Expand Up @@ -283,6 +293,14 @@ extension FootnoteIconStack: FootnoteIconList {
public var spacing: CGFloat {
footnoteIconsSpacing
}

public var text: (any View)? {
self.footnoteIconsText
}

public var textPosition: TextPosition {
self.footnoteIconsTextPosition
}
}

struct FootnoteIconsMaxCount: EnvironmentKey {
Expand Down Expand Up @@ -333,7 +351,71 @@ public extension EnvironmentValues {
}
}

struct FootnoteIconsText: EnvironmentKey {
static let defaultValue: (any View)? = nil
}

public extension EnvironmentValues {
/// Text draw around footnote icons.
var footnoteIconsText: (any View)? {
get { self[FootnoteIconsText.self] }
set { self[FootnoteIconsText.self] = newValue }
}
}

struct FootnoteIconsTextPosition: EnvironmentKey {
static let defaultValue: TextPosition = .trailing
}

public extension EnvironmentValues {
/// Text position for footnote icons.
var footnoteIconsTextPosition: TextPosition {
get { self[FootnoteIconsTextPosition.self] }
set { self[FootnoteIconsTextPosition.self] = newValue }
}
}

public extension View {
/// Text for footnote icons.
/// ```swift
/// ObjectItem(title: "Object Item",
/// footnoteIcons: {
/// Image(systemName: "circle.fill")
/// Image(systemName: "person.fill")
/// })
/// .footnoteIconsText {
/// Text("Description for footnote icons.")
/// }
/// ```
/// - Parameter text: Text for footnote icons
/// - Returns: A view with a footnote icons text.
func footnoteIconsText(@ViewBuilder text: () -> any View) -> some View {
environment(\.footnoteIconsText, text())
}

/// Text for footnote icons by an `AttributedString`.
/// ```swift
/// ObjectItem(title: "Object Item",
/// footnoteIcons: {
/// Image(systemName: "circle.fill")
/// Image(systemName: "person.fill")
/// })
/// .footnoteIconsText("Description for footnote icons.")
/// ```
/// - Parameter s: An attributed string.
/// - Returns: A view with a footnote icons text.
func footnoteIconsText(_ s: AttributedString) -> some View {
environment(\.footnoteIconsText, Text(s).font(.fiori(forTextStyle: .subheadline)).foregroundStyle(Color.preferredColor(.secondaryLabel)))
.lineLimit(2)
}

/// Specific the position of the text that drawn for footnote icons. Default value is `.trailing`.
/// - Parameter position: Text position.
/// - Returns: A view that footnote icons text with specific position.
func footnoteIconsTextPosition(_ position: TextPosition) -> some View {
environment(\.footnoteIconsTextPosition, position)
}

/// Maximum number of the footnote icons. Default value is 0. When the count is less or equal to 0, means the number is unlimited.
/// ```swift
/// _ObjectItem(title: "Object Item",
Expand All @@ -359,7 +441,7 @@ public extension View {
/// .isFootnoteIconsCircular(false)
/// ```
/// - Parameter isCircular: Boolean denoting whether the footnote icons are circular.
/// - Returns: A view that footnote icons are cirlcular or not.
/// - Returns: A view that footnote icons are circular or not.
func isFootnoteIconsCircular(_ isCircular: Bool) -> some View {
environment(\.isFootnoteIconsCircular, isCircular)
}
Expand Down Expand Up @@ -394,3 +476,28 @@ public extension View {
environment(\.footnoteIconsSize, size)
}
}

/// Text position for icons.
public enum TextPosition {
/// Top position for text.
case top
/// Bottom position for text.
case bottom
/// Leading position for text.
case leading
/// Trailing position for text.
case trailing

var alignment: Alignment {
switch self {
case .top:
return .top
case .bottom:
return .bottom
case .leading:
return .leading
case .trailing:
return .trailing
}
}
}
Loading
Loading