mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-09-18 21:50:56 +00:00
585 lines
19 KiB
Objective-C
Executable file
585 lines
19 KiB
Objective-C
Executable file
/*
|
|
* Copyright 2012 Facebook
|
|
*
|
|
* 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 "FBGraphObjectTableDataSource.h"
|
|
#import "FBGraphObjectTableCell.h"
|
|
#import "FBGraphObject.h"
|
|
#import "FBURLConnection.h"
|
|
#import "FBUtility.h"
|
|
|
|
// Magic number - iPhone address book doesn't show scrubber for less than 5 contacts
|
|
static const NSInteger kMinimumCountToCollate = 6;
|
|
|
|
@interface FBGraphObjectTableDataSource ()
|
|
|
|
@property (nonatomic, retain) NSArray *data;
|
|
@property (nonatomic, retain) NSArray *indexKeys;
|
|
@property (nonatomic, retain) NSDictionary *indexMap;
|
|
@property (nonatomic, retain) NSMutableSet *pendingURLConnections;
|
|
@property (nonatomic, assign) BOOL expectingMoreGraphObjects;
|
|
@property (nonatomic, retain) UILocalizedIndexedCollation *collation;
|
|
@property (nonatomic, assign) BOOL showSections;
|
|
|
|
- (BOOL)filterIncludesItem:(FBGraphObject *)item;
|
|
- (FBGraphObjectTableCell *)cellWithTableView:(UITableView *)tableView;
|
|
- (NSString *)indexKeyOfItem:(FBGraphObject *)item;
|
|
- (UIImage *)tableView:(UITableView *)tableView imageForItem:(FBGraphObject *)item;
|
|
- (void)addOrRemovePendingConnection:(FBURLConnection *)connection;
|
|
- (BOOL)isActivityIndicatorIndexPath:(NSIndexPath *)indexPath;
|
|
- (BOOL)isLastSection:(NSInteger)section;
|
|
|
|
@end
|
|
|
|
@implementation FBGraphObjectTableDataSource
|
|
|
|
@synthesize data = _data;
|
|
@synthesize defaultPicture = _defaultPicture;
|
|
@synthesize controllerDelegate = _controllerDelegate;
|
|
@synthesize groupByField = _groupByField;
|
|
@synthesize useCollation = _useCollation;
|
|
@synthesize showSections = _showSections;
|
|
@synthesize indexKeys = _indexKeys;
|
|
@synthesize indexMap = _indexMap;
|
|
@synthesize itemTitleSuffixEnabled = _itemTitleSuffixEnabled;
|
|
@synthesize itemPicturesEnabled = _itemPicturesEnabled;
|
|
@synthesize itemSubtitleEnabled = _itemSubtitleEnabled;
|
|
@synthesize pendingURLConnections = _pendingURLConnections;
|
|
@synthesize selectionDelegate = _selectionDelegate;
|
|
@synthesize sortDescriptors = _sortDescriptors;
|
|
@synthesize dataNeededDelegate = _dataNeededDelegate;
|
|
@synthesize expectingMoreGraphObjects = _expectingMoreGraphObjects;
|
|
@synthesize collation = _collation;
|
|
|
|
- (void)setUseCollation:(BOOL)useCollation
|
|
{
|
|
if (_useCollation != useCollation) {
|
|
_useCollation = useCollation;
|
|
self.collation = _useCollation ? [UILocalizedIndexedCollation currentCollation] : nil;
|
|
}
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
self = [super init];
|
|
|
|
if (self) {
|
|
NSMutableSet *pendingURLConnections = [[NSMutableSet alloc] init];
|
|
self.pendingURLConnections = pendingURLConnections;
|
|
[pendingURLConnections release];
|
|
self.expectingMoreGraphObjects = YES;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
FBConditionalLog(![_pendingURLConnections count],
|
|
@"FBGraphObjectTableDataSource pending connection did not retain self");
|
|
|
|
[_collation release];
|
|
[_data release];
|
|
[_defaultPicture release];
|
|
[_groupByField release];
|
|
[_indexKeys release];
|
|
[_indexMap release];
|
|
[_pendingURLConnections release];
|
|
[_sortDescriptors release];
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
#pragma mark - Public Methods
|
|
|
|
- (NSString *)fieldsForRequestIncluding:(NSSet *)customFields, ...
|
|
{
|
|
// Start with custom fields.
|
|
NSMutableSet *nameSet = [[NSMutableSet alloc] initWithSet:customFields];
|
|
|
|
// Iterate through varargs after the initial set, and add them
|
|
id vaName;
|
|
va_list vaArguments;
|
|
va_start(vaArguments, customFields);
|
|
while ((vaName = va_arg(vaArguments, id))) {
|
|
[nameSet addObject:vaName];
|
|
}
|
|
va_end(vaArguments);
|
|
|
|
// Add fields needed for data source functionality.
|
|
if (self.groupByField) {
|
|
[nameSet addObject:self.groupByField];
|
|
}
|
|
|
|
// get a stable order for our fields, because we use the resulting URL as a cache ID
|
|
NSMutableArray *sortedFields = [[nameSet allObjects] mutableCopy];
|
|
[sortedFields sortUsingSelector:@selector(caseInsensitiveCompare:)];
|
|
|
|
[nameSet release];
|
|
|
|
// Build the comma-separated string
|
|
NSMutableString *fields = [[[NSMutableString alloc] init] autorelease];
|
|
|
|
for (NSString *field in sortedFields) {
|
|
if ([fields length]) {
|
|
[fields appendString:@","];
|
|
}
|
|
[fields appendString:field];
|
|
}
|
|
|
|
[sortedFields release];
|
|
return fields;
|
|
}
|
|
|
|
- (void)prepareForNewRequest {
|
|
self.data = nil;
|
|
self.expectingMoreGraphObjects = YES;
|
|
}
|
|
|
|
- (void)clearGraphObjects {
|
|
self.indexKeys = nil;
|
|
self.indexMap = nil;
|
|
[self prepareForNewRequest];
|
|
}
|
|
|
|
- (void)appendGraphObjects:(NSArray *)data
|
|
{
|
|
if (self.data) {
|
|
self.data = [self.data arrayByAddingObjectsFromArray:data];
|
|
} else {
|
|
self.data = data;
|
|
}
|
|
if (data == nil) {
|
|
self.expectingMoreGraphObjects = NO;
|
|
}
|
|
}
|
|
|
|
- (BOOL)hasGraphObjects {
|
|
return self.data && self.data.count > 0;
|
|
}
|
|
|
|
- (void)bindTableView:(UITableView *)tableView
|
|
{
|
|
tableView.dataSource = self;
|
|
tableView.rowHeight = [FBGraphObjectTableCell rowHeight];
|
|
}
|
|
|
|
- (void)cancelPendingRequests
|
|
{
|
|
// Cancel all active connections.
|
|
for (FBURLConnection *connection in _pendingURLConnections) {
|
|
[connection cancel];
|
|
}
|
|
}
|
|
|
|
// Called after changing any properties. To simplify the code here,
|
|
// since this class is internal, we do not auto-update on property
|
|
// changes.
|
|
//
|
|
// This builds indexMap and indexKeys, the data structures used to
|
|
// respond to UITableDataSource protocol requests. UITable expects
|
|
// a list of section names, and then ask for items given a section
|
|
// index and item index within that section. In addition, we need
|
|
// to do reverse mapping from item to table location.
|
|
//
|
|
// To facilitate both of these, we build an array of section titles,
|
|
// and a dictionary mapping title -> item array. We could consider
|
|
// building a reverse-lookup map too, but this seems unnecessary.
|
|
- (void)update
|
|
{
|
|
NSInteger objectsShown = 0;
|
|
NSMutableDictionary *indexMap = [[[NSMutableDictionary alloc] init] autorelease];
|
|
NSMutableArray *indexKeys = [[[NSMutableArray alloc] init] autorelease];
|
|
|
|
for (FBGraphObject *item in self.data) {
|
|
if (![self filterIncludesItem:item]) {
|
|
continue;
|
|
}
|
|
|
|
NSString *key = [self indexKeyOfItem:item];
|
|
NSMutableArray *existingSection = [indexMap objectForKey:key];
|
|
NSMutableArray *section = existingSection;
|
|
|
|
if (!section) {
|
|
section = [[[NSMutableArray alloc] init] autorelease];
|
|
}
|
|
[section addObject:item];
|
|
|
|
if (!existingSection) {
|
|
[indexMap setValue:section forKey:key];
|
|
[indexKeys addObject:key];
|
|
}
|
|
objectsShown++;
|
|
}
|
|
|
|
if (self.sortDescriptors) {
|
|
for (NSString *key in indexKeys) {
|
|
[[indexMap objectForKey:key] sortUsingDescriptors:self.sortDescriptors];
|
|
}
|
|
}
|
|
if (!self.useCollation) {
|
|
[indexKeys sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
|
|
}
|
|
|
|
self.showSections = objectsShown >= kMinimumCountToCollate;
|
|
self.indexKeys = indexKeys;
|
|
self.indexMap = indexMap;
|
|
}
|
|
|
|
#pragma mark - Private Methods
|
|
|
|
- (BOOL)filterIncludesItem:(FBGraphObject *)item
|
|
{
|
|
if (![self.controllerDelegate respondsToSelector:
|
|
@selector(graphObjectTableDataSource:filterIncludesItem:)]) {
|
|
return YES;
|
|
}
|
|
|
|
return [self.controllerDelegate graphObjectTableDataSource:self
|
|
filterIncludesItem:item];
|
|
}
|
|
|
|
- (void)setSortingByFields:(NSArray*)fieldNames ascending:(BOOL)ascending {
|
|
NSMutableArray *sortDescriptors = [NSMutableArray arrayWithCapacity:fieldNames.count];
|
|
for (NSString *fieldName in fieldNames) {
|
|
NSSortDescriptor *sortBy = [NSSortDescriptor
|
|
sortDescriptorWithKey:fieldName
|
|
ascending:ascending
|
|
selector:@selector(localizedCaseInsensitiveCompare:)];
|
|
[sortDescriptors addObject:sortBy];
|
|
}
|
|
self.sortDescriptors = sortDescriptors;
|
|
}
|
|
|
|
- (void)setSortingBySingleField:(NSString*)fieldName ascending:(BOOL)ascending {
|
|
[self setSortingByFields:[NSArray arrayWithObject:fieldName] ascending:ascending];
|
|
}
|
|
|
|
- (FBGraphObjectTableCell *)cellWithTableView:(UITableView *)tableView
|
|
{
|
|
static NSString * const cellKey = @"fbTableCell";
|
|
FBGraphObjectTableCell *cell =
|
|
(FBGraphObjectTableCell*)[tableView dequeueReusableCellWithIdentifier:cellKey];
|
|
|
|
if (!cell) {
|
|
cell = [[FBGraphObjectTableCell alloc]
|
|
initWithStyle:UITableViewCellStyleSubtitle
|
|
reuseIdentifier:cellKey];
|
|
[cell autorelease];
|
|
|
|
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
|
|
- (NSString *)indexKeyOfItem:(FBGraphObject *)item
|
|
{
|
|
NSString *text = @"";
|
|
|
|
if (self.groupByField) {
|
|
text = [item objectForKey:self.groupByField] ?: @"";
|
|
}
|
|
|
|
if (self.useCollation) {
|
|
NSInteger collationSection = [self.collation sectionForObject:item collationStringSelector:NSSelectorFromString(self.groupByField)];
|
|
text = [[self.collation sectionTitles] objectAtIndex:collationSection];
|
|
} else {
|
|
|
|
if ([text length] > 1) {
|
|
text = [text substringToIndex:1];
|
|
}
|
|
|
|
text = [text uppercaseString];
|
|
}
|
|
return text;
|
|
}
|
|
|
|
- (FBGraphObject *)itemAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
id key = nil;
|
|
if (self.useCollation) {
|
|
NSString *sectionTitle = [self.collation.sectionTitles objectAtIndex:indexPath.section];
|
|
key = sectionTitle;
|
|
} else if (indexPath.section >= 0 && indexPath.section < self.indexKeys.count) {
|
|
key = [self.indexKeys objectAtIndex:indexPath.section];
|
|
}
|
|
NSArray *sectionItems = [self.indexMap objectForKey:key];
|
|
if (indexPath.row >= 0 && indexPath.row < sectionItems.count) {
|
|
return [sectionItems objectAtIndex:indexPath.row];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSIndexPath *)indexPathForItem:(FBGraphObject *)item
|
|
{
|
|
NSString *key = [self indexKeyOfItem:item];
|
|
NSMutableArray *sectionItems = [self.indexMap objectForKey:key];
|
|
if (!sectionItems) {
|
|
return nil;
|
|
}
|
|
|
|
NSInteger sectionIndex = 0;
|
|
if (self.useCollation) {
|
|
sectionIndex = [self.collation.sectionTitles indexOfObject:key];
|
|
} else {
|
|
sectionIndex = [self.indexKeys indexOfObject:key];
|
|
}
|
|
if (sectionIndex == NSNotFound) {
|
|
return nil;
|
|
}
|
|
|
|
id matchingObject = [FBUtility graphObjectInArray:sectionItems withSameIDAs:item];
|
|
if (matchingObject == nil) {
|
|
return nil;
|
|
}
|
|
|
|
NSInteger itemIndex = [sectionItems indexOfObject:matchingObject];
|
|
if (itemIndex == NSNotFound) {
|
|
return nil;
|
|
}
|
|
|
|
return [NSIndexPath indexPathForRow:itemIndex inSection:sectionIndex];
|
|
}
|
|
|
|
- (BOOL)isLastSection:(NSInteger)section {
|
|
if (self.useCollation) {
|
|
return section == self.collation.sectionTitles.count - 1;
|
|
} else {
|
|
return section == self.indexKeys.count - 1;
|
|
}
|
|
}
|
|
|
|
- (BOOL)isActivityIndicatorIndexPath:(NSIndexPath *)indexPath {
|
|
if ([self isLastSection:indexPath.section]) {
|
|
NSArray *sectionItems = [self sectionItemsForSection:indexPath.section];
|
|
|
|
if (indexPath.row == sectionItems.count) {
|
|
// Last section has one more row that items if we are expecting more objects.
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (NSString *)titleForSection:(NSInteger)sectionIndex
|
|
{
|
|
id key;
|
|
if (self.useCollation) {
|
|
NSString *sectionTitle = [self.collation.sectionTitles objectAtIndex:sectionIndex];
|
|
key = sectionTitle;
|
|
} else {
|
|
key = [self.indexKeys objectAtIndex:sectionIndex];
|
|
}
|
|
return key;
|
|
}
|
|
|
|
- (NSArray *)sectionItemsForSection:(NSInteger)sectionIndex
|
|
{
|
|
id key = [self titleForSection:sectionIndex];
|
|
NSArray *sectionItems = [self.indexMap objectForKey:key];
|
|
return sectionItems;
|
|
}
|
|
- (UIImage *)tableView:(UITableView *)tableView imageForItem:(FBGraphObject *)item
|
|
{
|
|
__block UIImage *image = nil;
|
|
NSString *urlString = [self.controllerDelegate graphObjectTableDataSource:self
|
|
pictureUrlOfItem:item];
|
|
if (urlString) {
|
|
FBURLConnectionHandler handler =
|
|
^(FBURLConnection *connection, NSError *error, NSURLResponse *response, NSData *data) {
|
|
[self addOrRemovePendingConnection:connection];
|
|
if (!error) {
|
|
image = [UIImage imageWithData:data];
|
|
|
|
NSIndexPath *indexPath = [self indexPathForItem:item];
|
|
if (indexPath) {
|
|
FBGraphObjectTableCell *cell =
|
|
(FBGraphObjectTableCell*)[tableView cellForRowAtIndexPath:indexPath];
|
|
|
|
if (cell) {
|
|
cell.picture = image;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
FBURLConnection *connection = [[[FBURLConnection alloc]
|
|
initWithURL:[NSURL URLWithString:urlString]
|
|
completionHandler:handler]
|
|
autorelease];
|
|
|
|
[self addOrRemovePendingConnection:connection];
|
|
}
|
|
|
|
// If the picture had not been fetched yet by this object, but is cached in the
|
|
// URL cache, we can complete synchronously above. In this case, we will not
|
|
// find the cell in the table because we are in the process of creating it. We can
|
|
// just return the object here.
|
|
if (image) {
|
|
return image;
|
|
}
|
|
|
|
return self.defaultPicture;
|
|
}
|
|
|
|
// In tableView:imageForItem:, there are two code-paths, and both always run.
|
|
// Whichever runs first adds the connection to the collection of pending requests,
|
|
// and whichever runs second removes it. This allows us to track all requests
|
|
// for which one code-path has run and the other has not.
|
|
- (void)addOrRemovePendingConnection:(FBURLConnection *)connection
|
|
{
|
|
if ([self.pendingURLConnections containsObject:connection]) {
|
|
[self.pendingURLConnections removeObject:connection];
|
|
} else {
|
|
[self.pendingURLConnections addObject:connection];
|
|
}
|
|
}
|
|
|
|
#pragma mark - UITableViewDataSource
|
|
|
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
|
{
|
|
if (self.useCollation) {
|
|
return self.collation.sectionTitles.count;
|
|
} else {
|
|
return [self.indexKeys count];
|
|
}
|
|
}
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
|
{
|
|
NSArray *sectionItems = [self sectionItemsForSection:section];
|
|
|
|
int count = [sectionItems count];
|
|
// If we are expecting more objects to be loaded via paging, add 1 to the
|
|
// row count for the last section.
|
|
if (self.expectingMoreGraphObjects &&
|
|
self.dataNeededDelegate &&
|
|
[self isLastSection:section]) {
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
|
{
|
|
if (!self.showSections) {
|
|
return nil;
|
|
}
|
|
|
|
NSArray *sectionItems = [self sectionItemsForSection:section];
|
|
return sectionItems.count > 0 ? [self titleForSection:section] : nil;
|
|
}
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
|
|
{
|
|
if (self.useCollation) {
|
|
return [self.collation sectionForSectionIndexTitleAtIndex:index];
|
|
} else {
|
|
return index;
|
|
}
|
|
}
|
|
|
|
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
|
|
{
|
|
if (!self.showSections) {
|
|
return nil;
|
|
}
|
|
|
|
if (self.useCollation) {
|
|
return self.collation.sectionIndexTitles;
|
|
} else {
|
|
return [self.indexKeys count] > 1 ? self.indexKeys : nil;
|
|
}
|
|
}
|
|
|
|
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView
|
|
cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
FBGraphObjectTableCell *cell = [self cellWithTableView:tableView];
|
|
|
|
if ([self isActivityIndicatorIndexPath:indexPath]) {
|
|
cell.picture = nil;
|
|
cell.subtitle = nil;
|
|
cell.title = nil;
|
|
cell.accessoryType = UITableViewCellAccessoryNone;
|
|
cell.selected = NO;
|
|
|
|
[cell startAnimatingActivityIndicator];
|
|
|
|
[self.dataNeededDelegate graphObjectTableDataSourceNeedsData:self
|
|
triggeredByIndexPath:indexPath];
|
|
} else {
|
|
FBGraphObject *item = [self itemAtIndexPath:indexPath];
|
|
|
|
// This is a no-op if it doesn't have an activity indicator.
|
|
[cell stopAnimatingActivityIndicator];
|
|
if (item) {
|
|
if (self.itemPicturesEnabled) {
|
|
cell.picture = [self tableView:tableView imageForItem:item];
|
|
} else {
|
|
cell.picture = nil;
|
|
}
|
|
|
|
if (self.itemTitleSuffixEnabled) {
|
|
cell.titleSuffix = [self.controllerDelegate graphObjectTableDataSource:self
|
|
titleSuffixOfItem:item];
|
|
} else {
|
|
cell.titleSuffix = nil;
|
|
}
|
|
|
|
if (self.itemSubtitleEnabled) {
|
|
cell.subtitle = [self.controllerDelegate graphObjectTableDataSource:self
|
|
subtitleOfItem:item];
|
|
} else {
|
|
cell.subtitle = nil;
|
|
}
|
|
|
|
cell.title = [self.controllerDelegate graphObjectTableDataSource:self
|
|
titleOfItem:item];
|
|
|
|
if ([self.selectionDelegate graphObjectTableDataSource:self
|
|
selectionIncludesItem:item]) {
|
|
cell.accessoryType = UITableViewCellAccessoryCheckmark;
|
|
cell.selected = YES;
|
|
} else {
|
|
cell.accessoryType = UITableViewCellAccessoryNone;
|
|
cell.selected = NO;
|
|
}
|
|
|
|
if ([self.controllerDelegate respondsToSelector:@selector(graphObjectTableDataSource:customizeTableCell:)]) {
|
|
[self.controllerDelegate graphObjectTableDataSource:self
|
|
customizeTableCell:cell];
|
|
}
|
|
} else {
|
|
cell.picture = nil;
|
|
cell.subtitle = nil;
|
|
cell.title = nil;
|
|
cell.accessoryType = UITableViewCellAccessoryNone;
|
|
cell.selected = NO;
|
|
}
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
|
|
@end
|