Force Custom NSView from PaintCode to Redraw - swift

I created an icon in PaintCode that is drawn programmatically (but this question isn't necessarily specific to that tool) and I'm trying to get that icon to redraw.
I use a custom class like this:
class IconTabGeneral: NSView {
override func draw(_ dirtyRect: NSRect) {
StyleKitMac.drawTabGeneral()
}
}
The drawTabGeneral() is a method in the StyleKitMac class (generated by PaintCode) that looks like this (I'll omit all the bezierPath details):
#objc dynamic public class func drawTabGeneral(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 22, height: 22), resizing: ResizingBehavior = .aspectFit) {
//// General Declarations
let context = NSGraphicsContext.current!.cgContext
//// Resize to Target Frame
NSGraphicsContext.saveGraphicsState()
let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 22, height: 22), target: targetFrame)
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 22, y: resizedFrame.height / 22)
//// Bezier Drawing
let bezierPath = NSBezierPath()
...
bezierPath.close()
StyleKitMac.accentColor.setFill() ⬅️Custom color set here
bezierPath.fill()
NSGraphicsContext.restoreGraphicsState()
}
The accentColor defined there is a setting that can be changed by the user. I can't get my instance of IconTabGeneral to redraw to pick up the new color after the user changes it.
I've tried this without any luck:
iconGeneralTabInstance.needsDisplay = true
My understanding is that needsDisplay would force the draw function to fire again, but apparently not.
Any idea how I can get this icon to redraw and re-fill its bezierPath?

How about using a NSImageView? Here is a working example:
import AppKit
enum StyleKit {
static func drawIcon(frame: CGRect) {
let circle = NSBezierPath(ovalIn: frame.insetBy(dx: 1, dy: 1))
NSColor.controlAccentColor.setFill()
circle.fill()
}
}
let iconView = NSImageView()
iconView.image = .init(size: .init(width: 24, height: 24), flipped: true) { drawingRect in
StyleKit.drawIcon(frame: drawingRect)
return true
}
import PlaygroundSupport
PlaygroundPage.current.liveView = iconView

I think I got it. It turns out the PaintCode-generated StyleKitMac class was caching the color. The icon was, in fact, being redrawn after needsDisplay was being set. So I just had to refresh the cache's color value.

Related

how to remove border from UIVIEW

I want to know if there is a way to remove the border from view in swift after I have done this:
func addRightBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x: self.frame.size.width - width,y: 0, width:width,
height:self.frame.size.height)
self.layer.addSublayer(border)
}
I have tried this ->
func removeBorder(){
self.layer.sublayers?.removeAll()
}
but it doesn't work
You should simply save your own reference to this layer, rather than using sublayers. Using sublayers is not prudent as there might be layers other than your border layer.
Thus:
weak var borderLayer: CALayer?
func addRightBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x: bounds.maxX - width,
y: bounds.minY,
width: width,
height: bounds.height)
layer.addSublayer(border)
self.borderLayer = border
}
func removeBorder() {
borderLayer?.removeFromSuperlayer()
}
This obviously assumes that this was for a subclass of UIView and not an extension. If for the latter, you’d have to use an associated object to keep track of the border layer.
Note, one should not use frame when figuring out where to place the sublayer. Always use bounds.
Taking it a step further, you may want it to gracefully respond to frame changes, too:
override func layoutSubviews {
super.layoutSubviews()
borderLayer?.frame = CGRect(x: bounds.maxX - width,
y: bounds.minY,
width: width,
height: bounds.height)
}

How to order views' layers on top of each others in Swift

I added the redView as a subView of the textField and used AutoLayout to constraint it to be as it appears in the image, but the problem here now is
I want to bring the red view to be on top of the border
I tried to use this method
bringSubviewToFront(floatingPlaceholderContainer)
but it didn't work, also tried to use the layer.zPosition = .greatestFiniteMagnitude and it also didn't work
Edit-
the code I used and didn't work as I expected
override func layoutSubviews() {
super.layoutSubviews()
self.redlayer.backgroundColor = UIColor.red.cgColor
self.redlayer.frame = CGRect(x: 10, y: -10, width: 100, height: 30)
self.layer.addSublayer(self.redlayer)
self.redlayer.zPosition = 10
}
and this is the result
You need to configure the timing of when you're adding this layer. This approach works fine for me:
class MyTextField : UITextField {
let redlayer = CALayer()
override func layoutSubviews() {
super.layoutSubviews()
self.redlayer.backgroundColor = UIColor.red.cgColor
self.redlayer.frame = CGRect(x: 10, y: -10, width: 100, height: 30)
self.layer.addSublayer(self.redlayer)
self.redlayer.zPosition = 10
}
}

How to set a border only for one of the edges of UIView

In swift, is there a way to only set a border for top side of a UIView ?
There are many ways, but drawing the border yourself might offer a little more control. I would recommend subclassing a UIView and use a CAShapeLayer to accomplish this.
Something to the effect of (using Swift 3):
import UIKit
class TopBorderedView: UIView {
//decalare a private topBorder
fileprivate weak var topBorder: CAShapeLayer?
//declare a border thickness to allow outside access to setting it
var topThickness: CGFloat = 1.0 {
didSet {
drawTopBorder()
}
}
//declare public color to allow outside access
var topColor: UIColor = UIColor.lightGray {
didSet {
drawTopBorder()
}
}
//implment the draw method
fileprivate func drawTopBorder() {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: bounds.width, y: 0)
removeIfNeeded(topBorder)
topBorder = addBorder(from: start, to: end, color: topColor, thickness: topThickness)
}
//implement a private border drawing method that could be used for border on other sides if desired, etc..
fileprivate func addBorder(from: CGPoint, to: CGPoint, color: UIColor, thickness: CGFloat) -> CAShapeLayer {
let border = CAShapeLayer()
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
border.path = path.cgPath
border.strokeColor = color.cgColor
border.lineWidth = thickness
border.fillColor = nil
layer.addSublayer(border)
return border
}
//used to remove the border and make room for a redraw to be autolayout friendly
fileprivate func removeIfNeeded(_ border: CAShapeLayer?) {
if let bdr = border {
bdr.removeFromSuperlayer()
}
}
//override layoutSubviews() (probably debatable) and call the drawTopBorder method to draw and redraw if needed
override func layoutSubviews() {
super.layoutSubviews()
drawTopBorder()
}
}
For maximum reusability in the context of storyboards - I would also take a look at using #IBDesignable and #IBInspectable for common UI patterns like this. For a decent intro, checkout NSHipster: IBDesignable and IBInspectable

Drawing in cocoa swift

I've been trying to draw a simple line in a cocoa application using swift, however I fail to fully understand how to use the CGContext class.
I've created this class:
import Cocoa
class Drawing: NSView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
let context = NSGraphicsContext.currentContext()?.CGContext;
CGContextBeginPath(context)
CGContextMoveToPoint(context, 0, 0)
CGContextAddLineToPoint(context, 100, 100)
CGContextSetRGBStrokeColor(context, 1, 1, 1, 1)
CGContextSetLineWidth(context, 5.0)
CGContextStrokePath(context)
}
}
and used it in the main window like so
override func viewDidLoad() {
super.viewDidLoad()
let dr = Drawing()
self.view.addSubview(dr)
dr.drawRect(NSRect(x: 0, y: 0, width: 100, height: 100))
}
but it didn't do a thing
You need to initialise your Drawing view with a frame otherwise the system won't know where to draw it, like so:
override func viewDidLoad() {
super.viewDidLoad()
let dr = Drawing(frame: NSRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(dr)
}
Also as David said, you do not need to call drawRect yourself as it is automatically called by the system.
my 2 cents for swift 5.x Xcode 10:
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current?.cgContext else{
return
}
context.beginPath()
context.move(to: CGPoint.zero)
context.addLine(to: CGPoint( x: 100, y: 100))
context.setStrokeColor(CGColor( red: 1, green: 0, blue: 0, alpha: 1))
context.setLineWidth(5.0)
context.strokePath()
}
You don't need to add a view or use CoreGraphics instructions.
Simply do like this: ( EDIT: I added update exemple code )
class View: NSView {
var p1: NSPoint = .zero {
didSet { needsDisplay = true }
}
var p2: NSPoint = NSPoint(x: 100, y: 100) {
didSet { needsDisplay = true }
}
// Returns the rect using the two points
var rect: NSRect {
NSRect(x: p1.x, y: p1.y,
width: p2.x - p1.x, height: p2.y - p1.y)
}
// Draw a rectangle and its diagonal
override func draw(_ dirtyRect: NSRect) {
// Draw rect
let rect = self.rect
NSColor.red.setFill()
NSColor.black.setStroke()
rect.fill()
rect.stroke()
// Draw line
let path = NSBezierPath()
path.move(to: p1)
path.line(to: p2)
path.stroke()
}
}
Don't forget to call myView.needsDisplay = true to update content
From there, check auto completion to discover Cocoa drawing functions to draw paths, ovals, rounded rects. Cheers :)

Why won't my NSView draw something on click?

On a custom NSView I made, I overriden the mouseDown method this way :
override func mouseDown(theEvent: NSEvent) {
let location = self.convertPoint(theEvent.locationInWindow, fromView: nil)
NSColor.blackColor().set()
let basisRect = CGRect(origin: location, size: CGSize(width: 10, height: 10))
let roundPath = NSBezierPath(ovalInRect: basisRect)
roundPath.fill()
needsDisplay = true
}
But I doesn't draw anything when I click (even if the mouseDown method is triggered as I checked with a println())
Have you any idea of why nothing happens and how to fix it ?
EDIT (here's my drawRect function) :
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.blackColor().set()
let middlePoint = (self.bounds.size.width/2)
let basisRect = CGRect(origin: CGPoint(x: middlePoint-5, y: 10), size: CGSize(width: 10, height: 10))
let roundPath = NSBezierPath(ovalInRect: basisRect)
roundPath.fill()
let bottomLegPath = NSBezierPath()
bottomLegPath.moveToPoint(CGPoint(x: middlePoint, y: 10))
bottomLegPath.lineToPoint(CGPoint(x: middlePoint, y: middlePoint))
bottomLegPath.lineWidth = 5
bottomLegPath.stroke()
}
Thank you.
An NSView draws by calling drawRect:. Only what is in drawRect: matters. You do not show your drawRect: code, but I am betting you don't have any. That is where you need to draw your round path, not in the mouseDown implementation.