UIButton subview is offset in subclass - ios

I have a UIButton subclass intended to show a selected state of a button. The selected state simply places a thick black line at the bottom of the button view and when unselected it hides the black line. However, when using this in a UIButton subclass, the black line view is offset. I have tried playing around with insets, but I don't think that is the problem. Here is my subclass:
class TabButton: UIButton {
private var height:CGFloat = 5
private var selectedIndicator:UIView?
override var isSelected: Bool {
didSet {
selectedIndicator?.isHidden = !isSelected
}
}
fileprivate func initializeSelector(_ frame: CGRect) {
selectedIndicator = UIView(frame: CGRect(x: 0, y: frame.size.height - height, width: frame.size.width, height: height))
selectedIndicator?.backgroundColor = UIColor.black
self.addSubview(selectedIndicator!)
}
override func awakeFromNib() {
super.awakeFromNib()
initializeSelector(self.frame)
}
}
The desired button should look like this:
But instead it looks like this:
Can anyone help me understand what is happening here and how to fix it? Thanks!

Try this, in layoutSubviews you get the final frame:
override layoutSubviews() {
super.layoutSubviews()
selectedIndicator.frame = CGRect(x: 0, y: frame.size.height - height, width: frame.size.width, height: height)
}

The frame of the selectedIndicator is set only once when initializeSelector is called. When the button changes its frame, it does not change the frame of subviews, you need to manually update the frame of selectedIndicator.
To do so, you need to override layoutSubviews() method of UIView.
override layoutSubviews() {
super.layoutSubviews()
selectedIndicator?.frame = CGRect(x: 0, y: frame.size.height - height, width: frame.size.width, height: height)
}
See this answer to know when layoutSubviews() is called.

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

Resizing UIView in runtime

I have a UIView that's centered vertically and horizontally in my storyboard and has a fixed height (100) and width (300). Now I want that uiview to be resized (100, 100) during runtime.
I have tried this so far but nothing worked.
let cgRect = CGRect(x: 0, y: 0, width: 100, height: 100)
sampleView.draw(cgRect)
and
sampleView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
and
sampleView.frame.size.height = 100
sampleView.frame.size.width = 100
Hook the width and height constraints as IBOutlet and in
#IBAction func btnClicked(_ sender: Any) {
self.widthCon.constant = 100
self.heightCon.constant = 100
self.view.layoutIfNeeded()
}
To change the constraints during runtime via a button press you need to add it in an IBAction. In order to animate the change rather than it just jump, put the code in the UIView animation method:
#IBAction func buttonPressed() {
self.widthCon.constant = 100
self.heightCon.constant = 100
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
}
You can do this:
override func layoutSubviews() {
super.layoutSubviews()
// set hight and width by constraints.
}
or:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// set hight and width by constraints.
}

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

UIView / UIControl ignoring frame rect. Always fullscreen

I am trying to build custom components in Swift 2.2, and obviously missing something essential. My views ignore the frame rect.
In the containing view, I add the view programmatically like this:
let arrowControl = ArrowControl(frame: CGRect(x: 100 , y: 100, width: 300, height: 300))
self.view.addSubview(arrowControl)
In fact, even if I programmatically add a UIView or UIControl, without subclassing it - then the view ignores the frame rect, and occupies the entire screen.
Here are the essential parts of my custom view:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override func layoutSubviews() {
super.layoutSubviews()
renderComponent(frame)
}
EDIT>>>>
If I change my example, and remove renderComponent, I still get the same problem.
override func layoutSubviews() {
super.layoutSubviews()
self.clipsToBounds = true
backgroundColor = UIColor.yellowColor()
// renderComponent(self.frame)
}
Or even doing the following ignores the frame rect, and gives me a full screen block of colour:
let test = UIView(frame: CGRect(x: 100 , y: 100, width: 300, height: 300))
test.backgroundColor = UIColor.redColor()
self.view.addSubview(test)
Doh! I worked it out. It turns out it was my own fault, as I'd implemented the following in the containing view:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews {
view.frame = UIScreen.mainScreen().bounds
}
}

How to customise UISlider height

I have project in which have to customise UISlider element.
I wondering if someone knows how to change, or is it possible to change height of UISlide bar line.
I tried something like this but don't work:
let customBounds = CGRect(origin: bounds.origin,
size: CGSize(width: bounds.size.width, height: 15.0))
feedbackSlider.trackRectForBounds(customBounds)
Thanks
i hope that you want edit it in storyboard, and only the line size, use it in your custom UISlider
class CustomSlide: UISlider {
#IBInspectable var trackHeight: CGFloat = 2
override func trackRectForBounds(bounds: CGRect) -> CGRect {
//set your bounds here
return CGRect(origin: bounds.origin, size: CGSizeMake(bounds.width, trackHeight))
}
}
Overriding trackRect is a way to go, however if you're using additional UISlider's views like minimumValueImage, maximumValueImage you would also need to take their bound into account, otherwise they will overlap with slider’s track. As a shortcut you can simply use super's func:
Fixed version.
Swift 3+
#IBDesignable
class CustomSlider: UISlider {
/// custom slider track height
#IBInspectable var trackHeight: CGFloat = 3
override func trackRect(forBounds bounds: CGRect) -> CGRect {
// Use properly calculated rect
var newRect = super.trackRect(forBounds: bounds)
newRect.size.height = trackHeight
return newRect
}
}
You can override a method in you custom slider
For Objective-C
- (CGRect)trackRectForBounds:(CGRect)bounds {
CGRect rect = CGRectMake(0, 0, 100, 30);//change it to any size you want
return rect;
}
For Swift
override func trackRectForBounds(bounds: CGRect) -> CGRect {
var rect:CGRect = CGRectMake(0, 0, 100, 30)
return rect
}
Swift 3
class MySlide: UISlider {
#IBInspectable var height: CGFloat = 2
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: bounds.origin, size: CGSize(width: bounds.width, height: height))
}
}
Swift 4 version without thumbImage.
class CustomSlider: UISlider {
#IBInspectable var trackHeight: CGFloat = 2
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: bounds.origin, size: CGSize(width: bounds.width, height: trackHeight))
}
}
I think the answers above have a flaw: the origin of the track rect must be offset to account for the change in height of the track, otherwise it will show up off-center. Here is the way I did it:
class CustomSlider: UISlider {
#IBInspectable var sliderTrackHeight : CGFloat = 2
override func trackRect(forBounds bounds: CGRect) -> CGRect {
let originalRect = super.trackRect(forBounds: bounds)
return CGRect(origin: CGPoint(x: originalRect.origin.x, y: originalRect.origin.y + (sliderTrackHeight / 2)), size: CGSize(width: bounds.width, height: sliderTrackHeight))
}
}
U can easily get it done by subclassing UISlider. see following code
class CustomUISlider : UISlider
{
override func trackRectForBounds(bounds: CGRect) -> CGRect {
//set your bounds here
return bounds
}
}
You should be able to subclass the UISlider and then implement:
- (CGRect)trackRectForBounds:(CGRect)bounds
Just return the new CGRect here.
If you're using autolayout you can set a height constraint on the UISlider in the storyboard. if you need to change it at runtime - create an IBOutlet for the constraint and modify its .constant value.

Resources