Is it possible to make a circular NSButton? - swift

I am trying to create a custom shape NSButton. In particular I am trying to make a round button, using a custom image. I've found a tutorial on the creation of custom UIButton and tried to adapt it to NSButton. But there's a huge problem. clipsToBounds seems to be iOS only(
Here's my code:
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var mainButton: NSButton!
var size = 32
override func viewDidLoad() {
super.viewDidLoad()
configureButton()
// Do any additional setup after loading the view.
}
func configureButton()
{
mainButton.wantsLayer = true
mainButton.layerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay
mainButton.layer?.cornerRadius = 0.5 * mainButton.bounds.size.width
mainButton.layer?.borderColor = NSColor(red:0.0/255.0, green:122.0/255.0, blue:255.0/255.0, alpha:1).CGColor as CGColorRef
mainButton.layer?.borderWidth = 2.0
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
}
What am I doing wrong? How can I make a circular NSButton? Can you suggest anything on replacing clipsToBounds?
Because here is what I was able to get so far:

NSButton is a subclass of NSView, so all methods in NSView, such as drawRect(_:), are also available in NSButton.
So create a new Button.swift which you draw your custom layout
import Cocoa
class Button: NSButton {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
// Drawing code here.
let path = NSBezierPath(ovalIn: dirtyRect)
NSColor.green.setFill()
path.fill()
}
}
Great Tutorial Here. It is iOS , but quite similar!

Don't play around with corner radius. A circle doesn't have corners. To make the button appear as a circle, mask the button's layer to a circle.

You are setting the radius based upon the width of the button. By the look of your screenshot, the button is not a square when you start, so rounding the corners cannot create a circle.

Related

How to give an NSOutlineView a tiled background image that scrolls?

I have a standard NSOutlineView. I would like it to have a background image, which tiles vertically, and which scrolls together with the outline view cells.
I've somewhat achieved this using the following in my ViewController:
class ViewController: NSViewController {
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let image = NSImage(named: "tile") {
let color = NSColor.init(patternImage: image)
outlineView.backgroundColor = color
}
}
}
That works, except when you scroll past the top or bottom of the view (with the stretch provided by the containing scroll view).
I've tried putting the background image on the scroll view, but then it is static and doesn't scroll with the outline view's content.
I've also tried subclassing various objects in the view hierarchy and overriding their draw(_ dirtyRect: NSRect) method and doing:
self.wantsLayer = true
self.layer?.backgroundColor = ...etc
but got no success from that either.
Can anyone provide any suggestions?
I ended up creating a new custom NSView:
class MyView: NSView {
override func draw(_ dirtyRect: NSRect) {
if let image = NSImage(named: "Tile") {
let color = NSColor.init(patternImage: image)
color.setFill()
dirtyRect.fill()
}
super.draw(dirtyRect)
}
}
Then in my ViewController class I added an instance of the custom view, and used autolayout constraints to pin the new view to my outlineView's clip view starting 2000points above it, and ending 2000 below. This means no matter how far you over-scroll into the stretch area, you still see the tiled background.
class MyViewController: NSViewController {
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
guard let clipView = self.outlineView.superview else { return }
let newView = MyView(frame: .zero) // Frame is set by autolayout below.
newView.translatesAutoresizingMaskIntoConstraints = false
clipView.addSubview(newView, positioned: .below, relativeTo: self.outlineView)
// Add autolayout constraints to pin the new view to the clipView.
// See https://apple.co/3c6EMcH
newView.leadingAnchor.constraint(equalTo: clipView.leadingAnchor).isActive = true
newView.widthAnchor.constraint(equalTo: clipView.widthAnchor).isActive = true
newView.topAnchor.constraint(equalTo: clipView.topAnchor, constant: -2000).isActive = true
newView.bottomAnchor.constraint(equalTo: clipView.bottomAnchor, constant: 2000).isActive = true
}
}
I've removed other code from the above so hopefully I've left everything needed to illustrate the solution.

How to set color of NSView in Cocoa App using Swift?

I used to develop for iOS and I do not understand why I can't easily change background color of NSViews inside my main view.
Let's say I have a view controller with a main view in it. In this view I've added 3 custom views, I've set their constraints to fit the main view.
I've created 3 outlets to my view controller:
#IBOutlet weak var topView: NSView!
#IBOutlet weak var leftView: NSView!
#IBOutlet weak var rightView: NSView!
After that, I'm trying to set the background of these views and can't do this. I don't see any color changes when I run the app.
Here is the code I've added to show colors for my custom views:
override func viewDidLoad() {
super.viewDidLoad()
self.view.wantsLayer = true
self.topView.layer?.backgroundColor = NSColor.blue.cgColor
self.rightView.layer?.backgroundColor = NSColor.green.cgColor
self.leftView.layer?.backgroundColor = NSColor.yellow.cgColor
}
And it shows nothing (no background color on my views):
I can't understand why this code doesn't work. Why it is so hard to set the color of an NSView (while it is so easy in iOS development)? I'm using Xcode 8 and writing code using Swift 3.
You should set wantsLayer to true for your subviews, not for your superview.
self.topView.wantsLayer = true
self.rightView.wantsLayer = true
self.leftView.wantsLayer = true
The layer approach is convenient, but it will create a terrible issue unless you have a very dark background again a bright text color (or vice versa... I don't remember which is which any more.) for an NSTextField label. Unlike Cocoa Touch, which lets you change the background color of a UIView object, unfortunately, you have to subclass NSView in Cocoa for certainty like the following.
import Cocoa
class RedView: NSView {
override func draw(_ rect: NSRect) {
super.draw(rect)
let color = NSColor.red
color.set()
NSRectFill(rect)
}
}
Then set the class name according to whatever you call your subclass (RedView here) under the identity inspector of the interface builder. If you want to change the background color programatically, wire up the view object. Then do something like the following.
import Cocoa
class MyView: NSView {
// MARK: - Variables
var backgroundColor = NSColor()
// MARK: - Draw
override func draw(_ rect:NSRect) {
super.draw(rect)
backgroundColor.set()
NSRectFill(rect)
}
// MARK: - Method
func changeBackgroundColor(color: NSColor) {
backgroundColor = color
setNeedsDisplay(self.bounds)
}
}
You can then call changeBackgroundColor from the view controller like...
#IBOutlet weak var myView: MyView!
myView.changeBackgroundColor(color: NSColor.green)

Why my background gradient view is not loading in my customer UINavigationController class

I'm trying to set a gradient to the background of my subclassed NavigationController. When I add a colour to the same code it works well but I can't seem to let my gradient show up. I created a subclass of a UIView that returns a CAGradientLayer as its background view.
Here is my subclassed UIView : (Note the colours are weird so I am sure its loading the right Gradient.
#IBDesignable
class GenericBackgrounView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
///The roundness for the corner
#IBInspectable var cornerRadius: CGFloat = 0.0 {
didSet{
setupGradient()
}
}
func setupGradient() {
//let gradientColors = [bgDarkColor.cgColor, bgDarkColor.blended(withFraction: 0.5, of: bgLightColor).cgColor, bgLightColor.cgColor]
let gradientColors = [UIColor.brown.cgColor, UIColor.red.blended(withFraction: 0.5, of: UIColor.cyan).cgColor, UIColor.yellow.cgColor]
gradientLayer.colors = gradientColors
gradientLayer.locations = ESDefault.backgroundGradientColorLocations
setNeedsDisplay()
}
var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override func awakeFromNib() {
super.awakeFromNib()
setupGradient()
}
override func prepareForInterfaceBuilder() {
setupGradient()
}
}
And Here is my UINavigationController :
class GenericNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
let backView = GenericBackgrounView(frame: self.view.frame)
backView.bounds = self.view.bounds
self.view.backgroundColor = UIColor.clear
self.view.addSubview(backView)
self.view.sendSubview(toBack: backView)
// Do any additional setup after loading the view.
}
}
Also note that my GenericBackgroundView works fine when I use it for any views I add in the interface builder.
I have been at this to long. I think I will suggest to Apple to setup some kind of Theming API in both code and Interface Builder... and the ability to add gradients straight into Interface Builder...
Thanks for you help.
Instead of setting it up in awakeFromNib() , try calling it in viewDidLayoutSubviews(). Reason is that in viewDidLayoutSubviews() will have the correct frame of the view , while in awakeFromNib() you wouldn't know the right frame of the view.From Apple Documentation.
Alright, I've tinkered a bit and found some working code. I would still love to understand the reason why this works and not the way I had it before. I hate feeling it works by magic...
here is the working code : (Remember that my gradient is in form of CAGradientLayer and I have made some static variable that has defaults.
import UIKit
class GenericNavigationController: UINavigationController {
let backViewGradient = Default.testGradientCALayer
override func viewDidLoad() {
super.viewDidLoad()
setupBackground()
}
func setupBackground() {
backViewGradient.frame = self.view.frame
self.view.layer.insertSublayer(backViewGradient, at: 0)
}
override func prepareForInterfaceBuilder() {
setupBackground()
}
}
What I'm wondering is how come since all the UIControls that are subclassed from UIView don't all work the same. They should all have a view that is the background and we should all be able to either add a layer or a subview to them and be able to get my previous code to work or my latest code too which does not work with TableViewCells.
I will leave this question open because I would love to know the truth behind this. I don't think I can fully grasp Swift or Xcode if it behaves somewhat magically and inconsistent.

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.

Swift - Cropping images *outside* of allowsEditing

I have a very very simple project set up that allows you to click a "browse photo" button. The user then selects a photo from their photo gallery, and it's displayed on a programmatically created UIImageView.
Works like a charm. However - I am missing key functionality that is required.
I need the user to be able to scale the image (via pinching and dragging) after it is displayed within the UIImageView. allowsEditing = true, lets the user crop before. I need similar functionality, however, allowing them to edit once it's on the main UI.
Help is appreciated. Please and thank you!!
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var imageViewLayer: CALayer{
return imageView.layer
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
imageViewLayer.contents = UIImage(named: "ss3.jpg")?.CGImage
imageViewLayer.contentsGravity = kCAGravityResizeAspect
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func newGesture(sender: AnyObject) {
imageViewLayer.transform = CATransform3DMakeScale(sender.scale, sender.scale, 1)
}
}
I did something similar a while back. I added the image to UIImageView's layer property, added gesture recognizer to the view and implemented the gesture call backs modifying the layer property and not the view. Adding the image to the UIImageView's layer did the trick. As a side note, I would like to add that every UIView is supported by CALayer class. It has a lot of methods and properties which help to dynamically change the view, which in your case will be done by gestures.
As an alternative, you can also use CALayer's hitTest method instead of implementing the call backs for gesture recognizers.
EDIT- Sample Code
You could do some thing like this:
#IBOutlet weak var imageView: UIImageView!
var imageViewLayer: CALayer{
return imageView.layer
}
In the viewDidLoad, set up the image
imageViewLayer.contents = UIImage(named: "CoreDataDemoApp")?.CGImage
imageViewLayer.contentsGravity = kCAGravityResizeAspect
Add pinch gesture to the imageview in storyboard (or programmatically) and in it's call back you could do something like this:
#IBAction func pinchGestureRecognized(sender: AnyObject) {
imageViewLayer.transform = CATransform3DMakeScale(sender.scale, sender.scale, 1)
}
Again this is just to give you an idea of how it could work and it is not the complete code. Hope this helps!
This is another way of doing it:
Stackoverflow link to related question