Showing posts with label UIStoryboard. Show all posts
Showing posts with label UIStoryboard. Show all posts

Thursday, May 10, 2012

UIStoryBoard Power Drill, Batteries Included

I got a lot of great response and questions about my post with the UIStoryBoard. This means people are interested in using it, and I'm not good with explaining things.

I did combo this drill down with a sorta complex core data model and assumed this was setup before handing off objects to each subsequent class. I'm going to nix the core data in favor of a simple input, but we are going to keep music on the table!

Lets start here : GitHub-UIStoryboardExamples

This Xcode project will start you out with some plist data that is a nested list of music genre, artist, album, song. At the same time not to complicate the ViewControllers on how it handles these different tiers the data is only nested as a "NSString name" , "NSArray items" pair. This way we concentrate straight on the loopback of the table segue.

First take a look at the "Auto" side of this app, this table view controller uses the cells prototype push outlet to auto loopback to the controller again. Because this does not utilize the didSelectRowAtIndexPath: (or to be clear) this does call but, performs the segue regardless. This practice will make use of the sender value in the prepareForSegue: to identify which cell was clicked on. Realize we are not making a decision where to go next, thats already happening via the segue, we are only deciding what to take with us. We send some values to the next view controller that is another instance of this current one, but is not this exact one. And the view controller is then stacked in the navcontroller and is now only aware of the sub-array we handed down to it.
This auto drilling can be a little limited because of the way we always drill into the same view controller class type. But! if you want to explore here, I suggest you make another cell prototype with a different identifier and a different segue outlet, and use it at your desired tier.

The "Manual" side takes on calling segues from code and needs the fake UIBarButtonItem to create the loopback segue. This little trick will let you define segue identifiers and then let you decide what direction to take when you call performSegueWithIdentifier: The other important thing is to know what object you want to send along here as the sender. The cell is no longer the sender that is triggering the segue, didSelectRowAtIndexPath is and we could likewise send the UITableViewCell or filter it down to the object we are after. This is the place I test for the data nesting and chose to end the drill down. Calling the different segue will push over to the details view, but the stack of drills downs is still managed by the navcontroller.

Good luck and happy storyboarding.

Tuesday, January 24, 2012

UIStoryboard Power Drill


I've read some good reasons why Storyboards are not ready for prime time. Some of the articles like Jonathan at toxic software.com simply help me find out that I wasn't doing something wrong, it just wasn't meant for that. But there are some benefits from using a storyboard that I wanted to keep, so I got adamant about finding workarounds. Here's one:




Drill down UITableViews - Standard



1. Provided you've started with a subclassed UITableViewController, or a TableView in a UIViewController that is already the root relationship from a UINavigationController.

2. You link the push outlet from the cell prototype back to the table view controller.
This will automatically provide the control to push-in another of the same ViewController when a cell is clicked.


3. Give the loopback segue an identifier string in the attributes inspector.

4. Instead of implementing tableView:didSelectRowAtIndexPath: in the subclassed viewcontroller you will use
prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender

This method will be passed the segue object that is the "tableLoopBack" the sender object for this segue type will be the UITableViewCell.

5. Prepare what data to send to the next instance the the table.


- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    
    NSArray* myObjectArray;
    UITableView* myTable;
    
    if ([segue.identifier isEqualToString:@"tableLoopBack"]) {
        SubClassedTable* nextSct = segue.destinationViewController;
        UITableViewCell* myCell = (UITableViewCell*)sender;
        NSIndexPath* idx = [myTable indexPathForCell:myCell];
        nextSct.myObject = [self.myObjectArray objectAtIndex:idx.row];
    }
}

6. In the TableDataSourceDelegate have it decide how to propagate your tables data array based now on the defined and set value of "myObject". This is very different for everyones data types.

An example, using Core Data and have say AMusicLib > Artists > Albums > Songs > Info

The myObject would ideally be of type NSManagedObject* so that you could be any of the above. When the dataDelegates are called like tableview:numberOfRowsInSection: you would want to implement something like the following to handle the many layers.

- (int)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if ([self.myObject isKindOfClass:[Music class]]) {
        Music* myMusicLib = self.myObject;
        return [myMusicLib.artists count];
    } else if ([self.myObject isKindOfClass:[Artist class]]) {
        Artist* myArtist = self.myObject;
        return [myArtist.albums count];
    } else if ([self.myObject isKindOfClass:[Album class]]) {
        Album* myAlbum = self.myObject;
        return [myAlbum.songs count];
    }
}

Now in the implementation of the tableview:cellForRowAtIndexPath: use a similar kindOf conditional so that the cells properties can feed from the difference of the objects at each layer.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellID = @"musicCell";
    UITableViewCell* newCell = [tableView dequeueReusableCellWithIdentifier:cellID];

    if ([self.myObject isKindOfClass:[Music class]]) {
        Music* myMusicLib = self.myObject;
        Artist* aArtist = [[myMusicLib.artists allObjects] objectAtIndex:indexPath.row];
        newCell.textLabel.text = aArtist.name;
    } else if ([self.myObject isKindOfClass:[Artist class]]) {
        Artist* myArtist = self.myObject;
        Album* aAlbum = [[myArtist.albums allObjects] objectAtIndex:indexPath.row];
        newCell.textLabel.text = aAlbum.title;
        newCell.imageView.image = [UIImage imageNamed:aAlbum.art];
    } else if ([self.myObject isKindOfClass:[Album class]]) {
        Album* myAlbum = self.myObject;
        Song* aSong = [[myAlbum.songs allObjects] objectAtIndex:indexPath.row];
        newCell.textLabel.text = aSong.name;
        NSDate* aTime = [NSDate dateWithTimeIntervalSince1970:[aSong.length intValue]];
        NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
        [dateFormatter setDateFormat:@"m:ss"];
        newCell.detailTextLabel.text = [dateFormatter stringFromDate:aTime];
    }
    return newCell;
}
And finally because earlier we used a property self.myObjectArray, this property could have been a consistent type simple array, but because I walked you through handing down different managedObjects this myObjectArray getter would also need to return an NSArray from the different managedObject's intended NSSet. Same as just above, just return [myArtist.albums allObjects] or [myAlbum.songs allSongs] and so on for the prepareForSegue can hand off the right object to the next controller.


Drill Down TableViews - Options

The above is simple but can be limiting. The segue always pushes down to another of the same table view. Now you want options, lets go this why, or that.

1. Don't use the push outlet on the cell prototype. This is whats limiting the control of where we go.

2. Do Implement the tableView:didSelectRowAtIndexPath: this is where we will decide what segue we take, Or none.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    if (!sec) {
        Section* aSect = [[self.sectionFetchedController fetchedObjects] objectAtIndex:indexPath.row];
        [self performSegueWithIdentifier:@"tableLoopBack" sender:aSect];
    } else {
        Clip* aClip = [self.clipFetchedController objectAtIndexPath:indexPath];
        [[VideoPlayerController Player] setMoviePlayerForClip:aClip];
    }
}

What we are effectively doing here is getting the option to loop into another table, or stay and do something else like load a video in a modal MPMoviePlayerController. Or even call another segue that goes to detail view.

3. Continue to make use of the prepareForSegue method

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"tableLoopBack"]) {
        VideoListController* vlc = segue.destinationViewController;
        Section* mySec = (Section*)sender;
        vlc.title = mySec.title;
        vlc.sec = mySec;
    }
}

This is so data is still passed to the new instance of this controller for drill down purposes, even if not all cells drill.

4. And now the hack, if in step 1 we do not link the cell to push to the this viewController, we still need to define a segue and name it "tableLoopBack".
Add a UIBarButtonItem to the viewController and set the push outlet on it to loopback, identify that new segue connection as "tableLoopBack" and you have access to it when calling performSegue.

5. Add another UIBarButtonItem, and link it to another ViewController like one that layout details level. Identify that segue as another name like "detailSegue" and performSegue from didSelectRowAtIndexPath: where you can pass down the object full of details right into the sender, which will also be handed through the prepareForSegue, so make sure you hand it down again to the desired property of the new destinationViewController.


Helpful? Vote me up: Stackoverflow.com