Set NSButton size in NSStackView - swift

I programmatically create buttons using this code
let button: NSButton = NSButton()
button.action = #selector(ViewController.eject)
button.bezelStyle = .texturedSquare
buttons.append(button)
stackView.addView(buttons[i], in: .leading)
The problem is that those buttons appear like that
I would like to set the (x , y) position and the size of those buttons. For example, I would like to set their width to fill the whole space.
I tried multiple solutions:
button.frame = CGRect(x: 0.0,y: 0.0,width: 100.0,height: 100.0)
and play with the arguments, but still their size don't change.
I then tried to add them as SubViews to stackView, but obviously they overlap with each other.
Any suggestion?

To fix those buttons you first have to modify the StackView element in the storyboard.
Go to the menu of the storyboard and modify it like that
With the "fill" distribution you let the buttons to fill the entire width of the stackview.
With "spacing" you define how much space there has to be between each button.
Then you go there
To give some space to the buttons just play with the Edge Insets values.
Then set the horizontal Hugging Propriety to 250.
The last thing you have to do is to add this code in your ViewController file.
button.setContentHuggingPriority(NSLayoutConstraint.Priority.init(1), for: .horizontal)
button.translatesAutoresizingMaskIntoConstraints = false
In this way you'll the the horizontal Hugging Propriety of the button to 1.
Here's the buttons I created

Related

How I can show/hide view inside another view with height changing of root swift?

I have two views one inside another, it looks like picture below:
when I press on orange arrow I would like to show/hide view above grey line and grey line too. I did it by such way:
#objc func showHide(tapGestureRecognizer: UITapGestureRecognizer)
{
let tappedImage = tapGestureRecognizer.view as! UIImageView
UIView.animate(withDuration: 0.2, animations: { () -> Void in
self.jobDataView.isHidden = !self.jobDataView.isHidden
self.view.layoutIfNeeded()
})
tappedImage.image = self.jobDataView.isHidden ? UIImage(systemName: "arrow.down"):UIImage(systemName: "arrow.up")
}
and my view above gray line can be hidden and shown. But root view doesn't change its' height and it has similar sizes before and after btn click. I tried to add constraint of height, but I it didn't solve my problem. Maybe someone knows how to solve my problem?
You need to use UIStackView either through Storyboard or from code. when you hide subview inside stackview it will automatically change stackview height. what you need to do is to make stackView distribution = fill and use vertical stack...
Hide view on button tap and when you need to show it .. add it in stack at. 0th index ..

Swift constraint doesn't update [duplicate]

(Xcode 11, Swift)
Being a newbie to iOS and Autolayout, I'm struggling with implementing a fairly simple (IMHO) view which displays a [vertical] list of items. The only problem is that items are decided dynamically and each of them could be either text or image (where either of those could be fairly large so scrolling would be required). WebView is not an option, so it has to be implemented natively.
This is how I understand the process:
Make in IB a UIScrollView and size it to the size of the outer frame.
Make a container view as a subview of UIScrollView (again, in IB) and size it the same.
Set constraint on equal width of both
At runtime, populate container view with UILabels/UIImageViews and also set constraints programmatically to ensure proper layout.
"Tell" scrollview about the subview height in order to make it manage the scrolling thereof.
Is this the right approach? It doesn't seem to work for me (for a toy example of dynamically adding a very tall image to a container view - I cannot get the scrolling to work). What would be the proper way to do the last step in the process above - just force the contentSize of the scrollview to the size of the populated container view (it doesn't seem to work for me). Any help would be appreciated.
When adding multiple elements to a scroll view at run-time, you may find it much easier to use a UIStackView... when setup properly, it will automatically grow in height with each added object.
As a simple example...
1) Start by adding a UIScrollView (I gave it a blue background to make it easier to see). Constrain it to Zero on all 4 sides:
Note that we see the "red circle" indicating missing / conflicting constraints. Ignore that for now.
2) Add a UIView as a "content view" to the scroll view (I gave it a systemYellow background to make it easier to see). Constrain it to Zero on all 4 sides to the Content Layout Guide -- this will (eventually) define the scroll view's content size. Also constrain it equal width and equal height to the Frame Layout Guide:
Important Step: Select the Height constraint, and in the Size Inspector pane select the Placeholder - Remove at build time checkbox. This will satisfy auto-layout in IB during design time, but will allow the height of that view to shrink / grow as necessary.
3) Add a Vertical UIStackView to the "content view". Constrain it to Zero on all 4 sides. Configure its properties to Fill / Fill / 8 (as shown below):
4) Add an #IBOutlet connection to the stack view in your view controller class. Now, at run-time, as you add UI elements to the stack view, all of your "scrollability" will be handled by auto-layout.
Here is an example class:
class DynaScrollViewController: UIViewController {
#IBOutlet var theStackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
// local var so we can reuse it
var theLabel = UILabel()
var theImageView = UIImageView()
// create a new label
theLabel = UILabel()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theLabel.translatesAutoresizingMaskIntoConstraints = false
// multi-line
theLabel.numberOfLines = 0
// cyan background to make it easy to see
theLabel.backgroundColor = .cyan
// add 9 lines of text to the label
theLabel.text = (1...9).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .yellow
// add 5 lines of text to the label
theLabel.text = (1...5).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// create a new UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load an image for it - I have one named background
if let img = UIImage(named: "background") {
theImageView.image = img
}
// let's give the image view a 4:3 width:height ratio
theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 4.0/3.0).isActive = true
// add it to the stack view
theStackView.addArrangedSubview(theImageView)
// add another label
theLabel = UILabel()
// multi-line
theLabel.numberOfLines = 0
// yellow background to make it easy to see
theLabel.backgroundColor = .green
// add 2 lines of text to the label
theLabel.text = (1...2).map({ "Line \($0)" }).joined(separator: "\n")
// add it to the stack view
theStackView.addArrangedSubview(theLabel)
// add another UIImageView
theImageView = UIImageView()
// this gets set to false when the label is added to a stack view,
// but good to get in the habit of setting it
theImageView.translatesAutoresizingMaskIntoConstraints = false
// load a different image for it - I have one named AquariumBG
if let img = UIImage(named: "AquariumBG") {
theImageView.image = img
}
// let's give this image view a 1:1 width:height ratio
theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor, multiplier: 1.0).isActive = true
// add it to the stack view
theStackView.addArrangedSubview(theImageView)
}
}
If the steps have been followed, you should get this output:
and, after scrolling to the bottom:
Alignment constraints (leading/trailing/top/bottom)
The alignment constraint between Scroll View and Content View defines the scrollable range of the content. For example,
If scrollView.bottom = contentView.bottom, it means Scroll View is
scrollable to the bottom of Content View.
If scrollView.bottom = contentView.bottom + 100, the scrollable
bottom end of Scroll View will exceed the end of Content View by 100
points.
If scrollView.bottom = contentView.bottom — 100, the bottom of
Content View will not be reached even the scrollView is scrolled to
the bottom end.
That is, the (bottom) anchor on Scroll View indicates the (bottom) edge of the outer frame, i.e., the visible part of Content View; the (bottom) anchor on Content View refers to the edge of the actual content, which will be hidden if not scrolled to.
Unlike normal use cases, alignment constraints between Scroll View and Content View have nothing to do with the actual size of Content View. They affect only “scrollable range of content view” but NOT “actual content size”. The actual size of Content View must be additionally defined.
Size constraints (width/height)
To actually size Content View, we may set the size of Content View to a specific length, like width/height of 500. If the width/height exceeds the width/height of Scroll View, there will be a scrollbar for users to scroll.
However, a more common case will be, we want Content View to have the same width (or height) as Scroll View. In this case, we will have
contentView.width = scrollView.width
The width of Content View refers to the actual full width of content. On the other hand, the width of Scroll View refers to the outer container frame width of Scroll View. Of course, it doesn’t have to be the same width, but can be other forms like a * scrollView.width + b.
And if we have Content View higher (or wider) than Scroll View, a scrollbar appears.
Content View can not only be a single view, but also multiple views, as long as they are appropriately constrained using alignment and size constraints to Scroll View.
For details, you may follow this article: Link.

Swift - Automatically adding a button to the right of the button just added

I have a UIScrollView set up, and am using addSubView to add 5 buttons to it. Here is the code inside my viewDidLoad.
let codedButton:UIButton = UIButton(frame: CGRectMake(0, 0, 100, 50))
codedButton.backgroundColor = UIColor.redColor()
codedButton.setTitle("Button 1", forState: UIControlState.Normal)
codedButton.addTarget(self, action: #selector(ViewController.buttonAction(_:)), forControlEvents: UIControlEvents.TouchUpInside)
codedButton.tag = 1
self.buttonScrollView.addSubview(codedButton)
let codedButton2:UIButton = UIButton(frame: CGRectMake(110, 0, 100, 50))
codedButton2.backgroundColor = UIColor.redColor()
codedButton2.setTitle("Button 2", forState: UIControlState.Normal)
codedButton2.addTarget(self, action: #selector(ViewController.buttonAction(_:)), forControlEvents: UIControlEvents.TouchUpInside)
codedButton2.tag = 2
self.buttonScrollView.addSubview(codedButton2)
(all the way to 5.)
The problem that I need help with is... Ultimately, these buttons will be dynamically displayed. Not just a set amount of 5.
How do I adjust the X value of the CGRectMake to automatically "float" new buttons to the right of the button that was just created? In my code above, I am explicitly stating where the X is, but that won't work properly once these buttons become dynamically created in code.
EDIT
To clarify my question above:
I have attached an image. Note how the buttons always stack horizontally / next to each other, regardless of if there's 2 3 4 or 5. These buttons will be dynamically shown. Therefore, with my code attempt I have provided above, it will not work because it is explicitly setting where to place the buttons on the X axis.
I need to modify my code to perform like the image below.
I see that you are doing this programmatically. I can't help with the code, but stack views in the storyboard are great. I have a feature in my app where I have 4 buttons, but the user may only see, say, 2 of the 4 buttons.
1) Add the buttons horizontally into the storyboard (They don't need to be perfectly aligned, just enough so that Xcode can recognize it's horizontal rather than vertical.
2) Select all 5 buttons
3) Click on the stack view button in the lower right-hand corner of Xcode (The button with 4 horizontal lines with and an arrow pointing down.
4) In the attributes inspector, choose horizontal as the axis, alignment as center, fill proportionately, play with the spacing, and choose scale to fill as the mode.
5) In the Swift file, you can add or hide the buttons using button.hidden = true / false
NOTE: Keep in mind that stack views are only available with iOS 9 when I last checked
Below is an example of the loop you can add to your current implementation of one button to add as many buttons as you would like, and in this example the number of buttons is dependent on the number of button names that you add to the array called "buttonList". For each iteration of the loop it will name the button from the text in the ButtonList and set the tag to its index in the array. It will then calculate the x coordinate of your frame, and in this example I made each button 100px wide, and have a 10px space in between, starting at x coordinate location 0. If you wanted to add 10px space between the first button and the leading edge of the view just increment the index when calculating the x coordinate with:
let xCoord = CGFloat(index*buttonWidth + (index+1)*buttonSpace)
You will also need to increase the size of your scroll view to fit buttons as more are added, this is the calculation I used for setting my scroll view width (Increment the buttonSpace width by 1 if adding a leading space to your scroll view) : buttonScrollView.contentSize = CGSizeMake(CGFloat((ButtonList.count * buttonWidth) + ((ButtonList.count + 1) * buttonSpace)), 50)
var ButtonList = ["Button 1", "Button 2", "Button 3"]
let buttonWidth = 100
let buttonSpace = 10
for (index,ButtonText) in ButtonList.enumerate(){
//calculate the x coordinate of each button
let xCoord = CGFloat(index*buttonWidth + index*buttonSpace)
let codedButton:UIButton = UIButton(frame: CGRectMake(xCoord, 0, 100, 50))
codedButton.backgroundColor = UIColor.redColor()
codedButton.setTitle(ButtonText, forState: UIControlState.Normal)
codedButton.addTarget(self, action: #selector(ViewController.buttonAction(_:)), forControlEvents: UIControlEvents.TouchUpInside)
codedButton.tag = index
self.buttonScrollView.addSubview(codedButton)
}
I do not know exactly if that is what you're asking for.. if not, leave a comment.
Create a counter and name your buttons..
button.name = "name\(counter)"
Get the position of your last button using..
let position = self.childNodeWithName("name\(counter)")?.position
Set the position of your new button..
newButton.position = CGPointMake(position.x + newButton.frame.size.width/2 + oldButton.frame.size.width/2, position.y)
.. your new button is now set right to your last button added

Setting my button's position programmatically in swift

I've been using auto layout so far, so I'm not sure how this works.
I have a simple button I want to be close to my down right corner.
How can its position programmatically in swift?
Here you go:
let button = UIButton()
button.setTitle("Button", forState: .Normal)
button.backgroundColor = UIColor.blackColor()
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor, constant: -20).active = true
button.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor, constant: -20).active = true
Setting the translatesAutoresizingMaskIntoConstraints to false is necessary. By default, the property is set to true for any view you programmatically create.
In this case you need only 2 constraints (bottomAnchor and trailingAnchor). Since the button has the intrinsicContentSize (the natural size based on the title and stuff), you don't have to add constraints for the height/width.
Also you might want to use the layoutMarginsGuide to pin the button to the edges of the superview. This way you don't need to specify the constants:
superView.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
let margins = superView.layoutMarginsGuide
button.bottomAnchor.constraintEqualToAnchor(margins.bottomAnchor).active = true
button.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor).active = true
Note that if the view is a view controller’s root view, then the system sets and manages the margins. The top and bottom margins are set to 0 points. The side margins vary depending on the current size class, but can be either 16 or 20 points. You cannot change these margins.
You can use autolayout programmatically.
SnapKit is a nice swift library to make the autolayour code more easy to use and readable.
Otherwise you can always use it the normal way. These are two nice tutorials on it:
1
2

tvOS: UIScrollView doesn't scroll

I am trying to reproduce natively the TVML template that provides a grid of clickable images that extends beyond the screen's bounds. I am using a scroll view for this attempt, but I am unable to select elements that are added to the scroll view, but outside its visible area.
The sketch code using buttons for simplicity is as follows:
let dim = 50
for i in 0..<10 {
for j in 0..<10 {
let frame = CGRect(x: i * (dim + 10), y: j * (dim + 10), width: dim, height: dim)
let button = UIButton(type: .System)
button.frame = frame
myScrollView.panGestureRecognizer.allowedTouchTypes = [UITouchType.Indirect.rawValue]
myScrollView.addSubview(button)
}
}
The scroll view is sized such that only half of these buttons are visible. Why is the scroll view not scrolling to the buttons outside this area (using Siri remote)?
I thought the panGesture touchType might help, but it didn't.
Am I missing something obvious?
Set contentSize property to your scrollview. Make sure all components comes under given content size.
myScrollView.contentSize = CGSizeMake(1880, 2000)
It would actually be way easier to just use a UICollectionView. If you add an image to each cell, you'll get exactly the behavior you want after adjusting the collection view to what you want.
This tutorial kind of explains how it works. http://www.brianjcoleman.com/tutorial-collection-views-using-flow-layout/