Skip to content

Commit

Permalink
Card Button in SwiftUI (#1981)
Browse files Browse the repository at this point in the history
* Card Button in SwiftUI

* Snapshot tests

* Examples

* README.md

* Use PreviewProvider

* Use linear animation to pass CI

* Record snapshots

* Updated snapshots

* retry

* AccessibilityAddTraits on selected

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
iHandle and github-actions[bot] authored Jun 7, 2024
1 parent 4730d4f commit 0553a29
Show file tree
Hide file tree
Showing 25 changed files with 661 additions and 5 deletions.
44 changes: 44 additions & 0 deletions Backpack-SwiftUI/CardButton/Classes/BPKCardButtonSize.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2024 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import SwiftUI

public enum BPKCardButtonSize {
case `default`
case small
}

extension BPKCardButtonSize {
var containerSize: BPKSpacing {
switch self {
case .default:
return .xxl
case .small:
return .xl
}
}

var iconSize: BPKIcon.Size {
switch self {
case .default:
return .large
case .small:
return .small
}
}
}
36 changes: 36 additions & 0 deletions Backpack-SwiftUI/CardButton/Classes/BPKCardButtonStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2024 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import SwiftUI

public enum BPKCardButtonStyle {
case `default`
case contained
case onDark
}

extension BPKCardButtonStyle {
var containerColor: BPKColor {
switch self {
case .contained:
return .cardButtonContainedFillColor
case .default, .onDark:
return .clear
}
}
}
145 changes: 145 additions & 0 deletions Backpack-SwiftUI/CardButton/Classes/BPKSaveCardButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2024 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import SwiftUI

public struct BPKSaveCardButton: View {

private let animationDuration: TimeInterval = 0.2
private let maxScale: CGFloat = 1.25
@State private var scale: CGFloat = 1.0
@State private var isPressed: Bool = false

let size: BPKCardButtonSize
let style: BPKCardButtonStyle
let checked: Bool
let accessibilityLabel: String
let action: () -> Void

public init(
size: BPKCardButtonSize,
style: BPKCardButtonStyle,
checked: Bool,
accessibilityLabel: String,
action: @escaping () -> Void
) {
self.size = size
self.style = style
self.checked = checked
self.accessibilityLabel = accessibilityLabel
self.action = action
}

public var body: some View {
Button(action: action) {
BPKIconView(icon, size: size.iconSize)
.foregroundColor(color)
.scaleEffect(scale)
}
.buttonStyle(InternalSaveCardButtonStyle(onIsPressedchange: { isPressed = $0 }))
.frame(width: size.containerSize, height: size.containerSize)
.background(style.containerColor)
.clipShape(Circle())
.accessibilityLabel(accessibilityLabel)
.accessibilityAddTraits(checked ? .isSelected : [])
.onChange(of: checked) { newValue in
guard newValue else { return }
showAnimation()
}
}

private var icon: BPKIcon {
(checked || isPressed) ? .heart : .heartOutline
}

private var color: BPKColor {
switch style {
case .onDark:
return .textOnDarkColor
case .default, .contained:
return checked ? .textLinkColor: .textPrimaryColor
}
}

private func showAnimation() {
withAnimation(.linear(duration: animationDuration)) {
scale = maxScale
}
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration * 2) {
withAnimation(.linear(duration: animationDuration)) {
scale = 1.0
}
}
}
}

private struct InternalSaveCardButtonStyle: ButtonStyle {
let onIsPressedchange: (Bool) -> Void

func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed, perform: onIsPressedchange)
}
}

// MARK: - previews
struct BPKSaveCardButton_Previews: PreviewProvider {
static var previews: some View {
VStack {
HStack(spacing: .lg) {
InteractiveDemo(size: .default, style: .default)
InteractiveDemo(size: .small, style: .default)
}
.padding()

HStack(spacing: .lg) {
InteractiveDemo(size: .default, style: .contained)
InteractiveDemo(size: .small, style: .contained)

}
.padding()
.background(.surfaceHighlightColor)

HStack(spacing: .lg) {
InteractiveDemo(size: .default, style: .onDark)
InteractiveDemo(size: .small, style: .onDark)
}
.padding()
.background(.black)
}
}
}

private struct InteractiveDemo: View {
let size: BPKCardButtonSize
let style: BPKCardButtonStyle

@State private var checked: Bool = false

var body: some View {
BPKSaveCardButton(
size: size,
style: style,
checked: checked,
accessibilityLabel: "",
action: {
checked.toggle()
}
)
}
}
99 changes: 99 additions & 0 deletions Backpack-SwiftUI/CardButton/Classes/BPKShareCardButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2024 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import SwiftUI

public struct BPKShareCardButton: View {

private let size: BPKCardButtonSize
private let style: BPKCardButtonStyle
private let accessibilityLabel: String
private let action: () -> Void

public init(
size: BPKCardButtonSize,
style: BPKCardButtonStyle,
accessibilityLabel: String,
action: @escaping () -> Void
) {
self.size = size
self.style = style
self.accessibilityLabel = accessibilityLabel
self.action = action
}

public var body: some View {
Button(action: action) {
BPKIconView(.shareiOs, size: size.iconSize)
}
.buttonStyle(InternalShareCardButtonStyle(style: style))
.frame(width: size.containerSize, height: size.containerSize)
.background(style.containerColor)
.clipShape(Circle())
.accessibilityLabel(accessibilityLabel)
}
}

private struct InternalShareCardButtonStyle: ButtonStyle {
let style: BPKCardButtonStyle

func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(configuration.isPressed ? highlightedColor: defaultColor)
}

var defaultColor: BPKColor {
if case .onDark = style {
return .textOnDarkColor
}
return .textPrimaryColor
}

var highlightedColor: BPKColor {
if case .onDark = style {
return .textDisabledOnDarkColor
}
return .textLinkColor
}
}

struct BPKShareCardButton_Previews: PreviewProvider {
static var previews: some View {
VStack {
HStack(spacing: .lg) {
BPKShareCardButton(size: .default, style: .default, accessibilityLabel: "") { }
BPKShareCardButton(size: .small, style: .default, accessibilityLabel: "") { }
}
.padding()

HStack(spacing: .lg) {
BPKShareCardButton(size: .default, style: .contained, accessibilityLabel: "") { }
BPKShareCardButton(size: .small, style: .contained, accessibilityLabel: "") { }
}
.padding()
.background(.surfaceHighlightColor)

HStack(spacing: .lg) {
BPKShareCardButton(size: .default, style: .onDark, accessibilityLabel: "") { }
BPKShareCardButton(size: .small, style: .onDark, accessibilityLabel: "") { }
}
.padding()
.background(.black)
}
}
}
44 changes: 44 additions & 0 deletions Backpack-SwiftUI/CardButton/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Backpack/CardButton

[![Cocoapods](https://img.shields.io/cocoapods/v/Backpack.svg?style=flat)](https://cocoapods.org/pods/Backpack)
[![class reference](https://img.shields.io/badge/Class%20reference-iOS-blue)](https://backpack.github.io/ios/versions/latest/uikit/Classes/BPKSaveCardButton.html)
[![view on Github](https://img.shields.io/badge/Source%20code-GitHub-lightgrey)](https://github.com/Skyscanner/backpack-ios/tree/main/Backpack/CardButton)

## Default
| Day | Night |
| --- | --- |
| <img src="https://raw.githubusercontent.com/Skyscanner/backpack-ios/main/screenshots/iPhone-swiftui_card-button___all_lm.png" alt="" width="375" /> | <img src="https://raw.githubusercontent.com/Skyscanner/backpack-ios/main/screenshots/iPhone-swiftui_card-button___all_dm.png" alt="" width="375" /> |

## Usage

Backpack Card button consists of 2 classes: `BPKSaveCardButton` & `BPKShareCardButton`. It supports 3 different styles defined in `BPKCardButtonStyle` and 2 sizes defined in `BPKCardButtonSize`

Make sure you provide a localized accessibility label that matches the function of the button.


#### BPKShareCardButton
```swift
import Backpack_SwiftUI

BPKShareCardButton(
size: .default,
style: .default,
accessibilityLabel: "share"
) {
print("Button tap closure")
}
```

#### BPKSaveCardButton

```swift
BPKSaveCardButton(
size: .default,
style: .default,
checked: true,
accessibilityLabel: "save"
) {
print("Button tap closure")
}

```
Loading

0 comments on commit 0553a29

Please sign in to comment.