CGPoint init - No exact matches in call to initializer error
What's wrong with my code? so ..
let ctx: CGContext = UIGraphicsGetCurrentContext()!
ctx.setLineWidth(1/UIScreen.main.scale);
ctx.setLineCap(.square)
ctx.setLineJoin(.round)
ctx.setStrokeColor(UIColor.black.cgColor)
for i in 0...3 {
ctx.move(to: CGPoint(x: 0, y: 40*i))
ctx.addLine(to: CGPoint(x: UIScreen.main.bounds.size.width, y: CGFloat(40*i)))
}
you pass a CGFloat to x, and an Int to y. So swift is looking in vain for a CGPoint(x: CGFloat, y: Int) initializer.
CGPoint(x: UIScreen.main.bounds.width, y: CGFloat(40 * i))
Should work
Related
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.
From a file I have a bunch of SVG paths that I convert to UIBezierPath. In order to make my example simple I create the path manually:
struct myShape: Shape {
func path(in rect: CGRect) -> Path {
let p = UIBezierPath()
p.move(to: CGPoint(x: 147, y: 32))
p.addQuadCurve(to: CGPoint(x: 203, y: 102), controlPoint: CGPoint(x: 181, y: 74))
p.addQuadCurve(to: CGPoint(x: 271, y: 189), controlPoint: CGPoint(x: 242, y: 166))
p.addQuadCurve(to: CGPoint(x: 274, y: 217), controlPoint: CGPoint(x: 287, y: 204))
p.addQuadCurve(to: CGPoint(x: 229, y: 235), controlPoint: CGPoint(x: 258, y: 229))
p.addQuadCurve(to: CGPoint(x: 193, y: 235), controlPoint: CGPoint(x: 204, y: 241))
p.addQuadCurve(to: CGPoint(x: 190, y: 219), controlPoint: CGPoint(x: 183, y: 231))
p.addQuadCurve(to: CGPoint(x: 143, y: 71), controlPoint: CGPoint(x: 199, y: 195))
p.addQuadCurve(to: CGPoint(x: 125, y: 33), controlPoint: CGPoint(x: 134, y: 55))
p.addCurve(to: CGPoint(x: 147, y: 32), controlPoint1: CGPoint(x: 113, y: 5), controlPoint2: CGPoint(x: 128, y: 9))
p.close()
return Path(p.cgPath)
}
}
The result is the following figure:
For this figure I have a separated path/shape that represents the "median" of the figure and the order in which this figure should be filled. The path is just a concatenation of lines.
struct myMedian: Shape {
func path(in rect: CGRect) -> Path {
let p = UIBezierPath()
p.move(to: CGPoint(x: 196, y: 226))
p.addLine(to: CGPoint(x: 209, y: 220))
p.addLine(to: CGPoint(x: 226, y: 195))
p.addLine(to: CGPoint(x: 170, y: 86))
p.addLine(to: CGPoint(x: 142, y: 43))
p.addLine(to: CGPoint(x: 131, y: 39))
return Path(p.cgPath)
}
}
To visualize the order of the lines I've added the red arrows:
Now I need to fill the "big figure" in the same order as the "median stroke". I know how to fill the whole figure in one step, but not splitwise and especially I don't know how to manage the direction of the animation.
The final result should look like this:
Since I'm using SwiftUI it should be compatible with it.
The main view is:
struct DrawCharacter: View {
var body: some View {
ZStack(alignment: .topLeading){
myShape()
myMedian()
}
}
}
You can define an animatableData property in your Shape to enable SwiftUI to interpolate between states (e.g., filled and unfilled). Where you start drawing each stroke will matter for direction. You could also use .trim on the path to truncate it if you prefer and tie that value into animatableData.
For a whole character/kanji, you may need to compose a meta Shape or View of multiple sub-Shapes with defined positions, but that's actually less work for you in the long run because you can create a library of strokes that are easy to recombine, no?
In the example below, I move a moon from full to crescent by changing the percentFullMoon property from other views. That property is used by the path drawing function to setup some arcs. SwiftUI reads the animatableData property to figure out how to draw however many frames it chooses to interpolate at the time of animation.
It's rather simple, even if this API is obscurely named.
Does that make sense? Note: the code below uses some helper functions like clamp, north/south, and center/radius, but are irrelevant to the concept.
import SwiftUI
/// Percent full moon is from 0 to 1
struct WaningMoon: Shape {
/// From 0 to 1
var percentFullMoon: Double
var animatableData: Double {
get { percentFullMoon }
set { self.percentFullMoon = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
addExteriorArc(&path, rect)
let cycle = percentFullMoon * 180
switch cycle {
case 90:
return path
case ..<90:
let crescent = Angle(degrees: 90 + .clamp(0.nextUp, 90, 90 - cycle))
addInteriorArc(&path, rect, angle: crescent)
return path
case 90.nextUp...:
let gibbous = Angle(degrees: .clamp(0, 90.nextDown, 180 - cycle))
addInteriorArc(&path, rect, angle: gibbous)
return path
default: return path
}
}
private func addInteriorArc(_ path: inout Path, _ rect: CGRect, angle: Angle) {
let xOffset = rect.radius * angle.tan()
let offsetCenter = CGPoint(x: rect.midX - xOffset, y: rect.midY)
return path.addArc(
center: offsetCenter,
radius: rect.radius / angle.cos(),
startAngle: .south() - angle,
endAngle: .north() + angle,
clockwise: angle.degrees < 90) // False == Crescent, True == Gibbous
}
private func addExteriorArc(_ path: inout Path, _ rect: CGRect) {
path.addArc(center: rect.center,
radius: rect.radius,
startAngle: .north(),
endAngle: .south(),
clockwise: true)
}
}
Helpers
extension Comparable {
static func clamp<N:Comparable>(_ min: N, _ max: N, _ variable: N) -> N {
Swift.max(min, Swift.min(variable, max))
}
func clamp<N:Comparable>(_ min: N, _ max: N, _ variable: N) -> N {
Swift.max(min, Swift.min(variable, max))
}
}
extension Angle {
func sin() -> CGFloat {
CoreGraphics.sin(CGFloat(self.radians))
}
func cos() -> CGFloat {
CoreGraphics.cos(CGFloat(self.radians))
}
func tan() -> CGFloat {
CoreGraphics.tan(CGFloat(self.radians))
}
static func north() -> Angle {
Angle(degrees: -90)
}
static func south() -> Angle {
Angle(degrees: 90)
}
}
extension CGRect {
var center: CGPoint {
CGPoint(x: midX, y: midY)
}
var radius: CGFloat {
min(width, height) / 2
}
var diameter: CGFloat {
min(width, height)
}
var N: CGPoint { CGPoint(x: midX, y: minY) }
var E: CGPoint { CGPoint(x: minX, y: midY) }
var W: CGPoint { CGPoint(x: maxX, y: midY) }
var S: CGPoint { CGPoint(x: midX, y: maxY) }
var NE: CGPoint { CGPoint(x: maxX, y: minY) }
var NW: CGPoint { CGPoint(x: minX, y: minY) }
var SE: CGPoint { CGPoint(x: maxX, y: maxY) }
var SW: CGPoint { CGPoint(x: minX, y: maxY) }
func insetN(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: midX, y: minY + height / denominator)
}
func insetE(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: minX - width / denominator, y: midY)
}
func insetW(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: maxX + width / denominator, y: midY)
}
func insetS(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: midX, y: maxY - height / denominator)
}
func insetNE(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: maxX + width / denominator, y: minY + height / denominator)
}
func insetNW(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: minX - width / denominator, y: minY + height / denominator)
}
func insetSE(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: maxX + width / denominator, y: maxY - height / denominator)
}
func insetSW(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: minX - width / denominator, y: maxY - height / denominator)
}
}
I tried to create a function inside a custom UIButton class to add a shape to an existing button.
func drawStartButton(){
let shape = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 500, y: 0))
path.addLine(to: CGPoint(x: 500, y: -100))
path.addLine(to: CGPoint(x: 250, y: -50))
path.addLine(to: CGPoint(x: 0, y: -100))
path.addLine(to: CGPoint(x: 0, y: 0))
path.close()
shape.path = path.cgPath
shape.fillColor = redColor.cgColor
shape.frame = self.bounds
self.layer.addSublayer(shape)
}
So far no problem... BUT when i add the layer to the button, the layer is to big of course! How can i "autoresize" the layer to its button? I did expect something like
shape.frame = self.bounds
... but the path still keeps the same size as without the self.bounds.
Your best bet is to subclass the button and then layout your layer in layoutSubviews which gets called when your surrounding frame changes.
final class StartButton: UIButton {
private let shapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.frame = bounds
}
}
But I'm noticing that you are dictating a giant shape in terms of your CGPoints. You might have to subclass the layer, too, and redraw based on your bounds in layoutSublayers.
class CustomShapeCAshapeLayer: CAShapeLayer {
func shapeButton(width: CGFloat, height: CGFloat){
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: width, y: 0))
path.addLine(to: CGPoint(x: width, y: height + 30))
path.addLine(to: CGPoint(x: width/2, y: height))
path.addLine(to: CGPoint(x: 0, y: height + 30))
path.addLine(to: CGPoint(x: 0, y: 0))
path.close()
self.fillColor = redColor.cgColor
self.path = path.cgPath
}
}
main:
let layer = CustomShapeCAshapeLayer()
layer.shapeButton(width: startGameButton.frame.width, height: startGameButton.frame.height)
startGameButton.layer.addSublayer(layer)
I have an extension to curve the bottom edge of views since this styling is used on multiple screens in the app I am trying to create.
However, I have noticed I can only make it work with views that I have added trough interface builder. If I try to apply it on view created programmatically they do not render.
I have created a simple example to illustrate the problem. The main storyboard contains two viewControllers with a single colored view in the middle: one created with Interface Builder while the other programmatically.
In StoryboardVC, the view with the curve is rendered correctly without any problem. The setBottomCurve() method is used to create the curve.
If you compare this to setting the entry point to ProgrammaticVC, running the app you can see a plain white screen. Comment this line out to see the view appear again.
This is the extension used:
extension UIView {
func setBottomCurve(curve: CGFloat = 40.0){
self.frame = self.bounds
let rect = self.bounds
let y:CGFloat = rect.size.height - curve
let curveTo:CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
I expect ProgrammaticVC to look identical to StoryboardVC (except for the difference in color)
The example project can be found here:
https://github.com/belamatedotdotipa/CurveTest2
I suggest to create a subclass instead of using an extension, this is a specific behaviour.
In this case you cannot see the result expected, when you are adding the view programmatically, because in the viewDidLoad you don't have the frame of your view, in this example you can use the draw function:
class BottomCurveView: UIView {
#IBInspectable var curve: CGFloat = 40.0 {
didSet {
setNeedsLayout()
}
}
override func draw(_ rect: CGRect) {
setBottomCurve()
}
private func setBottomCurve() {
let rect = bounds
let y: CGFloat = rect.size.height - curve
let curveTo: CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
I am trying to draw a shape using CGPath and SKShapeNode. However, whatever the lineJoin or lineCap combinations I try to use, the lines have gaps.
import PlaygroundSupport
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
super.didMove(to: view)
let path = CGMutablePath()
path.move(to: NSPoint(x: -66.585, y: 1.125))
path.addCurve(to: NSPoint(x: -60.585, y: -41.765), control1: NSPoint(x: -66.585, y: 1.125), control2: NSPoint(x: -65.835, y: -32.735))
path.addCurve(to: NSPoint(x: 66.585, y: -20.3150000000001), control1: NSPoint(x: -60.585, y: -41.765), control2: NSPoint(x: -3.39500000000001, y: -1.88499999999999))
path.addCurve(to: NSPoint(x: 56.545, y: 18.235), control1: NSPoint(x: 66.125, y: -7.09500000000003), control2: NSPoint(x: 62.695, y: 6.16499999999996))
path.addCurve(to: NSPoint(x: -66.585, y: 1.125), control1: NSPoint(x: 56.055, y: 19.1849999999999), control2: NSPoint(x: -15.415, y: 41.765))
path.closeSubpath()
let shapeNode = SKShapeNode(path: path)
shapeNode.fillColor = .red
shapeNode.lineWidth = 10
shapeNode.lineJoin = .round
shapeNode.strokeColor = .white
addChild(shapeNode)
}
}
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
if let scene = GameScene(fileNamed: "GameScene") {
scene.scaleMode = .aspectFill
sceneView.presentScene(scene)
}
PlaygroundSupport.PlaygroundPage.current.liveView = sceneView
What am I doing wrong here?
This looks odd to me:
path.move(to: NSPoint(x: -66.585, y: 1.125))
path.addCurve(to: NSPoint(x: -60.585, y: -41.765), control1: NSPoint(x: -66.585, y: 1.125), control2: NSPoint(x: -65.835, y: -32.735))
You move to (-66.58., 1.125). Then you add a curve where the first point is (-60.585, -41.765), and the first control point is the same as the original point you moved to. It seems like that will cause some odd issues. I'd think you'd want to have the point you move to be the first point of the curve, not its control point.