From 95912900387801174189c51011e49441ab6f7c3d Mon Sep 17 00:00:00 2001 From: Anton Martinsson Date: Sun, 5 Dec 2021 11:50:58 +0100 Subject: [PATCH 1/3] Adds the ability to configure at what value each gauge view should max out at, instead of always defaulting to 100. In the process, an unecessary conditional modifier has been removed. --- Sources/GaugeKit/GaugeBackView.swift | 6 ++-- Sources/GaugeKit/GaugeExtension.swift | 38 ++++++++++++++++++----- Sources/GaugeKit/GaugeIndicator.swift | 6 ++-- Sources/GaugeKit/GaugeLabelStack.swift | 15 +++------ Sources/GaugeKit/GaugeMeter.swift | 43 +++++++++++++++++--------- Sources/GaugeKit/GaugeView.swift | 36 +++++++++++---------- Sources/GaugeKit/ViewExtension.swift | 9 ------ 7 files changed, 89 insertions(+), 64 deletions(-) diff --git a/Sources/GaugeKit/GaugeBackView.swift b/Sources/GaugeKit/GaugeBackView.swift index cbfdd34..df17ede 100644 --- a/Sources/GaugeKit/GaugeBackView.swift +++ b/Sources/GaugeKit/GaugeBackView.swift @@ -22,9 +22,11 @@ public struct GaugeAdditionalInfo { */ struct GaugeBackView: View { @Binding var flipped: Bool - var additionalInfo: GaugeAdditionalInfo + let additionalInfo: GaugeAdditionalInfo var body: some View { + let flipAngle = Angle(degrees: flipped ? -180 : 0) + GeometryReader { geometry in VStack { if let preTitle = additionalInfo.preTitle { @@ -42,7 +44,7 @@ struct GaugeBackView: View { .position(x: geometry.size.width / 2, y: geometry.size.height / 2) } .opacity(flipped ? 1 : 0) - .rotation3DEffect(Angle(degrees: flipped ? -180 : 0), axis: (x: 0, y: 1, z: 0)) + .rotation3DEffect(flipAngle, axis: (x: 0, y: 1, z: 0)) .rotation3DEffect(Angle(degrees: 180), axis: (x: 0, y: 1, z: 0)) } } diff --git a/Sources/GaugeKit/GaugeExtension.swift b/Sources/GaugeKit/GaugeExtension.swift index f8d3435..98c3e0b 100644 --- a/Sources/GaugeKit/GaugeExtension.swift +++ b/Sources/GaugeKit/GaugeExtension.swift @@ -1,6 +1,6 @@ // -// File.swift -// File +// GaugeExtension.swift +// GaugeExtension // // Created by Anton Martinsson on 2021-09-03. // @@ -12,23 +12,43 @@ import SwiftUI extension GaugeView { /** - Initializes a gauge with a string, a value and an array of colors to create a background gradient from. + Initializes a gauge with a value and an array of colors to create a background gradient from. + Defaults the max value to 100. - Parameters: - title: A short title that will be displayed in the center of the gauge, below its value. - value: An integer between 0 and 100 to visualize using the gauge. - colors: An array of Color instances to create the gauge's background gradient from.. */ - init(title: String?, value: Int?, colors: [Color]) { + public init(value: Int?, colors: [Color]) { + self.title = nil + self.value = value + self.maxValue = 100 + self.colors = colors + self.additionalInfo = nil + } + + /** + Initializes a gauge with a title, a value and an array of colors to create a background gradient from. + Defaults the max value to 100. + + - Parameters: + - title: A short title that will be displayed in the center of the gauge, below its value. + - value: An integer between 0 and 100 to visualize using the gauge. + - colors: An array of Color instances to create the gauge's background gradient from.. + */ + public init(title: String?, value: Int?, colors: [Color]) { self.title = title self.value = value + self.maxValue = 100 self.colors = colors self.additionalInfo = nil } /** - Initializes a gauge with a string, a value, an array of colors to create a background gradient from, - as well as some additional information to display on the back of the gauge once it is tapped by the user.. + Initializes a gauge with a title, a value, an array of colors to create a background gradient from, + as well as some additional information to display on the back of the gauge once it is tapped by the user. + Defaults the max value to 100. - Parameters: - title: A short title that will be displayed in the center of the gauge, below its value. @@ -36,9 +56,10 @@ extension GaugeView { - colors: An array of Color instances to create the gauge's background gradient from.. - additionalInfo: A struct that contains three optional strings to display on the back of the gauge. */ - init(title: String?, value: Int?, colors: [Color], additionalInfo: GaugeAdditionalInfo) { + public init(title: String?, value: Int?, colors: [Color], additionalInfo: GaugeAdditionalInfo) { self.title = title self.value = value + self.maxValue = 100 self.colors = colors self.additionalInfo = additionalInfo } @@ -49,9 +70,10 @@ extension GaugeView { - Parameters: - colors: An array of Color instances to create the gauge's background gradient from.. */ - init(colors: [Color]) { + public init(colors: [Color]) { self.title = nil self.value = nil + self.maxValue = 0 self.colors = colors self.additionalInfo = nil } diff --git a/Sources/GaugeKit/GaugeIndicator.swift b/Sources/GaugeKit/GaugeIndicator.swift index 3c811d4..6f07f22 100644 --- a/Sources/GaugeKit/GaugeIndicator.swift +++ b/Sources/GaugeKit/GaugeIndicator.swift @@ -11,8 +11,8 @@ import SwiftUI The small circular indicator placed on top of the gauge to visualize it's current value. - Parameters: - - angle: The angle at which to place the indicator on top of the gauge. - - size: The size of the gauge being displayed. + - angle: The angle at which to place the indicator on top of the gauge. + - size: The size of the gauge being displayed. */ struct GaugeIndicator: View { var angle: Angle? @@ -36,7 +36,7 @@ struct GaugeIndicator: View { A custom ViewModifier mainly created to declutter the amount of attributes on the indicator slightly. - Parameters: - - size: The size of the gauge being displayed. + - size: The size of the gauge being displayed. */ private struct IndicatorPlacement: ViewModifier { var size: CGSize diff --git a/Sources/GaugeKit/GaugeLabelStack.swift b/Sources/GaugeKit/GaugeLabelStack.swift index 45be226..30deeaf 100644 --- a/Sources/GaugeKit/GaugeLabelStack.swift +++ b/Sources/GaugeKit/GaugeLabelStack.swift @@ -11,18 +11,18 @@ import SwiftUI A simple vertical stack of labels to be stashed within the gauge view. - Parameters: - - containerSize: The size of the gauge, as provided by a GeometryReader instance in Gauge. + - geometry: The frame of the container the label stack is contained within. - value: An integer between 0 and 100 displayed inside the gauge. - title: A title to be displayed below the value. */ struct GaugeLabelStack: View { - var containerSize: CGSize + var geometry: GeometryProxy var value: Int? var title: String? var body: some View { - let isTaller = containerSize.width < containerSize.height - let smallestDimension = isTaller ? containerSize.width : containerSize.height + let isTaller = geometry.size.width < geometry.size.height + let smallestDimension = isTaller ? geometry.size.width : geometry.size.height VStack { if let unwrappedValue = value { @@ -36,11 +36,6 @@ struct GaugeLabelStack: View { .font(.system(size: smallestDimension / 20)) } } + .position(x: geometry.size.width / 2, y: geometry.size.height / 2) } } - -struct GaugeLabelStack_Previews: PreviewProvider { - static var previews: some View { - GaugeLabelStack(containerSize: CGSize(width: 500, height: 500), value: 50, title: "Neutral") - } -} diff --git a/Sources/GaugeKit/GaugeMeter.swift b/Sources/GaugeKit/GaugeMeter.swift index 0eeb9e6..241a8a0 100644 --- a/Sources/GaugeKit/GaugeMeter.swift +++ b/Sources/GaugeKit/GaugeMeter.swift @@ -8,20 +8,29 @@ import SwiftUI /** - The circular meter of the gauge view. + The circular meter of the gauge view. - - Parameters: + - Parameters: - value: An integer between 0 and 100 displayed inside the gauge, which also determines the position of the gauge's indicator. - colors: The colors that should be used in the gradient that wipes across the gauge. + - maxValue: The value the gauge should top out at. */ struct GaugeMeter : View { - var value: Int? - var colors: [Color] + let value: Int? + let colors: [Color] + let maxValue: Int? let trimStart = 0.1 let trimEnd = 0.9 + init(value: Int? = nil, maxValue: Int? = nil, colors: [Color]) { + self.colors = colors + self.value = value + let defaultMaxValue = maxValue == nil && value != nil + self.maxValue = defaultMaxValue ? 100 : maxValue + } + var body: some View { let startAngle = 360 * trimStart let endAngle = 360 * trimEnd @@ -45,8 +54,13 @@ struct GaugeMeter : View { .shadow(color: .black.opacity(0.2), radius: 10, x: 0, y: 0) - if let unwrappedValue = value { - let degrees = (Double(unwrappedValue) * 2.88) + if let unwrappedValue = value, let unwrappedMax = maxValue { + let oneUnit = (Double(360) * 0.8) / Double(unwrappedMax) + let degrees = (Double(unwrappedValue) * oneUnit) + GaugeIndicator(angle: Angle(degrees: degrees), size: geometry.size) + } else if let unwrappedValue = value { + let oneUnit = (Double(360) * 0.8) / Double(100) + let degrees = (Double(unwrappedValue) * oneUnit) GaugeIndicator(angle: Angle(degrees: degrees), size: geometry.size) } } @@ -58,14 +72,14 @@ struct GaugeMeter : View { /** A circular view used as a mask over the angular gradient of the GaugeMeter. - Parameters: - - trimStart: A double between 0 and 1 that determines where the meter should begin. - - trimEnd: A double between 0 and 1 that determines where the meter should end. - - meterThickness: The thickness of the gauge meter. + - trimStart: A double between 0 and 1 that determines where the meter should begin. + - trimEnd: A double between 0 and 1 that determines where the meter should end. + - meterThickness: The thickness of the gauge meter. */ private struct GaugeMask: View { - var trimStart: Double - var trimEnd: Double - var meterThickness: Double + let trimStart: Double + let trimEnd: Double + let meterThickness: Double var body: some View { Circle() @@ -78,8 +92,7 @@ private struct GaugeMask: View { struct GaugeComponents_Previews: PreviewProvider { static var previews: some View { - GaugeView(title: "Speed", - value: 88, - colors: [.red, .orange, .yellow, .green]) + let colors: [Color] = [.red, .orange, .yellow, .green] + GaugeView(title: "Speed", value: 88, colors: colors) } } diff --git a/Sources/GaugeKit/GaugeView.swift b/Sources/GaugeKit/GaugeView.swift index 20662f1..e9e5d65 100644 --- a/Sources/GaugeKit/GaugeView.swift +++ b/Sources/GaugeKit/GaugeView.swift @@ -4,57 +4,61 @@ // // Created by Anton Martinsson on 2021-06-19. // -// A Gauge similar to the gauges used for some Apple Watch complication. +// A Gauge similar to the gauges used for some Apple Watch complications. import SwiftUI /** - A view ideal for visualizing a value between 0 and 100 in a gauge, + A view ideal for visualizing a value between in a gauge, not too different from the native gauges used by Apple for some Apple Watch complications. - Parameters: - title: A decriptive string value to display inside the gauge. - - value: An integer between 0 and 100 displayed inside the gauge, which also determines the position of the gauge's indicator. + - value: An integer displayed inside the gauge, which also determines the position of the gauge's indicator. + - maxValue: An integer value representing what the gauge should max out at. Defaults to nil if `value` is also nil, and to 100 if a `value` is set, but no explicit `maxValue`. - colors: The colors that should be used in the gradient that wipes across the gauge. - additionalInfo: A struct containing three (optional) strings to display when the user taps on the gauge. */ public struct GaugeView : View { @State private var flipped: Bool = false - public var title: String? - public var value: Int? - public var colors: [Color] - public var additionalInfo: GaugeAdditionalInfo? + let title: String? + let value: Int? + let maxValue: Int? + let colors: [Color] + let additionalInfo: GaugeAdditionalInfo? public init(title: String? = nil, value: Int? = nil, + maxValue: Int? = nil, colors: [Color], additionalInfo: GaugeAdditionalInfo? = nil) { self.title = title self.value = value + self.maxValue = maxValue self.colors = colors self.additionalInfo = additionalInfo } public var body: some View { + let flipAngle = Angle(degrees: flipped ? 180 : 0) + ZStack { ZStack { - GaugeMeter(value: value, colors: colors) + GaugeMeter(value: value, maxValue: maxValue, colors: colors) GeometryReader { geometry in - GaugeLabelStack(containerSize: geometry.size, value: value, title: title) - .position(x: geometry.size.width / 2, y: geometry.size.height / 2) + GaugeLabelStack(geometry: geometry, value: value, title: title) } } - .rotation3DEffect(Angle(degrees: flipped ? 180 : 0), axis: (x: 0, y: 1, z: 0)) + .rotation3DEffect(flipAngle, axis: (x: 0, y: 1, z: 0)) .opacity(flipped ? 0.1 : 1) if let info = additionalInfo { GaugeBackView(flipped: $flipped, additionalInfo: info) } } - - .if(additionalInfo != nil) { content in - content.onTapGesture { + .onTapGesture { + if additionalInfo != nil { withAnimation { self.flipped.toggle() } @@ -65,8 +69,6 @@ public struct GaugeView : View { struct Gauge_Previews: PreviewProvider { static var previews: some View { - GaugeView(title: "BTC F&G", - value: 50, - colors: [.red, .orange, .yellow, .green]) + GaugeView(value: 50, colors: [.red, .orange, .yellow, .green]) } } diff --git a/Sources/GaugeKit/ViewExtension.swift b/Sources/GaugeKit/ViewExtension.swift index b6d5447..9e9620e 100644 --- a/Sources/GaugeKit/ViewExtension.swift +++ b/Sources/GaugeKit/ViewExtension.swift @@ -79,15 +79,6 @@ extension View { } } - @ViewBuilder - func `if`(_ conditional: Bool, content: (Self) -> Content) -> some View { - if conditional { - content(self) - } else { - self - } - } - func widgetify() -> some View { self.modifier(RoundCornersAndAddShadows()) } From 3e20c11effa653dff63a12b838084172fc1c4c8c Mon Sep 17 00:00:00 2001 From: Anton Martinsson Date: Sun, 5 Dec 2021 11:58:28 +0100 Subject: [PATCH 2/3] Updates tests and README. --- README.md | 10 ++++++++-- Tests/GaugeKitTests/GaugeKitTests.swift | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e6e2ab..a53340a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GaugeKit -GaugeKit is a Swift package which enables easy, effortless creation of a gauge-like view ideal for visualizing a value between 0 and 100 in a gauge, +GaugeKit is a Swift package which enables easy, effortless creation of a gauge-like view ideal for visualizing a value in a gauge, not too different from the native gauges used by Apple for some Apple Watch complications. ## Importing GaugeKit @@ -24,7 +24,7 @@ GaugeKit is built with SwiftUI, and thus the minimum requirement to use it is th ## Usage -To create a basic Gauge is as simple as providing it with a title, an integer value between 0 and 100, and the colors that you want the gauge display along itself. For example: +To create a basic Gauge is as simple as providing it with a title, an integer value, and the colors that you want the gauge display along itself. For example: ```swift GaugeView(title: "Speed", value: 88, colors: [.red, .orange, .yellow, .green]) @@ -32,6 +32,12 @@ GaugeView(title: "Speed", value: 88, colors: [.red, .orange, .yellow, .green]) ![alt text](https://i.imgur.com/iXPEpmm.png) +A basic gauge like this will default to 100 for its max value. You can also explicitely set the max value of the gauge to a custom value. The following initialization will create a gauge that maxes out at 1000 instead of 100. + +```swift +GaugeView(title: "Speed", value: 100, maxValue: 1000, colors: [.red, .orange, .yellow, .green]) +``` + Additionally, as space for additional information is quite limited within the gauge, you can initialize a gauge with some additional information using three different (optional) strings. This information will be revealed to the user with a quick flip animation when a tap on the gauge view is recorded. ```swift diff --git a/Tests/GaugeKitTests/GaugeKitTests.swift b/Tests/GaugeKitTests/GaugeKitTests.swift index 1f49ecf..23ce474 100644 --- a/Tests/GaugeKitTests/GaugeKitTests.swift +++ b/Tests/GaugeKitTests/GaugeKitTests.swift @@ -9,21 +9,38 @@ final class GaugeKitTests: XCTestCase { XCTAssertNotNil(gauge.value) XCTAssertEqual(gauge.title, "A title") XCTAssertEqual(gauge.value, 100) + XCTAssertEqual(gauge.maxValue, 100) XCTAssertEqual(gauge.colors, [.red, .green]) + XCTAssertNil(gauge.additionalInfo) + } + + func testMaxValueInit() { + let gauge = GaugeView(title: "A title", value: 100, maxValue: 200, colors: [.red, .green]) + XCTAssertNotNil(gauge.title) + XCTAssertNotNil(gauge.value) + XCTAssertEqual(gauge.title, "A title") + XCTAssertEqual(gauge.value, 100) + XCTAssertEqual(gauge.maxValue, 200) + XCTAssertEqual(gauge.colors, [.red, .green]) + XCTAssertNil(gauge.additionalInfo) } func testNoTitleOrValue() { let gauge = GaugeView(colors: [.red, .green]) XCTAssertNil(gauge.title) XCTAssertNil(gauge.value) + XCTAssertEqual(gauge.maxValue, 0) XCTAssertNotNil(gauge.colors) + XCTAssertNil(gauge.additionalInfo) } func testNoColors() { let gauge = GaugeView(colors: []) XCTAssertNil(gauge.title) XCTAssertNil(gauge.value) + XCTAssertEqual(gauge.maxValue, 0) XCTAssertTrue(gauge.colors.isEmpty) + XCTAssertNil(gauge.additionalInfo) } func testAdditionalInfo() { @@ -34,6 +51,7 @@ final class GaugeKitTests: XCTestCase { XCTAssertEqual(gauge.title, "A title") XCTAssertEqual(gauge.value, 100) + XCTAssertEqual(gauge.maxValue, 100) XCTAssertEqual(gauge.colors, [.red, .green]) XCTAssertNotNil(gauge.additionalInfo?.preTitle) From 2243596642befc29db9df6c290c4a6b3e1b1c215 Mon Sep 17 00:00:00 2001 From: Anton Martinsson Date: Sun, 5 Dec 2021 12:02:00 +0100 Subject: [PATCH 3/3] Updates Roadmap section of README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a53340a..fafd719 100644 --- a/README.md +++ b/README.md @@ -66,4 +66,4 @@ GaugeView(colors: [.red, .orange, .yellow, .green]) ## Roadmap -While I don't have many concrete plans for GaugeKit at the moment and plan to just fiddle with it from time to time, I would like to add the ability to customize the scale instead of it being fixed between 0 and 100. Until I find the time and inspiration to do so, keep your eyes peeled on this repo. +While I don't have many concrete plans for GaugeKit at the moment and plan to just fiddle with it from time to time. If you have any feature requests or ideas you think I should take into consideration, please feel free to contact me here on GitHub.