programmatically set a button width and height - ios

I would like to programmatically customize the UIButton. My code starts here:
class MyButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
layer.shadowRadius = 5.0
...
}
}
Now I would like to define a constant width and height for the button, how to do it in code?

I would recommend to use autolayout:
class MyButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
layer.shadowRadius = 5.0
// autolayout solution
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: 200).isActive = true
self.heightAnchor.constraint(equalToConstant: 35).isActive = true
}
}

You need to override override init(frame: CGRect) method
class MyButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
// Set your code here
let width = 300
let height = 50
self.frame.size = CGSize(width: width, height: height)
backgroundColor = .red
layer.shadowRadius = 5.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

If your button is having constraints set from the storyboard as below and you want to change the width of the button, then this answer is helpful.
constraints set from the storyboard
Safe Area.trailing = Button.trailing + 20
Button.leading = Safe Area.leading + 20
Safe Area.bottom = Button.bottom + 20
height = 40
see the image for a better understanding.
Requirement :
if #condition 1 gets satisfied, then change button width to 100 or any width dimension.
else
if #condition 2 gets satisfied, then keep width as it is ( as per given constraints)
To handle this,
Create an IBOutlet of leading and trailing constraints of that button.
Set it to inactive.
Add width anchor for the button programmatically.
Setting it inactive is mandatory because both won't work at the same time, so be careful.
#IBOutlet weak var btnNextTrailingConstraint: NSLayoutConstraint!
#IBOutlet weak var btnNextLeadingConstraint: NSLayoutConstraint!
if (condition1) {
btnNextLeadingConstraint.isActive = false
btnNextTrailingConstraint.isActive = false
btnNext.widthAnchor.constraint(equalToConstant: 100).isActive = true
}
else {
btnNextLeadingConstraint.isActive = true
btnNextTrailingConstraint.isActive = true
}
swift ios autolayout constraints

I tried finding a way to create a square button box. Hope I`m not at the wrong place here, but when I tried to find my own solution it was as easy as this:
button.frame.size.width = 200
button.frame.size.height = 200
And this works of course with all the other views.

Related

Swift: How to fill a ScrollView from Interface Builder with UIViews programmatically

I am working on a project where I want the user to be able to select two methods of input for the same form. I came up with a scrollview that contains two custom UIViews (made programmatically). Here is the code for the responsible view controller:
import UIKit
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var customView1: CustomView1 = CustomView1()
var customView2: customView2 = CustomView2()
var frame = CGRect.zero
func setupScrollView() {
pageControl.numberOfPages = 2
frame.origin.x = 0
frame.size = scrollView.frame.size
customView1 = customView1(frame: frame)
self.scrollView.addSubview(customView1)
frame.origin.x = scrollView.frame.size.width
frame.size = scrollView.frame.size
customView2 = CustomView2(frame: frame)
self.scrollView.addSubview(customView2)
self.scrollView.contentSize = CGSize(width: scrollView.frame.size.width * 2, height: scrollView.frame.size.height)
self.scrollView.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(pageNumber)
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
scrollView.delegate = self
}
While it works, Xcode gives me an error message for auto layout:
Scrollable content size is ambiguous for "ScrollView"
Also a problem: content on the second UIView is not centered, even though it should be:
picture of the not centered content
import UIKit
class customView2: UIView {
lazy var datePicker: UIDatePicker = {
let datePicker = UIDatePicker()
datePicker.translatesAutoresizingMaskIntoConstraints = false
return datePicker
}()
//initWithFrame to init view from code
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
func setupView () {
self.backgroundColor = .systemYellow
datePicker.datePickerMode = .date
datePicker.addTarget(self, action: #selector(self.datePickerValueChanged(_:)), for: .valueChanged)
addSubview(datePicker)
setupLayout()
}
func setupLayout() {
let view = self
NSLayoutConstraint.activate([
datePicker.centerXAnchor.constraint(equalTo: view.centerXAnchor),
datePicker.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
datePicker.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
datePicker.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2)
])
}
#objc func datePickerValueChanged(_ sender: UIDatePicker) {
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd.MM.yyyy"
let selectedDate: String = dateFormatter.string(from: sender.date)
print("Selected value \(selectedDate)")
}
Any ideas on how to solve this? Thank you very much in advance. And please go easy on me, this is my first question on stackoverflow. I am also fairly new to programming in swift.
To make things easier on yourself,
add a horizontal UIStackView to the scroll view
set .distribution = .fillEqually
constrain all 4 sides to the scroll view's .contentLayoutGuide
constrain its height to the scroll view's .frameLayoutGuide
add your custom views to the stack view
constrain the width of the first custom view to the width of the scroll view's .frameLayoutGuide
Here is your code, modified with that approach:
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var customView1: CustomView1 = CustomView1()
var customView2: CustomView2 = CustomView2()
func setupScrollView() {
pageControl.numberOfPages = 2
// let's put the two custom views in a horizontal stack view
let stack = UIStackView()
stack.axis = .horizontal
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(customView1)
stack.addArrangedSubview(customView2)
// add the stack view to the scroll view
scrollView.addSubview(stack)
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain stack view to all 4 sides of content layout guide
stack.topAnchor.constraint(equalTo: contentG.topAnchor),
stack.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
stack.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
// stack view Height equal to scroll view frame layout guide height
stack.heightAnchor.constraint(equalTo: frameG.heightAnchor),
// stack is set to fillEqually, so we only need to set
// width of first custom view equal to scroll view frame layout guide width
customView1.widthAnchor.constraint(equalTo: frameG.widthAnchor),
])
self.scrollView.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(pageNumber)
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
scrollView.delegate = self
}
}
Edit
Couple additional notes...
UIScrollView layout ambiguity.
As I said in my initial comment, if we add a UIScrollView in Storyboard / Interface Builder, but do NOT give it any constrained content, IB will complain that it has Scrollable Content Size Ambiguity -- because it does. We haven't told IB what the content will be.
We can either ignore it, or select the scroll view and, at the bottom of the Size Inspector pane, change Ambiguity to Never Verify.
As a general rule, you should correct all auto-layout warnings / errors, but in specific cases such as this - where we know that it's setup how we want, and we'll be satisfying constraints at run-time - it doesn't hurt to leave it alone.
UIDatePicker not being centered horizontally.
It actually is centered. If you add this line:
datePicker.backgroundColor = .green
You'll see that the object frame itself is centered, but the UI elements inside the frame are left-aligned:
From quick research, it doesn't appear that can be changed.
Now, from Apple's docs, we see:
You should integrate date pickers in your layout using Auto Layout. Although date pickers can be resized, they should be used at their intrinsic content size.
Curiously, if we add a UIDatePicker in Storyboard, change its Preferred Style to Compact, and give it centerX and centerY constraints... Storyboard doesn't believe it has an intrinsic content size.
If we add it via code, giving it only X/Y position constraints, it will show up where we want it at its intrinsic content size. But... if we jump into Debug View Hierarchy, Xcode tells us its Position and size are ambiguous.
Now, what's even more fun...
Tap that control and watch the Debug console fill with 535 Lines of auto-layout errors / warnings!!!
Some quick investigation -- these are all internal auto-layout issues, and have nothing to do with our code or layout.
We see similar issues with the iOS built-in keyboard when it starts showing auto-complete options.
Those are safe to ignore.

How to set UIView height equal to max of two subviews UILabel?

I have to UILabel with dynamic height. I want to set it superview height equal to max of UILabel heights.
class ComponentCell: UIView {
private lazy var leftRow: UILabel = UILabel()
private lazy var rightRow: UILabel = UILabel()
init(leftValue: String, rightValue: String) {
super.init(frame: .zero)
leftRow.backgroundColor = .red
leftRow.numberOfLines = 0
leftRow.lineBreakMode = .byWordWrapping
leftRow.text = leftValue
rightRow.text = rightValue
rightRow.backgroundColor = .yellow
rightRow.numberOfLines = 0
rightRow.lineBreakMode = .byWordWrapping
self.addSubview(self.leftRow)
self.addSubview(self.rightRow)
leftRow.sizeToFit()
rightRow.sizeToFit()
leftRow.setContentHuggingPriority(.required, for: .vertical)
rightRow.setContentHuggingPriority(.required, for: .vertical)
self.translatesAutoresizingMaskIntoConstraints = false
self.leftRow.snp.makeConstraints { make in
make.top.equalToSuperview()
make.left.equalToSuperview()
make.width.equalToSuperview().dividedBy(2)
}
self.rightRow.snp.makeConstraints { make in
make.top.equalToSuperview()
make.right.equalToSuperview()
make.width.equalToSuperview().dividedBy(2)
}
self.layoutIfNeeded()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If I set leftRow.botton.equalTo(superview.bottom) and rightRow.botton.equalTo(superview.bottom) it's working. But I think is not a good way. And I don't understand why setContentHuggingPriority not helped me to solve this problem.
Content Hugging
Content hugging leads more likely to squeeze your labels. What you want is the height of the labels to be more respected. So you'd rather use compression resistance priority. However you actually need neither of those.
Layout Constraints
Since you're setting your constraints programatically, you'll need to set translatesAutoresizingMaskIntoConstraints to false for your labels as well:
leftRow.translatesAutoresizingMaskIntoConstraints = false
rightRow.translatesAutoresizingMaskIntoConstraints = false
The bottom constraint is actually a good start, but you don't want to fit the height of the smaller label unnecessarily to the height of the bigger label. So you would want to add a constraint that is "less than or equal to the bottom anchor":
make.bottom.lessThanOrEqualTo(self.snp.bottom)
Lazy Variables
If you want to use lazy variables you'll have to change the way there being initialized. The way you've written it, it initializes the variables right away when initializing the class. But you only want them to be initialized when they're used the first time. For that you need to write it like this:
private lazy var leftRow: UILabel = {
return UILabel()
}()
private lazy var rightRow: UILabel = {
return UILabel()
}()
However in your case you don't need lazy loading, so you can initialize them directly:
private let leftRow = UILabel()
private let rightRow = UILabel()
Other
Since you're using layout constraints, you don't need to call sizeToFit on the labels. It doesn't do anything.
Calling layoutIfNeeded() within the init doesn't do anything either since it will be called anyway once you add ComponentCell as a subview to another view.

How to add SubView to a horizontal StackView programmatically to create Stripes?

I have a collectionView Cell with a StackView in it.
class OuterCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var stackView: UIStackView!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
for i in 1...30 {
let tick = UIView()
if i % 2 == 0 {
tick.backgroundColor = UIColor.red
} else {
tick.backgroundColor = UIColor.clear
}
tick.tag = i
tick.heightAnchor.constraint(equalTo: self.heightAnchor, constant: 0.0).isActive = true
self.stackView?.insertArrangedSubview(tick, at:i)
}
}
I want to draw 30 cells on the stackView alternating between red and clear . Right now I am setting the index of the subview as well as the height with a auto layout anchor constraint and hoping that the .fillEqually distribution will compute the width and X locations of each of the subviews. However right now, it crashes when I try and set the height anchor and simply doesn't show anything when I remove the height anchor. How do I lay these out such that they are full height, and right next to each other with no margins?
I think that you shouldn't be adding a height anchor here to items within your stackview. Automatically, things contained in the stack view are equal heights (or widths depending on stackview orientation). You shouldn't need to be setting sizes within the stackview directly. It defeats the purpose of the stackview in the first place.

Swift: Apply CGSize to Button Class not changing

I am trying to add all style info into a class and then subclass a UIButton to avoid duplication of code.
At the moment, my class looks like:
class CustomButton: UIButton {
required init() {
super.init(frame: .zero)
// set other operations after super.init, if required
backgroundColor = .red
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
frame.size = CGSize(width: 700, height: 100)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In the viewDidLoad I am adding:
let b1 = CustomButton()
view.addSubview(b1)
// auto layout
b1.translatesAutoresizingMaskIntoConstraints = false
b1.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
b1.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
As you can see in the class I have set the frame.size
frame.size = CGSize(width: 700, height: 100)
However, when I run it, It looks like so:
The width is clearly not 700. Any suggestions where I am going wrong?
The problem is you are mixing frame and auto layout. Specifically, once you turn off autoresizing, the frames go away. Why not do 100% auto layout? In fact, why not make centerX/centerY part of the init()?
class CustomButton: UIButton {
required init(width:CGFloat, height:CGFloat, centerButton:Bool) {
super.init(frame: .zero)
// set other operations after super.init, if required
backgroundColor = .red
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: width).isActive = true
self.heightAnchor.constraint(equalToConstant: height).isActive = true
if centerButton {
self.centerXAnchor.constraint(equalTo: superview?.centerXAnchor).isActive = true
self.centerYAnchor.constraint(equalTo: superview?.centerYAnchor).isActive = true
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And change your call in viewDidLoad() to:
let b1 = CustomButton(width:700, height:100, centerButton:true)
(I added the width/height specs to init() to make your code more flexible.)
EDIT: Regards to my last (parenthesized) statement. If all you do is replace:
frame.size = CGSize(width: 700, height: 100)
with:
self.widthAnchor.constraint(equalToConstant: 700).isActive = true
self.heightAnchor.constraint(equalToConstant: 100).isActive = true
Everything will work. But, in your question you also said that (emphasis mine):
I am trying to add all style info into a class and then subclass a
UIButton to avoid duplication of code.
While the code is untested, I added parameters to the init in an effort to make the code more adaptable to creating buttons on the fly. Depending on your needs, you can extend this to everything from backgroundColor to cornerRadius.
I come from an OOP background (actually a top-down back in the 70s), so subclassing is way too intuitive to me. What I presented was just that - subclass to avoid duplication of code. Swift presents new ways - specifically extension and convenience init. I think both of these would even work for you. I'm not sure of the specific pros/cons of extension versus subclassing - my feeling is that duplication of code is about the same (technically) for both - but I'll always appreciate what a "modern" language brings to a developer's toolkit!
To decide any views position or size ,you can use either
Frames based layout or constrain based Autolayout not both together.
Thanks

Adding a UIView to the bottom of a UICollectionViewController

I have a UICollectionViewController embedded inside a UINavigationController which in turn embedded inside a UITabBarController.
I want to add a UIView to the UICollectionViewController just above the tab bar (shown by red rectangle).
I have the UIView created separately as a nib file.
import UIKit
class BottomView: UIView {
#IBOutlet var view: UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
fileprivate func commonInit() {
Bundle.main.loadNibNamed("BottomView", owner: self, options: nil)
view.frame = self.frame
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(view)
}
}
And I initialize and add it in the UICollectionViewController like so.
class CollectionViewController: UICollectionViewController {
fileprivate var bottomView: BottomView!
override func viewDidLoad() {
super.viewDidLoad()
let yPos = view.bounds.height - (tabBarController!.tabBar.frame.size.height + 44)
bottomView = BottomView(frame: CGRect(x: 0, y: yPos, width: view.bounds.width, height: 44))
collectionView?.addSubview(bottomView)
}
// ...
}
I figured if I substract the combined height of the bottom view plus the tab bar from the entire view's height, I should be able to get the correct y position value. But it's not happening. The view is getting added but way off screen.
How do I calculate the correct y position without hardcoding it?
Example demo project
I would suggest adding the BottomView to the UICollectionViewController's view rather than to the collection view itself. This is part of the problem you're having.
You're also trying to set the frame of the BottomView in the viewDidLoad() method using values from view.bounds. The CGRect will return (0.0, 0.0, 0.0, 0.0) at this point because the layout has yet to take place, which is most likely why your positioning is off. Try moving your layout logic to the viewWillLayoutSubviews() method and see if that helps.
A better approach would be by setting auto layout constrains rather than defining a frame manually, this will take a lot of the leg work out for you.
Here's a quick example:
self.bottomView.translatesAutoresizingMaskIntoConstraints = false
self.view.insertSubview(self.bottomView, at: 0)
self.bottomView.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor).isActive = true
self.bottomView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.bottomView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
self.bottomView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
You can apply autolayout logic in your viewDidLoad() and it should work correctly.
You can find some more information on setting autolayout constraints programatically here:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html
Sounds what you want to achieve is exactly the footer view for the UICollectionView.
A footerView is like a view that will stick to the bottom of the collectionView and wont move with the cells.
This will help you add a footer View: https://stackoverflow.com/a/26893334/3165112
Hope that helps!

Resources