forked from objcio/articles
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Florian Kugler
committed
Mar 1, 2014
0 parents
commit f7ae8ca
Showing
66 changed files
with
15,821 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
--- | ||
layout: post | ||
title: "View Controller Containment" | ||
category: "1" | ||
date: "2013-06-07 08:00:00" | ||
author: "<a href=\"https://twitter.com/rickigregersen\">Ricki Gregersen</a>" | ||
tags: article | ||
--- | ||
|
||
|
||
Before iOS 5, view controller containers were a privilege for Apple only. | ||
In fact, there was a passage in the view controller programming guide | ||
stating that you should not use them. | ||
The general advice from Apple on view controllers used to be that <q>a | ||
view controller manages a screenful of content</q>. | ||
This has since changed to <q>a view controller manages a self-contained | ||
unit of content</q>. | ||
Why didn't Apple want us to build our own tab bar controllers and | ||
navigation controllers? Or put more precisely, what is the problem with: | ||
|
||
[viewControllerA.view addSubView:viewControllerB.view] | ||
|
||
<img src="{{site.images_path}}/issue-1/[email protected]" style="width:357px" alt="Inconsistent view hierarchy"/> | ||
|
||
The UIWindow, which serves as an app's root view, is where rotation and | ||
initial layout messages originate from. In the | ||
illustration above, the child view controller, whose view was inserted | ||
in the root view controller view hierarchy, is excluded from those events. | ||
View event methods like `viewWillAppear:` will not get called. | ||
|
||
The custom view controller containers built before iOS 5 would keep a | ||
reference to the child view controllers and manually relay all the | ||
view event methods called on the parent view controller, which is | ||
pretty difficult to get right. | ||
|
||
## An Example | ||
|
||
When you were a kid playing in the sand, did your parents ever tell you that if you kept digging with your little shovel you would end up in China? | ||
Mine did, and I made a small demo app called *Tunnel* to test this claim. | ||
You can clone the [GitHub | ||
repo](https://github.com/RickiG/view-controller-containment) and run the | ||
app, which will make it easier to understand the example code. | ||
*(Spoiler: digging through the earth from western Denmark lands you somewhere in the South Pacific Ocean.)* | ||
|
||
<img src="{{site.images_path}}/issue-1/[email protected]" alt="Tunnel screenshot" style="width: 513px;" /> | ||
|
||
To find the antipode, as the opposite location is called, move the little guy with the shovel around and the map will tell you where your exit location is. Tap the radar button and the map will flip to reveal the name of the location. | ||
|
||
There are two map view controllers on screen. Each of them has to deal with dragging, annotation, and updating the map. | ||
Flipping these over reveals two new view controllers which reverse geocode the locations. | ||
All the view controllers are contained inside a parent view controller, which holds their views, and ensures that layout and rotation behaves as expected. | ||
|
||
The root view controller has two container views. These are added to | ||
make it easier to layout and animate the views of child view | ||
controllers, as we will see later on. | ||
|
||
- (void)viewDidLoad | ||
{ | ||
[super viewDidLoad]; | ||
|
||
//Setup controllers | ||
_startMapViewController = [RGMapViewController new]; | ||
[_startMapViewController setAnnotationImagePath:@"man"]; | ||
[self addChildViewController:_startMapViewController]; // 1 | ||
[topContainer addSubview:_startMapViewController.view]; // 2 | ||
[_startMapViewController didMoveToParentViewController:self]; // 3 | ||
[_startMapViewController addObserver:self | ||
forKeyPath:@"currentLocation" | ||
options:NSKeyValueObservingOptionNew | ||
context:NULL]; | ||
|
||
_startGeoViewController = [RGGeoInfoViewController new]; // 4 | ||
} | ||
|
||
The `_startMapViewController`, which displays the starting position, is instantiated and set up with the annotation image. | ||
|
||
1. The `_startMapViewcontroller` is added as a child of the root view | ||
controller. This automatically calls | ||
the method `willMoveToParentViewController:` on the child. | ||
2. The child's view is added as a subview of the first container view. | ||
3. The child is notified that it now has a parent view controller. | ||
4. The child view controller that does the geocoding is instantiated, | ||
but not inserted in any view or controller hierarchy yet. | ||
|
||
|
||
|
||
## Layout | ||
|
||
The root view controller defines the two container views, | ||
which determine the size of the child view controllers. | ||
The child view controllers do not know which container they will be | ||
added to, and therefore have to be flexible in size: | ||
|
||
- (void) loadView | ||
{ | ||
mapView = [MKMapView new]; | ||
mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth |UIViewAutoresizingFlexibleHeight; | ||
[mapView setDelegate:self]; | ||
[mapView setMapType:MKMapTypeHybrid]; | ||
|
||
self.view = mapView; | ||
} | ||
|
||
Now they will layout using the bounds of their super view. This increases the reusability of the child view controller; if we were to push it on the stack of a navigation controller, it would still layout correctly. | ||
|
||
## Transitions | ||
|
||
Apple has made the view controller containment API so fine-grained that | ||
it is possible to construct and animate any containment scenario we can | ||
think of. | ||
Apple also provides a block-based convenience method for exchanging two controller views on the screen. | ||
The method | ||
`transitionFromViewController:toViewController:(...)` | ||
takes care of a lot of the details for us: | ||
|
||
- (void) flipFromViewController:(UIViewController*) fromController | ||
toViewController:(UIViewController*) toController | ||
withDirection:(UIViewAnimationOptions) direction | ||
{ | ||
toController.view.frame = fromController.view.bounds; // 1 | ||
[self addChildViewController:toController]; // | ||
[fromController willMoveToParentViewController:nil]; // | ||
[self transitionFromViewController:fromController | ||
toViewController:toController | ||
duration:0.2 | ||
options:direction | UIViewAnimationOptionCurveEaseIn | ||
animations:nil | ||
completion:^(BOOL finished) { | ||
[toController didMoveToParentViewController:self]; // 2 | ||
[fromController removeFromParentViewController]; // 3 | ||
}]; | ||
} | ||
|
||
1. Before the animation we add the `toController` as a child and we | ||
inform the `fromController` that it will be removed. If the fromController's view is part of the container's view hierarchy, this is where `viewWillDisapear:` is called. | ||
2. `toController` is informed of its new parent, and appropriate view | ||
event methods will be called. | ||
3. The `fromController` is removed. | ||
|
||
This convenience method for view controller transitions automatically swaps out the old view controller's view for the new one. However, if you implement your own transition and you wish to only display one view at a time, you have to call `removeFromSuperview` on the old view and `addSubview:` for the new view yourself. Getting the sequence of method calls wrong will most likely result in an `UIViewControllerHierarchyInconsistency` warning. For example, this will | ||
happen if you call `didMoveToParentViewController:` before you added the view. | ||
|
||
In order to be able to use the `UIViewAnimationOptionTransitionFlipFromTop` animation, we had to add the children's views to our view containers instead of to the root view controller's view. Otherwise the animation would result in the entire root view flipping over. | ||
|
||
|
||
## Communication | ||
|
||
View controllers should be reusable and self-contained entities. Child view controllers are no exception to this rule of thumb. In order to achieve this, the parent view controller should only be concerned with two tasks: laying out the child view controller's root view, and communicating with the child view controller through its exposed API. It should never modify the child's view tree or other internal state directly. | ||
|
||
Child view controllers should contain the necessary logic to manage their view trees themselves -- don't treat them as dumb views. This will result in a clearer separation of concerns and better reusability. | ||
|
||
In the Tunnel example app, the parent view controller observes a property called `currentLocation` on the map view controllers: | ||
|
||
[_startMapViewController addObserver:self | ||
forKeyPath:@"currentLocation" | ||
options:NSKeyValueObservingOptionNew | ||
context:NULL]; | ||
|
||
When this property changes in response to moving the little guy with the shovel around on the map, the parent view controller communicates the antipode of the new location to the other map: | ||
|
||
[oppositeController updateAnnotationLocation:[newLocation antipode]]; | ||
|
||
Likewise, when you tap the radar button, the parent view controller sets the locations to be reverse geocoded on the new child view controllers: | ||
|
||
[_startGeoViewController setLocation:_startMapViewController.currentLocation]; | ||
[_targetGeoViewController setLocation:_targetMapViewController.currentLocation]; | ||
|
||
Independent of the technique you choose to communicate from child to parent view controllers (KVO, notifications, or the delegate pattern), the goal always stays the same: the child view controllers should be independent and reusable. In our example we could push one of the child view controllers on a navigation stack, but the communication would still work through the same API. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
layout: toc | ||
category: "1" | ||
date: "2013-06-07 12:00" | ||
tags: toc | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
--- | ||
layout: post | ||
title: "Introduction" | ||
category: "1" | ||
date: "2013-06-07 12:00" | ||
tags: editorial | ||
--- | ||
|
||
Welcome to the first edition of objc.io, a periodical about best practices and advanced techniques in Objective-C! | ||
|
||
objc.io was founded in Berlin by [Chris Eidhof](https://twitter.com/chriseidhof), [Daniel Eggert](https://twitter.com/danielboedewadt), and [Florian Kugler](https://twitter.com/floriankugler). We started objc.io to create a regular platform for in-depth technical topics relevant to all iOS and OS X developers. | ||
|
||
Each edition of objc.io has a focus on one particular subject, with multiple articles covering different aspects of that topic. The subject of this first edition is *Lighter View Controllers*, and it contains four articles – three from the founding team and one from [Ricki Gregersen](https://twitter.com/rickigregersen), whom we would like to welcome as our first guest writer! | ||
|
||
It's a common problem in the code base of iOS apps that view controllers get out of hand, because they do too much. By factoring out reusable code, they are easier to understand, easier to maintain and easier to test. This issue will focus on best practices and techniques how you can keep view controllers clean. | ||
|
||
We will look at how to make view controllers primarily coordinating objects by factoring out view and model code, as well as introducing other controller objects in addition to view controllers. Furthermore, we will look at splitting up view controllers using the view controller containment mechanism, and finally, discuss how to test clean view controllers. | ||
|
||
In upcoming editions we will have more articles from great guest writers of the Objective-C community; [Loren Brichter](https://twitter.com/lorenb), [Peter Steinberger](https://twitter.com/steipete), [Brent Simmons](https://twitter.com/brentsimmons), and [Ole Begemann](https://twitter.com/olebegemann) have committed to writing in the future. [Contact us](mailto:[email protected]) if you have an idea for an interesting topic and you would like to contribute an article about it to objc.io. | ||
|
||
Chris, Daniel, and Florian. |
Oops, something went wrong.