I am trying to use UIStackView with dynamic content on my project. The stackview is pinned to the leading & trailing of the container view. Why have I pinned it? Because Xcode will otherwise complained that I have to specify width or X position. But maybe this is not the right approach - let me know.
The stack view is currently configured with:
alignment: center (i've tried 'Filled' too)
distribution: equal spacing
Anyhow, the content of the stackview itself is created from a XIB file, which gets expanded into a custom UIView. This kind of works, but I'm having several issues with the layout:
When there are only a few items in the stackview, then they are sparsely distributed. Ideally I want the UIView (the orange button with the green area) to resize as big as possible to fill in these gaps
When there are a lot of items in the stackview, they are currently stacked on top of each other. This is an issue. The individual UIView should be resized to the biggest size that will make them fit horizontally in the UIStackview.
What I've done to add the UIView(s) to the stackview is as follow:
labelStackView.arrangedSubviews.forEach { (view) in
view.removeFromSuperview()
}
for (index, character) in model.modelName.enumerated() {
// CharacterPlaceholder extends UIView
let characterPlaceHolder = CharacterPlaceholder()
...
labelStackView.addArrangedSubview(characterPlaceHolder)
}
while CharacterPlaceholder roughly looks like below
class CharacterPlaceholder: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
customInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
customInit()
}
private func customInit(){
let nib = UINib(nibName: "CharacterPlaceholder", bundle: nil)
if let view = nib.instantiate(withOwner: self, options: nil).first as? UIView {
addSubview(view)
view.frame = self.bounds
}
}
I have uploaded the project to my github account:
https://github.com/alexwibowo/Flipcard
Any idea what I've done wrong? Should I grab the screensize at the start (or during screen rotation), and then manually calculate the width required for the buttons? That seems painful & very manual. I'm hoping that there is some autolayout magic that I can use here.
Thank you in advance for the help!
Edit: I gather that I need to use 'fill equally' for the distribution, so that the individual letter blocks will have the same size. But also, so that they are constrained to the available stackview space. However, now they can still overlap.
Ok.. I've figured a way to do this. First, as I've mentioned, I need to set the distribution to 'fill equally' so that each view in the stackview will be sized equally.
But doing that by itself is not enough.
I need to get the screen size. i.e. through
view.frame.width
Then, when I expand the xib file, i need to resize it manually using simple maths. E.g.:
let viewWidth = view.frame.width
let numberOfCharacters = model.modelName.count
let widthOfEach = Int(viewWidth) / numberOfCharacters
then the resizing part:
characterPlaceHolder.widthConstraint.constant = widthOfEachIncludingMargin
That widthConstraint is an outlet that I've set on the characterPlaceHolder view.
Related
I have a UIView on a XIB, containing an UIImageView and a UILabel with a small space in between. Horizontal layout is a simple chained |--image--label--| in which -- is some fixed space. The height is fixed to 40, this view is horizontally centred in its view controller, and it has an >= 100 width constraint.
If I change the label text, the width of my composed view updates as expected width the changed width of its label, and it stays nicely centred on the view controller.
Because I need this UIView, containing an image and label, in other places, I've created a custom class consisting of a XIB and Swift file. Let's call it ItemView.
Issue I have is that the empty UIView on my view controller XIB, which I've changed class to ItemView, no longer accepts the >= 40 width constraint. This is of course because the view controller XIB no longer sees the variable width UILabel, but instead just a plain UIView of class ItemView. I get an 'Inequality Constraint Ambiguity' IB error.
The result is that the width of my custom view remains 40. It works a little bit if I specify a larger >= label width; the text is then only cut off when this width is reached. But in that second case my custom view is no longer horizontally centred, but shifted a bit to the left instead.
How do I resolve this? Or, how can I tell IB to treat my custom ItemView in a similar way as a UILabel?
In my ItemView I've done all I could find:
override class var requiresConstraintBasedLayout: Bool
{
return true
}
Call setNeedsLayout() after setting the label text.
Call myLabel.translatesAutoresizingMaskIntoConstraints = false.
Call self.translatesAutoresizingMaskIntoConstraints = false.
And call self.setNeedsUpdateConstraints() in both init()s.
Configure this pop-up in the view's Size inspector:
Now IB won't worry about your size specifications. You know better than IB does, and this is how to tell IB that fact.
Another way: configure this pop-up in the view's Size inspector:
This tells IB that the view will have an intrinsic content size that it doesn't know about.
Either of those will work. As you can see in this screenshot, I've given my custom view a width constraint of greater-than-or-equal-to-40, but IB is not complaining of any error:
How I structured my custom UIView: My XIB's Files Owner is ItemView. ItemView has an #IBOutlet var: UIView! that's connected to the view in the XIB. Then I have:
init()
{
super.init(frame: .zero)
self.initView()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initView()
}
private func initView()
{
Bundle.main.loadNibNamed("ItemView", owner: self, options: nil)
self.view.frame = CGRect(x: 0.0, y: 0.0, width: self.width, height: self.height)
self.addSubview(self.view!)
}
(I seems this extra UIView is needed when creating a custom UIView with a XIB. Would love to hear if it's not needed after all.)
To make it work I needed to call self.invalidateIntrinsicContentSize() after updating the label. And override intrinsicContentSize:
override open var intrinsicContentSize: CGSize
{
return self.view.systemLayoutSizeFitting(self.bounds.size)
}
Note that I'm calling this on self.view, not on self.
Thanks #matt for pointing me in the right direction. Was the first time I've encountered something like this.
The following things I tried we all not necessary:
override class var requiresConstraintBasedLayout: Bool
{
return true
}
Call setNeedsLayout() after setting the label text.
Call myLabel.translatesAutoresizingMaskIntoConstraints = false.
Call self.translatesAutoresizingMaskIntoConstraints = false.
And call self.setNeedsUpdateConstraints() in both init()s.
Currently, I am implementing custom view from a xib with Content View size set to Freeform. Here is my Content View hierarchy
Although the Content View is Freeform, I set the width to 375, which is the same width with iPhone 8. Then I create a custom UIView file
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("DetailsWeather", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
After that, I set the xib File’s Owner custom class name to the same custom UIView file above. Then I implement it into Main.storyboard by adding a UIView, set constrains properly and give it the custom class name the same with File’s Owner
When running iPhone 8, everything is perfect but switching to smaller device like iPhone 5s, I notice my scroll view now have horizontal scroll. Contrast with iPhone 5s, the bigger screen device like iPhone 8+, my scroll view now lost a bit of to the right side.
Notice the labels does not align with the clock which is center on iP8+ anymore
So I tried to remove the scroll view and everything work normal across devices. So from these, I was thinking the scroll view must messed up my custom view. Then I do some self research and found out these topics.
How to update constraints after adding UIView from xib on UIScrollView in swift?
ScrollView Add SubView in swift 3
I tried their solution and modified to fit my situation but none of them seem to work with me. So my question is, is there a way to make Content View of a xib file to fit every width?
Just solved today, feels great tbh
Remove all above step, just keep the IBOutlet in your custom UIView file
Remove File’s Owner custom class name and replace Content View custom class name to the same name with your custom UIView
Add UIView into your Main.storyboard, my case is ScrollView. Add all required constrains (duh)
Add IBOutlet for the ScrollView like this #IBOutlet weak var scrollView: UIScrollView!
Navigate to your View Controller file
let alohaView = Bundle.main.loadNibNamed("Feature", owner: self, options: nil)?.first as? FeatureView with Feature is your xib file and FeatureView is your custom UIView file control that xib file
Add alohaView?.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 500) notice I hardcode the 500 height, will work more on this
scrollView.contentSize = CGSize(width: self.view.frame.size.width, height: (alohaView?.frame.size.height)!)
scrollView.addSubview(alohaView!) add the custom view back to scroll view
I have the following scenario:
I have a xib file of name View1.xib which contain some stack views containing certain set of buttons.
View1.xib UIView
Im having a coco touch class of type UIView for loading the Nib named View1. Also I have made it as owner of the previous xib. Below is the code I'm using to load the view:
override init(frame: CGRect) {
super.init(frame: frame)
commitInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commitInit()
}
private func commitInit() {
Bundle.main.loadNibNamed("View1", owner: self, options: nil)
containerView.frame = self.bounds
containerView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(containerView)
}
Now in the storyboard I included the scrollview and a sub UIView under it. I have defined the Subview type as View1.
ScrollView in main storyboard
Also I have defined proper constraints for the Views making it equal height and width as the Parent ScrollView.
Scrollview constraints
Also find the scrollview properties defined as below:
ScrollView Properties
This is how the screen looks like on emulator :
Emulator
Now when I'm running the project I'm getting the screen but I'm unable to scroll through the view included. Due to this some elements on top and bottom becomes hidden (Out of bounds) and I'm unable to scroll as well. Am I doing anything wrong? Please help me out.
Note - I'm very new to swift, any help is greatly appreciated. Thanks in advance.
Your newView can't flow out of scrollView's bound, Because it has same height with your scrollView. But your newView's content flows out of newView's bounds.(You can see if you put your newView out of scrollView with same height and set it a backgroundColor or set clipToBounds property true) That's why you can't scroll.
To Sove This, you should find another source for your newView's height(Maybe a constant value or its subview's heihgt or safeArea.heihgt . This is your choice)
For subview add
yourView.translatesAutoresizingMaskIntoConstraints = false, then left, right, top, bottom constraint programatically then add subview.
For scroll view set the height constraint for inner view with the scroll view. Make the outlet of that constraint and then set dynamic height of the scrollview outlet.
How can I have an UIStackView with the same space as padding and gap between views?
How can I achieve this layout:
When this one doesn't suit me:
Neither does this:
I just need the around views space to be the same as the between views space.
Why is it so hard?
Important
I'm using my fork of TZStackView to support iOS 7. So no layoutMargins for me :(
I know this is an older question, but the way I solved it was to add two UIViews with zero size at the beginning and end of my stack, then use the .equalSpacing distribution.
Note: this only guarantees equal around spacing along the main axis of the stack view (i.e. the left and right edges in my example)
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .equalSpacing
// add content normally
// ...
// add extra views for spacing
stack.insertArrangedSubview(UIView(), at: 0)
stack.addArrangedSubview(UIView())
You can almost achieve what you want using a UIStackView. When you set some constraints yourself on the UIViews inside the UIStackView you can come up with this:
This is missing the left and right padding that you are looking for. The problem is that UIStackView is adding its own constraints when you add views to it. In this case you can add top and bottom constraints to get the vertical padding, but when you try to add a trailing constraint for the right padding, UIStackView ignores or overrides that constraint. Interestingly adding a leading constraint for the left padding works.
But setting constraints on UIStackView's arranged subviews is not what you want to do anyway. The whole point of using a UIStackView is to just give it some views and let UIStackView handle the rest.
To achieve what you are trying to do is actually not too hard. Here is an example of a UIViewController that contains a custom stack view that can handle padding on all sides (I used SnapKit for the constraints):
import UIKit
import SnapKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let padding: CGFloat = 30
let customStackView = UIView()
customStackView.backgroundColor = UIColor(white: 0, alpha: 0.1)
view.addSubview(customStackView)
customStackView.snp_makeConstraints { (make) -> Void in
make.top.left.equalTo(padding)
make.right.equalTo(-padding)
}
// define an array of subviews
let views = [UIView(), UIView(), UIView()]
// UIView does not have an intrinsic contentSize
// so you have to set some heights
// In a real implementation the height will be determined
// by the views' content, but for this example
// you have to set the height programmatically
views[0].snp_makeConstraints { (make) -> Void in
make.height.equalTo(150)
}
views[1].snp_makeConstraints { (make) -> Void in
make.height.equalTo(120)
}
views[2].snp_makeConstraints { (make) -> Void in
make.height.equalTo(130)
}
// Iterate through the views and set the constraints
var leftHandView: UIView? = nil
for view in views {
customStackView.addSubview(view)
view.backgroundColor = UIColor(white: 0, alpha: 0.15)
view.snp_makeConstraints(closure: { (make) -> Void in
make.top.equalTo(padding)
make.bottom.lessThanOrEqualTo(-padding)
if let leftHandView = leftHandView {
make.left.equalTo(leftHandView.snp_right).offset(padding)
make.width.equalTo(leftHandView)
} else {
make.left.equalTo(padding)
}
leftHandView = view
})
}
if let lastView = views.last {
lastView.snp_makeConstraints(closure: { (make) -> Void in
make.right.equalTo(-padding)
})
}
}
}
This produces the following results:
For those who keep getting here looking for a solution for this problem. I found that the best way (in my case) would be to use a parent UIView as background and padding, like this:
In this case the UIStackView is contrained to the edges of the UIView with a padding and separate the subviews with spacing.
I've built a growing UITextView attached to the keyboard, similar to the stock Messages app, by loading a .xib into the keyboard's inputAccessoryView as such:
self.keyboardAccessoryView = [[[NSBundle mainBundle]
loadNibNamed:#"KeyboardAccessoryView"
owner:self options:nil]
firstObject];
The .xib looks like this, and is using layout constraints so that the textView grows vertically when the user enters more lines of text:
This is all working great, with rotation and everything, except one big bug -- when the text is multiple lines, only the very bottom line handles touch events. This means that a user cannot scroll inside the UITextView because their touch events are being passed to the (dark gray) view in the back and scrolling that instead. They also cannot select and edit their text on the top 3 lines.
I think I could do a workaround by capturing the coordinates of all tap events and checking if the keyboard is open and how tall the UITextView is, then selecting the correct element to receive the touch event. But this is brittle solution that is more complicated with rotation. Is there something I'm missing in my auto-growing text view approach, or some easier fix?
To make input accessory view grow vertically you just set its autoresizingMask = .flexibleHeight, calculate its intrinsicContentSize and let the framework do the rest.
The code:
class InputAccessoryView: UIView, UITextViewDelegate {
let textView = UITextView()
override init(frame: CGRect) {
super.init(frame: frame)
// This is required to make the view grow vertically
self.autoresizingMask = UIViewAutoresizing.flexibleHeight
// Setup textView as needed
self.addSubview(self.textView)
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[textView]|", options: [], metrics: nil, views: ["textView": self.textView]))
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[textView]|", options: [], metrics: nil, views: ["textView": self.textView]))
self.textView.delegate = self
// Disabling textView scrolling prevents some undesired effects,
// like incorrect contentOffset when adding new line,
// and makes the textView behave similar to Apple's Messages app
self.textView.scrollEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
// Calculate intrinsicContentSize that will fit all the text
let textSize = self.textView.sizeThatFits(CGSize(width: self.textView.bounds.width, height: CGFloat.max))
return CGSize(width: self.bounds.width, height: textSize.height)
}
// MARK: UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
// Re-calculate intrinsicContentSize when text changes
self.invalidateIntrinsicContentSize()
}
}
This approach is quite straightforward and reliable, as it doesn't require hacking constraints or recreating the view each time its size changes.
I figured out that even though the keyboard accessory input grows vertically with auto layout, its frame does not. So you have to adjust the keyboard accessory's frame each time the height of the uitextview grows, shrinks, and rotates. This introduces some complications as UITextView's in iOS7 are notoriously buggy -- I noticed behavior was not consistent across iPhone, iPad, and the Simulator.