diff --git a/CodeGenTestApp/Base.lproj/EvenMoreExamples.storyboard b/CodeGenTestApp/Base.lproj/EvenMoreExamples.storyboard new file mode 100644 index 0000000..ab8f478 --- /dev/null +++ b/CodeGenTestApp/Base.lproj/EvenMoreExamples.storyboard @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CodeGenTestApp/Base.lproj/Main.storyboard b/CodeGenTestApp/Base.lproj/Main.storyboard index 5e99825..5890d81 100644 --- a/CodeGenTestApp/Base.lproj/Main.storyboard +++ b/CodeGenTestApp/Base.lproj/Main.storyboard @@ -1,7 +1,7 @@ - + - + @@ -40,7 +40,7 @@ - + @@ -71,7 +71,7 @@ - + @@ -80,18 +80,50 @@ - + + + + + + + + + + + + + + + + + + + + + @@ -126,4 +158,4 @@ - \ No newline at end of file + diff --git a/CodeGenTestApp/Base.lproj/MoreExamples.storyboard b/CodeGenTestApp/Base.lproj/MoreExamples.storyboard new file mode 100644 index 0000000..18fdbe2 --- /dev/null +++ b/CodeGenTestApp/Base.lproj/MoreExamples.storyboard @@ -0,0 +1,1380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CodeGenTestApp/CGTADetailViewController.h b/CodeGenTestApp/CGTADetailViewController.h index 5141430..8a9424f 100644 --- a/CodeGenTestApp/CGTADetailViewController.h +++ b/CodeGenTestApp/CGTADetailViewController.h @@ -11,5 +11,6 @@ @interface CGTADetailViewController : UIViewController @property (nonatomic, strong) UIImage *image; +@property (nonatomic, copy) NSString *countryName; @end diff --git a/CodeGenTestApp/CGTADetailViewController.m b/CodeGenTestApp/CGTADetailViewController.m index 0c8c741..625fade 100644 --- a/CodeGenTestApp/CGTADetailViewController.m +++ b/CodeGenTestApp/CGTADetailViewController.m @@ -9,11 +9,16 @@ #import "CGTADetailViewController.h" #import "CGTATestAppColorList.h" +#import "CGTAMainStoryboardIdentifiers.h" @interface CGTADetailViewController () @property (nonatomic, strong) IBOutlet UIImageView *imageView; +@property (weak, nonatomic) IBOutlet UILabel *tapLabel; +@property (weak, nonatomic) IBOutlet UILabel *countryNameLabel; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *countryNameTopConstraint; +@property (nonatomic) BOOL countryNameVisible; @end @@ -26,6 +31,36 @@ - (void)setImage:(UIImage *)image; [self updateView]; } +- (void)setCountryName:(NSString *)countryName; +{ + _countryName = countryName; + [self updateView]; +} + +- (void)setCountryNameVisible:(BOOL)countryNameVisible; +{ + [self setCountryNameVisible:countryNameVisible animated:NO]; +} + +- (void)setCountryNameVisible:(BOOL)countryNameVisible animated:(BOOL)animated; +{ + _countryNameVisible = countryNameVisible; + + // the label was positioned perfectly via the storyboard, so now we can restore + // this positioning simply by refering to the constant that was generated for us! + self.countryNameTopConstraint.constant = countryNameVisible ? [self countryNameTopConstraintOriginalConstant] : 0; + + if (animated) { + [UIView animateWithDuration:0.2 + animations:^{ + self.tapLabel.alpha = countryNameVisible ? 0 : 1; + [self.view layoutIfNeeded]; + }]; + } else { + self.tapLabel.alpha = countryNameVisible ? 0 : 1; + } +} + - (void)viewDidLoad; { [self updateView]; @@ -37,11 +72,21 @@ - (void)viewDidLoad; layer.colors = @[(id)[UIColor whiteColor].CGColor, (id)[CGTATestAppColorList tealColor].CGColor]; layer.frame = self.view.layer.bounds; [self.view.layer insertSublayer:layer atIndex:0]; + + self.countryNameVisible = NO; } - (void)updateView; { self.imageView.image = self.image; + self.countryNameLabel.text = self.countryName; +} + +- (IBAction)imageTapped:(UITapGestureRecognizer *)sender; +{ + if (sender.state == UIGestureRecognizerStateEnded) { + [self setCountryNameVisible:!self.countryNameVisible animated:YES]; + } } @end diff --git a/CodeGenTestApp/CGTAFlagCollectionViewCell.h b/CodeGenTestApp/CGTAFlagCollectionViewCell.h new file mode 100644 index 0000000..6d4a9b7 --- /dev/null +++ b/CodeGenTestApp/CGTAFlagCollectionViewCell.h @@ -0,0 +1,8 @@ +#import + +@interface CGTAFlagCollectionViewCell : UICollectionViewCell + +@property (nonatomic, weak) IBOutlet UIImageView *imageView; +@property (nonatomic, copy) NSString *countryName; + +@end diff --git a/CodeGenTestApp/CGTAFlagCollectionViewCell.m b/CodeGenTestApp/CGTAFlagCollectionViewCell.m new file mode 100644 index 0000000..d529067 --- /dev/null +++ b/CodeGenTestApp/CGTAFlagCollectionViewCell.m @@ -0,0 +1,4 @@ +#import "CGTAFlagCollectionViewCell.h" + +@implementation CGTAFlagCollectionViewCell +@end \ No newline at end of file diff --git a/CodeGenTestApp/CGTAImagesCatalog+RuntimeHackery.h b/CodeGenTestApp/CGTAImagesCatalog+RuntimeHackery.h index b9c1bb8..74838e6 100644 --- a/CodeGenTestApp/CGTAImagesCatalog+RuntimeHackery.h +++ b/CodeGenTestApp/CGTAImagesCatalog+RuntimeHackery.h @@ -11,6 +11,7 @@ @interface CGTAImagesCatalog (RuntimeHackery) ++ (NSArray *)allImageNames; + (NSArray *)allImages; @end diff --git a/CodeGenTestApp/CGTAImagesCatalog+RuntimeHackery.m b/CodeGenTestApp/CGTAImagesCatalog+RuntimeHackery.m index c03380f..2206f66 100644 --- a/CodeGenTestApp/CGTAImagesCatalog+RuntimeHackery.m +++ b/CodeGenTestApp/CGTAImagesCatalog+RuntimeHackery.m @@ -15,13 +15,40 @@ @implementation CGTAImagesCatalog (RuntimeHackery) ++ (BOOL)isRuntimeHackeryMethod:(SEL)methodName; +{ + return sel_isEqual(methodName, @selector(isRuntimeHackeryMethod:)) || + sel_isEqual(methodName, @selector(allImageNames)) || + sel_isEqual(methodName, @selector(allImages)); +} + ++ (NSArray *)allImageNames; +{ + NSMutableArray *imageNames = [NSMutableArray array]; + unsigned int count; + Method *methods = class_copyMethodList(object_getClass(self), &count); + for (unsigned int index = 0; index < count; index++) { + SEL methodName = method_getName(methods[index]); + if ([self isRuntimeHackeryMethod:methodName]) { + continue; + } + NSString *imageName = NSStringFromSelector(method_getName(methods[index])); + // remove the "Image" suffix + imageName = [imageName substringToIndex:[imageName length] - [@"Image" length]]; + [imageNames addObject:[imageName uppercaseString]]; + } + free(methods); + return imageNames; +} + + (NSArray *)allImages; { NSMutableArray *images = [NSMutableArray array]; unsigned int count; Method *methods = class_copyMethodList(object_getClass(self), &count); for (unsigned int index = 0; index < count; index++) { - if (sel_isEqual(method_getName(methods[index]), _cmd)) { + SEL methodName = method_getName(methods[index]); + if ([self isRuntimeHackeryMethod:methodName]) { continue; } id image = method_invoke(self, methods[index]); diff --git a/CodeGenTestApp/CGTAMasterViewController.m b/CodeGenTestApp/CGTAMasterViewController.m index d1eb804..af75f3c 100644 --- a/CodeGenTestApp/CGTAMasterViewController.m +++ b/CodeGenTestApp/CGTAMasterViewController.m @@ -11,25 +11,33 @@ #import "CGTADetailViewController.h" #import "CGTAImagesCatalog+RuntimeHackery.h" #import "CGTAMainStoryboardIdentifiers.h" - - -@interface CGTAFlagCollectionViewCell : UICollectionViewCell - -@property (nonatomic, weak) IBOutlet UIImageView *imageView; - -@end - +#import "CGTAFlagCollectionViewCell.h" +#import "CGTATestAppColorList.h" +#import "CGTAMoreExamplesStoryboardIdentifiers.h" @interface CGTAMasterViewController () @property (nonatomic, weak) IBOutlet UISlider *cellSizeSlider; @property (nonatomic, strong) NSArray *flagImages; +@property (nonatomic, strong) NSArray *flagImageNames; @end @implementation CGTAMasterViewController +//#define ColorTester +#ifdef ColorTester +- (void)viewDidLoad +{ + [super viewDidLoad]; + UIViewController *controller = [CGTAMoreExamplesStoryboard instantiateColorTestScene]; + UIView *runTimeView = [controller.view viewWithTag:1]; + runTimeView.backgroundColor = [CGTATestAppColorList blueGenericRGBColor]; + [self.navigationController pushViewController:controller animated:NO]; +} +#endif + #pragma mark - NSObject - (void)awakeFromNib; @@ -41,9 +49,11 @@ - (void)awakeFromNib; - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender; { - if ([segue.identifier isEqualToString:CGTAMainStoryboardTapOnFlagIdentifier]) { + if ([segue.identifier isEqualToString:[self tapOnFlagSegueIdentifier]]) { CGTADetailViewController *detailViewController = segue.destinationViewController; - detailViewController.image = ((CGTAFlagCollectionViewCell *)sender).imageView.image; + CGTAFlagCollectionViewCell *cellSender = sender; + detailViewController.image = cellSender.imageView.image; + detailViewController.countryName = cellSender.countryName; } } @@ -58,17 +68,26 @@ - (IBAction)sliderValueChanged:(UISlider *)sender; - (NSArray *)flagImages; { - NSArray *allFlagImages = nil; - - // Initial version: full of strings that you have to type correctly! - // Misspell any of these and your app will crash on trying to add `nil` to an array. - allFlagImages = @[[UIImage imageNamed:@"USA"], [UIImage imageNamed:@"Canada"], [UIImage imageNamed:@"UK"], [UIImage imageNamed:@"Australia"]]; - - // New version: get the properly compiler-checked spelling from the image catalog. - allFlagImages = @[[CGTAImagesCatalog usaImage], [CGTAImagesCatalog canadaImage], [CGTAImagesCatalog ukImage], [CGTAImagesCatalog australiaImage]]; + if (!_flagImages) { + // What you might have done without this tool: full of strings that you have to type correctly! + // Misspell any of these and your app will crash on trying to add `nil` to an array. + _flagImages = @[[UIImage imageNamed:@"USA"], [UIImage imageNamed:@"Canada"], [UIImage imageNamed:@"UK"], [UIImage imageNamed:@"Australia"]]; + + // New version: get the properly compiler-checked spelling from the image catalog. + _flagImages = @[[CGTAImagesCatalog usaImage], [CGTAImagesCatalog canadaImage], [CGTAImagesCatalog ukImage], [CGTAImagesCatalog australiaImage]]; + + // But really, why not use a little runtime hackery because we can? + _flagImages = [CGTAImagesCatalog allImages]; + } + return _flagImages; +} - // But really, why not use a little runtime hackery because we can? - return [CGTAImagesCatalog allImages]; +- (NSArray *)flagImageNames; +{ + if (!_flagImageNames) { + _flagImageNames = [CGTAImagesCatalog allImageNames]; + } + return _flagImageNames; } #pragma mark - UICollectionViewDataSource @@ -85,13 +104,17 @@ - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSe - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath; { - CGTAFlagCollectionViewCell *cell = (CGTAFlagCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CGTAMainStoryboardImageCellIdentifier forIndexPath:indexPath]; + CGTAFlagCollectionViewCell *cell = nil; + + // What you might have done without this tool: we must type in the identifier, and have no guarantees as to which class it returns + cell = (CGTAFlagCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"Image Cell" forIndexPath:indexPath]; + + // New version: class extension which returns the exact type we are expecting + cell = [self dequeueImageCellForIndexPath:indexPath ofCollectionView:collectionView]; + cell.imageView.image = self.flagImages[indexPath.item]; + cell.countryName = self.flagImageNames[indexPath.item]; return cell; } @end - - -@implementation CGTAFlagCollectionViewCell -@end diff --git a/CodeGenTestApp/CGTATableViewController.h b/CodeGenTestApp/CGTATableViewController.h new file mode 100644 index 0000000..b23fc8e --- /dev/null +++ b/CodeGenTestApp/CGTATableViewController.h @@ -0,0 +1,5 @@ +#import + +@interface CGTATableViewController : UITableViewController + +@end diff --git a/CodeGenTestApp/CGTATableViewController.m b/CodeGenTestApp/CGTATableViewController.m new file mode 100644 index 0000000..bc788f4 --- /dev/null +++ b/CodeGenTestApp/CGTATableViewController.m @@ -0,0 +1,10 @@ +#import "CGTATableViewController.h" + +@interface CGTATableViewController () + +@end + +@implementation CGTATableViewController + + +@end diff --git a/CodeGenTestApp/CodeGenTestApp.xcodeproj/project.pbxproj b/CodeGenTestApp/CodeGenTestApp.xcodeproj/project.pbxproj index 8f5de75..f0c22bb 100644 --- a/CodeGenTestApp/CodeGenTestApp.xcodeproj/project.pbxproj +++ b/CodeGenTestApp/CodeGenTestApp.xcodeproj/project.pbxproj @@ -21,6 +21,12 @@ A881854118A9B622002803FC /* CGTAImagesCatalog+RuntimeHackery.m in Sources */ = {isa = PBXBuildFile; fileRef = A838793518A05B6D00B386D6 /* CGTAImagesCatalog+RuntimeHackery.m */; }; A881854218A9B622002803FC /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = A83878E518A0367C00B386D6 /* main.m */; }; A881854418A9B663002803FC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A83878EB18A0367C00B386D6 /* Main.storyboard */; }; + AA08DDC21914774A00141224 /* CGTATableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AA08DDC11914774A00141224 /* CGTATableViewController.m */; }; + AA509EE318F85CA5000F4136 /* CGTAFlagCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = AA509EE218F85CA5000F4136 /* CGTAFlagCollectionViewCell.m */; }; + AA520FA6191D7D5E005C913E /* CGTAEvenMoreExamplesStoryboardIdentifiers.m in Sources */ = {isa = PBXBuildFile; fileRef = AA520FA5191D7D5E005C913E /* CGTAEvenMoreExamplesStoryboardIdentifiers.m */; }; + AA72FDCA191D506E0092C2DA /* EvenMoreExamples.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA72FDC8191D506E0092C2DA /* EvenMoreExamples.storyboard */; }; + AA72FDCD191D509D0092C2DA /* MoreExamples.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA72FDCB191D509D0092C2DA /* MoreExamples.storyboard */; }; + AAA9F41518ED1A0C00BA7A27 /* CGTAMoreExamplesStoryboardIdentifiers.m in Sources */ = {isa = PBXBuildFile; fileRef = AAA9F41418ED1A0C00BA7A27 /* CGTAMoreExamplesStoryboardIdentifiers.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -95,6 +101,16 @@ A881852518A9B512002803FC /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS7.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; A881852718A9B520002803FC /* codegenutils.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = codegenutils.xcodeproj; path = ../codegenutils.xcodeproj; sourceTree = ""; }; A89D8FE617CFFDCE0077F2B5 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + AA08DDC01914774A00141224 /* CGTATableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CGTATableViewController.h; sourceTree = ""; }; + AA08DDC11914774A00141224 /* CGTATableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CGTATableViewController.m; sourceTree = ""; }; + AA509EE118F85CA5000F4136 /* CGTAFlagCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CGTAFlagCollectionViewCell.h; sourceTree = ""; }; + AA509EE218F85CA5000F4136 /* CGTAFlagCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CGTAFlagCollectionViewCell.m; sourceTree = ""; }; + AA520FA4191D7D5E005C913E /* CGTAEvenMoreExamplesStoryboardIdentifiers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CGTAEvenMoreExamplesStoryboardIdentifiers.h; sourceTree = ""; }; + AA520FA5191D7D5E005C913E /* CGTAEvenMoreExamplesStoryboardIdentifiers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CGTAEvenMoreExamplesStoryboardIdentifiers.m; sourceTree = ""; }; + AA72FDC9191D506E0092C2DA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/EvenMoreExamples.storyboard; sourceTree = ""; }; + AA72FDCC191D509D0092C2DA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MoreExamples.storyboard; sourceTree = ""; }; + AAA9F41318ED1A0C00BA7A27 /* CGTAMoreExamplesStoryboardIdentifiers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CGTAMoreExamplesStoryboardIdentifiers.h; sourceTree = ""; }; + AAA9F41418ED1A0C00BA7A27 /* CGTAMoreExamplesStoryboardIdentifiers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CGTAMoreExamplesStoryboardIdentifiers.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -117,11 +133,17 @@ A83878E818A0367C00B386D6 /* CGTAAppDelegate.h */, A83878E918A0367C00B386D6 /* CGTAAppDelegate.m */, A83878EB18A0367C00B386D6 /* Main.storyboard */, + AA72FDCB191D509D0092C2DA /* MoreExamples.storyboard */, + AA72FDC8191D506E0092C2DA /* EvenMoreExamples.storyboard */, A83878F418A0367C00B386D6 /* Images.xcassets */, A83878EE18A0367C00B386D6 /* CGTAMasterViewController.h */, A83878EF18A0367C00B386D6 /* CGTAMasterViewController.m */, A83878F118A0367C00B386D6 /* CGTADetailViewController.h */, A83878F218A0367C00B386D6 /* CGTADetailViewController.m */, + AA509EE118F85CA5000F4136 /* CGTAFlagCollectionViewCell.h */, + AA509EE218F85CA5000F4136 /* CGTAFlagCollectionViewCell.m */, + AA08DDC01914774A00141224 /* CGTATableViewController.h */, + AA08DDC11914774A00141224 /* CGTATableViewController.m */, A838791718A0456500B386D6 /* Derived Sources */, A83878E018A0367C00B386D6 /* Supporting Files */, ); @@ -146,6 +168,10 @@ A838791518A0455E00B386D6 /* CGTAImagesCatalog.m */, A838793118A0557E00B386D6 /* CGTAMainStoryboardIdentifiers.h */, A838793218A0557E00B386D6 /* CGTAMainStoryboardIdentifiers.m */, + AAA9F41318ED1A0C00BA7A27 /* CGTAMoreExamplesStoryboardIdentifiers.h */, + AAA9F41418ED1A0C00BA7A27 /* CGTAMoreExamplesStoryboardIdentifiers.m */, + AA520FA4191D7D5E005C913E /* CGTAEvenMoreExamplesStoryboardIdentifiers.h */, + AA520FA5191D7D5E005C913E /* CGTAEvenMoreExamplesStoryboardIdentifiers.m */, A838791818A04AB300B386D6 /* CGTATestAppColorList.h */, A838791918A04AB300B386D6 /* CGTATestAppColorList.m */, A838793418A05B6D00B386D6 /* CGTAImagesCatalog+RuntimeHackery.h */, @@ -280,7 +306,9 @@ buildActionMask = 2147483647; files = ( A881854418A9B663002803FC /* Main.storyboard in Resources */, + AA72FDCA191D506E0092C2DA /* EvenMoreExamples.storyboard in Resources */, A881853A18A9B614002803FC /* Images.xcassets in Resources */, + AA72FDCD191D509D0092C2DA /* MoreExamples.storyboard in Resources */, A83878E418A0367C00B386D6 /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -309,13 +337,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AAA9F41518ED1A0C00BA7A27 /* CGTAMoreExamplesStoryboardIdentifiers.m in Sources */, A881853918A9B609002803FC /* CGTAAppDelegate.m in Sources */, A881853C18A9B622002803FC /* CGTAMasterViewController.m in Sources */, A881853F18A9B622002803FC /* CGTAMainStoryboardIdentifiers.m in Sources */, A881854118A9B622002803FC /* CGTAImagesCatalog+RuntimeHackery.m in Sources */, + AA509EE318F85CA5000F4136 /* CGTAFlagCollectionViewCell.m in Sources */, A881853E18A9B622002803FC /* CGTAImagesCatalog.m in Sources */, A881854018A9B622002803FC /* CGTATestAppColorList.m in Sources */, + AA520FA6191D7D5E005C913E /* CGTAEvenMoreExamplesStoryboardIdentifiers.m in Sources */, A881854218A9B622002803FC /* main.m in Sources */, + AA08DDC21914774A00141224 /* CGTATableViewController.m in Sources */, A881853D18A9B622002803FC /* CGTADetailViewController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -357,6 +389,22 @@ name = Main.storyboard; sourceTree = ""; }; + AA72FDC8191D506E0092C2DA /* EvenMoreExamples.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AA72FDC9191D506E0092C2DA /* Base */, + ); + name = EvenMoreExamples.storyboard; + sourceTree = ""; + }; + AA72FDCB191D509D0092C2DA /* MoreExamples.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AA72FDCC191D509D0092C2DA /* Base */, + ); + name = MoreExamples.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ diff --git a/CodeGenTestApp/Test App.clr b/CodeGenTestApp/Test App.clr index cb9c103..6c7256f 100644 Binary files a/CodeGenTestApp/Test App.clr and b/CodeGenTestApp/Test App.clr differ diff --git a/README.md b/README.md index 31f1fef..2614705 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,91 @@ We already fixed the part about code reuse with `objc-colordump`, and now we can Call `objc-identifierconstants` with the `.storyboard` paths as arguments from the directory into which it should output the code. -For a storyboard named "Foo" with view controller identifier "Bar" and segue identifier "Baz" somewhere in it, you'll get `FooStoryboardIdenfitiers.h` and `FooStoryboardIdentifiers.m` with `extern NSString *const FooStoryboardBarIdentifier` and `extern NSString *const FooStoryboardBazIdentifier` in it. Put them in your DerivedSources folder and you're good to go. +For a storyboard named "Foo", you'll get `FooStoryboardIdenfitiers.h` and `FooStoryboardIdentifiers.m`. Put them in your DerivedSources folder and you're good to go. + +The tool will first attempt to add category methods on your existing view controller subclasses. If it does not find a given class, it resorts to outputting constants. For example, a segue identifier "Baz" will generate `extern NSString *const FooStoryboardBazIdentifier`. + +### Examples + +#### Storyboard scenes + +```objective-c +// old way: +UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; +id viewController = [storyboard instantiateViewControllerWithIdentifier:@"Detail View Controller"]; + +// new way: +MYDetailViewController *detailViewController = [MYMainStoryboard instantiateDetailViewController]; +``` + +#### Segues + +```objective-c +// old way: +[self performSegueWithIdentifier:@"Show Details Segue" sender:nil]; + +// new way: +[self performShowDetailsSegue]; +``` + +#### Table view cells + +```objective-c +// old way: +id cell = [tableView dequeueReusableCellWithIdentifier:@"Image Cell" forIndexPath:indexPath]; + +// new way: +MYImageCell *imageCell = [self dequeueImageCellForIndexPath:indexPath ofTableView:tableView]; +``` + +#### Auto Layout constraint constants + +```objective-c +// old way: +self.labelHeightConstraint.constant = showLabel ? 40.0f : 0.0f; + +// new way: +self.labelHeightConstraint.constant = showLabel ? [self labelHeightConstraintOriginalConstant] : 0.0f; +``` + +#### Other features + +```objective-c +// getting a storyboard +UIStoryboard *storyboard = [MYCustomStoryboard storyboard]; + +// getting a scene identifier +NSString *sceneIdentifier = [MYCustomStoryboard <#sceneID#>Identifier]; + +// instantiate initial storyboard view controller +MYCustomViewController *viewController = [MYCustomStoryboard instantiateInitialViewController]; + +// getting a segue identifier +NSString *segueID = [self <#segueID#>Identifier]; + +// getting a cell/view identifier (for UITableView or UICollectionView) +NSString *cellID = [self <#cellID#>Identifier]; + +// dequeue collection view reusable cell +MYCustomCell *cell = [self dequeue<#cellID#>ForIndexPath:indexPath ofCollectionView:collectionView]; + +// dequeue collectiov view reusable view +MYCustomView *view = [self dequeue<#viewID#>ForIndexPath:indexPath ofKind:kind ofCollectionView:collectionView]; +``` + +**Note:** in the above examples, `self` refers to a subclass of UIViewController. + +### Generated names + +Here is a list of the generated names along with naming recommendations. + +- **Storyboard:** "Foo_iPhone.storyboard" -> *MYFoo_iPhone*Storyboard +- **Scene:** "Foo Controller" -> instantiate*FooController*, *fooController*Identifier +- ... or "Foo Scene" -> instantiate*FooScene*, *fooScene*Identifier +- **Segue:** "Foo Segue" -> perform*FooSegue*, *fooSegue*Identifier +- **Reusable Cell/View:** "Foo Cell" -> dequeue*FooCell*ForIndexPath, *fooCell*Identifier +- **Constraint:** "fooConstraint" -> *fooConstraint*OriginalConstant + ## Command-line options (common to all three tools) diff --git a/Shared/CGUCodeGenTool.h b/Shared/CGUCodeGenTool.h index 75c003a..1017e7d 100644 --- a/Shared/CGUCodeGenTool.h +++ b/Shared/CGUCodeGenTool.h @@ -18,10 +18,15 @@ @property (copy) NSURL *inputURL; @property (copy) NSString *classPrefix; +@property (copy) NSSet *headerFilesFound; @property BOOL targetiOS6; @property BOOL skipClassDeclaration; @property (copy) NSString *className; +/// An array of strings such as "" which will be imported at the top of the .h file. +@property (strong) NSMutableSet *interfaceImports; +/// A dictionary of class names as keys (NSString *), and CGUClass instances as values. +@property (strong) NSMutableDictionary *classes; @property (strong) NSMutableArray *interfaceContents; @property (strong) NSMutableArray *implementationContents; @@ -29,6 +34,48 @@ - (void)writeOutputFiles; -- (NSString *)methodNameForKey:(NSString *)key; +/// @param camelCase If this is true, it will use @e camelCase, otherwise it will use @e TitleCase. ++ (NSString *)identifierNameForKey:(NSString *)key camelCase:(BOOL)camelCase; + +@end + + + +@interface CGUClass : NSObject + +@property (copy) NSString *categoryName; +/// An array of CGUMethods +@property (strong) NSMutableArray *methods; +@property (copy) NSString *name; +@property (copy) NSString *superClassName; + +- (NSString *)interfaceCode; +- (NSString *)implementationCode; + +@end + + + +@interface CGUMethod : NSObject + +/// A string that will be added right before the declaration of the method in the .h file with '///' prepended to each line. +@property NSString *documentation; + +/// Specifies if this is a class method rather than an instance method. +@property BOOL classMethod; + +/// E.g. "NSString *" +/// If this is nil, it will be replaced with void. +@property NSString *returnType; + +/// E.g. "doSomethingWithString:(NSString *)myString andNumber:(NSInteger)number" +@property (copy) NSString *nameAndArguments; +@property (copy) NSString *body; + +// E.g. "doSomethingWithString:andNumber:" +@property (readonly) NSString *name; + +- (NSString *)interfaceCode; +- (NSString *)implementationCode; @end diff --git a/Shared/CGUCodeGenTool.m b/Shared/CGUCodeGenTool.m index 07f07a0..f9d1b76 100644 --- a/Shared/CGUCodeGenTool.m +++ b/Shared/CGUCodeGenTool.m @@ -11,6 +11,11 @@ #import +typedef NS_ENUM(NSInteger, CGUClassType) { + CGUClassType_Definition, + CGUClassType_Category + // TODO: add extension in the future? +}; @interface CGUCodeGenTool () @@ -18,6 +23,15 @@ @interface CGUCodeGenTool () @end +@interface CGUClass () + +/// The class type is determined by the following: +/// - If there is a categoryName, this is a category. +/// - Otherwise this is a class definition. +@property (readonly) CGUClassType classType; + +@end + @implementation CGUCodeGenTool @@ -34,6 +48,7 @@ + (int)startWithArgc:(int)argc argv:(const char **)argv; NSString *classPrefix = @""; BOOL target6 = NO; NSMutableArray *inputURLs = [NSMutableArray array]; + NSMutableSet *headerFilesFound = [NSMutableSet set]; while ((opt = getopt(argc, (char *const*)argv, "o:f:p:h6")) != -1) { switch (opt) { @@ -91,6 +106,10 @@ + (int)startWithArgc:(int)argc argv:(const char **)argv; if ([url.pathExtension isEqualToString:[self inputFileExtension]]) { [inputURLs addObject:url]; } + if ([url.pathExtension isEqualToString:@"h"]) { + NSString *fileName = [url lastPathComponent]; + [headerFilesFound addObject:[fileName substringToIndex:[fileName length] - 2]]; + } } } @@ -101,6 +120,7 @@ + (int)startWithArgc:(int)argc argv:(const char **)argv; CGUCodeGenTool *target = [self new]; target.inputURL = url; + target.headerFilesFound = headerFilesFound; target.targetiOS6 = target6; target.classPrefix = classPrefix; target.toolName = [[NSString stringWithUTF8String:argv[0]] lastPathComponent]; @@ -137,6 +157,16 @@ - (void)writeOutputFiles; }]; NSMutableString *interface = [NSMutableString stringWithFormat:@"//\n// This file is generated from %@ by %@.\n// Please do not edit.\n//\n\n#import \n\n\n", self.inputURL.lastPathComponent, self.toolName]; + + for (NSString *import in self.interfaceImports) { + [interface appendFormat:@"#import %@\n", import]; + } + [interface appendString:@"\n"]; + + for (NSString *className in self.classes) { + CGUClass *class = self.classes[className]; + [interface appendFormat:@"%@\n", [class interfaceCode]]; + } if (self.skipClassDeclaration) { [interface appendString:[self.interfaceContents componentsJoinedByString:@""]]; @@ -149,6 +179,12 @@ - (void)writeOutputFiles; } NSMutableString *implementation = [NSMutableString stringWithFormat:@"//\n// This file is generated from %@ by %@.\n// Please do not edit.\n//\n\n#import \"%@\"\n\n\n", self.inputURL.lastPathComponent, self.toolName, classNameH]; + + for (NSString *className in self.classes) { + CGUClass *class = self.classes[className]; + [implementation appendFormat:@"%@\n", [class implementationCode]]; + } + if (self.skipClassDeclaration) { [implementation appendString:[self.implementationContents componentsJoinedByString:@""]]; } else { @@ -162,19 +198,206 @@ - (void)writeOutputFiles; NSLog(@"Wrote %@ to %@", self.className, currentDirectory); } -- (NSString *)methodNameForKey:(NSString *)key; ++ (NSString *)identifierNameForKey:(NSString *)key camelCase:(BOOL)camelCase; { - NSMutableString *mutableKey = [key mutableCopy]; - // If the string is already all caps, it's an abbrevation. Lowercase the whole thing. - // Otherwise, camelcase it by lowercasing the first character. - if ([mutableKey isEqualToString:[mutableKey uppercaseString]]) { - mutableKey = [[mutableKey lowercaseString] mutableCopy]; + /* + Standard examples (camelCase): + My Scene Identifier -> mySceneIdentifier + my scene identifier -> mySceneIdentifier + + Abbreviation examples (camelCase only feature) (i.e. uppercase first word -> lowercase): + USA -> usa + usa -> usa + USA2 -> usa2 // considered uppercased first word + USoA -> uSoA + usa image -> usaImage + USA image -> usaImage + image USA -> imageUSA // abbreviations are only searched for in the first word + + Number handling examples (camelCase): + 2url -> _2url // identifiers cannot begin with a number + A2test -> A2test // A2 is in uppercase, so it assumes it is a namespace + 2Atest -> _2Atest + 22test -> _22test + + Special character handling (camelCase): + usa image -> usaImage // space acts as word separator + usa-image -> usaImage // any non alphanumeric character acts as a word separator + usa_image -> usa_image // underscores are preserved + + More examples (camelCase): + NSString -> nSString + NS String -> nsString + my url -> myUrl + my uRL -> myURL + my URL -> myURL + my u r l -> myURL + myUrl list -> myUrlList + myUrl MyUrl -> myUrlMyUrl + myUrl NSUrl -> myUrlNSUrl + myURL NSUrl -> myURLNSUrl + */ + + NSRegularExpression *wordsRegex = [NSRegularExpression regularExpressionWithPattern:@"\\w+" options:0 error:NULL]; + NSArray *wordMatches = [wordsRegex matchesInString:key options:0 range:NSMakeRange(0, key.length)]; + NSMutableArray *words = [NSMutableArray array]; + for (NSTextCheckingResult *wordMatch in wordMatches) { + [words addObject:[key substringWithRange:wordMatch.range]]; + } + NSAssert([words count] > 0, @"Must have at least one character in an identifier."); + + // Process the first word. + if (camelCase) { + // If the first word is all caps, it's an abbrevation. Lowercase it. + // Otherwise, camelcase it by lowercasing the first character. + if ([words[0] isEqualToString:[words[0] uppercaseString]]) { + words[0] = [words[0] lowercaseString]; + } else { + words[0] = [words[0] stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[words[0] substringToIndex:1] lowercaseString]]; + } } else { - [mutableKey replaceCharactersInRange:NSMakeRange(0, 1) withString:[[key substringToIndex:1] lowercaseString]]; + words[0] = [words[0] stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[words[0] substringToIndex:1] uppercaseString]]; + } + + // If the first word starts with a number, prefix with underscore. + if ([words[0] rangeOfCharacterFromSet:[NSCharacterSet decimalDigitCharacterSet]].location == 0) { + words[0] = [NSString stringWithFormat:@"_%@", words[0]]; + } + + // Process the remaining words (uppercase first letter of each word). + for (NSInteger i = 1; i < [words count]; i++) { + words[i] = [words[i] stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[words[i] substringToIndex:1] uppercaseString]]; } - [mutableKey replaceOccurrencesOfString:@" " withString:@"" options:0 range:NSMakeRange(0, mutableKey.length)]; - [mutableKey replaceOccurrencesOfString:@"~" withString:@"" options:0 range:NSMakeRange(0, mutableKey.length)]; - return [mutableKey copy]; + + return [words componentsJoinedByString:@""]; } @end + + + +@implementation CGUClass + +- (instancetype)init; +{ + self = [super init]; + if (self) { + self.methods = [NSMutableArray array]; + } + return self; +} + +- (void)sortMethods; +{ + [self.methods sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { + CGUMethod *method1 = obj1; + CGUMethod *method2 = obj2; + + // 1. sort class methods first, then instance methods + if (method1.classMethod && !method2.classMethod) { + return NSOrderedAscending; + } else if (!method1.classMethod && method2.classMethod) { + return NSOrderedDescending; + } + + // 2. sort by the method name + return [method1.nameAndArguments caseInsensitiveCompare:method2.nameAndArguments]; + }]; +} + +- (NSString *)interfaceCode; +{ + if (self.methods.count == 0 && self.classType != CGUClassType_Definition) { + // no need to print a category/extension if it has no methods + return @""; + } + + [self sortMethods]; + + NSMutableString *result = [NSMutableString string]; + if (self.classType == CGUClassType_Definition) { + [result appendFormat:@"@interface %@ : %@\n", self.name, self.superClassName ?: @"NSObject"]; + } else { + [result appendFormat:@"@interface %@ (%@)\n", self.name, self.categoryName]; + } + for (CGUMethod *method in self.methods) { + [result appendString:[method interfaceCode]]; + [result appendString:@"\n"]; + } + [result appendFormat:@"@end\n"]; + return result; +} + +- (NSString *)implementationCode; +{ + if (self.methods.count == 0 && self.classType != CGUClassType_Definition) { + // no need to print a category/extension if it has no methods + return @""; + } + + [self sortMethods]; + + NSMutableString *result = [NSMutableString string]; + if (self.classType == CGUClassType_Definition) { + [result appendFormat:@"@implementation %@\n", self.name]; + } else { + [result appendFormat:@"@implementation %@ (%@)\n", self.name, self.categoryName]; + } + for (CGUMethod *method in self.methods) { + [result appendString:[method implementationCode]]; + [result appendString:@"\n"]; + } + [result appendFormat:@"@end\n"]; + return result; +} + +- (CGUClassType)classType; +{ + if (self.categoryName.length > 0) { + return CGUClassType_Category; + } else { + return CGUClassType_Definition; + } +} + +@end + + + +@implementation CGUMethod + +- (NSString *)name; +{ + if ([self.nameAndArguments rangeOfString:@":"].location == NSNotFound) { + return self.nameAndArguments; + } else { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\w+:" options:0 error:NULL]; + NSArray *matches = [regex matchesInString:self.nameAndArguments options:0 range:NSMakeRange(0, [self.nameAndArguments length])]; + NSMutableString *name = [NSMutableString string]; + for (NSTextCheckingResult *match in matches) { + [name appendString:[self.nameAndArguments substringWithRange:match.range]]; + } + return name; + } +} + +- (NSString *)interfaceCode; +{ + NSMutableString *interfaceCode = [NSMutableString string]; + if (self.documentation) { + NSArray *lines = [self.documentation componentsSeparatedByString:@"\n"]; + for (NSString *line in lines) { + [interfaceCode appendFormat:@"/// %@\n", line]; + } + } + [interfaceCode appendFormat:@"%@ (%@)%@;", (self.classMethod ? @"+" : @"-"), self.returnType ?: @"void", self.nameAndArguments]; + return [interfaceCode copy]; +} + +- (NSString *)implementationCode; +{ + // TODO: indent each line in the body? + return [NSString stringWithFormat:@"%@ (%@)%@ {\n%@\n}", (self.classMethod ? @"+" : @"-"), self.returnType ?: @"void", self.nameAndArguments, self.body]; +} + +@end \ No newline at end of file diff --git a/assetgen/AGCatalogParser.m b/assetgen/AGCatalogParser.m index ef8a100..a47e3ac 100644 --- a/assetgen/AGCatalogParser.m +++ b/assetgen/AGCatalogParser.m @@ -65,7 +65,7 @@ - (void)findImageSetURLs; - (void)parseImageSetAtURL:(NSURL *)url; { NSString *imageSetName = [[url lastPathComponent] stringByDeletingPathExtension]; - NSString *methodName = [self methodNameForKey:imageSetName]; + NSString *methodName = [CGUCodeGenTool identifierNameForKey:imageSetName camelCase:YES]; NSURL *contentsURL = [url URLByAppendingPathComponent:@"Contents.json"]; NSData *contentsData = [NSData dataWithContentsOfURL:contentsURL options:NSDataReadingMappedIfSafe error:NULL]; if (!contentsData) { diff --git a/colordump/CDColorListDumper.m b/colordump/CDColorListDumper.m index d77d4cb..cbf97ac 100644 --- a/colordump/CDColorListDumper.m +++ b/colordump/CDColorListDumper.m @@ -33,15 +33,15 @@ - (void)startWithCompletionHandler:(dispatch_block_t)completionBlock; for (NSString *key in colorList.allKeys) { NSColor *color = [colorList colorWithKey:key]; - if (![color.colorSpaceName isEqualToString:NSDeviceRGBColorSpace]) { - printf("Color %s isn't device RGB. Skipping.", [key UTF8String]); + if (![color.colorSpaceName isEqualToString:NSCalibratedRGBColorSpace]) { + printf("Color %s isn't generic calibrated RGB. Skipping.", [key UTF8String]); continue; } CGFloat r, g, b, a; [color getRed:&r green:&g blue:&b alpha:&a]; - NSString *declaration = [NSString stringWithFormat:@"+ (UIColor *)%@Color;\n", [self methodNameForKey:key]]; + NSString *declaration = [NSString stringWithFormat:@"+ (UIColor *)%@Color;\n", [CGUCodeGenTool identifierNameForKey:key camelCase:YES]]; [self.interfaceContents addObject:declaration]; NSMutableString *method = [declaration mutableCopy]; diff --git a/identifierconstants/IDStoryboardDumper.m b/identifierconstants/IDStoryboardDumper.m index c2af334..ca95bdb 100644 --- a/identifierconstants/IDStoryboardDumper.m +++ b/identifierconstants/IDStoryboardDumper.m @@ -9,21 +9,112 @@ #import "IDStoryboardDumper.h" +@interface IDStoryboardDumper () +/// Keys: NSString of class name; Values: @(BOOL) stating if it was successfully imported or not +@property (strong) NSMutableDictionary *classesImported; +@end -@interface NSString (IDStoryboardAddition) +@implementation NSString (IDStoryboardAddition) - (NSString *)IDS_titlecaseString; +{ + return [CGUCodeGenTool identifierNameForKey:self camelCase:NO]; +} + +- (NSString *)IDS_camelcaseString; +{ + return [CGUCodeGenTool identifierNameForKey:self camelCase:YES]; +} @end @implementation IDStoryboardDumper +- (void)removeDuplicatedExtensionMethods; +{ + // When there are duplicate category methods, the behavior is undefined. Therefore, we want to remove any duplicates. + + /// Stores all the methods that were added as NSStrings in the form "_" (e.g. "MYViewController_dequeueMYSampleCellForIndexPath:"). + static NSMutableSet *classCategoryMethodsAdded = nil; + if (classCategoryMethodsAdded == nil) { + classCategoryMethodsAdded = [NSMutableSet set]; + } + + for (NSString *className in self.classes) { + CGUClass *class = self.classes[className]; + NSMutableArray *methodsToRemove = [NSMutableArray array]; + NSMutableSet *methodNamesRemoved = [NSMutableSet set]; + for (CGUMethod *method in class.methods) { + NSString *methodName = method.name; + NSString *classMethodKey = [NSString stringWithFormat:@"%@_%@", className, methodName]; + if ([classCategoryMethodsAdded containsObject:classMethodKey]) { + [methodsToRemove addObject:method]; + [methodNamesRemoved addObject:methodName]; + } else { + [classCategoryMethodsAdded addObject:classMethodKey]; + } + } + + if ([methodNamesRemoved count] > 0) { + NSLog(@"Warning: Found (and removed) %lu duplicate identifier(s) associated with class %@. These are the methods that would have been duplicated: %@.", (unsigned long)[methodNamesRemoved count], className, [[methodNamesRemoved allObjects] componentsJoinedByString:@", "]); + } + + [class.methods removeObjectsInArray:methodsToRemove]; + } +} + + (NSString *)inputFileExtension; { return @"storyboard"; } +/// element is any that have a customClass attribute and contain the valid default class name as their name (e.g. viewController, or tableViewCell) +- (NSString *)classTypeForElement:(NSXMLElement *)element importedCustomClass:(out BOOL *)importedCustomClass; +{ + if (importedCustomClass) { + *importedCustomClass = NO; + } + + NSString *customClass = [[element attributeForName:@"customClass"] stringValue]; + if (customClass && [self importClass:customClass]) { + // we can use the custom class + if (importedCustomClass) { + *importedCustomClass = YES; + } + return customClass; + } else { + // element.name is the view controller type (e.g. tableViewController, navigationController, etc.) + NSString *defaultClass = [@"UI" stringByAppendingString:[element.name IDS_titlecaseString]]; + return defaultClass; + } +} + +/// You may call this method multiple times with the same className without it having to search the search path each time. It will only search once and cache the result. +- (BOOL)importClass:(NSString *)className; +{ + if (!self.classesImported) { + self.classesImported = [NSMutableDictionary dictionary]; + } + + if (self.classesImported[className]) { + // if we have arleady tried searching for this class, there is no need to search for it again + return [self.classesImported[className] boolValue]; + } + + BOOL successfullyImported = NO; + if ([self.headerFilesFound containsObject:className]) { + [self.interfaceImports addObject:[NSString stringWithFormat:@"\"%@.h\"", className]]; + successfullyImported = YES; + } + + if (!successfullyImported) { + NSLog(@"Unable to find class interface for '%@'. Reverting to global string constant behavior.", className); + } + self.classesImported[className] = @(successfullyImported); + return successfullyImported; +} + - (void)startWithCompletionHandler:(dispatch_block_t)completionBlock; { self.skipClassDeclaration = YES; @@ -42,12 +133,178 @@ - (void)startWithCompletionHandler:(dispatch_block_t)completionBlock; [identifiers addObjectsFromArray:reuseIdentifiers]; [identifiers addObjectsFromArray:segueIdentifiers]; + self.interfaceImports = [NSMutableSet set]; + self.classes = [NSMutableDictionary dictionary]; self.interfaceContents = [NSMutableArray array]; self.implementationContents = [NSMutableArray array]; - NSMutableDictionary *uniqueKeys = [NSMutableDictionary dictionary]; - uniqueKeys[[NSString stringWithFormat:@"%@%@StoryboardName", self.classPrefix, storyboardName]] = storyboardFilename; + NSMutableArray *viewControllers = [NSMutableArray array];; + [viewControllers addObjectsFromArray:[document nodesForXPath:@"//viewController" error:&error]]; + [viewControllers addObjectsFromArray:[document nodesForXPath:@"//tableViewController" error:&error]]; + [viewControllers addObjectsFromArray:[document nodesForXPath:@"//collectionViewController" error:&error]]; + [viewControllers addObjectsFromArray:[document nodesForXPath:@"//pageViewController" error:&error]]; + [viewControllers addObjectsFromArray:[document nodesForXPath:@"//navigationController" error:&error]]; + [viewControllers addObjectsFromArray:[document nodesForXPath:@"//tabBarController" error:&error]]; + // TODO: add support for GLKViewControllers + + [viewControllers sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { + NSString *storyboardIdentifier1 = [[[obj1 attributeForName:@"storyboardIdentifier"] stringValue] IDS_titlecaseString]; + NSString *storyboardIdentifier2 = [[[obj2 attributeForName:@"storyboardIdentifier"] stringValue] IDS_titlecaseString]; + return [storyboardIdentifier1 caseInsensitiveCompare:storyboardIdentifier2]; + }]; + + // generate the MYMainStoryboard class + CGUClass *storyboardClass = [CGUClass new]; + storyboardClass.name = [NSString stringWithFormat:@"%@%@Storyboard", self.classPrefix, storyboardName]; + storyboardClass.superClassName = @"NSObject"; + self.classes[storyboardClass.name] = storyboardClass; + + // output + [MYMainStoryboard storyboard] + CGUMethod *storyboardMethod = [CGUMethod new]; + storyboardMethod.classMethod = YES; + storyboardMethod.returnType = @"UIStoryboard *"; + storyboardMethod.nameAndArguments = @"storyboard"; + storyboardMethod.body = [NSString stringWithFormat:@" return [UIStoryboard storyboardWithName:@\"%@\" bundle:nil];", storyboardName]; + [storyboardClass.methods addObject:storyboardMethod]; + + NSString *initialViewControllerID = [[[document rootElement] attributeForName:@"initialViewController"] stringValue]; + if (initialViewControllerID) { + NSString *initialViewControllerClass = nil; + for (NSXMLElement *viewControllerElement in viewControllers) { + if ([[[viewControllerElement attributeForName:@"id"] stringValue] isEqualToString:initialViewControllerID]) { + // found initial view controller + initialViewControllerClass = [self classTypeForElement:viewControllerElement importedCustomClass:NULL]; + break; + } + } + + if (initialViewControllerClass) { + // output + [MYMainStoryboard instantiateInitialViewController] + CGUMethod *instantiateInitialViewControllerMethod = [CGUMethod new]; + instantiateInitialViewControllerMethod.classMethod = YES; + instantiateInitialViewControllerMethod.returnType = [NSString stringWithFormat:@"%@ *", initialViewControllerClass]; + instantiateInitialViewControllerMethod.nameAndArguments = @"instantiateInitialViewController"; + instantiateInitialViewControllerMethod.body = @" return [[self storyboard] instantiateInitialViewController];"; + [storyboardClass.methods addObject:instantiateInitialViewControllerMethod]; + } else { + NSLog(@"Warning: Initial view controller exists, but wasn't found in the storyboard: %@", initialViewControllerID); + } + } + + for (NSXMLElement *viewControllerElement in viewControllers) { + NSString *storyboardIdentifier = [[viewControllerElement attributeForName:@"storyboardIdentifier"] stringValue]; + BOOL importedCustomClass = NO; + NSString *className = [self classTypeForElement:viewControllerElement importedCustomClass:&importedCustomClass]; + if (storyboardIdentifier) { + [identifiers removeObject:storyboardIdentifier]; // prevent user from using the old string, they can now access it via [MYMainStoryboard instantiate...] + + // output + [MYMainStoryboard myCustomViewControllerIdentifier] + CGUMethod *customViewControllerIdentifierMethod = [CGUMethod new]; + customViewControllerIdentifierMethod.classMethod = YES; + customViewControllerIdentifierMethod.returnType = @"NSString *"; + customViewControllerIdentifierMethod.nameAndArguments = [NSString stringWithFormat:@"%@Identifier", [storyboardIdentifier IDS_camelcaseString]]; + customViewControllerIdentifierMethod.body = [NSString stringWithFormat:@" return @\"%@\";", storyboardIdentifier]; + [storyboardClass.methods addObject:customViewControllerIdentifierMethod]; + + // output + [MYMainStoryboard instantiateMyCustomViewController] + CGUMethod *instantiateCustomViewControllerMethod = [CGUMethod new]; + instantiateCustomViewControllerMethod.documentation = [NSString stringWithFormat:@"@return The scene with id '%@' from the '%@' storyboard.", storyboardIdentifier, storyboardName]; + instantiateCustomViewControllerMethod.classMethod = YES; + instantiateCustomViewControllerMethod.returnType = [NSString stringWithFormat:@"%@ *", className]; + instantiateCustomViewControllerMethod.nameAndArguments = [NSString stringWithFormat:@"instantiate%@", [storyboardIdentifier IDS_titlecaseString]]; + instantiateCustomViewControllerMethod.body = [NSString stringWithFormat:@" return [[self storyboard] instantiateViewControllerWithIdentifier:@\"%@\"];", storyboardIdentifier]; + [storyboardClass.methods addObject:instantiateCustomViewControllerMethod]; + } + + if (importedCustomClass) { + CGUClass *viewControllerClassCategory = self.classes[className]; // we may see the same class twice, so it is stored in a dictionary + if (viewControllerClassCategory == nil) { + viewControllerClassCategory = [CGUClass new]; + viewControllerClassCategory.name = className; + viewControllerClassCategory.categoryName = [NSString stringWithFormat:@"ObjcCodeGenUtils_%@", storyboardName]; + self.classes[className] = viewControllerClassCategory; + } + + NSArray *segueIdentifiers = [[viewControllerElement nodesForXPath:@".//segue/@identifier" error:&error] valueForKey:NSStringFromSelector(@selector(stringValue))]; + for (NSString *segueIdentifier in segueIdentifiers) { + [identifiers removeObject:segueIdentifier]; // we don't want the user accessing this segue via the old method + + // output - [(MYCustomViewController *) myCustomSegueIdentifier] + CGUMethod *segueIdentifierMethod = [CGUMethod new]; + segueIdentifierMethod.returnType = @"NSString *"; + segueIdentifierMethod.nameAndArguments = [NSString stringWithFormat:@"%@Identifier", [segueIdentifier IDS_camelcaseString]]; + segueIdentifierMethod.body = [NSString stringWithFormat:@" return @\"%@\";", segueIdentifier]; + [viewControllerClassCategory.methods addObject:segueIdentifierMethod]; + + // output - [(MYCustomViewController *) performMyCustomSegue] + CGUMethod *performSegueMethod = [CGUMethod new]; + performSegueMethod.nameAndArguments = [NSString stringWithFormat:@"perform%@", [segueIdentifier IDS_titlecaseString]]; + performSegueMethod.body = [NSString stringWithFormat:@" [self performSegueWithIdentifier:[self %@] sender:nil];", segueIdentifierMethod.nameAndArguments]; + [viewControllerClassCategory.methods addObject:performSegueMethod]; + } + + NSArray *reuseIdentifiers = [viewControllerElement nodesForXPath:@".//*[@reuseIdentifier]" error:&error]; + for (NSXMLElement *reuseIdentifierElement in reuseIdentifiers) { + NSString *reuseIdentifier = [[reuseIdentifierElement attributeForName:@"reuseIdentifier"] stringValue]; + [identifiers removeObject:reuseIdentifier]; + + // output - (NSString *)[(MYCustomViewController *) myCustomCellIdentifier]; + CGUMethod *reuseIdentifierMethod = [CGUMethod new]; + reuseIdentifierMethod.returnType = @"NSString *"; + reuseIdentifierMethod.nameAndArguments = [NSString stringWithFormat:@"%@Identifier", [reuseIdentifier IDS_camelcaseString]]; + reuseIdentifierMethod.body = [NSString stringWithFormat:@" return @\"%@\";", reuseIdentifier]; + [viewControllerClassCategory.methods addObject:reuseIdentifierMethod]; + + NSString *elementName = reuseIdentifierElement.name; // E.g. collectionViewCell, tableViewCell, etc. + NSString *additionalMethodArguments = nil; + NSString *code = nil; + if ([elementName isEqualToString:@"tableViewCell"]) { + additionalMethodArguments = @"ofTableView:(UITableView *)tableView"; + code = [NSString stringWithFormat:@"[tableView dequeueReusableCellWithIdentifier:@\"%@\" forIndexPath:indexPath]", reuseIdentifier]; + } else if ([elementName isEqualToString:@"collectionViewCell"]) { + additionalMethodArguments = @"ofCollectionView:(UICollectionView *)collectionView"; + code = [NSString stringWithFormat:@"[collectionView dequeueReusableCellWithReuseIdentifier:@\"%@\" forIndexPath:indexPath]", reuseIdentifier]; + } else if ([elementName isEqualToString:@"collectionReusableView"]) { + additionalMethodArguments = @"ofKind:(NSString *)kind ofCollectionView:(UICollectionView *)collectionView"; + code = [NSString stringWithFormat:@"[collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:@\"%@\" forIndexPath:indexPath]", reuseIdentifier]; + } else { + NSLog(@"Warning: Unknown reuse identifier %@.", elementName); + continue; + } + + NSString *reuseIdentifierClassName = [self classTypeForElement:reuseIdentifierElement importedCustomClass:NULL]; // E.g. UITableViewCell, UICollectionViewCell, UICollectionReusableView, or custom class + + // output - (MYCustomCell *)[(MYCustomViewController *) dequeueMyCustomCellForIndexPath:ofTableView:] + CGUMethod *dequeueMethod = [CGUMethod new]; + dequeueMethod.returnType = [NSString stringWithFormat:@"%@ *", reuseIdentifierClassName]; + dequeueMethod.nameAndArguments = [NSString stringWithFormat:@"dequeue%@ForIndexPath:(NSIndexPath *)indexPath %@", [reuseIdentifier IDS_titlecaseString], additionalMethodArguments]; + dequeueMethod.body = [NSString stringWithFormat:@" return %@;", code]; + [viewControllerClassCategory.methods addObject:dequeueMethod]; + } + + // Ex: + NSArray *constraints = [viewControllerElement nodesForXPath:@".//constraint[@constant]" error:NULL]; + for (NSXMLElement *constraint in constraints) { + NSString *constraintId = [[constraint attributeForName:@"id"] stringValue]; + NSXMLElement *node = [[viewControllerElement nodesForXPath:[NSString stringWithFormat:@"./connections/outlet[@destination='%@']", constraintId] error:NULL] firstObject]; + if (node) { + // Ex: + NSString *propertyName = [[node attributeForName:@"property"] stringValue]; + CGFloat constant = [[[constraint attributeForName:@"constant"] stringValue] floatValue]; + + // ouptut - (CGFloat)[(MYCustomViewController *) myCustomConstraintOriginalConstant] + CGUMethod *constraintMethod = [CGUMethod new]; + constraintMethod.returnType = @"CGFloat"; + constraintMethod.nameAndArguments = [NSString stringWithFormat:@"%@OriginalConstant", [propertyName IDS_camelcaseString]]; + constraintMethod.body = [NSString stringWithFormat:@" return %f;", constant]; + [viewControllerClassCategory.methods addObject:constraintMethod]; + } + } + } + } + + NSMutableDictionary *uniqueKeys = [NSMutableDictionary dictionary]; for (NSString *identifier in identifiers) { NSString *key = [NSString stringWithFormat:@"%@%@Storyboard%@Identifier", self.classPrefix, storyboardName, [identifier IDS_titlecaseString]]; uniqueKeys[key] = identifier; @@ -57,23 +314,10 @@ - (void)startWithCompletionHandler:(dispatch_block_t)completionBlock; [self.implementationContents addObject:[NSString stringWithFormat:@"NSString *const %@ = @\"%@\";\n", key, uniqueKeys[key]]]; } + [self removeDuplicatedExtensionMethods]; + [self writeOutputFiles]; completionBlock(); } @end - - -@implementation NSString (IDStoryboardAddition) - -- (NSString *)IDS_titlecaseString; -{ - NSArray *words = [self componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; - NSMutableString *output = [NSMutableString string]; - for (NSString *word in words) { - [output appendFormat:@"%@%@", [[word substringToIndex:1] uppercaseString], [word substringFromIndex:1]]; - } - return output; -} - -@end