Skip to content

Commit

Permalink
[KOA-5084] BPKStarRating SwiftUI (#1632)
Browse files Browse the repository at this point in the history
* initial setup

* Star Rating component

* addressing pr comments

* added hotel star ratings to readme
  • Loading branch information
frugoman committed Apr 14, 2023
1 parent e7d3618 commit 54349a6
Show file tree
Hide file tree
Showing 41 changed files with 833 additions and 152 deletions.
62 changes: 62 additions & 0 deletions Backpack-SwiftUI/StarRating/Classes/BPKHotelStarRating.swift
@@ -0,0 +1,62 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 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

/// A view that displays a star rating based on a rating value.
public struct BPKHotelStarRating: View {
@Binding private var rating: Int
private let size: BPKStarRatingSize

/// Initializes a new `BPKHotelStarRating` instance.
/// - Parameters:
/// - rating: A binding to the current rating value.
/// Represents the number of stars to display.
/// - size: The size of the star rating, defaults to `.small`.
public init(
rating: Binding<Int>,
size: BPKStarRatingSize = .small
) {
self._rating = rating
self.size = size
}

public var body: some View {
HStack(spacing: 0) {
ForEach(0..<rating, id: \.self) { index in
BPKStarView(type: .full, size: size.starSize)
.accessibilityHidden(true)
}
}
.accessibilityElement()
.accessibilityValue(Text(String(rating)))
}
}

struct BPKHotelStarRating_Previews: PreviewProvider {
static var previews: some View {
VStack(alignment: .leading) {
BPKText("Small", style: .heading3)
BPKHotelStarRating(rating: .constant(3))
BPKHotelStarRating(rating: .constant(4))
BPKText("Large", style: .heading3)
BPKHotelStarRating(rating: .constant(3), size: .large)
BPKHotelStarRating(rating: .constant(4), size: .large)
}
}
}
125 changes: 125 additions & 0 deletions Backpack-SwiftUI/StarRating/Classes/BPKStarRating.swift
@@ -0,0 +1,125 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 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

/// A view that displays a star rating based on a rating value.
public struct BPKStarRating: View {
/// An enum representing the rounding method to use.
/// - down: Rounds down to the nearest half star.
/// - up: Rounds up to the nearest half star.
/// - nearest: Rounds to the nearest half star.
public enum Rounding {
case down, up, nearest

func roundedRating(_ rating: Float) -> Float {
switch self {
case .down: return floor(rating * 2) / 2
case .up: return ceil(rating * 2) / 2
case .nearest: return round(rating * 2) / 2
}
}
}

@Binding private var rating: Float
private let maxRating: Int
private let size: BPKStarRatingSize
private let rounding: Rounding
private let onRatingChanged: ((Float) -> Void)?

/// Initializes a new `BPKStarRating` instance.
/// - Parameters:
/// - rating: A binding to the current rating value.
/// - maxRating: The maximum rating value allowed.
/// Represents the number of stars to display.
/// - size: The size of the star rating, defaults to `.small`.
/// - rounding: The rounding method to use, defaults to `.down`.
/// - onRatingChanged: A callback that is called when the user taps on a star.
public init(
rating: Binding<Float>,
maxRating: Int,
size: BPKStarRatingSize = .small,
rounding: Rounding = .down,
onRatingChanged: ((Float) -> Void)? = nil
) {
self._rating = rating
self.maxRating = maxRating
self.size = size
self.rounding = rounding
self.onRatingChanged = onRatingChanged
}

public var body: some View {
HStack(spacing: 0) {
ForEach(0..<maxRating, id: \.self) { index in
BPKStarView(type: starType(for: index), size: size.starSize)
.onTapGesture {
onRatingChanged?(Float(index + 1))
} .accessibilityHidden(true)
}
}
.accessibilityElement()
.accessibilityValue(Text(String(coercedRating)))
.if(onRatingChanged != nil) { view in
view.accessibilityAdjustableAction { direction in
switch direction {
case .increment: increment()
case .decrement: decrement()
@unknown default: break
}
}
}
}

private func increment() {
onRatingChanged?(Float(min(maxRating, Int(coercedRating + 1))))
}

private func decrement() {
onRatingChanged?(Float(max(0, Int(coercedRating - 1))))
}

private func starType(for index: Int) -> BPKStarView.StarType {
let clamped = max(0, min(coercedRating - Float(index), 1))
switch clamped {
case 0..<0.5: return .empty
case 0.5..<1: return .half
default: return .full
}
}

private var coercedRating: Float {
let coercedRating = max(0, min(Float(maxRating), rating))
return rounding.roundedRating(coercedRating)
}
}

struct BPKStarRating_Previews: PreviewProvider {
static var previews: some View {
VStack(alignment: .leading) {
BPKText("Small", style: .heading3)
BPKStarRating(rating: .constant(3), maxRating: 5)
BPKStarRating(rating: .constant(3.5), maxRating: 5)
BPKStarRating(rating: .constant(4), maxRating: 5)
BPKText("Large", style: .heading3)
BPKStarRating(rating: .constant(3), maxRating: 5, size: .large)
BPKStarRating(rating: .constant(3.5), maxRating: 5, size: .large)
BPKStarRating(rating: .constant(4), maxRating: 5, size: .large)
}
}
}
29 changes: 29 additions & 0 deletions Backpack-SwiftUI/StarRating/Classes/BPKStarRatingSize.swift
@@ -0,0 +1,29 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 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.
*/

/// An enum representing the size of the star rating.
public enum BPKStarRatingSize {
case small, large

var starSize: BPKIcon.Size {
switch self {
case .small: return .small
case .large: return .large
}
}
}
68 changes: 68 additions & 0 deletions Backpack-SwiftUI/StarRating/Classes/BPKStarView.swift
@@ -0,0 +1,68 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 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

struct BPKStarView: View {
enum StarType {
case empty, half, full

var iconName: BPKIcon {
switch self {
case .empty: return .starOutline
case .half: return .starHalf
case .full: return .star
}
}

var color: BPKColor {
switch self {
case .empty: return .textDisabledColor
case .half: return .statusWarningSpotColor
case .full: return .statusWarningSpotColor
}
}
}

let type: StarType
let size: BPKIcon.Size

var body: some View {
BPKIconView(type.iconName, size: size)
.foregroundColor(type.color)
}
}

struct BPKStarView_Previews: PreviewProvider {
static var previews: some View {
VStack{
HStack {
BPKText("Large", style: .heading3)
BPKStarView(type: .empty, size: .large)
BPKStarView(type: .half, size: .large)
BPKStarView(type: .full, size: .large)
}
HStack {
BPKText("Small", style: .heading3)
BPKStarView(type: .empty, size: .small)
BPKStarView(type: .half, size: .small)
BPKStarView(type: .full, size: .small)
}
}
}
}
52 changes: 52 additions & 0 deletions Backpack-SwiftUI/StarRating/README.md
@@ -0,0 +1,52 @@
# Backpack-SwiftUI/StarRating

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

| Day | Night |
| --- | --- |
| <img src="https://raw.githubusercontent.com/Skyscanner/backpack-ios/main/screenshots/iPhone-swiftui_star-rating___all_lm.png" alt="" width="375" /> |<img src="https://raw.githubusercontent.com/Skyscanner/backpack-ios/main/screenshots/iPhone-swiftui_star-rating___all_dm.png" alt="" width="375" /> |

## Usage

To use the star rating component in your code you will need to bind the `rating` property and set the `maxRating` property. The `maxRating` is the maximum number of stars that can be displayed.

### Basic star rating

```swift
@State var rating: Float = 3.5
BPKStarRating(rating: $rating, maxRating: 5)
```

### Interactive star rating

```swift
@State var rating: Float = 3.5
BPKStarRating(rating: $rating, maxRating: 5) { selectedRating in
rating = selectedRating
}
```

### Hotel star rating

```swift
@State var hotelRating: Int = 3
BPKHotelStarRating(rating: $hotelRating)
```

### Star rating with Large size

```swift
@State var rating: Float = 3.5
BPKStarRating(rating: $rating, maxRating: 5, size: .large)
```

### Customise Rating Rounding

Rounding can be customised by setting the `rounding` property. The default is `.down`.

```swift
@State var rating: Float = 3.5
BPKStarRating(rating: $rating, maxRating: 5, rounding: .down)
```

0 comments on commit 54349a6

Please sign in to comment.