addArrangedSubview vs addSubview - ios

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.

Related

How to disable the title of UIButton shifiting to right when an image is added to the button?

I have created a custom UIButton class to show the image of the button at the right edge of the screen.
The code is as follows -
class CutomButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
imageView?.translatesAutoresizingMaskIntoConstraints = false
imageView?.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8).isActive = true
imageView?.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
}
The above code works as expected. However, the title of the button moves to the right as if the imageView was in its original position (to the left of the title).
Can anyone point out how the title of the button can be placed to its original position as if the image of the button is not present?
By default the UIButton class manages this internally but you do have a few options:
Easy Mode: Modify the edge insets defined in the UIButton: titleEdgeInsets and imageEdgeInsets. Look at a different SO post for a previous answer that is similar enough but you will just have to modify it for your needs.
Advanced (but more flexible) Mode: Since you are creating a subclass, you could instead subclass UIControl instead of UIButton. Since UIButton IS a subclass of UIControl, touch events are still registered in the same manner as a UIButton and you can override the isHighlighed/isSelected to control the way the "custom button" looks in the touched state.

ios - switch within navigation item bar

I want to set a UISwitch within a UINavigationBar. But when I try place my finger on the switch and drag it to "switch" on and off the view is not responding.
This is what i have.
https://github.com/rchampa/views-within-navigationItem
As already stated in the comments above your GitHub project does not contain any data. Nevertheless everything works as expected (and seems cleaner to me) if you set the custom UIBarButtonItem up programmatically:
override func viewDidLoad() {
super.viewDidLoad()
setupBarButtonItem()
}
private func setupBarButtonItem() {
let offLabel = UILabel()
offLabel.font = UIFont.boldSystemFont(ofSize: UIFont.smallSystemFontSize)
offLabel.text = "OFF"
let onLabel = UILabel()
onLabel.font = UIFont.boldSystemFont(ofSize: UIFont.smallSystemFontSize)
onLabel.text = "ON"
let toggle = UISwitch()
toggle.addTarget(self, action: #selector(toggleValueChanged(_:)), for: .valueChanged)
let stackView = UIStackView(arrangedSubviews: [offLabel, toggle, onLabel])
stackView.spacing = 8
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: stackView)
}
#objc func toggleValueChanged(_ toggle: UISwitch) {
print("new value: \(toggle.isOn)")
}
Update:
I made it work via storyboard too. In contrast to setting it up programmatically you have to embed the UIStackView into a regular UIView to be able to add it as a UIBarButtonItem in storyboard. Then I added top, leading, bottom and trailing constraints (each with a constant of 0) from the UIStackView to its superview. To get rid of the storyboard warnings and errors at design time (at runtime it works without any problems) you have to manually calculate and set the width for the outer view (which contains the UIStackView) that is needed to enclose all of it subviews (offLabel.width + spacing + toggle.width + spacing + onLabel.width).

sequence of auto layout methods get called in rendering cycle

I was reading about auto layout rendering pipelines i mean how auto layout work under the hood. There are some methods which get called at different stages of autoLayout rendering like
layoutIfNeeded()
layoutSubviews()
updateConstraints()
updateConstraintsIfNeeded()
but i don't know which method is called when and what is the significance of that method and if i want to use auto layout then in which order i can use that methods and how can i control the autoLayout rendering pipeline
Usually you don't need to care about the autolayout method chain. You just need to create the constraints for the views to define their sizes and positions. You can add/remove, activate/deactivate constraints anytime in lifecycle of the view, but you want to always have a set of satisfiable (non-conflicting), yet complete set of constraints.
Take an example. You can tell the autolayout that button A should be 50 points wide, 20 points high, with its left top corner positioned at point (0,0) in the viewController's view. Now, this is non-conflicting, yet complete set of constraints for the button A. But lets say you want to expand that button, when the user taps it. So in the tap handler you will add one new constraint saying that the button should be 100 points wide - now you have unsatisfiable constraints - there is a constraint say it should be 50 points wide, and another one saying it shoul be 100 points wide. Therefore, to prevent conflict, before activating the new constraint, you have to deactivate the old one. Incomplete constraints is an opposite case, lets say you deactivate the old width constraint, but never activate the new one. Then autolayout can calculate position (because there are constraints defining it), and height, but not width, which usually ends in undefined behavior (now in case of a UIButton that's not true, because it has intrinsic size, which implicitly defines its width and height, but I hope you get the point).
So when you create those constraints is up to you (in my example you were manipulating them when the user tapped the button). Usually you start in initializer in case of a UIView subclass or in loadView in UIViewController subclass and there you can define and activate the default set of constraints. And then you can use handlers to react to user activity. My recommendation is prepare all the constraints in loadView, keep them in properties, and activate/deactivate them when necessary.
But there are of course some limitation as when and how not to create new constraints - for a more detailed discussion of those cases I really recommend looking at Advanced Autolayout Toolbox by objc.io.
EDIT
See following example of a simple custom SongView that uses autolayout for layout and supports also some dynamic changes in constraints by activating/deactivating them. You can just simply copy paste the whole code into a playground and test it out there, or include it in a project.
Notice there that I don't call any of the autolayout lifecycle methods, except of setNeedsLayout and layoutIfNeeded. setNeedsLayout sets a flag telling the autolayout that constraints have been changed, and layoutIfNeeded then tells it to recalculate frames. Normally, that would happen automatically, but to animate the constraints changes we need to tell it explicitly - see the setExpanded method in SongView. For more detailed explanation of using autolayout in animations, see my different answer.
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let songView = SongView()
let button = UIButton()
override func loadView() {
super.loadView()
view.backgroundColor = .white
self.view.addSubview(button)
self.view.addSubview(songView)
button.setTitle("Expand/Collapse", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(expandCollapse), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
songView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// button has intrinsic size, no need to define constraints for size, position is enough
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50),
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
// songView has defined its height (see SongView class), but not width, therefore we need more constraints
songView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
songView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
songView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
])
}
#objc func expandCollapse() {
if songView.isExpanded {
songView.setExpanded(to: false, animated: true)
} else {
songView.setExpanded(to: true, animated: true)
}
}
}
class SongView: UIView {
private let numberLabel: UILabel = UILabel()
private let nameLabel: UILabel = UILabel()
private var expandedConstraints: [NSLayoutConstraint] = []
private var collapsedConstraints: [NSLayoutConstraint] = []
// this can be triggered by some event
private(set) var isExpanded: Bool = false
func setExpanded(to expanded: Bool, animated: Bool) {
self.isExpanded = expanded
if animated {
if expanded {
// setup expanded state
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
} else {
// setup collapsed
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
}
self.setNeedsLayout()
UIView.animate(withDuration: 0.2, animations: {
self.layoutIfNeeded()
})
} else {
// non animated version (no need to explicitly call setNeedsLayout nor layoutIfNeeded)
if expanded {
// setup expanded state
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
} else {
// setup collapsed
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
}
}
}
var data: (String, String)? {
didSet {
numberLabel.text = data?.0
nameLabel.text = data?.1
}
}
init() {
super.init(frame: CGRect.zero)
setupInitialHierarchy()
setupInitialAttributes()
setupInitialLayout()
}
fileprivate func setupInitialHierarchy() {
self.addSubview(numberLabel)
self.addSubview(nameLabel)
}
fileprivate func setupInitialAttributes() {
numberLabel.font = UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: UIFontTextStyle.body).pointSize)
numberLabel.textColor = UIColor.darkGray
numberLabel.text = "0"
numberLabel.textAlignment = .right
nameLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
nameLabel.text = "NONE"
nameLabel.textAlignment = .left
self.backgroundColor = UIColor.lightGray
}
fileprivate func setupInitialLayout() {
self.translatesAutoresizingMaskIntoConstraints = false
numberLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.translatesAutoresizingMaskIntoConstraints = false
// just randomly selected different layouts for collapsed and expanded states
expandedConstraints = [
numberLabel.widthAnchor.constraint(equalToConstant: 35),
self.heightAnchor.constraint(equalToConstant: 80),
]
collapsedConstraints = [
numberLabel.widthAnchor.constraint(equalToConstant: 50),
self.heightAnchor.constraint(equalToConstant: 40),
]
// activating collapsed as default layout
NSLayoutConstraint.activate(collapsedConstraints)
NSLayoutConstraint.activate([
numberLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 4),
numberLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -4),
numberLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 4),
nameLabel.centerYAnchor.constraint(equalTo: numberLabel.centerYAnchor),
nameLabel.leftAnchor.constraint(equalTo: numberLabel.rightAnchor, constant: 8),
nameLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -4)
])
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
PlaygroundPage.current.liveView = ViewController()

Center UIButton to Superview class programmatically (Swift 3)

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)

Add button on top of UITableViewController (Swift)

I am trying to add a button ontop of a uitableview controller table view. The view controller has a navigation controller and static cells, which is why it is a uitableviewcontroller and not a uiviewcontroller. Now I am trying to add a button at the bottom of the screen that is attached to the navigation controller so that it doesn't scroll with the table view.
I am trying to make something similar to what is below. It has a navigation controller for the top bar, a table view with static cells and then a button, but how did they do the button?
Image: http://postimg.org/image/ilsmqqrip/
Thanks!
UPDATE: How can I use a uiviewcontroller with a tableview with static cells using Swift?
I find Container Views very useful in this scenario! A clean solution and very easy to implement.
Just create a normal UIViewController, add your button and a ContainerView as subviews of this UIViewController (the middle one in the image below). Finally create Embed Segue from ContainerView to your UITableViewController (the one on the right).
This way you can use static cell prototypes, not being limited only to UITableView at the same time.
Result:
there is a better solution for this. you can do this by disabling the Auto Layout(button.translatesAutoresizingMaskIntoConstraints = false) property of the corresponding Button or any UIView for floating button:
Swift 4
//create a button or any UIView and add to subview
let button=UIButton.init(type: .system)
button.setTitle("NEXT", for: .normal)
button.frame.size = CGSize(width: 100, height: 50)
self.view.addSubview(button)
//set constrains
button.translatesAutoresizingMaskIntoConstraints = false
if #available(iOS 11.0, *) {
button.rightAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true
button.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
} else {
button.rightAnchor.constraint(equalTo: tableView.layoutMarginsGuide.rightAnchor, constant: 0).isActive = true
button.bottomAnchor.constraint(equalTo: tableView.layoutMarginsGuide.bottomAnchor, constant: -10).isActive = true
}
I did something similar with UITableViewController and a static datasource. I added the button in the footerview of my tableview.
To make it align to the bottom of the screen i needed this code in my viewcontroller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Make footerview so it fill up size of the screen
// The button is aligned to bottom of the footerview
// using autolayout constraints
self.tableView.tableFooterView = nil
self.footerView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.tableView.frame.size.height - self.tableView.contentSize.height - self.footerView.frame.size.height)
self.tableView.tableFooterView = self.footerView
}
In short, I resize the footerview to take up all the remaining space after the contentsize of the table view is removed. Since the button is aligned to the bottom of the footerView with autolayout, it will stay in the bottom of the screen.
The Storyboard:
Here is the result:
The UITableViewController will take up the whole space, so you won't be able to add the button. Refactor your UITableViewController based code into UIViewController with UITableView manually added. This way you will be able to set the size of your table view and put the button to the bottom.
Unfortunately UITableViewController has a tableView as its top level view. Of course if you look in the view debugger you can see that the tableview is not the root view. Therefore you can add the buttons to the tableView's window programatically. If you have to do it after the fact, this is probably the easiest way to add a top level element over a UITableViewController. Otherwise if you are doing it in the initial design, you can use container view for your buttons and a UITableViewController for the TableView. The downside of this approach is you end up with two view controllers, one for the container and one for the table and its often necessary to pass information back and for between them. If you are using swift you can simplify this by nesting the tableViewcontroller inside the container view controller class.
If you want to add a button to the window, you can do this lazily once you are sure that the view has a window. Note that the buttons belong to the window and not to the view controller, so its your responsibility to remove them when the view controller disappears.
private weak var button: UIButton!
...
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
guard self.button == nil, let window = tableView.window else {
return
}
let button = UIButton(frame: CGRect(x:0, y:40, width: 200, height: 20))
button.setTitle("This is a red button", for: .normal)
button.backgroundColor = UIColor.red
window.addSubview(button)
self.button = button
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
button?.removeFromSuperview()
}
Step 1 :-
Drag and drop one uiview to UITable View Controller (Static)
Automatically it sticks to the bottom.
If you need to, you can also add two buttons inside UIView... It depends on your requirements.
Step 2 :-
Connect the outlet for uiview (outletView)
Step 3 :-
Add this below code in View Will Appear.
override func viewWillAppear(_ animated: Bool) {
outletViewBottom.backgroundColor = .red
tableView.addSubview(outletViewBottom)
// set position
outletView.translatesAutoresizingMaskIntoConstraints = false
outletView.leftAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.leftAnchor).isActive = true
outletView.rightAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.rightAnchor).isActive = true
outletView.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor).isActive = true
outletView.widthAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.widthAnchor).isActive = true
outletView.heightAnchor.constraint(equalToConstant: 50).isActive = true // specify the height of the view
}
Step 4 :-
Now run the code... Happy coding.
all you need to do is to add your Top view whichever it is to the navigationController.view like so:
self.navigationController?.view.addSubview(YOUR_TOP_VIEW)
so if you need a sticky button/view etc... on top of TableViewController which does not scroll with tableView, use this approach.
Here is a UIViewController, with a UITableView added as a subview. At the top right, you can see a dropdown that says Content: Dynamic Prototypes. Change it to Static Cells.

Resources