I have just recently started using Swiftui and have the following question:
I would like to convert the current time to an analog view. But when I use the code below, I always get an error message at the angle. What is the reason that it does not work?
struct Hand: Shape {
let inset: CGFloat
let angle: Angle
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: inset, dy: inset)
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addRoundedRect(in: CGRect(x: rect.midX - 4, y: rect.midY - 4, width: 8, height: 8), cornerSize: CGSize(width: 8, height: 8))
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: position(for: CGFloat(angle.radians), in: rect))
return path
}
private func position(for angle: CGFloat, in rect: CGRect) -> CGPoint {
let angle = angle - (.pi/2)
let radius = min(rect.width, rect.height)/2
let xPosition = rect.midX + (radius * cos(angle))
let yPosition = rect.midY + (radius * sin(angle))
return CGPoint(x: xPosition, y: yPosition)
}
}
struct TickHands: View {
#State private var currentDate = Date()
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Hand(inset: 50, angle: currentDate.hourAngle)
.stroke(lineWidth: 4)
.foregroundColor(.black)
Hand(inset: 22, angle: currentDate.minuteAngle)
.stroke(lineWidth: 4)
.foregroundColor(.black)
Hand(inset: 10, angle: currentDate.secondAngle)
.stroke(lineWidth: 2)
.foregroundColor(.gray)
}
.onReceive(timer) { (input) in
self.currentDate = input
}}}
Based on the link you provided, It looks like the author of the article did not provide some extensions.
Add the following extensions to your code (preferable in a new file), and you should be good 🚀:
extension Date {
var hourAngle: Angle {
return Angle (degrees: (360 / 12) * (self.hour + self.minutes / 60))
}
var minuteAngle: Angle {
return Angle(degrees: (self.minutes * 360 / 60))
}
var secondAngle: Angle {
return Angle (degrees: (self.seconds * 360 / 60))
}
}
extension Date {
var hour: Double {
return Double(Calendar.current.component(.hour, from: self))
}
var minutes: Double {
return Double(Calendar.current.component(.minute, from: self))
}
var seconds: Double {
return Double(Calendar.current.component(.second, from: self))
}
}
Related
I want to add a bottom gradient in my line graph like in the picture. I have the lines plotted out but I am not sure how to actually add the gradient. I was able to do this in UIKit but am not sure how to replicate it for SwiftUI.
SwiftUI Line Plot View:
var endingBalanceChart: some View {
GeometryReader { geometry in
Path { path in
for index in viewModel.endingBalance.indices {
let xPosition: CGFloat = geometry.size.width / CGFloat(viewModel.endingBalance.count) * CGFloat(index + 1)
let yAxis: CGFloat = maxY - minY
let maxYPosition: CGFloat = (1 - CGFloat((Double(viewModel.endingBalance[index].y) - minY) / yAxis)) * geometry.size.height
let yPosition: CGFloat = index == 0 ? 200 : maxYPosition
if index == 0 {
path.move(to: CGPoint(x: 0, y: yPosition))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
}
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round))
}
}
I tried converting the path in to a UIBezierPath and trying my UIKit implementation but no luck
This was my UIKit implementation:
func addGradient(path: UIBezierPath, hexString: String){
let color = UIColor(hexString: hexString).withAlphaComponent(0.4).cgColor
guard let clippingPath = path.copy() as? UIBezierPath else { return }
clippingPath.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
clippingPath.addLine(to: CGPoint(x: 0, y: bounds.height))
clippingPath.close()
clippingPath.addClip()
let colors = [color, UIColor.clear.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations) else { return }
guard let context = UIGraphicsGetCurrentContext() else { return }
let startPoint = CGPoint(x: 1, y: 1)
let endPoint = CGPoint(x: 1, y: bounds.maxY)
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsAfterEndLocation)
}
Here would be a pure SwiftUI implementation.
I suggest to convert the var into an own struct that conforms to Shape protocol. Then you can use it for both stroke and fill background.
This has the positive side effect that you don't need a GeometryReader any more, as Shape provides you with the drawing rectangle with func path(in rect: CGRect).
The result looks like this:
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data
struct ContentView: View {
var body: some View {
VStack {
Text("Chart")
EndingBalanceChart()
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
EndingBalanceChart(isBackground: true)
.fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 200)
.padding()
}
}
}
struct EndingBalanceChart: Shape { // chnaged var to a Shape struct
var isBackground: Bool = false
func path(in rect: CGRect) -> Path {
Path { path in
for index in endingBalance.indices {
let xPosition: CGFloat = rect.width / CGFloat(endingBalance.count) * CGFloat(index + 1)
let maxY = endingBalance.max() ?? 0
let minY = endingBalance.min() ?? 0
let yAxis: CGFloat = maxY - minY
let yPosition: CGFloat = (1 - CGFloat((Double(endingBalance[index]) - minY) / yAxis)) * rect.height
if index == 0 {
path.move(to: CGPoint(x: 0, y: rect.height))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
}
}
}
Here is an approach:
let amountPaid: [Double] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] // dummy data
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data
struct ContentView: View {
#State private var value: Double = 0
var body: some View {
VStack {
Text("Chart")
CurveChart(data: endingBalance)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
CurveChart(data: endingBalance, isBackground: true)
.fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 200)
.overlay(alignment: .topTrailing, content: {
plotPoint(data: endingBalance, index: Int(value))
.fill(.blue)
})
CurveChart(data: amountPaid)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
CurveChart(data: amountPaid, isBackground: true)
.fill(.linearGradient(colors: [.green, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 60)
.overlay(alignment: .topTrailing, content: {
plotPoint(data: amountPaid, index: Int(value))
.fill(.green)
})
.padding(.bottom)
Slider(value: $value, in: 0...Double(endingBalance.count-1), step: 1.0)
}
.padding()
}
}
struct CurveChart: Shape { // chnaged var to a Shape struct
let data: [Double]
var isBackground: Bool = false
func path(in rect: CGRect) -> Path {
Path { path in
for index in data.indices {
let xPosition: CGFloat = rect.width / CGFloat(data.count-1) * CGFloat(index)
let maxY = data.max() ?? 0
let minY = data.min() ?? 0
let yAxis: CGFloat = maxY - minY
let yPosition: CGFloat = (1 - CGFloat((Double(data[index]) - minY) / yAxis)) * rect.height
if index == 0 {
path.move(to: CGPoint(x: 0, y: rect.height))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
}
}
}
struct plotPoint: Shape { // chnaged var to a Shape struct
let data: [Double]
let index: Int
let size = 20.0
func path(in rect: CGRect) -> Path {
let xStep = rect.width / Double(data.count-1)
let yStep = rect.height / (data.max() ?? 0)
let xCenter = Double(index) * xStep
let yCenter = rect.height - yStep * data[index]
var path = Path()
path.addEllipse(in: CGRect(x: xCenter - size/2, y: yCenter - size/2, width: size, height: size))
return path
}
}
I used this tutorial to create a hexagon shape:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-draw-polygons-and-stars
My goal is to try to round the corners of my hexagon shape. I know I have to use the path.addCurve somehow, but I cannot figure out where I need to do that. I am only getting weird results. Has anyone got an idea?
struct Polygon: Shape {
let corners: Int
let smoothness: CGFloat
func path(in rect: CGRect) -> Path {
guard corners >= 2 else { return Path() }
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
var currentAngle = -CGFloat.pi / 2
let angleAdjustment = .pi * 2 / CGFloat(corners * 2)
let innerX = center.x * smoothness
let innerY = center.y * smoothness
var path = Path()
path.move(to: CGPoint(x: center.x * cos(currentAngle), y: center.y * sin(currentAngle)))
var bottomEdge: CGFloat = 0
for corner in 0 ..< corners * 2 {
let sinAngle = sin(currentAngle)
let cosAngle = cos(currentAngle)
let bottom: CGFloat
if corner.isMultiple(of: 2) {
bottom = center.y * sinAngle
path.addLine(to: CGPoint(x: center.x * cosAngle, y: bottom))
} else {
bottom = innerY * sinAngle
path.addLine(to: CGPoint(x: innerX * cosAngle, y: bottom))
}
if bottom > bottomEdge {
bottomEdge = bottom
}
currentAngle += angleAdjustment
}
let unusedSpace = (rect.height / 2 - bottomEdge) / 2
let transform = CGAffineTransform(translationX: center.x, y: center.y + unusedSpace)
return path.applying(transform)
}
}
struct Hexagon: View {
#Environment(\.colorScheme) var colorScheme
var body: some View {
Polygon(corners: 3, smoothness: 1)
.fill(.clear)
.frame(width: 76, height: 76)
}
}
Haven't found a fix but this library does what I want:
https://github.com/heestand-xyz/PolyKit
I'm trying to draw a line with markers at each point by using a Shape view in SwiftUI. I want the line to have a filled circle at each CGPoint on the line. The closest to this that I've gotten is by adding an arc at each point. Instead of using the arc, how can I add a Circle() shape at each point? I'm open to other approaches to accomplish this. My only requirements are to use SwiftUI and have the ability to interact with the markers.
import SwiftUI
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
path.addArc(center: pt, radius: 8, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: false)
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.padding()
.frame(width: 400, height: 300)
}
}
You can make two different shapes, one for the line and one for the markers, and overlay them. Then you can also control their coloring individually:
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
}
return path
}
}
struct MarkersShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addEllipse(in: CGRect(x: pt.x - 8, y: pt.y - 8, width: 16, height: 16))
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.overlay(
MarkersShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.fill(.blue)
)
.frame(width: 350, height: 300)
}
}
According to previous posts and comments here is my solution:
First there is need to use extension for filling and setting shape stroke:
extension Shape {
public func fill<Shape: ShapeStyle>(_ fillContent: Shape, strokeColor: Color, lineWidth: CGFloat) -> some View {
ZStack {
self.fill(fillContent)
self.stroke(strokeColor, lineWidth: lineWidth)
}
}
}
Another thing is Scaler struct used to make scaling calculations:
struct Scaler {
let bounds: CGPoint
let maxVal: Double
let minVal: Double
let valuesCount: Int
var xFactor: CGFloat {
valuesCount < 2 ? bounds.x : bounds.x / CGFloat(valuesCount - 1)
}
var yFactor: CGFloat {
bounds.y / maxVal
}
}
Now it's time for real chart shape drawing:
struct ChartLineDotsShape: Shape {
let values: [Double]
let dotRadius: CGFloat
func path(in rect: CGRect) -> Path {
guard let maxVal = values.max(), let minVal = values.min() else { return Path() }
let scaler = Scaler(bounds: CGPoint(x: rect.width, y: rect.height), maxVal: maxVal, minVal: minVal, valuesCount: values.count)
return Path { path in
var valuesIterator = values.makeIterator()
let dx = scaler.xFactor
var x = 0.0
guard let y = valuesIterator.next() else { return }
path.move(to: CGPoint(x: x, y: calculate(y, scaler: scaler)))
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
while let y = valuesIterator.next() {
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
}
}
}
private func calculate(_ value: CGFloat, scaler: Scaler) -> CGFloat {
scaler.bounds.y - value * scaler.yFactor
}
private func draw(point: CGPoint, on path: inout Path) {
path.addLine(to: CGPoint(x: point.x, y: point.y))
path.addEllipse(in: CGRect(x: point.x - dotRadius * 0.5, y: point.y - dotRadius * 0.5, width: dotRadius, height: dotRadius))
path.move(to: CGPoint(x: point.x, y: point.y))
}
}
If there is single iteration loop lines dots drawing shape we can try to use it in some view:
public struct ChartView: View {
public var body: some View {
ChartLineDotsShape(values: [0.3, 0.5, 1.0, 2.0, 2.5, 2.0, 3.0, 6.0, 2.0, 1.5, 2.0], dotRadius: 4.0)
.fill(Color.blue, strokeColor: Color.blue, lineWidth: 1.0)
.shadow(color: Color.blue, radius: 2.0, x: 0.0, y: 0.0)
.frame(width: 320.0, height: 200.0)
.background(Color.blue.opacity(0.1))
.clipped()
}
}
Finally preview setup:
struct ChartView_Previews: PreviewProvider {
static var previews: some View {
ChartView()
.preferredColorScheme(.dark)
}
}
and viola:
I am trying to animate my path with a move value, It works almost, but the needed update to path happens after animation is done! Which must happen before Animation! How can I make it possible?
PS: The issue is about Path, and I am trying solve it through path.
struct HatchedShap: Shape {
let dis: CGFloat
var move: CGFloat
var animatableData: CGFloat {
get { return move }
set { move = newValue }
}
func path(in rect: CGRect) -> Path {
return Path { path in
for index in -Int(move*2.0)...Int((rect.height)/dis) {
path.move(to: CGPoint(x: rect.minX, y: rect.minY + (CGFloat(index) + move)*dis))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + (CGFloat(index) + move)*dis))
}
}
}
}
use case:
struct ContentView: View {
#State private var move: CGFloat = CGFloat()
var body: some View {
HatchedShap(dis: 20.0, move: move)
.stroke(Color.red, style: StrokeStyle(lineWidth: 2.0))
.background(Color.black)
.frame(width: 100, height: 200)
.onTapGesture {
if (move == -0.5) { move = 0}
else { move = -0.5 }
}
.animation(Animation.linear(duration: 1.0), value: move)
}
}
I think using stride(from:through:by:) is much easier. You can then continue with the loop if it is out of the range of the rect. This example also nicely wraps around if the factor's absolute value is greater than 1.
Code:
struct HatchedShap: Shape {
let dis: CGFloat
var move: CGFloat
var animatableData: CGFloat {
get { return move }
set { move = newValue }
}
func path(in rect: CGRect) -> Path {
Path { path in
for start in stride(from: 0, through: rect.height, by: dis) {
let offset = start + dis * move.truncatingRemainder(dividingBy: 1)
guard 0 ... rect.height ~= offset else {
continue
}
path.move(to: CGPoint(x: rect.minX, y: rect.minY + offset))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + offset))
}
}
}
}
Results:
Factor of -0.5
Factor of -2
Small note:
You can simplify:
if (move == -0.5) {
move = 0
} else {
move = -0.5
}
To the following, which removes duplication & possibly faster because there is no branching (depends on if the compiler optimizes this):
move = -0.5 - move
In your path code, assign y to a let constant and check to make sure it is larger than 0 before adding the line to the path:
struct HatchedShap: Shape {
let dis: CGFloat
var move: CGFloat
var animatableData: CGFloat {
get { return move }
set { move = newValue }
}
func path(in rect: CGRect) -> Path {
return Path { path in
for index in -Int(move*2.0)...Int((rect.height)/dis) {
let y = rect.minY + (CGFloat(index) + move)*dis
if y > 0 {
path.move(to: CGPoint(x: rect.minX, y: y))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + (CGFloat(index) + move)*dis))
}
}
}
}
}
I'm trying to create a Set of cards. Each card has an object (shape) assigned, that is then shown with a different colour and appears in different numbers on each card. -> as a template I've created the struct SetCard
Already within the definition of the struct SetCard the var shape: Shape returns the error message:
Value of protocol type 'Shape' cannot conform to 'View'; only struct/enum/class types can conform to protocols"
import Foundation
import SwiftUI
var setCardSet: Array<SetCard> = []
var counter: Int = 0
func createSet() -> Array<SetCard> {
for object in 0..<3 {
var chosenShape = objectLibrary[object]
for color in 0..<3 {
var chosenColor = colors[color]
for numberOfShapes in 0..<3 {
counter += 1
setCardSet.append(SetCard(id: counter, shape: chosenShape, numberOfShapes: numberOfShapes, colorOfShapes: chosenColor))
}
}
}
return setCardSet
}
struct SetCard: Identifiable {
var id: Int
var shape: Shape
var numberOfShapes: Int
var colorOfShapes: Color
// var shadingOfShapes: Double
}
struct GameObject {
var name: String
var object: Shape
}
let objectLibrary: [Shape] = [Diamond(), Oval()]
let colors: [Color] = [Color.green, Color.red, Color.purple]
At a later step, the individual objects are shown and "stacked" on the same card:
import SwiftUI
struct ContentView: View {
let observed: Array<SetCard> = createSet()
var body: some View {
VStack (observed) { card in
CardView(card: card).onTapGesture {
withAnimation(.linear(duration: 0.75)) {
print(card.id)
}
}
.padding(2)
}
}
}
struct CardView: View {
var card: SetCard
var body: some View {
ZStack {
VStack{
ForEach(0 ..< card.numberOfShapes+1) {_ in
card.shape
}
}
}
}
}
let gradientStart = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
let gradientEnd = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What am I doing wrong here?
Definition of shapes:
import SwiftUI
struct Diamond: Shape {
func path(in rect: CGRect) -> Path {
let height = min(rect.width, rect.height)/4
let length = min(rect.width, rect.height)/3
let center = CGPoint(x: rect.midX, y: rect.midY)
let top: CGPoint = CGPoint(x: center.x + length, y: center.y)
let left: CGPoint = CGPoint(x: center.x, y: center.y - height)
let bottom: CGPoint = CGPoint(x: center.x - length, y: center.y)
let right: CGPoint = CGPoint(x: center.x, y: center.y + height)
var p = Path()
p.move(to: top)
p.addLine(to: left)
p.addLine(to: bottom)
p.addLine(to: right)
p.addLine(to: top)
return p
}
}
struct Oval: Shape {
func path(in rect: CGRect) -> Path {
let height = min(rect.width, rect.height)/4
let length = min(rect.width, rect.height)/3
let center = CGPoint(x: rect.midX, y: rect.midY)
let centerLeft = CGPoint(x: center.x - length + (height/2), y: center.y)
let centerRight = CGPoint(x: center.x + length - (height/2), y: center.y)
let bottomRight = CGPoint(x: centerRight.x, y: center.y - height)
let topLeft = CGPoint(x: centerLeft.x, y: center.y + height)
var p = Path()
p.move(to: topLeft)
p.addArc(center: centerLeft, radius: height, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 270), clockwise: false)
p.addLine(to: bottomRight)
p.addArc(center: centerRight, radius: height, startAngle: Angle(degrees: 270), endAngle: Angle(degrees: 90), clockwise: false)
p.addLine(to: topLeft)
return p
}
}
You need to use one type for shape in your model, like
struct SetCard: Identifiable {
var id: Int
var shape: AnyShape // << here !!
var numberOfShapes: Int
var colorOfShapes: Color
// var shadingOfShapes: Double
}
The AnyShape declaration and demo of usage can be observed in my different answer https://stackoverflow.com/a/62605936/12299030
And, of course, you have to update all other dependent parts to use it (I skipped that for simplicity).