Permalink
Browse files

CBLUITableSource cleanup. Added CBLUICollectionSource.

CBLUICollectionSource written by Ewan Mcdougall. It's not compiled into
the framework by default, to keep the code size down, but apps can
compile it in. (It is included in the iOS demo app, to ensure that it
keeps compiling.)
  • Loading branch information...
1 parent d7682a9 commit 982c808baea6366a5d499516d4617f699fe3ffb6 @snej snej committed Feb 15, 2013
View
6 CouchbaseLite.xcodeproj/project.pbxproj
@@ -285,6 +285,7 @@
27C706D41488679500F0F099 /* DemoQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = 27C706CE1488679500F0F099 /* DemoQuery.m */; };
27C706D61488679500F0F099 /* ShoppingDemo.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27C706D01488679500F0F099 /* ShoppingDemo.xib */; };
27C706D71488679500F0F099 /* ShoppingItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 27C706D21488679500F0F099 /* ShoppingItem.m */; };
+ 27CB056316CDD1ED0040C9F8 /* CBLUICollectionSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CB056216CDD1ED0040C9F8 /* CBLUICollectionSource.m */; };
27CF5D2D152F514A0015D7A9 /* CBLStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CF5D2C152F514A0015D7A9 /* CBLStatus.m */; };
27CF5D2E152F514A0015D7A9 /* CBLStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 27CF5D2C152F514A0015D7A9 /* CBLStatus.m */; };
27D895CB14E4EF9E00AC701E /* Entitlements.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2714CF511496AE5B00E03341 /* Entitlements.plist */; };
@@ -757,6 +758,8 @@
27C706D01488679500F0F099 /* ShoppingDemo.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShoppingDemo.xib; sourceTree = "<group>"; };
27C706D11488679500F0F099 /* ShoppingItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShoppingItem.h; sourceTree = "<group>"; };
27C706D21488679500F0F099 /* ShoppingItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShoppingItem.m; sourceTree = "<group>"; };
+ 27CB056216CDD1ED0040C9F8 /* CBLUICollectionSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBLUICollectionSource.m; sourceTree = "<group>"; };
+ 27CB056416CDD1FB0040C9F8 /* CBLUICollectionSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CBLUICollectionSource.h; sourceTree = "<group>"; };
27CF5D2C152F514A0015D7A9 /* CBLStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CBLStatus.m; sourceTree = "<group>"; };
27D90B1615601C2F0000735E /* MYErrorUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MYErrorUtils.h; sourceTree = "<group>"; };
27D90B1715601C2F0000735E /* MYErrorUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MYErrorUtils.m; sourceTree = "<group>"; };
@@ -1438,6 +1441,8 @@
children = (
27DA4317158FD9B400F9E7B5 /* CBLUITableSource.h */,
27DA4318158FD9B400F9E7B5 /* CBLUITableSource.m */,
+ 27CB056416CDD1FB0040C9F8 /* CBLUICollectionSource.h */,
+ 27CB056216CDD1ED0040C9F8 /* CBLUICollectionSource.m */,
);
name = iOS;
sourceTree = "<group>";
@@ -2304,6 +2309,7 @@
27B0B8571492D0AA00A817AD /* CBLReplicator_Tests.m in Sources */,
27C5308414E09E2B0078F886 /* CBLBlobStore_Tests.m in Sources */,
27E7FAEC155D78C20025F93A /* CBLChangeTracker_Tests.m in Sources */,
+ 27CB056316CDD1ED0040C9F8 /* CBLUICollectionSource.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
View
3 Demo-Mac/DemoQuery.h
@@ -14,7 +14,7 @@
// and limitations under the License.
#import <Cocoa/Cocoa.h>
-@class CBLQuery, CBLLiveQuery, RESTOperation;
+@class CBLQuery, CBLLiveQuery;
/** Simple controller for CouchbaseLite demo apps.
@@ -24,7 +24,6 @@
@interface DemoQuery : NSObject
{
CBLLiveQuery* _query;
- RESTOperation* _op;
NSMutableArray* _entries;
Class _modelClass;
}
View
2 Demo-iOS/RootViewController.m
@@ -211,7 +211,7 @@ - (IBAction)deleteCheckedItems:(id)sender {
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0)
return;
- [dataSource deleteDocuments: self.checkedDocuments];
+ [dataSource deleteDocuments: self.checkedDocuments error: NULL];
}
View
96 Source/API/CBLUICollectionSource.h
@@ -0,0 +1,96 @@
+//
+// CBLUICollectionSource.h
+//
+// Based on CBLUITableSource
+// CouchbaseLite
+//
+// Created by Ewan Mcdougall mrloop.com on 06/02/2013.
+// Copyright (c) 2013 Ewan Mcdougall. All rights reserved.
+//
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+// except in compliance with the License. You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software distributed under the
+// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+// either express or implied. See the License for the specific language governing permissions
+// and limitations under the License.
+
+#import <UIKit/UIKit.h>
+@class CBLDocument, CBLLiveQuery, CBLQueryRow, RESTOperation;
+
+/** A UICollectionView data source driven by a CBLLiveQuery.
+ It populates the collection view from the query rows, and automatically updates the collection
+ as the query results change when the database is updated.
+ A CBLUICollectionSource can be created in a nib. If so, its collectionView outlet should be
+ wired up to the UICollectionView it manages, and the collection view's dataSource outlet should
+ be wired to it. */
+@interface CBLUICollectionSource : NSObject <UICollectionViewDataSource
+#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000)
+ , UIDataSourceModelAssociation
+#endif
+>
+@property (nonatomic, retain) IBOutlet UICollectionView* collectionView;
+
+@property (retain) CBLLiveQuery* query;
+
+/** Rebuilds the collection from the query's current .rows property. */
+-(void) reloadFromQuery;
+
+#pragma mark Row Accessors:
+
+/** The current array of CBLQueryRows being used as the data source for the collection. */
+@property (nonatomic, readonly) NSMutableArray* rows;
+
+/** Convenience accessor to get the row object for a given collection row index. */
+- (CBLQueryRow*) rowAtIndex: (NSUInteger)index;
+
+/** Convenience accessor to find the index path of the row with a given document. */
+- (NSIndexPath*) indexPathForDocument: (CBLDocument*)document __attribute__((nonnull));
+
+/** Convenience accessor to return the query row at a given index path. */
+- (CBLQueryRow*) rowAtIndexPath: (NSIndexPath*)path __attribute__((nonnull));
+
+/** Convenience accessor to return the document at a given index path. */
+- (CBLDocument*) documentAtIndexPath: (NSIndexPath*)path __attribute__((nonnull));
+
+
+
+#pragma mark Editing The Collection:
+
+/** Deletes the documents at the given row indexes, animating the removal from the collection. */
+- (BOOL) deleteDocumentsAtIndexes: (NSArray*)indexPaths
+ error: (NSError**)outError;
+
+/** Deletes the given documents, animating the removal from the collection. */
+- (BOOL) deleteDocuments: (NSArray*)documents
+ error: (NSError**)outError;
+
+@end
+
+#pragma mark CouchUICollectionDelegate:
+
+/** Additional methods for the collection view's delegate, that will be invoked by the CouchUICollectionSource. */
+@protocol CBLUICollectionDelegate <UICollectionViewDelegate>
+@optional
+
+/** Allows delegate to return its own custom cell, just like -collectionView:cellForRowAtIndexPath:.
+ If this returns nil the collection source will create its own cell, as if this method were not implemented. */
+- (UICollectionViewCell *)couchCollectionSource:(CBLUICollectionSource*)source
+ cellForRowAtIndexPath:(NSIndexPath *)indexPath;
+
+/** Called after the query's results change, before the collection view is reloaded. */
+- (void)couchCollectionSource:(CBLUICollectionSource*)source
+ willUpdateFromQuery:(CBLLiveQuery*)query;
+
+/** Called after the query's results change to update the collection view. If this method is not implemented by the delegate, reloadData is called on the collection view.*/
+- (void)couchCollectionSource:(CBLUICollectionSource*)source
+ updateFromQuery:(CBLLiveQuery*)query
+ previousRows:(NSArray *)previousRows;
+
+/** Called from -collectionView:cellForItemAtIndexPath: just before it returns, giving the delegate a chance to customize the new cell. */
+- (void)couchCollectionSource:(CBLUICollectionSource*)source
+ willUseCell:(UICollectionViewCell*)cell
+ forRow:(CBLQueryRow*)row;
+
+@end
View
227 Source/API/CBLUICollectionSource.m
@@ -0,0 +1,227 @@
+//
+// CBLUICollectionSource.m
+//
+// Based on CBLUITableSource
+// CouchbaseLite
+//
+// Created by Ewan Mcdougall mrloop.com on 06/02/2013.
+// Copyright (c) 2013 Ewan Mcdougall. All rights reserved.
+
+
+#import "CBLUICollectionSource.h"
+#import "CouchbaseLite.h"
+
+@interface CBLUICollectionSource ()
+{
+ UICollectionView* _collectionView;
+ CBLLiveQuery* _query;
+ NSMutableArray* _rows;
+}
+@end
+
+@implementation CBLUICollectionSource
+
+- (void)dealloc {
+ [_query removeObserver: self forKeyPath:@"rows"];
+}
+
+
+#pragma mark -
+#pragma mark ACCESSORS:
+
+
+@synthesize collectionView=_collectionView;
+@synthesize rows=_rows;
+
+
+- (CBLQueryRow*) rowAtIndex: (NSUInteger)index {
+ return [_rows objectAtIndex: index];
+}
+
+
+- (NSIndexPath*) indexPathForDocument: (CBLDocument*)document {
+ NSString* documentID = document.documentID;
+ NSUInteger index = 0;
+ for (CBLQueryRow* row in _rows) {
+ if ([row.documentID isEqualToString: documentID])
+ return [NSIndexPath indexPathForRow: index inSection: 0];
+ ++index;
+ }
+ return nil;
+}
+
+
+- (CBLQueryRow*) rowAtIndexPath: (NSIndexPath*)path {
+ if (path.section == 0)
+ return [_rows objectAtIndex: path.row];
+ return nil;
+}
+
+
+- (CBLDocument*) documentAtIndexPath: (NSIndexPath*)path {
+ return [self rowAtIndexPath: path].document;
+}
+
+
+#define TELL_DELEGATE(sel, obj) \
+ (([_collectionView.delegate respondsToSelector: sel]) \
+ ? [_collectionView.delegate performSelector: sel withObject: self withObject: obj] \
+ : nil)
+
+
+#pragma mark -
+#pragma mark QUERY HANDLING:
+
+
+- (CBLLiveQuery*) query {
+ return _query;
+}
+
+- (void) setQuery:(CBLLiveQuery *)query {
+ if (query != _query) {
+ [_query removeObserver: self forKeyPath: @"rows"];
+ _query = query;
+ [_query addObserver: self forKeyPath: @"rows" options: 0 context: NULL];
+ [self reloadFromQuery];
+ }
+}
+
+
+-(void) reloadFromQuery {
+ CBLQueryEnumerator* rowEnum = _query.rows;
+ if (rowEnum) {
+ NSArray *oldRows = _rows;
+ _rows = [rowEnum.allObjects mutableCopy];
+ TELL_DELEGATE(@selector(couchCollectionSource:willUpdateFromQuery:), _query);
+
+ id delegate = _collectionView.delegate;
+ SEL selector = @selector(couchCollectionSource:updateFromQuery:previousRows:);
+ if ([delegate respondsToSelector: selector]) {
+ [delegate couchCollectionSource: self
+ updateFromQuery: _query
+ previousRows: oldRows];
+ } else {
+ [self.collectionView reloadData];
+ }
+ }
+}
+
+
+- (void) observeValueForKeyPath: (NSString*)keyPath ofObject: (id)object
+ change: (NSDictionary*)change context: (void*)context
+{
+ if (object == _query)
+ [self reloadFromQuery];
+}
+
+
+#pragma mark -
+#pragma mark DATA SOURCE PROTOCOL:
+
+
+- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
+ return _rows.count;
+}
+
+
+- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
+ cellForItemAtIndexPath:(NSIndexPath *)indexPath
+{
+ // Allow the delegate to create its own cell:
+ UICollectionViewCell* cell = TELL_DELEGATE(@selector(couchCollectionSource:cellForRowAtIndexPath:),
+ indexPath);
+ if (!cell) {
+ // ...if it doesn't, create a cell for it:
+ cell = [collectionView dequeueReusableCellWithReuseIdentifier: @"CBLUICollectionDelegate" forIndexPath:indexPath];
+ if (!cell){
+ cell = [[UICollectionViewCell alloc] init];
+ }
+ CBLQueryRow* row = [self rowAtIndex: indexPath.row];
+
+ // Allow the delegate to customize the cell:
+ id delegate = _collectionView.delegate;
+ if ([delegate respondsToSelector: @selector(couchCollectionSource:willUseCell:forRow:)])
+ [(id<CBLUICollectionDelegate>)delegate couchCollectionSource: self willUseCell: cell forRow: row];
+ }
+ return cell;
+}
+
+
+#pragma mark -
+#pragma mark EDITING:
+
+
+- (BOOL) deleteDocuments: (NSArray*)documents
+ atIndexes: (NSArray*)indexPaths
+ error: (NSError**)outError
+{
+ __block NSError* error = nil;
+ BOOL ok = [_query.database inTransaction: ^{
+ for (CBLDocument* doc in documents) {
+ if (![doc.currentRevision deleteDocument: &error])
+ return NO;
+ }
+ return YES;
+ }];
+ if (!ok) {
+ if (outError)
+ *outError = error;
+ return NO;
+ }
+
+
+ NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
+ for (NSIndexPath* path in indexPaths) {
+ if (path.section == 0)
+ [indexSet addIndex: path.row];
+ }
+ [_rows removeObjectsAtIndexes: indexSet];
+
+ [_collectionView deleteItemsAtIndexPaths: indexPaths];
+ return YES;
+}
+
+
+- (BOOL) deleteDocumentsAtIndexes: (NSArray*)indexPaths error: (NSError**)outError {
+ NSMutableArray* docs = [NSMutableArray array];
+ for (NSIndexPath* path in indexPaths)
+ [docs addObject: [self documentAtIndexPath: path]];
+ return [self deleteDocuments: docs atIndexes: indexPaths error: outError];
+}
+
+
+- (BOOL) deleteDocuments: (NSArray*)documents error: (NSError**)outError {
+ NSMutableArray* paths = [NSMutableArray array];
+ for (CBLDocument* doc in documents)
+ [paths addObject: [self indexPathForDocument: doc]];
+ return [self deleteDocuments: documents atIndexes: paths error: outError];
+}
+
+
+#pragma mark - STATE RESTORATION:
+
+
+- (NSString *) modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx
+ inView:(UIView *)view
+{
+ CBLQueryRow* row = [self rowAtIndexPath: idx];
+ return row.key;
+}
+
+
+- (NSIndexPath *) indexPathForElementWithModelIdentifier:(NSString *)identifier
+ inView:(UIView *)view
+{
+ if (identifier) {
+ NSUInteger i = 0;
+ for (CBLQueryRow* row in _rows) {
+ if ([row.key isEqual: identifier]) {
+ return [NSIndexPath indexPathForItem: i inSection: 0];
+ }
+ ++i;
+ }
+ }
+ return nil;
+}
+
+@end
View
14 Source/API/CBLUITableSource.h
@@ -7,7 +7,7 @@
//
#import <UIKit/UIKit.h>
-@class CBLDocument, CBLLiveQuery, CBLQueryRow, RESTOperation;
+@class CBLDocument, CBLLiveQuery, CBLQueryRow;
/** A UITableView data source driven by a CBLLiveQuery.
It populates the table rows from the query rows, and automatically updates the table as the
@@ -60,11 +60,13 @@
/** Is the user allowed to delete rows by UI gestures? (Defaults to YES.) */
@property (nonatomic) BOOL deletionAllowed;
-/** Asynchronously deletes the documents at the given row indexes, animating the removal from the table. */
-- (void) deleteDocumentsAtIndexes: (NSArray*)indexPaths;
+/** Deletes the documents at the given row indexes, animating the removal from the table. */
+- (BOOL) deleteDocumentsAtIndexes: (NSArray*)indexPaths
+ error: (NSError**)outError __attribute__((nonnull(1)));
/** Asynchronously deletes the given documents, animating the removal from the table. */
-- (void) deleteDocuments: (NSArray*)documents;
+- (BOOL) deleteDocuments: (NSArray*)documents
+ error: (NSError**)outError __attribute__((nonnull(1)));
@end
@@ -92,8 +94,8 @@
willUseCell:(UITableViewCell*)cell
forRow:(CBLQueryRow*)row;
-/** Called if a CBLDB operation invoked by the source (e.g. deleting a document) fails. */
+/** Called upon failure of a document deletion triggered by the user deleting a row. */
- (void)couchTableSource:(CBLUITableSource*)source
- operationFailed:(RESTOperation*)op;
+ deleteFailed:(NSError*)error;
@end
View
23 Source/API/CBLUITableSource.m
@@ -206,8 +206,7 @@ - (void)tableView:(UITableView *)tableView
NSError* error;
if (![[self rowAtIndex:indexPath.row].document.currentRevision deleteDocument: &error]) {
- TELL_DELEGATE(@selector(couchTableSource:operationFailed:), nil);
- [self reloadFromQuery];
+ TELL_DELEGATE(@selector(couchTableSource:deleteFailed:), error);
return;
}
@@ -219,7 +218,10 @@ - (void)tableView:(UITableView *)tableView
}
-- (void) deleteDocuments: (NSArray*)documents atIndexes: (NSArray*)indexPaths {
+- (BOOL) deleteDocuments: (NSArray*)documents
+ atIndexes: (NSArray*)indexPaths
+ error: (NSError**)outError
+{
__block NSError* error = nil;
BOOL ok = [_query.database inTransaction: ^{
for (CBLDocument* doc in documents) {
@@ -229,9 +231,9 @@ - (void) deleteDocuments: (NSArray*)documents atIndexes: (NSArray*)indexPaths {
return YES;
}];
if (!ok) {
- TELL_DELEGATE(@selector(couchTableSource:operationFailed:), nil);
- [self reloadFromQuery];
- return;
+ if (outError)
+ *outError = error;
+ return NO;
}
@@ -243,18 +245,19 @@ - (void) deleteDocuments: (NSArray*)documents atIndexes: (NSArray*)indexPaths {
[_rows removeObjectsAtIndexes: indexSet];
[_tableView deleteRowsAtIndexPaths: indexPaths withRowAnimation: UITableViewRowAnimationFade];
+ return YES;
}
-- (void) deleteDocumentsAtIndexes: (NSArray*)indexPaths {
+- (BOOL) deleteDocumentsAtIndexes: (NSArray*)indexPaths error: (NSError**)outError {
NSArray* docs = [indexPaths my_map: ^(id path) {return [self documentAtIndexPath: path];}];
- [self deleteDocuments: docs atIndexes: indexPaths];
+ return [self deleteDocuments: docs atIndexes: indexPaths error: outError];
}
-- (void) deleteDocuments: (NSArray*)documents {
+- (BOOL) deleteDocuments: (NSArray*)documents error: (NSError**)outError {
NSArray* paths = [documents my_map: ^(id doc) {return [self indexPathForDocument: doc];}];
- [self deleteDocuments: documents atIndexes: paths];
+ return [self deleteDocuments: documents atIndexes: paths error: outError];
}
View
1 Source/CBLView+Internal.m
@@ -379,7 +379,6 @@ - (NSArray*) _queryWithOptions: (const CBLQueryOptions*)options
bool group = options->group || groupLevel > 0;
if (options->reduce || group) {
// Reduced or grouped query:
- // Reduced or grouped query:
if (!_reduceBlock && !group) {
Warn(@"Cannot use reduce option in view %@ which has no reduce block defined", _name);
*outStatus = kCBLStatusBadParam;

0 comments on commit 982c808

Please sign in to comment.