Skip to content

Commit

Permalink
Add ADBIndexedTableView component and demo project
Browse files Browse the repository at this point in the history
  • Loading branch information
albertodebortoli committed Nov 4, 2012
1 parent 89d5179 commit d71dd2f
Show file tree
Hide file tree
Showing 10 changed files with 501 additions and 7 deletions.
17 changes: 17 additions & 0 deletions .gitignore
@@ -0,0 +1,17 @@
# Xcode
build/*
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
*.xcworkspace
!default.xcworkspace
xcuserdata
profile
*.moved-aside
.DS_Store

78 changes: 78 additions & 0 deletions ADBIndexedTableView/ADBIndexedTableView.h
@@ -0,0 +1,78 @@
//
// ADBIndexedTableView.h
// PassDesk
//
// Created by Alberto De Bortoli on 11/4/12.
// Copyright (c) 2012 Alberto De Bortoli. All rights reserved.
//
// ADBIndexedTableView inherits from UITableView (use it as a UITableView).
//
// 1. Set up delegate and dataSource as usual (using Interface Builder or programmatically).
// 2. Set indexDataSource and implement 'objectsFieldForIndexedTableView:' method
// (used for sorting and indexing).
// 3. dataSource 'tableView:cellForRowAtIndexPath' implementation can retrieve object for
// the given indexPath using 'objectAtIndexPath:' method.
// 4. Implementing 'indexedTableView:cellForRowAtIndexPath:objectAtIndexPath:' is required
// and it will be used only if 'tableView:cellForRowAtIndexPath' implementation is not
// provided by the dataSource.
// 5. Reload the table sending unsorted objects via 'reloadDataWithObjects:' method to let
// indexedTableView create the data structure. Use 'reloadData' for subsequent reloadings.
//

#import <UIKit/UIKit.h>
#import "ADBMessageInterceptor.h"

@class ADBIndexedTableView;

@protocol ADBIndexedTableViewDataSource <NSObject>

@required
/**
@return field used to retrieve the first letter (used for index)
@param tableView, the caller
Return value will be used for KVC on 'indexedObjects' objects
*/
- (NSString *)objectsFieldForIndexedTableView:(ADBIndexedTableView *)tableView;

@optional
/**
@return cell for row at index path
@param tableView, the caller
@param indexPath, the indexPath
@param object, the object at indexPath
Surrogate method for dataSource method 'tableView:cellForRowAtIndexPath:'
Required only if 'tableView:cellForRowAtIndexPath' implementation is not provided by dataSource.
*/
- (UITableViewCell *)indexedTableView:(ADBIndexedTableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
objectAtIndexPath:(id)object;
@end

@interface ADBIndexedTableView : UITableView {

@private
NSMutableDictionary *_indexedObjects;
NSArray *_objectsInitials;
ADBMessageInterceptor *_dataSourceInterceptor;
id <ADBIndexedTableViewDataSource> __weak _indexDataSource;
}

/**
@param objects, array of objects that will be organized in sections
Surrogate method for 'reloadData', creates data structure and calls 'reloadData' on super
Subsequent 'reloadData' messages use previously created data structure.
*/
- (void)reloadDataWithObjects:(NSArray *)objects;

- (id)objectAtIndexPath:(NSIndexPath *)indexPath;
- (void)removeObjectAtIndexPath:(NSIndexPath *)indexPath;
- (id)objectsWithInitials:(NSString *)initial;
- (id)objectsInSection:(NSUInteger)section;

@property (nonatomic, strong) NSMutableDictionary *indexedObjects;
@property (nonatomic, strong) NSArray *objectsInitials;
@property (nonatomic, weak) IBOutlet id <ADBIndexedTableViewDataSource> indexDataSource;

@end
140 changes: 140 additions & 0 deletions ADBIndexedTableView/ADBIndexedTableView.m
@@ -0,0 +1,140 @@
//
// ADBIndexedTableView.m
// PassDesk
//
// Created by Alberto De Bortoli on 11/4/12.
// Copyright (c) 2012 Alberto De Bortoli. All rights reserved.
//

#import "ADBIndexedTableView.h"

@implementation ADBIndexedTableView

#pragma mark - Message forwarding

- (id <UITableViewDataSource>)dataSource
{
return (id <UITableViewDataSource>)_dataSourceInterceptor;
}

- (void)setDataSource:(id <UITableViewDataSource>)dataSource
{
[super setDataSource:nil];

_dataSourceInterceptor = [[ADBMessageInterceptor alloc] init];
[_dataSourceInterceptor setReceiver:dataSource];
[_dataSourceInterceptor setSecondChance:self];

[super setDataSource:(id)_dataSourceInterceptor];
}

#pragma mark - Core

- (void)reloadDataWithObjects:(NSArray *)objects
{
NSString *field = [_indexDataSource objectsFieldForIndexedTableView:self];

NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:field ascending:YES];
objects = [objects sortedArrayUsingDescriptors:@[sortDescriptor]];

// calculate needed initials
NSMutableArray *initials = [NSMutableArray array];
for (NSString *property in [objects valueForKey:field]) {
if ([property length]) {
NSString *initial = [[property substringToIndex:1] capitalizedString];
if (![initials containsObject:initial]) {
[initials addObject:initial];
}
}
}

_objectsInitials = initials;

_indexedObjects = [NSMutableDictionary dictionary];

// create dictionary with objects grouped for initial
for (NSString *initial in _objectsInitials) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.%@ beginswith[cd] %@", field, initial];
NSArray *filteredForInitial = [objects filteredArrayUsingPredicate:predicate];
[_indexedObjects setObject:filteredForInitial forKey:initial];
}

[self reloadData];
}

- (id)objectAtIndexPath:(NSIndexPath *)indexPath
{
NSString *initial = self.objectsInitials[indexPath.section];
return self.indexedObjects[initial][indexPath.row];
}

- (void)removeObjectAtIndexPath:(NSIndexPath *)indexPath
{
NSString *initial = self.objectsInitials[indexPath.section];
NSMutableArray *objectsForGivenInitial = [self.indexedObjects[initial] mutableCopy];
[objectsForGivenInitial removeObjectAtIndex:indexPath.row];
[self.indexedObjects setObject:objectsForGivenInitial forKey:initial];
[self deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
}

- (id)objectsWithInitials:(NSString *)initial
{
NSArray *retVal = _indexedObjects[[initial uppercaseString]];
return retVal;
}

- (id)objectsInSection:(NSUInteger)section
{
NSString *initial = [_objectsInitials objectAtIndex:section];
NSArray *retVal = _indexedObjects[initial];
return retVal;
}

#pragma mark - UITableViewDatasource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [_objectsInitials count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSString *initial = _objectsInitials[section];
return [_indexedObjects[initial] count];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return _objectsInitials[section];
}

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
return _objectsInitials;
}

- (NSInteger)tableView:(UITableView *)tableView
sectionForSectionIndexTitle:(NSString *)title
atIndex:(NSInteger)index
{
return [_objectsInitials indexOfObject:title];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id objectAtIndexPath = [self objectAtIndexPath:indexPath];

UITableViewCell *cell = nil;

// since 'indexedTableView:cellForRowAtIndexPath:objectAtIndexPath:' is marked ad @optional
// we should check if indexDataSource responds to it, but if we get here dataSource does not implement
// 'tableView:cellForRowAtIndexPath:' and so indexDataSource must implement
// 'indexedTableView:cellForRowAtIndexPath:objectAtIndexPath:' (required in this scenario)
cell = [_indexDataSource indexedTableView:self
cellForRowAtIndexPath:indexPath
objectAtIndexPath:objectAtIndexPath];

return cell;
}

@end
55 changes: 55 additions & 0 deletions ADBIndexedTableView/ADBMessageInterceptor.h
@@ -0,0 +1,55 @@
//
// ADBMessageInterceptor.h
// PassDesk
//
// Created by Alberto De Bortoli on 11/4/12.
// Copyright (c) 2012 Alberto De Bortoli. All rights reserved.
//
// MessageInterceptor, proxy class to handle message forwarding.
//
// If you set a message inspector as a delegate object (for your delegating object)
// it will check if the real delegate (receiver) can respond to the message, otherwise
// it will check if the surrogate delegate (secondChance) can handle the message.
// If both tests fail, a check on super (NSObject) will be performed as last wish.
//
// Useful to let a class (instance I) implement some delegate methods and to let the
// rest of the delegate methods be implemented by the real delegate (R).
// In this scenario receiver must be the real delegate (R) and mainInTheMiddle must be the class object (I).
//
// Classes that encapsulate a interceptor must:
// 1. hold a ADBMessageInterceptor *iVar (here named _delegateInterceptor)
// 2. implement the following methods
//
// example given using UITableViewDelegate
//
// #pragma mark - Message forwarding
//
// - (id <UITableViewDelegate>)delegate
// {
// return (id <UITableViewDelegate>)_delegateInterceptor;
// }
//
// - (void)setDelegate:(id <UITableViewDelegate>)delegate
// {
// [super setDelegate:nil];
//
// _delegateInterceptor = [[ADBMessageInterceptor alloc] init];
// [_delegateInterceptor setSecondChance:self];
// [_delegateInterceptor setReceiver:delegate];
//
// [super setDelegate:(id)_delegateInterceptor];
// }
//

#import <Foundation/Foundation.h>

@interface ADBMessageInterceptor : NSObject {

id __weak _receiver;
id __weak _secondChance;
}

@property (nonatomic, weak) id receiver;
@property (nonatomic, weak) id secondChance;

@end
35 changes: 35 additions & 0 deletions ADBIndexedTableView/ADBMessageInterceptor.m
@@ -0,0 +1,35 @@
//
// ADBMessageInterceptor.m
// PassDesk
//
// Created by Alberto De Bortoli on 11/4/12.
// Copyright (c) 2012 Alberto De Bortoli. All rights reserved.
//

#import "ADBMessageInterceptor.h"

@implementation ADBMessageInterceptor

- (id)forwardingTargetForSelector:(SEL)aSelector
{
if ([_receiver respondsToSelector:aSelector]) {
return _receiver;
}
if ([_secondChance respondsToSelector:aSelector]) {
return _secondChance;
}
return [super forwardingTargetForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
if ([_receiver respondsToSelector:aSelector]) {
return YES;
}
if ([_secondChance respondsToSelector:aSelector]) {
return YES;
}
return [super respondsToSelector:aSelector];
}

@end
20 changes: 20 additions & 0 deletions ADBIndexedTableViewDemo.xcodeproj/project.pbxproj
Expand Up @@ -18,6 +18,8 @@
F0BF7BA61646CFDD00E5F0B5 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F0BF7BA51646CFDD00E5F0B5 /* Default-568h@2x.png */; };
F0BF7BA91646CFDD00E5F0B5 /* MainStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F0BF7BA71646CFDD00E5F0B5 /* MainStoryboard.storyboard */; };
F0BF7BAC1646CFDD00E5F0B5 /* ADBViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0BF7BAB1646CFDD00E5F0B5 /* ADBViewController.m */; };
F0BF7BB71646CFF900E5F0B5 /* ADBIndexedTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = F0BF7BB41646CFF900E5F0B5 /* ADBIndexedTableView.m */; };
F0BF7BB81646CFF900E5F0B5 /* ADBMessageInterceptor.m in Sources */ = {isa = PBXBuildFile; fileRef = F0BF7BB61646CFF900E5F0B5 /* ADBMessageInterceptor.m */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -37,6 +39,10 @@
F0BF7BA81646CFDD00E5F0B5 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/MainStoryboard.storyboard; sourceTree = "<group>"; };
F0BF7BAA1646CFDD00E5F0B5 /* ADBViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ADBViewController.h; sourceTree = "<group>"; };
F0BF7BAB1646CFDD00E5F0B5 /* ADBViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ADBViewController.m; sourceTree = "<group>"; };
F0BF7BB31646CFF900E5F0B5 /* ADBIndexedTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ADBIndexedTableView.h; sourceTree = "<group>"; };
F0BF7BB41646CFF900E5F0B5 /* ADBIndexedTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ADBIndexedTableView.m; sourceTree = "<group>"; };
F0BF7BB51646CFF900E5F0B5 /* ADBMessageInterceptor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ADBMessageInterceptor.h; sourceTree = "<group>"; };
F0BF7BB61646CFF900E5F0B5 /* ADBMessageInterceptor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ADBMessageInterceptor.m; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -56,6 +62,7 @@
F0BF7B801646CFDD00E5F0B5 = {
isa = PBXGroup;
children = (
F0BF7BB21646CFF900E5F0B5 /* ADBIndexedTableView */,
F0BF7B951646CFDD00E5F0B5 /* ADBIndexedTableViewDemo */,
F0BF7B8E1646CFDD00E5F0B5 /* Frameworks */,
F0BF7B8C1646CFDD00E5F0B5 /* Products */,
Expand Down Expand Up @@ -107,6 +114,17 @@
name = "Supporting Files";
sourceTree = "<group>";
};
F0BF7BB21646CFF900E5F0B5 /* ADBIndexedTableView */ = {
isa = PBXGroup;
children = (
F0BF7BB31646CFF900E5F0B5 /* ADBIndexedTableView.h */,
F0BF7BB41646CFF900E5F0B5 /* ADBIndexedTableView.m */,
F0BF7BB51646CFF900E5F0B5 /* ADBMessageInterceptor.h */,
F0BF7BB61646CFF900E5F0B5 /* ADBMessageInterceptor.m */,
);
path = ADBIndexedTableView;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -177,6 +195,8 @@
F0BF7B9C1646CFDD00E5F0B5 /* main.m in Sources */,
F0BF7BA01646CFDD00E5F0B5 /* ADBAppDelegate.m in Sources */,
F0BF7BAC1646CFDD00E5F0B5 /* ADBViewController.m in Sources */,
F0BF7BB71646CFF900E5F0B5 /* ADBIndexedTableView.m in Sources */,
F0BF7BB81646CFF900E5F0B5 /* ADBMessageInterceptor.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
6 changes: 6 additions & 0 deletions ADBIndexedTableViewDemo/ADBViewController.h
Expand Up @@ -7,7 +7,13 @@
//

#import <UIKit/UIKit.h>
#import "ADBIndexedTableView.h"

@interface ADBViewController : UIViewController
<UITableViewDelegate,
UITableViewDataSource,
ADBIndexedTableViewDataSource>

@property (nonatomic, strong) IBOutlet ADBIndexedTableView *tableView;

@end

0 comments on commit d71dd2f

Please sign in to comment.