Using custom UIView class with Auto Layout returns incorrect bounds - ios

I'm presenting a simple view controller. To follow MVC, I moved my programmatic view code into a separate UIView subclass. I followed the advice here of how to set up the custom view.
This works ok in iPhone. But, on iPad, the view controller is automatically presented with the new post-iOS 13 default modal style .pageSheet, which causes one of my buttons to be too wide. I looked into the view debugger and it's because the width constraint is set to self.bounds.width * 0.7, and self.bounds returns the full width of the iPad (1024 points) at the time the constraint is set, not the actual final view (which is only 704 points wide).
iPhone simulator screenshot
iPad 12.9" simulator screenshot
Three questions:
In general, am I setting up the custom view + view controller correctly? Or is there a best practice I'm not following?
How do I force an update to the constraints so that the view recognizes it's being presented in a .pageSheet modal mode on iPad, and automatically update the width of self.bounds? I tried self.view.setNeedsLayout() and self.view.layoutIfNeeded() but it didn't do anything.
Why is it that the Snooze button is laid out correctly on iPad, but not the Turn Off Alarm button... the Snooze button relies on constraints pinned to self.leadingAnchor and self.trailingAnchor. Why do those constraints correctly recognize the modal view they're being presented in, but self.bounds.width doesn't?
Code:
Xcode project posted here
View Controller using a custom view:
class CustomViewController: UIViewController {
var customView: CustomView {
return self.view as! CustomView
}
override func loadView() {
let customView = CustomView(frame: UIScreen.main.bounds)
self.view = customView // Set view to our custom view
}
override func viewDidLoad() {
super.viewDidLoad()
print("View's upon viewDidLoad is: \(self.view.bounds)") // <-- This gives incorrect bounds
// Add actions for buttons
customView.snoozeButton.addTarget(self, action: #selector(snoozeButtonPressed), for: .touchUpInside)
customView.dismissButton.addTarget(self, action: #selector(dismissButtonPressed), for: .touchUpInside)
}
override func viewDidAppear(_ animated: Bool) {
print("View's bounds upon viewDidAppear is: \(self.view.bounds)") // <-- This gives correct bounds
// This doesn't work
// //self.view.setNeedsLayout()
// // self.view.layoutIfNeeded()
}
#objc func snoozeButtonPressed() {
// Do something here
}
#objc func dismissButtonPressed() {
self.dismiss(animated: true, completion: nil)
}
}
Custom view code:
class CustomView: UIView {
public var snoozeButton: UIButton = {
let button = UIButton(type: .system)
button.isUserInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Snooze", for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.white.cgColor
button.layer.cornerRadius = 14
return button
}()
public var dismissButton: UIButton = {
let button = UIButton(type: .system)
button.isUserInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Turn off alarm", for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.backgroundColor = UIColor.clear
button.layer.cornerRadius = 14
button.layer.borderWidth = 2
button.layer.borderColor = UIColor.white.cgColor
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
self.backgroundColor = .systemPurple
// Add subviews
self.addSubview(snoozeButton)
self.addSubview(dismissButton)
}
func setupConstraints() {
print("Self.bounds when setting up custom view constraints is: \(self.bounds)") // <-- This gives incorrect bounds
NSLayoutConstraint.activate([
dismissButton.heightAnchor.constraint(equalToConstant: 60),
dismissButton.widthAnchor.constraint(equalToConstant: self.bounds.width * 0.7), // <-- This is the constraint that's wrong
dismissButton.centerXAnchor.constraint(equalTo: self.centerXAnchor),
dismissButton.centerYAnchor.constraint(equalTo: self.centerYAnchor),
snoozeButton.heightAnchor.constraint(equalToConstant: 60),
snoozeButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20),
snoozeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
snoozeButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -40)
])
}
}
.. and finally, the underlying view controller doing the presenting:
class BlankViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
let customVC = CustomViewController()
self.present(customVC, animated: true, completion: nil)
}
}
Thanks.

There's no need to reference bounds -- just use a relative width constraint.
Change this line:
dismissButton.widthAnchor.constraint(equalToConstant: self.bounds.width * 0.7), // <-- This gives incorrect bounds
to this:
dismissButton.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.7),
Now, the button will be 70% of the view's width. As an added benefit, it will adjust itself if/when the view width changes, instead of being stuck at a calculated Constant value.

Related

inputAccessoryView, API error? _UIKBCompatInputView? UIViewNoIntrinsicMetric, simple code, can't figure out

Help me in one of the two ways maybe:
How to solve the problem? or
How to understand the error message?
Project summary
So I'm learning about inputAccessoryView by making a tiny project, which has only one UIButton. Tapping the button summons the keyboard with inputAccessoryView which contains 1 UITextField and 1 UIButton. The UITextField in the inputAccessoryView will be the final firstResponder that is responsible for the keyboard with that inputAccessoryView
The error message
API error: <_UIKBCompatInputView: 0x7fcefb418290; frame = (0 0; 0 0); layer = <CALayer: 0x60000295a5e0>> returned 0 width, assuming UIViewNoIntrinsicMetric
The code
is very straightforward as below
The custom UIView is used as inputAccessoryView. It installs 2 UI outlets, and tell responder chain that it canBecomeFirstResponder.
class CustomTextFieldView: UIView {
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
let textField:UITextField = {
let textField = UITextField()
textField.placeholder = "placeholder"
return textField
}()
required init?(coder: NSCoder) {
super.init(coder: coder)
initSetup()
}
override init(frame:CGRect) {
super.init(frame: frame)
initSetup()
}
convenience init() {
self.init(frame: .zero)
}
func initSetup() {
addSubview(doneButton)
addSubview(textField)
}
func autosizing(to vc: UIViewController) {
frame = CGRect(x: 0, y: 0, width: vc.view.frame.size.width, height: 40)
let totalWidth = frame.size.width - 40
doneButton.frame = CGRect(x: totalWidth * 4 / 5 + 20,
y: 0,
width: totalWidth / 5,
height: frame.size.height)
textField.frame = CGRect(x: 20,
y: 0,
width: totalWidth * 4 / 5,
height: frame.size.height)
}
override var canBecomeFirstResponder: Bool { true }
override var intrinsicContentSize: CGSize {
CGSize(width: 400, height: 40)
} // overriding this variable seems to have no effect.
}
Main VC uses the custom UIView as inputAccessoryView. The UITextField in the inputAccessoryView becomes the real firstResponder in the end, I believe.
class ViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
customView.autosizing(to: self)
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}
I've seen people on the internet says this error message will go away if I run on a physical phone. I didn't go away when I tried.
I override intrinsicContentSize of the custom view, but it has no effect.
The error message shows twice together when I tap summon.
What "frame" or "layer" does the error message refer to? Does it refer to the custom view's frame and layer?
If we use Debug View Hierarchy we can see that _UIKBCompatInputView is part of the (internal) view hierarchy of the keyboard.
It's not unusual to see constraint errors / warnings with internal views.
Since frame and/or intrinsic content size seem to have no effect, I don't think it can be avoided (nor does it seem to need to be).
As a side note, you can keep the "Done" button round by using auto-layout constraints. Here's an example:
class CustomTextFieldView: UIView {
let textField: UITextField = {
let tf = UITextField()
tf.font = .systemFont(ofSize: 16)
tf.autocorrectionType = .no
tf.returnKeyType = .done
tf.placeholder = "placeholder"
// textField backgroundColor so we can see its frame
tf.backgroundColor = .yellow
return tf
}()
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
autoresizingMask = [.flexibleHeight, .flexibleWidth]
[doneButton, textField].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
NSLayoutConstraint.activate([
// constrain doneButton
// Trailing: 20-pts from trailing
doneButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
// Top and Bottom 8-pts from top and bottom
doneButton.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
doneButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
// Width equal to default height
// this will keep the button round instead of oval
doneButton.widthAnchor.constraint(equalTo: doneButton.heightAnchor),
// constrain textField
// Leading: 20-pts from leading
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
// Trailing: 8-pts from doneButton leading
textField.trailingAnchor.constraint(equalTo: doneButton.leadingAnchor, constant: -8.0),
// vertically centered
textField.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class CustomTextFieldViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}

UIViewController with custom UIView (that includes a button) doesn't recognize taps

When trying to load a custom UIView (BaseHeader.swift) that contains a UIButton into a UIViewController (ViewController.swift) programmatically, the taps on the button are not recognized.
If I extract the code within the custom UIView and paste it directly into the UIViewController, all taps are recognized and working as expected. What am I missing?
At first, I thought it was a problem with not defining a frame size for the UIView after it gets instantiated. I thought that there might be no "hit box" for the button despite it being displayed as intended. After giving the view a frame I still had no luck and tried various other things after googling about for awhile.
The view loads but button taps are not recognized -- see image
ViewController.swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = "Authentication"
}
override func loadView() {
super.loadView()
let header = BaseHeader()
header.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(header)
NSLayoutConstraint.activate([
header.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
header.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
header.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: -10),
])
}
}
BaseHeader.swift
import UIKit
class BaseHeader: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func setupView() {
self.isUserInteractionEnabled = true
let avenirFont = UIFont(name: "Avenir", size: 40)
let lightAvenirTitle = avenirFont?.light
let title = UILabel()
title.text = "Title"
title.font = lightAvenirTitle
title.textColor = .black
title.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(title)
NSLayoutConstraint.activate([
title.topAnchor.constraint(equalTo: self.topAnchor, constant: 20),
title.centerXAnchor.constraint(equalTo: self.centerXAnchor)
])
let profile = UIButton()
let profileImage = UIImage(named: "person-icon")
profile.setImage(profileImage, for: .normal)
profile.setImage(UIImage(named: "think-icon"), for: .highlighted)
profile.addTarget(self, action: #selector(profileTapped), for: .touchUpInside)
profile.backgroundColor = .systemBlue
profile.frame.size = CGSize(width: 30, height: 30)
profile.isUserInteractionEnabled = true
profile.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(profile)
NSLayoutConstraint.activate([
profile.centerYAnchor.constraint(equalTo: title.centerYAnchor),
profile.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
])
}
#objc func profileTapped(sender: UIButton) {
print("Tapped")
}
}
You forgot set height for BaseHeader
NSLayoutConstraint.activate([
header.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
header.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
header.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: -10),
header.heightAnchor.constraint(equalToConstant: 40) //add more
])
It might be that you are calling the wrong initializer for your custom view and that your setupView() function is not being called. It looks like in the view controller you are calling
let header = BaseHeader()
To initialize the view, but it's this init:
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
That calls your setupView() function and sets up the tap.

Custom BarButtonItem Item: Add red dot on top right corner, Swift 4 IOS

I want to add/set unread flag as red dot top right corner on UIbarButtonItem, See attached image for this
What should i do to add/set red dot on Bar Button item?
Once user tap on item then i want to remove red dot.
UIButton Subclass
Okay, let's start with creating custom UIButton subclass
class ButtonWithBadge: UIButton {
}
now let's create UIView for repsenting red dot
let badgeView: UIView = {
let view = UIView()
view.layer.cornerRadius = 3
view.backgroundColor = .red
return view
}()
Then override init for this subclass and inside add this badgeView to top right corner of your button: set its constraints (right and top equal to button's anchors and width and height to double of badgeView's cornerRadius value)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(badgeView)
badgeView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
badgeView.rightAnchor.constraint(equalTo: rightAnchor, constant: 3),
badgeView.topAnchor.constraint(equalTo: topAnchor, constant: 3),
badgeView.heightAnchor.constraint(equalToConstant: badgeView.layer.cornerRadius*2),
badgeView.widthAnchor.constraint(equalToConstant: badgeView.layer.cornerRadius*2)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Next create variable represeting current state of button:
var isRead: Bool = false
Now let's create some method which hide or unhide badgeView depending on isRead value
func setBadge() {
badgeView.isHidden = isRead
}
Now we have function, right? So let's call this function at the end of init and in didSet of isRead variable
class ButtonWithProperty: UIButton {
var isRead: Bool = false {
didSet {
setBadge()
}
}
override init(frame: CGRect) {
...
setBadge()
}
Adding To ViewController
First create variables for button and view
lazy var barButton: ButtonWithProperty = {
let button = ButtonWithProperty()
... // set color, title, target, etc.
return button
}()
now for example in viewDidLoad add this barButton to UINavigationBar and position it how you want to:
override func viewDidLoad() {
super.viewDidLoad()
...
guard let navigationBar = self.navigationController?.navigationBar else { return }
navigationBar.addSubview(barButton)
barButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
barButton.rightAnchor.constraint(equalTo: navigationBar.rightAnchor, constant: -20),
barButton.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: -6)
])
}
Now when you need to, you can just easily change barButton's isRead variable and red dot disappears or appears
barButton.isRead = true
class ButtonWithProperty: UIButton {
var isRead: Bool = false {
didSet {
setBadge()
}
}
lazy var badgeView: UIView = {
let view = UIView()
view.layer.cornerRadius = 3
view.backgroundColor = .red
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(badgeView)
badgeView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
badgeView.rightAnchor.constraint(equalTo: rightAnchor, constant: 3),
badgeView.topAnchor.constraint(equalTo: topAnchor, constant: 3),
badgeView.heightAnchor.constraint(equalToConstant: badgeView.layer.cornerRadius*2),
badgeView.widthAnchor.constraint(equalToConstant: badgeView.layer.cornerRadius*2)
])
setBadge()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setBadge() {
badgeView.isHidden = isRead
}
}
Inside ViewController:
class ViewController: UIViewController {
lazy var barButton: ButtonWithProperty = {
let button = ButtonWithProperty()
... // color, title, target, etc.
return button
}()
...
override func viewDidLoad() {
super.viewDidLoad()
...
guard let navigationBar = self.navigationController?.navigationBar else { return }
navigationBar.addSubview(barButton)
barButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
barButton.rightAnchor.constraint(equalTo: navigationBar.rightAnchor, constant: -20),
barButton.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: -6)
])
}
...
}
I've written a custom bar button class to handle this, where I'm using CAShapeLayer to draw a dot on top of the UIBarButtonItem.
// Custom Bar button
class CustomBarButton: UIBarButtonItem
{
// Unread Mark
private var unreadMark: CAShapeLayer?
// Keep track of unread status
var hasUnread: Bool = false
{
didSet
{
setUnread(hasUnread: hasUnread)
}
}
// Toggles unread status
private func setUnread(hasUnread: Bool)
{
if hasUnread
{
unreadMark = CAShapeLayer();
unreadMark?.path = UIBezierPath(ovalIn: CGRect(x: (self.customView?.frame.width ?? 0) - 10, y: 5, width: 5, height: 5)).cgPath;
unreadMark?.fillColor = UIColor.red.cgColor
self.customView?.layer.addSublayer(unreadMark!)
}
else
{
unreadMark?.removeFromSuperlayer()
}
}
}
There is no layer property available for bar button item, so you need to create your UIBarButtonItem using a custom view:
// Bar button property
var barButton:CustomBarButton!
// Initialisation
button = UIButton(type: .custom)
button.frame = CGRect(x: 0, y: 0, width: 70, height: 40)
button.setTitle("Right", for: .normal)
button.setTitleColor(UIColor.green, for: .normal)
// Bar button
barButton = CustomBarButton(customView: button)
button.addTarget(self, action: #selector(toggleStatus(sender:)), for: UIControl.Event.touchUpInside)
// Flexible space (Optional)
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
toolBar.items = [flexibleSpace, barButton]
And in the IBAction you can toggle the status using the hasUnread property:
#objc func toggleStatus(sender: AnyObject)
{
barButton.hasUnread = !barButton.hasUnread
}
And it will look like:
let dotView = UIView()
let btnSize =yourBarbutton.frame.size
let dotSize = 8
dotView.backgroundColor = .red //Just change colors
dotView.layer.cornerRadius = CGFloat(dotSize/2)
dotView.layer.frame = CGRect(x: Int(btnSize.width)-dotSize/2 , y:
dotSize, width: dotSize, height: dotSize)
yourBarbutton.addSubview(dotView)

How to animate Segmented Control selection?

I have a UISegmentedControl with 3 possible selections and I have added a coloured underline bar to the first selection. I'm trying to figure out how to animate the underline so that it moves to highlight the new selection, but the underline bar is not moving.
I'm not looking to change the index with animation. I'm trying to figure out how to keep the underline bar UIView in the same segment that is selected. I've seen this implemented commonly.
Here's a screen recording of what's happening right now - https://imgur.com/a/jTQ8z1M
I added the UISegmentedControl via Storyboard and then added the underline in code with view.addSubview(underlineBar). Here's is what I have:
class ViewController: UIViewController {
#IBOutlet weak var segmentedControl: UISegmentedControl!
override func viewDidLoad() {
super.viewDidLoad()
setSegmentedControlStyle()
}
func setSegmentedControlStyle() {
segmentedControl.backgroundColor = .clear
segmentedControl.tintColor = .clear
segmentedControl.setTitleTextAttributes([NSAttributedStringKey.font : UIFont.systemFont(ofSize: 16), NSAttributedStringKey.foregroundColor: UIColor.lightGray
], for: .normal)
segmentedControl.setTitleTextAttributes([NSAttributedStringKey.font : UIFont.systemFont(ofSize: 16),
NSAttributedStringKey.foregroundColor: UIColor.blue
], for: .selected)
let underlineBar = UIView()
underlineBar.translatesAutoresizingMaskIntoConstraints = false // false since we are using auto layout constraints
underlineBar.backgroundColor = UIColor.blue
view.addSubview(underlineBar)
underlineBar.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor).isActive = true
underlineBar.heightAnchor.constraint(equalToConstant: 3).isActive = true
underlineBar.leftAnchor.constraint(equalTo: segmentedControl.leftAnchor).isActive = true
underlineBar.widthAnchor.constraint(equalTo: segmentedControl.widthAnchor, multiplier: 1 / CGFloat(segmentedControl.numberOfSegments)).isActive = true
UIView.animate(withDuration: 0.3) {
underlineBar.frame.origin.x = (self.segmentedControl.frame.width / CGFloat(self.segmentedControl.numberOfSegments)) * CGFloat(self.segmentedControl.selectedSegmentIndex)
}
}
}
The UIView.animate is being called, but nothing happens.
The problem is that you are setting constraints for underlineBar that are updating the X, Y and width, height properties so when you are trying to animate the change in position, the view is not moving because the constraints have higher priority. In conclusion you will need to animate the constraints.
I have created a global leftAnchor that you will modify every time the segment is pressed.
var underlineBarLeftAnchor: NSLayoutConstraint!
Where you setup your constraints update the function with this:
underlineBar.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor).isActive = true
underlineBar.heightAnchor.constraint(equalToConstant: 3).isActive = true
underlineBar.widthAnchor.constraint(equalTo: segmentedControl.widthAnchor, multiplier: 1 / CGFloat(segmentedControl.numberOfSegments)).isActive = true
underlineBarLeftAnchor = underlineBar.leftAnchor.constraint(equalTo: segmentedControl.leftAnchor, constant: 0)
underlineBarLeftAnchor.isActive = true
Now you will need to register the target-action methods for a segmented control using the valueChanged constant as shown below, update viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
setSegmentedControlStyle()
segmentedControl.addTarget(self, action: #selector(updateSegmentedControl), for: .valueChanged)
}
Last step, create the updateSegmentedControl() function where you will animate the movement:
This function will get called every time the value of the segment control is changed
#objc func updateSegmentedControl() {
let padding = underlineBar.frame.width * CGFloat(self.segmentedControl.selectedSegmentIndex)
underlineBarLeftAnchor.constant = padding
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
You need to calculate how much left padding you need and then update the ui, layoutIfNeeded(), while animating the change.

UIStackView doesn't fill if removing and adding the same views

I found an issue in iOS 9 where the layout is not working properly when removing and adding the same views to UIStackView and uses intrinsic content size of the views instead of the specified fillEqually distribution type.
For example, this code:
class ViewController: UIViewController {
let stackView = UIStackView()
let red = UIButton()
let blue = UIButton()
let green = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
stackView.distribution = .fillEqually
red.backgroundColor = .red
blue.backgroundColor = .blue
green.backgroundColor = .green
let views = [red, blue, green]
for view in views {
stackView.addArrangedSubview(view)
}
view.backgroundColor = .lightGray
view.addSubview(stackView)
stackView.frame = view.bounds.insetBy(dx: 0, dy: view.frame.height / 3)
let button = UIButton()
button.frame = view.bounds
view.addSubview(button)
button.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
}
func buttonClicked() {
for view in stackView.arrangedSubviews {
view.removeFromSuperview()
}
stackView.addArrangedSubview(red)
stackView.addArrangedSubview(blue)
}
}
Shows the following result after tapping the screen:
But on iOS 10 it shows the expected result:
Creating new views instead of reusing the old ones outputs the expected layout on iOS 9:
func buttonClicked() {
for view in stackView.arrangedSubviews {
view.removeFromSuperview()
}
let red = UIButton()
let blue = UIButton()
red.backgroundColor = .red
blue.backgroundColor = .blue
stackView.addArrangedSubview(red)
stackView.addArrangedSubview(blue)
}
But the whole point of this code is reusing views to improve performance.
Is this an issue in my code? A bug on iOS 9? Is there any known workaround?
for view in stackView.arrangedSubviews {
stackView.removeArrangedSubview(view)
view.removeFromSuperview()
}
I think you forget to call removeArrangedSubview.
Solved it by explicitly calling layoutIfNeeded method after removing all the views:
func buttonClicked() {
for view in stackView.arrangedSubviews {
view.removeFromSuperview()
}
stackView.layoutIfNeeded() // Required to avoid layout issues in iOS 9
stackView.addArrangedSubview(red)
stackView.addArrangedSubview(blue)
}

Resources