diff --git a/Example/Base.lproj/Main_iPhone.storyboard b/Example/Base.lproj/Main_iPhone.storyboard index 2860eb7..e1673a9 100644 --- a/Example/Base.lproj/Main_iPhone.storyboard +++ b/Example/Base.lproj/Main_iPhone.storyboard @@ -1,7 +1,7 @@ - + @@ -165,12 +165,27 @@ + + + + + + + + + @@ -353,9 +368,30 @@ - + + + + + + + + + + + + @@ -375,7 +411,7 @@ - + @@ -395,8 +431,29 @@ + + + + + + + + + + + - + @@ -417,7 +474,7 @@ - + @@ -438,7 +495,7 @@ - + @@ -459,7 +516,7 @@ - + @@ -480,7 +537,7 @@ - + @@ -501,7 +558,7 @@ - + @@ -522,7 +579,7 @@ - + @@ -543,7 +600,7 @@ - + @@ -564,7 +621,7 @@ - + @@ -585,7 +642,7 @@ - + diff --git a/Example/MRActivityIndicatorViewController.m b/Example/MRActivityIndicatorViewController.m index 758f00f..b16e04e 100644 --- a/Example/MRActivityIndicatorViewController.m +++ b/Example/MRActivityIndicatorViewController.m @@ -14,6 +14,7 @@ @interface MRActivityIndicatorViewController () @property (weak, nonatomic) IBOutlet MRActivityIndicatorView *activityIndicatorView; +@property (weak, nonatomic) IBOutlet UISwitch *stopButtonSwitch; @end @@ -33,6 +34,10 @@ - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { } } +- (IBAction)onStopButtonSwitchValueChanged:(id)sender { + self.activityIndicatorView.mayStop = self.stopButtonSwitch.on; +} + - (void)colorPaletteViewController:(MRColorPaletteViewController *)colorPaletteViewController didSelectColor:(UIColor *)color { self.activityIndicatorView.tintColor = color; } diff --git a/Example/MRCircularProgressViewController.m b/Example/MRCircularProgressViewController.m index d1b4a08..959f89f 100644 --- a/Example/MRCircularProgressViewController.m +++ b/Example/MRCircularProgressViewController.m @@ -25,7 +25,7 @@ @implementation MRCircularProgressViewController - (void)viewDidLoad { [super viewDidLoad]; - [self.circularProgressView addTarget:self action:@selector(onCircularProgressViewTouchUpInside:) forControlEvents:UIControlEventTouchUpInside]; + [self.circularProgressView.stopButton addTarget:self action:@selector(onCircularProgressViewTouchUpInside:) forControlEvents:UIControlEventTouchUpInside]; } - (IBAction)onProgressSliderValueChanged:(id)sender { diff --git a/Example/MRProgressOverlayTableViewController.m b/Example/MRProgressOverlayTableViewController.m index 0f09281..4120177 100644 --- a/Example/MRProgressOverlayTableViewController.m +++ b/Example/MRProgressOverlayTableViewController.m @@ -34,6 +34,16 @@ - (IBAction)onShowIndeterminateProgressView:(id)sender { } afterDelay:2.0]; } +- (IBAction)onShowIndeterminateMayStopProgressView:(id)sender { + MRProgressOverlayView *progressView = [MRProgressOverlayView new]; + progressView.mode = MRProgressOverlayViewModeDeterminateCircular; + progressView.stopBlock = ^(MRProgressOverlayView *view){ + [view dismiss:YES]; + }; + [self.rootView addSubview:progressView]; + [self simulateProgressView:progressView]; +} + - (IBAction)onShowIndeterminateNoTextProgressView:(id)sender { MRProgressOverlayView *progressView = [MRProgressOverlayView new]; progressView.titleLabelText = @""; @@ -51,6 +61,16 @@ - (IBAction)onShowDeterminateCircularProgressView:(id)sender { [self simulateProgressView:progressView]; } +- (IBAction)onShowDeterminateCircularMayStopProgressView:(id)sender { + MRProgressOverlayView *progressView = [MRProgressOverlayView new]; + progressView.mode = MRProgressOverlayViewModeDeterminateCircular; + progressView.stopBlock = ^(MRProgressOverlayView *view){ + [view dismiss:YES]; + }; + [self.rootView addSubview:progressView]; + [self simulateProgressView:progressView]; +} + - (IBAction)onShowDeterminateCircularNoTextProgressView:(id)sender { MRProgressOverlayView *progressView = [MRProgressOverlayView new]; progressView.titleLabelText = @""; diff --git a/Images/circular_determinate.gif b/Images/circular_determinate.gif new file mode 100644 index 0000000..466a6bf Binary files /dev/null and b/Images/circular_determinate.gif differ diff --git a/Images/circular_indeterminate.gif b/Images/circular_indeterminate.gif new file mode 100644 index 0000000..afcb8ab Binary files /dev/null and b/Images/circular_indeterminate.gif differ diff --git a/MRProgress.podspec b/MRProgress.podspec index a56c25c..3bd60d3 100644 --- a/MRProgress.podspec +++ b/MRProgress.podspec @@ -3,6 +3,7 @@ Pod::Spec.new do |s| s.version = '0.2.2' s.summary = 'Collection of iOS drop-in components to visualize progress with different modes' s.homepage = 'https://github.com/mrackwitz/MRProgress' + s.social_media_url = 'https://twitter.com/mrackwitz' s.author = { 'Marius Rackwitz' => 'git@mariusrackwitz.de' } s.license = 'MIT License' s.source = { :git => 'https://github.com/mrackwitz/MRProgress.git', :tag => s.version.to_s } diff --git a/MRProgress.xcodeproj/project.pbxproj b/MRProgress.xcodeproj/project.pbxproj index 39415ae..6b402c7 100644 --- a/MRProgress.xcodeproj/project.pbxproj +++ b/MRProgress.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 29CE9679186E0129006006B0 /* MRStopButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 29CE9677186E0129006006B0 /* MRStopButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 29CE967A186E0129006006B0 /* MRStopButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 29CE9678186E0129006006B0 /* MRStopButton.m */; }; 710BD58B1816AD3700654750 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 710BD58A1816AD3700654750 /* QuartzCore.framework */; }; 710BD5AD1817353E00654750 /* MRBlurView.h in Headers */ = {isa = PBXBuildFile; fileRef = 710BD5961817353E00654750 /* MRBlurView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 710BD5AE1817353E00654750 /* MRBlurView.m in Sources */ = {isa = PBXBuildFile; fileRef = 710BD5971817353E00654750 /* MRBlurView.m */; }; @@ -35,6 +37,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 299730941881CB520022B27B /* MRStopableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MRStopableView.h; sourceTree = ""; }; + 29CE9677186E0129006006B0 /* MRStopButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MRStopButton.h; sourceTree = ""; }; + 29CE9678186E0129006006B0 /* MRStopButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MRStopButton.m; sourceTree = ""; }; 710BD58A1816AD3700654750 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 710BD5961817353E00654750 /* MRBlurView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MRBlurView.h; sourceTree = ""; }; 710BD5971817353E00654750 /* MRBlurView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MRBlurView.m; sourceTree = ""; }; @@ -104,6 +109,9 @@ 710BD5A41817353E00654750 /* MRNavigationBarProgressView.m */, 710BD5A51817353E00654750 /* MRProgressOverlayView.h */, 710BD5A61817353E00654750 /* MRProgressOverlayView.m */, + 299730941881CB520022B27B /* MRStopableView.h */, + 29CE9677186E0129006006B0 /* MRStopButton.h */, + 29CE9678186E0129006006B0 /* MRStopButton.m */, ); path = Components; sourceTree = ""; @@ -191,6 +199,7 @@ 710BD5BD1817353E00654750 /* MRMessageInterceptor.h in Headers */, 710BD5B91817353E00654750 /* MRNavigationBarProgressView.h in Headers */, 710BD5C01817353E00654750 /* MRWeakProxy.h in Headers */, + 29CE9679186E0129006006B0 /* MRStopButton.h in Headers */, 710BD5B31817353E00654750 /* MRCircularProgressView.h in Headers */, 710BD5B11817353E00654750 /* MRActivityIndicatorView.h in Headers */, 7197CF011816A1A10022A19D /* MRProgress.h in Headers */, @@ -275,6 +284,7 @@ 710BD5BC1817353E00654750 /* MRProgressOverlayView.m in Sources */, 710BD5B01817353E00654750 /* UIImage+MRImageEffects.m in Sources */, 710BD5AE1817353E00654750 /* MRBlurView.m in Sources */, + 29CE967A186E0129006006B0 /* MRStopButton.m in Sources */, 710BD5B41817353E00654750 /* MRCircularProgressView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/README.md b/README.md index 720fd00..8115b70 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ MRProgress is a collection of drop-in components that display a dimmed overlay w [![](Images/screenshot_009_2.jpg)](Images/screenshot_009.png) [![](Images/screenshot_010_2.jpg)](Images/screenshot_010.png) -* **Component oriented**: You don't have to use all components or ```MRProgressOverlayView```. You can use just the custom activity indicators or progress views. +* **Component oriented**: You don't have to use all components or `MRProgressOverlayView`. You can use just the custom activity indicators or progress views. * **Configurable**: All components implement tintColor. * **Customizable**: You can replace the given blur implementation and hook into your own you are maybe already using in other places of your app. Or simply throw in an [UIToolbar's layer](https://github.com/JagCesar/iOS-blur), if you prefer Apple's implementation. (Current blur implementation is as given by sample code of WWDC 2013.) * **Reusable**: The code is fragmented in small reusable pieces. @@ -22,56 +22,59 @@ MRProgress is a collection of drop-in components that display a dimmed overlay w ## Components -The components used in ```MRProgressOverlayView``` could be used seperately. +The components used in `MRProgressOverlayView` could be used seperately. The provided Example app demonstrates how they can be used. -### ```MRProgressOverlayView``` +### `MRProgressOverlayView` ![](Images/screenshot_004_1.jpg) * Supports different modes * Animated show and hide * Blured background -* With UIMotionEffects for tilting like ```UIAlertView``` +* With UIMotionEffects for tilting like `UIAlertView` * Supports multi-line title label text -### ```MRCircularProgressView``` +### `MRCircularProgressView` -![](Images/screenshot_001_1.jpg) +![](Images/circular_determinate.gif) ![](Images/screenshot_002_1.jpg) +* Tint color can be changed * Circular progress view like in AppStore -* Inherits from ```UIControl``` and can display a stop button -* Animated with ```CADisplayLink``` +* Can display a stop button +* Animated with `CABasicAnimation` +* Percentage label change is animated over a `NSTimer` -### ```MRNavigationBarProgressView``` +### `MRNavigationBarProgressView` ![](Images/screenshot_003_1.jpg) -* Display a progress view in a ```UINavigationController``` -* Hooks ```UINavigationControllerDelegate``` and is automatically removed on push or pop -* Can be used in ```UINavigationBar``` or ```UIToolbar``` +* Display a progress view in a `UINavigationController` +* Hooks `UINavigationControllerDelegate` and is automatically removed on push or pop +* Can be used in `UINavigationBar` or `UIToolbar` -### ```MRCheckmarkIconView``` and ```MRCrossIconView``` +### `MRCheckmarkIconView` and `MRCrossIconView` ![](Images/screenshot_011_1.jpg) * Tint color can be changed * Scalable * Animatable -* Backed by ```CAShapeLayer``` +* Backed by `CAShapeLayer` -### ```MRActivityIndicatorView``` +### `MRActivityIndicatorView` -![](Images/screenshot_012_1.jpg) +![](Images/circular_indeterminate.gif) * Tint color can be changed -* Same API as ```UIActivityIndicatorView``` -* Animated with ```CABasicAnimation``` +* Same API as `UIActivityIndicatorView` +* Can display a stop button +* Animated with `CABasicAnimation` @@ -81,16 +84,16 @@ The provided Example app demonstrates how they can be used. [CocoaPods](http://www.cocoapods.org) is the recommended way to add MRProgress to your project. -1. Add a pod entry for MRProgress to your *Podfile* ```pod 'MRProgress', '~> 0.2'```. -2. Install the pod(s) by running ```pod install```. -3. Include MRProgress wherever you need it with ```#import "MRProgress.h"```. +1. Add a pod entry for MRProgress to your *Podfile* `pod 'MRProgress', '~> 0.3'`. +2. Install the pod(s) by running `pod install`. +3. Include MRProgress wherever you need it with `#import "MRProgress.h"`. ### Source files 1. Download the [latest code version](http://github.com/mrackwitz/MRProgress/archive/master.zip) or add the repository as a git submodule to your git-tracked project. 2. Drag and drop the **src** directory from the archive in your project navigator. Make sure to select *Copy items* when asked if you extracted the code archive outside of your project. -3. Include MRProgress wherever you need any component with ```#import "MRProgress.h"```. +3. Include MRProgress wherever you need any component with `#import "MRProgress.h"`. ### Static library @@ -98,7 +101,7 @@ The provided Example app demonstrates how they can be used. 1. Drag and drop **MRProgress.xcodeproj** in your project navigator. 2. Select your target and go to the *Build Phases* tab. In the *Link Binary With Libraries* section select the add button. On the sheet find and add libMRProgress.a. 3. Add Target **MRProgress** to your *Target Dependencies* list. -4. ```import ``` whereever you want to use the components. You could add it to your Prefix header file, if you want. +4. `import ` whereever you want to use the components. You could add it to your Prefix header file, if you want. @@ -164,15 +167,15 @@ Make sure you also see [MRProgress documentation on Cocoadocs](http://cocoadocs. ### Modes -Name (```MRProgressOverlayView<...>```) | Screenshot | Description ----------------------------------------- | --------------------------------------------------------------- | :----------- -**Indeterminate** | [![](Images/screenshot_004_2.jpg)](Images/screenshot_004.png) | Progress is shown using a large round activity indicator view. (```MRActivityIndicatorView```) This is the default. -**DeterminateCircular** | [![](Images/screenshot_005_2.jpg)](Images/screenshot_005.png) | Progress is shown using a round, pie-chart like, progress view. (```MRCircularProgressView```) -**DeterminateHorizontalBar** | [![](Images/screenshot_006_2.jpg)](Images/screenshot_006.png) | Progress is shown using a horizontal progress bar. (```UIProgressView```) -**IndeterminateSmall** | [![](Images/screenshot_007_2.jpg)](Images/screenshot_007.png) | Shows primarily a label. Progress is shown using a small activity indicator. (```MRActivityIndicatorView```) -**IndeterminateSmallDefault** | [![](Images/screenshot_008_2.jpg)](Images/screenshot_008.png) | Shows primarily a label. Progress is shown using a small activity indicator. (```UIActivityIndicatorView``` in ```UIActivityIndicatorViewStyleGray```) -**Checkmark** | [![](Images/screenshot_009_2.jpg)](Images/screenshot_009.png) | Shows a checkmark. (```MRCheckmarkIconView```) -**Cross** | [![](Images/screenshot_010_2.jpg)](Images/screenshot_010.png) | Shows a cross. (```MRCrossIconView```) +Name (`MRProgressOverlayView<...>`) | Screenshot | Description +------------------------------------ | ------------------------------------------------------------- | :----------- +**Indeterminate** | [![](Images/screenshot_004_2.jpg)](Images/screenshot_004.png) | Progress is shown using a large round activity indicator view. (`MRActivityIndicatorView`) This is the default. +**DeterminateCircular** | [![](Images/screenshot_005_2.jpg)](Images/screenshot_005.png) | Progress is shown using a round, pie-chart like, progress view. (`MRCircularProgressView`) +**DeterminateHorizontalBar** | [![](Images/screenshot_006_2.jpg)](Images/screenshot_006.png) | Progress is shown using a horizontal progress bar. (`UIProgressView`) +**IndeterminateSmall** | [![](Images/screenshot_007_2.jpg)](Images/screenshot_007.png) | Shows primarily a label. Progress is shown using a small activity indicator. (`MRActivityIndicatorView`) +**IndeterminateSmallDefault** | [![](Images/screenshot_008_2.jpg)](Images/screenshot_008.png) | Shows primarily a label. Progress is shown using a small activity indicator. (`UIActivityIndicatorView` in `UIActivityIndicatorViewStyleGray`) +**Checkmark** | [![](Images/screenshot_009_2.jpg)](Images/screenshot_009.png) | Shows a checkmark. (`MRCheckmarkIconView`) +**Cross** | [![](Images/screenshot_010_2.jpg)](Images/screenshot_010.png) | Shows a cross. (`MRCrossIconView`) diff --git a/src/Components/MRActivityIndicatorView.h b/src/Components/MRActivityIndicatorView.h index ac8f6bd..95515fa 100644 --- a/src/Components/MRActivityIndicatorView.h +++ b/src/Components/MRActivityIndicatorView.h @@ -7,13 +7,14 @@ // #import +#import "MRStopableView.h" /** Use an activity indicator to show that a task is in progress. An activity indicator appears as a circle slice that is either spinning or stopped. */ -@interface MRActivityIndicatorView : UIControl { +@interface MRActivityIndicatorView : UIView { @package BOOL _animating; } diff --git a/src/Components/MRActivityIndicatorView.m b/src/Components/MRActivityIndicatorView.m index 1415a3f..3df7ece 100644 --- a/src/Components/MRActivityIndicatorView.m +++ b/src/Components/MRActivityIndicatorView.m @@ -8,6 +8,7 @@ #import #import "MRActivityIndicatorView.h" +#import "MRStopButton.h" NSString *const MRActivityIndicatorViewSpinAnimationKey = @"MRActivityIndicatorViewSpinAnimationKey"; @@ -15,11 +16,16 @@ @interface MRActivityIndicatorView () +@property (nonatomic, weak) CAShapeLayer *shapeLayer; +@property (nonatomic, weak, readwrite) MRStopButton *stopButton; + @end @implementation MRActivityIndicatorView +@synthesize stopButton = _stopButton; + - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { @@ -36,19 +42,21 @@ - (id)initWithCoder:(NSCoder *)aDecoder { return self; } -+ (Class)layerClass { - return CAShapeLayer.class; -} - -- (CAShapeLayer *)shapeLayer { - return (CAShapeLayer *)self.layer; -} - - (void)commonInit { self.hidesWhenStopped = YES; - self.layer.borderWidth = 0; - self.shapeLayer.lineWidth = 2.0f; - self.shapeLayer.fillColor = UIColor.clearColor.CGColor; + + CAShapeLayer *shapeLayer = [CAShapeLayer new]; + shapeLayer.borderWidth = 0; + shapeLayer.lineWidth = 2.0f; + shapeLayer.fillColor = UIColor.clearColor.CGColor; + [self.layer addSublayer:shapeLayer]; + self.shapeLayer = shapeLayer; + + MRStopButton *stopButton = [MRStopButton new]; + [self addSubview:stopButton]; + self.stopButton = stopButton; + + self.mayStop = NO; } - (void)dealloc { @@ -85,16 +93,18 @@ - (void)applicationWillEnterForeground:(NSNotificationCenter *)note { - (void)layoutSubviews { [super layoutSubviews]; - CGRect frame = self.frame; + CGRect frame = self.bounds; if (frame.size.width != frame.size.height) { // Ensure that we have a square frame - CGFloat s = MAX(frame.size.width, frame.size.height); + CGFloat s = MIN(frame.size.width, frame.size.height); frame.size.width = s; frame.size.height = s; - self.frame = frame; } + self.shapeLayer.frame = frame; self.shapeLayer.path = [self layoutPath].CGPath; + + self.stopButton.frame = [self.stopButton frameThatFits:self.bounds]; } - (UIBezierPath *)layoutPath { @@ -116,6 +126,7 @@ - (UIBezierPath *)layoutPath { - (void)tintColorDidChange { [super tintColorDidChange]; self.shapeLayer.strokeColor = self.tintColor.CGColor; + self.stopButton.tintColor = self.tintColor; } @@ -130,6 +141,17 @@ - (CGFloat)lineWidth { } +#pragma mark - MRStopableView's implementation + +- (void)setMayStop:(BOOL)mayStop { + self.stopButton.hidden = !mayStop; +} + +- (BOOL)mayStop { + return !self.stopButton.hidden; +} + + #pragma mark - Control animation - (void)startAnimating { @@ -177,11 +199,11 @@ - (void)addAnimation { spinAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; spinAnimation.duration = 1.0; spinAnimation.repeatCount = INFINITY; - [self.layer addAnimation:spinAnimation forKey:MRActivityIndicatorViewSpinAnimationKey]; + [self.shapeLayer addAnimation:spinAnimation forKey:MRActivityIndicatorViewSpinAnimationKey]; } - (void)removeAnimation { - [self.layer removeAnimationForKey:MRActivityIndicatorViewSpinAnimationKey]; + [self.shapeLayer removeAnimationForKey:MRActivityIndicatorViewSpinAnimationKey]; } @end diff --git a/src/Components/MRCircularProgressView.h b/src/Components/MRCircularProgressView.h index 1f54dfb..bf8d9cb 100644 --- a/src/Components/MRCircularProgressView.h +++ b/src/Components/MRCircularProgressView.h @@ -7,25 +7,18 @@ // #import +#import "MRStopableView.h" /** You use the MRCircularProgressView class to depict the progress of a task over time. */ -@interface MRCircularProgressView : UIControl - -/** - A Boolean value that controls whether the receiver shows a stop button. - - If the value of this property is NO (the default), the receiver doesnot show a stop button. If the mayStop property is - YES a stop button will be shown. You can catch fired events like known from UIControl. - */ -@property (nonatomic, assign) BOOL mayStop; +@interface MRCircularProgressView : UIView /** Current progress. - Use associated setter for non animated changes. Otherwises use setProgress:aniamted:. + Use associated setter for non animated changes. Otherwises use setProgress:animated:. */ @property (nonatomic, assign) float progress; diff --git a/src/Components/MRCircularProgressView.m b/src/Components/MRCircularProgressView.m index af5dd22..d2a0fd3 100644 --- a/src/Components/MRCircularProgressView.m +++ b/src/Components/MRCircularProgressView.m @@ -9,25 +9,28 @@ #import #import "MRCircularProgressView.h" #import "MRProgressHelper.h" -#import "MRWeakProxy.h" +#import "MRStopButton.h" + + +NSString *const MRCircularProgressViewProgressAnimationKey = @"MRCircularProgressViewProgressAnimationKey"; @interface MRCircularProgressView () @property (nonatomic, strong, readwrite) NSNumberFormatter *numberFormatter; +@property (nonatomic, strong, readwrite) NSTimer *valueLabelUpdateTimer; @property (nonatomic, weak, readwrite) UILabel *valueLabel; -@property (nonatomic, weak, readwrite) UIView *stopView; - -@property (nonatomic, assign, readwrite) float fromProgress; -@property (nonatomic, assign, readwrite) float toProgress; -@property (nonatomic, assign, readwrite) CFTimeInterval startTime; -@property (nonatomic, strong, readwrite) CADisplayLink *displayLink; +@property (nonatomic, weak, readwrite) MRStopButton *stopButton; @end -@implementation MRCircularProgressView +@implementation MRCircularProgressView { + int _valueLabelProgressPercentDifference; +} + +@synthesize stopButton = _stopButton; - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; @@ -57,9 +60,6 @@ - (void)commonInit { _animationDuration = 0.3; self.progress = 0; - [self addTarget:self action:@selector(didTouchDown) forControlEvents:UIControlEventTouchDown]; - [self addTarget:self action:@selector(didTouchUpInside) forControlEvents:UIControlEventTouchUpInside]; - NSNumberFormatter *numberFormatter = [NSNumberFormatter new]; self.numberFormatter = numberFormatter; numberFormatter.numberStyle = NSNumberFormatterPercentStyle; @@ -77,13 +77,16 @@ - (void)commonInit { valueLabel.textAlignment = NSTextAlignmentCenter; [self addSubview:valueLabel]; - UIControl *stopView = [UIControl new]; - self.stopView = stopView; - [self addSubview:stopView]; + MRStopButton *stopButton = [MRStopButton new]; + [self addSubview:stopButton]; + self.stopButton = stopButton; - [self mayStopDidChange]; + self.mayStop = NO; } + +#pragma mark - Layout + - (void)layoutSubviews { [super layoutSubviews]; @@ -96,24 +99,13 @@ - (void)layoutSubviews { self.layer.cornerRadius = self.frame.size.width / 2.0f; self.shapeLayer.path = [self layoutPath].CGPath; - CGFloat stopViewSizeValue = MIN(self.bounds.size.width, self.bounds.size.height); - CGSize stopViewSize = CGSizeMake(stopViewSizeValue, stopViewSizeValue); - const CGFloat stopViewSizeRatio = 0.35; - CGRect stopViewFrame = CGRectInset(MRCenterCGSizeInCGRect(stopViewSize, self.bounds), - self.bounds.size.width * stopViewSizeRatio, - self.bounds.size.height * stopViewSizeRatio); - if (self.tracking && self.touchInside) { - stopViewFrame = CGRectInset(stopViewFrame, - self.bounds.size.width * 0.033, - self.bounds.size.height * 0.033); - } - self.stopView.frame = stopViewFrame; + self.stopButton.frame = [self.stopButton frameThatFits:self.bounds]; } - (UIBezierPath *)layoutPath { const double TWO_M_PI = 2.0 * M_PI; const double startAngle = 0.75 * TWO_M_PI; - const double endAngle = startAngle + TWO_M_PI * self.progress; + const double endAngle = startAngle + TWO_M_PI; CGFloat width = self.frame.size.width; return [UIBezierPath bezierPathWithArcCenter:CGPointMake(width/2.0f, width/2.0f) @@ -132,46 +124,19 @@ - (void)tintColorDidChange { self.shapeLayer.strokeColor = tintColor.CGColor; self.layer.borderColor = tintColor.CGColor; self.valueLabel.textColor = tintColor; - self.stopView.backgroundColor = tintColor; + self.stopButton.backgroundColor = tintColor; } -#pragma mark - May stop implementation +#pragma mark - MRStopableView's implementation - (void)setMayStop:(BOOL)mayStop { - _mayStop = mayStop; - [self mayStopDidChange]; + self.stopButton.hidden = !mayStop; + self.valueLabel.hidden = mayStop; } -- (void)mayStopDidChange { - self.enabled = self.mayStop; - self.stopView.hidden = !self.mayStop; - self.valueLabel.hidden = self.mayStop; -} - -- (void)didTouchDown { - [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ - [self layoutSubviews]; - } completion:nil]; -} - -- (void)didTouchUpInside { - [UIView animateWithDuration:0.2 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ - [self layoutSubviews]; - } completion:nil]; -} - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - UIView *hitView = [super hitTest:point withEvent:event]; - if (hitView == self.stopView) { - // Allow hits inside stop view - return self; - } else if (hitView == self) { - // Ignore hits inside whole circular view - return nil; - } - // Allow all other subviews (external?) - return hitView; +- (BOOL)mayStop { + return !self.stopButton.hidden; } @@ -180,11 +145,7 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - (void)setProgress:(float)progress { NSParameterAssert(progress >= 0 && progress <= 1); - // Stop running animation - if (self.displayLink) { - [self.displayLink removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; - self.displayLink = nil; - } + [self stopAnimation]; _progress = progress; @@ -193,15 +154,15 @@ - (void)setProgress:(float)progress { - (void)updateProgress { [self updatePath]; - [self updateLabel]; + [self updateLabel:self.progress]; } - (void)updatePath { - self.shapeLayer.path = [self layoutPath].CGPath; + self.shapeLayer.strokeEnd = self.progress; } -- (void)updateLabel { - self.valueLabel.text = [self.numberFormatter stringFromNumber:@(self.progress)]; +- (void)updateLabel:(float)progress { + self.valueLabel.text = [self.numberFormatter stringFromNumber:@(progress)]; } - (void)setProgress:(float)progress animated:(BOOL)animated { @@ -210,14 +171,7 @@ - (void)setProgress:(float)progress animated:(BOOL)animated { return; } - if (self.displayLink) { - // Reuse current display link and manipulate animation params - self.startTime = CACurrentMediaTime(); - self.fromProgress = self.progress; - self.toProgress = progress; - } else { - [self animateToProgress:progress]; - } + [self animateToProgress:progress]; } else { self.progress = progress; } @@ -229,40 +183,55 @@ - (void)setAnimationDuration:(CFTimeInterval)animationDuration { } - (void)animateToProgress:(float)progress { - self.fromProgress = self.progress; - self.toProgress = progress; - self.startTime = CACurrentMediaTime(); + [self stopAnimation]; + + // Add shape animation + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; + animation.duration = self.animationDuration; + animation.fromValue = @(self.progress); + animation.toValue = @(progress); + animation.delegate = self; + [self.shapeLayer addAnimation:animation forKey:MRCircularProgressViewProgressAnimationKey]; - [self.displayLink removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; - self.displayLink = [CADisplayLink displayLinkWithTarget:[MRWeakProxy weakProxyWithTarget:self] selector:@selector(animateFrame:)]; - [self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; + // Add timer to update valueLabel + _valueLabelProgressPercentDifference = (progress - self.progress) * 100; + CFTimeInterval timerInterval = self.animationDuration / ABS(_valueLabelProgressPercentDifference); + self.valueLabelUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:timerInterval + target:self + selector:@selector(onValueLabelUpdateTimer:) + userInfo:nil + repeats:YES]; + + + _progress = progress; } -- (void)animateFrame:(CADisplayLink *)displayLink { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - CGFloat d = (displayLink.timestamp - self.startTime) / self.animationDuration; - - if (d >= 1.0) { - // Order is important! Otherwise concurrency will cause errors, because setProgress: will detect an - // animation in progress and try to stop it by itself. - [self.displayLink removeFromRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes]; - self.displayLink = nil; - - dispatch_async(dispatch_get_main_queue(), ^{ - self.progress = self.toProgress; - }); - - return; - } - - _progress = self.fromProgress + d * (self.toProgress - self.fromProgress); - UIBezierPath *path = [self layoutPath]; - - dispatch_async(dispatch_get_main_queue(), ^{ - self.shapeLayer.path = path.CGPath; - [self updateLabel]; - }); - }); +- (void)stopAnimation { + // Stop running animation + [self.layer removeAnimationForKey:MRCircularProgressViewProgressAnimationKey]; + + // Stop timer + [self.valueLabelUpdateTimer invalidate]; + self.valueLabelUpdateTimer = nil; +} + +- (void)onValueLabelUpdateTimer:(NSTimer *)timer { + if (_valueLabelProgressPercentDifference > 0) { + _valueLabelProgressPercentDifference--; + } else { + _valueLabelProgressPercentDifference++; + } + + [self updateLabel:self.progress - (_valueLabelProgressPercentDifference / 100.0f)]; +} + + +#pragma mark - CAAnimationDelegate + +- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { + [self updateProgress]; + [self.valueLabelUpdateTimer invalidate]; + self.valueLabelUpdateTimer = nil; } @end diff --git a/src/Components/MRProgressOverlayView.h b/src/Components/MRProgressOverlayView.h index 6926b7e..35b04a3 100644 --- a/src/Components/MRProgressOverlayView.h +++ b/src/Components/MRProgressOverlayView.h @@ -8,6 +8,13 @@ #import + +@class MRProgressOverlayView; + + +/** (MRProgressOverlayViewStopBlock) */ +typedef void(^MRProgressOverlayViewStopBlock)(MRProgressOverlayView *progressOverlayView); + /** (MRProgressOverlayViewMode) */ typedef NS_ENUM(NSUInteger, MRProgressOverlayViewMode){ /** Progress is shown using a large round activity indicator view. (MRActivityIndicatorView) This is the default. */ @@ -55,6 +62,18 @@ typedef NS_ENUM(NSUInteger, MRProgressOverlayViewMode){ */ + (instancetype)showOverlayAddedTo:(UIView *)view title:(NSString *)title mode:(MRProgressOverlayViewMode)mode animated:(BOOL)animated; +/** + Creates a new overlay, adds it to provided view and shows it. The counterpart to this method is dismissOverlayForView:animated. + + @param view The view that the overlay will be added to + @param title Title label text + @param mode Visualization mode + @param animated Specify YES to animate the transition or NO if you do not want the transition to be animated. + @param stopBlock Block, which will be called when stop button is tapped. + @return A reference to the created overlay. + */ ++ (instancetype)showOverlayAddedTo:(UIView *)view title:(NSString *)title mode:(MRProgressOverlayViewMode)mode animated:(BOOL)animated stopBlock:(MRProgressOverlayViewStopBlock)stopBlock; + /** Finds the top-most overlay subview and hides it. The counterpart to this method is showOverlayAddedTo:animated:. @@ -164,6 +183,14 @@ typedef NS_ENUM(NSUInteger, MRProgressOverlayViewMode){ */ @property (nonatomic, strong) UIView *modeView; +/** + Block, which will be called when stop button is tapped. + + Use this to set a block, which is callend when UIControlEventTouchUpInside is fired on the mode view's stop button, + if available. The receiver will not be hidden or dismissed, automatically. + */ +@property (nonatomic, copy) MRProgressOverlayViewStopBlock stopBlock; + /** Change the tint color of the mode views. diff --git a/src/Components/MRProgressOverlayView.m b/src/Components/MRProgressOverlayView.m index db97b81..5adcee7 100644 --- a/src/Components/MRProgressOverlayView.m +++ b/src/Components/MRProgressOverlayView.m @@ -82,6 +82,16 @@ + (instancetype)showOverlayAddedTo:(UIView *)view title:(NSString *)title mode:( return overlayView; } ++ (instancetype)showOverlayAddedTo:(UIView *)view title:(NSString *)title mode:(MRProgressOverlayViewMode)mode animated:(BOOL)animated stopBlock:(MRProgressOverlayViewStopBlock)stopBlock { + MRProgressOverlayView *overlayView = [self new]; + overlayView.mode = mode; + overlayView.titleLabelText = title; + overlayView.stopBlock = stopBlock; + [view addSubview:overlayView]; + [overlayView show:animated]; + return overlayView; +} + + (BOOL)dismissOverlayForView:(UIView *)view animated:(BOOL)animated { return [self dismissOverlayForView:view animated:animated completion:nil]; } @@ -109,24 +119,24 @@ + (NSUInteger)dismissAllOverlaysForView:(UIView *)view animated:(BOOL)animated c } + (instancetype)overlayForView:(UIView *)view { - NSEnumerator *subviewsEnum = view.subviews.reverseObjectEnumerator; - for (UIView *subview in subviewsEnum) { - if ([subview isKindOfClass:self]) { - return (MRProgressOverlayView *)subview; - } - } - return nil; + NSEnumerator *subviewsEnum = view.subviews.reverseObjectEnumerator; + for (UIView *subview in subviewsEnum) { + if ([subview isKindOfClass:self]) { + return (MRProgressOverlayView *)subview; + } + } + return nil; } + (NSArray *)allOverlaysForView:(UIView *)view { - NSMutableArray *overlays = [NSMutableArray new]; - NSArray *subviews = view.subviews; - for (UIView *view in subviews) { - if ([view isKindOfClass:self]) { - [overlays addObject:view]; - } - } - return overlays; + NSMutableArray *overlays = [NSMutableArray new]; + NSArray *subviews = view.subviews; + for (UIView *view in subviews) { + if ([view isKindOfClass:self]) { + [overlays addObject:view]; + } + } + return overlays; } @@ -282,6 +292,13 @@ - (UIView *)createModeView { UIView *modeView = [self createViewForMode:self.mode]; self.modeView = modeView; modeView.tintColor = self.tintColor; + + if ([modeView conformsToProtocol:@protocol(MRStopableView)] + && [modeView respondsToSelector:@selector(stopButton)]) { + UIButton *stopButton = [((id)modeView) stopButton]; + [stopButton addTarget:self action:@selector(modeViewStopButtonTouchUpInside) forControlEvents:UIControlEventTouchUpInside]; + } + return modeView; } @@ -444,6 +461,33 @@ - (void)hideModeView:(UIView *)modeView { } +#pragma mark - Stop button + +- (void)setStopBlock:(MRProgressOverlayViewStopBlock)stopBlock { + _stopBlock = stopBlock; + + BOOL mayStop = stopBlock != nil; + if ([self.modeView conformsToProtocol:@protocol(MRStopableView)] + && [self.modeView respondsToSelector:@selector(setMayStop:)]) { + [((id)self.modeView) setMayStop:mayStop]; + } else { + #if DEBUG + NSLog(@"** WARNING - %@: %@ is only valid to call when the mode view supports %@ declared in %@!", + NSStringFromClass(self.class), + NSStringFromSelector(_cmd), + NSStringFromSelector(@selector(setMayStop:)), + NSStringFromProtocol(@protocol(MRStopableView))); + #endif + } +} + +- (void)modeViewStopButtonTouchUpInside { + if (self.stopBlock) { + self.stopBlock(self); + } +} + + #pragma mark - Transitions - (void)setSubviewTransform:(CGAffineTransform)transform alpha:(CGFloat)alpha { diff --git a/src/Components/MRStopButton.h b/src/Components/MRStopButton.h new file mode 100644 index 0000000..eecb0cf --- /dev/null +++ b/src/Components/MRStopButton.h @@ -0,0 +1,24 @@ +// +// MRStopButton.h +// MRProgress +// +// Created by Marius Rackwitz on 27.12.13. +// Copyright (c) 2013 Marius Rackwitz. All rights reserved. +// + +#import + + +/** + Stop button used by progress views to stop the related and visualised running task. + */ +@interface MRStopButton : UIButton + +/** + Asks the view to calculate and return the frame to be displayed in its parent. + + @param parentSize size of the parent node in the view hierachy + */ +- (CGRect)frameThatFits:(CGRect)parentSize; + +@end diff --git a/src/Components/MRStopButton.m b/src/Components/MRStopButton.m new file mode 100644 index 0000000..9de28e7 --- /dev/null +++ b/src/Components/MRStopButton.m @@ -0,0 +1,88 @@ +// +// MRStopButton.m +// MRProgress +// +// Created by Marius Rackwitz on 27.12.13. +// Copyright (c) 2013 Marius Rackwitz. All rights reserved. +// + +#import "MRStopButton.h" +#import "MRProgressHelper.h" + + +@interface MRStopButton () + +@property (nonatomic, weak, readwrite) CAShapeLayer *shapeLayer; + +@end + + +@implementation MRStopButton + +- (id)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self commonInit]; + } + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + [self commonInit]; + } + return self; +} + +- (void)commonInit { + CAShapeLayer *shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer:shapeLayer]; + self.shapeLayer= shapeLayer; + + [self addTarget:self action:@selector(didTouchDown) forControlEvents:UIControlEventTouchDown]; + [self addTarget:self action:@selector(didTouchUpInside) forControlEvents:UIControlEventTouchUpInside]; +} + +- (CGRect)frameThatFits:(CGRect)parentBounds { + CGFloat sizeValue = MIN(parentBounds.size.width, parentBounds.size.height); + CGSize viewSize = CGSizeMake(sizeValue, sizeValue); + const CGFloat sizeRatio = 0.35; + return CGRectInset(MRCenterCGSizeInCGRect(viewSize, parentBounds), + sizeValue * sizeRatio, + sizeValue * sizeRatio); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + CGRect frame = self.bounds; + + if (self.tracking && self.touchInside) { + const CGFloat insetSizeRatio = 0.033; + frame = CGRectInset(frame, + frame.size.width * insetSizeRatio, + frame.size.height * insetSizeRatio); + } + + self.shapeLayer.frame = frame; +} + +- (void)tintColorDidChange { + [super tintColorDidChange]; + self.shapeLayer.backgroundColor = self.tintColor.CGColor; +} + +- (void)didTouchDown { + [UIView animateWithDuration:0.1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + [self layoutSubviews]; + } completion:nil]; +} + +- (void)didTouchUpInside { + [UIView animateWithDuration:0.2 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + [self layoutSubviews]; + } completion:nil]; +} + +@end diff --git a/src/Components/MRStopableView.h b/src/Components/MRStopableView.h new file mode 100644 index 0000000..f408f06 --- /dev/null +++ b/src/Components/MRStopableView.h @@ -0,0 +1,33 @@ +// +// MRStopableView.h +// MRProgress +// +// Created by Marius Rackwitz on 11.01.14. +// Copyright (c) 2014 Marius Rackwitz. All rights reserved. +// + +#import + + +/** + This protocol can be implemented by progress views, which supports a stop button, to stop the related and visualised + running task. + */ +@protocol MRStopableView + +/** + A Boolean value that controls whether the receiver shows a stop button. + + If the value of this property is NO (the default), the receiver doesnot show a stop button. If the mayStop property is + YES a stop button will be shown. You can catch fired events like known from UIButton by the property stopButton. + */ +@property (nonatomic, assign) BOOL mayStop; + +/** + A button, which should only be shown if mayStop is equal to YES. + + The button is in the middle of the control. + */ +@property (nonatomic, readonly) UIButton *stopButton; + +@end