Custom button is not showing correctly in Interface Builder - ios

I'm learning to develop iOS apps in XCode.
I've created simple custom button (subclass of UIButton) that should have red background and green text. When running the app in iOS simulator, everything is ok. But in interface builder other colors (specified in interface builder) are used. Test project is available at https://www.dropbox.com/s/ro7i4omdbpwapi0/testapp%20%282%29.zip?dl=0. Can somebody help?
Here is source code of custom UIbutton class:
import UIKit
#IBDesignable class DefaultButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
setTitleColor(UIColor.red, for: .normal)
backgroundColor = UIColor.green
//setBackgroundImage(UIImage.createGradient(colors: [ UIColor.defaultGradientStartColor, UIColor.defaultGradientEndColor ], startPoint: CGPoint(x: 0, y: 0), endPoint: CGPoint(x: 1, y: 0), size: CGSize(width: 200, height: 200)), for: .normal)
//layer.cornerRadius = 50
}
override func awakeFromNib() {
super.awakeFromNib()
setup()
}
}

The behaviour described in you answer is correct; but i think you mixes two different things.
Basically, if you want to tint your button title, and your button background, you have to use the fields availables in interface builder. You have some nice properties to set up these things easely.
Use interface builder's parameters to design your look
So, in this case, defining a titleColor and a backgroundColor in your view setup is not relevant, since it's hardcoded.
If instead, you want to display a custom look to your button - like rounded corner - in your storyboard, you could use the #IBInspectable keyword to add new properties in interface builder.
#IBDesignable
class DefaultButton: UIButton {
#IBInspectable var borderWidth: CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable var borderColor: UIColor? {
didSet {
layer.borderColor = borderColor?.cgColor
}
}
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadius
layer.masksToBounds = cornerRadius > 0
}
}
// ...
}
By doing this, you can edit and see your view with a nice render in your storyboard!
Custom properties directly in storyboard

Related

How to get Width or Height of a UIView when I'm using "Equal width" constrain

I need help to solve this problem: I have a UITextFiled and I'm trying to apply a border at the bottom using this code:
func addBottomBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height: width)
self.layer.addSublayer(border)
}
The problem is that the result is not correct, the border goes outside the textfield because in the text Field I'm using the "Equal width constrain" and the Width at design time is not the same Width at "Didload()" time. There is a way to get the width to the textField after "Equal width constrain" correction?
A much better approach is to
subclass UITextField
create the "underline border" layer on initialization
change the frame of that layer in layoutSubviews()
Example:
#IBDesignable
class UnderlinedTextField: UITextField {
let underlineLayer: CALayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(underlineLayer)
underlineLayer.backgroundColor = UIColor.black.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
underlineLayer.frame = CGRect(x: 0, y: bounds.height - 2.0, width: bounds.width, height: 2)
}
}
Result (I gave the text field a background color of .cyan to make it easy to see):
It automatically resizes the "underline" when the field size changes - such as on device rotation:
Note that, by making it #IBDesignable, you can also see the underline layer during design-time.
This example uses a default color of black for the "underline" but you can change it via code just like any other property change, e.g.:
testField.underlineLayer.backgroundColor = UIColor.red.cgColor
Override bounds variable and call your border drawing in didSet. Your layer would be updated every time view changes bounds.
var border = CALayer()
init(frame: CGRect) {
super.init(farme: frame)
self.layer.addSublayer(border)
}
override var bounds: CGRect {
didSet {
addBottomBorderWithColor(color: .black, width: 2)
}
}
func addBottomBorderWithColor(color: UIColor, width: CGFloat) {
border.backgroundColor = color.cgColor
border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height: width)
self.layer.setNeedsLayout()
}
I found a possible solution by myself (not the perfect one).
Because the Constrains are probably applied after DidLoad() and after viewDidLayoutSubviews(), I called the function to add the border inside the function viewDidAppear(). Now it works even if the new borders are shown with a small delay.
The best way is sub-class a UITextFiled as described here.
Custom class that can be applied to every UITextField - Swift
In this case there the object is created correctly

Fill UIView from bottom to top

I have this UIView that right now has that static light gray filled, I need it to fill based on a number I give it that comes from an API. I tried a couple of ways but don't work for me. What's the simplest approach to this? Doesn't have to have fancy effects or anything, just a simple animation that slowly fills up the circle by giving it a number.
The API returns Int that are 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 and 100.
So given those numbers I have to use them to fill the circle. They are basically percentages. So 10 should make the circle fill 10%.
This is the code that I have right now, it's in the same file as the ViewController but I don't think it's the best way, or at least it's not working because when I try to update the coeff it doesn't do it.
class BadgeView: UIView {
private let fillView = UIView(frame: CGRect.zero)
private var fillHeightConstraint: NSLayoutConstraint!
private(set) var coeff: CGFloat = 0.2 {
didSet {
updateFillViewFrame()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
public func setupView() {
layer.cornerRadius = bounds.height / 2.0
layer.masksToBounds = true
fillView.backgroundColor = UIColor(red: 220.0/255.0, green: 220.0/255.0, blue: 220.0/255.0, alpha: 0.4)
fillView.translatesAutoresizingMaskIntoConstraints = false // ensure autolayout works
addSubview(fillView)
// pin view to leading, trailing and bottom to the container view
fillView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
fillView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
fillView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
// save the constraint to be changed later
fillHeightConstraint = fillView.heightAnchor.constraint(equalToConstant: 0)
fillHeightConstraint.isActive = true
updateFillViewFrame()
}
public func updateFillViewFrame() {
fillHeightConstraint.constant = bounds.height * coeff // change the constraint value
layoutIfNeeded() // update the layout when a constraint changes
}
public func setCoeff(coeff: CGFloat, animated: Bool) {
if animated {
UIView.animate(withDuration: 0.5, animations:{ () -> Void in
self.coeff = coeff
})
} else {
self.coeff = coeff
}
}
}
The exact thing that it's not working is here:
if let ElapsedPercentual:Int = JSON.value(forKeyPath: "ResponseEntity.ElapsedPercentual") as? Int {
porcentaje = ElapsedPercentual
print(porcentaje)
>>> BadgeView().setCoeff(coeff: CGFloat(porcentaje)/100, animated: true)
That line isn't actually updating the coeff, so it's always 0.2 as previously setted. Instead it should go from 0.0 to 1.0.
First of all: I strongly suggest using a framework for build constrains programatically. Something like SnapKit.
Using a basic setup where you have a UIView (Container) that contains another UIView (myView). Where myView is used to fill the Container.
You could use the following code (didn't fully test that though) to animate the constraint and have the effect of filling up the container with the myView
self.myView.snp_makeConstraints { make in
make.left.right.bottom.equalTo(self.myContainer)
make.height.equalTo(0)
}
let newPercentage = 10
UIView.animateWithDuration(0.3) {
self.myView.snp_updateConstraints { make in
make.height.equalTo(self.myContainer.frame.height / 100 * newPercentage)
}
self.myView.superview.layoutIfNeeded()
}

How to create an IBDesignable custom UIView from a UIBezierPath?

I want to shape a UIView and be able to see its shape in the Interface Builder, when I set the following class to my UIView it fails to build, I'm not sure where's the error.
#IBDesignable
class CircleExampleView: UIView {
override func layoutSubviews() {
setupMask()
}
func setupMask() {
let path = makePath()
// mask the whole view to that shape
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
private func makePath() -> UIBezierPath {
//// Oval Drawing
let ovalPath = UIBezierPath(ovalIn: CGRect(x: 11, y: 12, width: 30, height: 30))
UIColor.gray.setFill()
ovalPath.fill()
return ovalPath
}
}
A couple of observations:
You are calling setFill followed by fill, but you only do that if drawing into a graphics context (e.g., in draw(_:)). When using CAShapeLayer, you should instead set the fillColor of the CAShapeLayer.
You are using the CAShapeLayer to set the mask of your view. If your UIView doesn't have a discernible backgroundColor, you won't see anything.
If you set the background color of the view to, say, blue, as shown below, your mask will reveal that blue background wherever the mask allows it to (in the oval of your path).
You have implemented layoutSubviews. You generally would do that only if you were doing something here that was contingent upon the bounds of the view. For example, here's a rendition where the oval path is based upon the bounds of the view:
#IBDesignable
class CircleView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
setupMask()
}
private func setupMask() {
let mask = CAShapeLayer()
mask.path = path.cgPath
mask.fillColor = UIColor.gray.cgColor
layer.mask = mask
}
private var path: UIBezierPath {
return UIBezierPath(ovalIn: bounds)
}
}
As E. Coms said, if you override layoutSubviews, you really should call the super implementation. This isn't critical, as the default implementation actually does nothing, but it's best practice. E.g. if you later changed this class to subclass some other UIView subclass, you don't want to have to go to revisit all these overrides.
If you have a designable view, it's advisable to put that in a separate target. That way, the rendering of the view in the storyboard is not dependent upon any work that may be underway in the main project. As long as the designables target (often the name of your main target with Kit suffix) can build, the designable view can be rendered.
For example, here is a rendition of your designable view, in a separate framework target, and used in a storyboard where the view in question has a blue backgroundColor:
For what it's worth, I think it's exceedingly confusing to have to mask to reveal the background color inside the oval. An app developer has to set "background" color in order to set what's inside the oval, but not the background.
I might instead remove the "mask" logic and instead give the designable view an inspectable property, fillColor, and just add a CAShapeLayer as a sublayer, using that fillColor:
#IBDesignable
class CircleView: UIView {
private var shapeLayer = CAShapeLayer()
#IBInspectable var fillColor: UIColor = .blue {
didSet {
shapeLayer.fillColor = fillColor.cgColor
}
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
shapeLayer.fillColor = fillColor.cgColor
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
}
}
This accomplishes the same thing, but I think the distinction of fill colors vs background colors is more intuitive. But you may have had other reasons for using the masking approach, but just make sure if you do that, that you have something to reveal after it’s masked (e.g. a background color or something else you’re rendering).
Please add "super.layoutSubviews()"
override func layoutSubviews() {
setupMask()
super.layoutSubviews()
}
By this way, your design view will be "up to date" .

Bottom Border Width on Swift TextField in TableView

i builded a static tableview with more Rowes than the screen has, so the user has to scroll to see all cell.
Every cell has a textfield with the following class to add a bottom border:
class TextFieldWithBottomBorder: UITextField {
let border = CALayer()
let width = CGFloat(1.0)
func addBottomBorder(color: UIColor){
self.border.borderColor = color.cgColor
self.border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height:self.frame.size.height)
self.border.borderWidth = self.width
self.layer.addSublayer(self.border)
self.layer.masksToBounds = true
}
func changeBorderColor(color: UIColor){
self.border.borderColor = color.cgColor
}
}
And i call the method after receiving some data from the server e. g.
self.firstnameTextField.text = firstNameFromDB
self.firstnameTextField.addBottomBorder(color: .blue)
This works fine for every cell is currently displayed. But the cells which are out of the current view the with is shorter than the textfield.
See this screenshot, for "Vorname", means firstName everything looks good, but for email, password etc. the border is to short.
http://share-your-photo.com/34b5e80253
Looks like the size of the UITextField is being resized after you have called addBottomBorder and so the UIView being used at the line is now not wide enough. It's difficult to say why this would be without seeing more code but there are several methods you could use to overcome it.
1) Switch to a UIView instead of a CALayer and use auto layout to keep the view in the correction position.
2) Override layoutSubviews to update the frame of the bottom line.
The simplest for you is probably option 2 (although I would go option 1) and it would look like this:
override func layoutSubviews() {
super.layoutSubviews()
self.border.frame = CGRect(x: 0, y: self.frame.size.height - width, width: self.frame.size.width, height:self.frame.size.height)
}
Now whenever the frame/size of the text field changes the frame/size of the border line CALayer will be updated appropriately.
Use this class for bottom line text field
#IBDesignable class BottomTextField: UITextField {
var lineView = UIView()
#IBInspectable var lineViewBgColor:UIColor = UIColor.gray{
didSet {
if !isFirstResponder {
lineView.backgroundColor = lineViewBgColor
}
}
}
required init?(coder aDecoder:NSCoder) {
super.init(coder:aDecoder)!
setup()
}
override init(frame:CGRect) {
super.init(frame:frame)
setup()
}
// MARK:- Private Methods
private func setup() {
lineView.frame = CGRect(x:CGFloat(0), y:self.frame.size.height-2, width:self.frame.size.width, height:CGFloat(1))
lineView.backgroundColor = lineViewBgColor
self.addSubview(lineView)
}
}

Interface Builder Clipping Designable Views

I really need a hand here. I have created an #IBDesignable subclass of UILabel which works fine in the XCode Interface Builder. However, even if I set 'clipsToBounds' to false, Interface Builder will still clip it whilst changing the #IBInspectable properties works.
If I'm running the app on simulator or device, the UILabel isn't clipped and gives me the desired results (whilst still applying the values that Interface Builder has).
BEFORE THE CHANGE (The subviews are visible)
AFTER THE CHANGE IN INTERFACE BUILDER (The subviews are out of view)
AFTER THE CHANGE IN SIMULATOR (The subviews are as expected)
Any help would be massively appreciated. The code for the Custom Class is below.
#IBDesignable class UIFeaturedLabel: UILabel {
#IBInspectable var borderWidth: Float = 4
#IBInspectable var borderOffsetX: Float = 15
#IBInspectable var borderOffsetY: Float = 5
#IBInspectable var borderColor: UIColor = UIColor.whiteColor()
private var headerView:UIView!
private var footerView:UIView!
override init() {
super.init()
createViews()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createViews()
}
override init(frame: CGRect) {
super.init(frame: frame)
createViews()
}
func createViews() {
clipsToBounds = false
layer.masksToBounds = false
headerView = UIView()
footerView = UIView()
headerView.backgroundColor = UIColor.whiteColor()
footerView.backgroundColor = UIColor.whiteColor()
addSubview(headerView)
addSubview(footerView)
}
override func layoutSubviews() {
super.layoutSubviews()
let left = CGFloat( -borderOffsetX )
let right = CGFloat( frame.width + CGFloat(borderOffsetX*2) )
let top = CGFloat( -borderOffsetY )
let bottom = CGFloat( frame.height - CGFloat(borderWidth/2) ) + CGFloat( borderOffsetY )
headerView.frame = CGRectMake(left, top, right, CGFloat(borderWidth))
footerView.frame = CGRectMake(left, bottom, right, CGFloat(borderWidth))
}
}
Still occurring with XCode 7.3 iOS9.3, but fixed in XCode Version 8.0 beta (8S128d).

Resources