Sunday, April 18, 2010

Navigation: Fixing toolbars

Toolbars on navigation items


In my previous version of the code, I was loosing my toolbar items when I changed views.  A quick perusal of the documentation reveled that instead of manually configuring the toolbar, I should be setting the toolbarItems property on my class.  (see
http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UINavigationController_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006934-CH3-SW30

So I changed my toolbar from:

- (void) showToolbar:(NSString *) buttonName target:(id) target action:(SEL) action
{
[self.navigationController setToolbarHidden:false];
self.toolbarItems =items;
    UIBarButtonItem *item= [[UIBarButtonItem alloc] initWithTitle:buttonName style:UIBarButtonItemStyleBordered target:target action:action];
NSArray *items = [NSArray arrayWithObjects: item, nil];
self.navigationController.toolbar.items =items;
[item release];
}

So I changed my toolbar to:

- (void) showToolbar:(NSString *) buttonName target:(id) target action:(SEL) action
{
[self.navigationController setToolbarHidden:false];
    UIBarButtonItem *item= [[UIBarButtonItem alloc] initWithTitle:buttonName style:UIBarButtonItemStyleBordered target:target action:action];
NSArray *items = [NSArray arrayWithObjects: item, nil];
self.toolbarItems =items;
[item release];
}

Now it works great, of course now that I still have the first level item on my toolbar, my code crashes when they choose it, because I am maintaining a separate callback for which dialog is active. 


To fix this, instead of having one current dialog variable, I just added two variables listSelectDialog and wordSelectDialog.  Now the code works great.






Saturday, April 17, 2010

List Selection: Creating a chain of events and predicates across relationships.

The previous code can be modified to create a chain of events:

  1. The user is presented with a set of word lists.  They select which ones they want.
  2. The user is then presented with all the words in those word lists.  The select which ones they want.
  3. Finally the user accepts all the words selected and those words are merged to the current word list.

I accomplished all of this in my main list editor, a generic class is used to select the nodes:

Modify selection list to expose selected results

First I modified my UISelectionMaster to expose the currently selected items.  

- (NSArray *) currentSelectedItems:(NSString*) sortKey;



- (NSArray *) currentSelectedItems:(NSString *)sortKey
{
if (self.selectionController==nil)
return nil;
NSArray *returnValue=[self.selectionController.selectedItems getSortedArray:sortKey];
return returnValue;
}

Simple, and I might later modify it to just return a set.

Add first step in the chain

From the previous blog entry, but I've added a member property to contain the currently active dialog.  This way the callback can use it.
- (IBAction) mergeFromLists:(id) sender
{
JLTableContainer *container=
[JLTableContainer createFetchControlledTable:nil
  forEntity:@"WordList"  
forSimpleKey:@"ListName" 
   inContext: [SightWordState Instance].control.managedObjectContext 
createSectionsBy:nil 
controlledBy:nil];
[container updatePredicate:nil];
UISelectionMaster * selector=[[UISelectionMaster alloc] init];
selector.listToSelect=container;
[self.navigationController pushViewController:selector animated:true];
[selector startSelection];
[selector showToolbar:@"Select Lists" target:self action:@selector(listsSelected:)];

self.currentDialog=selector;
}


Feed results of first step into another instance of the same selection 


The tricky part was building my predicate from the previous selected words.  My object model has few elements, but it does have a two way relationship between words and word lists.  It turns out that given a list of ManagedObjects, you can feed that into a predicate against a relationship.   


I'm not sure if this is more or less efficient than iterating over all the words in all the lists and making a new list.  For sight words the performance difference will not be relevant.

- (IBAction) listsSelected:(id) sender
{


JLTableContainer *container=

[JLTableContainer createFetchControlledTable:nil
  forEntity:@"WordInformation" forSimpleKey:@"wordName" 
   inContext: [SightWordState Instance].control.managedObjectContext 
createSectionsBy:nil 
controlledBy:nil];
NSArray *selectedLists=[currentDialog currentSelectedItems:@"ListName"];
NSPredicate *predicate= [NSPredicate predicateWithFormat:@"ANY containedIn in %@",selectedLists ]; 
[container updatePredicate:predicate];
UISelectionMaster * selector=[[UISelectionMaster alloc] init];
selector.listToSelect=container;
[self.navigationController pushViewController:selector animated:true];
[selector startSelection];
self.currentDialog=selector;
[selector showToolbar:@"Add Words" target:self action:@selector(wordsSelected:)];

}



Take the results of the second selection and use them, pop back to the top of the list


Note that the really neat popToViewController allows me to pop multiple layers of dialogs back to the starting one.   Its pretty handy.   Once again I suspect this is not the most efficient way to use core data, however I'm still learning about core data, and for this application it shouldn't matter.

- (IBAction) wordsSelected:(id) sender
{
    for (WordInformation *wordInfo in [currentDialog currentSelectedItems:@"wordName"])
{
[currentWordList addContainedWordsObject:wordInfo];
}
[self.navigationController popToViewController:self animated:true ];
[[SightWordState Instance] save];
}

Left over bugs

The selection still needs some 'select all/clear all' options, as well as potentially extra filters.  In addition the toolbars are a little flaky, when navigating back to a previous option, the toolbar is lost.

Monday, April 12, 2010

Edit Words: Toolbar

Adding a toolbar


The next step is to tie my word list display to the ability to add words.  I'm going to create a toolbar and have an 'add words' button on the toolbar.  I'm planning to reserve the area at the top for a 'delete button'.  This seems in common with the Mail paradigm, so I want to act like other apps.

Step 1: Refactoring my UISelection to allow toggling selection on and off
I plan to migrate my UISelectionMaster to be a 'general purpose' dialog that can display items and invoke actions.  This means that I need to be able to turn selection on and off as well as display toolbar items.  I'm refactoring it so that there are now routines to turn selection on and off:
I added a readonly property and two messages to start and stop selection.  I could have overridden the 'set/get' properties of this, but feel that having a message is a more explicit method of control than setting properties with side effects.

- (void) startSelection;
- (void) stopSelection;
- (bool) isInSelectionMode;



- (void) startSelection
{
if (!isInSelectionMode)
{
self.selectionController=[[JLSelectionController alloc] init];
[selectionController addToContainer:listToSelect];

isInSelectionMode=true;
}
}
- (void) stopSelection{
if (isInSelectionMode)
{
[selectionController removeFromContainer:listToSelect];
isInSelectionMode=false;
  self.selectionController=nil;
}
}



Step 2: Adding the ability to create a simple toolbar.

Just to test the functionality I created a method that will display a toolbar with a single button.  This is certainly not an optimal way to do it.  Note that I allow the target to be passed in, this keeps my control separate from my display.


- (void) showToolbar:(NSString *) buttonName target:(id) target action:(SEL) action;

- (void) showToolbar:(NSString *) buttonName target:(id) target action:(SEL) action
{
[self.navigationController setToolbarHidden:false];
    UIBarButtonItem *item= [[UIBarButtonItem alloc] initWithTitle:buttonName style:UIBarButtonItemStyleBordered target:target action:action];
NSArray *items = [NSArray arrayWithObjects: item, nil];
self.navigationController.toolbar.items =items;
[item release];
}

Step 3:Invoking the toolbar.

Finally I modified my edit words so that it displays the dialog with the toolbar, and has a callback that invokes the next dialog in the sequence when the item is chosen.  Then it has additional callbacks to handle adding words from that dialog.

- (IBAction) editList:(id) sender
{

JLTableContainer *container=
[JLTableContainer createSetController:nil forList:currentWordList.containedWords
sortKey:@"wordName"   inContext:[SightWordState Instance].control.managedObjectContext 
controlledBy:nil];
UISelectionMaster * selector=[[UISelectionMaster alloc] init];
selector.listToSelect=container;
[self.navigationController pushViewController:selector animated:true];
[selector showToolbar:@"Add Words" target:self action:@selector(AddWords:)];
}
- (IBAction) AddWords:(id) sender
{
SelectSuggestedItems *selectItems=[[SelectSuggestedItems alloc] init];
selectItems.coreControl=[SightWordState Instance].control;
selectItems.managedTable=@"WordInformation";
selectItems.searchVariable=@"wordName";
selectItems.delegate=self;
[self.navigationController pushViewController:selectItems animated:YES];
[selectItems release];
}


The reason I'm keeping the control at the higher level is that I can isolate my business logic into a class that is separate from my display logic.  Right now I have everything glommed into one class, I plan to refractor this into a set of smaller helper classes in the future.
Bugs:
This is a rather incomplete API.  When you go to a subview the toolbar stays displayed, but blank, and when you return to the previous item, the toolbar is still blank.

In addition I need to refractor the code so that I can have nicer toolbars than a single button.

The goal of the framework is to have feature rich hierarchy and list management without having to create .NIB files for toolbars and the like.  In addition I want to be able to control the display and behavior easily.  By doing this I can concentrate my GUI design on a smaller set of GUIs, but the basic editing is contained in a set of helper classes.   All of this behavior is VERY similar no matter what your problem domain is, I want to avoid cutting & pasting the same code for behavior everywhere:

Deleting items
Selecting items
Adding items

List Creation: Creating from a set.

Creating a list based on a set



Instead of using a fetched data controller, sometimes it's nice to take a set or are and display the contents of that array.  This is a reference directly supported by the core data, it reveals items 'related' to your current one in a set.   I was already using this for sight word display and editing.

The goal is to expand my list controller so that I can take in a set, and operate on it.  The usage will be:


- (IBAction) editList:(id) sender
{

JLTableContainer *container=
[JLTableContainer createSetController:nil forList:currentWordList.containedWords
  sortKey:@"wordName"   inContext:[SightWordState Instance].control.managedObjectContext 
controlledBy:nil];
UISelectionMaster * selector=[[UISelectionMaster alloc] init];
selector.listToSelect=container;
[self.navigationController pushViewController:selector animated:true];
}

In this case, I'm just creating a table that displays the 'contained words' set.  The rest of my logic is the same, and I can even decorate it with a selector in the same fashion as my FetchedController table display.  

The implementation is fairly simple given the above:

Initializing the container


First I expand a routine that creates the container, and initializes it.  This needs some refactoring with the other API creation routines, but for now I'll leave it alone.
+ (JLTableContainer*) createSetController:(UITableView *)table
forList:(NSSet *) setToUse
  sortKey:(NSString *) key
  inContext:(NSManagedObjectContext *) context
controlledBy:(NSObject<JLTableController>*) controller
{
JLTableContainer *returnValue=[[JLTableContainer alloc]init];
SimpleDescriptionCellProvider *simpleCellProvider=[[SimpleDescriptionCellProvider alloc] init];

returnValue.table=table;
JLSetSource *source=[[JLSetSource alloc] initWithSet:setToUse sortKey:key];
source.fetchContext=context;
JLStandardCallbackHandler *callHandler=[[JLStandardCallbackHandler alloc]init];
returnValue.tableController=controller;
returnValue.cellProvider=simpleCellProvider;
returnValue.tableSource=source;
returnValue.callbackHandler=callHandler;
[returnValue connectObjects];
return [returnValue autorelease];
}


The header file for the data source


This is the definition of a table source that has a 'set' (converted to a sorted array) as a backing store.  Note that it currently does not support reloading.  This will have to be added.
#import "JLTableControl.h"

@interface JLSetSource : NSObject {
id<JLCellProvider> cellProvider;
NSSet *set;
UITableView *table;
NSArray *currentList;
NSManagedObjectContext *fetchContext;
}
@property (retain,nonatomic) NSSet *set;
@property (retain,nonatomic) NSManagedObjectContext *fetchContext;
@property (retain,nonatomic) NSArray *currentList;
- (JLSetSource *) initWithSet:(NSSet*)set sortKey:(NSString*) sort;

@end


The source for the above




@implementation JLSetSource
@synthesize set;
@synthesize table;
@synthesize cellProvider;
@synthesize currentList;
@synthesize fetchContext;
- (JLSetSource*) initWithSet:(NSSet*)setToUse sortKey:(NSString*) sort
{
self.set=setToUse;
self.currentList=[setToUse getSortedArray:sort];
return self;  
 
}

// Standard section headers.
//- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
//    return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles];
//}
// Get the cell for the current object.  Default to a simple description cell if no actual cell provider is created.
- (UITableViewCell *)tableView:(UITableView *)tableViewb cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (self.cellProvider==nil)
self.cellProvider=[[SimpleDescriptionCellProvider alloc] initWithIdentifier:@"GenericQueue"];
NSManagedObject *managedObject=[self.currentList objectAtIndex:indexPath.row];
if (managedObject !=nil)
{
  UITableViewCell *returnValue=[cellProvider cellForObject:managedObject atIndexPath:indexPath forTable:tableViewb];
return returnValue;
}
return nil;
}

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

}

- (id) objectAtIndexPath:(NSIndexPath *) path
{
return [self.currentList objectAtIndex:path.row];
}

// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.currentList count];
}

All in all its pretty straightforward stuff, but it's nice to have it isolated and implemented once.

Monday, April 5, 2010

Selection Controller: Decorating with checkmarks

The first stage was to create a selection controller which allows any list to be decorated with selections.  In this case I extended my previous table list so that it can decorate one of my tables and intercept the selections and modify the cells.  The goal is that I can easily add selection to any list.

Usage:
The following method will create a unique selector for a particular table, in this case I'm creating a new table from scratch, however this should work when changing to 'select mode' on another list (such as multiple delete).

Right now I'm just using the Checkmark accessory to select items.  Eventually I want to add custom images on the left (such as the mail app on the iPhone).


JLTableContainer *container=
[JLTableContainer createFetchControlledTable:nil 
  forEntity:@"WordList"
    forSimpleKey:@"ListName"
  inContext:
[SightWordState Instance].control.managedObjectContext 
createSectionsBy:nil 
controlledBy:nil];
[container updatePredicate:nil];

Now after creating the list, add the selector.
UISelectionMaster * selector=[[UISelectionMaster alloc] init];
selector.listToSelect=container;
[self.navigationController pushViewController:selector animated:true];
Now the selector takes one of my JLTableContainer objects and decorates it with selection items:

@interface JLSelectionController : NSObject {
id<JLTableController> originalController;
id<JLCellProvider> originalCellProvider;
NSMutableSet *selectedItems;
UITableView *tableView;
}
@property (nonatomic,retain) id originalController;
@property (nonatomic,retain) id originalCellProvider;
@property (nonatomic,retain) NSMutableSet *selectedItems;
@property (nonatomic,retain) UITableView *tableView;
- (void) toggleObject:(id) objToToggle;
- (void) addToContainer:(JLTableContainer*) container;
- (void) removeFromContainer:(JLTableContainer*) container;

Here is the implementation.  What this is doing is creating a new level of controller for the strategy pattern keeping track of the old pattern.  In addition it creates a new cellForObject method, which takes the lower level one and changes the accessory type.  (I want to change it to use an icon instead).

@implementation JLSelectionController
@synthesize originalController;
@synthesize originalCellProvider;
@synthesize selectedItems;
@synthesize tableView;
- (UITableViewCell *)cellForObject:(id) object  atIndexPath:(NSIndexPath *)indexPath forTable:(UITableView*) table
{
tableView=table;
// Delegate to previous provider.
UITableViewCell *returnCell=[originalCellProvider 
cellForObject:object 
atIndexPath:indexPath forTable:table] ;
// Check it for now, later I'd like to make a custom image and put it at the left (ala mail).
if ([selectedItems  containsObject:object])
{
returnCell.accessoryType=UITableViewCellAccessoryCheckmark;
}
else {
returnCell.accessoryType=UITableViewCellAccessoryNone;
}

return returnCell;

}

- (void) rowSelected:(id) selectedObject
{
[self toggleObject:selectedObject];
}
- (void) acessorySelected:(id) selectedObject
{
[self toggleObject:selectedObject];
}
- (void) toggleObject:(id) objToToggle
{
if (selectedItems==nil)
selectedItems=[[NSMutableSet alloc]init];
if ([selectedItems containsObject:objToToggle])
{
[selectedItems removeObject:objToToggle];
}
else {
[selectedItems addObject:objToToggle];
}
if (tableView!=nil)
{
[tableView reloadData];
}

}
- (void) dealloc
{
[super dealloc];
[self.selectedItems release];
}
- (void) addToContainer:(JLTableContainer*) container
{
self.originalController=container.tableController;
container.tableController=self;
self.originalCellProvider=container.cellProvider;
container.cellProvider=self;
[container connectObjects];
container.table =container.table;
[container.table reloadData];
}
- (void) removeFromContainer:(JLTableContainer*) container
{
container.tableController=self.originalController;
container.cellProvider=self.originalCellProvider;
[container.table reloadData];

}

I have this working for one case.  The next step will be to extend eventing so that choices can be made on toolbars and navigation bars for the items and passing the results on in a workflow.

In addition I suspect the reload data for the table is a performance problem, I did it when I was having trouble getting the checkmarks to update.