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