Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-2707] SwiftUI RatingControl Enhancement #794

Merged
merged 4 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

120 changes: 67 additions & 53 deletions Sources/FioriSwiftUICore/DataTypes/RatingControl+DataType.swift
Original file line number Diff line number Diff line change
@@ -1,38 +1,53 @@
import FioriThemeManager
import SwiftUI

public extension RatingControl {
/**
The available styles for the `FUIRatingControl`.
The available styles for the `RatingControl`.
*/
enum Style {
/**
Editable style.

Each rating star is a SF Symbol body light style (large scale) with tint color.
Each rating star is large scale with tint color.
This is the default style.
*/
case editable

/**
Disabled editable style.

Each rating star is the same as `Editable` style but with grey color.
Each rating star is the same as `Editable` style but with grey color. User interaction is disabled.
*/
case editableDisabled

/**
Standard style.

This `FUIRatingControl` is read-only. Each rating star is a SF Symbol body light style (small scale).
This `RatingControl` is read-only. Each rating star is in small scale.
*/
case standard

/**
Standard large style.

This `RatingControl` is read-only. Each rating star is in large scale.
*/
case standardLarge
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to use the Bool variable isLarge with the UIKit FUIRatingControl. By default, it is set to false for the read-only state and always true for the editable and disabled states. This approach is more simple to me when a smaller size is needed for the editable or disabled states, or for any new states that may be introduced in the future. For us, this means that we do not need to add too many styles, and for the App team, it makes it easy to understand that there are two sizes for each state.
I just want to let you know about the difference between your API and mine. However, I am definitely okay with your implementation for these two new styles here for SWiftUI Rating Control.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JunSong-SH I was originally using the isLarge property. However, later I removed it to reduce the number of properties. I think there are too many properties already.


/**
Accented read-only style.

This `FUIRatingControl` is read-only with accented color. Each rating star is the same as in `standard` style.
This `RatingControl` is read-only. Each rating star is in small scale with accented color.
*/
case accented

/**
Accented read-only style.

This `RatingControl` is read-only. Each rating star is in large scale with accented color.
*/
case accentedLarge
}

internal static func getAccessibilityLabelString(_ rating: Int, ratingBounds: ClosedRange<Int>) -> String {
Expand All @@ -45,6 +60,7 @@ extension RatingControlConfiguration {
struct RatingItem: Identifiable {
public let id = UUID()
let isOn: Bool
let isHalf: Bool
}

func ratingItems(_ rating: Int) -> [RatingItem] {
Expand All @@ -53,80 +69,78 @@ extension RatingControlConfiguration {
guard i != self.ratingBounds.upperBound else {
continue
}
items.append(RatingItem(isOn: i < rating))
items.append(RatingItem(isOn: i < rating, isHalf: false))
}
return items
}

func getOnColor() -> Color {
if let onColor {
return onColor
func ratingItems(_ averageRating: CGFloat) -> [RatingItem] {
var items: [RatingItem] = []
for i in self.ratingBounds {
guard i != self.ratingBounds.upperBound else {
continue
}
let diff = averageRating - CGFloat(i)
if diff < 0.25 {
items.append(RatingItem(isOn: false, isHalf: false))
} else if diff < 0.75 {
items.append(RatingItem(isOn: false, isHalf: true))
} else {
items.append(RatingItem(isOn: true, isHalf: false))
}
}
return items
}

func getOnColor() -> Color {
switch self.ratingControlStyle {
case .editable:
return .preferredColor(.tintColor)
case .editableDisabled:
return .preferredColor(.quaternaryLabel)
case .standard:
case .standard, .standardLarge:
return .preferredColor(.tertiaryLabel)
case .accented:
return .preferredColor(.mango3)
case .accented, .accentedLarge:
return .preferredColor(.mango8)
}
}

func getOffColor() -> Color {
if let offColor {
return offColor
}
switch self.ratingControlStyle {
case .editable:
return .preferredColor(.tintColor)
case .editableDisabled:
return .preferredColor(.quaternaryLabel)
case .standard:
case .standard, .standardLarge:
return .preferredColor(.tertiaryLabel)
case .accented:
return .preferredColor(.mango4)
case .accented, .accentedLarge:
return .preferredColor(.mango8)
}
}

func getOnImageView() -> some View {
self.getOnImage()
.resizable()
.scaledToFit()
.frame(width: self.getItemSize().width, height: self.getItemSize().height)
.font(.body)
.fontWeight(.light)
.imageScale(self.getScale())
.foregroundColor(self.getOnColor())
}

func getOffImageView() -> some View {
self.getOffImage()
.resizable()
.scaledToFit()
.frame(width: self.getItemSize().width, height: self.getItemSize().height)
.font(.body)
.fontWeight(.light)
.imageScale(self.getScale())
.foregroundColor(self.getOffColor())
}

func getOnImage() -> Image {
let image: Image = (onImage ?? Image(systemName: "star.fill"))
.renderingMode(.template)
return image
func getDefaultLabelFont() -> Font {
switch ratingControlStyle {
case .editable, .editableDisabled, .standardLarge, .accentedLarge:
return .fiori(forTextStyle: .body)
case .standard, .accented:
return .fiori(forTextStyle: .subheadline)
}
}

func getOffImage() -> Image {
let image: Image = (offImage ?? Image(systemName: "star"))
.renderingMode(.template)
return image
func getDefaultLabelColor() -> Color {
switch ratingControlStyle {
case .editable:
return .preferredColor(.primaryLabel)
case .editableDisabled:
return .preferredColor(.quaternaryLabel)
case .standard, .standardLarge, .accented, .accentedLarge:
return .preferredColor(.tertiaryLabel)
}
}

func getScale() -> Image.Scale {
switch self.ratingControlStyle {
case .editable, .editableDisabled:
case .editable, .editableDisabled, .standardLarge, .accentedLarge:
return .large
case .standard, .accented:
return .small
Expand All @@ -138,7 +152,7 @@ extension RatingControlConfiguration {
return itemSize
}
switch self.ratingControlStyle {
case .editable, .editableDisabled:
case .editable, .editableDisabled, .standardLarge, .accentedLarge:
return CGSize(width: 28, height: 28)
case .standard, .accented:
return CGSize(width: 16, height: 16)
Expand All @@ -150,10 +164,10 @@ extension RatingControlConfiguration {
return interItemSpacing
}
switch self.ratingControlStyle {
case .editable, .editableDisabled:
return CGFloat(4)
case .editable, .editableDisabled, .standardLarge, .accentedLarge:
return CGFloat(8)
case .standard, .accented:
return CGFloat(2)
return CGFloat(6)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,36 @@ protocol _AvatarsTitleComponent {
// sourcery: @ViewBuilder
var avatarsTitle: AttributedString? { get }
}

// sourcery: BaseComponent
protocol _ReviewCountLabelComponent {
// sourcery: @ViewBuilder
var reviewCountLabel: AttributedString? { get }
}

// sourcery: BaseComponent
// sourcery: importFrameworks = ["FioriThemeManager"]
protocol _OnStarImageComponent {
/// The image to be used for "On" rating star.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

// sourcery: @ViewBuilder
// sourcery: defaultValue = "FioriIcon.actions.favorite.renderingMode(.template).resizable()"
var onStarImage: Image { get }
}

// sourcery: BaseComponent
// sourcery: importFrameworks = ["FioriThemeManager"]
protocol _OffStarImageComponent {
//// The image to be used for "Off" rating star.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

// sourcery: @ViewBuilder
// sourcery: defaultValue = "FioriIcon.actions.unfavorite.renderingMode(.template).resizable()"
var offStarImage: Image { get }
}

// sourcery: BaseComponent
// sourcery: importFrameworks = ["FioriThemeManager"]
protocol _HalfStarImageComponent {
//// The image to be used for "half" rating star.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned Doc Comment Violation: A doc comment should be attached to a declaration. (orphaned_doc_comment)

// sourcery: @ViewBuilder
// sourcery: defaultValue = "FioriIcon.actions.halfStar.renderingMode(.template).resizable()"
var halfStarImage: Image { get }
}
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ protocol _BannerMessageComponent: _IconComponent, _TitleComponent, _CloseActionC
/// The default "On" image is a filled star while the default "Off" inmage
/// is an unfilled star.
// sourcery: CompositeComponent
protocol _RatingControlComponent {
// sourcery: importFrameworks = ["FioriThemeManager"]
protocol _RatingControlComponent: _ValueLabelComponent, _OnStarImageComponent, _OffStarImageComponent, _HalfStarImageComponent, _ReviewCountLabelComponent {
// sourcery: @Binding
/// The rating value.
var rating: Int { get }
Expand All @@ -308,23 +309,48 @@ protocol _RatingControlComponent {
// sourcery: defaultValue = 0...5
var ratingBounds: ClosedRange<Int> { get }

/// The custom image to be used for "On".
var onImage: Image? { get }

/// The custom image to be used for "Off".
var offImage: Image? { get }

/// The custom fixed size of each item image view.
var itemSize: CGSize? { get }

/// The custom color for the ON image.
var onColor: Color? { get }

/// The custom color for the OFF image.
var offColor: Color? { get }

/// The custom spacing between images.
var interItemSpacing: CGFloat? { get }

/// The rating format for displaying rating value.
/// When this is `nil`, the default format is "%d of %d" where "of" is the localized "of". The first parameter is the rating value while the second parameter is the total number of stars.
// sourcery: defaultValue = "nil"
var ratingValueFormat: String? { get }

/// This property indicates if the value label is to be displayed or not. The default value is `false` for backward compatibility.
// sourcery: defaultValue = "false"
var showsValueLabel: Bool { get }

/// The average rating for read-only style.
// sourcery: defaultValue = "nil"
var averageRating: CGFloat? { get }

/// The format for display the average rating. The default is "%.1f"
// sourcery: defaultValue = ""%.1f""
var averageRatingFormat: String { get }

/// The number of reviews.
// sourcery: defaultValue = "nil"
var reviewCount: Int? { get }

/// The format for the review count string. The default is "%d reviews" where "reviews" is the localized "reviews" string.
// sourcery: defaultValue = "nil"
var reviewCountFormat: String? { get }

/// The ceiling number to be displayed for review count. If the `reviewCount` is larger than this number, this number will be displayed with a "+" sign after the number.
// sourcery: defaultValue = "nil"
var reviewCountCeiling: Int? { get }

/// The format for the review count string when the count is over the ceiling. The default is "%d+ reviews" where "reviews" is the localized "reviews" string.
// sourcery: defaultValue = "nil"
var reviewCountCeilingFormat: String? { get }

/// This property indicates if the review count label is to be displayed or not. The default value is `false` for backward compatibility.
// sourcery: defaultValue = "false"
var showsReviewCountLabel: Bool { get }
}

/// `TimelineMarker` is a non-selectable view intended for timelineMarkers that require beforeStart, start, beforeEnd and end status that displays timelineMarker details.
Expand Down Expand Up @@ -392,7 +418,8 @@ protocol _TimelineNowIndicatorComponent: _NowIndicatorNodeComponent {}

/// The form view which contains a title, rating control, and a subtitle
// sourcery: CompositeComponent
protocol _RatingControlFormViewComponent: _TitleComponent, _RatingControlComponent, _SubtitleComponent, _FormViewComponent {
// sourcery: importFrameworks = ["FioriThemeManager"]
protocol _RatingControlFormViewComponent: _TitleComponent, _RatingControlComponent, _SubtitleComponent {
/// Indicates if the axis for displaying the title and rating control.
// sourcery: defaultValue = .horizontal
var axis: Axis { get }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import FioriThemeManager
import Foundation
import SwiftUI

// Base Layout style
public struct HalfStarImageBaseStyle: HalfStarImageStyle {
@ViewBuilder
public func makeBody(_ configuration: HalfStarImageConfiguration) -> some View {
// Add default layout here
configuration.halfStarImage
}
}

// Default fiori styles
public struct HalfStarImageFioriStyle: HalfStarImageStyle {
@ViewBuilder
public func makeBody(_ configuration: HalfStarImageConfiguration) -> some View {
HalfStarImage(configuration)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import FioriThemeManager
import Foundation
import SwiftUI

// Base Layout style
public struct OffStarImageBaseStyle: OffStarImageStyle {
@ViewBuilder
public func makeBody(_ configuration: OffStarImageConfiguration) -> some View {
// Add default layout here
configuration.offStarImage
}
}

// Default fiori styles
public struct OffStarImageFioriStyle: OffStarImageStyle {
@ViewBuilder
public func makeBody(_ configuration: OffStarImageConfiguration) -> some View {
OffStarImage(configuration)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import FioriThemeManager
import Foundation
import SwiftUI

// Base Layout style
public struct OnStarImageBaseStyle: OnStarImageStyle {
@ViewBuilder
public func makeBody(_ configuration: OnStarImageConfiguration) -> some View {
// Add default layout here
configuration.onStarImage
}
}

// Default fiori styles
public struct OnStarImageFioriStyle: OnStarImageStyle {
@ViewBuilder
public func makeBody(_ configuration: OnStarImageConfiguration) -> some View {
OnStarImage(configuration)
}
}
Loading
Loading