How do I add Text label in CGRect? - swift

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

Related

Swift UIView How to add more drawing after calling draw function ? Using UIBezierPath

This example I am drawing a line and I want to add an arrow on the tip of it. The arrow function works fine when I use it in draw function but it is not being added to the view when I call it later on. Is there some way for me to add it later on ?
**My BezierView Class
**
final class BezierView: UIView {
var currentPoint: CGPoint = CGPoint()
override func draw(_ rect: CGRect) {
let line1 = UIBezierPath()
line1.move(to: CGPoint(x: 20, y: 65))
line1.addLine(to: CGPoint(x: 20, y: 15))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 5))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 65))
line1.lineWidth = 2
UIColor.darkGray.setStroke()
line1.stroke()
currentPoint = line1.currentPoint
}
func drawArrow() {
let point = currentPoint
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
**My View Controller
**
override func viewDidLoad() {
super.viewDidLoad()
let bezierView = BezierView(frame: CGRect(x: 0, y: 40, width: self.view.frame.width, height: 200))
bezierView.drawArrow()
}
I have tried to use: self.setNeedsDisplay(), self.setNeedsLayout(), self.layoutSubviews() functions in the drawArrow function but they didn't change the outcomes.
The reason why filling and stroking a bezier path draws things on the screen is because the draw(_:) method is called (by iOS) in a graphics context created by the system that allows you to draw on the screen. After draw(_:) returns, the graphics context ends, and you can no longer draw things on the screen.
Therefore, calling drawArrow in viewDidLoad does not work. You are not in that specific graphics context. Unfortunately, you cannot get this graphics context from outside of draw(_:) and draw in it.
In other words, all the drawing has to be done in draw(_:). If you just want to draw one arrow, you can just keep a boolean to store whether the arrow has been drawn or not:
final class BezierView: UIView {
var isArrowDrawn = false {
didSet {
// this causes draw(_:) to be called again
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let line1 = UIBezierPath()
line1.move(to: CGPoint(x: 20, y: 65))
line1.addLine(to: CGPoint(x: 20, y: 15))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 5))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 65))
line1.lineWidth = 2
UIColor.darkGray.setStroke()
line1.stroke()
if isArrowDrawn {
let point = line1.currentPoint
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
func drawArrow() {
isArrowDrawn = true
}
}
If you want multiple arrows, to be drawn whenever drawArrow is called, then you would need to keep track of the list of points at which the arrows all are located. Something like
final class BezierView: UIView {
var currentPoint: CGPoint = CGPoint()
var arrowPoints = [CGPoint]() {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
// ... omitted for brevity
currentPoint = line1.currentPoint
for point in arrowPoints {
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
func drawArrow() {
arrowPoints.append(currentPoint)
}
}
Basically, you need to keep track of all the "states" that your view can be in, and your draw(_:) method is responsible for drawing the current state.
If you find this quite annoying, consider switching to using layers instead. You can, at any time, create a CAShapeLayer with a path, and add it as a sublayer as self.layer. Here is a tutorial to get started with.

How to round corners of this custom shape SwiftUI?

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

Add filled circles (markers) at SwiftUI path points

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:

bezierPath setfill with diff colours Not Working ( to create a mountain climb color coding )

My goal is to create a chart much like below. Where the diff colours represent the different slope / gradient % for each segment.
My previous code, I have already managed to get the "mountain" outline, now further improved it such that I am now making a set of "boxes" which represents each colour segment. But am facing some issue.
Here is the code I'm using right now.
let myBezier = UIBezierPath()
let nextPt = fillSlopes(at: idx)
if idx > 0 {
let prevPt = fillSlopes(at: idx - 1)
myBezier.move(to: CGPoint(x: prevPt.x, y: height))
myBezier.addLine(to: CGPoint(x: prevPt.x, y: prevPt.y))
myBezier.addLine(to: CGPoint(x: nextPt.x, y: nextPt.y))
myBezier.addLine(to: CGPoint(x: nextPt.x, y: height))
myBezier.addLine(to: CGPoint(x: prevPt.x, y: height))
myBezier.close()
// This is test code, actual code will compare current slope %
// (in an array) and then based on the slope %, will change
// the colour accordingly.
if idx % 2 == 0 {
UIColor.systemRed.setFill()
myBezier.fill()
} else {
UIColor.systemBlue.setFill()
myBezier.fill()
}
This is the results. Unfortunately, the colours is not coming out properly. What Am I missing?
In addition to this, Drawing so many little boxes is really eating up a lot of CPU cycles, if there are other suggestions to get the desired output result is also appreciated. Thanks
Update:
Many Thanks for #seaspell, I realised what was happening. I was putting my let myBezier = UIBezierPath() outside of the for loop hence my [UIBezierPath] array was getting mangled up. eg:
This got things all wrong:
func xxx {
let myBezier = UIBezierPath() // <<<<<< this is wrong
for idx in slopeBars.indices {
// do stuff
}
This is Correct and fixed some performance issue as well:
func xxx {
var slopeBars = [UIBezierPath]()
var slopeBarColorArray = [UIColor]()
for idx in slopeBars.indices {
let myBezier = UIBezierPath() // <<<<< Moved here
let nextPt = fillSlopes(at: idx)
if idx > 0 {
let prevPt = fillSlopes(at: idx - 1)
myBezier.move(to: CGPoint(x: prevPt.x, y: height))
myBezier.addLine(to: CGPoint(x: prevPt.x, y: prevPt.y))
myBezier.addLine(to: CGPoint(x: nextPt.x, y: nextPt.y))
myBezier.addLine(to: CGPoint(x: nextPt.x, y: height))
myBezier.addLine(to: CGPoint(x: prevPt.x, y: height))
myBezier.close()
slopeBars.append(myBezier)
if gradient < 0.0 {
slopeBarColorArray.append(UIColor(cgColor: Consts.elevChart.slope0_0))
} else if gradient < 4.0 {
slopeBarColorArray.append(UIColor(cgColor: Consts.elevChart.slope0_4))
}
for i in 0..<slopeBars.count {
if !slopeBarColorArray.isEmpty {
slopeBarColorArray[i].setStroke()
slopeBarColorArray[i].setFill()
}
slopeBars[i].stroke()
slopeBars[i].fill()
}
}
There's nothing really wrong with using bezier paths for this. You need to use a new one for each bar though and also you don't want to build them in draw(rect:). I suspect that is the cause of the CPU issues.
Take a look at this example I put together.
class BarChartView: UIView {
lazy var dataPoints: [CGFloat] = {
var points = [CGFloat]()
for _ in 1...barCount {
let random = arc4random_uniform(UInt32(bounds.maxY / 2))
points.append(CGFloat(random))
}
return points
}()
var bars = [UIBezierPath]()
private let barCount = 20
lazy var colors: [UIColor] = {
let colors: [UIColor] = [.red, .blue, .cyan, .brown, .darkGray, .green, .yellow, .gray, .lightGray, .magenta, .orange, .purple, .systemPink, .white]
var random = [UIColor]()
for i in 0...barCount {
random.append(colors[Int(arc4random_uniform(UInt32(colors.count)))])
}
return random
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.bars = buildBars()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.bars = buildBars()
}
func buildBars() -> [UIBezierPath] {
let lineWidth = Int(bounds.maxX / CGFloat(barCount))
var bars = [UIBezierPath]()
for i in 0..<barCount {
let line = UIBezierPath()
let leftX = lineWidth * i
let rightX = leftX + lineWidth
let centerX = leftX + (lineWidth / 2)
let nextLeftPoint = (i == 0 ? dataPoints[i] : dataPoints[i - 1])
let nextRightPoint = (i == barCount - 1 ? dataPoints[i] : dataPoints[i + 1])
let currentPoint = dataPoints[i]
//bottom left
line.move(to: CGPoint(x: leftX, y: Int(bounds.maxY)) )
//bottom right
line.addLine(to: CGPoint(x: rightX, y: Int(bounds.maxY)) )
//top right
line.addLine(to: CGPoint(x: rightX, y: Int((currentPoint + nextRightPoint) / 2)) )
//top center
line.addLine(to: CGPoint(x: centerX, y: Int(currentPoint)) )
//top left
line.addLine(to: CGPoint(x: leftX, y: Int((currentPoint + nextLeftPoint) / 2)) )
//close the path
line.close()
bars.append(line)
}
return bars
}
override func draw(_ rect: CGRect) {
super.draw(rect)
for i in 0..<bars.count {
colors[i].setStroke()
colors[i].setFill()
bars[i].stroke()
bars[i].fill()
}
}
}

Swift, polar coordinates

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))