macOS draw a rectangle with two holes inside - swift

I have one big CGRect and two small CGRect inside. I want to draw the big CGRect in red and to form two transparent holes corresponding to the small CGRect.
I am not able to do it. I have tried to use NSBezierPath but in macOS there is no method NSBezierPath.CGPath like in UIBezierPath for iOS.

You don't have to use Core Graphics. You can create a NSView subclass and just stroke/fill the path in draw(_:). In Swift 3:
class HolyView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path = ... // build the `NSBezierPath` however you want
NSColor.blue.setFill()
path.fill()
}
}
You can then add that view programmatically, or you can make it #IBDesignable and add it directly on your storyboard.

Related

How to make a CAShapeLayer have a blur effect?

I have this CAShapeLayer that I want to have a blur effect. How would I be able to do that?
EDIT
I tried it this way but the blur view doesn't show. Anyone know why? Thanks!
func createLayer(in rect: CGRect) -> CAShapeLayer{
let effectView = UIVisualEffectView(effect:UIBlurEffect(style: .regular))
effectView.frame = rect
let view = UIView(frame: rect)
view.addSubview(effectView)
let mask = CAShapeLayer()
mask.frame = rect
mask.cornerRadius = 10
effectView.layer.mask = mask
maskLayer.append(mask)
layer.insertSublayer(mask, at: 1)
return mask
}
The short answer: You don't. You can add a visual effects view (UIVisualEffectView) of type blur (a UIBlurEffect) on top of the shape layer's view, or you could write code that takes the contents of the shape layer, applies a Core Image filter to it, and copies the output to another layer.
Using a UIVisualEffectView is a lot easier than working with Core Image filters, but a visual effect view operates on a view, not a layer. You'll need to make the shaper layer be part of the layer's layer hierarchy in order to use it.
Edit:
Your code has errors and doesn't really make sense. Your method createLayer (which I guess is a view controller instance method?) creates and returns a shape layer.
That method creates a throw-away UIView that is never added to the view hierarchy, nor passed back to the caller. That view will get deallocated as soon as your method returns.
Next you create a visual effects view and make that a subview of the throw-away view. Since the only place that view is attached is to the throw-away view, it will also get deallocated as soon as your method returns.
Next you create a shape layer and set it up as the mask of some other layer maskLayer, which you don't explain. Nor do you install a path into the shape layer.
If you have a view called shapeView, of class ShapeView, and you want to attach a visual effects view to it, you could use code like this:
class ViewController: UIViewController {
#IBOutlet weak var shapeView: ShapeView!
var blurView: UIVisualEffectView?
override func viewDidLoad() {
super.viewDidLoad()
blurView = UIVisualEffectView(effect:UIBlurEffect(style: .regular))
blurView?.frame = shapeView.frame
//Add the blur view on top of the shape view
view.addSubview(blurView!)
}
override func viewDidLayoutSubviews() {
//Update the blurView's frame if needed
blurView?.frame = shapeView.frame
}
}

How can I round the corners of only one side of an NSButton in Swift 4?

I am new to Swift and Stack Overflow in general, so I hope I'm not asking much to bear with me.
I am trying to achieve a 'grouped' button style that can be found on Finder or in the XCode editor toolbar, like these two button groups. As you can see in the first group of buttons, the left button is only rounded on the left side, the centre button is not rounded at all, and the right button is only rounded on the right side. The same thing applies to the second group of buttons. I want to accomplish something like this, but I'm unsure of how to achieve this.
After searching for a solution online (including iOS tutorials), I tried providing an extension to the NSButton class and manually rounding the two left corners like so:
// Extensions.swift
extension NSButton {
func roundLeftCorners() {
self.layer?.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
self.layer?.cornerRadius = 20.0 // Some arbitrary number, just wanted to make the rounded corner visible
self.layer?.masksToBounds = true
}
}
Then, on my view controller's viewDidLoad() function, I tried calling this member:
// MyViewController.swift
class MyViewController: NSViewController {
#IBOutlet weak var leftButton: NSButton!
override func viewDidLoad() {
super.viewDidLoad()
leftButton.roundLeftCorners()
}
// ...
}
...but that didn't work for me. Some simple debugging showed that the Optional values of self.layer were nil, so I'm not sure what's going on there.
Next, I tried creating my own custom class and overriding the draw(_ dirtyRect:) function with the same code above, like so:
// LeftButton.swift
class LeftButton: NSButton {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
self.layer?.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
self.layer?.cornerRadius = 20.0
self.layer?.masksToBounds = true
}
}
// MyViewController.swift
class MyViewController: NSViewController {
#IBOulet weak var leftButton: LeftButton!
// ...
}
...but that didn't remove the rounded corners on the right side. Weirdly enough, the new cornerRadius value is only obvious if the number is around 50.0 or greater; any less and the left corners looks exactly the same as any other NSButton.
Some answers mentioned manually drawing the points in a path with NSBezierPath, but it doesn't achieve what I want. I also can't find any related properties/attributes on the Storyboard editor. Perhaps I've overcomplicated my approach to this seemingly easy problem, or maybe I'm not looking at it the right way, but I hope someone could help me with this. Thanks in advance!
1) The images you showed are using a simple NSSegmentedControl. Nothing needs customized.
2) What you tried to do wouldn't work anyway; If it could mechanically work, what it would end up doing is merely clipping the drawn content on the left corners. It wouldn't magically fill in drawing on the right, and create appropriate control borders etc.
AppKit controls are not merely CALayers with filled in properties like border, background, etc. They are almost all entirely drawn using Core Graphics via the classic drawRect: method one way or another. The fact that views have a layer is due to layer-backing. There are very few things you can end up doing with the layer of an existing control. To customize them properly, you would override the standard drawing routines in NSView, NSControl, NSCell, etc as appropriate.

How to make Scroller background transparent in NSScrollView?

I've used Storyboard to set up the NSScrollView and I cannot find any option where I can disable the scroller's background. Any ideas on how to make this happen?
Basically idea behind this is subclass the NSScroller and then make changes accordingly.
I had the same requirement so I subclassed it and did the changes as shown.
objective c code is converted with help of online tool so apologies for mistakes
and have look.
this may help.
// Converted to Swift 4 by Swiftify v4.1.6766 - https://objectivec2swift.com/
// GridScroller.h
// Created by Vikram on 08/09/16.
import Cocoa
class OpaqGridScroller: NSScroller {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.clear.setFill()
dirtyRect.fill()
// whatever style you want here for knob if you want
knobStyle = .dark
}
}
You can try override draw scroller rect
override func draw(_ dirtyRect: NSRect) {
NSDrawWindowBackground(bounds);
self.drawKnob()
}
You always can draw custom rect color by setting it before
override func draw(_ dirtyRect: NSRect) {
NSColor.clear.set()
__NSRectFill(dirtyRect)
self.drawKnob()
}
This solution worked for me.
class OpaqueGridScroller: NSScroller {
override func draw(_ dirtyRect: NSRect) {
// NSColor.clear.set()
// dirtyRect.fill()
self.drawKnob()
}
}
The foregoing answers that call self.drawKnob() are only partially correct. The problem is that the knob should usually not be drawn at all if there's nothing to scroll in that direction.
For my solution, I sub-classed the vertical NSScroller (scroll bar) in a scrolling NSTextView, in order to override the scroll bar's draw() method, as above. But when there was nothing to scroll, this draws a knob that has a length that's not correct.
So I added
#IBOutlet var myScroller : NSScrollView!
as a property of the sub-classed NSScroller and wired it up in the storyboard, so that the scroller bar can know the NSScrollView that uses it, and then draw the scroll bar with
override draw(_ dirtyRect: NSRect)
{
if myScroller.contentSize.height < myScroller.documentView!.bounds.height {
self.drawKnob()
}
}
The knob disappears when the NSScrollView is resized enough that there's nothing vertical to scroll, and re-appears when there is, while at the same time allowing whatever's in the background below it to show through.
I'm not sure if this is a hack, or the correct solution, but it seems to work as expected.

Can't override NSTableViewRow draw() behaviour to make transparent in NSOutlineView

I have an NSOutlineView with custom NSTableViewRows used throughout.
I have overridden the draw method on the NSTableViewRow:
override func draw(_ dirtyRect: NSRect) {}
...so it should never draw anything. However, rows are occasionally solid black, and occasionally clear. I can't work out a pattern to when.
If I do put something in the draw function, it will be drawing over the black when it occurs, I can't seem to clear the black in the draw function, other than by filling with a solid colour.
To clear I have tried:
let context = NSGraphicsContext.current?.cgContext
context?.clear(dirtyRect)
and
NSColor.clear.setFill()
dirtyRect.fill(using: .copy)
If I look in the Debug View Hierarchy I can clearly see that it is the NSTableViewRow itself that is black.
I have tried setting wantsLayer and setting the backgroundColor of the layer in the draw function but that has no effect.
Can anyone explain where this black fill may be coming from and where it lives!
The only way I managed to ensure it wasn't there was to use:
override var wantsUpdateLayer: Bool { get { return true } }
...which suggests that NSTableViewRow is doing something a little weird.
(copied from comments now that we've discovered a workaround)
The documentation for NSTableRowView states that it "is responsible for displaying attributes associated with the row, including the selection highlight, and group row look." So the base row view class is clearly doing something, and the table view probably makes assumptions about it, and I would image it's tricky and highly optimized. :(
A workaround would be to call super.draw(dirtyRect) in your draw(_:NSRect) just to let the base NSTableRowView class do whatever internal magic it needs to do, and then erase whatever it has drawn and draw over that.
I actually thought of a possibly-less-hacky solution: Add an opaque subview to NSTableRowView that completely fills its bounds and draw whatever you're trying to draw in that subview.
Subclass NSTableRowView add override isOpaque
Swift:
override var isOpaque: Bool {
get {
return false
}
set {
}
}
Obj-C:
- (BOOL)isOpaque {
return NO;
}
Take in a consideration that there are a lot of drawings. Like:
override func drawBackground(in dirtyRect: NSRect) {
override func drawSelection(in dirtyRect: NSRect) {
override func drawSeparator(in dirtyRect: NSRect) {
and more...

How to autoresize sublayers

I have an imageview and on button click I draw some bezier paths and some text layer too. On undo, I've removed last drawn path and text layer.
Now I want to resize my image View to set up in uper half portion of view and duplicated in lower half view, and want to resize all layers at the same time and also want to continue drawing after that.
Its too late to answer...
But may be this can help others. There is no autoresize masks for CALayers
You can manage that with some tricks..
- (void)layoutSubviews {
// resize your layers based on the view's new frame
layer.frame = self.bounds;
}
This will work if you have created a subclass of UIView. If not that, you can just set the bounds to the frame of CALayer anywhere if you have reference to your layer.
At least there are 3 options:
use delegate of CALayer
override layoutSublayers, and call it by setNeedsLayout()
use extension and call view's fitLayers
Example below, option 3:
Swift 5, 4
Code
extension UIView {
func fitLayers() {
layer.fit(rect: bounds)
}
}
extension CALayer {
func fit(rect: CGRect) {
frame = rect
sublayers?.forEach { $0.fit(rect: rect) }
}
}
Example
// inside UIViewController
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
view.fitLayers()
}
// or
// inside UIView
override func layoutSubviews() {
super.layoutSubviews()
fitLayers()
}