Loading view on UITableViewController - ios

I am attempting to show a loading view on top of a UITableViewController when a user taps on a cell or a button in the cell. For some reason, the view does not show up, nor does it show any constraint failures. Can someone spot error in my code. I need this view to show up covering the tableview on both orientations, when shown. I thought this to be a view render issue, tried the same code in viewWillAppear. Still does not work. Hence I eliminated layout rendering issues. Same code works perfectly fine on UIViewController derived classes. Seems to have issues on UITableViewController classes alone!!!
private func initActivityView() {
let overlayView = UIView(frame: CGRectZero)
overlayView.backgroundColor = UIColor.lightGrayColor()
overlayView.translatesAutoresizingMaskIntoConstraints = false
overlayView.alpha = 0.5
self.view.addSubview(overlayView)
// add constraints
let viewDictionary = ["overlayView":overlayView]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[overlayView]-0-|",
options: .AlignAllBaseline, metrics: nil, views: viewDictionary))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[overlayView]-0-|",
options: .AlignAllBaseline, metrics: nil, views: viewDictionary))
}

You should let the constraints engine know that it needs update. See the triggering Auto layout section of the UIView Class Reference. Also I would consider making your overlay view an instance variable or find some other way to keep a reference of it so that when the time comes you can remove it.
Do not call updateConstraints() but rather setNeedsUpdateContraints()
From the UIView Class Reference.
Discussion
When a property of your custom view changes in a way that
would impact constraints, you can call this method to indicate that
the constraints need to be updated at some point in the future. The
system will then call updateConstraints as part of its normal layout
pass. Updating constraints all at once just before they are needed
ensures that you don’t needlessly recalculate constraints when
multiple changes are made to your view in between layout passes.
If your having trouble yo can always set a breakpoint on the UIViewController's
viewWillLayoutSubViews() and viewDidLayoutSubViews() and inspect your subview frames.
It is also pretty helpful to keep update with Apple's documentation for Auto Layout as Auto Layout is constantly changing and improving.

Apparently doing it the autolayout way will not work, due to the inherent nature of UITableViewController. However a better solution for this problem will be something like this
let overlayView1 = UIView(frame: self.tableView.frame)
overlayView1.autoresizingMask = self.tableView.autoresizingMask
overlayView1.backgroundColor = UIColor.lightGrayColor()
overlayView1.alpha = 0.5
self.tableView.addSubview(overlayView1)

Related

Subview's bounds are zero in layoutSubviews()

The bounds of a subview of a subview of a custom UIView seem to be 0 in layoutSubviews(), hence using the bounds in layoutSubviews() is a problem.
To show the problem I put a demo on GitHub: SOBoundsAreZero
Here's a direct link to the custom view's implementation: DemoView.swift
The structure of the custom view "DemoView" is like this:
DemoView
firstLevelSubview
secondLevelSubview
This structure is being created programmatically using Auto Layout.
When layoutSubviews() is called, the view and the firstLevelSubview have the expected bounds, but the secondLevelSubview's bounds are 0.
I expected all subviews using Auto Layout to have the correct bounds, at least in the last call to layoutSubviews.
The structure is an abstraction of a real case. To avoid the problem, secondLevelSubview could be added as a first level subview to DemoView. Though, this is something, that is not feasible in the real case.
I feel like I'm missing something simple here, even if it's expected behaviour.
I was able to fix this with a call to secondLevelSubview.layoutIfNeeded() in layoutSubviews().
override func layoutSubviews() {
super.layoutSubviews()
secondLevelSubview.layoutIfNeeded()
firstLevelSubview.layer.cornerRadius = bounds.width / 2
secondLevelSubview.layer.cornerRadius = secondLevelSubview.bounds.width / 2
print("bounds.width: \(bounds.width), contentView.bounds.width: \(firstLevelSubview.bounds.width), backgroundView.bounds.width: \(secondLevelSubview.bounds.width)")
}
The description of layoutIfNeeded() is:
Use this method to force the view to update its layout immediately.
When using Auto Layout, the layout engine updates the position of
views as needed to satisfy changes in constraints. Using the view that
receives the message as the root view, this method lays out the view
subtree starting at the root. If no layout updates are pending, this
method exits without modifying the layout or calling any
layout-related callbacks.
So, essentially you have an ordering issue here. The subview is scheduled for layout, and Auto Layout will get to it, but it hasn't done it yet. By calling layoutIfNeeded(), you tell Auto Layout to perform the pending layout immediately so that you can get the updated frame information.
Note: You can also just call self.layoutIfNeeded() and that will layout DemoView and all of its subviews. This would be useful if you had many such subviews and didn't want to have to call layoutIfNeeded() on each of them.
First, Your view is being construed without problem cause you have an initializer based on ‘CGRect’ , so it gets its dimension!
Second,
Your first subview get initialized after its parents view has got its dimensions. First subview dimensions depends on the parent view dimensions which are ready. So no problem here as you mentioned.
But meanwhile your first subview is trying to get its dimensions, your second subview also starts to get its dimensions based on first subview which is not ready yet , so it got nothing.
I suggest make different functions for each subview.
Make your object from the view class with its frame initializer in Your view controller. In ‘viewDidLoad’ call first subview function and in ’viewDidLoadSubView’ call second subview function.
Hope it helps.
You can get the desired result, if you try to access UIView.bounds under main queue.
override func layoutSubviews() {
super.layoutSubviews()
DispatchQueue.main.async {
self.firstLevelSubview.layer.cornerRadius = self.bounds.width / 2
self.secondLevelSubview.layer.cornerRadius = self.secondLevelSubview.bounds.width / 2
print("bounds.width: \(self.bounds.width), contentView.bounds.width: \(self.firstLevelSubview.bounds.width), backgroundView.bounds.width: \(self.secondLevelSubview.bounds.width)")
}
}
//print:
//bounds.width: 64.0, contentView.bounds.width: 64.0, backgroundView.bounds.width: 54.0
But why? So here I dig down into your problem. Here is the calling view hierarchy of your DemoView layoutSubviews method
DemoView
|
|___layoutSubviews
|
|___firstLevelSubview
|
|___layoutSubviews //Here you are trying to access bounds of second view which is still need to be resize or achieve it's bound
|
|___ secondLevelSubview
|
|___layoutSubviews
And so in this case, main queue will help you to get the actual bounds of any UIView.subView.
Another case I did it, I try to add secondLevelSubview to the DemoView itself with the firstLevelSubview constraints like below:
fileprivate func setupViews() {
backgroundColor = UIColor.clear
firstLevelSubview.translatesAutoresizingMaskIntoConstraints = false
firstLevelSubview.layer.masksToBounds = true
firstLevelSubview.backgroundColor = UIColor.green
addSubview(firstLevelSubview)
firstLevelSubview.topAnchor.constraint(equalTo: topAnchor).isActive = true
firstLevelSubview.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
firstLevelSubview.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
firstLevelSubview.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
secondLevelSubview.translatesAutoresizingMaskIntoConstraints = false
secondLevelSubview.layer.masksToBounds = true
secondLevelSubview.backgroundColor = UIColor.magenta
// firstLevelSubview.addSubview(secondLevelSubview)
addSubview(secondLevelSubview) //Here
secondLevelSubview.widthAnchor.constraint(equalTo: firstLevelSubview.widthAnchor, multiplier: 0.84).isActive = true
secondLevelSubview.heightAnchor.constraint(equalTo: firstLevelSubview.heightAnchor, multiplier: 0.84).isActive = true
secondLevelSubview.centerXAnchor.constraint(equalTo: firstLevelSubview.centerXAnchor).isActive = true
secondLevelSubview.centerYAnchor.constraint(equalTo: firstLevelSubview.centerYAnchor).isActive = true
}
And the hierarchy it follows like below:
DemoView
|
|___layoutSubviews
|
|___firstLevelSubview
|
|___layoutSubviews
|
|___secondLevelSubview
|
|___layoutSubviews
So, in the above case if you try to print the UIView.bounds without any main queue you will get the desired result.
Let me know, this helps you to understand the hierarchy.

Are StackView Constraints behaviour different in a NIB/XIB?

I'm trying to create reusable views in a xib/nib file in swift iOS 10.
When I create a stack view and have 2 objects in it and make sure my stackview is constrained to the container view of the nib/xib (top to top, bottom to bottom, leading to leading and finally trailing to trailing), I get an error saying I'm missing my Y position for the first and the second object. Creating just one of them usually fixes it. Although this is where things to go sideways. In all my previous research, I shouldn't need to do this if I have my distribution of my stackview to Fill. Although this seems to quiet Xcode, it creates a over constraint issue when I try to run my program.
Here is what I used to load my nib in a view from my main storyboard :
public protocol NibOwnerLoadable: class {
static var nib: UINib { get }
}
//MARK:- Generic Implementation
public extension NibOwnerLoadable {
// Use the xib file with the same name as your UIView subclass located in the bundle of that class
static var nib: UINib {
return UINib(nibName: String(describing: self), bundle: Bundle(for: self))
}
}
//MARK:- Support for instation from the XIB file
public extension NibOwnerLoadable where Self: UIView {
// Function to load content and constraints automatically
func loadNibContent() {
let layoutAttributes: [NSLayoutAttribute] = [.top, .leading, .bottom, .trailing]
for view in Self.nib.instantiate(withOwner: self, options: nil) {
if let view = view as? UIView {
view.translatesAutoresizingMaskIntoConstraints = false
view.frame = bounds
self.addSubview(view)
layoutAttributes.forEach{ attribute in self.addConstraint(NSLayoutConstraint(item: view, attribute: attribute, relatedBy: .equal, toItem: self, attribute: attribute, multiplier: 1, constant: 0.0))
}
}
}
}
}
I know my issue is an extra constraint being added by Auto Layout but why and where. This other question is very close to what I'm trying to do but for some reason my AutoLayout knowledge is a jumbled in my head. Adding a subclassed UIView to a Nib with its auto layout constraints
A little help would be appreciate. Please explain the why, not just how to do it. I'm a guy who likes to understand the back story. It makes it logical and easier to retain.
Here are pictures of my filterView as a nib/xib that is loaded inside searchAndFilterView as FilterView (UIView) that is loaded inside one my view controller inside my storyboard. Think reusable Tool Views.
Woohou!
Here is how I figured this out.
I realized that my height constraints on my views using XIB/NIB are necessary to setup the view when its not hidden. But when I set the .isHidden to true value this would conflict with my height constraint.
I needed to let Autolayout do its auto layout when the view changed from hidden to not hidden and vice-versa. By setting a constraint with a Priority default value of 1000, I was telling to Autolayout to absolutely make sure this constraint is always active.
I realized that by setting the priority to 999, I told Autolayout it could override my constraint when its really needed to do it. Why 999, I assume that when a view isHidden, it really has no size and that is very high priority for Autolayout.
I wish I knew this or knew how to find out default autolayout priorities before.
If anyone knows more about this I would appreciate more info or a link!!!

Hiding a UIStackView with nested UISTackviews

Preface: I took a peek at this post and it didn't solve my problem. Here's a link to a repo that shows the error I'm encountering.
I have a UIViewController with a vertical UIStackView containing nested horizontal UIStackViews.
There are two UIStackViews that are toggled. If UIStackViewNumber1 is displayed, UIStackViewNumber2 is hidden & vice versa. I originally set the view to display both, but it's too crowded.
UIStackView
stackViewThatDoesntMove
stackViewNumber1
stackViewNumber2
stackViewThatDoesntMove
Everything works fine with no AutoLayout errors if I don't hide anything. Each of the UIStackViews has nested stackViews containing buttons, labels, sliders, etc. I found the view was too crowded, so I thought I'd set hide one of stackViews and animate the change, as follows:
stackViewNumber2.isHidden = true
UIView.animate(withDuration: 0.33,
options: [.curveEaseOut],
animations: {
self.stackViewNumber1.layoutIfNeeded()
self.stackViewNumber2.layoutIfNeeded()
self.view.layoutIfNeeded()
},
completion: nil)
}
While I got the desired result in Simulator and on a physical device, the moment I hide one of the UIStackViews, my console fills up with AutoLayout errors.
I took a look at this post and my problem seemed pretty straightforward. I went with the approach with the least "bookkeeping" and wrapped stackViewNumber2 in a UIView. I end up with the same set of errors I originally had without wrapping stackViewNumber1 in a UIView.
I also read Apple's documentation on UIStackView and tried playing around with the arrangedSubviews() to set constraints isActive = false, as follows:
for subview in stackViewNumber2.arrangedSubviews {
for constraint in subview.constraints {
constraint.isActive = false
}
}
stackViewNumber2.updateConstraints()
// then run animation block to update view
Is there a more efficient way to hide nested UIStackViews without adding an IBOutlet for constraint, making sure it's not weak, and doing bookkeeping on whether isActive is true/false?
I think my problem has to do with nested stackViews, but I'm unable to figure out how to address it. Thank you for reading and I welcome suggestions on how to hide a UIStackView containing nested UIStackViews.
Instead of hiding I think it would be better to remove the stackView itself.
myStackView.removeArrangedSubview(playButton)
playButton.removeFromSuperview()
Hope it helps.
I started out wanting to animate hiding one view and unhiding another, but at Md. Ibrahim Hassan's suggestion, I tried removing the stackViewNumber1 from the superview and replacing it with stackViewNumber2. The end result is much cleaner with minimal bookkeeping, no dragging a bazillion #IBOutlets onto the viewController for updating constraints, etc.
The views of parentStackView are stored in the stack's arrangedSubviews() array in the order they're displayed in Main.storyboard.
In this example, the arrangedSubviews array looks like this:
[topStackView, stackViewNumber1, stackViewNumber2, stopButton]
I only need to know about parentStackView, stackViewNumber1, and stackViewNumber2, so I dragged 3 outlets to the storyboard, as follows:
// StackViews the viewController needs to know about...
#IBOutlet weak var parentStackView: UIStackView!
// note weak is removed from the following 2 stackViews
#IBOutlet var stackViewNumber1: UIStackView!
#IBOutlet var stackViewNumber2: UIStackView!
From there, if I want to remove stackViewNumber2, the following code removes it:
parentStackView.removeArrangedSubview(stackViewNumber2)
// parentStackView.arrangedSubviews() = [topStackView, stackViewNumber1, stopButton]
stackViewNumber2.removeFromSuperview()
parentStackView.insertArrangedSubview(stackViewNumber1, at: 1)
// parentStackView.arrangedSubviews() = [topStackView, stackViewNumber1, stopButton]
Now, let's say I want to take out stackViewNumber1 and replace it with stackViewNumber2:
parentStackView.removeArrangedSubview(stackViewNumber1)
// parentStackView.arrangedSubviews() = [topStackView, stopButton]
stackViewNumber2.removeFromSuperview()
parentStackView.insertArrangedSubview(stackViewNumber2, at: 1)
// parentStackView.arrangedSubviews() = [topStackView, stackViewNumber2, stopButton]
Recall stackViewNumber1 and stackViewNumber2 are not weak. The reason is I want to keep them around so I can swap them in and out. I didn't see any adverse impact on memory usage as a result of changing the 2 UIStackView outlets to weak.
End result: The slew of AutoLayout errors I encountered with my initial approach is gone without a ton of bookkeeping.
Update:
With a little more tinkering, I got animation to work, too. Here's the code:
UIView.animate(withDuration: 0.25,
delay: 0,
usingSpringWithDamping: 2.0,
initialSpringVelocity: 10.0,
options: [.curveEaseOut],
animations: {
self.view.layoutIfNeeded()
},
completion: nil)

Constraints not respected when loading from XIB

So, I have the following XIB
This XIB when loaded as a tableviewcell looks like this
I've since decided that I will not need a TableView, so I changed my XIB class from UITableViewCell to UIView. In a ViewController I added this code to viewDidLoad()
var nView = MyChartView.instanceFromNib() as! MyChartView
self.view.addSubview(nView)
And I got this as a result
As you can see, it ignores the margins and continues to right side (ignore the red color since I was using it to try and debug the problem. No chart data is not the problem either). I've printed the xib's frame width and I've seen that it's quite a bit bigger than the screen size, but I've not been able to fix it. Anyone can figure out the problem?
When you add a subView programmatically, you should also add constraints between the subView and its superView
var nView = MyChartView.instanceFromNib() as! MyChartView
self.view.addSubview(nView)
nView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[nView]|", options: [], metrics: nil, views: ["nView": nView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[nView]|", options: [], metrics: nil, views: ["nView": nView]))
You haven't constrained the MyChartView instance's width, either by specifying an exact size when you add the subview, or programmatically adding constraints between the MyChartView instance and its superview. Without doing one or the other of these, the view's dimensions will match whatever they are in the xib.

Move button after adding subview

Edited following comments below - using constraints to control the layout.
I am trying to add/remove views controllers, which are set up within the storyboard, via a button. The button and any previously added views should move down the screen as per the following visual format strings:
Optional("V:|-50-[viewXIB0(100)]-30-[buttonKey]")
Optional("V:|-50-[viewXIB1(100)]-30-[viewXIB0(100)]-30-[buttonKey]")
Optional("V:|-50-[viewXIB2(100)]-30-[viewXIB1(100)]-30-[viewXIB0(100)]-30-[buttonKey]")
(This is the println output of the visual format string fed into NSLayoutConstraint.constraintsWithVisualFormat.)
Thanks already to the comments below, the view does appear each time the button is pressed, and moves down the page correctly. However, the spacing between the superview and top view is zero - without margin. A similar issue also when additional views are added - each one gets tucked underneath the spacing. The bottom spacing is greater than the desired 30. And, after a few inserts, even weirder things start to happen towards the top of the screen.
There are no constraint errors on build or running. When my computer is running super slow, I do see it appear briefly in the correct place, and then snap to the top of the screen. I think I can hear it laughing at me too, but that might be a joke. Autolayout is enabled in both View Controllers.
The full method I'm using to add the view from a touch up inside button is:
#IBAction func addVCBenj(sender: AnyObject)
{
// remove original constraint from button
if (topConstraintBenjV != nil)
{
view.removeConstraint(topConstraintBenjV)
topConstraintBenjV = nil
println("addVCBen constraints removed")
}
// remove previous constraints as views are added
if (constraintV != nil)
{
view.removeConstraints(constraintV!)
println("removed")
}
// instantiate add add the view within the storyboard VC
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let myVC = storyboard.instantiateViewControllerWithIdentifier("VCtoInsert") as UIViewController
view.addSubview(myVC.view)
// create and add a unique key for each view in the dictionary of views
viewsDictionaryKey = "\(viewsDictionaryString)\(viewsDictionaryCounter)"
viewsDictionaryCounter += 1
viewsDictionary[viewsDictionaryKey!] = myVC.view
// create the visual format string for layout
suffixVisualFormatString_V = "[" + viewsDictionaryKey! + "(100)]-30-" + suffixVisualFormatString_V
visualFormatString_V = prefixVisualFormatString_V + suffixVisualFormatString_V
println(visualFormatString_V)
// create and add horizontal and vertical constraints
let constraintH = NSLayoutConstraint.constraintsWithVisualFormat("H:[\(viewsDictionaryKey!)(>=300)]", options: NSLayoutFormatOptions(0), metrics: metricsDictionary, views: viewsDictionary)
myVC.view.setTranslatesAutoresizingMaskIntoConstraints(false)
constraintV = NSLayoutConstraint.constraintsWithVisualFormat(visualFormatString_V!, options: NSLayoutFormatOptions(0), metrics: metricsDictionary, views: viewsDictionary)
self.view.addConstraints(constraintV!)
self.view.addConstraints(constraintH)
}
I've seen the WWDC videos, and searched for a similar problem, but in truth, I'm now so confused, I don't know what to even search for anymore.
As per a comment by BooRanger, by adding an IBOutlet to the top and bottom contraints, it was possible to manipulate the views each time a button was pressed. Ultimately, however, collection views are an easier way to add and remove views.

Resources