From 517c44e28f616097ffd608f8710d9680f2960c8d Mon Sep 17 00:00:00 2001 From: xiaoyu0722 Date: Tue, 3 Sep 2024 16:55:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20[JIRA:=20HCPSDKFIORIUIKI?= =?UTF-8?q?T-2708]=20avatar=20stack=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Examples.xcodeproj/project.pbxproj | 12 + .../AvatarStack/AvatarStackExample.swift | 112 ++++++ .../FioriSwiftUICore/CoreContentView.swift | 7 + .../Models/ModelDefinitions.swift | 7 - .../Views/CustomBuilder/AvatarListView.swift | 127 ++++++ .../AvatarStackEnvironments.swift | 176 +++++++++ .../Views/CustomBuilder/AvatarsBuilder.swift | 367 +++--------------- .../CustomBuilder/FootnoteIconsBuilder.swift | 7 +- .../CustomBuilder/FootnoteIconsListView.swift | 6 +- .../BaseComponentProtocols.swift | 8 +- .../CompositeComponentProtocols.swift | 3 + .../_FioriStyles/AvatarStackStyle.fiori.swift | 82 ++++ .../AvatarsTitleStyle.fiori.swift | 20 + .../_FioriStyles/ObjectItemStyle.fiori.swift | 8 +- .../AvatarStack/AvatarStack.generated.swift | 70 ++++ .../AvatarStackStyle.generated.swift | 38 ++ .../Avatars/Avatars.generated.swift | 2 +- .../AvatarsTitle/AvatarsTitle.generated.swift | 63 +++ .../AvatarsTitleStyle.generated.swift | 28 ++ .../ObjectItem/ObjectItem.generated.swift | 2 +- ...entStyleProtocol+Extension.generated.swift | 70 ++++ .../EnvironmentVariables.generated.swift | 42 ++ .../ModifiedStyle.generated.swift | 56 +++ .../ResolvedStyle.generated.swift | 32 ++ .../View+Extension_.generated.swift | 34 ++ ...iewEmptyChecking+Extension.generated.swift | 13 + .../API/AvatarStack+API.generated.swift | 21 - 27 files changed, 1054 insertions(+), 359 deletions(-) create mode 100644 Apps/Examples/Examples/FioriSwiftUICore/AvatarStack/AvatarStackExample.swift create mode 100644 Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarListView.swift create mode 100644 Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarStackEnvironments.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/AvatarStackStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_FioriStyles/AvatarsTitleStyle.fiori.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarStack/AvatarStack.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarStack/AvatarStackStyle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarsTitle/AvatarsTitle.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarsTitle/AvatarsTitleStyle.generated.swift delete mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/AvatarStack+API.generated.swift diff --git a/Apps/Examples/Examples.xcodeproj/project.pbxproj b/Apps/Examples/Examples.xcodeproj/project.pbxproj index 8f0861c3f..2502be8b9 100644 --- a/Apps/Examples/Examples.xcodeproj/project.pbxproj +++ b/Apps/Examples/Examples.xcodeproj/project.pbxproj @@ -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 */; }; @@ -338,6 +339,7 @@ B1DD86502B07534D00D7EDFD /* NavigationBarFioriStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarFioriStyle.swift; sourceTree = ""; }; B1DD86522B0758F000D7EDFD /* NavigationBarPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarPopover.swift; sourceTree = ""; }; B1DD86542B0759DD00D7EDFD /* NavigationBarCustomItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarCustomItem.swift; sourceTree = ""; }; + B1F384312C815A540090A858 /* AvatarStackExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackExample.swift; sourceTree = ""; }; B1F6FC2F2B22BDDA005190F9 /* ToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarView.swift; sourceTree = ""; }; B80DA9B9260BBF8600C0B2E9 /* SingleActionProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleActionProfiles.swift; sourceTree = ""; }; B80DA9BB260BED9400C0B2E9 /* SingleActionCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleActionCollectionView.swift; sourceTree = ""; }; @@ -608,6 +610,7 @@ 8A5579C824C1293C0098003A /* FioriSwiftUICore */ = { isa = PBXGroup; children = ( + B1F384302C815A410090A858 /* AvatarStack */, 8732C2C32C35092D002110E9 /* Timeline */, B19006582C201BAC000C8B10 /* ProfileHeader */, 1F1A1FF82C0BDA42007109D8 /* MenuSelection */, @@ -806,6 +809,14 @@ path = Picker; sourceTree = ""; }; + B1F384302C815A410090A858 /* AvatarStack */ = { + isa = PBXGroup; + children = ( + B1F384312C815A540090A858 /* AvatarStackExample.swift */, + ); + path = AvatarStack; + sourceTree = ""; + }; B80DA9C32612A54E00C0B2E9 /* Onboarding */ = { isa = PBXGroup; children = ( @@ -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 */, diff --git a/Apps/Examples/Examples/FioriSwiftUICore/AvatarStack/AvatarStackExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/AvatarStack/AvatarStackExample.swift new file mode 100644 index 000000000..c42b152b9 --- /dev/null +++ b/Apps/Examples/Examples/FioriSwiftUICore/AvatarStack/AvatarStackExample.swift @@ -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) + .avatarBorderColor(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() +} diff --git a/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift b/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift index 1f783e55a..8158ba9be 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift @@ -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: { diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index e4584351a..d123dba43 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -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" diff --git a/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarListView.swift b/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarListView.swift new file mode 100644 index 000000000..661c837d0 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarListView.swift @@ -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: 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)) + } + } + } + } +} diff --git a/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarStackEnvironments.swift b/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarStackEnvironments.swift new file mode 100644 index 000000000..5a6f96601 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarStackEnvironments.swift @@ -0,0 +1,176 @@ +import SwiftUI + +struct AvatarBorderColor: EnvironmentKey { + public static let defaultValue = Color.clear +} + +public extension EnvironmentValues { + /// The avatars border color. Default value is `clear`. + var avatarBorderColor: Color { + get { self[AvatarBorderColor.self] } + set { self[AvatarBorderColor.self] = newValue } + } +} + +struct AvatarBorderWidth: EnvironmentKey { + public static let defaultValue: CGFloat = 0 +} + +public extension EnvironmentValues { + /// Dimensions of the avatars border width. Default value is 0. + var avatarBorderWidth: CGFloat { + get { self[AvatarBorderWidth.self] } + set { self[AvatarBorderWidth.self] = newValue } + } +} + +struct IsAvatarCircular: EnvironmentKey { + public static let defaultValue: Bool = true +} + +public extension EnvironmentValues { + /// Specifies whether the `avatars` are drawn as circular. Default value is `true`. + var isAvatarCircular: Bool { + get { self[IsAvatarCircular.self] } + set { self[IsAvatarCircular.self] = newValue } + } +} + +struct AvatarSize: EnvironmentKey { + public static let defaultValue: CGSize? = nil +} + +public extension EnvironmentValues { + /// Dimensions of avatars size. Default value is `45x45` for `group`, and `16x16` for horizontal avatars. + var avatarSize: CGSize? { + get { self[AvatarSize.self] } + set { self[AvatarSize.self] = newValue } + } +} + +struct AvatarsTitlePosition: EnvironmentKey { + public static let defaultValue: AvatarStack.TextPosition = .trailing +} + +public extension EnvironmentValues { + /// Title position of avatar stack. Default value is `trailing`. + var avatarsTitlePosition: AvatarStack.TextPosition { + get { self[AvatarsTitlePosition.self] } + set { self[AvatarsTitlePosition.self] = newValue } + } +} + +struct AvatarsLayout: EnvironmentKey { + public static let defaultValue: AvatarStack.Layout = .grouped +} + +public extension EnvironmentValues { + /// Layout for avatars in the stack. Default value is `.grouped`. + var avatarsLayout: AvatarStack.Layout { + get { self[AvatarsLayout.self] } + set { self[AvatarsLayout.self] = newValue } + } +} + +struct AvatarsMaxCount: EnvironmentKey { + public static let defaultValue: Int? = nil +} + +public extension EnvironmentValues { + /// Max count for avatars in the stack. Default value is nil. + var avatarsMaxCount: Int? { + get { self[AvatarsMaxCount.self] } + set { self[AvatarsMaxCount.self] = newValue } + } +} + +struct AvatarsSpacing: EnvironmentKey { + public static let defaultValue: CGFloat = -1 +} + +public extension EnvironmentValues { + /// Spacing for avatars in horizontal stack. Default value is -1. + var avatarsSpacing: CGFloat { + get { self[AvatarsSpacing.self] } + set { self[AvatarsSpacing.self] = newValue } + } +} + +public extension View { + /// Set the avatars border color. Default value is `clear`. + /// ```swift + /// _ObjectItem(title: "Object Item", + /// avatars: { + /// Image(systemName: "circle.fill") + /// }) + /// .avatarBorderColor(Color.red, width: 2) + /// ``` + /// - Parameter color: Border color. + /// - Parameter width: Border width. + /// - Returns: A view with specific border color of avatars. + func avatarBorderColor(_ color: Color, width: CGFloat = 1) -> some View { + environment(\.avatarBorderColor, color) + .environment(\.avatarBorderWidth, width) + } + + /// Dimensions of avatars. Default value is `45x45`. + /// ```swift + /// _ObjectItem(title: "Object Item", + /// avatars: { + /// Image(systemName: "circle.fill") + /// }) + /// .avatarSize(CGSize(30, 30)) + /// ``` + /// - Parameter size: The size of the avatars. + /// - Returns: A view that limits the size of avatars. + func avatarSize(_ size: CGSize?) -> some View { + environment(\.avatarSize, size) + } + + /// Specifies whether the `avatars` are drawn as circular. Default value is `true`. + /// ```swift + /// _ObjectItem(title: "Object Item", + /// avatars: { + /// Image(systemName: "circle.fill") + /// }) + /// .isAvatarCircular(true) + /// ``` + /// - Parameter isCircular: Boolean denoting whether the avatars are circular. + /// - Returns: A view that avatars are circular or not. + func isAvatarCircular(_ isCircular: Bool) -> some View { + environment(\.isAvatarCircular, isCircular) + } + + @available(*, deprecated, message: "Use `func avatarBorderColor(_ color: Color, width: CGFloat = 1) -> some View` instead. And this will be removed in the future release.") + func avatarBorderWidth(_ borderWidth: CGFloat) -> some View { + environment(\.avatarBorderWidth, borderWidth) + } + + /// Text position for avatar stack. Default value is `.trailing`. + /// - Parameter position: Position for text in avatar stack. + /// - Returns: A view that avatar stack text with specific position. + func avatarsTitlePosition(_ position: AvatarStack.TextPosition) -> some View { + environment(\.avatarsTitlePosition, position) + } + + /// Layout for avatars in the stack. Default value is `.grouped`. + /// - Parameter layout: Layout for avatars in the stack. + /// - Returns: A view that avatar stack with specific layout. + func avatarsLayout(_ layout: AvatarStack.Layout) -> some View { + environment(\.avatarsLayout, layout) + } + + /// Max count for avatars in the stack. + /// - Parameter count: Max count for avatars in the stack. + /// - Returns: A view that avatar stack with specific max count. + func avatarsMaxCount(_ count: Int?) -> some View { + environment(\.avatarsMaxCount, count) + } + + /// Spacing for avatars in horizontal stack. + /// - Parameter spacing: Spacing for avatars in horizontal stack. + /// - Returns: A view that avatar stack with specific spacing. + func avatarsSpacing(_ spacing: CGFloat) -> some View { + environment(\.avatarsSpacing, spacing) + } +} diff --git a/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift b/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift index b70241a86..7292c6257 100644 --- a/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift +++ b/Sources/FioriSwiftUICore/Views/CustomBuilder/AvatarsBuilder.swift @@ -1,189 +1,48 @@ import SwiftUI +/// :nodoc: public protocol AvatarList: View, _ViewEmptyChecking { associatedtype V: View var count: Int { get } func view(at index: Int) -> V - - var borderColor: Color { get } - var borderWidth: CGFloat { get } - var isCircular: Bool { get } - var size: CGSize { get } } +/// :nodoc: public extension AvatarList { - /// :nodoc: - @ViewBuilder func buildAvatar(_ avatar: V) -> some View { - 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= 2 { - ZStack(alignment: .topLeading) { - self.buildAvatar(view(at: 0)) - self.buildAvatar(view(at: 1)) - .position(x: size.width, y: size.height) - } - .frame(width: size.width * 1.5, height: size.height * 1.5) - } else { - EmptyView() - } - } + AvatarListView(avatars: self) } -} -public extension AvatarList { + /// :nodoc: var isEmpty: Bool { count == 0 } } -public struct SingleAvatar: AvatarList { - let view: Content +struct AvatarsListStack: AvatarList { + let avatars: [any View] - public var count: Int { - self.view.isEmpty ? 0 : 1 - } - - public func view(at index: Int) -> some View { - self.view - } - - @Environment(\.avatarBorderColor) var avatarBorderColor - @Environment(\.avatarBorderWidth) var avatarBorderWidth - @Environment(\.isAvatarCircular) var isAvatarCircular - @Environment(\.avatarSize) var avatarSize - - public var borderColor: Color { - self.avatarBorderColor - } - - public var borderWidth: CGFloat { - self.avatarBorderWidth - } - - public var isCircular: Bool { - self.isAvatarCircular - } - - public var size: CGSize { - self.avatarSize - } -} - -public struct ConditionalSingleAvatar: AvatarList { - let first: TrueContent? - let second: FalseContent? - - public var count: Int { - if let first, !first.isEmpty { - return 1 - } - - if let second, !second.isEmpty { - return 1 - } - - return 0 - } - - public func view(at index: Int) -> some View { - Group { - if self.first == nil { - self.second - } else { - self.first - } + init(_ avatars: [TextOrIcon]) { + self.avatars = avatars.map { + TextOrIconView($0) } } - @Environment(\.avatarBorderColor) var avatarBorderColor - @Environment(\.avatarBorderWidth) var avatarBorderWidth - @Environment(\.isAvatarCircular) var isAvatarCircular - @Environment(\.avatarSize) var avatarSize - - public var borderColor: Color { - self.avatarBorderColor - } - - public var borderWidth: CGFloat { - self.avatarBorderWidth - } - - public var isCircular: Bool { - self.isAvatarCircular + init(avatars: [any View]) { + self.avatars = avatars } - - public var size: CGSize { - self.avatarSize - } -} - -public struct PairAvatar: AvatarList { - let first: First - let remainder: Second public var count: Int { - let firstCount = self.first.isEmpty ? 0 : 1 - return self.remainder.count + firstCount + self.avatars.count } public func view(at index: Int) -> some View { - Group { - if index == 0 { - self.first - } else { - self.remainder.view(at: index - 1) - } - } + self.avatars[index].typeErased } - @Environment(\.avatarBorderColor) var avatarBorderColor - @Environment(\.avatarBorderWidth) var avatarBorderWidth - @Environment(\.isAvatarCircular) var isAvatarCircular - @Environment(\.avatarSize) var avatarSize - - public var borderColor: Color { - self.avatarBorderColor - } - - public var borderWidth: CGFloat { - self.avatarBorderWidth - } - - public var isCircular: Bool { - self.isAvatarCircular - } - - public var size: CGSize { - self.avatarSize + var body: some View { + AvatarListView(avatars: self) } } @@ -191,182 +50,54 @@ public struct PairAvatar: AvatarList { @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @resultBuilder public enum AvatarsBuilder { - /// Builds an empty view from a block containing no statements. - public static func buildBlock() -> EmptyView { - EmptyView() - } - - /// Passes a single view written as a child view through unmodified. - /// - /// An example of a single view written as a child view is - /// `{ Text("Hello") }` - public static func buildBlock(_ content: some View) -> some AvatarList { - SingleAvatar(view: content) - } - /// :nodoc: - public static func buildBlock(_ c0: some View, _ c1: some View) -> some AvatarList { - PairAvatar(first: c0, remainder: SingleAvatar(view: c1)) - } - - /// Provides support for “if” statements in multi-statement closures, - /// producing an optional view that is visible only when the condition - /// evaluates to `true`. - public static func buildIf(_ content: (some View)?) -> some AvatarList { - SingleAvatar(view: content == nil ? AnyView(EmptyView()) : AnyView(content!)) + public static func buildBlock(_ components: any View...) -> some AvatarList { + let flatAvatars = components.flatMap { component -> [any View] in + if let c = component as? AvatarsListStack { + return c.avatars + } else { + return [component] + } + } + return AvatarsListStack(avatars: flatAvatars) } - /// Provides support for "if" statements in multi-statement closures, - /// producing conditional content for the "then" branch. - public static func buildEither(first: TrueContent) -> ConditionalSingleAvatar where TrueContent: View, FalseContent: View { - ConditionalSingleAvatar(first: first, second: nil) + /// :nodoc: + public static func buildBlock() -> EmptyView { + EmptyView() } - /// Provides support for "if-else" statements in multi-statement closures, - /// producing conditional content for the "else" branch. - public static func buildEither(second: FalseContent) -> ConditionalSingleAvatar where TrueContent: View, FalseContent: View { - ConditionalSingleAvatar(first: nil, second: second) - } -} - -extension AvatarStack: AvatarList { - public var count: Int { - let tmpIcons: [TextOrIcon] = _avatars == nil ? [] : _avatars! - - return tmpIcons.count + /// :nodoc: + public static func buildExpression(_ expression: some View) -> some View { + expression } - public func view(at index: Int) -> some View { - let tmpIcons: [TextOrIcon] = _avatars == nil ? [] : _avatars! - return Group { - switch tmpIcons[index] { - case .text(let txt): - Text(txt) - case .icon(let icon): - icon - } - } + /// :nodoc: + public static func buildExpression( + _ expression: ForEach + ) -> some AvatarList { + AvatarsListStack(avatars: expression.data.map { item in + expression.content(item) + }) } - public var borderColor: Color { - avatarBorderColor - } - - public var borderWidth: CGFloat { - avatarBorderWidth - } - - public var isCircular: Bool { - isAvatarCircular - } - - public var size: CGSize { - avatarSize - } -} - -struct AvatarBorderColor: EnvironmentKey { - public static let defaultValue = Color.clear -} - -public extension EnvironmentValues { - /// The avatars border color. Default value is `clear`. - var avatarBorderColor: Color { - get { self[AvatarBorderColor.self] } - set { self[AvatarBorderColor.self] = newValue } - } -} - -struct AvatarBorderWidth: EnvironmentKey { - public static let defaultValue: CGFloat = 0 -} - -public extension EnvironmentValues { - /// Dimensions of the avatars border width. Default value is 0. - var avatarBorderWidth: CGFloat { - get { self[AvatarBorderWidth.self] } - set { self[AvatarBorderWidth.self] = newValue } - } -} - -struct IsAvatarCircular: EnvironmentKey { - public static let defaultValue: Bool = true -} - -public extension EnvironmentValues { - /// Specifies whether the `avatars` are drawn as circular. Default value is `true`. - var isAvatarCircular: Bool { - get { self[IsAvatarCircular.self] } - set { self[IsAvatarCircular.self] = newValue } - } -} - -struct AvatarSize: EnvironmentKey { - public static let defaultValue = CGSize(width: 45, height: 45) -} - -public extension EnvironmentValues { - /// Dimensions of avatars size. Default value is `45x45`. - var avatarSize: CGSize { - get { self[AvatarSize.self] } - set { self[AvatarSize.self] = newValue } - } -} - -public extension View { - /// Set the avatars border color. Default value is `clear`. - /// ```swift - /// _ObjectItem(title: "Object Item", - /// avatars: { - /// Image(systemName: "circle.fill") - /// }) - /// .avatarBorderColor(Color.red) - /// ``` - /// - Parameter color: Border color. - /// - Returns: A view with specific border color of avatars. - func avatarBorderColor(_ color: Color) -> some View { - environment(\.avatarBorderColor, color) + /// :nodoc: + public static func buildEither(first: any View) -> some AvatarList { + AvatarsListStack(avatars: [first]) } - /// Dimensions of avatars. Default value is `45x45`. - /// ```swift - /// _ObjectItem(title: "Object Item", - /// avatars: { - /// Image(systemName: "circle.fill") - /// }) - /// .avatarSize(CGSize(30, 30)) - /// ``` - /// - Parameter size: The size of the avatars. - /// - Returns: A view that limits the size of avatars. - func avatarSize(_ size: CGSize) -> some View { - environment(\.avatarSize, size) + /// :nodoc: + public static func buildEither(second: any View) -> some AvatarList { + AvatarsListStack(avatars: [second]) } - /// Specifies whether the `avatars` are drawn as circular. Default value is `true`. - /// ```swift - /// _ObjectItem(title: "Object Item", - /// avatars: { - /// Image(systemName: "circle.fill") - /// }) - /// .isAvatarCircular(true) - /// ``` - /// - Parameter isCircular: Boolean denoting whether the avatars are circular. - /// - Returns: A view that avatars are cirlcular or not. - func isAvatarCircular(_ isCircular: Bool) -> some View { - environment(\.isAvatarCircular, isCircular) + /// :nodoc: + public static func buildOptional(_ component: (any View)?) -> some AvatarList { + AvatarsListStack(avatars: component.map { [$0] } ?? []) } - /// Dimensions of the avatars border width. Default value is 0. - /// ```swift - /// _ObjectItem(title: "Object Item", - /// avatars: { - /// Image(systemName: "circle.fill") - /// }) - /// .avatarBorderWidth(2) - /// ``` - /// - Parameter borderWidth: Dimensions of the avatars border width. - /// - Returns: A view that avatars with specific boreder width. - func avatarBorderWidth(_ borderWidth: CGFloat) -> some View { - environment(\.avatarBorderWidth, borderWidth) + /// :nodoc: + public static func buildArray(_ components: [any View]) -> some AvatarList { + AvatarsListStack(avatars: components) } } diff --git a/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsBuilder.swift b/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsBuilder.swift index a42f6b483..babf43b48 100644 --- a/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsBuilder.swift +++ b/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsBuilder.swift @@ -312,12 +312,12 @@ public extension EnvironmentValues { } struct FootnoteIconsTextPosition: EnvironmentKey { - static let defaultValue: TextPosition = .trailing + static let defaultValue: AvatarStack.TextPosition = .trailing } public extension EnvironmentValues { /// Text position for footnote icons. - var footnoteIconsTextPosition: TextPosition { + var footnoteIconsTextPosition: AvatarStack.TextPosition { get { self[FootnoteIconsTextPosition.self] } set { self[FootnoteIconsTextPosition.self] = newValue } } @@ -327,7 +327,7 @@ public extension View { /// 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 { + func footnoteIconsTextPosition(_ position: AvatarStack.TextPosition) -> some View { environment(\.footnoteIconsTextPosition, position) } @@ -392,6 +392,7 @@ public extension View { } } +@available(*, deprecated, message: "Use AvatarStack.TextPosition instead. And this will be removed in the future release.") /// Text position for icons. public enum TextPosition { /// Top position for text. diff --git a/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsListView.swift b/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsListView.swift index d7fcc14e9..bb2a40e55 100644 --- a/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsListView.swift +++ b/Sources/FioriSwiftUICore/Views/CustomBuilder/FootnoteIconsListView.swift @@ -38,8 +38,8 @@ struct FootnoteIconsListView: View { } @ViewBuilder - func avatarsView(withText: Bool = false) -> some View { - FootnoteIconsHStack(spacing: self.spacing) { + func avatarsView() -> some View { + HorizontalIconsHStack(spacing: self.spacing) { let itemsCount = self.maxCount <= 0 ? self.count : min(self.count, self.maxCount) ForEach(0 ..< itemsCount, id: \.self) { index in self.icons.view(at: index) @@ -65,7 +65,7 @@ struct FootnoteIconsListView: View { } } -struct FootnoteIconsHStack: Layout { +struct HorizontalIconsHStack: Layout { struct CacheData { var width: CGFloat var count: Int diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift index 1a6028652..928af4edb 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/BaseComponentProtocols.swift @@ -97,7 +97,7 @@ protocol _FootnoteIconsTextComponent { // sourcery: BaseComponent protocol _AvatarsComponent { - // sourcery: resultBuilder.name = @AvatarsBuilder, resultBuilder.backingComponent = AvatarStack + // sourcery: resultBuilder.name = @AvatarsBuilder, resultBuilder.backingComponent = AvatarsListStack var avatars: [TextOrIcon] { get } } @@ -333,3 +333,9 @@ protocol _ValueLabelComponent { // sourcery: @ViewBuilder var valueLabel: AttributedString? { get } } + +// sourcery: BaseComponent +protocol _AvatarsTitleComponent { + // sourcery: @ViewBuilder + var avatarsTitle: AttributedString? { get } +} diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index ca26d7589..f3cd209f0 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -498,3 +498,6 @@ protocol _DateTimePickerComponent: _TitleComponent, _ValueLabelComponent { // sourcery: defaultValue = [.date, .hourAndMinute] var pickerComponents: DatePicker.Components { get } } + +// sourcery: CompositeComponent +protocol _AvatarStackComponent: _AvatarsComponent, _AvatarsTitleComponent {} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/AvatarStackStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/AvatarStackStyle.fiori.swift new file mode 100644 index 000000000..da5ecd95e --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/AvatarStackStyle.fiori.swift @@ -0,0 +1,82 @@ +import FioriThemeManager +import Foundation +import SwiftUI + +@available(*, deprecated, message: "Use AvatarStack to create an avatar stack. We will remove this model in the future.") +public protocol AvatarStackModel: AvatarsComponent {} + +/// :nodoc: +public extension AvatarStack { + /// Layout for avatars in the stack. + enum Layout { + /// Horizontal layout for avatars. + case horizontal + /// Grouped layout for avatars. + case grouped + } + + /// Text position for icons. + 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 + } + } + } +} + +// Base Layout style +public struct AvatarStackBaseStyle: AvatarStackStyle { + @Environment(\.avatarsTitlePosition) var titlePosition + + public func makeBody(_ configuration: AvatarStackConfiguration) -> some View { + AvatarsAndTextLayout(textPosition: self.titlePosition) { + configuration.avatarsTitle + configuration.avatars + } + } +} + +// Default fiori styles +extension AvatarStackFioriStyle { + struct ContentFioriStyle: AvatarStackStyle { + func makeBody(_ configuration: AvatarStackConfiguration) -> some View { + AvatarStack(configuration) + } + } + + struct AvatarsFioriStyle: AvatarsStyle { + let avatarStackConfiguration: AvatarStackConfiguration + + func makeBody(_ configuration: AvatarsConfiguration) -> some View { + Avatars(configuration) + } + } + + struct AvatarsTitleFioriStyle: AvatarsTitleStyle { + let avatarStackConfiguration: AvatarStackConfiguration + + func makeBody(_ configuration: AvatarsTitleConfiguration) -> some View { + AvatarsTitle(configuration) + .font(.fiori(forTextStyle: .subheadline)) + .lineLimit(1) + .foregroundStyle(Color.preferredColor(.secondaryLabel)) + } + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/AvatarsTitleStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/AvatarsTitleStyle.fiori.swift new file mode 100644 index 000000000..e3c695ec5 --- /dev/null +++ b/Sources/FioriSwiftUICore/_FioriStyles/AvatarsTitleStyle.fiori.swift @@ -0,0 +1,20 @@ +import FioriThemeManager +import Foundation +import SwiftUI + +// Base Layout style +public struct AvatarsTitleBaseStyle: AvatarsTitleStyle { + @ViewBuilder + public func makeBody(_ configuration: AvatarsTitleConfiguration) -> some View { + // Add default layout here + configuration.avatarsTitle + } +} + +// Default fiori styles +public struct AvatarsTitleFioriStyle: AvatarsTitleStyle { + @ViewBuilder + public func makeBody(_ configuration: AvatarsTitleConfiguration) -> some View { + AvatarsTitle(configuration) + } +} diff --git a/Sources/FioriSwiftUICore/_FioriStyles/ObjectItemStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/ObjectItemStyle.fiori.swift index 4b7c51541..badc511b0 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/ObjectItemStyle.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/ObjectItemStyle.fiori.swift @@ -497,7 +497,7 @@ extension ObjectItemBaseStyle { @ViewBuilder func footnoteIconsView(_ context: Context) -> some View { - FootnoteIconsAndTextLayout(textPosition: self.footnoteIconsTextPosition) { + AvatarsAndTextLayout(textPosition: self.footnoteIconsTextPosition) { context.configuration.footnoteIconsText context.configuration.footnoteIcons } @@ -753,8 +753,8 @@ public struct ObjectItemBorderedAction: ActionStyle { } } -struct FootnoteIconsAndTextLayout: Layout { - let textPosition: TextPosition +struct AvatarsAndTextLayout: Layout { + let textPosition: AvatarStack.TextPosition let margin = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0) let textAndIconsSpacing: CGFloat = 6 let textMinimumWidth: CGFloat = 60 @@ -820,7 +820,7 @@ struct FootnoteIconsAndTextLayout: Layout { case .bottom: iconsView.place(at: bounds.origin, proposal: .unspecified) let nextOrigin = CGPoint(x: bounds.minX, - y: bounds.minY + textView.sizeThatFits(.unspecified).height + self.textAndIconsSpacing) + y: bounds.minY + iconsView.sizeThatFits(.unspecified).height + self.textAndIconsSpacing) textView.place(at: nextOrigin, proposal: .unspecified) case .leading: let textActualSize = textView.sizeThatFits(.unspecified) diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarStack/AvatarStack.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarStack/AvatarStack.generated.swift new file mode 100644 index 000000000..6f967cc69 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarStack/AvatarStack.generated.swift @@ -0,0 +1,70 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct AvatarStack { + let avatars: any View + let avatarsTitle: any View + + @Environment(\.avatarStackStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@AvatarsBuilder avatars: () -> any View = { EmptyView() }, + @ViewBuilder avatarsTitle: () -> any View = { EmptyView() }) + { + self.avatars = Avatars { avatars() } + self.avatarsTitle = AvatarsTitle { avatarsTitle() } + } +} + +public extension AvatarStack { + init(avatars: [TextOrIcon] = [], + avatarsTitle: AttributedString? = nil) + { + self.init(avatars: { AvatarsListStack(avatars) }, avatarsTitle: { OptionalText(avatarsTitle) }) + } +} + +public extension AvatarStack { + init(_ configuration: AvatarStackConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: AvatarStackConfiguration, shouldApplyDefaultStyle: Bool) { + self.avatars = configuration.avatars + self.avatarsTitle = configuration.avatarsTitle + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension AvatarStack: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(avatars: .init(self.avatars), avatarsTitle: .init(self.avatarsTitle))).typeErased + .transformEnvironment(\.avatarStackStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension AvatarStack { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + AvatarStack(.init(avatars: .init(self.avatars), avatarsTitle: .init(self.avatarsTitle))) + .shouldApplyDefaultStyle(false) + .avatarStackStyle(AvatarStackFioriStyle.ContentFioriStyle()) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarStack/AvatarStackStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarStack/AvatarStackStyle.generated.swift new file mode 100644 index 000000000..c250af57a --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarStack/AvatarStackStyle.generated.swift @@ -0,0 +1,38 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol AvatarStackStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: AvatarStackConfiguration) -> Body +} + +struct AnyAvatarStackStyle: AvatarStackStyle { + let content: (AvatarStackConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (AvatarStackConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: AvatarStackConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct AvatarStackConfiguration { + public let avatars: Avatars + public let avatarsTitle: AvatarsTitle + + public typealias Avatars = ConfigurationViewWrapper + public typealias AvatarsTitle = ConfigurationViewWrapper +} + +public struct AvatarStackFioriStyle: AvatarStackStyle { + public func makeBody(_ configuration: AvatarStackConfiguration) -> some View { + AvatarStack(configuration) + .avatarsStyle(AvatarsFioriStyle(avatarStackConfiguration: configuration)) + .avatarsTitleStyle(AvatarsTitleFioriStyle(avatarStackConfiguration: configuration)) + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/Avatars/Avatars.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/Avatars/Avatars.generated.swift index 974301e95..9d08e4f0d 100755 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/Avatars/Avatars.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/Avatars/Avatars.generated.swift @@ -17,7 +17,7 @@ public struct Avatars { public extension Avatars { init(avatars: [TextOrIcon] = []) { - self.init(avatars: { AvatarStack(avatars) }) + self.init(avatars: { AvatarsListStack(avatars) }) } } diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarsTitle/AvatarsTitle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarsTitle/AvatarsTitle.generated.swift new file mode 100644 index 000000000..ac1d53feb --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarsTitle/AvatarsTitle.generated.swift @@ -0,0 +1,63 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public struct AvatarsTitle { + let avatarsTitle: any View + + @Environment(\.avatarsTitleStyle) var style + + fileprivate var _shouldApplyDefaultStyle = true + + public init(@ViewBuilder avatarsTitle: () -> any View = { EmptyView() }) { + self.avatarsTitle = avatarsTitle() + } +} + +public extension AvatarsTitle { + init(avatarsTitle: AttributedString? = nil) { + self.init(avatarsTitle: { OptionalText(avatarsTitle) }) + } +} + +public extension AvatarsTitle { + init(_ configuration: AvatarsTitleConfiguration) { + self.init(configuration, shouldApplyDefaultStyle: false) + } + + internal init(_ configuration: AvatarsTitleConfiguration, shouldApplyDefaultStyle: Bool) { + self.avatarsTitle = configuration.avatarsTitle + self._shouldApplyDefaultStyle = shouldApplyDefaultStyle + } +} + +extension AvatarsTitle: View { + public var body: some View { + if self._shouldApplyDefaultStyle { + self.defaultStyle() + } else { + self.style.resolve(configuration: .init(avatarsTitle: .init(self.avatarsTitle))).typeErased + .transformEnvironment(\.avatarsTitleStyleStack) { stack in + if !stack.isEmpty { + stack.removeLast() + } + } + } + } +} + +private extension AvatarsTitle { + func shouldApplyDefaultStyle(_ bool: Bool) -> some View { + var s = self + s._shouldApplyDefaultStyle = bool + return s + } + + func defaultStyle() -> some View { + AvatarsTitle(avatarsTitle: { self.avatarsTitle }) + .shouldApplyDefaultStyle(false) + .avatarsTitleStyle(.fiori) + .typeErased + } +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarsTitle/AvatarsTitleStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarsTitle/AvatarsTitleStyle.generated.swift new file mode 100644 index 000000000..758122259 --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/AvatarsTitle/AvatarsTitleStyle.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import Foundation +import SwiftUI + +public protocol AvatarsTitleStyle: DynamicProperty { + associatedtype Body: View + + func makeBody(_ configuration: AvatarsTitleConfiguration) -> Body +} + +struct AnyAvatarsTitleStyle: AvatarsTitleStyle { + let content: (AvatarsTitleConfiguration) -> any View + + init(@ViewBuilder _ content: @escaping (AvatarsTitleConfiguration) -> any View) { + self.content = content + } + + public func makeBody(_ configuration: AvatarsTitleConfiguration) -> some View { + self.content(configuration).typeErased + } +} + +public struct AvatarsTitleConfiguration { + public let avatarsTitle: AvatarsTitle + + public typealias AvatarsTitle = ConfigurationViewWrapper +} diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItem.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItem.generated.swift index cbfea9124..e9525541b 100755 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItem.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/ObjectItem/ObjectItem.generated.swift @@ -68,7 +68,7 @@ public extension ObjectItem { tags: [AttributedString] = [], action: FioriButton? = nil) { - self.init(title: { Text(title) }, subtitle: { OptionalText(subtitle) }, footnote: { OptionalText(footnote) }, description: { OptionalText(description) }, status: { TextOrIconView(status) }, substatus: { TextOrIconView(substatus) }, detailImage: { detailImage }, icons: { IconStack(icons) }, avatars: { AvatarStack(avatars) }, footnoteIcons: { FootnoteIconStack(footnoteIcons) }, footnoteIconsText: { OptionalText(footnoteIconsText) }, tags: { TagStack(tags) }, action: { action }) + self.init(title: { Text(title) }, subtitle: { OptionalText(subtitle) }, footnote: { OptionalText(footnote) }, description: { OptionalText(description) }, status: { TextOrIconView(status) }, substatus: { TextOrIconView(substatus) }, detailImage: { detailImage }, icons: { IconStack(icons) }, avatars: { AvatarsListStack(avatars) }, footnoteIcons: { FootnoteIconStack(footnoteIcons) }, footnoteIconsText: { OptionalText(footnoteIconsText) }, tags: { TagStack(tags) }, action: { action }) } } diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift index dc1cfa43a..0390dfb2b 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ComponentStyleProtocol+Extension.generated.swift @@ -45,6 +45,62 @@ public extension AttributeStyle where Self == AttributeFioriStyle { } } +// MARK: AvatarStackStyle + +public extension AvatarStackStyle where Self == AvatarStackBaseStyle { + static var base: AvatarStackBaseStyle { + AvatarStackBaseStyle() + } +} + +public extension AvatarStackStyle where Self == AvatarStackFioriStyle { + static var fiori: AvatarStackFioriStyle { + AvatarStackFioriStyle() + } +} + +public struct AvatarStackAvatarsStyle: AvatarStackStyle { + let style: any AvatarsStyle + + public func makeBody(_ configuration: AvatarStackConfiguration) -> some View { + AvatarStack(configuration) + .avatarsStyle(self.style) + .typeErased + } +} + +public extension AvatarStackStyle where Self == AvatarStackAvatarsStyle { + static func avatarsStyle(_ style: some AvatarsStyle) -> AvatarStackAvatarsStyle { + AvatarStackAvatarsStyle(style: style) + } + + static func avatarsStyle(@ViewBuilder content: @escaping (AvatarsConfiguration) -> some View) -> AvatarStackAvatarsStyle { + let style = AnyAvatarsStyle(content) + return AvatarStackAvatarsStyle(style: style) + } +} + +public struct AvatarStackAvatarsTitleStyle: AvatarStackStyle { + let style: any AvatarsTitleStyle + + public func makeBody(_ configuration: AvatarStackConfiguration) -> some View { + AvatarStack(configuration) + .avatarsTitleStyle(self.style) + .typeErased + } +} + +public extension AvatarStackStyle where Self == AvatarStackAvatarsTitleStyle { + static func avatarsTitleStyle(_ style: some AvatarsTitleStyle) -> AvatarStackAvatarsTitleStyle { + AvatarStackAvatarsTitleStyle(style: style) + } + + static func avatarsTitleStyle(@ViewBuilder content: @escaping (AvatarsTitleConfiguration) -> some View) -> AvatarStackAvatarsTitleStyle { + let style = AnyAvatarsTitleStyle(content) + return AvatarStackAvatarsTitleStyle(style: style) + } +} + // MARK: AvatarsStyle public extension AvatarsStyle where Self == AvatarsBaseStyle { @@ -59,6 +115,20 @@ public extension AvatarsStyle where Self == AvatarsFioriStyle { } } +// MARK: AvatarsTitleStyle + +public extension AvatarsTitleStyle where Self == AvatarsTitleBaseStyle { + static var base: AvatarsTitleBaseStyle { + AvatarsTitleBaseStyle() + } +} + +public extension AvatarsTitleStyle where Self == AvatarsTitleFioriStyle { + static var fiori: AvatarsTitleFioriStyle { + AvatarsTitleFioriStyle() + } +} + // MARK: BannerMessageStyle public extension BannerMessageStyle where Self == BannerMessageBaseStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift index d284153f7..25c8eca93 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/EnvironmentVariables.generated.swift @@ -66,6 +66,27 @@ extension EnvironmentValues { } } +// MARK: AvatarStackStyle + +struct AvatarStackStyleStackKey: EnvironmentKey { + static let defaultValue: [any AvatarStackStyle] = [] +} + +extension EnvironmentValues { + var avatarStackStyle: any AvatarStackStyle { + self.avatarStackStyleStack.last ?? .base.concat(.fiori) + } + + var avatarStackStyleStack: [any AvatarStackStyle] { + get { + self[AvatarStackStyleStackKey.self] + } + set { + self[AvatarStackStyleStackKey.self] = newValue + } + } +} + // MARK: AvatarsStyle struct AvatarsStyleStackKey: EnvironmentKey { @@ -87,6 +108,27 @@ extension EnvironmentValues { } } +// MARK: AvatarsTitleStyle + +struct AvatarsTitleStyleStackKey: EnvironmentKey { + static let defaultValue: [any AvatarsTitleStyle] = [] +} + +extension EnvironmentValues { + var avatarsTitleStyle: any AvatarsTitleStyle { + self.avatarsTitleStyleStack.last ?? .base + } + + var avatarsTitleStyleStack: [any AvatarsTitleStyle] { + get { + self[AvatarsTitleStyleStackKey.self] + } + set { + self[AvatarsTitleStyleStackKey.self] = newValue + } + } +} + // MARK: BannerMessageStyle struct BannerMessageStyleStackKey: EnvironmentKey { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift index 3d88f099e..fe1c3bd6e 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ModifiedStyle.generated.swift @@ -92,6 +92,34 @@ public extension AttributeStyle { } } +// MARK: AvatarStackStyle + +extension ModifiedStyle: AvatarStackStyle where Style: AvatarStackStyle { + public func makeBody(_ configuration: AvatarStackConfiguration) -> some View { + AvatarStack(configuration) + .avatarStackStyle(self.style) + .modifier(self.modifier) + } +} + +public struct AvatarStackStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.avatarStackStyle(self.style) + } +} + +public extension AvatarStackStyle { + func modifier(_ modifier: some ViewModifier) -> some AvatarStackStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some AvatarStackStyle) -> some AvatarStackStyle { + style.modifier(AvatarStackStyleModifier(style: self)) + } +} + // MARK: AvatarsStyle extension ModifiedStyle: AvatarsStyle where Style: AvatarsStyle { @@ -120,6 +148,34 @@ public extension AvatarsStyle { } } +// MARK: AvatarsTitleStyle + +extension ModifiedStyle: AvatarsTitleStyle where Style: AvatarsTitleStyle { + public func makeBody(_ configuration: AvatarsTitleConfiguration) -> some View { + AvatarsTitle(configuration) + .avatarsTitleStyle(self.style) + .modifier(self.modifier) + } +} + +public struct AvatarsTitleStyleModifier: ViewModifier { + let style: Style + + public func body(content: Content) -> some View { + content.avatarsTitleStyle(self.style) + } +} + +public extension AvatarsTitleStyle { + func modifier(_ modifier: some ViewModifier) -> some AvatarsTitleStyle { + ModifiedStyle(style: self, modifier: modifier) + } + + func concat(_ style: some AvatarsTitleStyle) -> some AvatarsTitleStyle { + style.modifier(AvatarsTitleStyleModifier(style: self)) + } +} + // MARK: BannerMessageStyle extension ModifiedStyle: BannerMessageStyle where Style: BannerMessageStyle { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift index 436f892d4..79cc672e9 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ResolvedStyle.generated.swift @@ -51,6 +51,22 @@ extension AttributeStyle { } } +// MARK: AvatarStackStyle + +struct ResolvedAvatarStackStyle: View { + let style: Style + let configuration: AvatarStackConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension AvatarStackStyle { + func resolve(configuration: AvatarStackConfiguration) -> some View { + ResolvedAvatarStackStyle(style: self, configuration: configuration) + } +} + // MARK: AvatarsStyle struct ResolvedAvatarsStyle: View { @@ -67,6 +83,22 @@ extension AvatarsStyle { } } +// MARK: AvatarsTitleStyle + +struct ResolvedAvatarsTitleStyle: View { + let style: Style + let configuration: AvatarsTitleConfiguration + var body: some View { + self.style.makeBody(self.configuration) + } +} + +extension AvatarsTitleStyle { + func resolve(configuration: AvatarsTitleConfiguration) -> some View { + ResolvedAvatarsTitleStyle(style: self, configuration: configuration) + } +} + // MARK: BannerMessageStyle struct ResolvedBannerMessageStyle: View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift index 89a312333..d748c32af 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/View+Extension_.generated.swift @@ -54,6 +54,23 @@ public extension View { } } +// MARK: AvatarStackStyle + +public extension View { + func avatarStackStyle(_ style: some AvatarStackStyle) -> some View { + self.transformEnvironment(\.avatarStackStyleStack) { stack in + stack.append(style) + } + } + + func avatarStackStyle(@ViewBuilder content: @escaping (AvatarStackConfiguration) -> some View) -> some View { + self.transformEnvironment(\.avatarStackStyleStack) { stack in + let style = AnyAvatarStackStyle(content) + stack.append(style) + } + } +} + // MARK: AvatarsStyle public extension View { @@ -71,6 +88,23 @@ public extension View { } } +// MARK: AvatarsTitleStyle + +public extension View { + func avatarsTitleStyle(_ style: some AvatarsTitleStyle) -> some View { + self.transformEnvironment(\.avatarsTitleStyleStack) { stack in + stack.append(style) + } + } + + func avatarsTitleStyle(@ViewBuilder content: @escaping (AvatarsTitleConfiguration) -> some View) -> some View { + self.transformEnvironment(\.avatarsTitleStyleStack) { stack in + let style = AnyAvatarsTitleStyle(content) + stack.append(style) + } + } +} + // MARK: BannerMessageStyle public extension View { diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift index c9234ccbf..39735d69f 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/ViewEmptyChecking+Extension.generated.swift @@ -21,12 +21,25 @@ extension Attribute: _ViewEmptyChecking { } } +extension AvatarStack: _ViewEmptyChecking { + public var isEmpty: Bool { + avatars.isEmpty && + avatarsTitle.isEmpty + } +} + extension Avatars: _ViewEmptyChecking { public var isEmpty: Bool { avatars.isEmpty } } +extension AvatarsTitle: _ViewEmptyChecking { + public var isEmpty: Bool { + avatarsTitle.isEmpty + } +} + extension BannerMessage: _ViewEmptyChecking { public var isEmpty: Bool { icon.isEmpty && diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/AvatarStack+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/AvatarStack+API.generated.swift deleted file mode 100644 index d60793f65..000000000 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/AvatarStack+API.generated.swift +++ /dev/null @@ -1,21 +0,0 @@ -// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery -// DO NOT EDIT -import SwiftUI - -public struct AvatarStack { - @Environment(\.avatarsModifier) private var avatarsModifier - @Environment(\.avatarBorderColor) var avatarBorderColor - @Environment(\.avatarBorderWidth) var avatarBorderWidth - @Environment(\.isAvatarCircular) var isAvatarCircular - @Environment(\.avatarSize) var avatarSize - - var _avatars: [TextOrIcon]? = nil - - public init(model: AvatarStackModel) { - self.init(avatars: model.avatars) - } - - public init(avatars: [TextOrIcon]? = nil) { - self._avatars = avatars - } -}