From d71dd2fc8c475f841093af4aae7a0e0d27371621 Mon Sep 17 00:00:00 2001 From: Alberto De Bortoli Date: Sun, 4 Nov 2012 18:48:11 +0100 Subject: [PATCH] Add ADBIndexedTableView component and demo project --- .gitignore | 17 +++ ADBIndexedTableView/ADBIndexedTableView.h | 78 ++++++++++ ADBIndexedTableView/ADBIndexedTableView.m | 140 ++++++++++++++++++ ADBIndexedTableView/ADBMessageInterceptor.h | 55 +++++++ ADBIndexedTableView/ADBMessageInterceptor.m | 35 +++++ .../project.pbxproj | 20 +++ ADBIndexedTableViewDemo/ADBViewController.h | 6 + ADBIndexedTableViewDemo/ADBViewController.m | 58 +++++++- .../en.lproj/MainStoryboard.storyboard | 27 +++- README.md | 72 +++++++++ 10 files changed, 501 insertions(+), 7 deletions(-) create mode 100644 .gitignore create mode 100644 ADBIndexedTableView/ADBIndexedTableView.h create mode 100644 ADBIndexedTableView/ADBIndexedTableView.m create mode 100644 ADBIndexedTableView/ADBMessageInterceptor.h create mode 100644 ADBIndexedTableView/ADBMessageInterceptor.m create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbcf904 --- /dev/null +++ b/.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 + diff --git a/ADBIndexedTableView/ADBIndexedTableView.h b/ADBIndexedTableView/ADBIndexedTableView.h new file mode 100644 index 0000000..6e51fdf --- /dev/null +++ b/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 +#import "ADBMessageInterceptor.h" + +@class ADBIndexedTableView; + +@protocol ADBIndexedTableViewDataSource + +@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 __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 indexDataSource; + +@end diff --git a/ADBIndexedTableView/ADBIndexedTableView.m b/ADBIndexedTableView/ADBIndexedTableView.m new file mode 100644 index 0000000..44ffcbf --- /dev/null +++ b/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 )dataSource +{ + return (id )_dataSourceInterceptor; +} + +- (void)setDataSource:(id )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 diff --git a/ADBIndexedTableView/ADBMessageInterceptor.h b/ADBIndexedTableView/ADBMessageInterceptor.h new file mode 100644 index 0000000..b802c0b --- /dev/null +++ b/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 )delegate +// { +// return (id )_delegateInterceptor; +// } +// +// - (void)setDelegate:(id )delegate +// { +// [super setDelegate:nil]; +// +// _delegateInterceptor = [[ADBMessageInterceptor alloc] init]; +// [_delegateInterceptor setSecondChance:self]; +// [_delegateInterceptor setReceiver:delegate]; +// +// [super setDelegate:(id)_delegateInterceptor]; +// } +// + +#import + +@interface ADBMessageInterceptor : NSObject { + + id __weak _receiver; + id __weak _secondChance; +} + +@property (nonatomic, weak) id receiver; +@property (nonatomic, weak) id secondChance; + +@end diff --git a/ADBIndexedTableView/ADBMessageInterceptor.m b/ADBIndexedTableView/ADBMessageInterceptor.m new file mode 100644 index 0000000..6bcfea3 --- /dev/null +++ b/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 diff --git a/ADBIndexedTableViewDemo.xcodeproj/project.pbxproj b/ADBIndexedTableViewDemo.xcodeproj/project.pbxproj index 3060fcb..ed979f5 100644 --- a/ADBIndexedTableViewDemo.xcodeproj/project.pbxproj +++ b/ADBIndexedTableViewDemo.xcodeproj/project.pbxproj @@ -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 */ @@ -37,6 +39,10 @@ F0BF7BA81646CFDD00E5F0B5 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/MainStoryboard.storyboard; sourceTree = ""; }; F0BF7BAA1646CFDD00E5F0B5 /* ADBViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ADBViewController.h; sourceTree = ""; }; F0BF7BAB1646CFDD00E5F0B5 /* ADBViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ADBViewController.m; sourceTree = ""; }; + F0BF7BB31646CFF900E5F0B5 /* ADBIndexedTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ADBIndexedTableView.h; sourceTree = ""; }; + F0BF7BB41646CFF900E5F0B5 /* ADBIndexedTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ADBIndexedTableView.m; sourceTree = ""; }; + F0BF7BB51646CFF900E5F0B5 /* ADBMessageInterceptor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ADBMessageInterceptor.h; sourceTree = ""; }; + F0BF7BB61646CFF900E5F0B5 /* ADBMessageInterceptor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ADBMessageInterceptor.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,6 +62,7 @@ F0BF7B801646CFDD00E5F0B5 = { isa = PBXGroup; children = ( + F0BF7BB21646CFF900E5F0B5 /* ADBIndexedTableView */, F0BF7B951646CFDD00E5F0B5 /* ADBIndexedTableViewDemo */, F0BF7B8E1646CFDD00E5F0B5 /* Frameworks */, F0BF7B8C1646CFDD00E5F0B5 /* Products */, @@ -107,6 +114,17 @@ name = "Supporting Files"; sourceTree = ""; }; + F0BF7BB21646CFF900E5F0B5 /* ADBIndexedTableView */ = { + isa = PBXGroup; + children = ( + F0BF7BB31646CFF900E5F0B5 /* ADBIndexedTableView.h */, + F0BF7BB41646CFF900E5F0B5 /* ADBIndexedTableView.m */, + F0BF7BB51646CFF900E5F0B5 /* ADBMessageInterceptor.h */, + F0BF7BB61646CFF900E5F0B5 /* ADBMessageInterceptor.m */, + ); + path = ADBIndexedTableView; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -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; }; diff --git a/ADBIndexedTableViewDemo/ADBViewController.h b/ADBIndexedTableViewDemo/ADBViewController.h index 2644042..21932d4 100644 --- a/ADBIndexedTableViewDemo/ADBViewController.h +++ b/ADBIndexedTableViewDemo/ADBViewController.h @@ -7,7 +7,13 @@ // #import +#import "ADBIndexedTableView.h" @interface ADBViewController : UIViewController + + +@property (nonatomic, strong) IBOutlet ADBIndexedTableView *tableView; @end diff --git a/ADBIndexedTableViewDemo/ADBViewController.m b/ADBIndexedTableViewDemo/ADBViewController.m index d0ac085..b990df2 100644 --- a/ADBIndexedTableViewDemo/ADBViewController.m +++ b/ADBIndexedTableViewDemo/ADBViewController.m @@ -18,12 +18,64 @@ - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. + + NSDictionary *obj1 = @{ @"name" : @"Annalisa" }; + NSDictionary *obj2 = @{ @"name" : @"Fabio" }; + NSDictionary *obj3 = @{ @"name" : @"Zeff" }; + NSDictionary *obj4 = @{ @"name" : @"Andrei" }; + NSDictionary *obj5 = @{ @"name" : @"Alberto" }; + NSDictionary *obj6 = @{ @"name" : @"Debo" }; + NSDictionary *obj7 = @{ @"name" : @"Luca" }; + NSDictionary *obj8 = @{ @"name" : @"Monica" }; + NSDictionary *obj9 = @{ @"name" : @"Laura" }; + NSDictionary *obj10 = @{ @"name" : @"Giuseppe" }; + NSDictionary *obj11 = @{ @"name" : @"Lucia" }; + NSDictionary *obj12 = @{ @"name" : @"Sarah" }; + NSDictionary *obj13 = @{ @"name" : @"Vera" }; + + // unsorted objects + NSArray *objects = @[obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12, obj13]; + + [self.tableView reloadDataWithObjects:objects]; } -- (void)didReceiveMemoryWarning +#pragma mark - ADBIndexedTableViewDataSource + +- (NSString *)objectsFieldForIndexedTableView:(ADBIndexedTableView *)tableView { - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. + return @"name"; } +- (UITableViewCell *)indexedTableView:(ADBIndexedTableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath + objectAtIndexPath:(id)object +{ + static NSString *identifier = @"Cell"; + UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:identifier]; + + if (cell == nil) + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; + + NSDictionary *obj = object; + cell.textLabel.text = [NSString stringWithFormat:@"%@ (via indexDataSource)", [obj valueForKey:@"name"]]; + + return cell; +} + +// otherwise we can implement original tableView:cellForRowAtIndexPath: method + +//- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +//{ +// static NSString *identifier = @"Cell"; +// UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:identifier]; +// +// if (cell == nil) +// cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; +// +// NSDictionary *obj = [self.tableView objectAtIndexPath:indexPath]; +// cell.textLabel.text = [NSString stringWithFormat:@"%@ (via dataSource)", [obj valueForKey:@"name"]]; +// +// return cell; +//} + @end diff --git a/ADBIndexedTableViewDemo/en.lproj/MainStoryboard.storyboard b/ADBIndexedTableViewDemo/en.lproj/MainStoryboard.storyboard index 38c46a2..8081209 100644 --- a/ADBIndexedTableViewDemo/en.lproj/MainStoryboard.storyboard +++ b/ADBIndexedTableViewDemo/en.lproj/MainStoryboard.storyboard @@ -1,18 +1,37 @@ - + - + - + - + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba54ead --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +ADBIndexedTableView +=========================== + +Indexed UITableView using first letter objects property. +ADBIndexedTableView uses Objective-C runtime, introspection and message forwarding. + +Try out the included demo project. + +Simple usage: +- copy ADBIndexedTableView and ADBMessageInterceptor classes into your project +- import `ADBIndexedTableView.h` in your class +- create an ADBIndexedTableView as usually done with UITableView +- set up delegate and dataSource as usual (using Interface Builder or programmatically) +- set `indexDataSource` and implement `objectsFieldForIndexedTableView:` method (used for sorting and indexing) + +``` objective-c +#pragma mark - ADBIndexedTableViewDataSource +- (NSString *)objectsFieldForIndexedTableView:(ADBIndexedTableView *)tableView { return objectPropertyAsString; } +``` + +- dataSource `tableView:cellForRowAtIndexPath` implementation can retrieve object for the given indexPath using `objectAtIndexPath:` method +- implementing `indexedTableView:cellForRowAtIndexPath:objectAtIndexPath:` is required and it will be used only if `tableView:cellForRowAtIndexPath` implementation is not provided by the dataSource. + +``` objective-c +- (UITableViewCell *)indexedTableView:(ADBIndexedTableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath + objectAtIndexPath:(id)object { ... } +``` + +- reload the table sending unsorted objects via `reloadDataWithObjects:` method to let indexedTableView create the data structure. Use `reloadData` for subsequent reloadings. + +``` objective-c +{ + NSArray *objects = @[...] // new objects retrieved + [self.tableView reloadDataWithObjects:objects]; +} +``` + +![1](http://www.albertodebortoli.it/GitHub/ADBIndexedTableView/01.png) + +# License + +Licensed under the New BSD License. + +Copyright (c) 2012, Alberto De Bortoli +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Alberto De Bortoli nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL Alberto De Bortoli BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## Resources + +Info can be found on [my website](http://www.albertodebortoli.it), [and on Twitter](http://twitter.com/albertodebo). \ No newline at end of file