Center UIButton to Superview class programmatically (Swift 3) - ios

I'm trying to center button in the view within a UIStackView subclass.
// ButtonsController.swift
// player
#IBDesignable class ButtonsController: UIStackView {
override init(frame: CGRect) {
super.init(frame: frame)
setupControls()
}
required init(coder: NSCoder) {
super.init(coder: coder)
setupControls()
}
private func setupControls(){
//Buttons variables
let playButton = UIButton()
playButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
addArrangedSubview(playButton)
}
In the Storyboard I've create Horizontal Stack View and set it to custom class ButtonsController. I want Play button to be centered within that view, that's why I use "self.centerXAnchor" as reference to Superview.
But simulator crash with an error:
Terminating app due to uncaught exception 'NSGenericException',
reason: 'Unable to activate constraint with anchors
(NSLayoutXAxisAnchor:0x60800026c680 "UIButton:0x7feba5d112f0.centerX")
and (NSLayoutXAxisAnchor:0x60800026c700
"player.ButtonsController:0x7feba5c0e390.centerX") because they have
no common ancestor. Does the constraint or its anchors reference
items in different view hierarchies? That's illegal.'
How I actually can reference to Superview within that class?

First problem is, you are trying to add a constraint (which references other views) before adding your view as subview. You should add it as subview first, then create constraints. That will get rid of the crash.
addArrangedSubview(playButton)
playButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
Also, I don't know what you are trying to achieve here, why you are using a UIStackView as container. Bu that constraint will probably cause trouble for you. You are adding your UIButton as "arranged subview", which means its constraints will be created automatically. You can activate some constraints like width if you configured your UIStackView accordingly (its distribution propery etc.). But centerX is problematic on a horizontal stack view. You can try centering your stack view inside its superview or instead of adding your uibutton as arrangedsubview, adding it as subview:
addSubview(playButton)

Related

IOS Stackview autolayout issue with dynamic content

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.

addArrangedSubview vs addSubview

I'm newbie and I've wrote a customView inherited from StackView and I created a button programmatically with few attributes and when I add it to my custom view, I have two problems:
If I use addArrangedSubview(myBtn), my view ignores attributes that
I added and fills the whole width. But if I use addSubView(myBtn),
It's ok(a blue square in 44x44)
If I use addArrangedSubview(myBtn), addTarget() not works and
myBtn is not clickable, but when I use addSubView(myBtn), It works
perfectly.
Here is my custom view class:
import UIKit
class RatingControl: UIStackView {
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupButtons()
}
required init(coder: NSCoder) {
super.init(coder:coder)
setupButtons()
}
//MARK: Private Methods
private func setupButtons() {
// Create the button
let button = UIButton()
button.backgroundColor = UIColor.blue
// Add constraints
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
button.widthAnchor.constraint(equalToConstant: 44.0).isActive = true
// Setup the button action
button.addTarget(self, action: #selector(ratingButtonTapped(_:)), for: .touchUpInside)
// Add the button to the stack
addArrangedSubview(button)
}
//MARK: Button Action
#objc func ratingButtonTapped(_ sender: Any) {
print("Button pressed 👍")
}
}
Here is the preview:
What's difference between addSubView() and addArrangedSubview()? why these problems happens?
I'm assuming you want to add several buttons horizontally (such as with a typical "stars" rating control).
A UIStackView, as might be guessed from its .addArrangedSubview() method, arranges its subviews, based on its .axis, .alignment, .distribution and .spacing properties, as well as its frame.
So, do some reading about UIStackView and how it can be used. Most likely, you are currently constraining your custom view with:
top
leading
trailing (or width)
So adding your button as an arrangedSubView results in it stretching to the width of the stack view, because that's the default.
Adding it as a subview simply overlays the button on the stack view, instead of allowing the stack view to arrange it, and your stack view likely then has a height of zero -- so the button cannot be tapped.
Try setting only top and leading constraints when you add your custom stack view. That should give you a 44 x 44 button that can be tapped.
As you add more buttons using .addArrangedSubview(), those buttons will be arranged horizontally, which is probably what you want.

How to make autolayout work with custom UIView on XIB?

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.

Swift - After loading subview from xib to UIScrollView, scroll is not working and subview is going out of bounds

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.

Custom View - self.frame is not correct?

So I have a custom UIView class
class MessageBox: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
createSubViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createSubViews()
}
func createSubViews() {
let testView = UIView(frame: self.frame)
testView.backgroundColor = UIColor.brown
self.addSubview(testView)
}
}
I added a UIView inside the storyboard and gave it some constraints:
100 from the top (superview), 0 from the left and right, height is 180
But when I run the app the brown subview I created in the code is way to big. I printed self.frame in my custom view and it turns out that the frame is (0,0,1000,1000). But why? I set constraints, it should be something like (0,0,deviceWith, 180).
What did I do wrong?
EDIT: That's my Storyboard setup:
Short and simple answer:
You're doing it too early.
Detailed answer:
When a view is initialized from an Interface Builder file (a xib or a storyboard) its frame is initially set to the frame it has in Interface Builder. You can look at it as a temporary placeholder.
When using Auto Layout the constraints are resolved (= the view's actual frame is computed) inside the view's layoutSubviews() method.
Thus, there are two possible solutions for your problem:
(preferrable) If you use Auto Layout, use it throughout your view.
Either add your testView in Interface Builder as well and create an outlet for it
or create your testView in code as you do, then set its translatesAutoResizingMaskIntoConstraints property to false (to sort of "activate Auto Layout") and add the required constraints for it in code.
Set your testView's frame after the MessageBox view's frame itself has been set by the layout engine. The only place where you can be sure that the system has resolved the view's frame from the constraints is when layoutSubviews() is called.
override func layoutSubviews() {
super.layoutSubviews()
testView.frame = self.frame
}
(You need to declare your testView as a property / global variable, of course.)
Try to use the anchors for your view:
MessageBox.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor).active
= true
MessageBox.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor).active
= true
MessageBox.widthAnchor.constraintEqualToConstant(150).active = true
MessageBox.heightAnchor.constraintEqualToConstant(100).active = true
This method have to be used inside your class
override func layoutSubviews() {
super.layoutSubviews()
testView.frame = self.frame
}
this also works when you add a custom class to a UIView in the storyboard and that uses autolayout.
thanks Mischa !
try to add a height and width constraint relative to the superview height, with some multiplier.

Resources