From 38f6a6a15eef8127c4dd3b2c26c500770cffbc54 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Sun, 31 Jul 2011 17:32:02 -0700 Subject: [PATCH] Added CouchLiveQuery CouchLiveQuery's .rows property is a live result set -- the instance watches for changes to the database, re-runs the query, and posts KVO notifications to .rows when the results change. Updated the demo apps (particularly DemoQuery) to use this, which simplifies their code. --- Couch/CouchQuery.h | 27 ++++++++-- Couch/CouchQuery.m | 109 ++++++++++++++++++++++++++++++++++++++- Couch/CouchRevision.h | 1 - Demo/DemoAppController.m | 42 +++++---------- Demo/DemoQuery.h | 6 +-- Demo/DemoQuery.m | 38 +++++--------- README.md | 2 + 7 files changed, 162 insertions(+), 63 deletions(-) diff --git a/Couch/CouchQuery.h b/Couch/CouchQuery.h index 6fc40f7..e52698f 100644 --- a/Couch/CouchQuery.h +++ b/Couch/CouchQuery.h @@ -14,11 +14,8 @@ // and limitations under the License. #import "CouchResource.h" -@class CouchDatabase; -@class CouchDocument; -@class CouchDesignDocument; -@class CouchQueryEnumerator; -@class CouchQueryRow; +@class CouchDatabase, CouchDocument, CouchDesignDocument; +@class CouchLiveQuery, CouchQueryEnumerator, CouchQueryRow; /** Represents a CouchDB 'view', or a view-like resource like _all_documents. */ @@ -72,6 +69,26 @@ /** Same as -rows, except returns nil if the query results have not changed since the last time it was evaluated (Synchronous). */ - (CouchQueryEnumerator*) rowsIfChanged; + +/** Returns a live query with the same parameters. */ +- (CouchLiveQuery*) asLiveQuery; + +@end + + +/** A CouchQuery subclass that automatically refreshes the result rows every time the database changes. + All you need to do is watch for changes to the .rows property. */ +@interface CouchLiveQuery : CouchQuery +{ + @private + BOOL _observing; + RESTOperation* _op; + CouchQueryEnumerator* _rows; +} + +/** In CouchLiveQuery the -rows accessor is now a non-blocking property that can be observed using KVO. Its value will be nil until the initial query finishes. */ +@property (readonly, retain) CouchQueryEnumerator* rows; + @end diff --git a/Couch/CouchQuery.m b/Couch/CouchQuery.m index 5bc7874..7c61dd2 100644 --- a/Couch/CouchQuery.m +++ b/Couch/CouchQuery.m @@ -35,6 +35,22 @@ - (id) initWithQuery: (CouchQuery*)query result: (id)result; @implementation CouchQuery +- (id) initWithQuery: (CouchQuery*)query { + self = [super initWithParent: query.parent relativePath: query.relativePath]; + if (self) { + _limit = query.limit; + _skip = query.skip; + self.startKey = query.startKey; + self.endKey = query.endKey; + _descending = query.descending; + _prefetch = query.prefetch; + self.keys = query.keys; + _groupLevel = query.groupLevel; + } + return self; +} + + @synthesize limit=_limit, skip=_skip, descending=_descending, startKey=_startKey, endKey=_endKey, prefetch=_prefetch, keys=_keys, groupLevel=_groupLevel; @@ -116,6 +132,11 @@ - (NSError*) operation: (RESTOperation*)op willCompleteWithError: (NSError*)erro } +- (CouchLiveQuery*) asLiveQuery { + return [[[CouchLiveQuery alloc] initWithQuery: self] autorelease]; +} + + @end @@ -154,6 +175,82 @@ - (NSDictionary*) jsonToPost { +@implementation CouchLiveQuery + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver: self]; + [_op release]; + [super dealloc]; +} + + +- (CouchQueryEnumerator*) rows { + if (!_observing) + [self start]; + return _rows; +} + + +- (void) setRows:(CouchQueryEnumerator *)rows { + [_rows autorelease]; + _rows = [rows retain]; +} + + +- (RESTOperation*) start { + if (!_op) { + if (!_observing) { + _observing = YES; + self.database.tracksChanges = YES; + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(databaseChanged) + name: kCouchDatabaseChangeNotification + object: self.database]; + } + NSLog(@"CouchLiveQuery: Starting..."); + _op = [[super start] retain]; + [_op start]; + } + return _op; +} + + +- (void) updateRows { + if (_op) + return; // TODO: Should probably re-run query after current _op completes, instead + if (_rows) + self.prefetch = NO; // (prefetch disables conditional GET shortcut) + [self start]; +} + + +- (void) databaseChanged { + [self updateRows]; +} + + +- (NSError*) operation: (RESTOperation*)op willCompleteWithError: (NSError*)error { + error = [super operation: op willCompleteWithError: error]; + + if (op == _op) { + NSLog(@"CouchLiveQuery: ...Finished (status=%i)", op.httpStatus); + [_op release]; + _op = nil; + CouchQueryEnumerator* rows = op.resultObject; + if (rows && ![rows isEqual: _rows]) { + NSLog(@"CouchLiveQuery: ...Rows changed! (now %lu)", (unsigned long)rows.count); + self.rows = rows; // Triggers KVO notification + } + } + + return error; +} + + +@end + + + @implementation CouchQueryEnumerator @@ -166,7 +263,7 @@ - (id) initWithQuery: (CouchQuery*)query op: (RESTOperation*)op { if (self) { NSDictionary* result = $castIf(NSDictionary, op.responseBody.fromJSON); _query = [query retain]; - _rows = [$castIf(NSArray, [result objectForKey: @"rows"]) retain]; // BLOCKING + _rows = [$castIf(NSArray, [result objectForKey: @"rows"]) retain]; if (!_rows) { [self release]; return nil; @@ -186,6 +283,16 @@ - (void) dealloc } +- (BOOL) isEqual:(id)object { + if (object == self) + return YES; + if (![object isKindOfClass: [CouchQueryEnumerator class]]) + return NO; + CouchQueryEnumerator* otherEnum = object; + return [otherEnum->_rows isEqual: _rows]; +} + + - (NSUInteger) count { return _rows.count; } diff --git a/Couch/CouchRevision.h b/Couch/CouchRevision.h index e72f1af..cc6e66c 100644 --- a/Couch/CouchRevision.h +++ b/Couch/CouchRevision.h @@ -20,7 +20,6 @@ @interface CouchRevision : CouchResource { @private - NSDictionary* _contents; NSDictionary* _properties; BOOL _isDeleted; } diff --git a/Demo/DemoAppController.m b/Demo/DemoAppController.m index 67203ed..c097ec4 100644 --- a/Demo/DemoAppController.m +++ b/Demo/DemoAppController.m @@ -52,11 +52,9 @@ - (void) applicationDidFinishLaunching: (NSNotification*)n { NSAssert(op.error.code == 412, @"Error creating db: %@", op.error); } - self.query = [[[DemoQuery alloc] initWithQuery: [_database getAllDocuments]] autorelease]; - - [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(databaseChanged:) - name: kCouchDatabaseChangeNotification - object: _database]; + CouchQuery* q = [_database getAllDocuments]; + q.descending = YES; + self.query = [[[DemoQuery alloc] initWithQuery: q] autorelease]; // Enable continuous sync: NSString* otherDbURL = [bundleInfo objectForKey: @"SyncDatabaseURL"]; @@ -85,29 +83,9 @@ - (void) startContinuousSyncWith: (NSURL*)otherDbURL { #pragma mark HIGHLIGHTING NEW ITEMS: -- (void) databaseChanged: (NSNotification*)n { - if (!_glowing) { - // Wait to redraw the table, else there is a race condition where if the - // DemoItem gets notified after I do, it won't have updated timeSinceExternallyChanged yet. - _glowing = YES; - [self performSelector: @selector(updateTableGlows) withObject: nil afterDelay:0.0]; - } -} - - - (void) updateTableGlows { - BOOL glowing = NO; - for (DemoItem* item in _tableController.arrangedObjects) { - if (item.timeSinceExternallyChanged < kChangeGlowDuration) { - glowing = YES; - break; - } - } - if (glowing || _glowing) - [_table setNeedsDisplay: YES]; - _glowing = glowing; - if (glowing) - [self performSelector: @selector(updateTableGlows) withObject: nil afterDelay: 0.1]; + _glowing = NO; + [_table setNeedsDisplay: YES]; } @@ -118,7 +96,10 @@ - (void)tableView:(NSTableView *)tableView { NSColor* bg = nil; - DemoItem* item = [_tableController.arrangedObjects objectAtIndex: row]; + NSArray* items = _tableController.arrangedObjects; + if (row >= items.count) + return; // Don't know why I get called on illegal rows, but it happens... + DemoItem* item = [items objectAtIndex: row]; NSTimeInterval changedFor = item.timeSinceExternallyChanged; if (changedFor > 0 && changedFor < kChangeGlowDuration) { float fraction = 1.0 - changedFor / kChangeGlowDuration; @@ -127,6 +108,11 @@ - (void)tableView:(NSTableView *)tableView ofColor: [NSColor yellowColor]]; else bg = [[NSColor yellowColor] colorWithAlphaComponent: fraction]; + + if (!_glowing) { + _glowing = YES; + [self performSelector: @selector(updateTableGlows) withObject: nil afterDelay: 0.1]; + } } [cell setBackgroundColor: bg]; diff --git a/Demo/DemoQuery.h b/Demo/DemoQuery.h index 42d5719..c1be106 100644 --- a/Demo/DemoQuery.h +++ b/Demo/DemoQuery.h @@ -14,7 +14,7 @@ // and limitations under the License. #import -@class CouchQuery, RESTOperation; +@class CouchQuery, CouchLiveQuery, RESTOperation; /** Simple controller for CouchDB demo apps. @@ -23,7 +23,7 @@ without needing any code. */ @interface DemoQuery : NSObject { - CouchQuery* _query; + CouchLiveQuery* _query; RESTOperation* _op; NSMutableArray* _entries; Class _modelClass; @@ -34,8 +34,6 @@ /** Class to instantiate for entries. Defaults to DemoItem. */ @property (assign) Class modelClass; -- (void) updateEntries; - /** The documents returned by the query, wrapped in DemoItem objects. An NSArrayController can be bound to this property. */ //@property (readonly) NSMutableArray* entries; diff --git a/Demo/DemoQuery.m b/Demo/DemoQuery.m index f99ffad..c98a7a3 100644 --- a/Demo/DemoQuery.m +++ b/Demo/DemoQuery.m @@ -33,17 +33,13 @@ - (id) initWithQuery: (CouchQuery*)query self = [super init]; if (self != nil) { _modelClass = [DemoItem class]; - _query = [query retain]; + _query = [[query asLiveQuery] retain]; _query.prefetch = YES; // for efficiency, include docs on first load - [self loadEntriesFrom: [_query rows]]; - - // Listen for external changes: - _query.database.tracksChanges = YES; - [[NSNotificationCenter defaultCenter] addObserver: self - selector: @selector(updateEntries) - name: kCouchDatabaseChangeNotification - object: nil]; + [_query start]; + + // Observe changes to _query.rows: + [_query addObserver: self forKeyPath: @"rows" options: 0 context: NULL]; } return self; } @@ -51,9 +47,8 @@ - (id) initWithQuery: (CouchQuery*)query - (void) dealloc { - [[NSNotificationCenter defaultCenter] removeObserver: self]; - [_op cancel]; [_entries release]; + [_query removeObserver: self forKeyPath: @"rows"]; [_query release]; [super dealloc]; } @@ -76,7 +71,8 @@ - (void) loadEntriesFrom: (CouchQueryEnumerator*)rows { } if (![entries isEqual:_entries]) { - NSLog(@" ...entries changed!"); + NSLog(@" ...entries changed! (was %u, now %u)", + (unsigned)_entries.count, (unsigned)entries.count); [self willChangeValueForKey: @"entries"]; [_entries release]; _entries = [entries mutableCopy]; @@ -85,18 +81,12 @@ - (void) loadEntriesFrom: (CouchQueryEnumerator*)rows { } -- (void) updateEntries { - if (_op) - return; - NSLog(@"Updating the query..."); - _query.prefetch = NO; // prefetch disables rowsIfChanged optimization - _op = [_query start]; - [_op onCompletion: ^{ - CouchQueryEnumerator* rows = _op.resultObject; - _op = nil; - if (rows) - [self loadEntriesFrom: rows]; - }]; +- (void)observeValueForKeyPath: (NSString*)keyPath ofObject: (id)object + change: (NSDictionary*)change context: (void*)context +{ + if (object == _query) { + [self loadEntriesFrom: _query.rows]; + } } diff --git a/README.md b/README.md index 51bd149..62d7816 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ There are two simple Mac demo apps included in the Demo/ subfolder. One lets you ### Building The Framework +(You only need to do this if you checked out the CouchCocoa source code and want to build it yourself. If you downloaded a precompiled framework, just go onto the next section.) + 1. Open CouchDemo.xcodeproj 2. Select "Mac Framework" or "iOS Framework" from the scheme pop-up in the toolbar 3. Product > Build