Skip to content

Commit

Permalink
Merge pull request #130 from SchweizerischeBundesbahnen/feature/tinyS…
Browse files Browse the repository at this point in the history
…BBLoadingIndicator

Feature/tiny sbb loading indicator
  • Loading branch information
fleuryj authored Jul 8, 2024
2 parents 9dd9baf + 0e7723f commit fe67057
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 41 deletions.
30 changes: 21 additions & 9 deletions SBBDesignSystemMobileSwiftUI/Views/SBBListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public struct SBBListItem: View {
private let footnote: Text?
private let footnoteAccessibility: Text?
private let showBottomLine: Bool
private let rightImageIsCircled: Bool

var leftSwipeButtonLabel: AnyView?
var leftSwipeButtonAction: (() -> ())?
Expand All @@ -52,15 +53,17 @@ public struct SBBListItem: View {
- footnote: An optional label displayed underneath the main label.
- footnoteAccessibility: The optional alternative text for the footnote's VoiceOver.
- showBottomLine: Shows or hides a separator line at the bottom of the View (typically only false for last elements in a List).
- rightImageIsCircled: The right icon is in a circle.
*/
public init(label: Text, labelAccessibility: Text? = nil, leftImage: Image? = nil, rightImage: Image? = Image(sbbIcon: .chevron_small_right_small), footnote: Text? = nil, footnoteAccessibility: Text? = nil, showBottomLine: Bool = true) {
public init(label: Text, labelAccessibility: Text? = nil, leftImage: Image? = nil, rightImage: Image? = Image(sbbIcon: .chevron_small_right_small), footnote: Text? = nil, footnoteAccessibility: Text? = nil, showBottomLine: Bool = true, rightImageIsCircled: Bool = true) {
self.label = label
self.labelAccessibility = labelAccessibility
self.leftImage = leftImage
self.rightImage = rightImage
self.footnote = footnote
self.footnoteAccessibility = footnoteAccessibility
self.showBottomLine = showBottomLine
self.rightImageIsCircled = rightImageIsCircled
}

/// SBBListItem Type.
Expand All @@ -72,25 +75,27 @@ public struct SBBListItem: View {
}

@available(*, deprecated, message: "image renamed to leftImage. SBBListItemType removed, instead use rightImage to specify the desired image.")
public init(label: Text, labelAccessibility: Text? = nil, image: Image, footnote: Text? = nil, footnoteAccessibility: Text? = nil, type: SBBListItemType = .normal, showBottomLine: Bool = true) {
public init(label: Text, labelAccessibility: Text? = nil, image: Image, footnote: Text? = nil, footnoteAccessibility: Text? = nil, type: SBBListItemType = .normal, showBottomLine: Bool = true, rightImageIsCircled: Bool = true) {
self.label = label
self.labelAccessibility = labelAccessibility
self.leftImage = image
self.footnote = footnote
self.footnoteAccessibility = footnoteAccessibility
self.rightImage = Image(sbbIcon: (type == .normal ? .chevron_small_right_small : .circle_information_small_small))
self.showBottomLine = showBottomLine
self.rightImageIsCircled = rightImageIsCircled
}

@available(*, deprecated, message: "image renamed to leftImage. SBBListItemType removed, instead use rightImage to specify the desired image.")
public init(label: Text, labelAccessibility: Text? = nil, footnote: Text? = nil, footnoteAccessibility: Text? = nil, type: SBBListItemType, showBottomLine: Bool = true) {
public init(label: Text, labelAccessibility: Text? = nil, footnote: Text? = nil, footnoteAccessibility: Text? = nil, type: SBBListItemType, showBottomLine: Bool = true, rightImageIsCircled: Bool = true) {
self.label = label
self.labelAccessibility = labelAccessibility
self.leftImage = nil
self.footnote = footnote
self.footnoteAccessibility = footnoteAccessibility
self.rightImage = Image(sbbIcon: (type == .normal ? .chevron_small_right_small : .circle_information_small_small))
self.showBottomLine = showBottomLine
self.rightImageIsCircled = rightImageIsCircled
}

public var body: some View {
Expand Down Expand Up @@ -160,12 +165,19 @@ public struct SBBListItem: View {
.padding(.vertical, 12)
Spacer()
if let rightImage {
rightImage
.accessibility(hidden: true)
.frame(width: 32, height: 32)
.clipShape(Circle())
.overlay(Circle().stroke(Color.sbbColor(.border)))
.padding(.vertical, 6)
if rightImageIsCircled {
rightImage
.accessibility(hidden: true)
.frame(width: 32, height: 32)
.clipShape(Circle())
.overlay(Circle().stroke(Color.sbbColor(.border)))
.padding(.vertical, 6)
} else {
rightImage
.accessibility(hidden: true)
.frame(width: 32, height: 32)
.padding(.vertical, 6)
}
}
}
.frame(minHeight: 44)
Expand Down
83 changes: 55 additions & 28 deletions SBBDesignSystemMobileSwiftUI/Views/SBBLoadingIndicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ public struct SBBLoadingIndicator: View {
case normal
/// Small SBBLoadingIndicator Size.
case small
/// Tiny SBBLoadingIndicator Size.
case tiny

var size: CGSize {
switch self {
case .normal:
return CGSize(width: 55, height: 32)
case .small:
return CGSize(width: 31, height: 18)
case .tiny:
return CGSize(width: 15, height: 9)
}
}
}
Expand Down Expand Up @@ -90,7 +94,7 @@ public struct SBBLoadingIndicator: View {

let innerWidth = width / CGFloat(cos(rotationInDegrees * .pi / 180)) * 3.0
self.innerWidth = innerWidth
let paddingBetweenRectangles = size == .normal ? 8.0 : 4.0
let paddingBetweenRectangles = size == .normal ? 8.0 : size == .small ? 4.0 : 2.0
self.rectangleWidth = innerWidth / CGFloat(numberOfRectangles) + paddingBetweenRectangles

rectangleOffsets = [-rectangleWidth, 0, rectangleWidth, 2.0*rectangleWidth, 3.0*rectangleWidth, 4.0*rectangleWidth]
Expand All @@ -99,37 +103,34 @@ public struct SBBLoadingIndicator: View {
}

public var body: some View {
Group {
ZStack {
ForEach(rectangleProperties.indices, id:\.self) { index in
Rectangle()
.fill(style.color(for: colorScheme))
.frame(width: innerWidth / CGFloat(numberOfRectangles), height: height)
.opacity(rectangleProperties[index].opacity)
.offset(x: rectangleProperties[index].offset)
.onAppear {
DispatchQueue.main.async {
withAnimation(Animation.linear(duration: animationDuration)
.repeatForever(autoreverses: false)
)
{
rectangleProperties[index].offset = rectangleOffsets[index]
}

withAnimation(Animation.linear(duration: animationDuration)
.repeatForever(autoreverses: false)
) {
rectangleProperties[index].opacity = rectangleOpacity[index]
}
ZStack {
ForEach(rectangleProperties.indices, id:\.self) { index in
Rectangle()
.fill(style.color(for: colorScheme))
.frame(width: innerWidth / CGFloat(numberOfRectangles), height: height)
.opacity(rectangleProperties[index].opacity)
.offset(x: rectangleProperties[index].offset)
.onAppear {
DispatchQueue.main.async {
withAnimation(Animation.linear(duration: animationDuration)
.repeatForever(autoreverses: false)
)
{
rectangleProperties[index].offset = rectangleOffsets[index]
}

withAnimation(Animation.linear(duration: animationDuration)
.repeatForever(autoreverses: false)
) {
rectangleProperties[index].opacity = rectangleOpacity[index]
}
}
}
}
}
.rotation3DEffect(.degrees(rotationInDegrees), axis: (x: 0, y: 1, z: 0), anchor: .leading)
}
.frame(width: width, height: height, alignment: .leading)
.padding(16)
.rotation3DEffect(.degrees(rotationInDegrees), axis: (x: 0, y: 1, z: 0), anchor: .leading)
.frame(width: width, height: height, alignment: .leading)
.padding(16)
.accessibility(label: Text("Loading.".localized))
}
}
Expand All @@ -138,6 +139,15 @@ public struct SBBLoadingIndicator: View {
struct SBBLoadingIndicator_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack {
SBBLoadingIndicator()
.previewDisplayName("normal, red, light")
SBBLoadingIndicator(size: .small)
.previewDisplayName("normal, red, light")
SBBLoadingIndicator(size: .tiny)
.previewDisplayName("normal, red, light")

}
Group {
SBBLoadingIndicator()
.previewDisplayName("normal, red, light")
Expand Down Expand Up @@ -170,6 +180,23 @@ struct SBBLoadingIndicator_Previews: PreviewProvider {
.previewDisplayName("small, white, dark")
.environment(\.colorScheme, .dark)
}

Group {
SBBLoadingIndicator(size: .tiny)
.previewDisplayName("tiny, red, light")
SBBLoadingIndicator(size: .tiny)
.previewDisplayName("tiny, red, dark")
.environment(\.colorScheme, .dark)
SBBLoadingIndicator(size: .tiny, style: .grey)
.previewDisplayName("tiny, grey, light")
SBBLoadingIndicator(size: .tiny, style: .grey)
.previewDisplayName("tiny, grey, dark")
SBBLoadingIndicator(size: .tiny, style: .primaryBackground)
.previewDisplayName("tiny, white, light")
SBBLoadingIndicator(size: .small, style: .primaryBackground)
.previewDisplayName("tiny, white, dark")
.environment(\.colorScheme, .dark)
}
}
.previewLayout(.sizeThatFits)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,57 @@ import SBBDesignSystemMobileSwiftUI

class SBBLoadingIndicatorTests: XCTestCase {

// WORKAROUND: no 3D rotation applied on standard snapshots but Documentation snapshots renders indicator correctly (e.g. image) -> renders image and compare that
private func renderSBBLoadingIndicator(view: any View, colorScheme: ColorScheme) -> UIViewController {
let controller = view.colorScheme(colorScheme).toVC()
let view = controller.view

let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear

let renderer = UIGraphicsImageRenderer(size: targetSize)

let image = renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}

return Image(uiImage: image).toVC()
}

func testLoadingIndicatorNormalPrimary() {
let view = SBBLoadingIndicator(size: .normal, style: .primary)
for colorScheme in ColorScheme.allCases {
view.recordDocumentationSnapshot(name: "SBBLoadingIndicator", colorScheme: colorScheme)
assertSnapshot(matching: view.colorScheme(colorScheme).toVC(), as: imagePortrait, record: recordNewReferenceSnapshots)
assertSnapshot(matching: renderSBBLoadingIndicator(view: view, colorScheme: colorScheme), as: imagePortrait, record: recordNewReferenceSnapshots)
}
}

func testLoadingIndicatorNormalGrey() {
let view = SBBLoadingIndicator(size: .normal, style: .grey)
for colorScheme in ColorScheme.allCases {
assertSnapshot(matching: view.colorScheme(colorScheme).toVC(), as: imagePortrait, record: recordNewReferenceSnapshots)
assertSnapshot(matching: renderSBBLoadingIndicator(view: view, colorScheme: colorScheme), as: imagePortrait, record: recordNewReferenceSnapshots)
}
}

func testLoadingIndicatorNormalWhite() {
let view = SBBLoadingIndicator(size: .normal, style: .primaryBackground)
for colorScheme in ColorScheme.allCases {
assertSnapshot(matching: view.colorScheme(colorScheme).toVC(), as: imagePortrait, record: recordNewReferenceSnapshots)
assertSnapshot(matching: renderSBBLoadingIndicator(view: view, colorScheme: colorScheme), as: imagePortrait, record: recordNewReferenceSnapshots)
}
}

func testLoadingIndicatorSmallPrimary() {
let view = SBBLoadingIndicator(size: .small, style: .primary)
for colorScheme in ColorScheme.allCases {
assertSnapshot(matching: view.colorScheme(colorScheme).toVC(), as: imagePortrait, record: recordNewReferenceSnapshots)
assertSnapshot(matching: renderSBBLoadingIndicator(view: view, colorScheme: colorScheme), as: imagePortrait, record: recordNewReferenceSnapshots)
}
}

func testLoadingIndicatorTineyPrimary() {
let view = SBBLoadingIndicator(size: .tiny, style: .primary)
for colorScheme in ColorScheme.allCases {
assertSnapshot(matching: renderSBBLoadingIndicator(view: view, colorScheme: colorScheme), as: imagePortrait, record: recordNewReferenceSnapshots)
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit fe67057

Please sign in to comment.