I have a UIView with a custom class of TimerView. I am trying to draw a dot in the exact center of the UIView but it is appearing at the bottom slightly off center:
Custom class is here:
import UIKit
class TimerView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawDot()
}
func drawDot() {
let midViewX = self.frame.midX
let midViewY = self.frame.midY
let dotPath = UIBezierPath(ovalIn: CGRect(x: midViewX, y: midViewY, width: 5, height: 5))
let dotLayer = CAShapeLayer()
dotLayer.path = dotPath.cgPath
dotLayer.strokeColor = UIColor.blue.cgColor
self.layer.addSublayer(dotLayer)
}
}
Here is the re-implementation of TimerView class. For UIView, init is not the best place to get the frame/bounds values because it might change once the autolayout applies the constraints in runtime. layoutSubviews is the best place to get the correct frame/bounds values for parent/child views and to setup the child space properties. Secondly you should be using parent view bounds to setup the child's frame.
class TimerView: UIView {
private var dotLayer: CAShapeLayer?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
dotLayer = CAShapeLayer()
dotLayer?.strokeColor = UIColor.blue.cgColor
self.layer.addSublayer(dotLayer!)
}
override func layoutSubviews() {
super.layoutSubviews()
drawDot()
}
func drawDot() {
let midViewX = self.bounds.midX
let midViewY = self.bounds.midY
let dotPath = UIBezierPath(ovalIn: CGRect(x: midViewX, y: midViewY, width: 5, height: 5))
dotLayer?.path = dotPath.cgPath
}
}
Related
I'm having quite a bit of difficulty simply ensuring a circular progress bar is always in the centre of the UIView it is associated to.
This is what is happening:
Ignore the grey region, this is simply the UIView on a placeholder card. The red is the UIView I have added as an outlet to the UIViewController.
Below is the code for the class that I have made:
class CircleProgress: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
var progressLyr = CAShapeLayer()
var trackLyr = CAShapeLayer()
var progressClr = UIColor.red {
didSet {
progressLyr.strokeColor = progressClr.cgColor
}
}
var trackClr = UIColor.black {
didSet {
trackLyr.strokeColor = trackClr.cgColor
}
}
private func setupView() {
self.backgroundColor = UIColor.red
let centre = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let circlePath = UIBezierPath(arcCenter: centre,
radius: 10,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
trackLyr.path = circlePath.cgPath
trackLyr.fillColor = UIColor.clear.cgColor
trackLyr.strokeColor = trackClr.cgColor
trackLyr.lineWidth = 5.0
trackLyr.strokeEnd = 1.0
layer.addSublayer(trackLyr)
}
}
The aim is simply to have all 4 edges of the black circle touching the edges of the red square.
Any help is hugely appreciated. I'm thinking it must be too obvious but this has cost me too many hours tonight. :)
The problem is that there is no frame being passed when creating the object. No need for changing anything to your code. For sure you have to change the width and the height to whatever you want.
Here is an example...
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
// Add circleProgress
addCircleProgress()
}
private func addCircleProgress() {
let circleProgress = CircleProgress(frame: CGRect(x: self.view.center.x - 50, y: self.view.center.x - 50, width: 100, height: 100))
self.view.addSubview(circleProgress)
}
}
class CircleProgress: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var progressLyr = CAShapeLayer()
var trackLyr = CAShapeLayer()
var progressClr = UIColor.red {
didSet {
progressLyr.strokeColor = progressClr.cgColor
}
}
var trackClr = UIColor.black {
didSet {
trackLyr.strokeColor = trackClr.cgColor
}
}
private func setupView() {
self.backgroundColor = UIColor.red
let centre = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let circlePath = UIBezierPath(arcCenter: centre,
radius: 50,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
trackLyr.path = circlePath.cgPath
trackLyr.fillColor = UIColor.clear.cgColor
trackLyr.strokeColor = trackClr.cgColor
trackLyr.lineWidth = 5.0
trackLyr.strokeEnd = 1.0
layer.addSublayer(trackLyr)
}
}
A few suggestions:
I’d suggest making this CircularProgress take up the whole view. If you want this to be inset within the view, then, fine, add a property for that. In my example below, I created a property called inset to capture this value.
Make sure to update your path in layoutSubviews. Especially if you use constraints, you want this to respond to size changes. So add these layers from init, but update the path in layoutSubviews.
Don’t reference frame (which is the location within the superview coordinate system). Use bounds. And inset it by half the line width, so the circle doesn’t exceed the bounds of the view.
You created a progress color and progress layer, but didn’t use either one. I’ve guessed you wanted that to show the progress within the track.
You are stroking your path from 0 to 2π. People tend to expect these circular progress views to start from -π/2 (12 o’clock) and progress to 3π/2. So I’ve updated the path to use those values. But use whatever you want.
If you want, you can make it #IBDesignable if you want to see this rendered in IB.
Thus, pulling that together, you get:
#IBDesignable
public class CircleProgress: UIView {
#IBInspectable
public var lineWidth: CGFloat = 5 { didSet { updatePath() } }
#IBInspectable
public var strokeEnd: CGFloat = 1 { didSet { progressLayer.strokeEnd = strokeEnd } }
#IBInspectable
public var trackColor: UIColor = .black { didSet { trackLayer.strokeColor = trackColor.cgColor } }
#IBInspectable
public var progressColor: UIColor = .red { didSet { progressLayer.strokeColor = progressColor.cgColor } }
#IBInspectable
public var inset: CGFloat = 0 { didSet { updatePath() } }
private lazy var trackLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = trackColor.cgColor
layer.lineWidth = lineWidth
return layer
}()
private lazy var progressLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = progressColor.cgColor
layer.lineWidth = lineWidth
return layer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
public override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
}
private extension CircleProgress {
func setupView() {
layer.addSublayer(trackLayer)
layer.addSublayer(progressLayer)
}
func updatePath() {
let rect = bounds.insetBy(dx: lineWidth / 2 + inset, dy: lineWidth / 2 + inset)
let centre = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2
let path = UIBezierPath(arcCenter: centre,
radius: radius,
startAngle: -.pi / 2,
endAngle: 3 * .pi / 2,
clockwise: true)
trackLayer.path = path.cgPath
trackLayer.lineWidth = lineWidth
progressLayer.path = path.cgPath
progressLayer.lineWidth = lineWidth
}
}
That yields:
Update this function
private func setupView() {
self.backgroundColor = UIColor.red
let centre = CGPoint(x: bounds.midX, y: bounds.midY)
let circlePath = UIBezierPath(arcCenter: centre,
radius: bounds.maxX/2,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
trackLyr.path = circlePath.cgPath
trackLyr.fillColor = UIColor.clear.cgColor
trackLyr.strokeColor = trackClr.cgColor
trackLyr.lineWidth = 5.0
trackLyr.strokeEnd = 1.0
layer.addSublayer(trackLyr)
}
How can I create a simple line that I can control its originating point, but let the user determine where it ends?
I have a UIView on my storyboard and on top of this I want to insert a line. The line would be restricted to be within this particular view and would start vertically centered and from the left side.
I have buttons with a - and a + to allow the user to increase its length or decrease it.
I suppose the easiest method will be to a add another UIView (call it lineView) inside your main view, set it's width to whatever you want the width of your line to be and hook it up with an #IBOutlet. Then in your #IBAction methods for - and +, change the height of lineView
I found the answer here, How to draw a line between two points over an image in swift 3?
class LineView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.init(white: 0.0, alpha: 0.0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.blue.cgColor)
context.setLineWidth(3)
context.beginPath()
context.move(to: CGPoint(x: 5.0, y: 5.0)) // This would be oldX, oldY
context.addLine(to: CGPoint(x: 50.0, y: 50.0)) // This would be newX, newY
context.strokePath()
}
}
}
let imageView = UIImageView(image: #imageLiteral(resourceName: "image.png")) // This would be your mapView, here I am just using a random image
let lineView = LineView(frame: imageView.frame)
imageView.addSubview(lineView)
I am new to swift, I want to draw a line between 2 points over an image which I called mapView, I tried to use CGContext but got no result, any idea to help? Thanks.
UIGraphicsBeginImageContext(mapView.bounds.size)
let context : CGContext = UIGraphicsGetCurrentContext()!
context.addLines(between: [CGPoint(x:oldX,y:oldY), CGPoint(x:newX, y:newY)])
context.setStrokeColorSpace(CGColorSpaceCreateDeviceRGB())
context.setStrokeColor(UIColor.blue.cgColor.components!)
context.setLineWidth(3)
mapView?.image?.draw(at: CGPoint(x:0, y:0))
context.strokePath()
mapView.image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
One option is to add a sub view to your image view and add the line drawing code into its draw(_ rect: CGRect) method.
A sample playground implementation:
class LineView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.init(white: 0.0, alpha: 0.0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.blue.cgColor)
context.setLineWidth(3)
context.beginPath()
context.move(to: CGPoint(x: 5.0, y: 5.0)) // This would be oldX, oldY
context.addLine(to: CGPoint(x: 50.0, y: 50.0)) // This would be newX, newY
context.strokePath()
}
}
}
let imageView = UIImageView(image: #imageLiteral(resourceName: "image.png")) // This would be your mapView, here I am just using a random image
let lineView = LineView(frame: imageView.frame)
imageView.addSubview(lineView)
The update function in the class below, which is a subclass of SKSpriteNode, is supposed to change the texture of the class. According to this SO answer, updating the texture property is sufficient for changing a SKSpriteNode's texture. However, this code doesn't work.
Any ideas why?
class CharacterNode: SKSpriteNode {
let spriteSize = CGSize(width: 70, height: 100)
var level = 0
init(level: Int) {
self.level = level
super.init(texture: nil, color: UIColor.clear, size: spriteSize)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(level: Int) {
self.level = level
self.texture = textureForLevel(level: level)
}
func textureForLevel(level: Int) -> SKTexture {
return SKTexture(imageNamed: "TestTexture")
}
Your code works well. In fact this SO answer is correct.
About SKSpriteNode subclass, the update method is a new custom function added by you, because the instance method update(_:) is valid only for SKScene class, this just to say that this function is not performed if it is not called explicitly.
To make a test about your class you can change your code as below( I've changed only textureForLevel method only to make this example):
class CharacterNode: SKSpriteNode {
let spriteSize = CGSize(width: 70, height: 100)
var level = 0
init(level: Int) {
self.level = level
super.init(texture: nil, color: UIColor.clear, size: spriteSize)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(level: Int) {
self.level = level
self.texture = textureForLevel(level: level)
}
func textureForLevel(level: Int) -> SKTexture {
if level == 3 {
return SKTexture(imageNamed: "TestTexture3.jpg")
}
return SKTexture(imageNamed: "TestTexture.jpg")
}
}
class GameScene: SKScene {
override func didMove(to view: SKView) {
let characterNode = CharacterNode(level:2)
characterNode.update(level:2)
addChild(characterNode)
characterNode.position = CGPoint(x:self.frame.midX,y:self.frame.midY)
characterNode.run(SKAction.wait(forDuration: 2.0), completion: {
characterNode.update(level:3)
})
}
}
As you can see when you launch GameScene a characterNode sprite will be shown in the center of the screen. After 2 seconds, the texture will change.
I get the error written in the title. I'm new to this all. Any help appreciated.
This is the GameScene.swift file:
class GameScene: SKScene {
let player = SKSpriteNode(imageNamed: "shuttle")
let bulletSound = SKAction.playSoundFileNamed("torpedo.mp3", waitForCompletion: false)
let gameArea: CGRect
override init(size: CGSize) {
let maxRatio: CGFloat = 16.0/9.0
let playableWidth = size.height / maxRatio
let margin = (size.width - playableWidth) / 2
gameArea = CGRect(x: margin - size.width / 2, y: size.height / 2, width: playableWidth, height: size.height)
super.init(size: size)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
I would recommend you to read about initialization process. Swift preforms two step initialization. First phase is intialization of all stored properties. Also Swift compiler perform few safety checks to make sure two phase initialization is performed correctly. This is from the docs:
Safety check 1
A designated initializer must ensure that all of the properties introduced by its class are initialized before it delegates up to a
superclass initializer.
So you can either initialize your property inside of an initializer:
required init?(coder aDecoder: NSCoder) {
self.gameArea = CGRect()
super.init(coder: aDecoder)
}
or at property declaration (and optionally declaring it as var):
var gameArea = CGRect()
If you don't make it var, after initialization (at declaration) you won't be able to change it later on. Same goes when you initialize a property inside of an initializer. If it is a let constant, further modifications are not possible.