In the previous lesson, you added rudimentary synchronization that was performed on the main thread. Blocking the main thread in such a way is not recommended. In this lesson you move the synchronization to a background thread, and add a synchronization observing method to update a progress display.
To show the progress of the synchronization, you use a progress bar with a label that is placed in the bottom navigation toolbar, similar to the look of the Mail application when downloading new messages. To recreate this look, you must create a custom view:
Control-click the Classes folder in the Groups & Files pane. From the Add menu click New File.
Under Cocoa Touch Class, click UIViewController subclass.
Clear UITableViewController subclass.
Click With XIB for user interface.
Click Next.
Name the file: ProgressToolbarViewController.m and place it in the Classes subdirectory. Click the box to have a header file created.
Click Finish.
Move the ProgressToolbarViewController.xib file to the Resources directory.
Before creating the layout in Interface Builder, replace the interface definition with the following in ProgressToolbarViewController:
@interface ProgressToolbarViewController : UIViewController { IBOutlet UILabel *label; IBOutlet UIProgressView *progressBar; } @end |
Add the properties in the interface:
@property (readonly) IBOutlet UILabel *label; @property (readonly) IBOutlet UIProgressView *progressBar; |
Synthesize the properties in the implementation file:
@synthesize label; @synthesize progressBar; |
Add a release call in the dealloc method:
- (void)dealloc { [super dealloc]; [label release]; [progressBar release]; } |
Open the ProgressToolbarViewController by double-clicking ProgressToolbarViewController.xib in the Xcode Resources folder. Since this view is displayed in a toolbar, you must size it appropriately, and set its background properties:
In the Document window (Command-0), click View.
In the Attributes Inspector (Command-1), set the simulated status bar to Unspecified.
Click Background and set Background opacity to 0%.
Clear the Opaque setting.
In the Size Inspector (Command-3), set the width to 232 and the height to 44.
Click and drag a UIProgressView from the Library to the View.
In the Size Inspector (Command-3), set the position of the progress view to 26, 29.
Set the width of the progress view to 186.
In the Attributes Inspector (Command-1), set the style to Bar and the progress to zero.
Click and drag a UILabel from the Library to the View.
In the Size Inspector (Command-3), set the position of the label to 14, 5.
Set the size of the label to 210, 16.
In the Attribute Inspector (Command-1), set the text to Sync Progress.
Set the layout alignment to center.
Set the font to Helvetica Bold, size 12.
Set the text color to white.
Set the shadow color to RGB: (103, 114, 130), with 100% opacity.
Click File's Owner in the Document window.
In the Connectors Inspector (Command-2), link the label outlet to the UILabel you created in the last steps.
Link the progress bar outlet to the UIProgressView you created in the last steps.
Save the XIB file and close Interface Builder.
The ProgressBar view is added to the RootViewController's toolbar. However, the DataAccess object also uses a reference to it to display the progress of the synchronization. Add the following instance variable to the interface:
ProgressToolbarViewController * progressToolbar; |
and add the property to the DataAccess class:
@property (retain, readwrite) IBOutlet ProgressToolbarViewController * progressToolbar; |
Import the ProgressToolbarViewController header into the DataAccess header:
#import "ProgressToolbarViewController.h" |
Synthesize the property in the implementation.
@synthesize progressToolbar |
To add the progress view to the toolbar, add the following to the RootViewController's viewDidLoad method:
// Create progress display ProgressToolbarViewController * progress = [[ProgressToolbarViewController alloc] initWithNibName:@"ProgressToolbarViewController" bundle:nil]; // Register the toolbar with the DataAccess [[DataAccess sharedInstance] setProgressToolbar:progress]; // Setup UIBarButtonItems UIBarButtonItem * space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; UIBarButtonItem * progressButtonItem = [[UIBarButtonItem alloc] initWithCustomView:progress.view]; // Put them in the toolbar self.toolbarItems = [NSArray arrayWithObjects:space, progressButtonItem, space, nil]; [space release]; [progressButtonItem release]; |
The RootViewController now has a progress view in its toolbar, even though the toolbar is hidden. In the next section, you move synchronization to a background thread and display the progress toolbar during the synchronization.
So far, everything the application does is performed on the main thread of the application. Since this is also the thread used to draw the interface and process user event such as touches, blocking it is not recommended.
The flow of the application during the sync is as follows:
The user clicks the sync button in RootViewController. The toolbar progress view appears.
RootViewController initiates the sync on a detached thread, passing itself as an argument to the synchronization function. The main thread continues with the sync running in another thread.
The synchronization function updates the user interface using performSelectorOnMainThread to update the progress display throughout the synchronization.
When synchronization is complete, the background thread displays an alert box showing the result of the sync, with RootViewController set as the delegate to the alert so that it knows when the alert is dismissed.
Once RootViewController is told the alert box is dismissed, the toolbar progress view is hidden.
Since the RootViewController must implement the UIAlertViewDelegate protocol to handle the dismissing of the alert view, change its header file to the following:
@interface RootViewController : UITableViewController <UIAlertViewDelegate> { } // Displays the screen to add a name. - (void)showAddNameScreen; @end |
The only method from the UIAlertViewDelegate protocol that the RootViewController needs to implement is the alertView:clickedButtonAtIndex: method. This method is called when the user presses a button of the UIAlertView. Since the view only has a single button, you don't need to inspect which button was pressed. Put the following code in the RootViewController.m file.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { NSLog(@"User dismissed sync alert, refreshing table."); [self.tableView reloadData]; [self.navigationController setToolbarHidden:YES animated: YES]; } |
Also in RootViewController.m, change the sync method to do the synchronization call on a separate thread by using detachNewThreadSelector:
- (void)sync { [self.navigationController setToolbarHidden:NO animated: YES]; [NSThread detachNewThreadSelector:@selector(synchronize:) toTarget:[DataAccess sharedInstance] withObject:self]; } |
Now that the synchronization is on a separate thread, you need to change a few things. As you might have noticed in the RootViewController's sync method, DataAccess' synchronize method is being passed a parameter. Change its signature to the following:
- (void)synchronize:(id<UIAlertViewDelegate>)sender; |
Update the DataAccess.mm implementation file with the following:
// Since the synchronization method uses auto-released object instances, you also need // to create an NSAutoreleasePool and release it at the end of the synchronization: - (void)synchronize:(id<UIAlertViewDelegate>)sender { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; // Rest of synchronize method... [pool release]; } |
Still in the synchronize method, you also need to set the passed-in RootViewController as the alert delegate. To do this, update the previously create UIAlertView and change the delegate from nil to sender:
// Set RootViewController as the alert delegate if (sender != nil) { NSLog(@"Showing Alert with sync result."); [[[[UIAlertView alloc] initWithTitle:@"Synchronization" message:result delegate:sender // changed from nil to sender cancelButtonTitle:nil otherButtonTitles:@"OK", nil] autorelease] show]; } else { NSLog(@"Not showing alert since sender was nil."); } |
You now need to create the synchronization callback that updates the progress bar to the appropriate completion and display the correct message.
However, since the synchronization callback is executed on a non-main thread (the synchronization thread created by the RootViewController) you must use performSelectorOnMainThread to update the user interface. This method only allows the passing of a single parameter, so in order to pass the progress, as well as the message, create a Objective-C class named UpdateInfo with two properties, a float, and an NSString in the header file:
@interface UpdateInfo : NSObject { NSString * message; float progress; } // Message to display in the progress view @property (readonly) NSString * message; // Progress of the sync [0.0-1.0] @property (readonly) float progress; // Preferred initializer - (id)initWithMessage:(NSString*) message andProgress:(float)progress; @end |
Add the following to the implementation file:
// The implementation is a single constructor that takes both parameters: @implementation UpdateInfo @synthesize message; @synthesize progress; - (id)initWithMessage:(NSString*) msg andProgress:(float) syncProgress { if (self = [super init]) { message = msg; progress = syncProgress; } return self; } @end |
Now that you can bundle all the information needed by the user interface updating method, you can define it in the DataAccess class:
- (void)updateSyncProgress:(UpdateInfo *)info { progressToolbar.label.text = info.message; progressToolbar.progressBar.progress = info.progress; } |
Now import UpdateInfo.h in DataAccess.h.
#import "UpdateInfo.h" |
You can also define the callback method in the DataAccess.mm implementation file along with the other static methods:
static void UL_CALLBACK_FN progressCallback(ul_synch_status * status) { // Sync information for the GUI float percentDone = 0.0; NSString * message; // Note: percentDone is approximate. switch (status->state) { case UL_SYNCH_STATE_STARTING: percentDone = 0; message = @"Starting Sync"; break; case UL_SYNCH_STATE_CONNECTING: percentDone = 5; message = @"Connecting to Server"; break; case UL_SYNCH_STATE_SENDING_HEADER: percentDone = 10; message = @"Sending Sync Header"; break; case UL_SYNCH_STATE_SENDING_TABLE: percentDone = 10 + 25 * (status->sync_table_index / status->sync_table_count); message = [NSString stringWithFormat:@"Sending Table %s: %d of %d", status->table_name, status->sync_table_index, status->sync_table_count]; break; case UL_SYNCH_STATE_SENDING_DATA: percentDone = 10 + 25 * (status->sync_table_index / status->sync_table_count); message = @"Sending Name Changes"; break; case UL_SYNCH_STATE_FINISHING_UPLOAD: case UL_SYNCH_STATE_RECEIVING_UPLOAD_ACK: percentDone = 50; message = @"Finishing Upload"; break; case UL_SYNCH_STATE_RECEIVING_TABLE: case UL_SYNCH_STATE_RECEIVING_DATA: percentDone = 50 + 25 * (status->sync_table_index / status->sync_table_count); message = [NSString stringWithFormat:@"Receiving Table %s: %d of %d", status->table_name, status->sync_table_index, status->sync_table_count]; break; case UL_SYNCH_STATE_COMMITTING_DOWNLOAD: case UL_SYNCH_STATE_SENDING_DOWNLOAD_ACK: percentDone = 80; message = @"Committing Downloaded Updates"; break; case UL_SYNCH_STATE_DISCONNECTING: percentDone = 90; message = @"Disconnecting from Server"; break; case UL_SYNCH_STATE_DONE: percentDone = 100; message = @"Finished Sync"; break; case UL_SYNCH_STATE_ERROR: percentDone = 95; message = @"Error During Sync"; break; case UL_SYNCH_STATE_ROLLING_BACK_DOWNLOAD: percentDone = 100; message = @"Rolling Back due to Error"; break; default: percentDone = 100; NSLog(@"Unknown sync state: '%d'", status->state); break; } // Wrap the GUI info in an object and have the main thread update UpdateInfo * info = [[[UpdateInfo alloc] initWithMessage:message andProgress:percentDone / 100] autorelease]; [[DataAccess sharedInstance] performSelectorOnMainThread:@selector(updateSyncProgress:) withObject:info waitUntilDone:YES]; } |
With everything in place, you can now set the sync observer to the callback method in the synchronize method in the DataAccess.mm file:
// Set the sync parameters info.user_name = (char*)"user"; // Set to your username info.password = (char*)"password"; // Set to your password info.version = (char*)"NamesModel"; info.stream = "tcpip"; info.stream_parms = (char*)"host=localhost"; info.observer = progressCallback; // Add this line |
Discuss this page in DocCommentXchange.
|
Copyright © 2012, iAnywhere Solutions, Inc. - SQL Anywhere 12.0.1 |