Created
May 5, 2021 03:29
-
-
Save gpdawson/1fd61f0021c67ec6d6b42f5bb9c19cae to your computer and use it in GitHub Desktop.
Similar to watchOS gauges but for iOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// GaugeView.swift | |
// WidgetTest | |
// | |
// Created by Graham Dawson on 26/9/20. | |
// | |
import SwiftUI | |
struct GaugeView: View { | |
var gradientColors: [Color] | |
var minLabel: String = "" | |
var maxLabel: String = "" | |
var typeLabel: String = "RH" | |
var valueLabel: String = "71%" | |
var valueRatio: CGFloat = 0.71 | |
var valueColor: Color = Color.primary | |
let trimRatio: CGFloat = 0.75 | |
let ringWidthRatio: CGFloat = 0.18 | |
var isProgressGauge = false | |
var ringColor: Color = Color.gray | |
var ringColorComplete = Color.black | |
var backgroundColor = userBackgroundColor // For dial progress ring | |
@ViewBuilder | |
var body: some View { | |
GeometryReader { geometry in | |
let center = CGPoint(x: geometry.size.width/2.0, y:geometry.size.height/2.0) | |
let extent = min(geometry.size.width, geometry.size.height) / 2.0 | |
let ringWidth: CGFloat = extent * ringWidthRatio // 30 | |
let radius = CGFloat(extent - ringWidth/2) | |
let valuePosition = positionOfValueRatio(center: center, radius: radius) | |
let trimFrom: CGFloat = (1.0 - trimRatio) * 0.5 | |
let trimTo: CGFloat = 1.0 - trimFrom | |
ZStack { | |
if (isProgressGauge) { | |
Circle() | |
.trim(from: trimFrom, to: trimTo) | |
.stroke( | |
ringColor, | |
style: StrokeStyle(lineWidth: ringWidth, lineCap: .round) | |
) | |
.rotationEffect(.degrees(90)) | |
.padding(ringWidth / 2) | |
let trimToB = trimTo - (1-valueRatio) * trimRatio | |
Circle() | |
.trim(from: trimFrom, to: trimToB) | |
.stroke( | |
ringColorComplete, | |
style: StrokeStyle(lineWidth: ringWidth, lineCap: .round) | |
) | |
.rotationEffect(.degrees(90)) | |
.padding(ringWidth / 2) | |
} else { | |
Circle() | |
.trim(from: trimFrom, to: trimTo) | |
.stroke( | |
AngularGradient( | |
gradient: Gradient(stops: gradientStopsFromColors(colors: gradientColors)), | |
center: .center, | |
startAngle: .degrees(0), | |
endAngle: .degrees(360) | |
), | |
style: StrokeStyle(lineWidth: ringWidth, lineCap: .round) | |
) | |
.rotationEffect(.degrees(90)) | |
.padding(ringWidth / 2) | |
} | |
if (minLabel.count > 0) { | |
Text(minLabel) | |
.position(x: center.x - extent * 0.35, y: center.y + extent * 0.84) | |
.font(Font.system(size: extent * 0.25, weight: .bold, design: .rounded)) | |
.foregroundColor(gradientColors.first) | |
} | |
if (maxLabel.count > 0) { | |
Text(maxLabel) | |
.position(x: center.x + extent * 0.35, y: center.y + extent * 0.84) | |
.font(Font.system(size: extent * 0.25, weight: .bold, design: .rounded)) | |
.foregroundColor(gradientColors.last) | |
} | |
if (typeLabel.count > 0) { | |
Text(typeLabel) | |
.padding(EdgeInsets(top: 0, leading: extent * 0.3, bottom: 0, trailing: extent * 0.3)) | |
.position(x: center.x, y: center.y + extent * 0.72) | |
.font(Font.system(size: extent * 0.3, weight: .bold, design: .default)) | |
.lineLimit(1) | |
.minimumScaleFactor(0.1) | |
} | |
if (valueLabel.count > 0) { | |
Text(valueLabel) | |
.foregroundColor(valueColor) | |
.frame(maxWidth: extent * 1.3) | |
.font(Font.system(size: extent * 0.6, weight: .bold, design: .rounded)) | |
.lineLimit(1) | |
.minimumScaleFactor(0.01) | |
} | |
} | |
if (!isProgressGauge) { | |
//let color = Color(UIColor.systemBackground) | |
GaugeValueHighlightView(color:backgroundColor, radiusInner: ringWidth, radiusOuter: ringWidth * 1.8) | |
.position(x: valuePosition.x, y: valuePosition.y) | |
} | |
} | |
} | |
func positionOfValueRatio(center: CGPoint, radius: CGFloat) -> CGPoint { | |
let valueAngleDeg = -valueRatio * 360.0 * trimRatio - 45.0 | |
let valueAngleRad = Double(valueAngleDeg) * Double.pi / 180.0 | |
let xCoord = CGFloat(sin(valueAngleRad)) * radius | |
let yCoord = CGFloat(cos(valueAngleRad)) * radius | |
let valuePoint: CGPoint = CGPoint(x: center.x + xCoord, y: center.y + yCoord) | |
return valuePoint | |
} | |
func gradientStopsFromColors(colors: [Color]) -> [Gradient.Stop] { | |
var stops: [Gradient.Stop] = [] | |
stops.append(Gradient.Stop(color: colors[0], location: 0.0)) | |
for (index, color) in colors.enumerated() { | |
var loc : CGFloat = CGFloat(index) / CGFloat(colors.count-1) | |
// Compress into narrower range about 0.5 | |
loc = (loc - 0.5) * trimRatio + 0.5 | |
stops.append(Gradient.Stop(color: color, location: loc)) | |
} | |
stops.append(Gradient.Stop(color: colors[colors.count-1], location: 1.0)) | |
return stops | |
} | |
} | |
struct GaugeValueHighlightView: View { | |
// By default color is system background color - if background is non-standard then set it on this inut param | |
var color: Color = Color(UIColor.systemBackground) | |
var radiusInner: CGFloat = 30 | |
var radiusOuter: CGFloat = 50 | |
var body: some View { | |
GeometryReader { geometry in | |
let radiusSpan: CGFloat = (radiusOuter - radiusInner) / 2.0; | |
let radiusMid: CGFloat = (radiusInner + radiusOuter) / 2.0 | |
Circle() | |
.stroke( | |
style: StrokeStyle(lineWidth: radiusSpan, lineCap: .round) | |
) | |
.frame(width: radiusMid, height: radiusMid, alignment: .center) | |
.position(x: geometry.size.width/2, y: geometry.size.height/2) | |
.foregroundColor(color) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample previews of gauges using above code