Swift creating a line inside a view that the user can resize - swift

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)

Related

missing argument for parameter in coding error in trying to subclass a uibezierPath

I want my swift code to display a uibezierPath button. The code uses override func draw to draw the button. The code is getting a compile error. Its telling me I am missing a parameter in let customButton = FunkyButton(coder: <#NSCoder#>) you can see the error in NSCODER. I dont know what to put for nscoder. What do you think I should put?
import UIKit
class ViewController: UIViewController {
var box = UIImageView()
override open var shouldAutorotate: Bool {
return false
}
// Specify the orientation.
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscapeRight
}
let customButton = FunkyButton(coder: <#NSCoder#>)
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(box)
// box.frame = CGRect(x: view.frame.width * 0.2, y: view.frame.height * 0.2, width: view.frame.width * 0.2, height: view.frame.height * 0.2)
box.backgroundColor = .systemTeal
customButton!.backgroundColor = .systemPink
self.view.addSubview(customButton!)
customButton?.addTarget(self, action: #selector(press), for: .touchDown)
}
#objc func press(){
print("hit")
}
}
class FunkyButton: UIButton {
var shapeLayer = CAShapeLayer()
let aPath = UIBezierPath()
override func draw(_ rect: CGRect) {
let aPath = UIBezierPath()
aPath.move(to: CGPoint(x: rect.width * 0.2, y: rect.height * 0.8))
aPath.addLine(to: CGPoint(x: rect.width * 0.4, y: rect.height * 0.2))
//design path in layer
shapeLayer.path = aPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.path = aPath.cgPath
// draw is called multiple times so you need to remove the old layer before adding the new one
shapeLayer.removeFromSuperlayer()
layer.addSublayer(shapeLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isHidden == true || self.alpha < 0.1 || self.isUserInteractionEnabled == false {
return nil
}
if aPath.contains(point) {
return self
}
return nil
}
}
When instantiating FunkyButton, don’t manually call the coder rendition. Just call
let button = FunkyButton()
Or add it in IB and hook up an outlet to
#IBOutlet weak var button: FunkyButton!
In FunkyButton, you shouldn't update shape layer path inside draw(_:) method. During initialization, just add the shape layer to the layer hierarchy, and whenever you update the shape layer’s path, it will be rendered for you. No draw(_:) is needed/desired:
#IBDesignable
class FunkyButton: UIButton {
private let shapeLayer = CAShapeLayer()
private var path = UIBezierPath()
// called if button is instantiated programmatically (or as a designable)
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
// called if button is instantiated via IB
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
// called when the button’s frame is set
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard path.contains(point) else {
return nil
}
return super.hitTest(point, with: event)
}
}
private extension FunkyButton {
func configure() {
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1
layer.addSublayer(shapeLayer)
}
func updatePath() {
path = UIBezierPath()
path.move(to: CGPoint(x: bounds.width * 0.2, y: bounds.height * 0.8))
path.addLine(to: CGPoint(x: bounds.width * 0.4, y: bounds.height * 0.2))
path.addLine(to: CGPoint(x: bounds.width * 0.2, y: bounds.height * 0.2))
path.close()
shapeLayer.path = path.cgPath
}
}
If you really want to draw your path in draw(_:), that is an acceptable pattern, too, but you wouldn't use CAShapeLayer at all, and just manually stroke() the UIBezierPath in draw(_:). (If you implement this draw(_:) method, though, do not use the rect parameter of this method, but rather always refer back to the view’s bounds.)
Bottom line, either use draw(_:) (triggered by calling setNeedsDisplay) or use CAShapeLayer (and just update its path), but don't do both.
A few unrelated observations related to my code snippet:
You do not need to check for !isHidden or isUserInteractionEnabled in hitTest, as this method won't be called if the button is hidden or has user interaction disabled. As the documentation says:
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01.
I have also removed the alpha check in hitTest, as that is non-standard behavior. It is not a big deal, but this is the sort of thing that bites you later on (e.g. change button base class and now it behaves differently).
You might as well make it #IBDesignable so that you can see it in Interface Builder (IB). There is no harm if you're only using it programmatically, but why not make it capable of being rendered in IB, too?
I have moved the configuration of the path into layoutSubviews. Anything based upon the bounds of the view should be responsive to changes in the layout. Sure, in your example, you are manually setting the frame, but this is an unnecessary limitation to place on this button class. You might use auto-layout in the future, and using layoutSubviews ensures that it will continue to function as intended. Plus, this way, the path will be updated if the size of the button changes.
There's no point in checking for contains if the path is a line. So, I've added a third point so that I can test whether the hit point falls within the path.

IBInspectable variable value is not changing in interface builder and simulator

I am creating custom parallelogram view and view is rendering fine with default offset value. But when i change the offset value from ib it's not working
#IBDesignable class CustomParallelogramView: UIView {
#IBInspectable var offset: Float = 10.0
var path = UIBezierPath()
lazy var shapeLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.path = path.cgPath
return layer
}()
override init(frame: CGRect) {
super.init(frame:frame)
drawParallelogram()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
drawParallelogram()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
drawParallelogram()
}
override func layoutSubviews() {
super.layoutSubviews()
updateFrame()
}
func drawParallelogram() {
print(offset)
path.move(to: CGPoint(x: bounds.minX + CGFloat(offset), y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX - CGFloat(offset), y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
self.layer.mask = shapeLayer
}
func updateFrame() {
shapeLayer.frame = bounds
}
}
I changed the offset value from IB but it doesn't changes IB and simulator
A couple of thoughts:
As Daniel said, you really want to call drawParallelgram in your offset observer.
Also, in layoutSubviews, you’re updating the frame of your shape layer. You want to reset its path and update your mask again.
You’re just adding more and more strokes to your UIBezierPath. You probably just want to make it a local variable and avoid adding more and more strokes to the existing path.
The prepareForInterfaceBuilder suggests some misconception about the purpose of this method. This isn’t for doing initialization when launching from IB. This is for doing some special configuration, above and beyond what is already done by the init methods, required by IB.
E.g. If you have a sophisticated chart view where you’ll be programmatically providing real chart data programmatically later, but you wanted to see something in IB, nonetheless, you might have prepareForInterfaceBuilder populate some dummy data, for example. But you shouldn’t repeat the configuration you already did in init methods.
It’s not relevant here (because I’m going to suggest getting rid of these init methods), but for what it’s worth, if I need to do configuration during init, I generally write two init methods:
override init(frame: CGRect = .zero) {
super.init(frame: frame)
<#call configure routine here#>
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
<#call configure routine here#>
}
Note that in init(frame:) I also supply a default value of .zero. That ensures that I’m covered in all three scenarios:
CustomView()
CustomView(frame:); or
if the storyboard calls CustomView(decoder:).
In short, I get three init methods for the price of two. Lol.
All that being said, you can greatly simplify this:
#IBDesignable public class CustomParallelogramView: UIView {
#IBInspectable public var offset: CGFloat = 10 { didSet { updateMask() } }
override public func layoutSubviews() {
super.layoutSubviews()
updateMask()
}
private func updateMask() {
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX + offset, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX - offset, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
layer.mask = shapeLayer
}
}
Try:
#IBInspectable var offset: Float = 10.0 {
didSet {
drawParallelogram()
}
}

I can't get an arc to fill in Swift 4

I have a class that makes a UIView of a circle. I when I add the circle to my view, it shows the red outline, but the interior is clear instead of black. I want it to have a black fill.
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setLineWidth(1.0);
UIColor.red.set()
let center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let radius = (frame.size.width - 10)/2
context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.strokePath()
context.closePath()
context.setFillColor(UIColor.black.cgColor)
context.fillPath()
}
}
}
This is more easily solved with a UIBezierPath.
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: bounds)
path.lineWidth = 1
UIColor.red.setStroke()
UIColor.black.setFill()
path.fill()
path.stroke()
}
If you look to the documentation of strokePath():
The current path is cleared as a side effect of calling this function.
Basically, after stroking your path, your paths is reset and there is nothing to fill.
A workaround could be to save context.path first and then use context.addPath(path). However, I usually prefer to build a separate CGMutablePath first and then add it to the context.

Custom Class Not Drawing at Center - Swift

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

How to draw a line between two points over an image in swift 3?

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)