Monday, April 26, 2010

Sight Words: The Path ahead

The path ahead


With time limitations, this has taken a long time to get to a semi usable state.  I want to actually put this out in the app store, and see how it does (and move forward).   There are still additional steps that need to be done first.


Features required for publication



  1. Add the ability to select all/clear all in the selection lists.
  2. Add the ability to paste words into the word list.
  3. Ensure that I don't have duplicate words added when adding words.  Right now I can get duplicates in easily.
  4. Add the ability to delete words from the word list (multiple delete and swipe to delete ).
  5. Enhance iPad support so that it properly uses a split controller.
  6. Modify the sight word display so that it uses a better card metaphor, and eliminate the toolbar.
  7. Do an aesthetic sweep through the application, try to make it look nicer.
  8. Add the ability to migrate the core data database in the 'automated' fashion.
  9. Add the ability to email words and word lists (copy to clipboard, possibly email).

Features that are not going to make it

  1. Speech, speaking words and recording new words.
  2. Filtering based on categories.
  3. Multiple categories for a list.
  4. Help screens
  5. Reset database
  6. Edit individual words/delete/change
  7. bluetooth list exchange

Thursday, April 22, 2010

Sight Words: Workflow and refactoring

Refactoring and extra functionality

Now that I have added the ability to merge words from multiple lists into my current list, I want the ability to merge words from my current list INTO other lists.   While doing this I'm also going to refractor the current functions so that the word selection stuff is in one place. 

In my previous code I had two routines specifically for the merge from lists functionality, I pull the common code out, and made two new routines that take parameters.  One routine that selects a set of words and then calls another function, and a second routine that selects a set of lists and calls a set of routines.  I also renamed the callback routines so that the workflow is explicit.

A case could be made for factoring this into a separate class, but I don't think it's necessary, all the context information is present in this class, and it's not a lot of code. 

Selecting lists

Note that I now take the label for the next action, and the function to be called as parameters.
- (void) doWordListSelection:(NSString*)nextAction nextFunction:(SEL) action
{
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:nextAction target:self action:action];
self.listSelectDialog=selector;
}

Selecting words

- (void) doWordSelection:(NSString*)nextAction nextFunction:(SEL) action wordSet:(NSSet*) set
{
JLTableContainer *container=
[JLTableContainer createSetController:nil forList:set sortKey:@"wordName"
inContext:[SightWordState Instance].control.managedObjectContext
controlledBy:nil];
UISelectionMaster * selector=[[UISelectionMaster alloc] init];
selector.listToSelect=container;
[self.navigationController pushViewController:selector animated:true];
[selector startSelection];
self.wordSelectDialog=selector;
[selector showToolbar:nextAction target:self action:action];
}

The merge words from multiple lists into the current workflow

Note that I modified the names to violate standard coding standards a bit.  I want to make the purpose and workflow very clear.  I was loosing track of which routine was which, and this way it is explicit:

- (IBAction) copyFromLists1_selectLists:(id) sender
{
[self doWordListSelection:@"Select Lists" nextFunction:@selector(copyFromLists2_selectWords:)];
}
- (IBAction) copyFromLists2_selectWords:(id) sender
{
NSSet *selectedLists=[listSelectDialog currentSelectedItems];
NSMutableSet *words=[[NSMutableSet alloc]init];

for (WordList *list in selectedLists)
{
[words unionSet:list.containedWords];
}
[self doWordSelection:@"Add Words" nextFunction:@selector(copyFromLists3_doCopy:) wordSet:words];

}
// the final action.
- (IBAction) copyFromLists3_doCopy:(id) sender
{
[currentWordList addContainedWords:[wordSelectDialog currentSelectedItems]];
[self.navigationController popToViewController:self animated:true ];
self.wordSelectDialog=nil;
self.listSelectDialog=nil;
[[SightWordState Instance] save];
}
 
 

The merge words from the current list into multiple lists workflow

- (IBAction) mergeToLists1_selectWords:(id) sender
{
[self doWordSelection:@"Merge To Lists" nextFunction:@selector(mergeToLists2_selectLists:) wordSet:currentWordList.containedWords];
}

- (IBAction) mergeToLists2_selectLists:(id) sender
{
[self doWordListSelection:@"Do Merge" nextFunction:@selector(mergeToLists3_doMerge:)]; 
}
 
- (IBAction) mergeToLists3_doMerge:(id) sender
{
for (WordList *wordList in [listSelectDialog currentSelectedItems])
{
[wordList addContainedWords:[wordSelectDialog currentSelectedItems]]; 
}
[self.navigationController popToViewController:self animated:true ];
self.wordSelectDialog=nil;
self.listSelectDialog=nil;
[[SightWordState Instance] save];
}
A few things to note when renaming stuff, the : at the end of the selector is important, and the refactoring of the routines did not pick up the selectors when I did it.   It took a few debugging cycles to get it right.  Refactoring mostly fixed the nibs, but I had to do some reconnections to get everything working correctly.

Tuesday, April 20, 2010

Fetched LIst Controller: Using a predicate on a relationship.

In my previous blog entry I was pretty proud of figuring out the way to select multiple word lists and then build a predicate that found the union of all the words belong to those lists.  It wasn't clear from the documentation and websearches how to do it.   It worked great on the simulator.  However when I put it on the device, it was extremely slow (which is probably why there wasn't great documentation on how to do it).

I ended up just abandoning using a fetched data controller for this information, and just building a complete set that contains all the words I want.  I modified:


JLTableContainer *container=
[JLTableContainer createFetchControlledTable:nil    forEntity:@"WordInformation" forSimpleKey:@"wordName"    inContext: [SightWordState Instance].control.managedObjectContext 
createSectionsBy:nil  controlledBy:nil];
NSArray *selectedLists=[listSelectDialog 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.wordSelectDialog=selector;
[selector showToolbar:@"Add Words" target:self action:@selector(wordsSelected:)];
To:
- (IBAction) listsSelected:(id) sender
{

NSArray *selectedLists=[listSelectDialog currentSelectedItems:@"ListName"];
NSMutableSet *words=[[NSMutableSet alloc]init];

for (WordList *list in selectedLists)
{
[words unionSet:list.containedWords];
}
JLTableContainer *container=
[JLTableContainer createSetController:nil forList:words sortKey:@"wordName"
inContext:[SightWordState Instance].control.managedObjectContext
controlledBy:nil];
UISelectionMaster * selector=[[UISelectionMaster alloc] init];
selector.listToSelect=container;
[self.navigationController pushViewController:selector animated:true];

[selector startSelection];
self.wordSelectDialog=selector;
[selector showToolbar:@"Add Words" target:self action:@selector(wordsSelected:)];

}
I didn't get timing numbers, but there was a perceptibly strong performance improvement for the second set of code.  This is one of the reasons I'm writing a general library, I can change how something is implemented with (hopefully) minimal impact to the remainder of the code.


Sunday, April 18, 2010

Xcode Fun: warning: building for deployment target '3.1.3' requires an armv6 architecture.

I just spent an hour tracking down this error, I was trying to deploy to my iPhone device after testing in the simulator, and upgrading to a universal application.  I found that I was unable to select armv6 from the active architecture window.  I kept getting the error:
warning: building for deployment target '3.1.3' requires an armv6 architecture.
and another error about needing to use 3.2 for an IPAD.

I selected project settings from xcode, and everything was set up.  What I finally figured out is that I had to select the target, and then select 'Get Info' in order to display the dialog for MY TARGET.  From there I found my Architectures was set incorrectly.  I changed it to standard, (armv6 & armv7) and then I was able to deploy to my target device.

The key concept to remember seems to be that the targets is what matters, not the project setting (which is different from Visual Studio).

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.