Skip to content

Commit cc60cb0

Browse files
shengxu7dyongxu
andauthored
feat: 🎸 add card footer button width mode [jira: 0] (#1230)
* feat: 🎸 add card footer button width mode [jira: 0] * style: 💄 fix a few swiftlint warnings * Update Sources/FioriSwiftUICore/_FioriStyles/CardFooterStyle.fiori.swift Co-authored-by: dyongxu <61523257+dyongxu@users.noreply.github.com> * Update Sources/FioriSwiftUICore/_FioriStyles/CardFooterStyle.fiori.swift Co-authored-by: dyongxu <61523257+dyongxu@users.noreply.github.com> --------- Co-authored-by: dyongxu <61523257+dyongxu@users.noreply.github.com>
1 parent 630d7fe commit cc60cb0

File tree

2 files changed

+143
-47
lines changed

2 files changed

+143
-47
lines changed

Apps/Examples/Examples/FioriSwiftUICore/Card/MobileCardExample.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import SwiftUI
55

66
struct MobileCardExample: View {
77
@Environment(\.horizontalSizeClass) var horizontalSizeClass
8+
@State private var isPresented: Bool = false
9+
@State private var buttonWidthMode: Int = 0
810

911
var body: some View {
1012
List {
@@ -15,6 +17,7 @@ struct MobileCardExample: View {
1517
}
1618
.listRowBackground(Color.preferredColor(.primaryGroupedBackground))
1719
}
20+
.environment(\.cardFooterButtonWidthMode, CardFooterButtonWidthMode(rawValue: self.buttonWidthMode) ?? .auto)
1821
.cardStyle(.card)
1922
.listStyle(.plain)
2023
.navigationBarTitle("Cards in List", displayMode: .inline)
@@ -31,6 +34,7 @@ struct MobileCardExample: View {
3134
.cardStyle(.intrinsicHeightCard)
3235
}
3336
.background(Color.preferredColor(.primaryGroupedBackground))
37+
.environment(\.cardFooterButtonWidthMode, CardFooterButtonWidthMode(rawValue: self.buttonWidthMode) ?? .auto)
3438
}.padding()
3539
}
3640
.navigationBarTitle("Cards in VStack", displayMode: .inline)
@@ -44,6 +48,7 @@ struct MobileCardExample: View {
4448
CardFooterTests.examples[i]
4549
}
4650
.listRowBackground(Color.preferredColor(.primaryGroupedBackground))
51+
.environment(\.cardFooterButtonWidthMode, CardFooterButtonWidthMode(rawValue: self.buttonWidthMode) ?? .auto)
4752
}
4853
.listStyle(.plain)
4954
.navigationBarTitle("Footers", displayMode: .inline)
@@ -53,6 +58,7 @@ struct MobileCardExample: View {
5358

5459
NavigationLink {
5560
MasonryTestView()
61+
.environment(\.cardFooterButtonWidthMode, CardFooterButtonWidthMode(rawValue: self.buttonWidthMode) ?? .auto)
5662
.navigationBarTitle("Masonry", displayMode: .inline)
5763
} label: {
5864
Text("Masonry")
@@ -61,6 +67,7 @@ struct MobileCardExample: View {
6167
NavigationLink {
6268
CarouselTestView(self.horizontalSizeClass == .compact ? 1 : (UIDevice.current.localizedModel == "iPhone" ? 2 : 3))
6369
.navigationBarTitle("Carousel", displayMode: .inline)
70+
.environment(\.cardFooterButtonWidthMode, CardFooterButtonWidthMode(rawValue: self.buttonWidthMode) ?? .auto)
6471
} label: {
6572
Text("Carousel")
6673
}
@@ -309,6 +316,22 @@ struct MobileCardExample: View {
309316
}
310317
}
311318
.navigationBarTitle("Cards", displayMode: .inline)
319+
.sheet(isPresented: self.$isPresented) {
320+
Form {
321+
Text("Card Footer Button Width Mode")
322+
Picker("", selection: self.$buttonWidthMode) {
323+
Text("Auto").tag(0)
324+
Text("Equal").tag(1)
325+
Text("Intrinsic").tag(2)
326+
}
327+
.pickerStyle(.segmented)
328+
}
329+
}
330+
.toolbar(content: {
331+
FioriButton(title: "Options") { _ in
332+
self.isPresented = true
333+
}
334+
})
312335
}
313336
}
314337

Sources/FioriSwiftUICore/_FioriStyles/CardFooterStyle.fiori.swift

Lines changed: 120 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,34 @@ import FioriThemeManager
22
import Foundation
33
import SwiftUI
44

5-
private struct CardFooterLayout: Layout {
6-
enum ButtonWidthMode {
7-
/// same and fill its width to use available container width
8-
case sameAndFill
9-
10-
/// intrinsic size's width
11-
case intrinsic
5+
struct CardFooterButtonWidthModeKey: EnvironmentKey {
6+
/// Default value is `.auto`
7+
public static let defaultValue: CardFooterButtonWidthMode = .auto
8+
}
9+
10+
public extension EnvironmentValues {
11+
/// Sets the button width mode for `CardFooter`.
12+
var cardFooterButtonWidthMode: CardFooterButtonWidthMode {
13+
get { self[CardFooterButtonWidthModeKey.self] }
14+
set { self[CardFooterButtonWidthModeKey.self] = newValue }
1215
}
16+
}
17+
18+
/// CardFooter button width mode
19+
public enum CardFooterButtonWidthMode: Int {
20+
/// auto size based on card footer's width. When it is regular size class, up to 3 buttons are shown with intrinsic width; when it is compact size class, up to 2 buttons are shown with equal width.
21+
case auto
22+
23+
/// equal size and fill up the whole width except the overflow button
24+
case equal
1325

26+
/// intrinsic size's width
27+
case intrinsic
28+
}
29+
30+
private struct CardFooterLayout: Layout {
1431
struct LayoutMode: Equatable {
15-
let mode: ButtonWidthMode
32+
let mode: CardFooterButtonWidthMode
1633
let num: Int
1734
}
1835

@@ -28,21 +45,18 @@ private struct CardFooterLayout: Layout {
2845
}
2946
}
3047

31-
@Binding var numButtonsDisplayInOverflow: Int
32-
3348
/// The distance between adjacent subviews.
3449
var spacing: CGFloat? = 8
3550

3651
/// Maximum width for each element in the container
3752
var maxButtonWidth: CGFloat
3853

39-
var horizontalSizeClass: UserInterfaceSizeClass? = .compact
54+
let cardFooterButtonWidthMode: CardFooterButtonWidthMode
4055

41-
init(numButtonsDisplayInOverflow: Binding<Int>, spacing: CGFloat? = nil, maxButtonWidth: CGFloat? = nil, horizontalSizeClass: UserInterfaceSizeClass? = nil) {
42-
self._numButtonsDisplayInOverflow = numButtonsDisplayInOverflow
56+
init(spacing: CGFloat? = nil, maxButtonWidth: CGFloat? = nil, cardFooterButtonWidthMode: CardFooterButtonWidthMode = .auto) {
4357
self.spacing = spacing
4458
self.maxButtonWidth = max(100, maxButtonWidth ?? CGFloat.greatestFiniteMagnitude)
45-
self.horizontalSizeClass = horizontalSizeClass
59+
self.cardFooterButtonWidthMode = cardFooterButtonWidthMode
4660
}
4761

4862
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) -> CGSize {
@@ -58,8 +72,17 @@ private struct CardFooterLayout: Layout {
5872
}
5973

6074
func calculateLayout(proposal: ProposedViewSize, subviews: Subviews, cache: inout CacheData) {
61-
let isRegular = proposal.width ?? 1024 > 667
62-
let layoutMode = LayoutMode(mode: isRegular ? .intrinsic : .sameAndFill,
75+
let isRegular: Bool
76+
switch self.cardFooterButtonWidthMode {
77+
case .auto:
78+
isRegular = proposal.width ?? 1024 > 667
79+
case .equal:
80+
isRegular = false
81+
case .intrinsic:
82+
isRegular = true
83+
}
84+
85+
let layoutMode = LayoutMode(mode: isRegular ? .intrinsic : .equal,
6386
num: isRegular ? 3 : 2)
6487
if !cache.frames.isEmpty, cache.fitSize.width == proposal.width, cache.layoutMode == layoutMode {
6588
return
@@ -71,16 +94,30 @@ private struct CardFooterLayout: Layout {
7194
$0.sizeThatFits(.unspecified)
7295
}
7396

74-
let hideRect = CGRect(x: -2000, y: 0, width: 0, height: 0)
75-
self.calculateLayout(proposalWidth: proposal.width, subViewSizes: subViewSizes, hideRect: hideRect, layoutMode: layoutMode, cache: &cache)
97+
self.calculateLayout(proposalWidth: proposal.width, subViewSizes: subViewSizes, layoutMode: layoutMode, cache: &cache)
7698
}
7799

78-
/// .compact, .sameAndFill, same size, up to 2 buttons
79-
/// .reguar, .intrinsic, up to 3 buttons
80-
func calculateLayout(proposalWidth: CGFloat?, subViewSizes: [CGSize], hideRect: CGRect, layoutMode: LayoutMode, cache: inout CacheData) {
81-
let subViewNoOflSizes = subViewSizes.dropLast()
100+
/**
101+
case 1: totol is 5 buttons, 3 buttons (tertiary, secondary, primary), overflow menu with 2 buttons, overflow menu with 1 button
102+
case 2: total is 3 buttons, 2 buttons (only two of tertiary, secondary and primary exist), overflow menu with 1 button
103+
case 3: total is 1 button, 1 button, only one of tertiary, secondary or primary exist
104+
105+
In compact width, .equal, same size, up to 2 buttons
106+
In regular width, .intrinsic, up to 3 buttons
107+
*/
108+
func calculateLayout(proposalWidth: CGFloat?, subViewSizes: [CGSize], layoutMode: LayoutMode, cache: inout CacheData) {
109+
let subViewNoOflSizes: [CGSize]
110+
switch subViewSizes.count {
111+
case 5:
112+
subViewNoOflSizes = Array(subViewSizes.dropLast(2))
113+
case 3:
114+
subViewNoOflSizes = Array(subViewSizes.dropLast(1))
115+
default:
116+
subViewNoOflSizes = subViewSizes
117+
}
118+
82119
let numButtons = subViewNoOflSizes.count
83-
let overflowSize = subViewSizes[numButtons]
120+
let overflowSize = subViewNoOflSizes.count < subViewSizes.count ? subViewSizes[numButtons] : CGSize.zero
84121
let theSpacing: CGFloat = self.spacing ?? 0
85122
var maxHeight: CGFloat = 0
86123
var requiredFinalWidth: CGFloat = 0
@@ -92,7 +129,7 @@ private struct CardFooterLayout: Layout {
92129

93130
/// calculate numToShow, buttonWidth, requiredFinalWidth
94131
if finalWidth == 0 {
95-
if layoutMode.mode == .sameAndFill {
132+
if layoutMode.mode == .equal {
96133
let tmpButtonWidth: CGFloat = subViewNoOflSizes.suffix(numToShow).reduce(0) { partialResult, size in
97134
min(self.maxButtonWidth, max(partialResult, size.width))
98135
}
@@ -117,7 +154,7 @@ private struct CardFooterLayout: Layout {
117154
} else { // there is a proposalWidth
118155
var tmpButtonWidth: CGFloat = 0
119156
for i in 0 ..< idealNumToShow {
120-
if layoutMode.mode == .sameAndFill {
157+
if layoutMode.mode == .equal {
121158
tmpButtonWidth = min(self.maxButtonWidth, max(tmpButtonWidth, subViewNoOflSizes[i].width))
122159
requiredFinalWidth = tmpButtonWidth * CGFloat(i + 1) + theSpacing * CGFloat(i)
123160
} else {
@@ -131,7 +168,7 @@ private struct CardFooterLayout: Layout {
131168
if numToShow > 1 {
132169
numToShow -= 1
133170
}
134-
if layoutMode.mode == .sameAndFill {
171+
if layoutMode.mode == .equal {
135172
var availableWidth = finalWidth - theSpacing * CGFloat(max(0, numToShow - 1))
136173
if numButtons > 1 {
137174
availableWidth -= theSpacing + overflowSize.width
@@ -142,7 +179,7 @@ private struct CardFooterLayout: Layout {
142179
}
143180
}
144181

145-
if buttonWidth == nil, layoutMode.mode == .sameAndFill {
182+
if buttonWidth == nil, layoutMode.mode == .equal {
146183
var availableWidth = finalWidth - theSpacing * CGFloat(numToShow - 1)
147184
if numToShow < numButtons {
148185
availableWidth -= min(self.maxButtonWidth, overflowSize.width) + theSpacing
@@ -159,29 +196,27 @@ private struct CardFooterLayout: Layout {
159196
/// set up frames for each subview
160197

161198
let y = maxHeight / 2
162-
199+
// Move the hidden buttons out of visible area
200+
let hideRect = CGRect(x: -2000, y: y, width: 0, height: 0)
163201
var frames = [CGRect]()
164202

165203
var x: CGFloat = 0
166-
for i in 0 ... numButtons {
204+
for i in 0 ..< subViewSizes.count {
167205
if i < numToShow {
168206
let btWidth = buttonWidth ?? min(finalWidth - (numToHide > 0 ? theSpacing + overflowSize.width : 0), self.maxButtonWidth, subViewNoOflSizes[i].width)
169207
x += btWidth + (i > 0 ? theSpacing : 0)
170208
frames.append(CGRect(origin: CGPoint(x: finalWidth - x + btWidth / 2, y: y), size: CGSize(width: btWidth, height: maxHeight)))
171209
} else if i < numButtons { // rest button to hide
172210
frames.append(hideRect)
173211
} else { // overflow
174-
if numToHide > 0 {
212+
if numToHide > 0, i == numButtons + numToHide - 1 { // last one to show overflow
175213
frames.append(CGRect(x: overflowSize.width / 2, y: y, width: min(self.maxButtonWidth, overflowSize.width), height: overflowSize.height))
176-
} else {
214+
} else { // hide the other overflow
177215
frames.append(hideRect)
178216
}
179217
}
180218
}
181-
182-
DispatchQueue.main.async {
183-
self.numButtonsDisplayInOverflow = numToHide
184-
}
219+
185220
cache.frames = frames.reversed()
186221
cache.fitSize = CGSize(width: finalWidth, height: maxHeight)
187222
cache.layoutMode = layoutMode
@@ -214,29 +249,36 @@ private struct CardFooterLayout: Layout {
214249

215250
// Base Layout style
216251
public struct CardFooterBaseStyle: CardFooterStyle {
217-
@Environment(\.horizontalSizeClass) var horizontalSizeClass
218-
@State var numButtonsDisplayInOverflow: Int = 0
252+
@Environment(\.cardFooterButtonWidthMode) var cardFooterButtonWidthMode
219253

220254
@ViewBuilder
221255
public func makeBody(_ configuration: CardFooterConfiguration) -> some View {
222256
// Add default layout here
223-
CardFooterLayout(numButtonsDisplayInOverflow: self.$numButtonsDisplayInOverflow, spacing: 8, maxButtonWidth: nil, horizontalSizeClass: self.horizontalSizeClass) {
224-
Menu {
225-
if self.numButtonsDisplayInOverflow == 1 {
257+
CardFooterLayout(spacing: 8, maxButtonWidth: nil, cardFooterButtonWidthMode: self.cardFooterButtonWidthMode) {
258+
if self.numOfButtons(configuration) == 3 {
259+
Menu {
260+
configuration.secondaryAction.environment(\.isInMenu, true)
261+
configuration.tertiaryAction.environment(\.isInMenu, true)
262+
} label: {
263+
configuration.overflowAction
264+
}
265+
/// set the accessibilityLabel as same as SF symbol "ellipsis" which is "More"
266+
.accessibilityLabel(Text(Image(systemName: "ellipsis")))
267+
}
268+
269+
if self.numOfButtons(configuration) > 1 {
270+
Menu {
226271
if !configuration.tertiaryAction.isEmpty {
227272
configuration.tertiaryAction.environment(\.isInMenu, true)
228273
} else {
229274
configuration.secondaryAction.environment(\.isInMenu, true)
230275
}
231-
} else if self.numButtonsDisplayInOverflow == 2 {
232-
configuration.secondaryAction.environment(\.isInMenu, true)
233-
configuration.tertiaryAction.environment(\.isInMenu, true)
276+
} label: {
277+
configuration.overflowAction
234278
}
235-
} label: {
236-
configuration.overflowAction
279+
/// set the accessibilityLabel as same as SF symbol "ellipsis" which is "More"
280+
.accessibilityLabel(Text(Image(systemName: "ellipsis")))
237281
}
238-
/// set the accessibilityLabel as same as SF symbol "ellipsis" which is "More"
239-
.accessibilityLabel(Text(Image(systemName: "ellipsis")))
240282

241283
if !configuration.tertiaryAction.isEmpty {
242284
configuration.tertiaryAction
@@ -251,6 +293,27 @@ public struct CardFooterBaseStyle: CardFooterStyle {
251293
}
252294
}
253295
}
296+
297+
/**
298+
case 1: totol is 5 buttons, 3 buttons (tertiary, secondary, primary), overflow menu with 2 buttons, overflow menu with 1 button
299+
case 2: total is 3 buttons, 2 buttons (only two of tertiary, secondary and primary exist), overflow menu with 1 button
300+
case 3: total is 1 button, 1 button, only one of tertiary, secondary or primary exist
301+
*/
302+
func numOfButtons(_ configuration: CardFooterConfiguration) -> Int {
303+
var value = 0
304+
if !configuration.action.isEmpty {
305+
value += 1
306+
}
307+
308+
if !configuration.secondaryAction.isEmpty {
309+
value += 1
310+
}
311+
312+
if !configuration.tertiaryAction.isEmpty {
313+
value += 1
314+
}
315+
return value
316+
}
254317
}
255318

256319
// Default fiori styles
@@ -316,6 +379,16 @@ public enum CardFooterTests {
316379
public static let examples = [footer0, footer1, footer2, footer3, footer4, footer5, footer6, footer7, footer8, footer9, footer10]
317380
}
318381

382+
#Preview("Empty") {
383+
VStack {
384+
CardFooter(action: { EmptyView() }, overflowAction: { EmptyView() }).border(Color.green)
385+
386+
Text("Empty")
387+
388+
CardFooter(action: { EmptyView() }).border(Color.green)
389+
}.border(Color.red)
390+
}
391+
319392
#Preview("P") {
320393
CardFooter(action: FioriButton(title: "Primary"), overflowAction: FioriButton(title: "Overflow"))
321394
}

0 commit comments

Comments
 (0)