UIView autolayout: hierarchy of subviews VS minimum views hierarchy? - ios

I'm wondering which is the better way to implement a view which is designed to have complicated subview hierarchy, say a view with one sub view on left and one sub view on right, the left subview has X number of sub-subviews in one column, the right subview has Y number of sub-subviews in a row.
(X and Y varies)
Two ways to implement it:
Custom left and right views (ie. UIView subclass), custom left sub-subview, custom right sub-subviews, the root view only deals with custom left and right views, and they configure their sub-subviews
Only one view with a column of views (ie left view's sub-subview) on left and a row of views (ie right view's sub-subview) on right
First approach:
pros:
clean hierarchy means better maintainability.
responsibilities distributed over subviews, so less complication in each view
cons:
nested subviews hierarchy
may have worse performance due to auto-layout
delegation chain is more difficult, consider each subview as a button which need to perform certain action, the custom view need to delegate the action all the way back to root view
Second approach:
pros:
less subviews
may have better performance
easy delegation chain compared to 1st approach
cons:
hard to maintain / modify, as all subviews are in one level, especially with auto-layout
messy code base since all views are in one base view
Looks like 1st approach is better, but it still has several cons, is there a completely new way to implement it which copes with all the cons?

Both subviews (left column and right column) must have something in common, otherwise you would not be showing them at the same time. Because they have a general relation I would have no issue going with option 2. With that said, I would still prefer option 1 for the following reasons:
A clean hierarchy is easier to understand and maintain.
View logic that is distributed is easier to understand and will keep your classes smaller (which also means it will be easier to reuse sub views elsewhere).
"May have worse performance" is a big MAYBE. You should take actual measurements with Instruments or by using NSDate and timeIntervalSinceNow. As long as the constraints are always installed at the nearest common ancestor you should be fine.
Delegation and target/action won't be so bad self.firstView.subView.button.target = self.
Acceptable performance on all supported hardware should be the primary deciding factor. Maintainability should be a close second.
Go with the first approach.

Related

Tips for how to code a very complicated custom UITableViewCell

Please see my mockup pictured above. I am a bit puzzled as how to code this. I guess I would use one UITableView and have all customizations on one cell. Any other ideas? I wasn't sure if it would be better to have multiple tableviews on one controller or something instead.
First off, this design really won't work except on iPad. It's just too much to try to cram onto an iPhone screen.
Collection views are like a more flexible form of table view. You can arrange collection views in rows, columns, rows AND columns, circles, or whatever you want.
You can make a collection view act like a table view, but it's more work, and a little more confusing.
If your UI is a vertical list of cells, a table view might still be a better fit, since it matches what you're doing.
Assuming this app is iPad only:
Create a custom subclass of UITableViewCell, with it's own XIB file. Define the contents as desired. Making each "tile" (the 5 boxes shown in each cell) in the table view cell a separate custom subclass of UIView might make sense, or it might not, depending on how you expect to use them. If they are always in the same order, and never used anywhere else but in this table view, then no. Just build the cell from components. If you ever think you might use one of these tiles somewhere else, or if you might ever display them in a different order, then yes, make each tile a separate custom UIView subclass, and use those custom views in constructing your cell.
Having multiple UITableView is bad idea – when you scroll one of them others will remain static. I would recommend you to use one table view but decouple a cell in the separate views (or even view controllers) and have a special view (maybe with .xib) for each of 5 components.

How to protect a collection view subview from reuse

UICollectionView lets you define custom views for cell items, for supplementary views, and for decorative views. These must all subclass UICollectionReusableView, so that the collection view can dispose of them to save resources when they are offscreen.
However, I want to add a view which scrolls along with the rest of the collection view's content, but which will never be disposed of. That is because this view contains some complex state I do not wish to manage in the view controller (long story). This view is a bit like a decoration view, in that it does not depend on data vended by the UICollectionViewDatasource.
So what is a valid way to do this?
Option 1. Is there a way to make this a decoration view, but somehow mark it so that the collection view will never dispose of it when it's offscreen?
Option 2. If not, is the best alternative to just add this as an ordinary subview to the collection view, taking advantage of the collection view's functionality as a scroll view? Is this supported by the collection view explicitly, or am I at risk that manually adding a subview will break the collection view's own layout management?
Option 3. If this isn't supported by collection views, then is there another conventional best practice for this case? For instance, I could add the decoration-like view as a sibling to the collection view, and then try to hook into the collection view's pan gesture recognizer, but this feels hacky and fragile.
Since
this view contains some complex state I do not wish to manage in the view controller
This is the root of the problem. It's never a good idea to use views to store state. You say that you're looking for a "best practice," but the truth of the matter is that the best practice is to move your state management code out of the view and into a model object.
The separation between model and view is is there pretty much for the reason you've discovered -- views have different lifetimes, relationships, and responsibilities than model classes. Any solution to your problem that tries to turn a view into a model class is likely to give you that fragile, hacky feeling.
It seems like my question is a duplicate (at least in the likely special case of a header or footer view) of How to add HeaderView in UICollectionView like UITableView's tableHeaderView .
And that answers shows that one solution is to use the contentInset property on the UICollectionView to create space for headers or footers, and then manually add them as subviews into that space.
Another solution that worked for me is to subclass UICollectionViewFlowLayout to create an additional space, instead of using contentInset.
However, using the contentInset is obviously more straightforward. The only difference I can see is that the inset-based method means that the collection view's contentSize only reflects the cells it manages, and not the manually added header and footer views. I don't see any problems with this.

UIPageViewController: how to have "negative" spacing between view controllers (with scroll-transition style)

I have to implement a view controller (on iPhone, portrait only, full screen view) where the upper part of the view must have an horinzontal, paged scrolling behavior, potentially infinite.
I already used for similar purposes UIPageViewControllers, to take advantage of the datasource and delegate protocols, which are very helpul for manage memory and other stuff (keeping only 3 view controllers in memory, providing delegates to handle actions exactly when a transition is done and so on): so I think that in this case too this component is the best choice.
But here comes my problem. In the view I'm realizing, I have to let the user understand that he can swipe left and right to move to another view: a page control is not a good choice, since the scroll could be potentially infinite, so I would like to let a small portion of the views of the left and right view controllers to be visible.
Something like that:
link to the image (sorry I cannot include images in my posts yet)
Up to now I have not been able to figure out how to realize this. In the options during initialization, UIPageViewControllerOptionSpineLocationKey can be specified to set (from documentation) "Space between pages, in points": but this seems to work only with positive value, so that the space increases, while it ignores negative values that could reduce the space.
I hope there might be a solution using page view controllers, since at the same time I need to refresh a table view in the lower part of the screen when a transition is complete, and the delegate method of page controllers is ideal for this aim.
Maybe a collection view can be an alternate solution, but it is more complicated and I'm not sure how to obtain a behavior like the one I described to refresh the table view.
If needed I can attach some code and a screenshot of the prototype
Ok, I understand that this is not possible and why it is.
The datasource methods load, when needed, the view controllers that are before and after the current one. Making these view controllers' views always visible, as I desired, will require that the datasource loads more than one view controllers after (or before, depends on the direction of scrolling) the current one, to be ready for the pan actions (potentially, before the animation is ended by the user lifting up its finger, two view controllers "after" or "before" could become visible it my desired configuration), and this is not intended by UIPageViewController on iPhone, especially in portrait mode.
So actually, the only way to achieve that more than one view is visible in an horizontal-scrolling component at any time, is to implement a UIScrollview with horizontal paging and calculate the contentSize and other sizes accordingly.

Dismiss VC very slow due to autolayout and lots of subviews

I have a scrollview loaded with lots of subviews, and for reasons I won't go into, I opted against cell re-use and a UITableView. It works well, save for a little initial main thread lag when the view is populated. However, every time a VC "on top" is dismissed (e.g. after presenting a modal overtop), there is a significant delay (2-3 seconds). I ran it through the profiler and the stack trace delves very deeply into IOS / Autolayout, as seen in the attached image.
The code seems to go into
NSLayoutConstraint("+0x06 calll "-[NSLayoutConstraint _addToEngine:]+0x0b").
Is there anything I can do to bypass this apparent re-application of constraints?
Why your scroll view's performance is poor
From looking at your trace, the root of your problem begins when transitioning back: you spend 80%+ of your time in -[UITransitionView transition:fromView:toView:removeFromView:]. This method immediately adds the view you transition to back into the view hierarchy (the -addSubview: call following), which necessitates a full Auto Layout pass.
I'm not aware of any way to force the normal iOS view controller transition system into keeping your old view around in the background, so to speak, so as to avoid this Auto Layout pass. (Such an approach might actually run into its own set of problems, if multiple views stack up offscreen and start to cause memory pressure.)
Speeding up Auto Layout in your existing structure
Despite the above, you can try to optimize your Auto Layout pass in a couple ways. First, rework your scroll view to have fewer subviews. Apple's developer tech support has also sometimes recommended flattening your view hierarchy; causing Auto Layout to do less recursion can sometimes be helpful. (See, for example, the performance tests performed by Florian Kugler and Martin Pilkington.)
You should also always ensure that a constraint is installed on the deepest possible view within your hierarchy, while still being on the common ancestor of the two views it relates to. For example, if view A has subviews B and C, you would want a constraint referencing only view C installed on view C, not view A; however, a constraint relating B and C would need to exist on A. This is a good guideline for all Auto Layout work, not just in this particular case; the deeper a constraint is installed in the hierarchy, the faster the underlying solver can work at any particular level.
Switching your approach
UITableView is able to avoid this problem by providing a sort of break in the Auto Layout system: the table view can position properly with constraints, then use its own delegate methods (such as -tableView:heightForRowAtIndexPath:) and some internal layout magic to run Auto Layout individually for each cell, without requiring the entire view to be laid out all at once. Regular UIScrollViews do not have the luxury of this optimization.
I know you said you have reasons for not adopting a UITableView here, but you might consider revisiting those reasons. You can see some very significant speed improvements from switching to UITableView and implementing the delegate method -tableView:estimatedHeightForRowAtIndexPath:, which lets you short-circuit a lot of Auto Layout calculations at view load time and defer them until the point you're about to scroll a new cell onscreen. There has been some fairly significant experimentation done in this direction, both here on StackOverflow and by others in the community. (A quick search for "UITableView Auto Layout height" will turn up dozens of results.)
Good luck!

IOS Storyboard: Load Multiple Subviews for given position

I have 3 different subviews all defined directly in the storyboard, so they have outlets to them as well. All of these subviews are meant to occupy the same coordinates on a screen at different times, with only one occupying the space at any time, so that it looks like some appearance is changing. How do I go about doing this? Say I also have a enumeration that defines what state I'm currently in and thus what subview is shown for that location.
Two basic options:
Just go ahead and add the three subviews to your scene. If you do this, there are a couple of tricks that will make your life much easier in IB:
For each of the views, go to the "identity inspector" tab (the third one) in the far right panel, expand the "Document" section, and give each of the three views unique "labels" (not to be confused with UILabel controls; this is just a label or description that IB will use internally to refer to your view). That way, as you navigate the tree of controls listed in the "Document Outline" (that list of all of your scenes that appears in the left side of the center panel), you'll be able to figure out which is which. As you work with these overlapping views, a strong command of this "document outline" will make your life much easier.
When you have the three views on the scene, you may find that it will be easiest to drag the view you want to work on to the end of the list of the three views (but at the same level as its peers) in that "Document Outline". You can then edit that subview. Repeat that process for the three subviews as you do your IB work on them.
You can make an outlet collection for your three subviews, if you want. This makes it easier when you want to perform some action on all of the subviews. Perhaps not of great utility when dealing with only three, but if you ever had more subviews, the collections can be useful.
You can define unique UIView subclasses for each of the three views, which can be useful to keep your list of IBOutlet references a little more structured. Also any view-specific UI logic can be isolated into the individual UIView subviews.
If you use this technique, if you plan on animating the transition between these three subviews, it's actually quite useful to not just put these three subviews on the top-level view of the scene in question. It's quite useful to have a view on the scene that defines the dimensions of the three subviews, and then put your three subviews inside this new interim subview. This way, when you animate changes, you can constrain the animation to just that portion of the screen. This new, interim UIView is often called a container view, but should not be confused with the iOS 6 container view that you'll see in IB, which is related to the next technique, defined below.
While all of those tricks can make the manipulation and management of the three sets of overlapping views in a single scene a little easier, I actually think that a custom container view controller is the best way to go. One scene for the parent scene/view controller, and a separate view controller and IB scene for each of the three different child views. It takes a little extra code up front (not hard, but a little alien the first time you do it), but then your code and the IB scenes are nicely isolated. Architecturally, this is the most elegant approach, IMHO. If you want to do this, you should refer to:
WWDC 2011 #102 on UIViewController Containment (Apple developer ID required)
the containment section of the View Controller Programming Guide
the containment section of the UIViewController Reference document

Resources