Skip to content

Instantly share code, notes, and snippets.

@epologee
Created May 29, 2012 13:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save epologee/93982af3b65d2151672e to your computer and use it in GitHub Desktop.
Save epologee/93982af3b65d2151672e to your computer and use it in GitHub Desktop.
StackOverflow - how to synchronize CALayer and UIView animations
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.epologee.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
#import <Availability.h>
#ifndef __IPHONE_3_0
#warning "This project uses features only available in iOS SDK 3.0 and later."
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#endif
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
version = "1.3">
<BuildAction>
<BuildActionEntries>
<BuildActionEntry
buildForRunning = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8215700D754C2F15E43C8A3B"
BuildableName = "AnimatedLayers.app"
BlueprintName = "AnimatedLayers"
ReferencedContainer = "container:AnimatedLayers.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.GDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8215700D754C2F15E43C8A3B"
BuildableName = "AnimatedLayers.app"
BlueprintName = "AnimatedLayers"
ReferencedContainer = "container:AnimatedLayers.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
</Scheme>
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@end
#import "AppDelegate.h"
#import "JustAViewController.h"
@implementation AppDelegate
{
UIWindow *_window;
UINavigationController *_navigationController;
}
@synthesize window = _window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
_window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
JustAViewController *vc = [[JustAViewController alloc] init];
vc.title = @"Synchronize the squares?";
_navigationController = [[UINavigationController alloc] initWithRootViewController:vc];
_navigationController.navigationBar.tintColor = [UIColor blackColor];
[_window addSubview:_navigationController.view];
_window.backgroundColor = [UIColor whiteColor];
[_window makeKeyAndVisible];
return YES;
}
@end
#import <Foundation/Foundation.h>
@interface ContainerView : UIView
@end
#import <QuartzCore/QuartzCore.h>
#import "ContainerView.h"
@implementation ContainerView
{
UIView *_subview;
CALayer *_sublayer;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.backgroundColor = [UIColor whiteColor];
/*
The 'subview' (emphasis on *view*) is a UIView instance. It is colored blue.
*/
_subview = [[UIView alloc] initWithFrame:[self frameForSubview]];
_subview.backgroundColor = [UIColor blueColor];
[self addSubview:_subview];
/*
The 'sublayer' (emphasis on *layer*) is a CALayer instance. It is colored red.
*/
_sublayer = [CALayer layer];
_sublayer.backgroundColor = [UIColor redColor].CGColor;
[self.layer addSublayer:_sublayer];
}
return self;
}
- (CGRect)frameForSubview
{
return CGRectMake(self.bounds.size.width - 22, self.bounds.size.height - 22, 22, 22);
}
- (CGRect)frameForSublayer
{
return CGRectMake(self.bounds.size.width - 44, self.bounds.size.height - 44, 22, 22);
}
/**
* I'm using the setFrame: method to set the frames of the subview and the sublayers to make sure that
* a possible enclosing `UIView animateWithDuration:animations:` call is not violated.
* Setting an autoresizingMask on the `UIView *_subview` would have achieved the same effect, but there
* is no such property on `CALayer *_sublayer`.
*/
- (void)setFrame:(CGRect)aFrame
{
[super setFrame:aFrame];
_subview.frame = [self frameForSubview];
_sublayer.frame = [self frameForSublayer];
}
@end
/* Localized versions of Info.plist keys */
#import <Foundation/Foundation.h>
@interface JustAViewController : UIViewController
@end
#import "JustAViewController.h"
#import "ContainerView.h"
@implementation JustAViewController
{
ContainerView *_containerView;
}
/**
* Most of this loadView method is not too interesting.
* The `ContainerView` instance added as a subview is the class that this project is about.
*/
- (void)loadView
{
self.view = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
[self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap)]];
// The autoresizingMask is only of use the very first time this view controller's view is added to the window.
_containerView = [[ContainerView alloc] initWithFrame:CGRectMake(20, 20, self.view.bounds.size.width - 40, self.view.bounds.size.height / 2 - 20)];
_containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:_containerView];
UILabel *hint = [[UILabel alloc] initWithFrame:CGRectMake(20, self.view.bounds.size.height / 2 + 20, self.view.bounds.size.width - 40, self.view.bounds.size.height / 2 - 40)];
hint.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
hint.text = @"Tap anywhere to trigger the frame change animation.\n\nThe red square is a (CALayer) sublayer, the blue square is a (UIView) subview.\n\nThe animation duration of either 0.0s or 1.0s is picked randomly";
hint.lineBreakMode = UILineBreakModeWordWrap;
hint.numberOfLines = 0;
hint.backgroundColor = [UIColor clearColor];
[self.view addSubview:hint];
}
- (void)handleTap
{
// This just creates a random frame in the upper half of this view controller's view bounds.
CGRect maxFrame = CGRectMake(20, 20, self.view.bounds.size.width - 40, self.view.bounds.size.height / 2 - 20);
CGRect subFrame = CGRectMake(20, 20, MAX(44, arc4random() % (NSInteger)maxFrame.size.width), MAX(44, arc4random() % (NSInteger)(maxFrame.size.height / 2)));
subFrame.origin.x = 20 + arc4random() % (NSInteger)(maxFrame.size.width - subFrame.size.width);
subFrame.origin.y = 20 + arc4random() % (NSInteger)(maxFrame.size.height - subFrame.size.height);
// Assign that sub frame to the container view in an animation block. The duration is either 0.0s (instant) or 1.0s (slow animation).
// The red square within does not regard this duration, so regardless of the duration it's either always too late or always too soon.
[UIView animateWithDuration:arc4random() % 2 delay:0 options:UIViewAnimationCurveEaseOut | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState animations:^{
_containerView.frame = subFrame;
} completion:nil];
}
@end
#import "AppDelegate.h"
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
8215700D754C2F15E43C8A44 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A43 /* UIKit.framework */; };
8215700D754C2F15E43C8A46 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A45 /* Foundation.framework */; };
8215700D754C2F15E43C8A48 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A47 /* CoreGraphics.framework */; };
8215700D754C2F15E43C8A4E /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A4D /* InfoPlist.strings */; };
8215700D754C2F15E43C8A50 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A4F /* main.m */; };
8215700D754C2F15E43C8A54 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A53 /* AppDelegate.m */; };
8215700D754C2F15E43C8A57 /* JustAViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A56 /* JustAViewController.m */; };
8215700D754C2F15E43C8A5A /* ContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A59 /* ContainerView.m */; };
8215700D754C2F15E43C8A5D /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8215700D754C2F15E43C8A5C /* QuartzCore.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
8215700D754C2F15E43C8A3A /* AnimatedLayers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AnimatedLayers.app; sourceTree = BUILT_PRODUCTS_DIR; };
8215700D754C2F15E43C8A43 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
8215700D754C2F15E43C8A45 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
8215700D754C2F15E43C8A47 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
8215700D754C2F15E43C8A4B /* AnimatedLayers-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = "AnimatedLayers-Info.plist"; sourceTree = "<group>"; };
8215700D754C2F15E43C8A4C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
8215700D754C2F15E43C8A4F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
8215700D754C2F15E43C8A51 /* AnimatedLayers-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AnimatedLayers-Prefix.pch"; sourceTree = "<group>"; };
8215700D754C2F15E43C8A52 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
8215700D754C2F15E43C8A53 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
8215700D754C2F15E43C8A56 /* JustAViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JustAViewController.m; sourceTree = "<group>"; };
8215700D754C2F15E43C8A58 /* JustAViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JustAViewController.h; sourceTree = "<group>"; };
8215700D754C2F15E43C8A59 /* ContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContainerView.m; sourceTree = "<group>"; };
8215700D754C2F15E43C8A5B /* ContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContainerView.h; sourceTree = "<group>"; };
8215700D754C2F15E43C8A5C /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
8215700D754C2F15E43C8A40 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8215700D754C2F15E43C8A44 /* UIKit.framework in Frameworks */,
8215700D754C2F15E43C8A46 /* Foundation.framework in Frameworks */,
8215700D754C2F15E43C8A48 /* CoreGraphics.framework in Frameworks */,
8215700D754C2F15E43C8A5D /* QuartzCore.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
8215700D754C2F15E43C8A36 = {
isa = PBXGroup;
children = (
8215700D754C2F15E43C8A37 /* Products */,
8215700D754C2F15E43C8A42 /* Frameworks */,
8215700D754C2F15E43C8A49 /* AnimatedLayers */,
);
sourceTree = "<group>";
};
8215700D754C2F15E43C8A37 /* Products */ = {
isa = PBXGroup;
children = (
8215700D754C2F15E43C8A3A /* AnimatedLayers.app */,
);
name = Products;
sourceTree = "<group>";
};
8215700D754C2F15E43C8A42 /* Frameworks */ = {
isa = PBXGroup;
children = (
8215700D754C2F15E43C8A5C /* QuartzCore.framework */,
8215700D754C2F15E43C8A47 /* CoreGraphics.framework */,
8215700D754C2F15E43C8A45 /* Foundation.framework */,
8215700D754C2F15E43C8A43 /* UIKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
8215700D754C2F15E43C8A49 /* AnimatedLayers */ = {
isa = PBXGroup;
children = (
8215700D754C2F15E43C8A53 /* AppDelegate.m */,
8215700D754C2F15E43C8A52 /* AppDelegate.h */,
8215700D754C2F15E43C8A4A /* Supporting Files */,
8215700D754C2F15E43C8A55 /* Classes */,
);
path = AnimatedLayers;
sourceTree = "<group>";
};
8215700D754C2F15E43C8A4A /* Supporting Files */ = {
isa = PBXGroup;
children = (
8215700D754C2F15E43C8A51 /* AnimatedLayers-Prefix.pch */,
8215700D754C2F15E43C8A4F /* main.m */,
8215700D754C2F15E43C8A4D /* InfoPlist.strings */,
8215700D754C2F15E43C8A4B /* AnimatedLayers-Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
8215700D754C2F15E43C8A55 /* Classes */ = {
isa = PBXGroup;
children = (
8215700D754C2F15E43C8A5B /* ContainerView.h */,
8215700D754C2F15E43C8A59 /* ContainerView.m */,
8215700D754C2F15E43C8A58 /* JustAViewController.h */,
8215700D754C2F15E43C8A56 /* JustAViewController.m */,
);
path = Classes;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
8215700D754C2F15E43C8A3B /* AnimatedLayers */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8215700D754C2F15E43C8A3C /* Build configuration list for PBXNativeTarget "AnimatedLayers" */;
buildPhases = (
8215700D754C2F15E43C8A3F /* Sources */,
8215700D754C2F15E43C8A40 /* Frameworks */,
8215700D754C2F15E43C8A41 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = AnimatedLayers;
productName = AnimatedLayers;
productReference = 8215700D754C2F15E43C8A3A /* AnimatedLayers.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
8215700D754C2F15E43C8A34 /* Project object */ = {
isa = PBXProject;
buildConfigurationList = 8215700D754C2F15E43C8A35 /* Build configuration list for PBXProject "AnimatedLayers" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = 8215700D754C2F15E43C8A36;
productRefGroup = 8215700D754C2F15E43C8A37 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
8215700D754C2F15E43C8A3B /* AnimatedLayers */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
8215700D754C2F15E43C8A41 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8215700D754C2F15E43C8A4E /* InfoPlist.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
8215700D754C2F15E43C8A3F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8215700D754C2F15E43C8A50 /* main.m in Sources */,
8215700D754C2F15E43C8A54 /* AppDelegate.m in Sources */,
8215700D754C2F15E43C8A57 /* JustAViewController.m in Sources */,
8215700D754C2F15E43C8A5A /* ContainerView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
8215700D754C2F15E43C8A4D /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
8215700D754C2F15E43C8A4C /* en */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
8215700D754C2F15E43C8A38 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = "$(ARCHS_STANDARD_32_BIT)";
CLANG_ENABLE_OBJC_ARC = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 5.1;
OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1";
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
8215700D754C2F15E43C8A39 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = "$(ARCHS_STANDARD_32_BIT)";
CLANG_ENABLE_OBJC_ARC = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 5.1;
SDKROOT = iphoneos;
};
name = Debug;
};
8215700D754C2F15E43C8A3D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = "AnimatedLayers/AnimatedLayers-Prefix.pch";
INFOPLIST_FILE = "AnimatedLayers/AnimatedLayers-Info.plist";
PRODUCT_NAME = "$(TARGET_NAME)";
WRAPPER_EXTENSION = app;
};
name = Release;
};
8215700D754C2F15E43C8A3E /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = "AnimatedLayers/AnimatedLayers-Prefix.pch";
INFOPLIST_FILE = "AnimatedLayers/AnimatedLayers-Info.plist";
PRODUCT_NAME = "$(TARGET_NAME)";
WRAPPER_EXTENSION = app;
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
8215700D754C2F15E43C8A35 /* Build configuration list for PBXProject "AnimatedLayers" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8215700D754C2F15E43C8A38 /* Release */,
8215700D754C2F15E43C8A39 /* Debug */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
8215700D754C2F15E43C8A3C /* Build configuration list for PBXNativeTarget "AnimatedLayers" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8215700D754C2F15E43C8A3D /* Release */,
8215700D754C2F15E43C8A3E /* Debug */,
);
defaultConfigurationIsVisible = 0;
};
/* End XCConfigurationList section */
};
rootObject = 8215700D754C2F15E43C8A34 /* Project object */;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment