How can I get frame of superview's frame in swift? - swift

I want to create multiple buttons and position it inside uiview and fit to uiview.(as picture)
I need to get uiview frame to calculate and divide as I need , to set button's width and height depending on device size.
for row in 0 ..< 4 {
for col in 0..<3 {
let numberButton = UIButton()
numberButton.frame = CGRect(x: Int(buttonView.frame.width / 3 - 20) * col, y: row * (320 / 4), width: Int(buttonView.frame.width) / 3, height: Int(buttonView.frame.height) / 4)
numberButton.setTitle("button", for: .normal)
numberButton.titleLabel?.font = numberButton.titleLabel?.font.withSize(30)
numberButton.setTitleColor(UIColor.black)
buttonView.addSubview(numberButton)
}
}
I tried like code above, but buttonView.frame.width returns nil.
How can I calculate this view's frame?

You can use UIStackViews to achieve this grid layout. This way, you don't have to calculate the frames of each button. Doing so is bad practice anyway. You should instead use AutoLayout constraints to layout your views. Here's a tutorial to get you started.
Anyway, here's how you would use UIStackViews to create a grid of buttons:
// here I hardcoded the frame of the button view, but in reality you should add
// AutoLayout constraints to it to specify its frame
let buttonView = UIStackView(frame: CGRect(x: 0, y: 0, width: 600, height: 320))
buttonView.alignment = .fill
buttonView.axis = .vertical
buttonView.distribution = .fillEqually
buttonView.spacing = 20 // this is the spacing between each row of buttons
for _ in 0..<4 {
var buttons = [UIButton]()
for _ in 0..<3 {
let numberButton = UIButton(type: .system)
numberButton.setTitle("button", for: .normal)
numberButton.titleLabel?.font = numberButton.titleLabel?.font.withSize(30)
numberButton.setTitleColor(UIColor.black, for: .normal)
// customise your button more if you want...
buttons.append(numberButton)
}
let horizontalStackView = UIStackView(arrangedSubviews: buttons)
horizontalStackView.alignment = .fill
horizontalStackView.axis = .horizontal
horizontalStackView.distribution = .fillEqually
horizontalStackView.spacing = 20 // this is the spacing between each column of buttons
buttonView.addArrangedSubview(horizontalStackView)
}
Result from playground quick look:

Related

Buttons on UIScrollView bounces back when centered

My UIScrollView bounces back from left side, but stays normal from the right side.
Look at the gif please.
Important note: The buttons should be centered when the screen opens.
To center my buttons, I create them like this:
button.frame = CGRect(
x: screenWidth/2 + gap,
y: 0,
width: buttonWidth,
height: buttonHeight)
buttonsScrollView.addSubview(button)
I already set wide content size
buttonsScrollView.contentSize.width = screenWidth
and adding extra space - screenWidth + 100 does not help, it makes wider only from the right side.
You can do this with "calculated" frames or with auto-layout. In either case, the general idea...
Add your buttons to a UIView - we'll call it buttonsView:
Start the first button at gap distance from Zero, and set the frame of buttonsView to the height of the buttons, and add gap distance to the right-end.
Add buttonsView to the scroll view, at x: 0, y: 0.
Set the scroll view's .contentSize = buttonsView.frame.size.
Do all of that in viewDidLoad().
We can't center the buttonsView until we know the scroll view's frame width, which will likely vary depending on device (and if you rotate the device), so...
Add a class property to track the scroll view's width:
// track the scroll view frame width
var scrollViewWidth: CGFloat = 0
then, in viewDidLayoutSubviews() we know the scroll view's frame, so we'll adjust the .contentOffset.x to horizontally center the buttonsView:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// here we know frame size, but
// viewDidLayoutSubviews() can (and usually will) be called multiple times
// so we only want to execute this code when the scroll view width changes
if scrollViewWidth != scrollView.frame.width {
scrollViewWidth = scrollView.frame.width
// calculate content offset x so the row of buttons is centered horizontally
scrollView.contentOffset.x = (buttonsView.frame.width - scrollView.frame.width) * 0.5
}
}
Here's a complete sample implementation. No #IBOutlet connections -- everything is done via code -- so just set a view controller's class to CenterScrollViewController:
class CenterScrollViewController: UIViewController {
// create a scroll view
let scrollView: UIScrollView = {
let v = UIScrollView()
// background color so we can see its frame
v.backgroundColor = .systemYellow
return v
}()
// create a buttons holder view
let buttonsView: UIView = {
let v = UIView()
return v
}()
// gap between buttons and on left/right sides of button row
let gap: CGFloat = 12
// buttons will be (round) at 44 x 44 points
let btnSize: CGFloat = 44
// number of buttons
let numButtons: Int = 9
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
title = "Calc"
// add scroll view to view
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
// add buttons view to scroll view
scrollView.addSubview(buttonsView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain scroll view
// Top + 40
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
// Leading and Trailing
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// Height equal to button height + "padding" on bottom
scrollView.heightAnchor.constraint(equalToConstant: btnSize + gap),
])
// let's add some buttons to the buttons view
var x: CGFloat = gap
for i in 1...numButtons {
let b = UIButton()
b.backgroundColor = .systemBlue
b.setTitle("\(i)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
// let's keep the buttons square (1:1 ratio) so we can make them round
b.frame = CGRect(x: x, y: 0, width: btnSize, height: btnSize)
b.layer.cornerRadius = btnSize * 0.5
b.layer.borderColor = UIColor.black.cgColor
b.layer.borderWidth = 1
buttonsView.addSubview(b)
x += btnSize + gap
}
// x now equals the total buttons width plus gap on each side
// so set the frame size of the buttons view to (x, btnSize)
buttonsView.frame = CGRect(x: 0, y: 0, width: x, height: btnSize)
// set scroll view content size to the size of the buttons view
scrollView.contentSize = buttonsView.frame.size
}
// track the scroll view frame width
var scrollViewWidth: CGFloat = 0
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// here we know frame size, but
// viewDidLayoutSubviews() can (and usually will) be called multiple times
// so we only want to execute this code when the scroll view width changes
if scrollViewWidth != scrollView.frame.width {
scrollViewWidth = scrollView.frame.width
// calculate content offset x so the row of buttons is centered horizontally
scrollView.contentOffset.x = (buttonsView.frame.width - scrollView.frame.width) * 0.5
}
}
}

Is there a way to display characters in a for loop without duplicating code?

I'm still very new to learning Swift and have a frame that displays the alphabet in it evenly.
I have 3 for loops in which letters A-X are displayed in 4 rows with 6 columns while Y and Z are in the 5th row in columns 2-3 (so it is centered). However, to achieve this, I had to duplicate my code. Is there a better way to go about this so I do not have to duplicate the code?
let allLetters = (65...90).map { Character(Unicode.Scalar($0)) }
let width = 80
let height = 80
for row in 0..<4 {
for col in 0..<6 {
let letterButton = UIButton(type: .system)
letterButton.titleLabel?.font = UIFont.systemFont(ofSize: 36)
letterButton.addTarget(self, action: #selector(letterTapped), for: .touchUpInside)
let frame = CGRect(x: col * width, y: row * height, width: width, height: height)
letterButton.frame = frame
buttonsView.addSubview(letterButton)
letterButtons.append(letterButton)
}
}
for col in 2..<4 {
let letterButton = UIButton(type: .system)
letterButton.titleLabel?.font = UIFont.systemFont(ofSize: 36)
letterButton.addTarget(self, action: #selector(letterTapped), for: .touchUpInside)
let frame = CGRect(x: col * width, y: 4 * height, width: width, height: height)
letterButton.frame = frame
buttonsView.addSubview(letterButton)
letterButtons.append(letterButton)
}
for (index, button) in letterButtons.enumerated() {
button.setTitle(String(allLetters[index]), for: .normal)
}
}
You can simplify and improve the performance, removing two for loops like this:
let allLetters = (65...90).map { Character(Unicode.Scalar($0)) }
let width = 80
let height = 80
var letterIndex = 0
let lastRowIndex = 4
let defaultRowColumnRange = 0..<6
let lastRowColumnRange = 2..<4
for row in 0...lastRowIndex {
let rangeForRow = row == lastRowIndex ? lastRowColumnRange : defaultRowColumnRange
for col in rangeForRow {
let letterButton = UIButton(type: .system)
letterButton.titleLabel?.font = UIFont.systemFont(ofSize: 36)
letterButton.addTarget(self, action: #selector(letterTapped), for: .touchUpInside)
if allLetters.count > letterIndex {
letterButton.setTitle(String(allLetters[letterIndex]), for: .normal)
letterIndex += 1
}
let frame = CGRect(x: col * width, y: row * height, width: width, height: height)
letterButton.frame = frame
view.addSubview(letterButton)
letterButtons.append(letterButton) // Might not be necessary anymore
}
}
First of all, I would still move all the button manipulations into its own function:
func addButton(letter: Character, x: Int, y: Int) {
let letterButton = UIButton(type: .system)
letterButton.setTitle(String(letter), for: .normal)
letterButton.titleLabel?.font = UIFont.systemFont(ofSize: 36)
letterButton.addTarget(self, action: #selector(letterTapped), for: .touchUpInside)
let frame = CGRect(x: x, y: y, width: width, height: height)
letterButton.frame = frame
buttonsView.addSubview(letterButton)
letterButtons.append(letterButton)
}
Because this provides a nice isolation to all ui operations. Even if you replace your repetitive code with this function, you've got the improvement.
But also assume
let numberOfLettersPerRow = 6
You know how many buttons will be in each row:
let buttonsInCurrentRow = min(numberOfLettersPerRow, allLetters.count - row * numberOfLettersPerRow) // i.e. 6 or less
So you can then decide if buttons will be back to back, or there's some empty space before and after them:
let gap = (numberOfLettersPerRow - buttonsInCurrentRow) * width / 2
This gap will be 0 for rows where numberOfLettersPerRow == buttonsInCurrentRow, but will be more than 0 for rows that have numberOfLettersPerRow < buttonsInCurrentRow, and we want it to be half of total gap (so there's equal space in front and back)
So now you place your button like this:
let frame = CGRect(x: gap + col * width, y: row * height, width: width, height: height)
And then you run your loop over buttons you have, rather than over rows and columns. Why? because if you change your mind tomorrow about how to place your buttons, you don't need to change much of the code:
for i in 0...allLetters.count - 1 {
let row = Int(i / numberOfLettersPerRow)
let col = i % numberOfLettersPerRow
let buttonsInCurrentRow = min(numberOfLettersPerRow, allLetters.count - row * numberOfLettersPerRow)
let gap = (numberOfLettersPerRow - buttonsInCurrentRow) * width / 2
addButton(letter: allLetters[i], x: gap + col * width, y: row * height)
}
And that's it.

Adding UIButtons dynamically in UICollectionViewCell - Swift

Trying to show buttons dynamically in a UICollectionViewCell depends on data coming in array from api. Below is what trying to achieve:
This is so far what i have achieved using button, but i need multiple buttons means button next to "About us" then next until space there then move to next line and setting height of cell as per height of view in cell. Please guide how it can be achieved.
Below is my code:
var xPosition = cell.btnOptions.frame.origin.x
var yPosition = cell.btnOptions.frame.origin.y
var width = cell.btnOptions.frame.size.width
var height = cell.btnOptions.frame.size.height
if((arrOptions?.count ?? 0) > 0)
{
for i in 0..<arrOptions!.count
{
//
cell.btnOptions.tag = i
cell.btnOptions.layer.cornerRadius = 3.0
cell.btnOptions.frame = CGRect.init(x: xPosition, y: yPosition, width: width, height: height)
//
cell.btnOptions.setTitle(arrOptions?.objectAt(i).object(forKey: "title") as? String, for: .normal)
xPosition = width + width + 8
yPosition = height
}

Swift: Center NSAttributedString in View horizontally and vertically

Hi there,
I have a class CardButton: UIButton . In the draw method of this CardButton, I would like to add and center(vertically and horizontally) NSAttributed String, which is basically just one Emoji, inside of it. The result would look something like this:
However, NSAttributedString can be only aligned to center in horizontal dimension inside the container.
My idea for solution:
create a containerView inside of CardButton
center containerView both vertically and horizontally in it's container(which is CardButton)
add NSAttributedString inside the containerView and size containerView to fit the string's font.
So the result would look something like this:
My attempt for this to happen looks like this:
class CardButton: UIButton {
override func draw(){
//succesfully drawing the CardButton
let stringToDraw = attributedString(symbol, fontSize: symbolFontSize) //custom method to create attributed string
let containerView = UIView()
containerView.backgroundColor = //green
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
let centerXConstraint = containerView.centerXAnchor.constraint(equalTo: (self.centerXAnchor)).isActive = true
let centerYConstraint = containerView.centerYAnchor.constraint(equalTo: (self.centerYAnchor)).isActive = true
stringToDraw.draw(in: containerView.bounds)
containerView.sizeToFit()
}
}
Long story short, I failed terribly. I first tried to add containerView to cardButton, made the background green, gave it fixed width and height jut to make sure that it got properly added as a subview. It did. But once I try to active constraints on it, it totally disappears.
Any idea how to approach this?
There is always more than one way to achieve any particular UI design goal, but the procedure below is relatively simple and has been adapted to suit the requirements as presented and understood in your question.
The UIButton.setImage method allows an image to be assigned to a UIButton without the need for creating a container explicitly.
The UIGraphicsImageRenderer method allows an image to be made from various components including NSAttributedText, and a host of custom shapes.
The process utilising these two tools to provide the rudiments for your project will be to:
Render an image with the appropriate components & size
Assign the rendered image to the button
A class could be created for this functionality, but that has not been explored here.
Additionally, your question mentions that when applying constraints the content disappears. This effect can be observed when image dimensions are too large for the container, constraints are positioning the content out of view and possibly a raft of other conditions.
The following code produces the above image:
func drawRectangleWithEmoji() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { (ctx) in
// Create the outer square:
var rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 7.5, dy: 7.5)
var roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 50).cgPath
// MARK: .cgPath creates a CG representation of the path
ctx.cgContext.setFillColor(UIColor.white.cgColor)
ctx.cgContext.setStrokeColor(UIColor.blue.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Create the inner square:
rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 180, dy: 180)
roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 10).cgPath
ctx.cgContext.setFillColor(UIColor.green.cgColor)
ctx.cgContext.setStrokeColor(UIColor.green.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Add emoji:
var fontSize: CGFloat = 144
var attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: fontSize)//,
//.backgroundColor: UIColor.gray //uncomment to see emoji background bounds
]
var string = "❤️"
var attributedString = NSAttributedString(string: string, attributes: attrs)
let strWidth = attributedString.size().width
let strHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - strWidth / 2, y: 256 - strHeight / 2))
// Add NSAttributedString:
fontSize = 56
attrs = [
.font: UIFont.systemFont(ofSize: fontSize),
.foregroundColor: UIColor.brown
]
string = "NSAttributedString"
attributedString = NSAttributedString(string: string, attributes: attrs)
let textWidth = attributedString.size().width
let textHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - textWidth / 2, y: 384 - textHeight / 2))
}
return img
}
Activate the NSLayoutContraints and then the new image can be set for the button:
override func viewDidLoad() {
super.viewDidLoad()
view = UIView()
view.backgroundColor = .white
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
// buttonsView.backgroundColor = UIColor.gray
view.addSubview(buttonsView)
NSLayoutConstraint.activate([
buttonsView.widthAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.heightAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonsView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
let cardButton = UIButton(type: .custom)
cardButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
cardButton.setImage(drawRectangleWithEmoji(), for: .normal)
let frame = CGRect(x: 0, y: 0, width: buttonWidth, height: buttonWidth)
cardButton.frame = frame
buttonsView.addSubview(cardButton)
}
Your comments will be appreciated as would constructive review of the code provided.

How to add space between buttons in uiscrollview

I was trying so hard to add space between buttons array in UIScrollView but I couldn't make it. There is any way to make space between buttons array in UIScrollView ?
I have attached a picture explaining what I want.
This is my code :
//MARK: - SETUP CATEGORIES BUTTONS
func setupCategoriesButtons() {
var xCoord: CGFloat = 1
let yCoord: CGFloat = 1
let buttonHeight: CGFloat = 40
let buttonwidth: CGFloat = 40
let gap: CGFloat = 0
// Counter for items
var itemCount = 0
// Loop for creating buttons
for i in 0..<myArray.count {
itemCount = i
// Create a Button
let catButt = UIButton(type: .custom)
catButt.showsTouchWhenHighlighted = true
catButt.setTitle("\(myArray[itemCount])", for: .normal)
catButt.sizeToFit()
catButt.frame = CGRect(x: xCoord, y: yCoord, width: catButt.frame.size.width + 30, height: buttonHeight)
catButt.layer.cornerRadius = 8
catButt.titleLabel?.font = UIFont(name: "Titillium-Semibold", size: 12)
// catButt.addTarget(self, action: #selector(categoryButt(_:)), for: UIControlEvents.touchUpInside)
// Add Buttons & Labels based on xCood
xCoord += catButt.frame.size.width + gap
categoriesScrollView.addSubview(catButt)
letterButtons.append(catButt)
} // END LOOP --------------------------
for i in 0..<letterButtons.count {
letterButtons[i].tag = i
letterButtons[i].backgroundColor = RandomColor()
letterButtons[i].setTitleColor(UIColor.white, for: .normal)
}
// Place Buttons into the ScrollView
categoriesScrollView.contentSize = CGSize(width: buttonwidth * CGFloat(itemCount+1), height: yCoord)
}
let gap: CGFloat = 0
Looks wrong, if you want a gap. Shouldn't it be e.g. ... = 15?
Alternatives could include placing them in a UIStackView with a spacing set. This would also be simple using auto-layout, creating constraints between the buttons which provide the gaps.
Another suggestion:
I would suggest to use a UIStackView for achieving the desired appearance instead of adding the buttons directly in the scroll view. The gap would be represented as spacing property.
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
// here is the "gap":
stackView.spacing = 16.0
therefore, you should add the buttons in it:
stackView.addArrangedSubview(button)