Subclass of UIButton with rounded corners (swift) - swift

Using swift, I'd like like to create a button with rounded corners. To make this re-usable, my preference is to subclass UIButton, and have come up with the following:
import Foundation
import UIKit
class LoginButton: UIButton {
let corner_radius : CGFloat = 4.0
override func drawRect(rect: CGRect) {
super.drawRect(rect)
self.layer.cornerRadius = corner_radius
}
}
Unfortunately, this doesn't seem to work as I had hoped, even though it compiles fine. Perhaps I'm missing something - I'm very new to this!
Any ideas?

You need to use clipsToBounds to ensure that the containing view isn't drawn over the corner radius:
self.clipsToBounds = true

You need also to turn on the masksToBounds on the layer:
self.layer.masksToBounds = true

Related

How to create a global function that affects all or specific UIButton instance types?

I want to create a global function that affects either all UIButton instances or only those of a certain UIButton type which would update the corner radius or border property. I'm familiar with UIAppearances however my client would like to have a global file where they could update changes on the fly as if it were a CSS stylesheet. So far I've been able to make extensions of UIColor and UIFont which returns specific colors and fonts however I can't figure out how this would work for UIButton instances. Here is what I've thought of so far however I don't think this would work:
#objc extension UIButton {
func changeUIButtonBorder() -> UIButton {
self.layer.borderWidth = 3
self.layer.borderColor = UIColor.white.cgColor
return self
}
}
What you're doing is great, and it does work for UIButton instances. But there is no need to return anything. In the extension, self is the button. So it can just change itself.
#objc extension UIButton {
func changeUIButtonBorder() {
self.layer.borderWidth = 3
self.layer.borderColor = UIColor.white.cgColor
}
}
You can now call changeUIButtonBorder on any UIButton instance.
#IBOutlet var myButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
myButton.changeUIButtonBorder()
}
However, there is no magical way to shoutcast to all UIButtons that they should call that method; you'll have to deal with them one at a time.
The "magical" way is, as #Sh_Khan suggests, to make a UIButton subclass that calls changeUIButtonBorder in its own initializer. You would then simply have to make sure that all your buttons are instances of that subclass.
For example, here's a UIButton subclass that's always red (assuming that all instances come from the storyboard):
class RedButton : UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = .red
}
}

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.

Is it possible to make a circular NSButton?

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.

how can I remove the top border on UIToolBar

I have set my UIToolBar tint color to some value, and there is this border line that I see in which I want to remove:
How do I remove this black border>
You can do like this:
self.navigationController.toolbar.clipsToBounds = YES;
toolbar1.clipsToBounds = YES;
Worked for me incase someone is still trying with Navigational bar
Correct answer is the one by totalitarian...FYI. https://stackoverflow.com/a/14448645/627299
My response is still below for reference.
Here's what I did with my WHITE background toolbar...
whiteToolBar.layer.borderWidth = 1;
whiteToolBar.layer.borderColor = [[UIColor whiteColor] CGColor];
Perhaps you could do the same thing with your color instead.
setShadowImage to [UIImage new]
navigationController?.toolbar.barTintColor = .white
navigationController?.toolbar.setShadowImage(UIImage(), forToolbarPosition: .any)
Setting the style to UIBarStyleBlackTranslucent did it for me (iOS 6)
create a 1 pixel x 1 pixel clear image and call it clearPixel.png
toolbar.setShadowImage(UIImage(named: "clearPixel.png"), forToolbarPosition: UIBarPosition.any)
this doesn't work consistently on iOS versions, doesn't seem to work on iOS7. i answered this in another question: https://stackoverflow.com/a/19893602/452082 and you can modify that solution to just remove the background shadow (and leave your toolbar.backgroundColor whatever color you like)
The clipsToBounds technique clips the UIToolBar's shadow as well as the background view. On an iPhone X, that means the background no longer reaches outside the safe area.
The solution below uses a mask to clip only the top of the UITabBar. The mask is rendered in a UIToolBar subclass, and the mask frame is kept updated in an override of layoutSubviews.
class Toolbar: UIToolbar {
fileprivate let maskLayer: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.black.cgColor
return layer
}()
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
fileprivate func initialize() {
layer.mask = maskLayer
// Customize toolbar here
}
override func layoutSubviews() {
super.layoutSubviews()
// height is an arbitrary number larger than the distance from the top of the UIToolbar to the bottom of the screen
maskLayer.frame = CGRect(x: -10, y: 0, width: frame.width + 20, height: 500)
}
}
I got a bit confused with these answers but I was missing the point that you were using an Outlet so just to be clear here is the swift code I used to hide the border:
import UIKit
class ViewController: UIViewController {
//MARK Outlets
#IBOutlet weak var ToolBar: UIToolbar!
//MARK View Functions
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Hide the bottom toolbar's top border
ToolBar.clipsToBounds = true
}
}
I had dragged a toolbar to the bottom of a view for this and it's not the top nav bar some other questions refer to.
[[UIToolbar appearance] setBackgroundImage:[[UIImage alloc] init] forToolbarPosition:UIBarPositionBottom barMetrics:UIBarMetricsDefault];
[[UIToolbar appearance] setShadowImage:[[UIImage alloc] init] forToolbarPosition:UIBarPositionBottom];
[UIToolbar appearance].barTintColor = [UIColor ...];```