MacOS does not display NSBezierPath line - swift

I'm trying to draw a line in my MacOS app but I do not see the line. What can be a problem?
My code looks like:
func addLine() {
let path = NSBezierPath()
path.move(to: NSPoint(x: 100.0, y: 100))
path.line(to: NSPoint(x: 200.0, y: 200.0))
NSColor.green.setFill()
NSColor.green.setStroke()
path.close()
path.stroke()
}
And I call it in:
override func viewDidLoad() {
super.viewDidLoad()
addLine()
}
Am I doing something wrong? I just do not see anything in my window.

Have you created your own subclass of a NSView?
If I create a new view and add your code like so:
import Cocoa
class MyView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
addLine()
}
func addLine() {
let path = NSBezierPath()
path.move(to: NSPoint(x: 100.0, y: 100))
path.line(to: NSPoint(x: 200.0, y: 200.0))
NSColor.green.setFill()
NSColor.green.setStroke()
path.close()
path.stroke()
}
}
And I then - in a storyboard - drag a "Custom View" to the canvas, change the type of the view to be MyView like so
Then I see this when running the app:
If you prefer to add the view in code, you can do something like this:
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let myView = MyView()
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
myView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
myView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
myView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
}
So, your code seems to work, I'm just not sure how you are trying to use it.
Hope that gives you something to continue with.

Related

How to update ScrollView Width after device rotation?

I am following a tutorial to create a swift app that adds a number of views to a scroll view and then allows me to scroll between them. I have the app working and understand it for the most part. When I change the orientation of the device the views width don't get updated so I have parts of more than one view controller on the screen? Does anybody know how to fix this? From my understanding I need to call something in the viewsDidTransition method to redraw the views. Any help would be greatly appreciated. Thanks!
Here is what I have so far:
import UIKit
class ViewController: UIViewController {
private let scrollView = UIScrollView()
private let pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.numberOfPages = 5
pageControl.backgroundColor = .systemBlue
return pageControl
}()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
pageControl.addTarget(self,
action: #selector(pageControlDidChange(_:)),
for: .valueChanged)
scrollView.backgroundColor = .red
view.addSubview(scrollView)
view.addSubview(pageControl)
}
#objc private func pageControlDidChange(_ sender: UIPageControl){
let current = sender.currentPage
scrollView.setContentOffset(CGPoint(x: CGFloat(current) * view.frame.size.width,
y: 0), animated: true)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
pageControl.frame = CGRect(x: 10, y: view.frame.size.height - 100, width: view.frame.size.width - 20, height: 70)
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height - 100)
if scrollView.subviews.count == 2 {
configureScrollView()
}
}
private func configureScrollView(){
scrollView.contentSize = CGSize(width: view.frame.size.width*5, height: scrollView.frame.size.height)
scrollView.isPagingEnabled = true
let colors: [UIColor] = [.systemRed, .systemGray, .systemGreen, .systemOrange, .systemPurple]
for x in 0..<5{
let page = UIView(frame: CGRect(x: CGFloat(x) * view.frame.size.width, y: 0, width: view.frame.size.width, height: scrollView.frame.size.height))
page.backgroundColor = colors[x]
scrollView.addSubview(page)
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
print("hello world")
}
}
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
pageControl.currentPage = Int(floorf(Float(scrollView.contentOffset.x) / Float(scrollView.frame.size.width)))
}
}

Cocoa: Set NSView mask as CAShapeLayer

Hi I am trying to set CAShapeLayer inside to NSView. But I can't set corner radius of the CAShapeLayer outside of draw(_ dirtyRect: NSRect) function.
I have to use CAShapeLayer to set NSBezierPath to draw custom shapes. So, that only I am using CAShapeLayer instead of the NSView's default CALayer.
Here is the code:
extension NSBezierPath
extension NSBezierPath {
var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< elementCount {
let type = element(at: i, associatedPoints: &points)
switch type {
case .moveTo: path.move(to: points[0])
case .lineTo: path.addLine(to: points[0])
case .curveTo: path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath: path.closeSubpath()
#unknown default:
fatalError("Unknown!")
}
}
return path
}
}
CustomView.swift
class CustomView: NSView{
let shapeLayer = CAShapeLayer()
var cornerRadius: CGFloat {
set{
let path = NSBezierPath(roundedRect: bounds, xRadius: newValue, yRadius: newValue)
shapeLayer.path = path.cgPath
path.close()
}
get{
return shapeLayer.cornerRadius
}
}
var backgroundColor: NSColor{
set{
shapeLayer.fillColor = newValue.cgColor
}
get{
return NSColor(cgColor: shapeLayer.backgroundColor ?? .clear)!
}
}
init() {
super.init(frame: .zero)
wantsLayer = true
shapeLayer.masksToBounds = true
layer?.addSublayer(shapeLayer)
// layer?.mask = shapeLayer
// layer?.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
shapeLayer.frame = dirtyRect
let path = NSBezierPath(rect: dirtyRect)
shapeLayer.path = path.cgPath
// shapeLayer.cornerRadius = 22
shapeLayer.borderColor = NSColor.black.cgColor
shapeLayer.borderWidth = 2.0
path.close()
}
}
ViewController.swift
class ViewController: NSViewController {
private lazy var customView: CustomView = {
let customView = CustomView()
view.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
customView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
customView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
customView.heightAnchor.constraint(equalToConstant: 44),
customView.widthAnchor.constraint(equalToConstant: 144)
])
return customView
}()
override func viewDidLoad() {
super.viewDidLoad()
// customView.layer?.cornerRadius = 22
customView.backgroundColor = .red
customView.cornerRadius = 22
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
Current output:
Expected Output:
Code is commented in the attached codes..
Please help me to solve this problem. Thank you in advance...
The following is a simple example. First, draw a simple rectangle with a subclass of NSView. So instantiate it to make an object with a view controller. Then use object's layer to make it a round rect with a stroke color.
// Subclass of `NSView` //
import Cocoa
class MyView: NSView {
var fillColor: NSColor
init(frame: CGRect, fillColor: NSColor){
self.fillColor = fillColor
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: NSRect) {
super.draw(rect)
let path = NSBezierPath()
path.move(to: CGPoint.zero)
path.line(to: CGPoint(x: 0.0, y: self.frame.height))
path.line(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.line(to: CGPoint(x: self.frame.width, y: 0.0))
path.line(to: CGPoint.zero)
fillColor.set()
path.fill()
}
}
// View controller //
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rect = CGRect(origin: CGPoint(x: 50, y: 50), size: CGSize(width: 200, height: 50))
let myView = MyView(frame: rect, fillColor: NSColor.red)
myView.wantsLayer = true
if let myLayer = myView.layer {
myLayer.cornerRadius = 24.0
myLayer.borderWidth = 4.0
//myLayer.borderColor = NSColor.green.cgColor
}
view.addSubview(myView)
}
}

make only points within UIBezierPath button selectable

My swift code features a custom shape button made from a UIBezierPath. The code should only call the func press if it the user toches the red part if the user touches the green part the func press should not be called. Right now if you touch the green part the func is still called.
import UIKit
class ViewController: UIViewController {
let customButton = UIButton(frame: CGRect(x: 100.0, y: 100.0, width: 200.0, height: 140.0))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
customButton.backgroundColor = UIColor.green
let aPath = UIBezierPath()
aPath.move(to: CGPoint(x: 50, y: 0))
aPath.addLine(to: CGPoint(x: 200.0, y: 40.0))
aPath.addLine(to: CGPoint(x: 160, y: 140))
aPath.addLine(to: CGPoint(x: 40.0, y: 140))
aPath.addLine(to: CGPoint(x: 0.0, y: 40.0))
aPath.close()
let layer = CAShapeLayer()
layer.fillColor = UIColor.red.cgColor
layer.strokeColor = UIColor.red.cgColor
layer.path = aPath.cgPath
customButton.layer.addSublayer(layer)
self.view.addSubview(customButton)
customButton.addTarget(self, action: #selector(press), for: .touchDown)
}
#objc func press(){
print("hit")
}
}
My solution to the problem is to subclass UIButton into a FunkyButton. That button contains the path information. FunkyButton overrides the hitTest(_:event:) method, and checks if the point is contained in the path.
Give this a whirl:
class ViewController: UIViewController {
let customButton = FunkyButton(frame: CGRect(x: 100.0, y: 100.0, width: 200.0, height: 140.0))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
customButton.backgroundColor = UIColor.green
self.view.addSubview(customButton)
customButton.addTarget(self, action: #selector(press), for: .touchDown)
}
#objc func press(){
print("hit")
}
}
class FunkyButton: UIButton {
let aPath = UIBezierPath()
override init (frame : CGRect) {
super.init(frame : frame)
aPath.move(to: CGPoint(x: 50, y: 0))
aPath.addLine(to: CGPoint(x: 200.0, y: 40.0))
aPath.addLine(to: CGPoint(x: 160, y: 140))
aPath.addLine(to: CGPoint(x: 40.0, y: 140))
aPath.addLine(to: CGPoint(x: 0.0, y: 40.0))
aPath.close()
let shapedLayer = CAShapeLayer()
shapedLayer.fillColor = UIColor.red.cgColor
shapedLayer.strokeColor = UIColor.red.cgColor
shapedLayer.path = aPath.cgPath
layer.addSublayer(shapedLayer)
}
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
}
}
First things first add your constraint for your custom buttom so community trying to solve problem recognize your problem easily. For example;
customButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
customButton.widthAnchor.constraint(equalToConstant: 250),
customButton.heightAnchor.constraint(equalToConstant: 100),
customButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
customButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])
Your problem is in your code, you are adding padding to your layer over of your layer already exists in your button itself. So can't recognize your event responder and your function naturally doesn't work.
yourGreenPartWhichYouClickable.isUserInteractionEnabled = false
yourRedPartWhichYouClickable.isUserInteractionEnabled = false

draw line in place of x axis in view controller

I want my swift code to draw a line like the place of a x axis. So the line is in the center of the screen no matter what screen size the object is displayed in. My code below produces a line but it does not account for the device size. My image below displays a black line which is what my current code produces and the orange line is the desired output.
Pic
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let lineView = UIView(frame: CGRect(x: 0, y: 100, width: 320, height: 1.0))
lineView.layer.borderWidth = 1.0
lineView.layer.borderColor = UIColor.black.cgColor
self.view.addSubview(lineView)
}
}
To draw a line in the middle of the screen, just assign the Line.swift file to UIView on which you want to draw a line.
Line.swift file
class Line:UIView {
var line = UIBezierPath()
func drawLine() {
line.move(to: CGPoint(x: 0, y: bounds.height / 2))
line.addLine(to: CGPoint(x: (bounds.width) , y: bounds.height / 2))
UIColor.black.setStroke()
line.lineWidth = 0.1
line.stroke()
}
override func draw(_ rect: CGRect) {
drawLine()
}
}
To assign it to your UIView, open the main.storyboard and click on the view where you want to draw the line. Then, go into upper right tabs which would look like this
You can see inside the class field, there is a placeholder called "UIView". Type there Line and hit enter, and then just run the project. You 'll be able to see the line in the middle of the view. No need to declare or call any function. Just assign this class and run the project.
If you want to respond with the width of the view, you can use constraints:
override func viewDidLoad() {
super.viewDidLoad()
let lineView = UIView()
lineView.translatesAutoresizingMaskIntoConstraints = false
lineView.layer.borderWidth = 1
lineView.layer.borderColor = UIColor.black.cgColor
view.addSubview(lineView)
NSLayoutConstraint.activate([
lineView.leftAnchor.constraint(equalTo: view.leftAnchor),
lineView.rightAnchor.constraint(equalTo: view.rightAnchor),
lineView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
lineView.heightAnchor.constraint(equalToConstant: 1)
])
}
For what it’s worth, rather than drawing lines using the border of a view, I'd define a UIBezierPath and use that for a CAShapeLayer’s path:
#IBDesignable
class GraphView: UIView {
let shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 2
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
return shapeLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
layer.addSublayer(shapeLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX, y: 100))
path.addLine(to: CGPoint(x: bounds.maxX, y: 100))
shapeLayer.path = path.cgPath
}
}
Then you can add this view to your view hierarchy like shown above, or right in Interface Builder (obviously, defining the constraints in IB, too).

Drawing a circle in Swift (MacOS)

I've found this code for drawing the line for MacOS app.
class Drawing: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let context = NSGraphicsContext.current?.cgContext;
context!.beginPath()
context!.move(to: CGPoint(x: 0.0, y: 0.0))
context!.addLine(to: CGPoint(x: 100.0, y: 100.0))
context!.setStrokeColor(red: 1, green: 0, blue: 0, alpha: 1)
context!.setLineWidth(1.0)
context!.strokePath()
}
override func viewDidLoad() {
super.viewDidLoad()
let dr = Drawing(frame: NSRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(dr)
}
How to change this code for circle? It's difficult for me to solve this problem. Help me, please.
The circle equivalent is
class Drawing: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let context = NSGraphicsContext.current!.cgContext
context.saveGState()
context.setFillColor(NSColor.red.cgColor)
context.fillEllipse(in: dirtyRect)
context.restoreGState()
}
}
or the classic NSBezierPath way
class Drawing: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let fillColor = NSColor.red
let path = NSBezierPath(ovalIn: dirtyRect)
fillColor.setFill()
path.fill()
}
}
Depending on the side of the circle, you can also do this:
class YourParentView: NSView {
// Create it as a view of its own
let circleView = NSView()
circleView.wantsLayer = true
circleView.layer?.cornerRadius = 7
//depending on the size, adjust this
//and that's it. Now it's a circle.
//Then just addict the style
circleView.layer?.backgroundColor = NSColor.green.cgColor
circleView.layer?.borderColor = NSColor.white.cgColor
//Be sure to add it to the parent view
self.view.addSubview(circleView)
}