Objective c - Flip between views with one ViewCont

2019-08-03 05:34发布

问题:

I'm building a location app that show the user some places around him.
I have a NearbyPlacesViewController with a segment control that has two buttons "list" and "map"

If user press on "list" - I show him a table view with the list of places around him
If user press on "map" - The view flips around and I show the user a mapView with the places on it as pins.

In "list" I also use a UISearchBar and a UISearchDisplayController to search the tableView
In "map" I also have some other subviews beside the mapView

Currently I keep all of the views (UITableView, MKMapView, UISearchBar and more...)
and the delegate methods (UITableViewDelegate, UITableViewDataSource, MKMapViewDelegate, UISearchDisplayDelegate, and more..)
in the NearbyPlacesViewController.

When user press on "map" button I hide all of the views relevant to the "list" view (tableView, search bar...), and I un-hide all of the views relevant to the "map" view (mapView, some other subviews...), I then use UIView transitionWithView to do flip animation between them.

Everything works but It seems a bit messy, and the result is a big NearbyPlacesViewController with a lot of code and delegate methods.

Is it better to do it with separate viewControllers?
And if so, how do I do it? do I create ListViewController and MapViewController and put them in a NearbyViewController?
How do I share the model between them?

回答1:

Just so you get a sense of what view controller containment might look like (iOS 5), this is one way to do it. It consists of four classes, the container view controller (whose view will have the segmented control for switching between two child view controllers), two random child view controllers, and a model class where we'll store the data (which can be accessed by the two child view controllers).

First you create a container view controller with your segmented control (I also added a UIView which basically defines the frame where child view controllers' views will be placed, just to make it easier to figure out where to put that view):

//  ContainerViewController.h

#import <UIKit/UIKit.h>

@interface ContainerViewController : UIViewController

@property (weak, nonatomic) IBOutlet UISegmentedControl *segmentedControl;
@property (weak, nonatomic) IBOutlet UIView *childView;

- (IBAction)changeChild:(id)sender;

@end

And then you implement it:

//  ContainerViewController.m

#import "ContainerViewController.h"
#import "FirstContainedViewController.h"
#import "SecondContainedViewController.h"
#import "MyModel.h"

@interface ContainerViewController ()
{
    FirstContainedViewController  *_controller0;
    SecondContainedViewController *_controller1;
    MyModel                       *_model;

    UIViewController __weak *_currentChildController; // let's keep track of the current 
}

@end

@implementation ContainerViewController

@synthesize segmentedControl = _segmentedControl;
@synthesize childView = _childView;

- (void)dealloc
{
    // let's release our child controllers

    _controller0 = nil;
    _controller1 = nil;

    // and release the model, too

    _model = nil;
}

// this is my own method to
// 1. add the child view controller to the view controller hierarchy;
// 2. do the appropriate notification (even though I don't use it, Apple says you should do this, so I will); and 
// 3. set the frame size to the appropriate size

- (void)addChildToThisContainerViewController:(UIViewController *)childController
{
    [self addChildViewController:childController];
    [childController didMoveToParentViewController:self];
    childController.view.frame = CGRectMake(0.0, 
                                            0.0, 
                                            self.childView.frame.size.width, 
                                            self.childView.frame.size.height);
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // let's create our model, our data

    _model = [[MyModel alloc] init];

    // set the segmented index to point to the first one

    [self.segmentedControl setSelectedSegmentIndex:0];

    // let's create our two controllers and provide each a pointer to our model

    _controller0 = [[FirstContainedViewController  alloc] initWithNibName:@"FirstContainedView"  bundle:nil];
    _controller0.model = _model;

    _controller1 = [[SecondContainedViewController alloc] initWithNibName:@"SecondContainedView" bundle:nil];
    _controller1.model = _model;

    // let's add them to the view controller hierarchy

    [self addChildToThisContainerViewController:_controller0];
    [self addChildToThisContainerViewController:_controller1];

    // let's add the currently selected controller as the "current child controller" and add it to our current view

    _currentChildController = [self.childViewControllers objectAtIndex:self.segmentedControl.selectedSegmentIndex];
    [self.childView addSubview:_currentChildController.view];
}

- (void)viewDidUnload
{
    [self setChildView:nil];
    [self setSegmentedControl:nil];
    [super viewDidUnload];
    // Release any retained subviews of the main view.
}

- (IBAction)segmentedControlValueChanged:(UISegmentedControl *)sender 
{
    UIViewController *oldChildController = _currentChildController;
    UIViewController *newChildController = [self.childViewControllers objectAtIndex:sender.selectedSegmentIndex];
    UIViewAnimationOptions options;

    // let's change the animation based upon which segmented control you select ... you may change this as fits your desired UI

    if (sender.selectedSegmentIndex == 0)
        options = UIViewAnimationOptionTransitionFlipFromLeft;
    else 
        options = UIViewAnimationOptionTransitionFlipFromRight;

    [self transitionFromViewController:oldChildController 
                      toViewController:newChildController
                              duration:0.5 
                               options:options 
                            animations:nil 
                            completion:nil];

    _currentChildController = newChildController;
}

@end

My model just has two data elements, an array of objects, and a string, but obviously you can do whatever you want. I'll just show the header (as the implementation details are trivial and uninteresting):

//  MyModel.h

#import <Foundation/Foundation.h>

@interface MyModel : NSObject

@property (nonatomic, strong) NSMutableArray *listOfItems;
@property (nonatomic, strong) NSString *displayText;

- (MyModel *)init;

@end

And the child view controllers are similarly trivial:

//  FirstContainedViewController.h

#import <UIKit/UIKit.h>

@class MyModel;

@interface FirstContainedViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

@property (nonatomic, weak) MyModel *model;

@end

And the implementation might look something like (this is a trivial example, but shows how you can retrieve information from the shared model):

//  FirstContainedViewController.m

#import "FirstContainedViewController.h"
#import "MyModel.h"

@implementation FirstContainedViewController

@synthesize model = _model;

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark - tableview data source delegate methods

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.model.listOfItems count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"fcvc";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (!cell)
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];

    cell.textLabel.text = [self.model.listOfItems objectAtIndex:indexPath.row];

    return cell;
}

@end

Hopefully this gives you a sense of how you could use separate view controllers for your two views, how to switch between them, make your model accessible to both. This is a fairly simple example, but it's functional. There are some optimizations I might suggest, but hopefully this is enough to get you going in the right direction.



回答2:

A couple of options leap out at me:

  1. If you use a UISegmentedControl, you could have two UIView objects for your two screens (you can create them in interface builder or build them programmatically) and then you could just hide/show or addSubview/removeFromSuperview as you jump between them. Since all of the data will be managed by your single view controller, this is pretty easy. If the two subviews get really complicated, though, this single view controller might get pretty hairy. But should work fine.

  2. If you wanted separate view controllers, you would probably pursue iOS 5's view controller containment (see Implementing a Container View Controller in the UIViewController reference, or see WWDC 2011 session 102), though this is a little more complicated. You could store your data as properties of the container view controller which are either passed to or reference by the child controllers.

  3. If you're not wed to the segmented control UI, you could just use the UITabBarController which is ideally suited for this scenario (it's effectively a permutation of the previous option, but the container view controller, the tab bar controller in this case, is already written for you). One view controller for each of the two views, and store the data either in the UITabBarController custom class, a singleton, some persistent storage (like user defaults, core data, sqlite), etc.



回答3:

When dealing with a segmentedControl this is how I've done it in the past. You might be able to create a separate controller class to handle the underlying model for everything and clean out some of that code, but that really just depends on how far you want to take it.



回答4:

Think ModelViewController. If they are truly only views that you are switching between, use of one UIViewController is appropriate, that's what I usually do with segmented controls because it usually comes down to switching around the view for the current controller. The controller should definitely handle the IBAction for the segmented control.

Delegates and Datasources were meant to be extracted into a separate class when it made sense, and in your case it does. I would consider separate classes for the various delegates you are using, this will clean it up significantly while also sticking close to the design principles Apple intended. You could keep the UITableView delegate and datasource together in their own class, but other than that, creating a separate class for each different delegate will clean it up significantly.



回答5:

Seperate view controllers is probably a neater way to go. I prefer them. Also means you can make the initial view controller the parent to the others and lay out the various views you've got and add your child view controllers as subvuews to the views of the parent as and when their needed. Also makes it easier to add some custom animations or relay out at a later date.



回答6:

In my opinion, your best option would be to have three View Controllers and create Modal Segues between them. (I assume you're using storyboards.) You would have your parent View Controller with two children (List & Map).

(1) In your storyboard (that has ParentViewController, ListViewController & MapViewController) create a Modal Segue from each button to the child VCs. Give them identifiers (showList & showMap would work well) and choose Transition:Flip Horizontal.

Set up the protocol and delegates: (I'll show you how to do one, then just repeat it.)

(2a) In ListViewController.h add above @interface:

@class ListViewController;

@protocol ListViewControllerDelegate
    - (void)listViewControllerDidFinish:(ListViewController *)controller;
@end

(2b) Add the delegate as a property:

@property (weak, nonatomic) id <ListViewControllerDelegate> delegate;

(3a) In ListViewController.m synthesize:

@synthesize delegate;

(3b) And delegate in the IBAction button method to flip back:

- (IBAction)flipBack:(id)sender
{
    [self.delegate ListViewControllerDidFinish:self];
}

(4) In ParentViewController.h add at the very top #import "ListViewController.h" and on the end of @interface <ListViewControllerDelegate>

(5a) In ParentViewController.m add the method to conform to the protocol:

- (void)listViewControllerFinish:(ListViewController *)controller
{
    [self dismissModalViewControllerAnimated:YES];
}

(5b) Then set it as the delegate in prepareForSegue:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showList"]) {
        [[segue destinationViewController] setDelegate:self];
    }
}