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
Related
I am modifying this swift radar chart. I would like to draw an image on each of the edge vertices. I cannot figure out how to draw an image at the same time as drawing a path. Barring that, I figured I could let the shape draw itself and then somehow recover the edge vertices from its path and draw images as overlays on the shape, but I cannot figure out how to get those vertices.
So I have two questions, and I only need to know the answer to one of them to proceed.
How can I draw an image at the same time as drawing a path?
How can I pull out the edge vertices of this shape and then use them to draw overlays on the shape (the code below is closer to this version, but I'd rather be able to draw the images at the same time as drawing the path).
import SwiftUI
struct RadarChart: View {
var data: [Double]
let gridColor: Color
let dataColor: Color
private var edgeVertices: [CGPoint]
init(data: [Double], gridColor: Color = .gray, dataColor: Color = .blue) {
self.data = data
self.gridColor = gridColor
self.dataColor = dataColor
self.edgeVertices = []
}
var body: some View {
GeometryReader { geo in
ZStack {
RadarChartGrid(categories: data.count, divisions: 5)
.stroke(gridColor, lineWidth: 0.5)
.overlay(
Image(systemName: "star")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:20)
.position(x: geo.size.width/2, y: geo.size.height/2)
)
RadarChartPath(data: data)
.fill(dataColor.opacity(0.3))
RadarChartPath(data: data)
.stroke(dataColor, lineWidth: 2.0)
}
.padding(20)
}
}
}
struct RadarChartGrid: Shape {
let categories: Int
let divisions: Int
private func calculateEdgePoints(rect: CGRect) -> [CGPoint]{
var edgeVertices: [CGPoint] = []
let radius = min(rect.maxX - rect.midX, rect.maxY - rect.midY)
for category in 1 ... categories {
let edgePoint = CGPoint(x: rect.midX + cos(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius,
y: rect.midY + sin(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius)
edgeVertices.append(edgePoint)
}
return edgeVertices
}
func path(in rect: CGRect) -> Path {
let radius = min(rect.maxX - rect.midX, rect.maxY - rect.midY)
let stride = radius / CGFloat(divisions)
var path = Path()
for category in 1 ... categories {
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.midX + cos(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius,
y: rect.midY + sin(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius))
}
for step in 1 ... divisions {
let rad = CGFloat(step) * stride
path.move(to: CGPoint(x: rect.midX + cos(-.pi / 2) * rad,
y: rect.midY + sin(-.pi / 2) * rad))
for category in 1 ... categories {
path.addLine(to: CGPoint(x: rect.midX + cos(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * rad,
y: rect.midY + sin(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * rad))
}
}
return path
}
}
struct RadarChartPath: Shape {
let data: [Double]
func path(in rect: CGRect) -> Path {
guard
3 <= data.count,
let minimum = data.min(),
0 <= minimum,
let maximum = data.max()
else { return Path() }
let radius = min(rect.maxX - rect.midX, rect.maxY - rect.midY)
var path = Path()
for (index, entry) in data.enumerated() {
switch index {
case 0:
path.move(to: CGPoint(x: rect.midX + CGFloat(entry / maximum) * cos(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius,
y: rect.midY + CGFloat(entry / maximum) * sin(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius))
default:
path.addLine(to: CGPoint(x: rect.midX + CGFloat(entry / maximum) * cos(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius,
y: rect.midY + CGFloat(entry / maximum) * sin(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius))
}
}
path.closeSubpath()
return path
}
}
struct RadarChart_Previews: PreviewProvider {
static var previews: some View {
RadarChart(data: [1, 2, 4, 3, 5, 2, 3])
}
}
The above code produces this image. Note that I would like to put a different image on each vertex, but cannot figure out how to get the vertices.
For posterity and for anyone who also struggled in piecing together how to do stuff on the canvas, here is an implementation following what #rob mayoff said by drawing everything on the canvas.
import SwiftUI
struct RadarChart: View {
var data: [Double]
let gridColor: Color
let fillColor: Color
let strokeColor: Color
let divisions: Int
let radiusBuffer: Double
init(data: [Double], gridColor: Color = .gray, fillColor: Color = .blue, strokeColor: Color = .blue, divisions: Int = 10, radiusBuffer: Double = 0.0) {
self.data = data
self.gridColor = gridColor
self.fillColor = fillColor
self.strokeColor = strokeColor
self.divisions = divisions
self.radiusBuffer = radiusBuffer
}
var body: some View {
Canvas { context, size in
let edges = calculateEdgePoints(rect: context.clipBoundingRect, categories: data.count)
let radarCharGridPath = radarChartGridPath(in: context.clipBoundingRect, categories: data.count)
let dataPath = radarChartPath(in: context.clipBoundingRect)
context.stroke(radarCharGridPath, with: .color(gridColor), lineWidth: 0.5)
context.fill(dataPath, with: .color(fillColor.opacity(0.3)))
context.stroke(dataPath, with: .color(strokeColor), lineWidth: 2.0)
for idx in 0...data.count-1 {
var edge = edges[idx]
let image = Image(systemName: "star.fill")
let imageEdgeSize = 20.0
edge.x -= imageEdgeSize / 2
edge.y -= imageEdgeSize / 2
let rect = CGRect(origin: edge, size: CGSize(width:imageEdgeSize, height:imageEdgeSize))
context.draw(image, in: rect)
}
}.navigationBarTitle("Radar Chart")
}
private func calculateEdgePoints(rect: CGRect, categories: Int) -> [CGPoint] {
var edgeVertices: [CGPoint] = []
let radius = min(rect.maxX - rect.midX, rect.maxY - rect.midY) - radiusBuffer
print("Edgepoints radius: \(radius)")
for category in 1 ... categories {
let edgePoint = CGPoint(x: rect.midX + cos(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius,
y: rect.midY + sin(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius)
edgeVertices.append(edgePoint)
}
return edgeVertices
}
func radarChartGridPath(in rect: CGRect, categories: Int) -> Path {
let radius = min(rect.maxX - rect.midX, rect.maxY - rect.midY) - radiusBuffer
let stride = radius / CGFloat(divisions)
var path = Path()
for category in 1 ... categories {
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.midX + cos(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius,
y: rect.midY + sin(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * radius))
}
for step in 1 ... divisions {
let rad = CGFloat(step) * stride
path.move(to: CGPoint(x: rect.midX + cos(-.pi / 2) * rad,
y: rect.midY + sin(-.pi / 2) * rad))
for category in 1 ... categories {
path.addLine(to: CGPoint(x: rect.midX + cos(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * rad,
y: rect.midY + sin(CGFloat(category) * 2 * .pi / CGFloat(categories) - .pi / 2) * rad))
}
}
return path
}
func radarChartPath(in rect: CGRect) -> Path {
guard
3 <= data.count,
let minimum = data.min(),
0 <= minimum,
let maximum = data.max()
else { return Path() }
let radius = min(rect.maxX - rect.midX, rect.maxY - rect.midY) - radiusBuffer
var path = Path()
for (index, entry) in data.enumerated() {
switch index {
case 0:
path.move(to: CGPoint(x: rect.midX + CGFloat(entry / maximum) * cos(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius,
y: rect.midY + CGFloat(entry / maximum) * sin(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius))
default:
path.addLine(to: CGPoint(x: rect.midX + CGFloat(entry / maximum) * cos(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius,
y: rect.midY + CGFloat(entry / maximum) * sin(CGFloat(index) * 2 * .pi / CGFloat(data.count) - .pi / 2) * radius))
}
}
path.closeSubpath()
return path
}
}
func makeList(_ n: Int) -> [Double] {
return (0..<n).map{ _ in Double.random(in: 1 ... 20) }
}
struct RadarChart_Previews: PreviewProvider {
static var previews: some View {
RadarChart(
data: makeList(30),
divisions: 5,
radiusBuffer: 10
)
.padding()
}
}
I have made a path, and to center it I have used CGRect. I want to add some text to the path at an exact position. However, since I have centered the path using CGAffineTransform I can't just write in the coordinates with the .position function.
This is my path:
struct elevationLegend: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 10, y: (100 - (90-0))))
path.addLine(to: CGPoint(x: 10, y: (100 - (90-180))))
let scale = (rect.width / 350) * (9/10)
var xOffset = (rect.midX / 4)
var yOffset = (rect.midY / 2)
return path.applying(CGAffineTransform(scaleX: scale, y: scale)).applying(CGAffineTransform(translationX: xOffset, y: yOffset))
}
}
And this is my hopeless try of trying to position the Text.
struct eText: TextField {
func text(in rect: CGRect) -> String {
let swag = Text("90").position(x: 10, y: 100 - (90-180))
let scale = (rect.width / 350) * (9/10)
var xOffset = (rect.midX / 4)
var yOffset = (rect.midY / 2)
return swag.applying(CGAffineTransform(scaleX: scale, y: scale)).applying(CGAffineTransform(translationX: xOffset, y: yOffset))
}
}
This dosen't work. Does anyone have an idea on how I can fix this problem
I create this circle view like this:
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext()
{
let width = fmin(self.frame.size.width, self.frame.size.height)
let offset_x = abs(width - self.frame.size.width)/2
let offset_y = abs(width - self.frame.size.height)/2
let padding = CGFloat(0.5)
let radius_size = (width/2) - (padding*2)
let circle_width = radius_size/4
context.setStrokeColor(UIColor.black.cgColor)
// Draw a circle
for i in 0 ..< 4
{
let offset = CGFloat(i) * circle_width
context.strokeEllipse(in:
CGRect(
x: padding + offset + offset_x,
y: padding + offset + offset_y,
width: (radius_size - offset)*2,
height: (radius_size - offset)*2))
}
context.strokePath()
}
}
How can I create a line to the circle center, if I have an array, of angles for the most top circle? And how can I do the same, for the middle circle?
For example, I have an array, with the given angles in degrees: [87.0, 112.0, 150.0]
Here's a function drawLine that draws a line from a center point at an angle with a specific radius. To change which circle the line reaches, just change the radius:
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext()
{
let width = fmin(self.frame.size.width, self.frame.size.height)
let offset_x = abs(width - self.frame.size.width)/2
let offset_y = abs(width - self.frame.size.height)/2
let padding = CGFloat(0.5)
let radius_size = (width/2) - (padding*2)
let circle_width = radius_size/4
context.setStrokeColor(UIColor.black.cgColor)
// Draw a circle
for i in 0 ..< 4
{
let offset = CGFloat(i) * circle_width
context.strokeEllipse(in:
CGRect(
x: padding + offset + offset_x,
y: padding + offset + offset_y,
width: (radius_size - offset)*2,
height: (radius_size - offset)*2))
}
let angles: [CGFloat] = [87.0, 112.0, 150]
let angles2: [CGFloat] = [210.0, 250.0, 330.0]
let center = CGPoint(x: width/2 + offset_x, y: width/2 + offset_y)
for angle in angles {
drawLine(context: context, center: center, radius: radius_size, angle: angle)
}
for angle in angles2 {
drawLine(context: context, center: center, radius: radius_size * 3 / 4, angle: angle)
}
context.strokePath()
}
}
func drawLine(context: CGContext, center: CGPoint, radius: CGFloat, angle: CGFloat) {
context.move(to: center)
context.addLine(to: CGPoint(x: center.x + radius * cos(angle * .pi / 180), y: center.y - radius * sin(angle * .pi / 180)))
}
This question already has answers here:
Animate CAShapeLayer path change
(1 answer)
Circular Progress Bars in IOS
(7 answers)
Closed 4 years ago.
I am trying to implement a custom circular analysis view.
The view should be circular but cut-off.
Goal:
My Code:
let circlePath = UIBezierPath(ovalIn: CGRect(x: innerRect.minX, y: innerRect.minY, width: innerRect.width, height: innerRect.height))
if trackBackgroundColor != UIColor.clear {
trackBackgroundColor.setFill()
circlePath.fill();
}
if trackBorderWidth > 0 {
circlePath.lineWidth = trackBorderWidth
trackBorderColor.setStroke()
circlePath.stroke()
}
// progress Drawing
let progressPath = UIBezierPath()
let progressRect: CGRect = CGRect(x: innerRect.minX, y: innerRect.minY, width: innerRect.width, height: innerRect.height)
let center = CGPoint(x: progressRect.midX, y: progressRect.midY)
let radius = progressRect.width / 2.0
let startAngle:CGFloat = clockwise ? CGFloat(-internalProgress * Double.pi / 180.0) : CGFloat(constants.twoSeventyDegrees * Double.pi / 180)
let endAngle:CGFloat = clockwise ? CGFloat(constants.twoSeventyDegrees * Double.pi / 180) : CGFloat(-internalProgress * Double.pi / 180.0)
progressPath.addArc(withCenter: center, radius:radius, startAngle:startAngle, endAngle:endAngle, clockwise:!clockwise)
Current Output:
How do I draw the custom circle described as Goal.
Here's a rough implementation of what you need. This draws the two arcs.
class CircularProgressView: UIView {
var trackBackgroundColor = UIColor.lightGray
var trackBorderWidth: CGFloat = 10
var progressColor = UIColor.red
var percent: Double = 0 {
didSet {
setNeedsDisplay()
}
}
// Adjust these to meet your needs. 90 degrees is the bottom of the circle
static let startDegrees: CGFloat = 120
static let endDegrees: CGFloat = 60
override func draw(_ rect: CGRect) {
let startAngle: CGFloat = radians(of: CircularProgressView.startDegrees)
let endAngle: CGFloat = radians(of: CircularProgressView.endDegrees)
let progressAngle = radians(of: CircularProgressView.startDegrees + (360 - CircularProgressView.startDegrees + CircularProgressView.endDegrees) * CGFloat(max(0.0, min(percent, 1.0))))
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(center.x, center.y) - trackBorderWidth / 2 - 10
print(startAngle, endAngle, progressAngle)
let trackPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let progressPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: progressAngle, clockwise: true)
trackPath.lineWidth = trackBorderWidth
trackPath.lineCapStyle = .round
progressPath.lineWidth = trackBorderWidth
progressPath.lineCapStyle = .round
trackBackgroundColor.set()
trackPath.stroke()
progressColor.set()
progressPath.stroke()
}
private func radians(of degrees: CGFloat) -> CGFloat {
return degrees / 180 * .pi
}
}
let progress = CircularProgressView(frame: CGRect(x: 0, y: 0, width: 500, height: 400))
progress.backgroundColor = .white
progress.percent = 0.95
I have to draw 12 points in a circle with different radiant. From point 1, draw a line to point 2, from point 2 to 3, etc. The lines will not be a problem.
I can not find a formula to find the 12 * (x,y), but I think it's something with polar coordinates / circle?
Is anyone working with it and maybe want to share with me?
See the picture which might explain better than I can:
This is the result I got:
And This is my Playground:
//: Playground - noun: a place where people can play
import Foundation
import UIKit
class DemoView: UIView {
override func draw(_ rect: CGRect) {
let origin = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let radius = frame.size.width / 2
self.createCircle(origin: origin, radius: radius)
self.addLinesInCircle(origin: origin, radius: radius)
}
func createCircle(origin: CGPoint, radius: CGFloat) {
let path = UIBezierPath()
path.addArc(withCenter: origin, radius: radius, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: true)
path.close()
UIColor.orange.setFill()
path.fill()
}
func addLinesInCircle(origin: CGPoint, radius: CGFloat) {
let path = UIBezierPath()
let incrementAngle: CGFloat = CGFloat.pi / 6
let ratios: [CGFloat] = [3/6, 5/6, 3/6, 1/6, 5/6, 2/6, 4/6, 2/6, 4/6, 4/6, 4/6, 4/6, 3/6]
for (index, ratio) in ratios.enumerated() {
let point = CGPoint(x: origin.x + cos(CGFloat(index) * incrementAngle) * radius * ratio,
y: origin.y + sin(CGFloat(index) * incrementAngle) * radius * ratio)
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.close()
UIColor.black.set()
path.stroke()
}
}
let demoView = DemoView(frame: CGRect(x: 0, y: 0, width: 320, height: 320))