I am still improving my programing skills in iOS(Swift). Today I am thinking about creating a UIView subclasses in a good way. I want to create custom view which is:
#IBDesignable - I want to see a render of my view in storyboard/xib;
Creatable from code
I know such methods as init(withFrame) init() init(withDecoder) prepareForInterfacebuilder(), etc but I don't know how to use them to minimize redundancy of code.
For example here is my custom UIButton:
import UIKit
#IBDesignable
class SecurityButton: UIButton {
#IBInspectable
var color: UIColor = UIColor.black {
didSet {
setTitleColor(color, for: .normal)
contentEdgeInsets = UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)
layer.borderWidth = 1.0
layer.borderColor = color.cgColor
layer.cornerRadius = 6.0
}
}
}
It works good but I know that contentEdgeInsets = UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0) layer.cornerRadius = 6.0 layer.borderWidth = 1.0 are called every I set a new color. Where should I place this custom setup to keep my requirements about custom view?
Edit
I do not looking for fix my example. I looking for a good place to initialize my custom view. Suppose that you need to do some time-consuming operations but only ones on create view.
You are missing the function
self.setNeedsDisplay()
Your code should look as follows
import UIKit
#IBDesignable
class SecurityButton: UIButton {
#IBInspectable
var color: UIColor = UIColor.black {
didSet {
setTitleColor(color, for: .normal)
contentEdgeInsets = UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)
layer.borderWidth = 1.0
layer.borderColor = color.cgColor
layer.cornerRadius = 6.0
self.setNeedsDisplay()
}
}
}
This will call the draw function that will update the storyboard view.
The function should be called as last in the didSet Now it will show up after you set the color.
I tested it and It works for me in xcode, after I set the value color in the storyboard.
If you want it to draw always not only when u set a color by the inspector you can use the draw(rect) function it will look following
import UIKit
#IBDesignable
class SecurityButton: UIButton {
#IBInspectable
var color: UIColor = UIColor.black
override func draw(_ rect: CGRect) {
setTitleColor(color, for: .normal)
contentEdgeInsets = UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)
layer.borderWidth = 1.0
layer.borderColor = color.cgColor
layer.cornerRadius = 6.0
setNeedsDisplay()
}
}
This will draw the border always, not only when you set in in the interface inspector.
You can create a "custom init" function to handle the setup tasks.
So, for example:
import UIKit
#IBDesignable
class SecurityButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override func awakeFromNib() {
super.awakeFromNib()
commonInit()
}
func commonInit() {
layer.borderWidth = 1.0
layer.borderColor = color.cgColor
layer.cornerRadius = 6.0
_contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0)
updateEdgeInsets()
}
private var _contentEdgeInsets: UIEdgeInsets = UIEdgeInsets.zero {
didSet { updateEdgeInsets() }
}
override public var contentEdgeInsets: UIEdgeInsets {
get { return super.contentEdgeInsets }
set { _contentEdgeInsets = newValue }
}
private func updateEdgeInsets() {
let initialEdgeInsets = contentEdgeInsets
let t = _contentEdgeInsets.top + initialEdgeInsets.top
let l = _contentEdgeInsets.left + initialEdgeInsets.left
let b = _contentEdgeInsets.bottom + initialEdgeInsets.bottom
let r = _contentEdgeInsets.right + initialEdgeInsets.right
super.contentEdgeInsets = UIEdgeInsets(top: t, left: l, bottom: b, right: r)
}
#IBInspectable
var color: UIColor = UIColor.black {
didSet {
setTitleColor(color, for: .normal)
}
}
}
Edit: Custom contentEdgeInsets have to be handled a little differently than most other properties.
#KamilHarasimowicz - Your "question edit" states "I do not looking for fix my example. I looking for a good place to initialize my custom view." ...
My initial answer did exactly that - it showed you where to initialize your custom view.
However, since your comment "storyboard don't render contentEdgeInsets in your solution" indicates that you actually DO want your example fixed (otherwise you would have just fixed it yourself), I have edited my answer to do so.
Related
I've a switch inside table which I'm creating programmatically. I can't change switch's off border colour to gray. I tried tint colour which isn't working either.
How to fix it?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell else {
fatalError("...")
}
//...
let switchView = UISwitch()
switchView.layer.borderColor = UIColor.greyColour.cgColor
cell.accessoryView = switchView
return cell
}
You didn't specify what effect you want to achieve but for layer.borderColor to work you need to setup layer.borderWidth also. However, because switch layer is rectangular it will look like this:
Which might be not what you want. So to make the border follow the switcher's shape you'll need to modify its corner radius:
switchView.layer.borderColor = UIColor.gray.cgColor
switchView.layer.borderWidth = 1.0
switchView.layer.cornerRadius = 16.0
to make it looks like this:
Update
If you want to apply border only for switcher off state it'll be a bit more tricky because you need to handle switcher states changes. The easiest way I could think of is to subclass UISwitch and provide your own behaviour by overriding sendActions method:
class BorderedSwitch: UISwitch {
var borderColor: UIColor = UIColor.gray {
didSet {
layer.borderColor = borderColor.cgColor
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override var isOn: Bool {
didSet {
updateState()
}
}
override func sendActions(for controlEvents: UIControl.Event) {
super.sendActions(for: controlEvents)
if controlEvents.contains(.valueChanged) {
updateState()
}
}
private func setup() {
layer.borderColor = borderColor.cgColor
layer.cornerRadius = frame.height / 2
layer.borderWidth = 1.0
}
private func updateState() {
layer.borderWidth = isOn ? 0.0 : 1.0
}
}
Notice that I also updated cornerRadius value to frame.height / 2 to avoid magic numbers
If you add a switch with code, it looks just like a switch you add in a storyboard. Neither way of creating a switch has a border color.
The code below adds a switch to a view controller's content view:
#IBOutlet var switchView: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
switchView = UISwitch()
switchView.translatesAutoresizingMaskIntoConstraints = false //Remember to do this for UIViews you create in code
if false {
//Add a gray border to the switch
switchView.layer.borderWidth = 1.0 // Draw a rounded rect around the switchView so you can see it
switchView.layer.borderColor = UIColor.gray.cgColor
switchView.layer.cornerRadius = 16
}
//Add it to the container view
view.addSubview(switchView)
//Create center x & y layout anchors (with no offset to start)
let switchViewXAnchor = switchView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
switchViewXAnchor.isActive = true
let switchViewYAnchor = switchView.centerYAnchor.constraint(equalTo: view.centerYAnchor,
constant: 0.0)
switchViewYAnchor.isActive = true
}
It looks perfectly normal.
So, I have this custom UITextField and I have two methods to add CALayer and remove the CALayer but remove is not working.
#IBDesignable class AppTextField : UITextField {
private let bottomLine = CALayer()
override func layoutSubviews() {
self.font = .systemFont(ofSize: 20)
self.addBottomLine()
self.clearButtonMode = .unlessEditing
super.layoutSubviews()
}
func removeBttomLine() {
bottomLine.removeFromSuperlayer()
}
private func addBottomLine() {
bottomLine.frame = CGRect(origin: CGPoint(x: 0, y: self.frame.height + 4), size: CGSize(width: self.frame.width, height: 1))
bottomLine.backgroundColor = UIColor.init(hexString: "#DCCFCA")?.cgColor
self.borderStyle = .none
self.layer.addSublayer(bottomLine)
}
}
The only thing you should do in layoutSubviews() is update frames as necessary.
This will show the red line when the field is NOT being edited, and will remove it while the field IS being edited:
#IBDesignable class AppTextField : UITextField {
private let bottomLine = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.font = .systemFont(ofSize: 20)
self.backgroundColor = .white
self.clearButtonMode = .unlessEditing
self.borderStyle = .none
bottomLine.backgroundColor = UIColor.red.cgColor
addBottomLine()
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
r.origin.y = bounds.maxY
r.size.height = 4.0
bottomLine.frame = r
}
func removeBottomLine() {
bottomLine.removeFromSuperlayer()
}
private func addBottomLine() {
self.layer.addSublayer(bottomLine)
}
override func resignFirstResponder() -> Bool {
super.resignFirstResponder()
addBottomLine()
return true
}
override func becomeFirstResponder() -> Bool {
super.becomeFirstResponder()
removeBottomLine()
return true
}
}
The reason could because the layoutSubviews method gets called multiple times and the layer is getting added multiple times. Try moving the addBottomLine method to required init?(coder:) method if you're using storyboard or use init(frame:) or custom init whichever gets called just once. Here's an example:
#IBDesignable class AppTextField : UITextField {
required init?(coder: NSCoder) {
super.init(coder: coder)
addBottomLine()
}
}
You're doing 3 things in your add method:
adusting frame
setting color
adding as sublayer
And because you're calling it from layoutSubviews it's called multiple times and you're ending with multiple layers added and that's why calling remove doesn't seem to work.
To make this code work you should move adding part to init (withCoder, withFrame or both). You can join it with setting color because it can be done once. Next part is adjusting frame in layoutSubviews which is required because layers can't into autolayout. At the end you will have creation, adding as sublayer and set part called once at init, and adjusting called multiple times on layout pass. Now remove when called once - it would work with visible effect this time.
I have an extension for UIView to apply gradient:
extension UIView {
func applyGradient(colors: [CGColor]) {
self.backgroundColor = nil
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds // Here new gradientLayer should get actual UIView bounds
gradientLayer.cornerRadius = self.layer.cornerRadius
gradientLayer.colors = colors
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.masksToBounds = true
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
In my UIView subclass I'm creating all my view and setting up constraints:
private let btnSignIn: UIButton = {
let btnSignIn = UIButton()
btnSignIn.setTitle("Sing In", for: .normal)
btnSignIn.titleLabel?.font = UIFont(name: "Avenir Medium", size: 35)
btnSignIn.layer.cornerRadius = 30
btnSignIn.clipsToBounds = true
btnSignIn.translatesAutoresizingMaskIntoConstraints = false
return btnSignIn
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSubViews()
}
func addSubViews() {
self.addSubview(imageView)
self.addSubview(btnSignIn)
self.addSubview(signUpstackView)
self.addSubview(textFieldsStackView)
setConstraints()
}
I've overridden layoutSubviews function which is called each time when view bounds are changed(Orientation transition included), where I'm calling applyGradient.
override func layoutSubviews() {
super.layoutSubviews()
btnSignIn.applyGradient(colors: [Colors.ViewTopGradient, Colors.ViewBottomGradient])
}
The problem is that after orientation transition gradient applied wrong for some reason...
See the screenshot please
What am I missing here?
If you look at your button, you’ll see two gradients. That’s because layoutSubviews is called at least twice, first when the view was first presented and again after the orientation change. So you’ve added at least two gradient layers.
You want to change this so you only insertSublayer once (e.g. while the view is being instantiated), and because layoutSubviews can be called multiple times, it should limit itself to just adjusting existing layers, not adding any.
You can also just use the layerClass class property to make the button’s main layer a gradient, and then you don’t have to manually adjust layer frames at all:
#IBDesignable
public class RoundedGradientButton: UIButton {
static public override var layerClass: AnyClass { CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { layer as! CAGradientLayer }
#IBInspectable var startColor: UIColor = .blue { didSet { updateColors() } }
#IBInspectable var endColor: UIColor = .red { didSet { updateColors() } }
override public init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override public func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.height, bounds.width) / 2
}
}
private extension RoundedGradientButton {
func configure() {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
updateColors()
titleLabel?.font = UIFont(name: "Avenir Medium", size: 35)
}
func updateColors() {
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
}
}
This technique eliminates the need to adjust the layer’s frame manually and results in better mid-animation renditions, too.
I'm trying to create a custom UIButton class like so, but I don't see the backGround color updated, also it doesn't show as the default color in the inspector either. I want it to be so that I can change all the colors of those instances from this class file without having to set it manually for each button.
import UIKit
#IBDesignable class PrimaryButton: UIButton {
#IBInspectable var borderwidth: CGFloat = 0.0 {
didSet {
self.layer.borderWidth = borderwidth
}
}
#IBInspectable var cornerradius: CGFloat = 4.0 {
didSet {
self.layer.cornerRadius = cornerradius
}}
}
#IBInspectable var backgroundcolor: UIColor = UIColor.red {
didSet {
self.layer.backgroundColor = backgroundcolor.cgColor
}
}
}
You can try to create custom class like this then set it for any button in IB
#IBDesignable class MyNewButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
shared()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
shared()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
shared()
}
func shared() {
//TODO: add any custom settings here
self.layer.cornerRadius = value
self.layer.borderWidth = value
self.layer.borderColor = UIColor.red.cgColor
}
}
To start off I already looked at this question and tried to implement it but it didn't change anything so I'll just show my own attempt in this code.
Like the question says I'm trying to change the height of the UITextField inside the searchbar. I've made a custom searchbar as well as a custom searchcontroller.
The searchbar class looks like this
class CustomSearchBar: UISearchBar {
var preferredFont: UIFont!
var preferredTextColor: UIColor!
override func drawRect(rect: CGRect) {
// Drawing code
// Find the index of the search field in the search bar subviews.
if let index = indexOfSearchFieldInSubviews() {
// Access the search field
let searchField: UITextField = (subviews[0]).subviews[index] as! UITextField
// Set its frame.
//searchField.frame = CGRectMake(5.0, 5.0, frame.size.width - 10.0, frame.size.height - 10.0)
searchField.frame = CGRectMake(5.0, 5.0, frame.size.width, 100)
// Set the font and text color of the search field.
searchField.font = preferredFont
searchField.textColor = preferredTextColor
// Set the background color of the search field.
searchField.backgroundColor = barTintColor
}
let startPoint = CGPointMake(0.0, frame.size.height)
let endPoint = CGPointMake(frame.size.width, frame.size.height)
let path = UIBezierPath()
path.moveToPoint(startPoint)
path.addLineToPoint(endPoint)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = preferredTextColor.CGColor
shapeLayer.lineWidth = 2.5
layer.addSublayer(shapeLayer)
super.drawRect(rect)
}
init(frame: CGRect, font: UIFont, textColor: UIColor){
super.init(frame: frame)
self.frame = frame
preferredFont = font
preferredTextColor = textColor
searchBarStyle = UISearchBarStyle.Prominent
translucent = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func indexOfSearchFieldInSubviews() -> Int! {
var index: Int!
let searchBarView = subviews[0]
for var i=0; i<searchBarView.subviews.count; ++i {
if searchBarView.subviews[i].isKindOfClass(UITextField) {
index = i
break
}
}
return index
}
}
My Custom searchController looks like this
class CustomSearchController: UISearchController, UISearchBarDelegate {
var customSearchBar: CustomSearchBar!
var customDelegate: CustomSearchControllerDelegate!
init(searchResultsController: UIViewController!, searchBarFrame: CGRect, searchBarFont: UIFont, searchBarTextColor: UIColor, searchBarTintColor: UIColor){
super.init(searchResultsController: searchResultsController)
configureSearchBar(searchBarFrame, font: searchBarFont, textColor: searchBarTextColor, bgColor: searchBarTintColor)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?){
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
func configureSearchBar(frame: CGRect, font: UIFont, textColor:UIColor, bgColor: UIColor){
customSearchBar = CustomSearchBar(frame: frame, font: font, textColor: textColor)
customSearchBar.barTintColor = bgColor
customSearchBar.tintColor = textColor
customSearchBar.showsBookmarkButton = true
customSearchBar.setImage(UIImage(named: "recording.png"), forSearchBarIcon: UISearchBarIcon.Bookmark, state: UIControlState.Normal)
customSearchBar.setImage(UIImage(named:"record_tap.png"), forSearchBarIcon: UISearchBarIcon.Bookmark, state: UIControlState.Selected)
customSearchBar.showsCancelButton = false
customSearchBar.delegate = self
}
In my view controller where I'm creating this searchbar I use the following piece of code. that is being called in my viewDidLoad()
func configureCustomSearchController() {
customSearchController = CustomSearchController(searchResultsController: self, searchBarFrame: CGRectMake(0.0, 0.0, UIScreen.mainScreen().bounds.width, 100.0), searchBarFont: UIFont(name: "Futura", size: 18.0)!, searchBarTextColor: UIColor(red: 0.612, green: 0.984, blue: 0.533, alpha: 1), searchBarTintColor: UIColor.blackColor())
customSearchController.customSearchBar.placeholder = "Search.."
tblSearchResults.tableHeaderView = customSearchController.customSearchBar
customSearchController.customDelegate = self
}
I'm able to change the frame of the entire searchbar see above where I set it too 100. But I'm unable to change the height of the UITextField which I want to make bigger. I'm also sure I'm reaching the UITextView. When I change the color in the drawRect function it changes. Just the height is the problem here. Does someone have any clue what I'm doing wrong or what I need to do to achieve this.
I find if I move your sizing code into layoutSubviews(), I think it works as you expect.
override func layoutSubviews() {
// Find the index of the search field in the search bar subviews.
if let index = indexOfSearchFieldInSubviews() {
... //Added red background
}
}
override func drawRect(rect: CGRect) {...
I believe by the time drawRect() is called, the time for layout has passed. I cannot really comment on if this is the best implementation though.