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

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.

Related

How can I lower the view into the safe area?

I'm practicing using snapkit to place ui of view.
However, I tried many things to move the red box into the safe area under the notch, but I couldn't find a way.
var redView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(redView)
redView.backgroundColor = .red
redView.snp.makeConstraints{ make in
make.top.equalTo(view.safeAreaInsets.top)
make.size.width.height.equalTo(100)
make.left.equalTo(view.snp.left)
}
here is my code.
Why doesn't it still come into the safe area even if I designate the red box tower as Safe Area Insets.top?
I would appreciate it if you could let me know my mistake.
Try this:
redView.snp.makeConstraints{ make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
make.size.width.height.equalTo(100)
make.left.equalTo(view.snp.left)
}

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

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

NSSearchField: How to hide icon and border?

This is kind of a duplicate of this question. Because everything I know about Swift is Swift3, I`m wondering if someone could "translate" the suggested solution in this answer.
Also:
I made a NSSearchfield without border, put it in a framed view, and it still shows the gray border. I would be curious of how to disable the animated gray border and maybe even how to change the color of the gray "search" line.
My ugly result now looks like this:
It would be a big help if someone could tell me how to manage this difficult NSSearchfield.
//UPDATE
According to firstinq´s answer, the icon now disappeared, which is great. But still, there is this disturbing animated gray border. Which I can´t understand: The NSSearchFielt is inside a NSView (blue border). So everything outside the NSView should be hidden, right?. So why am I still seeing the gray border? cell.isBordered = falsehas no effect.
Any advice how to handle that?
This is how I draw the border of the NSView:
class SearchFieldBorder: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
self.layer?.borderWidth = 1
self.layer?.borderColor = NSColor.blue.cgColor
}
}
To hide the icon: cast the cell to NSSearchFieldCell and set the cell's searchButtonCell to transparent. Possible swift3 version:
if let cell = self.searchField.cell as? NSSearchFieldCell {
cell.searchButtonCell?.isTransparent = true
}
Here searchField is an NSSearchField
To remove the focus border:
searchField.focusRingType = .none
To change grey line/cursor it would be better to subclass the NSSearchField and override the methods.
You can get an idea from here.
I'll supplement the answer above.
To hide the search icon, assign a nil to the SearchButtonCell property
if let cell = searchField.cell as? NSSearchFieldCell {
cell.searchButtonCell = nil
}

OSX Cocoa NSSearchField clear button not responding to click

I place an NSSearchField and set its border to none and I found that the clear button is not clickable a.k.a. not responding when clicked. If I set the border again it's working fine.
I've been debugging this for a few hours, and found out that when I set the border to none, the text editor width will expand and shadow (cover) the clear button.
Screenshot
View hierarchy debug screenshot
Steps to reproduce:
Create an empty cocoa project/app
Place an NSSearchField
Set border to none
Run the app, fill the search field and try to click the clear button
Is this a bug? Or is it intended to behave that way?
Note: Newbie in cocoa development
I faced with this problem and deemed it as a bug in Cocoa. But it is easy to fix in custom control or in a view controller. Just keep text field bordered in interface builder and then kill the border by having new CALayer. For example:
class ViewController: NSViewController {
#IBOutlet weak var searchField: NSSearchField!
override func viewDidLoad() {
super.viewDidLoad()
let maskLayer = CALayer()
searchField.layer = maskLayer
maskLayer.backgroundColor = searchField.backgroundColor?.CGColor
}
}
As you see, I am just restoring control color in new layer not preserving anything else. It is not perfect, but at least gives good start.
julia_v's answer is almost correct. You should also remove searchButtonWidth from rect.origin.x to offset rect back.
And also I've added some more logic to make these "tricks" only, when it needed.
override func select(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, start selStart: Int, length selLength: Int) {
var newRect = rect
if !isBordered || isBezeled {
let cancelButtonWidth = NSWidth(cancelButtonRect(forBounds: rect))
let searchButtonWidth = NSWidth(searchButtonRect(forBounds: rect))
newRect.size.width -= (cancelButtonWidth + searchButtonWidth)
newRect.origin.x += searchButtonWidth
}
super.select(withFrame: newRect, in: controlView, editor: textObj, delegate: delegate, start: selStart, length: selLength)
}
After creating subclass simply set it to NSSearchFieldCell instance in IB identity inspector.
Had the same problem in NSSearchField, created in code. Solved it by overriding the NSSearchFieldCell method in a subclass:
- (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(NSInteger)selStart length:(NSInteger)selLength
{
NSRect newRect = aRect;
newRect.size.width -= (NSWidth([self searchButtonRectForBounds:aRect]) + NSWidth([self cancelButtonRectForBounds:aRect]));
[super selectWithFrame:newRect inView:controlView editor:textObj delegate:anObject start:selStart length:selLength];
}
This method is called after the mouse click on the text area of the field. It also appeared to be a nice place to set the color of the insertion point.