How to use a variable UIStackView in UITableView or UICollectionView? - ios

I'm currently building a generic form builder using a UICollectionView.
Each form element is a cell, and for checkboxes, I'm using a UIStackView inside the cell to display all the available options.
The problem is that each time I reuse the cell, even if I remove all the arrangedSubviews, they stay in the view with the new one.
The following code is a simplified version of what I'm doing:
class cell: UICollectionViewCell {
#IBOutlet weak var stackView: UIStackView!
func setup(options: [String]) {
for option in options {
let label = UILabel()
label.text = option
stackView.addArrangedSubview(label)
}
}
override func prepareForReuse() {
super.prepareForReuse()
optionsStackView.arrangedSubviews.forEach({ optionsStackView.removeArrangedSubview(view: $0) })
}
}
Instead of that, my current workaround is to hide() each arrangedSubview in prepareForReuse() instead of removing them, but I don't like that.

If you read the Xcode docs on the removeArrangedSubview method, they say:
Discussion: This method removes the provided view from the stack’s
arrangedSubviews array. The view’s position and size will no longer be
managed by the stack view. However, this method does not remove the
provided view from the stack’s subviews array; therefore, the view is
still displayed as part of the view hierarchy.
To prevent the view from appearing on screen after calling the stack’s
removeArrangedSubview: method, explicitly remove the view from the
subviews array by calling the view’s removeFromSuperview() method, or
set the view’s isHidden property to true.
So you need to also remove the subviews from the stack view. (I also struggled with this when I first started using stack views.)
Edit:
In fact, as #kid_x points out, simply removing the subview with removeFromSuperView() works. I'm not sure what the point of removeArrangedSubview() is, TBH.
Edit #2:
I would advise against using removeArrangedSubview() at all. Instead, do one of the following:
Option 1:
If you need to remove a view from a stack view permanently, simply use removeFromSuperView().
Option 2:
If you want to remove views from your stack view and then put them back later, simply toggle the child view's isHidden property, as mentioned in suprandr's answer. The stack view will close up the empty space and reposition the remaining views as if you removed them.

In many cases, since removing from a superview in a reusable view could (and mostly, will) cause the view to be released, in a UIStackView simply putting
dynamicView.isHidden = false
to remove and
dynamicView.isHidden = true
to add again, will cause the stack view to rearrange the other arranged views.

Related

How can I have a parentVC observe when one of its child subviews(collectionview) is being scrolled?

I am trying to emulate Apple's weather app animation where when you scroll, the top header collapses and remains as a condensed sticky cell. I am attempting to do this with all of the UI being done programmatically and without storyboards.
The two ways I've thought of implementing this each have an issue I have not been able to solve.
My initial attempt was having a containerVC contain a UIView (as a header) and a UICollectionViewController. The issue is that The containerVC cannot access the UICollectionViews scrollViewDidScroll() which I would use to calculate and adjsut the size of the UIView. I could make the containerVC the collectionViews delegate but I wanted to avoid that to keep my logic separated. I also tried using Key Value observers but I could not figure out how to make it work.
My second attempt was to use a UICollectionReusableView as a header cell, that way there is no container view, just a single collectionViewController. The issue here is I can't figure out how to dynamically resize the headercell. The header size is currently being returned from referenceSizeForHeaderInSection and I have been unable to find another way of updating this.
Is there a better way to be going about this? Or an easier solution to the issues described that I haven't tried yet?
You can implement your own delegates:
protocol ParentDelegate: class {
func childDidScroll()
}
extension ParentViewController: ParentDelegate {
func childDidScroll() {
print("My child controller did scroll")
}
}
Create a delegate variable in the class that you want to call it:
weak var delegate: ParentDelegate?
Set it to your parent ViewController:
delegate = parentVC
then inside your child scroll method you call it:
delegate?.childDidScroll()

Resize subview when superview finishes loading

I know theres countless similar questions on this that either all result in using flexible height/width or setting translatesAutoresizingMaskIntoConstraints to false.
I have add a view using an extension I created:
extension UIView {
func addView(storyboard: String, viewIdentier: String) {
let story = UIStoryboard(name: storyboard, bundle: nil)
let subview = story.instantiateViewController(withIdentifier: viewIdentifier)
subview.view.frame = self.bounds
self.addSubview(subview.view)
}
}
If I use this to initialise a view, in the ViewDidAppear everything works fine. But if its in the view did load then the constraints are all over the place because the contrainView that contains the view has its own constraints that are not yet loaded.
I currently use like this:
#IBOutlet weak var container: UIView!
override func viewDidLoad() {
container.addView(storyboard: "Modules", viewIdentifier: "subview")
}
I don't want to initialise the view in the ViewDidAppear because of reasons. Is there any way of fixing this so it works as expected? Or reload the subview constraints after the superview has loaded?
Also I've really tried using other solutions. I can't make this work so would really appreciate some help.
There is no way to do things in the hard-coded manner your code proposes. You are setting the frame of the subview based on self.bounds. But in viewDidLoad, we do not yet know the final value for self.bounds. It is too soon.
The appropriate place to do something that depends on knowing layout values is after layout has taken place, namely in viewDidLayoutSubviews. Be aware that this can be called many times in the course of the app's lifetime; you don't want to add the subview again every time that happens, so use a Bool flag to make sure that you add the subview only once. You might, on subsequent resizes of the superview, need to do something else in viewDidLayoutSubviews in order to make appropriate adjustments to the subview.
But the correct way to do this kind of thing is to add the subview (possibly in viewDidLoad) and give it constraints so that its frame will henceforth remain correct relative to its superview regardless of when and how the superview is resized. You seem, in your question, to reject that kind of approach out of hand, but it is, nevertheless, the right thing to do and you should drop your resistance to it.

Is it necessary to set scrollview.delegate as self?

I am taking an online iOS course provided by Stanford. In the sample code,
#IBOutlet weak var scrollView: UIScrollView! {
didSet {
scrollView.contentSize = imageView.frame.size
// all three of the next lines of code
// are necessary to make zooming work
scrollView.delegate = self
scrollView.minimumZoomScale = 0.03
scrollView.maximumZoomScale = 1.0
}
}
However, if I remove scrollView.delegate = self, this scroll view still works on the simulator.
My questions:
Is it necessary to set scrollview.delegate as self? Why or why not?
What does self refer to? command + left click locate "did set".
You do not have to set the delegate for scrollView. You only do it if you want to react delegate methods that scrollView calls. For example, if you want to do something when user performs a scroll, you need to set the delegate and implement func scrollViewDidScroll(scrollView: UIScrollView) method. Everytime user scrolls, you will be able to react in this method's body.
Self refers to the class that holds this variable. In this case it will be probably your UIViewController
Define, it still works? I mean if you can move it with touch, yeah it will work.
The reason for
scrollView.delegate = self
is that it allows you to add code which can execute upon scroll began, scroll ended etc. That way you have customization points to alter the behavior or actions of scrollview. Without that little code the delegate code will never get called.
Makes sense?
What does self refer to?
self is the object that's executing the code that refers to self. The scrollview instance variable defined in your code snippet is part of some class; in this case it's surely a view controller. The difference between an object and a class is like the difference between a peach pie and a recipe for peach pie: the recipe is a specification that tells you all about peach pies, but you can't eat the recipe, whereas an actual peach pie is a distinct thing, and you can make several similar pies from a single recipe. Anyway, self is the name of the specific object that's executing the didSet method associated with the scrollview variable.
Is it necessary to set scrollview.delegate as self? Why or why not?
A UIScrollView object has a delegate property the you can set to any object that knows how to be a scroll view's delegate, i.e. any object that implements the UIScrollViewDelegate protocol. But since views are almost always managed by a view controller of some sort, it's pretty typical to have the view controller that manages the scroll view also act as it's delegate. The scroll view will work just fine with no delegate at all; setting up a delegate is only important if you want to do something in response to changes in the scroll view, or otherwise modify its behavior somehow. So, if a scroll view has a delegate, it's typically the view controller that manages that scroll view, and since it's probably also the view controller that sets up the scroll view in the first place self (meaning the view controller) is what you'll see most of the time.

How to access custom view properties after initialize

I'm targeting IOS 8+.
I have a form that is used in more than one place. So I decided to create a custom view where I define the various "form" text fields.
I have built my XIB, and the UIView subclass contains the outlets for each textField.
The view is composed of a background image and a scroll with the form fields over it.
Now, my first obstacle was: I need to have this custom view in a container that may or may not have a navigation bar. This made me create a constraint outlet so I could update its value to push down the scroller view. This way I'd have the whole image in the frame, the top being behind the navbar and the scroller bellow the nav bar).
Here's a manual drawing to help understanding the problem.
It's very possible that I'm making a lot of mess and confusion on my way to solve this. :)
The problem is:
After awakeFromNib runs I have no access to the constraint property. I then noticed the same thing happens for the TextFields outlets.
So, how can I access the custom view's properties when I instantiate them programatically?
Something like:
Controller:
let customView = SignupView(frame: f)
view.addSubview(customView)
customView.pushScrollerDownBy(50.0)
Custom view:
func pushScrollerDownBy(yOffset: CGFloat) {
//topScrollerConstraint is the outlet for the textField.
topScrollerConstraint.constant = yOffset //right now topScrollerConstraint is nil.
}
You should check if you have connected your topScrollerConstraint to the file's owner since it will not get instantiated and therefore, error. Here is a recent SO question regarding difference between these two:
What is File’s owner in XIB in this case?

Where should I be setting autolayout constraints when creating views programmatically

I see different examples where constraints are set. Some set them in viewDidLoad / loadView (after the subview was added). Others set them in the method updateViewConstraints, which gets called by viewDidAppear.
When I try setting constraints in updateViewContraints there can be a jumpiness to the layout, e.g. slight delay before the view appears. Also, if I use this method, should I clear out existing constraints first i.e. [self.view [removeConstraints:self.view.constraints]?
I set up my constraints in viewDidLoad/loadView (I'm targeting iOS >= 6). updateViewConstraints is useful for changing values of constraints, e.g. if some constraint is dependent on the orientation of the screen (I know, it's a bad practice) you can change its constant in this method.
Adding constraints in viewDidLoad is showed during the session "Introduction to Auto Layout for iOS and OS X" (WWDC 2012), starting from 39:22. I think it's one of those things that are said during lectures but don't land in the documentation.
UPDATE: I've noticed the mention of setting up constraints in Resource Management in View Controllers:
If you prefer to create views programmatically, instead of using a
storyboard, you do so by overriding your view controller’s loadView
method. Your implementation of this method should do the following:
(...)
3.If you are using auto layout, assign sufficient constraints to each of
the views you just created to control the position and size of your
views. Otherwise, implement the viewWillLayoutSubviews and
viewDidLayoutSubviews methods to adjust the frames of the subviews in
the view hierarchy. See “Resizing the View Controller’s Views.”
UPDATE 2: During WWDC 2015 Apple gave a new explanation of updateConstraints and updateViewConstraints recommended usage:
Really, all this is is a way for views to have a chance to make changes to constraints just in time for the next layout pass, but it's often not actually needed.
All of your initial constraint setup should ideally happen inside Interface Builder.
Or if you really find that you need to allocate your constraints programmatically, some place like viewDidLoad is much better.
Update constraints is really just for work that needs to be repeated periodically.
Also, it's pretty straightforward to just change constraints when you find the need to do that; whereas, if you take that logic apart from the other code that's related to it and you move it into a separate method that gets executed at a later time, your code becomes a lot harder to follow, so it will be harder for you to maintain, it will be a lot harder for other people to understand.
So when would you need to use update constraints?
Well, it boils down to performance.
If you find that just changing your constraints in place is too slow, then update constraints might be able to help you out.
It turns out that changing a constraint inside update constraints is actually faster than changing a constraint at other times.
The reason for that is because the engine is able to treat all the constraint changes that happen in this pass as a batch.
I recommend creating a BOOL and setting them in the -updateConstraints of UIView (or -updateViewConstraints, for UIViewController).
-[UIView updateConstraints]: (apple docs)
Custom views that set up constraints themselves should do so by overriding this method.
Both -updateConstraints and -updateViewConstraints may be called multiple times during a view's lifetime. (Calling setNeedsUpdateConstraints on a view will trigger this to happen, for example.) As a result, you need to make sure to prevent creating and activating duplicate constraints -- either using a BOOL to only perform certain constraint setup only once, or by making sure to deactivate/remove existing constraints before creating & activating new ones.
For example:
- (void)updateConstraints { // for view controllers, use -updateViewConstraints
if (!_hasLoadedConstraints) {
_hasLoadedConstraints = YES;
// create your constraints
}
[super updateConstraints];
}
Cheers to #fresidue in the comments for pointing out that Apple's docs recommend calling super as the last step. If you call super before making changes to some constraints, you may hit a runtime exception (crash).
This should be done in ViewDidLoad, as per WWDC video from Apple and the documentation.
No idea why people recommend updateConstraints. If you do in updateConstraints you will hit issues with NSAutoresizingMaskLayoutConstraint with auto resizing because your views have already taken into account the auto masks. You would need to remove them in updateConstraints to make work.
UpdateConstraints should be for just that, when you need to 'update' them, make changes etc from your initial setup.
Do it in view did layout subviews method
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
I have this solution to change constraints before those who are in the storyboard are loaded.
This solution removes any lags after the view is loaded.
-(void)updateViewConstraints{
dispatch_async(dispatch_get_main_queue(), ^{
//Modify here your Constraint -> Activate the new constraint and deactivate the old one
self.yourContraintA.active = true;
self.yourContraintB.active= false;
//ecc..
});
[super updateViewConstraints]; // This must be the last thing that you do here -> if! ->Crash!
}
You can set them in viewWillLayoutSubviews: too:
override func viewWillLayoutSubviews() {
if(!wasViewLoaded){
wasViewLoaded = true
//update constraint
//also maybe add a subview
}
}
This worked for me:
Swift 4.2
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Modify your constraints in here
...
}
Although honestly I am not sure if it is worth it. It seems a bit slower to load than in viewDidLoad(). I just wanted to move them out of the latter, because it's getting massive.
Add your constraints in viewWillLayoutSubviews() to add constraints programmatically
See Apple Documentation in Custom Layout Section
If possible, use constraints to define all of your layouts. The
resulting layouts are more robust and easier to debug. You should only
override the viewWillLayoutSubviews or layoutSubviews methods when you
need to create a layout that cannot be expressed with constraints
alone.
Following example is to pass any view to another class. create my view from storyboard
Swift 5.0
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
self.abcInstance = ABC(frame: self.myView.frame)
}
}
If you miss DispatchQueue.main.async, it will take time to update constraints in viewWillAppear. Create myView in storyboard and give constraints same as screen width & height, then try printing frame of myView. It will give accurate value in DispatchQueue.main.async or in viewDidAppear but not give accurate value in viewWillAppear without DispatchQueue.main.async.

Resources