Adding UIButtons in a UIView with dynamic width in swift - ios

I am trying to add UIButtons in a UIView, buttons width will change depending on their title. So it could be one button in a row or 3 buttons in a row. If there is no space in same row button should be added in the next row. Something like this
and UIView height should increase if more lines are added up-to a certain height.
I am thinking to start with UIStackViews but not sure how to achieve this functionality. Any help is appreciated.

A UICollectionView using a UICollectionViewFlowLayout would do this by default. The only thing you would have to do is update the height of the collevtionView to match its content size if you don't want it to certainly scroll.
Using a UIStackView would require you to calculate the the size of every button and move down to a new row when they don't fit. While that doesn't sound to hard, it's a lot of layout math you'll have to do.

I was able to do this with a custom button generator
import Foundation
import UIKit
class ButtonGenerator {
private var buttonTitleArray = [String]()
private var buttonBackgroundColor: UIColor = UIColor.white
private var buttonFontColor: UIColor = UIColor.black
private var passedFunctionCall: Selector?
private var passedParentView = UIScrollView()
private var parentViewWidth = CGFloat()
private var desiredMargin = CGFloat()
private var buttonsToReturn = [UIButton]()
func setButtonStyle(bkgColor:UIColor,fontColor:UIColor) {
buttonBackgroundColor = bkgColor
buttonFontColor = fontColor
}
func setMargin(to margin:CGFloat) {
desiredMargin = margin
}
func addButtons(to parentView:UIScrollView,withTitles passedTitles [String],calling function:Selector){
//Receive Passed Arguments
passedParentView = parentView
parentViewWidth = parentView.frame.width
buttonTitleArray = passedTitles
passedFunctionCall = function
//Track current origin coordinates
var XPos = desiredMargin
var YPos = desiredMargin
var buttonHeight = CGFloat()
for i in 0..<buttonTitleArray.count {
let buttonToAdd = UIButton(type: .custom)
buttonToAdd.backgroundColor = buttonBackgroundColor
buttonToAdd.setTitleColor(buttonFontColor, for: .normal)
buttonToAdd.setTitle(buttonTitleArray[i], for: .normal)
buttonToAdd.sizeToFit()
buttonHeight = buttonToAdd.frame.height
buttonToAdd.addTarget(self, action: passedFunctionCall!, for: .touchUpInside)
if XPos + buttonToAdd.frame.width < parentViewWidth-(2*desiredMargin) {
//buttonToAdd won't be clipped --> Okay to add to current row
buttonToAdd.frame.origin.x = XPos
buttonToAdd.frame.origin.y = YPos
//no change in row --> only update X position
XPos += buttonToAdd.frame.width + desiredMargin
}
else {
//buttonToAdd will be clipped --> display on next row
XPos = desiredMargin
YPos += buttonHeight + desiredMargin
//set button position
buttonToAdd.frame.origin.x = XPos
buttonToAdd.frame.origin.y = YPos
//update X position
XPos += buttonToAdd.frame.width + desiredMargin
}
//Add Button to return array
buttonsToReturn.append(buttonToAdd)
}
//Set YPos to position of content bottom
YPos += buttonHeight + desiredMargin
//Set content size of the parent scroll view to the generated height
passedParentView.contentSize = CGSize(width: parentViewWidth, height: YPos)
//Add the buttons to the scroll view
for i in 0..<buttonsToReturn.count {
passedParentView.addSubview(buttonsToReturn[i])
}
}
}
Then to call the button generator in your ViewController you'd add the following:
let btnGen = ButtonGenerator()
btnGen.setButtonStyle(bkgColor: UIColor.white, fontColor: UIColor.blue)
btnGen.setMargin(to: 10)
btnGen.addButtons(to: hashtagScrollView, withTitles: PostModel.Post.genericMusicHashtags, calling:#selector(buttonClicked(sender:)))
Also, because of the selector, you'll have to add an #objc func to your ViewController Class.
#objc func buttonClicked(sender: UIButton) {
print(sender.titleLabel!.text!)
}
It doesn't have the limiting height functionality, but you should be able to modify the button generator to include an if statement to stop adding buttons.

Related

Is there any way to place buttons according to a central point in swift?

this is what I am trying to do:
Is anyone know how can i do it in swift?
I know there are some libraries that do something similar but I am trying to do it myself
You can achieve something like that with UICollectionView and custom UICollectionViewLayout
Some examples:
https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel
https://augmentedcode.io/2019/01/20/circle-shaped-collection-view-layout-on-ios/
https://github.com/robertmryan/CircularCollectionView
SwiftUI
You can position any view anywhere using either offset(x: y) or position(x:y).
offset(x:y:)
Offset this view by the specified horizontal and vertical distances. - https://developer.apple.com
position(x:y:)
Positions the center of this view at the specified coordinates in its
parent’s coordinate space.https://developer.apple.com
For instance:
ZStack {
Text("A")
.background(Color.red)
.position(x: 10, y: 20)
Text("b")
.background(Color.red)
.position(x: 50, y: 30)
Text("c")
.background(Color.red)
.position(x: 100, y: 40)
Text("d")
.background(Color.red)
.position(x: 150, y: 200)
}
Find the exact x and y position yourself
Fore more info, read this articel
https://www.hackingwithswift.com/books/ios-swiftui/absolute-positioning-for-swiftui-views
Sure. Set up constraints with offsets from your center point, and use trig to calculate the offsets. The code might look something like this:
let steps = 16 // The number of buttons you want
let radius = 75.0. // Your desired circle radius, in points
let angleStep = Double.pi * 2.0 / Double(steps)
for index in 0 ..< steps {
let angle = Double(index) * angleStep
let xOffset = CGFloat(radius * cos(angle))
let yOffset = CGFloat(radius * sin(angle))
// add button to superview with center anchored to center of superview
// offset by xOffset and yOffset
}
Edit:
I mostly create my views and controls using storyboards, so this was a good excuse to practice creating them in code. I made a demo project on github that creates a circle of buttons. You can download it here:
https://github.com/DuncanMC/ButtonsInCircle.git
All the logic is in the View controller's source file:
//
// ViewController.swift
// ButtonsInCircle
//
// Created by Duncan Champney on 2/28/21.
//
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var buttonContainerView: UIView!
let buttonCount = 12 //The number of buttons to create
var angleStep: Double = 0 //The change in angle between buttons
var radius: CGFloat = 75.0 // The radius to use (will be updated at runtime based on the size of the container view.)
//A type to hold a layout anchor for a button, it's index, and whether it's a horizontal or veritical anchor
typealias ConstraintTuple = (index: Int, anchor: NSLayoutConstraint, axis: NSLayoutConstraint.Axis)
//An array of the layout anchors for our buttons.
var constraints = [ConstraintTuple]()
//Our layout has changed. Update the array of layout anchors
func updateButtonConstraints() {
for (index, constraint, axis) in self.constraints {
let angle = Double(index) * self.angleStep
let xOffset = self.radius * CGFloat(cos(angle))
let yOffset = self.radius * CGFloat(sin(angle))
if axis == .horizontal {
constraint.constant = xOffset
} else {
constraint.constant = yOffset
}
}
}
override func viewDidLayoutSubviews() {
//Pick a radius that's a little less than 1/2 the shortest side of our bounding rectangle
radius = min(buttonContainerView.bounds.width, buttonContainerView.bounds.height) / 2 - 30
print("Radius = \(radius)")
updateButtonConstraints()
}
func createButtons() {
for index in 0 ..< buttonCount {
//Create a button
let button = UIButton(primaryAction:
//Define the button title, and the action to trigger when it's tapped.
UIAction(title: "Button \(index+1)") { action in
print("Button \(index + 1) tapped")
}
)
button.translatesAutoresizingMaskIntoConstraints = false //Remember to do this for UIViews you create in code
button.layer.borderWidth = 1.0 // Draw a rounded rect around the button so you can see it
button.layer.cornerRadius = 5
button.setTitle("\(index+1)", for: .normal)
button.setTitleColor(.blue, for: .normal)
//Add it to the container view
buttonContainerView.addSubview(button)
button.sizeToFit()
//Create center x & y layout anchors (with no offset to start)
let buttonXAnchor = button.centerXAnchor.constraint(equalTo: buttonContainerView.centerXAnchor, constant: 0)
buttonXAnchor.isActive = true
//Add a tuple for this layout anchor to our array
constraints.append(ConstraintTuple(index: index, anchor: buttonXAnchor, axis: .horizontal))
let buttonYAnchor = button.centerYAnchor.constraint(equalTo: buttonContainerView.centerYAnchor, constant: 0)
buttonYAnchor.isActive = true
//Add a tuple for this layout anchor to our array
constraints.append(ConstraintTuple(index: index, anchor: buttonYAnchor, axis: .vertical))
}
}
override func viewDidLoad() {
super.viewDidLoad()
angleStep = Double.pi * 2.0 / Double(buttonCount)
createButtons()
}
}
It looks like this when you run it:
(On iOS, the starting angle (0°) is "East" in terms of a compass. If you want your first button to start at the top you'd have to add an offset to your starting angle.)

How to dynamically create multiple buttons after the text of a UITextView

I'm trying to create a small choose your own adventure game. After the text of each page I want to have an area for different options the player can choose. The buttons need to be displayed after all the text and not be visible until the player reads (or scrolls down) all the text.
Here's how I'm trying to create the buttons:
func CreateButtons(buttons: Int, loadedQuest: XML) {
let buttonHeight: CGFloat = 44
let contentInset: CGFloat = 8
//inset the textView
txtQuestText.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: (buttonHeight+contentInset*2), right: 0)
for i in 1...buttons{
let button = UIButton(frame: CGRect(x: contentInset, y: (txtQuestText.contentSize.height - (contentInset * CGFloat(i))) + (buttonHeight * CGFloat(i)) - contentInset, width: txtQuestText.contentSize.width-contentInset*2, height: buttonHeight))
//setup your button here
button.setTitle("Option \(i)", for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
button.backgroundColor = UIColor.darkGray
//button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
//Add the button to the text view
txtQuestText.addSubview(button)
}
}
Here's how it's currently looking(make sure to scroll all the way down) The buttons are somehow being created outside the scroll range of the textview. How would I fix this and add padding between the buttons?
First you have to check the scroll view is at the bottom or not?
if it is at the end/bottom you may add buttons
this will help you to check it is at the end or not
extension UIScrollView {
var isAtTop: Bool {
return contentOffset.y <= verticalOffsetForTop
}
var isAtBottom: Bool {
return contentOffset.y >= verticalOffsetForBottom
}
var verticalOffsetForTop: CGFloat {
let topInset = contentInset.top
return -topInset
}
var verticalOffsetForBottom: CGFloat {
let scrollViewHeight = bounds.height
let scrollContentSizeHeight = contentSize.height
let bottomInset = contentInset.bottom
let scrollViewBottomOffset = scrollContentSizeHeight + bottomInset - scrollViewHeight
return scrollViewBottomOffset
}
}
add this extension
Hopeful it will work

How can you make a custom scroll indicator using Swift?

I have a UITableView that has the potential to have a lot of rows (importing contacts) and I'm looking to implement a custom scroll indicator similar to how Snapchat has. The two basic goals are as follows:
When you scroll the tableview, the scroll indicator moves down/up the correct amount (and does not exceed the top or bottom of the tableview).
When you drag the scroll indicator, it not only follows your finger, but scrolls the tableview to correct amount.
I've been trying to use different calculations for the scroll amount, but I can't seem to get it to be correct. I've been doing work on a sample app (just containing names in a tableview) and I can get it to work okay on there but when I transfer the code to a larger tableview, it doesn't work properly.
I've created a custom view for my scroll indicator which is basically just a view with a label on it that changes based on the letter of the name in the cell:
class IndicatorView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var letterLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("IndicatorView", owner: self, options: nil)
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView)
contentView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
contentView.layer.cornerRadius = 5
}
}
Here are the methods I'm using...
scrollViewWillEndDragging:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
//Get velocity and store in global variable
self.velocity = velocity.y
}
scrollViewDidScroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollBar.becomeFirstResponder()
let offset: CGPoint = scrollView.contentOffset
var frame = self.scrollBar.frame
var numberOfRows = 0
for i in 0...friendsTableView.numberOfSections - 1 {
let rows = friendsTableView.numberOfRows(inSection: i)
numberOfRows += rows
}
//Calculate the amount of scroll
let percentage = (44*CGFloat(numberOfRows) + 40)/friendsTableView.frame.height
UIView.animate(withDuration: 0.0) {
//Set the y value to new percentage
frame.origin.y = 30 + offset.y + (offset.y/percentage)
//Set label to correct letter
let currentPoint = CGPoint(x: 30, y: frame.origin.y)
if let indexPath = self.friendsTableView.indexPathForRow(at: currentPoint) {
let letter = self.sectionHeaders[indexPath.section]
self.scrollBar.letterLabel.text = letter
}
//Only expand the scroll indicator if the user scrolls fast enough
if abs(self.velocity) > 1.5 {
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 50
frame.origin.x = self.screenWidth - 50
}
//Set the new y value of the scroll indicator
self.scrollBar.frame = frame
}
}
Custom Pan Gesture Recognizer:
#objc func customDrag(_ sender: UIPanGestureRecognizer) {
self.friendsTableView.bringSubview(toFront: scrollBar)
//Only expand fully if the state is not ended
if sender.state != .ended {
var frame = self.scrollBar.frame
//Expand the view fully
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 100
frame.origin.x = self.screenWidth - 100
self.scrollBar.frame = frame
//Get translation and track the content offset
let translation = sender.translation(in: friendsTableView)
let originalContentOffset = friendsTableView.contentOffset
var numberOfRows = 0
for i in 0...friendsTableView.numberOfSections - 1 {
let rows = friendsTableView.numberOfRows(inSection: i)
numberOfRows += rows
}
//Calculate the percentage of scroll and add the translation to the original content offset
let percentage = (44*CGFloat(numberOfRows) + 50)/friendsTableView.frame.height
let vertTranslation = (percentage*translation.y) + originalContentOffset.y
//Set the content offset and the center of the scroll indicator
friendsTableView.contentOffset = CGPoint(x: 0, y: (vertTranslation))
self.scrollBar.center = CGPoint(x: self.screenWidth-50, y: self.scrollBar.center.y + (translation.y/percentage))
sender.setTranslation(CGPoint.zero, in: friendsTableView)
} else {
//once the state is ended, collapse back to the smaller size
var frame = self.scrollBar.frame
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 50
frame.origin.x = self.screenWidth - 50
self.scrollBar.frame = frame
}
}
I found the basics for the code that does the percentage calculation on another StackOverflow post but admittedly, I'm not 100% sure what it is doing/why it works in my demo app and not the real one. I've applied different number and calculations but still can't seem to get it right.
Any help or insight would be much appreciated.

UIButton as subview programmatically constraint

I have one UIScrollView (IBOutlet) with success constraint inside a storyboard. then I programmatically create UIButton and put them as a subview to UIScrollView. how do I programmatically set these UIButton constraints so their height and size would tally with their super view?
class ViewController: UIViewController {
#IBOutlet weak var aScrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var xCoord: CGFloat = 5
let yCoord: CGFloat = 5
let buttonWidth:CGFloat = 100
let buttonHeight: CGFloat = 100
let gapBetweenButtons: CGFloat = 10
var itemCount = 0
// MARK: - filter buttons
for i in 0..<6 {
itemCount = i
let aButton = UIButton(type: .custom)
aButton.frame = CGRect(x: xCoord, y: yCoord, width: buttonWidth, height: buttonHeight)
aButton.backgroundColor = UIColor.blue
aButton.layer.cornerRadius = aButton.frame.size.width / 2
aButton.clipsToBounds = true
xCoord += buttonWidth + gapBetweenButtons
aScrollView.addSubview(aButton)
}
}
Try this --
This is just an idea about how to add constraint programmatically.
For more understanding you can go through below link - https://developer.apple.com/reference/appkit/nslayoutanchor
let myButton = UIButton()
self.aScrollView.addSubview(myButton)
self.view.translatesAutoresizingMaskIntoConstraints = false
let margins = self.view.layoutMarginsGuide
myButton.leadingAnchor.constraint(equalTo: forView. aScrollView.leadingAnchor, constant: 5).active = true
myButton.topAnchor.constraint(equalTo: forView. aScrollView.topAnchor, constant: 5).active = true
myButton.heightAnchor.constraintEqualToConstant(100.0).active = true
myButton.widthAnchor.constraintEqualToConstant(100.0).active = true
Change your code to:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var xCoord: CGFloat = 5
let yCoord: CGFloat = 5
let gapBetweenButtons: CGFloat = 10
let buttonCount = 6
let buttonWidth = (aScrollView.frame.width - CGFloat(buttonCount - 1) * gapBetweenButtons - xCoord - xCoord) / CGFloat(buttonCount) // - (2 * xCoord) = - (margin left + margin right).
let buttonHeight = buttonWidth
var itemCount = 0
// MARK: - filter buttons
for i in 0..<buttonCount {
itemCount = i
let aButton = UIButton(type: .custom)
aButton.frame = CGRect(x: xCoord, y: yCoord, width: buttonWidth, height: buttonHeight)
aButton.backgroundColor = UIColor.blue
aButton.layer.cornerRadius = aButton.frame.size.width / 2
aButton.clipsToBounds = true
xCoord += buttonWidth + gapBetweenButtons
aScrollView.addSubview(aButton)
}
}
This is how to do it. I made a video tutorial for you add unbutton programmatically to scrollview in iOS, swift. please refer the link.
add a scrollview (add constraints -> top, bottom, left, right).
then add a view to your scroll view (add constraints -> top, bottom, left, right, width, height and set width and height as you need. explain in the video)
add those two views as subviews.
then add the button and its constraints programmatically.
follow the video tutorial. add UIButton(outlet) to scrollview programmatically

view object not showing in swift simulator running xcode 8

I have been working through the Food Tracker app tutorial as a project to learn Swift. I understand that this is full of bugs, but I have been researching ways to correctly format my code. I got to the star rating section, added the red button, separated it into 5 buttons, then added the stars. The stars showed up for a bit, but now do not show up in my simulator at all. I have cleaned my code, disconnected and added images and image code, disconnected and added my IBOutlets, etc. They still are not showing so I am thinking there has to be something I am missing in my code since I am new to this. Any help would be much appreciated.
import UIKit
class RatingControl: UIView {
// MARK: Properties
var rating = 0 {
didSet {
setNeedsLayout()
}
}
var ratingButtons = [UIButton]()
var spacing = 5
var stars = 5
// MARK: Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let filledStar = UIImage(named: "filledStar")
let emptyStar = UIImage(named: "emptyStar")
for _ in 0..<5 {
let button = UIButton()
button.setImage(emptyStar, for: UIControlState())
button.setImage(filledStar, for: .selected)
button.setImage(filledStar, for: [.highlighted, .selected])
button.adjustsImageWhenHighlighted = false
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(_:)), for: .touchDown); ratingButtons += [button]
addSubview(button)
}
}
override func layoutSubviews() {
// Set the button's width and height to a square the size of the frame's height.
let buttonSize = Int(frame.size.height)
var buttonFrame = CGRect(x: 0, y: 0, width: buttonSize, height: buttonSize)
// Offset each button's origin by the length of the button plus spacing.
for (index, button) in ratingButtons.enumerated() {
buttonFrame.origin.x = CGFloat(index * (buttonSize + spacing))
button.frame = buttonFrame
}
updateButtonSelectionStates()
}
override var intrinsicContentSize : CGSize {
let buttonSize = Int(frame.size.height)
let width = (buttonSize + spacing) * stars
return CGSize(width: width, height: buttonSize)
}
// MARK: Button Action
func ratingButtonTapped(_ button: UIButton) {
rating = ratingButtons.index(of: button)! + 1
updateButtonSelectionStates()
}
func updateButtonSelectionStates() {
for (index, button) in ratingButtons.enumerated() {
// If the index of a button is less than the rating, that button should be selected.
button.isSelected = index < rating
}
}
}
In your init?() method, you've missed adding button to ratingButtons array as a result nothing is turned up when the array is enumerated and accessed, and it is empty
Please add the following
self.ratingsButton.append(button)
right after
addSubview(button)
Hopefully the buttons will start showing up.

Resources