From 8af574ea0fe717f1ad5ba82e195f2c86f34ddec0 Mon Sep 17 00:00:00 2001 From: Igor Smirnov Date: Wed, 15 Apr 2015 14:46:25 +0300 Subject: [PATCH] swift version --- .../project.pbxproj | 276 ++++++++++ .../contents.xcworkspacedata | 7 + .../ODRefreshControlDemo/AppDelegate.swift | 24 + .../AppIcon.appiconset/Contents.json | 38 ++ .../LaunchImage.launchimage/Contents.json | 21 + Swift/Demo/ODRefreshControlDemo/Info.plist | 38 ++ .../ODRefreshControlDemo/ViewController.swift | 37 ++ Swift/ODRefreshControl/ODRefreshControl.swift | 481 ++++++++++++++++++ 8 files changed, 922 insertions(+) create mode 100644 Swift/Demo/ODRefreshControlDemo.xcodeproj/project.pbxproj create mode 100644 Swift/Demo/ODRefreshControlDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Swift/Demo/ODRefreshControlDemo/AppDelegate.swift create mode 100644 Swift/Demo/ODRefreshControlDemo/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Swift/Demo/ODRefreshControlDemo/Images.xcassets/LaunchImage.launchimage/Contents.json create mode 100644 Swift/Demo/ODRefreshControlDemo/Info.plist create mode 100644 Swift/Demo/ODRefreshControlDemo/ViewController.swift create mode 100644 Swift/ODRefreshControl/ODRefreshControl.swift diff --git a/Swift/Demo/ODRefreshControlDemo.xcodeproj/project.pbxproj b/Swift/Demo/ODRefreshControlDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..aad8fbe --- /dev/null +++ b/Swift/Demo/ODRefreshControlDemo.xcodeproj/project.pbxproj @@ -0,0 +1,276 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 780013EE1ADE80C0005F0382 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780013ED1ADE80C0005F0382 /* AppDelegate.swift */; }; + 780013F01ADE80C0005F0382 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780013EF1ADE80C0005F0382 /* ViewController.swift */; }; + 780013F51ADE80C0005F0382 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 780013F41ADE80C0005F0382 /* Images.xcassets */; }; + 7800140E1ADE82C7005F0382 /* ODRefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7800140D1ADE82C7005F0382 /* ODRefreshControl.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 780013E81ADE80C0005F0382 /* ODRefreshControlDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ODRefreshControlDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 780013EC1ADE80C0005F0382 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 780013ED1ADE80C0005F0382 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 780013EF1ADE80C0005F0382 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 780013F41ADE80C0005F0382 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 7800140D1ADE82C7005F0382 /* ODRefreshControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ODRefreshControl.swift; path = ../../ODRefreshControl/ODRefreshControl.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 780013E51ADE80C0005F0382 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 780013DF1ADE80C0005F0382 = { + isa = PBXGroup; + children = ( + 780013EA1ADE80C0005F0382 /* ODRefreshControlDemo */, + 780013E91ADE80C0005F0382 /* Products */, + ); + sourceTree = ""; + }; + 780013E91ADE80C0005F0382 /* Products */ = { + isa = PBXGroup; + children = ( + 780013E81ADE80C0005F0382 /* ODRefreshControlDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 780013EA1ADE80C0005F0382 /* ODRefreshControlDemo */ = { + isa = PBXGroup; + children = ( + 780013ED1ADE80C0005F0382 /* AppDelegate.swift */, + 780013EF1ADE80C0005F0382 /* ViewController.swift */, + 780013F41ADE80C0005F0382 /* Images.xcassets */, + 780013EB1ADE80C0005F0382 /* Supporting Files */, + ); + path = ODRefreshControlDemo; + sourceTree = ""; + }; + 780013EB1ADE80C0005F0382 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 7800140D1ADE82C7005F0382 /* ODRefreshControl.swift */, + 780013EC1ADE80C0005F0382 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 780013E71ADE80C0005F0382 /* ODRefreshControlDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 780014071ADE80C0005F0382 /* Build configuration list for PBXNativeTarget "ODRefreshControlDemo" */; + buildPhases = ( + 780013E41ADE80C0005F0382 /* Sources */, + 780013E51ADE80C0005F0382 /* Frameworks */, + 780013E61ADE80C0005F0382 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ODRefreshControlDemo; + productName = ODRefreshControlDemo; + productReference = 780013E81ADE80C0005F0382 /* ODRefreshControlDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 780013E01ADE80C0005F0382 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0630; + ORGANIZATIONNAME = "Complex Numbers"; + TargetAttributes = { + 780013E71ADE80C0005F0382 = { + CreatedOnToolsVersion = 6.3; + }; + }; + }; + buildConfigurationList = 780013E31ADE80C0005F0382 /* Build configuration list for PBXProject "ODRefreshControlDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 780013DF1ADE80C0005F0382; + productRefGroup = 780013E91ADE80C0005F0382 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 780013E71ADE80C0005F0382 /* ODRefreshControlDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 780013E61ADE80C0005F0382 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 780013F51ADE80C0005F0382 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 780013E41ADE80C0005F0382 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 780013F01ADE80C0005F0382 /* ViewController.swift in Sources */, + 7800140E1ADE82C7005F0382 /* ODRefreshControl.swift in Sources */, + 780013EE1ADE80C0005F0382 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 780014051ADE80C0005F0382 /* 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-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = 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_SYMBOLS_PRIVATE_EXTERN = NO; + 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 = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 780014061ADE80C0005F0382 /* 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 = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 780014081ADE80C0005F0382 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = ODRefreshControlDemo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 780014091ADE80C0005F0382 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = ODRefreshControlDemo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 780013E31ADE80C0005F0382 /* Build configuration list for PBXProject "ODRefreshControlDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 780014051ADE80C0005F0382 /* Debug */, + 780014061ADE80C0005F0382 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 780014071ADE80C0005F0382 /* Build configuration list for PBXNativeTarget "ODRefreshControlDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 780014081ADE80C0005F0382 /* Debug */, + 780014091ADE80C0005F0382 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 780013E01ADE80C0005F0382 /* Project object */; +} diff --git a/Swift/Demo/ODRefreshControlDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Swift/Demo/ODRefreshControlDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..828fe20 --- /dev/null +++ b/Swift/Demo/ODRefreshControlDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Swift/Demo/ODRefreshControlDemo/AppDelegate.swift b/Swift/Demo/ODRefreshControlDemo/AppDelegate.swift new file mode 100644 index 0000000..78d6e74 --- /dev/null +++ b/Swift/Demo/ODRefreshControlDemo/AppDelegate.swift @@ -0,0 +1,24 @@ +// +// AppDelegate.swift +// ODRefreshControlDemo +// +// Created by Igor Smirnov on 15/04/15. +// Copyright (c) 2015 Complex Numbers. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + window = UIWindow(frame: UIScreen.mainScreen().bounds) + window?.rootViewController = ViewController() + window?.makeKeyAndVisible() + return true + } + +} + diff --git a/Swift/Demo/ODRefreshControlDemo/Images.xcassets/AppIcon.appiconset/Contents.json b/Swift/Demo/ODRefreshControlDemo/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..118c98f --- /dev/null +++ b/Swift/Demo/ODRefreshControlDemo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "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" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Swift/Demo/ODRefreshControlDemo/Images.xcassets/LaunchImage.launchimage/Contents.json b/Swift/Demo/ODRefreshControlDemo/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000..e37b649 --- /dev/null +++ b/Swift/Demo/ODRefreshControlDemo/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "subtype" : "retina4", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Swift/Demo/ODRefreshControlDemo/Info.plist b/Swift/Demo/ODRefreshControlDemo/Info.plist new file mode 100644 index 0000000..0c2882f --- /dev/null +++ b/Swift/Demo/ODRefreshControlDemo/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.complexnumbers.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Swift/Demo/ODRefreshControlDemo/ViewController.swift b/Swift/Demo/ODRefreshControlDemo/ViewController.swift new file mode 100644 index 0000000..96a2ec0 --- /dev/null +++ b/Swift/Demo/ODRefreshControlDemo/ViewController.swift @@ -0,0 +1,37 @@ +// +// ViewController.swift +// ODRefreshControlDemo +// +// Created by Igor Smirnov on 15/04/15. +// Copyright (c) 2015 Complex Numbers. All rights reserved. +// + +import UIKit + +class ViewController: UITableViewController { + + override func viewDidLoad() { + super.viewDidLoad() + let refreshControl = ODRefreshControl(scrollView: tableView) + refreshControl.addTarget(self, action: Selector("dropViewDidBeginRefreshing:"), forControlEvents: .ValueChanged) + } + + override func shouldAutorotate() -> Bool { + if UIDevice.currentDevice().userInterfaceIdiom == .Phone { + let interfaceOrientation = UIApplication.sharedApplication().statusBarOrientation + return interfaceOrientation != .PortraitUpsideDown + } else { + return true + } + } + + func dropViewDidBeginRefreshing(refreshControl: ODRefreshControl) { + let delayInSeconds: UInt64 = 3 + let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delayInSeconds * NSEC_PER_SEC)) + dispatch_after(popTime, dispatch_get_main_queue()) { + refreshControl.endRefreshing() + } + } + +} + diff --git a/Swift/ODRefreshControl/ODRefreshControl.swift b/Swift/ODRefreshControl/ODRefreshControl.swift new file mode 100644 index 0000000..dcdf07a --- /dev/null +++ b/Swift/ODRefreshControl/ODRefreshControl.swift @@ -0,0 +1,481 @@ +// +// ORRefreshControl.swift +// +// Created by Igor Smirnov on 15/04/15. +// Copyright (c) 2015 Complex Nymbers. All rights reserved. +// +// https://github.com/megavolt605/ODRefreshControl +// +// +// Based on ODRefreshControl +// +// Created by Fabio Ritrovato on 6/13/12. +// Copyright (c) 2012 orange in a day. All rights reserved. +// +// https://github.com/Sephiroth87/ODRefreshControl +// +import UIKit + +class ODRefreshControl: UIControl { + + var shapeLayer: CAShapeLayer! + var arrowLayer: CAShapeLayer! + var highlightLayer: CAShapeLayer! + var activity: UIView! + var refreshing = false + var canRefresh = false + var ignoreInset = false + var ignoreOffset = false + var didSetInset = false + var hasSectionHeaders = false + var lastOffset: CGFloat = 0.0 + + var activityIndicatorViewStyle: UIActivityIndicatorViewStyle { + get { + if let a = activity as? UIActivityIndicatorView { + return a.activityIndicatorViewStyle + } + return .Gray + } + set { + if let a = activity as? UIActivityIndicatorView { + a.activityIndicatorViewStyle = newValue + } + } + } + + var activityIndicatorViewColor: UIColor? { + get { + if let a = activity as? UIActivityIndicatorView { + return a.color + } + return nil + } + set { + if let a = activity as? UIActivityIndicatorView { + a.color = newValue + } + } + } + + // setup + var kTotalViewHeight : CGFloat = 400 + var kOpenedViewHeight : CGFloat = 44 + var kMinTopPadding : CGFloat = 9 + var kMaxTopPadding : CGFloat = 5 + var kMinTopRadius : CGFloat = 12.5 + var kMaxTopRadius : CGFloat = 16 + var kMinBottomRadius : CGFloat = 3 + var kMaxBottomRadius : CGFloat = 16 + var kMinBottomPadding : CGFloat = 4 + var kMaxBottomPadding : CGFloat = 6 + var kMinArrowSize : CGFloat = 2 + var kMaxArrowSize : CGFloat = 3 + var kMinArrowRadius : CGFloat = 5 + var kMaxArrowRadius : CGFloat = 7 + var kMaxDistance : CGFloat = 53 + + var scrollView: UIScrollView! + var originalContentInset: UIEdgeInsets = UIEdgeInsetsZero + + func lerp(a: CGFloat, b: CGFloat, p: CGFloat) -> CGFloat { + return a + (b - a) * p; + } + + convenience init(scrollView: UIScrollView) { + self.init(scrollView: scrollView, activityIndicatorView: nil) + } + + init(scrollView: UIScrollView, activityIndicatorView activityView: UIView?) { + super.init(frame: CGRectMake(0, -(kTotalViewHeight + scrollView.contentInset.top), scrollView.frame.size.width, kTotalViewHeight)) + self.scrollView = scrollView; + originalContentInset = scrollView.contentInset; + + self.autoresizingMask = .FlexibleWidth; + scrollView.addSubview(self) + // NSKeyValueObservingOption + scrollView.addObserver(self, forKeyPath: "contentOffset", options: .New, context: nil) + scrollView.addObserver(self, forKeyPath: "contentInset", options: .New, context: nil) + + activity = activityView != nil ? activityView : UIActivityIndicatorView(activityIndicatorStyle: .Gray) + activity.center = CGPointMake(floor(self.frame.size.width / 2), floor(self.frame.size.height / 2)) + activity.autoresizingMask = .FlexibleLeftMargin | .FlexibleRightMargin + activity.alpha = 0 + if let a = activity as? UIActivityIndicatorView { + a.startAnimating() + } + addSubview(activity) + + refreshing = false + canRefresh = true + ignoreInset = false + ignoreOffset = false + didSetInset = false + hasSectionHeaders = false + + shapeLayer = CAShapeLayer() + shapeLayer.fillColor = tintColor.CGColor + shapeLayer.strokeColor = UIColor.darkGrayColor().colorWithAlphaComponent(0.5).CGColor + shapeLayer.lineWidth = 0.5 + shapeLayer.shadowColor = UIColor.blackColor().CGColor + shapeLayer.shadowOffset = CGSizeMake(0, 1) + shapeLayer.shadowOpacity = 0.4 + shapeLayer.shadowRadius = 0.5 + layer.addSublayer(shapeLayer) + + tintColor = UIColor(red:155.0 / 255.0, green: 162.0 / 255.0, blue: 172.0 / 255.0, alpha: 1.0) + + arrowLayer = CAShapeLayer() + arrowLayer.strokeColor = UIColor.darkGrayColor().colorWithAlphaComponent(0.5).CGColor + arrowLayer.lineWidth = 0.5 + arrowLayer.fillColor = UIColor.whiteColor().CGColor + shapeLayer.addSublayer(arrowLayer) + + highlightLayer = CAShapeLayer() + highlightLayer.fillColor = UIColor.whiteColor().colorWithAlphaComponent(0.2).CGColor + shapeLayer.addSublayer(highlightLayer) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + scrollView.removeObserver(self, forKeyPath: "contentOffset") + scrollView.removeObserver(self, forKeyPath: "contentInset") + scrollView = nil + } + + override var enabled: Bool { + didSet { + shapeLayer.hidden = !enabled + } + } + + override func willMoveToSuperview(newSuperview: UIView?) { + super.willMoveToSuperview(newSuperview) + if newSuperview == nil { + scrollView.removeObserver(self, forKeyPath: "contentOffset") + scrollView.removeObserver(self, forKeyPath: "contentInset") + scrollView = nil; + } + } + + override var tintColor: UIColor! { + didSet { + shapeLayer.fillColor = tintColor.CGColor + } + } + + override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) { + if keyPath == "contentInset" { + if !ignoreInset { + originalContentInset = (change["new"] as! NSValue).UIEdgeInsetsValue() + frame = CGRectMake(0, -(kTotalViewHeight + self.scrollView.contentInset.top), self.scrollView.frame.size.width, kTotalViewHeight) + } + return + } + + if !enabled || ignoreOffset { + return + } + + let offset = (change["new"] as! NSValue).CGPointValue().y + originalContentInset.top; + + if refreshing { + if offset != 0 { + // Keep thing pinned at the top + + CATransaction.begin() + CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) + shapeLayer.position = CGPointMake(0, kMaxDistance + offset + kOpenedViewHeight) + CATransaction.commit() + + activity.center = CGPointMake(floor(frame.size.width / 2.0), min(offset + frame.size.height + floor(kOpenedViewHeight / 2.0), frame.size.height - kOpenedViewHeight / 2.0)) + + ignoreInset = true + ignoreOffset = true + + if offset < 0 { + // Set the inset depending on the situation + if offset >= -kOpenedViewHeight { + if !scrollView.dragging { + if !didSetInset { + didSetInset = true + hasSectionHeaders = false + if let tv = scrollView as? UITableView { + for i in 0..= 0 { + // We can refresh again after the control is scrolled out of view + canRefresh = true + didSetInset = false + } else { + dontDraw = true + } + } else { + if offset >= 0 { + // Don't draw if the control is not visible + dontDraw = true + } + } + if offset > 0 && (lastOffset > offset) && !scrollView.tracking { + // If we are scrolling too fast, don't draw, and don't trigger unless the scrollView bounced back + canRefresh = false + dontDraw = true + } + if dontDraw { + shapeLayer.path = nil + shapeLayer.shadowPath = nil + arrowLayer.path = nil + highlightLayer.path = nil + lastOffset = offset + return + } + } + + lastOffset = offset + + var triggered = false + + let path = CGPathCreateMutable() + + //Calculate some useful points and values + let verticalShift = max(0, -((kMaxTopRadius + kMaxBottomRadius + kMaxTopPadding + kMaxBottomPadding) + offset)) + let distance = min(kMaxDistance, fabs(verticalShift)) + let percentage = 1 - (distance / kMaxDistance) + + let currentTopPadding = lerp(kMinTopPadding, b: kMaxTopPadding, p: percentage) + let currentTopRadius = lerp(kMinTopRadius, b: kMaxTopRadius, p: percentage) + let currentBottomRadius = lerp(kMinBottomRadius, b: kMaxBottomRadius, p: percentage) + let currentBottomPadding = lerp(kMinBottomPadding, b: kMaxBottomPadding, p: percentage) + + var bottomOrigin = CGPointMake(floor(self.bounds.size.width / 2), self.bounds.size.height - currentBottomPadding - currentBottomRadius) + var topOrigin = CGPointZero + if distance == 0 { + topOrigin = CGPointMake(floor(self.bounds.size.width / 2), bottomOrigin.y) + } else { + topOrigin = CGPointMake(floor(self.bounds.size.width / 2), self.bounds.size.height + offset + currentTopPadding + currentTopRadius) + if percentage == 0 { + bottomOrigin.y -= (fabs(verticalShift) - kMaxDistance) + triggered = true + } + } + + //Top semicircle + CGPathAddArc(path, nil, topOrigin.x, topOrigin.y, currentTopRadius, 0, CGFloat(M_PI), true) + + //Left curve + let leftCp1 = CGPointMake( + lerp((topOrigin.x - currentTopRadius), b: (bottomOrigin.x - currentBottomRadius), p: 0.1), + lerp(topOrigin.y, b: bottomOrigin.y, p: 0.2) + ) + let leftCp2 = CGPointMake( + lerp((topOrigin.x - currentTopRadius), b: (bottomOrigin.x - currentBottomRadius), p: 0.9), + lerp(topOrigin.y, b: bottomOrigin.y, p: 0.2) + ) + let leftDestination = CGPointMake(bottomOrigin.x - currentBottomRadius, bottomOrigin.y) + + CGPathAddCurveToPoint(path, nil, leftCp1.x, leftCp1.y, leftCp2.x, leftCp2.y, leftDestination.x, leftDestination.y) + + //Bottom semicircle + CGPathAddArc(path, nil, bottomOrigin.x, bottomOrigin.y, currentBottomRadius, CGFloat(M_PI), 0, true) + + //Right curve + let rightCp2 = CGPointMake( + lerp((topOrigin.x + currentTopRadius), b: (bottomOrigin.x + currentBottomRadius), p: 0.1), + lerp(topOrigin.y, b: bottomOrigin.y, p: 0.2) + ) + let rightCp1 = CGPointMake( + lerp((topOrigin.x + currentTopRadius), b: (bottomOrigin.x + currentBottomRadius), p: 0.9), + lerp(topOrigin.y, b: bottomOrigin.y, p: 0.2) + ) + let rightDestination = CGPointMake(topOrigin.x + currentTopRadius, topOrigin.y) + + CGPathAddCurveToPoint(path, nil, rightCp1.x, rightCp1.y, rightCp2.x, rightCp2.y, rightDestination.x, rightDestination.y) + CGPathCloseSubpath(path) + + if !triggered { + // Set paths + + shapeLayer.path = path; + shapeLayer.shadowPath = path; + + // Add the arrow shape + + let currentArrowSize = lerp(kMinArrowSize, b: kMaxArrowSize, p: percentage) + let currentArrowRadius = lerp(kMinArrowRadius, b: kMaxArrowRadius, p: percentage) + let arrowBigRadius = currentArrowRadius + (currentArrowSize / 2) + let arrowSmallRadius = currentArrowRadius - (currentArrowSize / 2) + let arrowPath = CGPathCreateMutable() + CGPathAddArc(arrowPath, nil, topOrigin.x, topOrigin.y, arrowBigRadius, 0, CGFloat(3 * M_PI_2), false) + CGPathAddLineToPoint(arrowPath, nil, topOrigin.x, topOrigin.y - arrowBigRadius - currentArrowSize) + CGPathAddLineToPoint(arrowPath, nil, topOrigin.x + (2 * currentArrowSize), topOrigin.y - arrowBigRadius + (currentArrowSize / 2)) + CGPathAddLineToPoint(arrowPath, nil, topOrigin.x, topOrigin.y - arrowBigRadius + (2 * currentArrowSize)) + CGPathAddLineToPoint(arrowPath, nil, topOrigin.x, topOrigin.y - arrowBigRadius + currentArrowSize) + CGPathAddArc(arrowPath, nil, topOrigin.x, topOrigin.y, arrowSmallRadius, CGFloat(3 * M_PI_2), 0, true) + CGPathCloseSubpath(arrowPath) + arrowLayer.path = arrowPath + arrowLayer.fillRule = kCAFillRuleEvenOdd + //CGPathRelease(arrowPath) + + // Add the highlight shape + + let highlightPath = CGPathCreateMutable() + CGPathAddArc(highlightPath, nil, topOrigin.x, topOrigin.y, currentTopRadius, 0, CGFloat(M_PI), true) + CGPathAddArc(highlightPath, nil, topOrigin.x, topOrigin.y + 1.25, currentTopRadius, CGFloat(M_PI), 0, false) + + highlightLayer.path = highlightPath + highlightLayer.fillRule = kCAFillRuleNonZero + //CGPathRelease(highlightPath) + + } else { + // Start the shape disappearance animation + + let radius = lerp(kMinBottomRadius, b: kMaxBottomRadius, p: 0.2) + let pathMorph = CABasicAnimation(keyPath: "path") + pathMorph.duration = 0.15 + pathMorph.fillMode = kCAFillModeForwards + pathMorph.removedOnCompletion = false + + let toPath = CGPathCreateMutable() + CGPathAddArc(toPath, nil, topOrigin.x, topOrigin.y, radius, 0, CGFloat(M_PI), true) + CGPathAddCurveToPoint(toPath, nil, topOrigin.x - radius, topOrigin.y, topOrigin.x - radius, topOrigin.y, topOrigin.x - radius, topOrigin.y) + CGPathAddArc(toPath, nil, topOrigin.x, topOrigin.y, radius, CGFloat(M_PI), 0, true) + CGPathAddCurveToPoint(toPath, nil, topOrigin.x + radius, topOrigin.y, topOrigin.x + radius, topOrigin.y, topOrigin.x + radius, topOrigin.y) + CGPathCloseSubpath(toPath) + pathMorph.toValue = toPath + shapeLayer.addAnimation(pathMorph, forKey: nil) + + let shadowPathMorph = CABasicAnimation(keyPath: "shadowPath") + shadowPathMorph.duration = 0.15 + shadowPathMorph.fillMode = kCAFillModeForwards + shadowPathMorph.removedOnCompletion = false + shadowPathMorph.toValue = toPath + shapeLayer.addAnimation(shadowPathMorph, forKey: nil) + //CGPathRelease(toPath); + + let shapeAlphaAnimation = CABasicAnimation(keyPath: "opacity") + shapeAlphaAnimation.duration = 0.1 + shapeAlphaAnimation.beginTime = CACurrentMediaTime() + 0.1 + shapeAlphaAnimation.toValue = NSNumber(float: 0) + shapeAlphaAnimation.fillMode = kCAFillModeForwards + shapeAlphaAnimation.removedOnCompletion = false + shapeLayer.addAnimation(shapeAlphaAnimation, forKey: nil) + + let alphaAnimation = CABasicAnimation(keyPath: "opacity") + alphaAnimation.duration = 0.1 + alphaAnimation.toValue = NSNumber(float: 0) + alphaAnimation.fillMode = kCAFillModeForwards + alphaAnimation.removedOnCompletion = false + arrowLayer.addAnimation(alphaAnimation, forKey: nil) + highlightLayer.addAnimation(alphaAnimation, forKey: nil) + + CATransaction.begin() + CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) + activity.layer.transform = CATransform3DMakeScale(0.1, 0.1, 1) + CATransaction.commit() + UIView.animateWithDuration(0.2, delay:0.15, options: UIViewAnimationOptions.CurveLinear, + animations: { + self.activity.alpha = 1 + self.activity.layer.transform = CATransform3DMakeScale(1, 1, 1) + }, + completion: nil + ) + + refreshing = true + canRefresh = false + sendActionsForControlEvents(.ValueChanged) + } + + //CGPathRelease(path) + } + + func beginRefreshing() { + if !refreshing { + let alphaAnimation = CABasicAnimation(keyPath: "opacity") + alphaAnimation.duration = 0.0001 + alphaAnimation.toValue = NSNumber(float: 0) + alphaAnimation.fillMode = kCAFillModeForwards + alphaAnimation.removedOnCompletion = false + shapeLayer.addAnimation(alphaAnimation, forKey: nil) + arrowLayer.addAnimation(alphaAnimation, forKey: nil) + highlightLayer.addAnimation(alphaAnimation, forKey: nil) + + activity.alpha = 1 + activity.layer.transform = CATransform3DMakeScale(1, 1, 1) + + let offset = self.scrollView.contentOffset + ignoreInset = true + scrollView.contentInset = UIEdgeInsetsMake(kOpenedViewHeight + originalContentInset.top, originalContentInset.left, originalContentInset.bottom, originalContentInset.right) + ignoreInset = false + scrollView.setContentOffset(offset, animated: false) + + refreshing = true + canRefresh = false + } + } + + func endRefreshing() { + if refreshing { + refreshing = false + // Create a temporary retain-cycle, so the scrollView won't be released + // halfway through the end animation. + // This allows for the refresh control to clean up the observer, + // in the case the scrollView is released while the animation is running + + //__block UIScrollView *blockScrollView = self.scrollView; + let sv = scrollView + UIView.animateWithDuration(0.4, + animations: { [sv] + self.ignoreInset = true + sv.contentInset = self.originalContentInset + self.ignoreInset = false + self.activity.alpha = 0 + self.activity.layer.transform = CATransform3DMakeScale(0.1, 0.1, 1) + }, completion: { [sv] finished in + self.shapeLayer.removeAllAnimations() + self.shapeLayer.path = nil + self.shapeLayer.shadowPath = nil + self.shapeLayer.position = CGPointZero + self.arrowLayer.removeAllAnimations() + self.arrowLayer.path = nil + self.highlightLayer.removeAllAnimations() + self.highlightLayer.path = nil + // We need to use the scrollView somehow in the end block, + // or it'll get released in the animation block. + self.ignoreInset = true + sv.contentInset = self.originalContentInset + self.ignoreInset = false + } + ) + } + } +}