Repository: AndreasVerhoeven/AvePurchaseButton Branch: master Commit: 875b005c2a79 Files: 23 Total size: 65.4 KB Directory structure: gitextract_urm7z6zh/ ├── .gitignore ├── AvePurchaseButton.podspec ├── Example/ │ ├── PurchaseButtonExample/ │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── TableViewController.h │ │ ├── TableViewController.m │ │ └── main.m │ └── PurchaseButtonExample.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ └── contents.xcworkspacedata ├── LICENSE ├── README.md └── Source/ ├── BorderedButton/ │ ├── AveBorderedButton.h │ ├── AveBorderedButton.m │ ├── AveBorderedView.h │ └── AveBorderedView.m └── PurchaseButton/ ├── AvePurchaseActivityIndicatorView.h ├── AvePurchaseActivityIndicatorView.m ├── AvePurchaseButton.h └── AvePurchaseButton.m ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Xcode # build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.hmap *.ipa *.xcuserstate *.DS_Store # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control # #Pods/ ================================================ FILE: AvePurchaseButton.podspec ================================================ Pod::Spec.new do |s| s.name = "AvePurchaseButton" s.version = "1.0.6" s.summary = "iOS App Store Styled Purchase Button" s.description = <<-DESC Drop In App Store Styled Purchase Button, with proper animations. Title configurable in Interface Builder. DESC s.homepage = "https://github.com/AndreasVerhoeven/AvePurchaseButton" s.screenshots = "https://cloud.githubusercontent.com/assets/168214/11920880/852741d6-a77a-11e5-839d-e2f572e49475.gif", "https://cloud.githubusercontent.com/assets/168214/11920878/7c5d708e-a77a-11e5-8553-3806e89ba434.png" s.license = { :type => "MIT", :file => "LICENSE" } s.author = "Andreas Verhoeven" s.social_media_url = "http://twitter.com/aveapps" s.platform = :ios, "7.0" s.source = { :git => "https://github.com/AndreasVerhoeven/AvePurchaseButton.git", :tag => "1.0.6" } s.source_files = "Source", "Source/**/*.{h,m}" s.exclude_files = "Example" s.public_header_files = "Source/**/AvePurchaseButton.h", "Source/**/AveBorderedButton.h", "Source/**/AveBorderedView.h" s.requires_arc = true end ================================================ FILE: Example/PurchaseButtonExample/AppDelegate.h ================================================ // // AppDelegate.h // PurchaseButtonExample // // Created by Andreas Verhoeven on 20-12-15. // Copyright © 2015 AveApps. All rights reserved. // #import @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end ================================================ FILE: Example/PurchaseButtonExample/AppDelegate.m ================================================ // // AppDelegate.m // PurchaseButtonExample // // Created by Andreas Verhoeven on 20-12-15. // Copyright © 2015 AveApps. All rights reserved. // #import "AppDelegate.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. return YES; } @end ================================================ FILE: Example/PurchaseButtonExample/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "1x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "2x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "1x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "2x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "1x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/PurchaseButtonExample/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Example/PurchaseButtonExample/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Example/PurchaseButtonExample/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Example/PurchaseButtonExample/TableViewController.h ================================================ // // TableViewController.h // PurchaseButtonExample // // Created by Andreas Verhoeven on 20-12-15. // Copyright © 2015 AveApps. All rights reserved. // #import @interface TableViewController : UITableViewController @end ================================================ FILE: Example/PurchaseButtonExample/TableViewController.m ================================================ // // TableViewController.m // PurchaseButtonExample // // Created by Andreas Verhoeven on 20-12-15. // Copyright © 2015 AveApps. All rights reserved. // #import "TableViewController.h" #import "AvePurchaseButton.h" @implementation TableViewController { NSMutableIndexSet* _busyIndexes; } -(void)purchaseButtonTapped:(AvePurchaseButton*)button { NSIndexPath* indexPath = [self.tableView indexPathForCell:(UITableViewCell*)button.superview]; NSInteger index = indexPath.row; // handle taps on the purchase button by switch(button.buttonState) { case AvePurchaseButtonStateNormal: // progress -> confirmation [button setButtonState:AvePurchaseButtonStateConfirmation animated:YES]; break; case AvePurchaseButtonStateConfirmation: // confirmation -> "purchase" progress [_busyIndexes addIndex:index]; [button setButtonState:AvePurchaseButtonStateProgress animated:YES]; break; case AvePurchaseButtonStateProgress: // progress -> back to normal [_busyIndexes removeIndex:index]; [button setButtonState:AvePurchaseButtonStateNormal animated:YES]; break; } } -(void)viewDidLoad { [super viewDidLoad]; _busyIndexes = [NSMutableIndexSet new]; self.tableView.rowHeight = 54; self.tableView.layoutMargins = UIEdgeInsetsZero; self.tableView.separatorInset = UIEdgeInsetsZero; } -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 10; } -(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString* const CellIdentifier = @"Cell"; UITableViewCell* cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if(nil == cell) { // create a cell with some nice defaults cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; cell.layoutMargins = UIEdgeInsetsZero; cell.separatorInset = UIEdgeInsetsZero; cell.detailTextLabel.textColor = [UIColor grayColor]; // add a buttons as an accessory and let it respond to touches AvePurchaseButton* button = [[AvePurchaseButton alloc] initWithFrame:CGRectZero]; [button addTarget:self action:@selector(purchaseButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; cell.accessoryView = button; } // configure the cell cell.textLabel.text = @"Some Nice Item"; cell.detailTextLabel.text = @"Really nice!"; // configure the purchase button in state normal AvePurchaseButton* button = (AvePurchaseButton*)cell.accessoryView; button.buttonState = AvePurchaseButtonStateNormal; button.normalTitle = @"$ 2.99"; button.confirmationTitle = @"BUY"; [button sizeToFit]; // if the item at this indexPath is being "busy" with purchasing, update the purchase // button's state to reflect so. if([_busyIndexes containsIndex:indexPath.row] == YES) { button.buttonState = AvePurchaseButtonStateProgress; } return cell; } @end ================================================ FILE: Example/PurchaseButtonExample/main.m ================================================ // // main.m // PurchaseButtonExample // // Created by Andreas Verhoeven on 20-12-15. // Copyright © 2015 AveApps. All rights reserved. // #import #import "AppDelegate.h" int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ================================================ FILE: Example/PurchaseButtonExample.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 49324B351C275EF20007A068 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 49324B341C275EF20007A068 /* main.m */; }; 49324B381C275EF20007A068 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 49324B371C275EF20007A068 /* AppDelegate.m */; }; 49324B3E1C275EF20007A068 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49324B3C1C275EF20007A068 /* Main.storyboard */; }; 49324B401C275EF20007A068 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49324B3F1C275EF20007A068 /* Assets.xcassets */; }; 49324B431C275EF20007A068 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 49324B411C275EF20007A068 /* LaunchScreen.storyboard */; }; 49324B561C2765930007A068 /* TableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 49324B551C2765930007A068 /* TableViewController.m */; }; 49324B761C276E420007A068 /* AveBorderedButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 49324B6E1C276E420007A068 /* AveBorderedButton.m */; }; 49324B771C276E420007A068 /* AveBorderedView.m in Sources */ = {isa = PBXBuildFile; fileRef = 49324B701C276E420007A068 /* AveBorderedView.m */; }; 49324B781C276E420007A068 /* AvePurchaseActivityIndicatorView.m in Sources */ = {isa = PBXBuildFile; fileRef = 49324B731C276E420007A068 /* AvePurchaseActivityIndicatorView.m */; }; 49324B791C276E420007A068 /* AvePurchaseButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 49324B751C276E420007A068 /* AvePurchaseButton.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 49324B301C275EF20007A068 /* PurchaseButtonExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PurchaseButtonExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49324B341C275EF20007A068 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 49324B361C275EF20007A068 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 49324B371C275EF20007A068 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 49324B3D1C275EF20007A068 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 49324B3F1C275EF20007A068 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49324B421C275EF20007A068 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49324B441C275EF20007A068 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49324B541C2765930007A068 /* TableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TableViewController.h; sourceTree = ""; }; 49324B551C2765930007A068 /* TableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TableViewController.m; sourceTree = ""; }; 49324B6D1C276E420007A068 /* AveBorderedButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AveBorderedButton.h; sourceTree = ""; }; 49324B6E1C276E420007A068 /* AveBorderedButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AveBorderedButton.m; sourceTree = ""; }; 49324B6F1C276E420007A068 /* AveBorderedView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AveBorderedView.h; sourceTree = ""; }; 49324B701C276E420007A068 /* AveBorderedView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AveBorderedView.m; sourceTree = ""; }; 49324B721C276E420007A068 /* AvePurchaseActivityIndicatorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvePurchaseActivityIndicatorView.h; sourceTree = ""; }; 49324B731C276E420007A068 /* AvePurchaseActivityIndicatorView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvePurchaseActivityIndicatorView.m; sourceTree = ""; }; 49324B741C276E420007A068 /* AvePurchaseButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvePurchaseButton.h; sourceTree = ""; }; 49324B751C276E420007A068 /* AvePurchaseButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvePurchaseButton.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 49324B2D1C275EF20007A068 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 49324B271C275EF20007A068 = { isa = PBXGroup; children = ( 49324B321C275EF20007A068 /* PurchaseButtonExample */, 49324B311C275EF20007A068 /* Products */, ); sourceTree = ""; }; 49324B311C275EF20007A068 /* Products */ = { isa = PBXGroup; children = ( 49324B301C275EF20007A068 /* PurchaseButtonExample.app */, ); name = Products; sourceTree = ""; }; 49324B321C275EF20007A068 /* PurchaseButtonExample */ = { isa = PBXGroup; children = ( 49324B6B1C276E420007A068 /* Source */, 49324B361C275EF20007A068 /* AppDelegate.h */, 49324B371C275EF20007A068 /* AppDelegate.m */, 49324B3C1C275EF20007A068 /* Main.storyboard */, 49324B3F1C275EF20007A068 /* Assets.xcassets */, 49324B411C275EF20007A068 /* LaunchScreen.storyboard */, 49324B441C275EF20007A068 /* Info.plist */, 49324B331C275EF20007A068 /* Supporting Files */, 49324B541C2765930007A068 /* TableViewController.h */, 49324B551C2765930007A068 /* TableViewController.m */, ); path = PurchaseButtonExample; sourceTree = ""; }; 49324B331C275EF20007A068 /* Supporting Files */ = { isa = PBXGroup; children = ( 49324B341C275EF20007A068 /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; 49324B6B1C276E420007A068 /* Source */ = { isa = PBXGroup; children = ( 49324B6C1C276E420007A068 /* BorderedButton */, 49324B711C276E420007A068 /* PurchaseButton */, ); name = Source; path = ../../Source; sourceTree = ""; }; 49324B6C1C276E420007A068 /* BorderedButton */ = { isa = PBXGroup; children = ( 49324B6D1C276E420007A068 /* AveBorderedButton.h */, 49324B6E1C276E420007A068 /* AveBorderedButton.m */, 49324B6F1C276E420007A068 /* AveBorderedView.h */, 49324B701C276E420007A068 /* AveBorderedView.m */, ); path = BorderedButton; sourceTree = ""; }; 49324B711C276E420007A068 /* PurchaseButton */ = { isa = PBXGroup; children = ( 49324B721C276E420007A068 /* AvePurchaseActivityIndicatorView.h */, 49324B731C276E420007A068 /* AvePurchaseActivityIndicatorView.m */, 49324B741C276E420007A068 /* AvePurchaseButton.h */, 49324B751C276E420007A068 /* AvePurchaseButton.m */, ); path = PurchaseButton; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 49324B2F1C275EF20007A068 /* PurchaseButtonExample */ = { isa = PBXNativeTarget; buildConfigurationList = 49324B471C275EF20007A068 /* Build configuration list for PBXNativeTarget "PurchaseButtonExample" */; buildPhases = ( 49324B2C1C275EF20007A068 /* Sources */, 49324B2D1C275EF20007A068 /* Frameworks */, 49324B2E1C275EF20007A068 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = PurchaseButtonExample; productName = PurchaseButtonExample; productReference = 49324B301C275EF20007A068 /* PurchaseButtonExample.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 49324B281C275EF20007A068 /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 0720; ORGANIZATIONNAME = AveApps; TargetAttributes = { 49324B2F1C275EF20007A068 = { CreatedOnToolsVersion = 7.2; }; }; }; buildConfigurationList = 49324B2B1C275EF20007A068 /* Build configuration list for PBXProject "PurchaseButtonExample" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 49324B271C275EF20007A068; productRefGroup = 49324B311C275EF20007A068 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 49324B2F1C275EF20007A068 /* PurchaseButtonExample */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 49324B2E1C275EF20007A068 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 49324B431C275EF20007A068 /* LaunchScreen.storyboard in Resources */, 49324B401C275EF20007A068 /* Assets.xcassets in Resources */, 49324B3E1C275EF20007A068 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 49324B2C1C275EF20007A068 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 49324B561C2765930007A068 /* TableViewController.m in Sources */, 49324B761C276E420007A068 /* AveBorderedButton.m in Sources */, 49324B381C275EF20007A068 /* AppDelegate.m in Sources */, 49324B351C275EF20007A068 /* main.m in Sources */, 49324B791C276E420007A068 /* AvePurchaseButton.m in Sources */, 49324B771C276E420007A068 /* AveBorderedView.m in Sources */, 49324B781C276E420007A068 /* AvePurchaseActivityIndicatorView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 49324B3C1C275EF20007A068 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 49324B3D1C275EF20007A068 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 49324B411C275EF20007A068 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 49324B421C275EF20007A068 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 49324B451C275EF20007A068 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.2; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 49324B461C275EF20007A068 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.2; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 49324B481C275EF20007A068 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = PurchaseButtonExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.aveapps.PurchaseButtonExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 49324B491C275EF20007A068 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = PurchaseButtonExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.aveapps.PurchaseButtonExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 49324B2B1C275EF20007A068 /* Build configuration list for PBXProject "PurchaseButtonExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 49324B451C275EF20007A068 /* Debug */, 49324B461C275EF20007A068 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 49324B471C275EF20007A068 /* Build configuration list for PBXNativeTarget "PurchaseButtonExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 49324B481C275EF20007A068 /* Debug */, 49324B491C275EF20007A068 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 49324B281C275EF20007A068 /* Project object */; } ================================================ FILE: Example/PurchaseButtonExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Andreas Verhoeven Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # AvePurchaseButton Drop In App Store Styled Purchase Button, with proper animations. Title configurable in Interface Builder. ## Animations A gif-movie showing how the button animates from normal to confirmation to in-progress state: ![Movie](https://cloud.githubusercontent.com/assets/168214/11920880/852741d6-a77a-11e5-839d-e2f572e49475.gif) ## Screenshot A screenshot showing the different states of the button: ![Screenshot](https://cloud.githubusercontent.com/assets/168214/11920878/7c5d708e-a77a-11e5-8553-3806e89ba434.png) ## How to use Create an instance of AvePurchaseButton. Set the normalTitle, confirmationTitle and handle UIControlEventTouchUpInside. To change states, use -[AvePurchaseButton setButtonState:animated:]. ## Example usage: ``` - (void)addPurchaseButton { AvePurchaseButton* button = [[AvePurchaseButton alloc] initWithFrame:CGRectZero]; [button addTarget:self action:@selector(purchaseButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; button.buttonState = AvePurchaseButtonStateNormal; button.normalTitle = @"$ 2.99"; button.confirmationTitle = @"BUY"; [button sizeToFit]; [self.view addSubview:button]; } - (void)purchaseButtonTapped:(AvePurchaseButton*)button { switch(button.buttonState) { case AvePurchaseButtonStateNormal: [button setButtonState:AvePurchaseButtonStateConfirmation animated:YES]; break; case AvePurchaseButtonStateConfirmation: // start the purchasing progress here, when done, go back to // AvePurchaseButtonStateProgress [button setButtonState:AvePurchaseButtonStateProgress animated:YES]; [self startPurchaseWithCompletionHandler:^{ [button setButtonState:AvePurchaseButtonStateNormal animated:YES]; }]; break; case AvePurchaseButtonStateProgress: break; } } ``` ================================================ FILE: Source/BorderedButton/AveBorderedButton.h ================================================ // // AveBorderedButton.h // // The MIT License (MIT) // // Copyright (c) 2015 Andreas Verhoeven // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import // defaults extern CGFloat kAveBorderedButtonDefaultBorderWidth; extern CGFloat kAveBorderedButtonDefaultCornerRadius; /*! @abstract Button that has a border and fills itself on highlighting. Use the .title property to change the title of this button. */ IB_DESIGNABLE @interface AveBorderedButton : UIControl // title for this button - no differences between states @property (nonatomic, copy) IBInspectable NSString* title; @property (nonatomic, copy) IBInspectable NSAttributedString* attributedTitle; @property (nonatomic, strong) IBInspectable UIImage* image; // title properties @property (nonatomic, readonly) UILabel* titleLabel; // don't set .text property directly, use .title @property (nonatomic, assign) UIEdgeInsets titleEdgeInsets; // looks, animates when updated @property (nonatomic, assign) IBInspectable CGFloat borderWidth; // defaults to kAveBorderedButtonDefaultBorderWidth (animatable) @property (nonatomic, assign) IBInspectable CGFloat cornerRadius; // defaults to kAveBorderedButtonDefaultCornerRadius (animatable) @end ================================================ FILE: Source/BorderedButton/AveBorderedButton.m ================================================ // // AveBorderedButton.m // // The MIT License (MIT) // // Copyright (c) 2015 Andreas Verhoeven // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import "AveBorderedButton.h" #import "AveBorderedView.h" CGFloat kAveBorderedButtonDefaultBorderWidth = 1.0; CGFloat kAveBorderedButtonDefaultCornerRadius = 4.0; @implementation AveBorderedButton { AveBorderedView* _borderView; // the border around the button UIImageView* _fillView; // the highlighting 'fill' when the button is tapped UIView* _labelClipView; // the title label is clipped UIImageView* _accessoryImageView; // accessoryImageView UIView* _accessoryAndLabelView; } #pragma mark - Initialization -(void)setup { if(nil == _titleLabel) { // defaults for properties _cornerRadius = kAveBorderedButtonDefaultCornerRadius; _borderWidth = kAveBorderedButtonDefaultBorderWidth; // view defaults self.backgroundColor = [UIColor clearColor]; // the border view _borderView = [[AveBorderedView alloc] initWithFrame:self.bounds]; _borderView.backgroundColor = [UIColor clearColor]; _borderView.userInteractionEnabled = NO; _borderView.layer.cornerRadius = self.cornerRadius; _borderView.layer.borderWidth = self.borderWidth; [self addSubview:_borderView]; // the title label is clipped inside this view _labelClipView = [[UIView alloc] initWithFrame:CGRectInset(self.bounds, 2, 2)]; _labelClipView.backgroundColor = [UIColor clearColor]; _labelClipView.opaque = NO; _labelClipView.clipsToBounds = YES; _labelClipView.userInteractionEnabled = NO; [self addSubview:_labelClipView]; _accessoryAndLabelView = [[UIView alloc] initWithFrame:_labelClipView.bounds]; _accessoryAndLabelView.backgroundColor = [UIColor clearColor]; _accessoryAndLabelView.opaque = NO; [_labelClipView addSubview:_accessoryAndLabelView]; _accessoryImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; [_accessoryAndLabelView addSubview:_accessoryImageView]; // the title label itself _titleLabel = [[UILabel alloc] initWithFrame:_accessoryImageView.bounds]; _titleLabel.opaque = NO; _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.font = [UIFont systemFontOfSize:[UIFont buttonFontSize]]; _titleLabel.userInteractionEnabled = NO; _titleLabel.textAlignment = NSTextAlignmentCenter; [_accessoryAndLabelView addSubview:_titleLabel]; [self updateTintColors]; } } -(id)init { self = [super init]; if(self != nil) { [self setup]; } return self; } -(id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if(self != nil) { [self setup]; } return self; } -(id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if(self != nil) { [self setup]; } return self; } -(CGSize)sizeThatFits:(CGSize)size { CGSize s = [self.titleLabel sizeThatFits:size]; if(self.image != nil) { s.width += s.height - 8; } s.width += 20; // add some spacing s.height += 8; s.width = ceilf(s.width); s.height = ceilf(s.height); return s; } #pragma mark - Properties -(void)setCornerRadius:(CGFloat)cornerRadius { if(cornerRadius != _cornerRadius) { _cornerRadius = cornerRadius; _borderView.layer.cornerRadius = self.cornerRadius; } } -(void)setBorderWidth:(CGFloat)borderWidth { if(borderWidth != _borderWidth) { _borderWidth = borderWidth; _borderView.layer.borderWidth = self.borderWidth; } } -(BOOL)shouldShowFillView { return self.highlighted; } #pragma mark - Layout code -(void)layoutSubviews { [super layoutSubviews]; _borderView.frame = self.bounds; _fillView.frame = self.bounds; _labelClipView.frame = CGRectInset(_borderView.bounds, 2, 2); _accessoryAndLabelView.frame = UIEdgeInsetsInsetRect(_labelClipView.bounds, self.titleEdgeInsets); _accessoryImageView.frame = _accessoryImageView.image != nil ? CGRectInset(CGRectMake(0, 0, CGRectGetHeight(_accessoryAndLabelView.bounds), CGRectGetHeight(_accessoryAndLabelView.bounds)), 4, 4) : CGRectZero; _titleLabel.frame = CGRectMake(CGRectGetMaxX(_accessoryImageView.frame), 0, CGRectGetWidth(_accessoryAndLabelView.bounds) - CGRectGetMaxX(_accessoryImageView.frame), CGRectGetHeight(_accessoryAndLabelView.bounds)); } #pragma mark - FillView -(void)ensureFillView { if(nil == _fillView) { _fillView = [[UIImageView alloc] initWithFrame:self.bounds]; _fillView.userInteractionEnabled = NO; _fillView.alpha = 0; _fillView.layer.cornerRadius = self.cornerRadius; [self insertSubview:_fillView aboveSubview:_borderView]; } } -(UIImage*)fillViewMaskImage { CGFloat actualWidth = _fillView.bounds.size.width; CGFloat actualHeight = _fillView.bounds.size.height; CGFloat deviceScale = [UIScreen mainScreen].scale; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); CGContextRef context = CGBitmapContextCreate(nil, actualWidth * deviceScale, actualHeight * deviceScale, 8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaNone); CGContextScaleCTM(context, deviceScale, deviceScale); CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); CGContextFillRect(context, _fillView.bounds); CGContextSetBlendMode(context, kCGBlendModeMultiply); CGContextDrawImage(context, [_accessoryImageView convertRect:_accessoryImageView.bounds toView:_fillView], self.image.CGImage); CGImageRef grayImage = CGBitmapContextCreateImage(context); CGColorSpaceRelease(colorSpace); CGContextRelease(context); return [UIImage imageWithCGImage:grayImage scale:deviceScale orientation:UIImageOrientationUp]; } -(UIImage*)fillViewImage { if(_fillView.bounds.size.width <= 0 || _fillView.bounds.size.height <= 0) return nil; UIGraphicsBeginImageContextWithOptions(_fillView.bounds.size, NO, 0.0); [self.compatibleTintColor setFill]; // This image won't get tinted on iOS6, so pre-fill it with the right color // fill the background with a rounded corner UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.cornerRadius]; [path fill]; // cut out the title by drawing the title label with a 'clear' blend mode CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear); [_titleLabel drawTextInRect:[self.titleLabel convertRect:self.titleLabel.bounds toView:_fillView]]; UIImage* image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); if(self.image != nil) { CGImageRef cgImage = CGImageCreateWithMask(image.CGImage, self.fillViewMaskImage.CGImage); image = [UIImage imageWithCGImage:cgImage]; } return [image respondsToSelector:@selector(imageWithRenderingMode:)] ? [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] : image; } -(void)updateFillView { [self ensureFillView]; _fillView.image = [self fillViewImage]; } -(void)updateFillViewWhenNeeded { _fillView.image = nil; if(_fillView.alpha != 0) { [self updateFillView]; } } #pragma mark - Highlighting -(void)updateHighlightedStateAnimated { [self updateFillView]; [UIView animateWithDuration:0.45 animations:^{ _fillView.alpha = [self shouldShowFillView] ? 1 : 0; _accessoryAndLabelView.alpha = [self shouldShowFillView] ? 0 : 1; } completion:nil]; } -(void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; [self updateHighlightedStateAnimated]; } #pragma mark - set Text -(void)setTitle:(NSString *)title { self.titleLabel.text = title; [self updateFillViewWhenNeeded]; } -(NSString*)title { return self.titleLabel.text; } -(void)setAttributedTitle:(NSAttributedString *)attributedTitle { self.titleLabel.attributedText = attributedTitle; [self updateFillViewWhenNeeded]; } -(NSAttributedString*)attributedTitle { return self.titleLabel.attributedText; } #pragma mark - Set Image -(void)setImage:(UIImage *)image { _accessoryImageView.image = image; [self updateFillViewWhenNeeded]; } -(UIImage*)image { return _accessoryImageView.image; } #pragma mark - responding to updates -(void)setFrame:(CGRect)frame { BOOL sizeChanged = !CGSizeEqualToSize(frame.size, self.bounds.size); [super setFrame:frame]; if(sizeChanged) { [self updateFillViewWhenNeeded]; } } #pragma mark - set TintColor -(BOOL)supportsTintColor { return [[[UIDevice currentDevice] systemVersion] floatValue] >= 7; } -(UIColor*)compatibleTintColor { if([self supportsTintColor]) return self.tintColor; else return [UIColor colorWithRed:3.0/255.0 green:122.0/255.0 blue:255.0/255.0 alpha:1]; } -(void)setTintColor:(UIColor *)tintColor { if([self supportsTintColor]) { [super setTintColor:tintColor]; [self updateTintColors]; } } -(void)tintColorDidChange { [super tintColorDidChange]; [self updateTintColors]; } -(void)didMoveToWindow { [super didMoveToWindow]; [self updateTintColors]; } -(void)updateTintColors { _borderView.layer.borderColor = self.compatibleTintColor.CGColor; _titleLabel.textColor = self.compatibleTintColor; } @end ================================================ FILE: Source/BorderedButton/AveBorderedView.h ================================================ // // AveBorderedView.h // // The MIT License (MIT) // // Copyright (c) 2015 Andreas Verhoeven // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import /*! @abstract view that has a border and a cornerRadius. Use the .title property to change the title of this button. */ IB_DESIGNABLE @interface AveBorderedView : UIView @property (nonatomic, strong) IBInspectable UIColor* borderColor; // animatable @property (nonatomic, assign) IBInspectable CGFloat borderWidth; // animatable @property (nonatomic, assign) IBInspectable CGFloat cornerRadius; // animatable @end ================================================ FILE: Source/BorderedButton/AveBorderedView.m ================================================ // // AveBorderedView.m // // The MIT License (MIT) // // Copyright (c) 2015 Andreas Verhoeven // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import "AveBorderedView.h" @implementation AveBorderedView -(id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if(self != nil) { [self initDefaults]; [self updateBorder]; } return self; } -(instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if(self != nil) { [self initDefaults]; [self updateBorder]; } return self; } -(instancetype)init { self = [super init]; if(self != nil) { [self initDefaults]; [self updateBorder]; } return self; } -(void)initDefaults { _cornerRadius = 4; _borderWidth = 1; _borderColor = [UIColor blueColor]; } -(void)updateBorder { self.layer.borderWidth = self.borderWidth; self.layer.borderColor = self.borderColor.CGColor; self.layer.cornerRadius = self.cornerRadius; } -(void)setBorderColor:(UIColor *)borderColor { _borderColor = borderColor; [self updateBorder]; } -(void)setBorderWidth:(CGFloat)borderWidth { _borderWidth = borderWidth; [self updateBorder]; } -(void)setCornerRadius:(CGFloat)cornerRadius { _cornerRadius = cornerRadius; [self updateBorder]; } - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)key { if([key isEqualToString:@"borderColor"] || [key isEqualToString:@"cornerRadius"] || [key isEqualToString:@"borderWidth"]) { // piggy back on the animation for opacity, which only will be non nil if we are in an animation block. // This way, we will only animate those properties when in an animation block. CABasicAnimation* animation = (CABasicAnimation*)[layer actionForKey:@"opacity"]; if([animation isKindOfClass:[CABasicAnimation class]]) { animation.keyPath = key; animation.fromValue = [layer valueForKey:key]; animation.toValue = nil; animation.byValue = nil; return animation; } } return [super actionForLayer:layer forKey:key]; } @end ================================================ FILE: Source/PurchaseButton/AvePurchaseActivityIndicatorView.h ================================================ // // AvePurchaseActivityIndicatorView.h // // The MIT License (MIT) // // Copyright (c) 2015 Andreas Verhoeven // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import /** Activity indicator as used by AverPurchaseButton. Shows a rotating arc. */ @interface AvePurchaseActivityIndicatorView : UIView @property (nonatomic, assign) CGFloat lineWidth; - (void)startAnimating; - (void)stopAnimating; - (BOOL)isAnimating; @end ================================================ FILE: Source/PurchaseButton/AvePurchaseActivityIndicatorView.m ================================================ // // AvePurchaseActivityIndicatorView.m // // The MIT License (MIT) // // Copyright (c) 2015 Andreas Verhoeven // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import "AvePurchaseActivityIndicatorView.h" static NSString* const kAvePurchaseActivityIndicatorViewRotationAnimationKey = @"AvePurchaseActivityIndicatorViewRotationAnimationKey"; @implementation AvePurchaseActivityIndicatorView { UIImageView* _imageView; BOOL _animating; } -(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } -(void)ensureImageView { if(nil == _imageView) { _imageView = [[UIImageView alloc] initWithFrame:self.bounds]; _imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin; [self addSubview:_imageView]; } } -(void)setTintColor:(UIColor *)tintColor { if([self respondsToSelector:@selector(tintColor)]) // for iOS6 compatibility { [super setTintColor:tintColor]; } } - (void)layoutSubviews { [super layoutSubviews]; CGSize size = self.bounds.size; _imageView.bounds = CGRectMake(0, 0, size.width, size.height); _imageView.center = CGPointMake(size.width * 0.5, size.height * 0.5); } #pragma mark - Animation -(void)startAnimating { if(!_animating) { _animating = YES; // CA animations are removed when an app moves to the background, so we need to monitor for the app going to the // foreground in order to restart the rotation animation. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; } [self ensureImageView]; [self ensureImageViewImage]; [self ensureRotationAnimation]; } -(void)stopAnimating { if(_animating) { _animating = NO; [_imageView.layer removeAnimationForKey:kAvePurchaseActivityIndicatorViewRotationAnimationKey]; // not animating anymore, so no need to know when the app goes to the foreground [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil]; } } -(BOOL)isAnimating { return _animating; } -(void)ensureRotationAnimation { if([_imageView.layer animationForKey:kAvePurchaseActivityIndicatorViewRotationAnimationKey] == nil) { CABasicAnimation* rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation.toValue = @(M_PI * 2.0); rotationAnimation.duration = 1.0; rotationAnimation.cumulative = YES; rotationAnimation.repeatCount = HUGE_VALF; [_imageView.layer addAnimation:rotationAnimation forKey:kAvePurchaseActivityIndicatorViewRotationAnimationKey]; } } -(void)didMoveToWindow { [super didMoveToWindow]; // CA animations are also removed when a view is removed from a window, // so we need to restart the rotation animation if needed [self ensureAnimatingWhenNeeded]; } -(void)_applicationWillEnterForeground { [self ensureAnimatingWhenNeeded]; } -(void)ensureAnimatingWhenNeeded { if(_animating && self.window != nil) { [self ensureImageViewImage]; [self ensureRotationAnimation]; } } -(void)setFrame:(CGRect)frame { BOOL sizeChanged = !CGSizeEqualToSize(frame.size, self.bounds.size); [super setFrame:frame]; if(sizeChanged) { [self resetImageViewImage]; } } -(CGFloat)lineWidth { return _lineWidth == 0 ? 1 : _lineWidth; } #pragma mark - Image -(void)ensureImageViewImage { if(_imageView.image == nil) { _imageView.image = [self imageForAnimation]; } } -(void)resetImageViewImage { _imageView.image = nil; if(_animating && self.window != nil) { [self ensureImageViewImage]; } } -(UIImage*)imageForAnimation { CGRect rc = self.bounds; if(CGRectIsEmpty(rc) || CGRectIsNull(rc)) return nil; UIGraphicsBeginImageContextWithOptions(rc.size, NO, 0.0); // use blue, for iOS6 compatibility, since the image won't get tinted on iOS6 [[UIColor colorWithRed:3.0/255.0 green:122.0/255.0 blue:255.0/255.0 alpha:1] setStroke]; // create a circle CGContextSaveGState(UIGraphicsGetCurrentContext()); UIBezierPath* path = [UIBezierPath bezierPathWithOvalInRect:rc]; path.lineWidth = self.lineWidth * 2; [path addClip]; [path stroke]; CGContextRestoreGState(UIGraphicsGetCurrentContext()); // cut-out a hole UIRectFillUsingBlendMode(CGRectMake(rc.size.width - 6, rc.size.height / 2.0 - 2, 6, 6), kCGBlendModeClear); UIImage* image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return [image respondsToSelector:@selector(imageWithRenderingMode:)] ? [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] : image; } @end ================================================ FILE: Source/PurchaseButton/AvePurchaseButton.h ================================================ // // AvePurchaseButton.h // // The MIT License (MIT) // // Copyright (c) 2015 Andreas Verhoeven // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import typedef NS_ENUM(NSUInteger, AvePurchaseButtonState) { AvePurchaseButtonStateNormal, // normal, colored button AvePurchaseButtonStateConfirmation, // confirmation button, using the confirmationTitle/Color AvePurchaseButtonStateProgress, // progress indicator state }; /** @abstract A button which toggles between different states */ IB_DESIGNABLE @interface AvePurchaseButton : UIControl @property (nonatomic, assign) IBInspectable AvePurchaseButtonState buttonState; -(void)setButtonState:(AvePurchaseButtonState)buttonState animated:(BOOL)animated; @property (nonatomic, strong) IBInspectable UIImage* image; //Button font @property (nonatomic, strong) IBInspectable UIFont* titleLabelFont; // normal state colors @property (nonatomic, copy) IBInspectable NSString* normalTitle; @property (nonatomic, copy) IBInspectable NSAttributedString* attributedNormalTitle; @property (nonatomic, retain) IBInspectable UIColor* normalColor; // needed, equals tintColor // confirmation state colors @property (nonatomic, copy) IBInspectable NSString* confirmationTitle; @property (nonatomic, copy) IBInspectable NSAttributedString* attributedConfirmationTitle; @property (nonatomic, retain) IBInspectable UIColor* confirmationColor; @end ================================================ FILE: Source/PurchaseButton/AvePurchaseButton.m ================================================ // // AvePurchaseButton.m // // The MIT License (MIT) // // Copyright (c) 2015 Andreas Verhoeven // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #import "AvePurchaseButton.h" #import "AvePurchaseActivityIndicatorView.h" #import "AveBorderedButton.h" @implementation AvePurchaseButton { AveBorderedButton* _button; AvePurchaseActivityIndicatorView* _activityIndicatorView; NSString* _normalTitle; NSString* _confirmationTitle; NSAttributedString* _attributedNormalTitle; NSAttributedString* _attributedConfirmationTitle; } -(id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if(self != nil) { [self setup]; } return self; } -(id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if(self != nil) { [self setup]; } return self; } -(void)setup { self.clearsContextBeforeDrawing = YES; self.backgroundColor = [UIColor clearColor]; self.confirmationColor = [UIColor colorWithRed:0 green:0.5 blue:0 alpha:1]; _button = [[AveBorderedButton alloc] initWithFrame:self.bounds]; _button.titleLabel.font = [UIFont boldSystemFontOfSize:14]; _button.userInteractionEnabled = NO; [self addSubview:_button]; _activityIndicatorView = [[AvePurchaseActivityIndicatorView alloc] init]; _activityIndicatorView.userInteractionEnabled = NO; [self addSubview:_activityIndicatorView]; } -(void)layoutSubviews { [super layoutSubviews]; CGFloat progressSize = self.bounds.size.height; CGRect progressFrame = CGRectMake(self.bounds.size.width - progressSize,0, progressSize, progressSize); _activityIndicatorView.frame = progressFrame; if(self.buttonState != AvePurchaseButtonStateProgress) { _button.frame = CGRectMake(0, 0, CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)); } else { _button.frame = progressFrame; } } -(void)setImage:(UIImage *)image { _button.image = image; } -(UIImage*)image { return _button.image; } -(void)setButtonState:(AvePurchaseButtonState)buttonState { [self setButtonState:buttonState animated:NO]; } -(void)stopProgressAnimation { _activityIndicatorView.alpha = 0; _button.alpha = 1; [_activityIndicatorView stopAnimating]; } -(void)configureButtonForProgressState { _button.cornerRadius = self.bounds.size.height / 2.0; _button.titleEdgeInsets = UIEdgeInsetsMake(0, self.bounds.size.height, 0, -(self.bounds.size.width - self.bounds.size.height) - self.bounds.size.height); _button.frame = CGRectMake(self.bounds.size.width - self.bounds.size.height, 0, self.bounds.size.height, self.bounds.size.height); } -(void)configureButtonForRegularState { _button.cornerRadius = kAveBorderedButtonDefaultCornerRadius; _button.titleEdgeInsets = UIEdgeInsetsZero; _button.frame = self.bounds; } -(void)setButtonState:(AvePurchaseButtonState)buttonState animated:(BOOL)animated { if(buttonState != self.buttonState) { _buttonState = buttonState; [self updateTintColors]; [self updateTitle]; if(self.buttonState == AvePurchaseButtonStateProgress) { if(animated) { // don't animate the button highlight state. (use iOS6 compatible transactions, instead of performWithoutAnimation:) [CATransaction begin]; [CATransaction setDisableActions:YES]; _button.highlighted = NO; [CATransaction commit]; [UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionLayoutSubviews animations:^{ [self configureButtonForProgressState]; } completion:^(BOOL finished){ if(self.buttonState == AvePurchaseButtonStateProgress) { [self updateTintColors]; [UIView animateWithDuration:0.5 animations:^{ _button.alpha = 0; _activityIndicatorView.alpha = 1; [_activityIndicatorView startAnimating]; }]; } }]; } else { [self configureButtonForProgressState]; _button.alpha = 0; _activityIndicatorView.alpha = 1; [_activityIndicatorView startAnimating]; [self setNeedsLayout]; } } else if(self.buttonState == AvePurchaseButtonStateNormal) { _button.alpha = 1; _activityIndicatorView.alpha = 0; if(animated) { [UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionLayoutSubviews animations:^{ [self configureButtonForRegularState]; } completion:^(BOOL finished){ [self updateTintColors]; }]; } else { [self configureButtonForRegularState]; [self setNeedsLayout]; } } else if(self.buttonState == AvePurchaseButtonStateConfirmation) { [self configureButtonForRegularState]; [self setNeedsLayout]; } } else if(self.buttonState == AvePurchaseButtonStateProgress && !animated) { // make sure we restart animation in re-used TableViewCells: they remove // all subview animations when they get reused. [_activityIndicatorView startAnimating]; } } - (void)setNormalColor:(UIColor *)normalColor { _normalColor = normalColor; [self updateTintColors]; } -(void)setNormalTitle:(NSString *)normalTitle { _normalTitle = [normalTitle copy]; _attributedNormalTitle = nil; [self updateTitle]; } -(NSString*)normalTitle { if(_normalTitle != nil) return _normalTitle; else if(_attributedNormalTitle != nil) return _attributedNormalTitle.string; else return nil; } -(void)setAttributedNormalTitle:(NSAttributedString *)attributedNormalTitle { _attributedNormalTitle = [attributedNormalTitle copy]; _normalTitle = nil; [self updateTitle]; } -(NSAttributedString*)attributedNormalTitle { if(_attributedNormalTitle != nil) return _attributedNormalTitle; else if(_normalTitle != nil) return [[NSAttributedString alloc] initWithString:_normalTitle]; else return nil; } - (void)setConfirmationColor:(UIColor *)confirmationColor { _confirmationColor = confirmationColor; [self updateTintColors]; } -(void)setConfirmationTitle:(NSString *)confirmationTitle { _confirmationTitle = [confirmationTitle copy]; _attributedConfirmationTitle = nil; [self updateTitle]; } -(NSString*)confirmationTitle { if(_confirmationTitle != nil) return _confirmationTitle; else if(_attributedConfirmationTitle != nil) return _attributedConfirmationTitle.string; else return nil; } -(void)setAttributedConfirmationTitle:(NSAttributedString *)attributedConfirmationTitle { _attributedConfirmationTitle = [attributedConfirmationTitle copy]; _confirmationTitle = nil; [self updateTitle]; } -(NSAttributedString*)attributedConfirmationTitle { if(_attributedConfirmationTitle != nil) return _attributedConfirmationTitle.copy; else if(_confirmationTitle != nil) return [[NSAttributedString alloc] initWithString:_confirmationTitle]; else return nil; } -(CGSize)sizeThatFits:(CGSize)size { return [_button sizeThatFits:size]; } -(CGSize)intrinsicContentSize { return [self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; } -(void)updateTitle { if(self.buttonState == AvePurchaseButtonStateConfirmation) { if(_attributedConfirmationTitle != nil) _button.attributedTitle = _attributedConfirmationTitle; else _button.title = _confirmationTitle; } else { if(_attributedNormalTitle != nil) _button.attributedTitle = _attributedNormalTitle; else _button.title = _normalTitle; } [self invalidateIntrinsicContentSize]; } -(void)updateTintColors { _button.tintColor = self.buttonState == AvePurchaseButtonStateConfirmation ? self.confirmationColor : self.normalColor; _activityIndicatorView.tintColor = self.buttonState == AvePurchaseButtonStateConfirmation ? self.confirmationColor : self.normalColor; } -(void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; _button.highlighted = highlighted && self.buttonState != AvePurchaseButtonStateProgress; } -(UIFont *)titleLabelFont { return _button.titleLabel.font; } -(void)setTitleLabelFont:(UIFont *)font { _button.titleLabel.font = font; [self invalidateIntrinsicContentSize]; } @end