Skip to content

Commit

Permalink
feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-2631] SwiftUI RatingControl (#732)
Browse files Browse the repository at this point in the history
* feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-2631] SwiftUI RatingControl

SwiftUI migration for RatingControl

* feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-2631] Add header doc

Adding header doc for RatingControl

* feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-2631] address review comments
  • Loading branch information
janhuachu authored Jul 1, 2024
1 parent e3de493 commit a7c41dd
Show file tree
Hide file tree
Showing 15 changed files with 665 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Apps/Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
99B6EF8C2672224D00515E8E /* UserConsentSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B6EF8B2672224C00515E8E /* UserConsentSample.swift */; };
9D0086692BA8F6820004BE15 /* TitleFormViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0086672BA8F6810004BE15 /* TitleFormViewExample.swift */; };
9D00866A2BA8F6820004BE15 /* TextFieldFormViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0086682BA8F6820004BE15 /* TextFieldFormViewExample.swift */; };
9D057DAB2C2F260200F5331C /* RatingControlExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D057DAA2C2F260200F5331C /* RatingControlExample.swift */; };
9D0B26082B9BA5C0004278A5 /* FormViewExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0B26042B9BA5C0004278A5 /* FormViewExamples.swift */; };
9D0B26092B9BA5C0004278A5 /* KeyValueFormViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0B26052B9BA5C0004278A5 /* KeyValueFormViewExample.swift */; };
9D0B260A2B9BA5C0004278A5 /* NoteFormViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0B26062B9BA5C0004278A5 /* NoteFormViewExample.swift */; };
Expand Down Expand Up @@ -254,6 +255,7 @@
99B6EF8B2672224C00515E8E /* UserConsentSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConsentSample.swift; sourceTree = "<group>"; };
9D0086672BA8F6810004BE15 /* TitleFormViewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleFormViewExample.swift; sourceTree = "<group>"; };
9D0086682BA8F6820004BE15 /* TextFieldFormViewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldFormViewExample.swift; sourceTree = "<group>"; };
9D057DAA2C2F260200F5331C /* RatingControlExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingControlExample.swift; sourceTree = "<group>"; };
9D0B26042B9BA5C0004278A5 /* FormViewExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormViewExamples.swift; sourceTree = "<group>"; };
9D0B26052B9BA5C0004278A5 /* KeyValueFormViewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyValueFormViewExample.swift; sourceTree = "<group>"; };
9D0B26062B9BA5C0004278A5 /* NoteFormViewExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteFormViewExample.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -611,6 +613,7 @@
9D0B26062B9BA5C0004278A5 /* NoteFormViewExample.swift */,
9D0086682BA8F6820004BE15 /* TextFieldFormViewExample.swift */,
9D0086672BA8F6810004BE15 /* TitleFormViewExample.swift */,
9D057DAA2C2F260200F5331C /* RatingControlExample.swift */,
);
path = FormViews;
sourceTree = "<group>";
Expand Down Expand Up @@ -947,6 +950,7 @@
8A5579CC24C1293C0098003A /* SettingsColorForCategory.swift in Sources */,
C18868D12B32535100F865F7 /* SearchFontAndColor.swift in Sources */,
9D0B26092B9BA5C0004278A5 /* KeyValueFormViewExample.swift in Sources */,
9D057DAB2C2F260200F5331C /* RatingControlExample.swift in Sources */,
8A557A1A24C12C820098003A /* ChartsContentView.swift in Sources */,
8A5579CE24C1293C0098003A /* SettingColor.swift in Sources */,
1F55FEF32AC941FF00D7A1BE /* View+Extensions.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ struct FormViewExamples: View {
{
Text("TextFieldFormView Example")
}
NavigationLink(
destination: RatingControlExample())
{
Text("RatingControl Example")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import FioriSwiftUICore
import SwiftUI

struct RatingControlExample: View {
@State var rating1: Int = 1

@State var rating2: Int = 2

@State var rating3: Int = 3

@State var rating4: Int = 4

@State var rating5: Int = 1

@State var rating6: Int = 2

@State var rating7: Int = 3

@State var rating8: Int = 4

@State var rating9: Int = 1

@State var rating10: Int = 2

@State var rating11: Int = 3

@State var rating12: Int = 4

@State var rating13: Int = 1

@State var rating14: Int = 2

@State var rating15: Int = 3

@State var rating16: Int = 4

@State var rating17: Int = 1

@State var rating18: Int = 2

@State var rating19: Int = 3

@State var rating20: Int = 4

var body: some View {
List {
Text("RatingControl Default Example")
RatingControl(rating: self.$rating1, ratingControlStyle: .editable)
RatingControl(rating: self.$rating2, ratingControlStyle: .editableDisabled)
RatingControl(rating: self.$rating3, ratingControlStyle: .standard)
RatingControl(rating: self.$rating4, ratingControlStyle: .accented)

Text("Custom Color Example")
RatingControl(rating: self.$rating5, ratingControlStyle: .editable, onColor: .red, offColor: .yellow)
RatingControl(rating: self.$rating6, ratingControlStyle: .editableDisabled, onColor: .orange, offColor: .yellow)
RatingControl(rating: self.$rating7, ratingControlStyle: .standard, onColor: .brown, offColor: .yellow)
RatingControl(rating: self.$rating8, ratingControlStyle: .accented, onColor: .black, offColor: .yellow)

Text("Larger Size Example")
RatingControl(rating: self.$rating9, ratingControlStyle: .editable, itemSize: CGSize(width: 50, height: 50))
RatingControl(rating: self.$rating10, ratingControlStyle: .editableDisabled, itemSize: CGSize(width: 40, height: 40))
RatingControl(rating: self.$rating11, ratingControlStyle: .standard, itemSize: CGSize(width: 10, height: 10))
RatingControl(rating: self.$rating12, ratingControlStyle: .accented, itemSize: CGSize(width: 5, height: 5))

Text("Custom Image Example")
RatingControl(rating: self.$rating13, ratingControlStyle: .editable, onImage: Image(systemName: "hand.thumbsup.fill"), offImage: Image(systemName: "hand.thumbsdown.fill"))
RatingControl(rating: self.$rating14, ratingControlStyle: .editableDisabled, onImage: Image(systemName: "hand.thumbsup.fill"), offImage: Image(systemName: "hand.thumbsdown.fill"))
RatingControl(rating: self.$rating15, ratingControlStyle: .standard, onImage: Image(systemName: "hand.thumbsup.fill"), offImage: Image(systemName: "hand.thumbsdown.fill"))
RatingControl(rating: self.$rating16, ratingControlStyle: .accented, onImage: Image(systemName: "hand.thumbsup.fill"), offImage: Image(systemName: "hand.thumbsdown.fill"))

Text("Custom Number of Stars Example")
RatingControl(rating: self.$rating17, ratingControlStyle: .editable, ratingBounds: -5 ... 5)
RatingControl(rating: self.$rating18, ratingControlStyle: .editableDisabled, ratingBounds: -5 ... 5)
RatingControl(rating: self.$rating19, ratingControlStyle: .standard, ratingBounds: -5 ... 5)
RatingControl(rating: self.$rating20, ratingControlStyle: .accented, ratingBounds: -5 ... 5)
}
}
}

#Preview {
RatingControlExample()
}
166 changes: 166 additions & 0 deletions Sources/FioriSwiftUICore/DataTypes/RatingControl+DataType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import SwiftUI

public extension RatingControl {
/**
The available style for the `FUIRatingControl`.
*/
enum Style {
/**
Editable style.
Each rating star is a SF Symbol body light style (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.
*/
case editableDisabled

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

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

extension RatingControlConfiguration {
struct RatingItem: Identifiable {
public let id = UUID()
let isOn: Bool
}

func ratingItems(_ rating: Int) -> [RatingItem] {
var items: [RatingItem] = []
for i in self.ratingBounds {
guard i != self.ratingBounds.upperBound else {
continue
}
items.append(RatingItem(isOn: i < rating))
}
return items
}

func getOnColor() -> Color {
if let onColor {
return onColor
}
switch self.ratingControlStyle {
case .editable:
return .preferredColor(.tintColor)
case .editableDisabled:
return .preferredColor(.quaternaryLabel)
case .standard:
return .preferredColor(.tertiaryLabel)
case .accented:
return .preferredColor(.mango3)
}
}

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

func getOnImageView() -> some View {
self.getOnImage()
.resizable(resizingMode: .stretch)
.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(resizingMode: .stretch)
.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 getOffImage() -> Image {
let image: Image = (offImage ?? Image(systemName: "star"))
.renderingMode(.template)
return image
}

func getScale() -> Image.Scale {
switch self.ratingControlStyle {
case .editable, .editableDisabled:
return .large
case .standard, .accented:
return .small
}
}

func getItemSize() -> CGSize {
if let itemSize {
return itemSize
}
switch self.ratingControlStyle {
case .editable, .editableDisabled:
return CGSize(width: 28, height: 28)
case .standard, .accented:
return CGSize(width: 16, height: 16)
}
}

func getItemSpacing() -> CGFloat {
if let interItemSpacing {
return interItemSpacing
}
switch self.ratingControlStyle {
case .editable, .editableDisabled:
return CGFloat(4)
case .standard, .accented:
return CGFloat(2)
}
}

func getRatingValue(_ location: CGPoint) -> Int {
let x = location.x
if x <= 0 {
return self.ratingBounds.lowerBound
}
let itemWidth = self.getItemSize().width + self.getItemSpacing()
let n = Int(location.x / itemWidth) + 1
if n >= self.ratingBounds.count {
return self.ratingBounds.upperBound
} else {
return self.ratingBounds.lowerBound + n
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,41 @@ protocol _BannerMessageComponent: _TitleComponent, _CloseActionComponent, _TopDi
/// The action to be performed when the banner is tapped.
var bannerTapAction: (() -> Void)? { get }
}

/// `RatingControl` uses images to represent a rating.
///
/// The number of "On" images denotes the rating.
/// The default "On" image is a filled star while the default "Off" inmage
/// is an unfilled star.
// sourcery: CompositeComponent
protocol _RatingControlComponent {
// sourcery: @Binding
/// The rating value.
var rating: Int { get }

/// The style of this `RatingControl`.
// sourcery: defaultValue = ".editable"
var ratingControlStyle: RatingControl.Style { get }

/// The range of the rating values. The default is `0...5`.
// 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 }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import FioriThemeManager
import Foundation
import SwiftUI

// Base Layout style
public struct RatingControlBaseStyle: RatingControlStyle {
public func makeBody(_ configuration: RatingControlConfiguration) -> some View {
HStack(spacing: configuration.getItemSpacing()) {
ForEach(configuration.ratingItems(configuration.rating)) { ratingItem in
if ratingItem.isOn {
configuration.getOnImageView()
} else {
configuration.getOffImageView()
}
}
}
.onTapGesture { location in
if configuration.ratingControlStyle == .editable {
self.setRatingValue(configuration, location: location)
}
}
.gesture(
DragGesture(minimumDistance: 0.5)
.onChanged { value in
if configuration.ratingControlStyle == .editable {
self.setRatingValue(configuration, location: value.location)
}
}
)
}

func setRatingValue(_ configuration: RatingControlConfiguration, location: CGPoint) {
let newValue = configuration.getRatingValue(location)
if configuration.rating != newValue {
configuration.rating = newValue
}
}
}

// Default fiori styles
extension RatingControlFioriStyle {
struct ContentFioriStyle: RatingControlStyle {
func makeBody(_ configuration: RatingControlConfiguration) -> some View {
RatingControl(configuration)
// Add default style for its content
// .background()
}
}
}
Loading

0 comments on commit a7c41dd

Please sign in to comment.