From 897e02d0dd599a3652fbcf95cf0851ca5f47551f Mon Sep 17 00:00:00 2001 From: raza robson Date: Wed, 18 Nov 2015 22:15:01 -0500 Subject: [PATCH 1/3] Added indeterminate progress view --- Example/MaterialKit.xcodeproj/project.pbxproj | 8 + Source/MKProgressLayer.swift | 240 ++++++++++++++++++ Source/MKProgressView.swift | 84 ++++++ 3 files changed, 332 insertions(+) create mode 100644 Source/MKProgressLayer.swift create mode 100644 Source/MKProgressView.swift diff --git a/Example/MaterialKit.xcodeproj/project.pbxproj b/Example/MaterialKit.xcodeproj/project.pbxproj index 60f0228..5b30366 100644 --- a/Example/MaterialKit.xcodeproj/project.pbxproj +++ b/Example/MaterialKit.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 06B58EB51BFD75BA00222E50 /* MKProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B58EB31BFD75BA00222E50 /* MKProgressView.swift */; }; + 06B58EB61BFD75BA00222E50 /* MKProgressLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B58EB41BFD75BA00222E50 /* MKProgressLayer.swift */; }; 8667C0111A299B7200025664 /* MKLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8667C0101A299B7200025664 /* MKLabel.swift */; }; 8667C0131A29A53100025664 /* uibaritem_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 8667C0121A29A53100025664 /* uibaritem_icon.png */; }; 866D96861A17B12500897451 /* icon1.png in Resources */ = {isa = PBXBuildFile; fileRef = 866D96841A17B12500897451 /* icon1.png */; }; @@ -42,6 +44,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 06B58EB31BFD75BA00222E50 /* MKProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MKProgressView.swift; sourceTree = ""; }; + 06B58EB41BFD75BA00222E50 /* MKProgressLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MKProgressLayer.swift; sourceTree = ""; }; 8667C0101A299B7200025664 /* MKLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MKLabel.swift; sourceTree = ""; }; 8667C0121A29A53100025664 /* uibaritem_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = uibaritem_icon.png; sourceTree = ""; }; 866D96841A17B12500897451 /* icon1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon1.png; sourceTree = ""; }; @@ -204,6 +208,8 @@ 86EA09671A29990D008EDEF8 /* MKImageView.swift */, 86FE3A361A1788FF004A0CD5 /* MKLayer.swift */, 8C780DD61A16209200C9DEB9 /* MKColor.swift */, + 06B58EB31BFD75BA00222E50 /* MKProgressView.swift */, + 06B58EB41BFD75BA00222E50 /* MKProgressLayer.swift */, ); name = Source; path = ../../Source; @@ -322,9 +328,11 @@ 86FE3A2F1A1755CF004A0CD5 /* TextFieldViewController.swift in Sources */, 86EA09661A298A6A008EDEF8 /* BarButtonItemViewController.swift in Sources */, 866D96891A17C21D00897451 /* TableViewController.swift in Sources */, + 06B58EB51BFD75BA00222E50 /* MKProgressView.swift in Sources */, 86FE3A351A178863004A0CD5 /* MKTableViewCell.swift in Sources */, 866D968B1A17CE2700897451 /* MyCell.swift in Sources */, 8667C0111A299B7200025664 /* MKLabel.swift in Sources */, + 06B58EB61BFD75BA00222E50 /* MKProgressLayer.swift in Sources */, 8C4D63961A160C95003629F6 /* ButtonViewController.swift in Sources */, 86FE3A311A175604004A0CD5 /* ViewController.swift in Sources */, 8C780DD71A16209200C9DEB9 /* MKColor.swift in Sources */, diff --git a/Source/MKProgressLayer.swift b/Source/MKProgressLayer.swift new file mode 100644 index 0000000..42e3385 --- /dev/null +++ b/Source/MKProgressLayer.swift @@ -0,0 +1,240 @@ +// +// MKProgressLayer.swift +// MaterialKit +// +// Created by Raza Robson on 2015-11-08. +// Copyright © 2015 Le Van Nghia. All rights reserved. +// + +import Foundation +import UIKit + +/// Layer that defines an animatable arc circle +class MKPogressLayer: CALayer { + + /// Defines the different states of + /// the arc circle animation + @objc enum StateId:Int { + + /// id for state when the arc circle's endpoints are the closest to each other. + case Collapsed + + /// id for state when the arc circle's endpoints are getting further from each other. + case Expanding + + /// id for state when the arc circle's endpoints are the furthest from each other. + case Expanded + + /// id for state when the arc circle's endpoints are getting closer each other. + case Collapsing + } + + struct State { + let id:StateId + + /// The duration for which this state's animation will play + let duration:NSTimeInterval + + /// The state to play after this state's animation's finishes + let nextStateId:StateId + + /// Closure that takes a value between 0 and 1 and returns + /// the startAngle and the endAngle that to apply to the arc circle + /// at each frame when this state's animation is played. + let angleValueFunction:(Float) -> (Float, Float) + } + + /// The central angle of this layer's arc circle when the arc's + /// endpoints are the closest to each other, in radians. + private static let minAngle = Float(0.05 * 2 * M_PI) + + /// The central angle of this layer's arc circle when the arc's + /// endpoints are the furthest from each other, in radians. + private static let maxAngle = Float(0.80 * 2 * M_PI ) + + private static let collapsedState = State( + id: .Collapsed, + duration: 0.2, + nextStateId: .Expanding, + angleValueFunction: {(_) in (0, MKPogressLayer.minAngle)}) + + private static let expandingState = State( + id: .Expanding, + duration: 0.5, + nextStateId: .Expanded, + angleValueFunction: { (t) -> (Float, Float) in + return ( + 0, + (MKPogressLayer.maxAngle - MKPogressLayer.minAngle) * t + MKPogressLayer.minAngle + ) + }) + + private static let expandedState = State( + id: .Expanded, + duration: 0.25, + nextStateId: .Collapsing, + angleValueFunction: {(_) in (0, MKPogressLayer.maxAngle)}) + + private static let collapsingState = State( + id: .Collapsing, + duration: 0.5, + nextStateId: .Collapsed, + angleValueFunction: { (t) -> (Float, Float) in + return ( + (MKPogressLayer.maxAngle - MKPogressLayer.minAngle) * t, + MKPogressLayer.maxAngle + ) + }) + + dynamic var angleOffSet: Float = 0 + dynamic var lineColor = UIColor.MKColor.Blue + dynamic var lineWidth: CGFloat = 8 + dynamic var stateId = StateId.Collapsed + dynamic var t:Float = 0 + + private let path = UIBezierPath() + + override init() { + super.init() + configure() + } + + override init(layer: AnyObject) { + angleOffSet = layer.angleOffSet + lineColor = layer.lineColor + lineWidth = layer.lineWidth + stateId = layer.stateId + t = layer.t + super.init(layer: layer) + configure() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + configure() + } + + private func configure() { + setNeedsDisplay() + + // setting the content scale will make + // a smooth circle on high-resolution devices + contentsScale = UIScreen.mainScreen().scale + + #if TARGET_INTERFACE_BUILDER + stateId = .Expanded + #endif + } + + override func actionForKey(event: String) -> CAAction? { + if(event == "stateId" + || event == "angleOffSet" + || event == "lineColor" + || event == "lineWidth" + || event == "t"){ + return implicitAnimationForKeypath(event) + } + return super.actionForKey(event) + } + + override class func needsDisplayForKey(key: String) -> Bool { + if (key == "stateId" + || key == "angleOffSet" + || key == "lineColor" + || key == "lineWidth" + || key == "t"){ + return true + } + return super.needsDisplayForKey(key) + } + + override func drawInContext(ctx: CGContext) { + + path.lineWidth = lineWidth + + let size = frame.size + let diameter = min(size.height, size.width) - path.lineWidth + let center = CGPointMake( + 0.5 * size.width, + 0.5 * size.height) + + let state = MKPogressLayer.stateWithId(stateId) + let (startAngle, endAngle) = state.angleValueFunction(t) + UIGraphicsPushContext(ctx) + path.removeAllPoints() + path.addArcWithCenter( + center, + radius: 0.5 * diameter, + startAngle: CGFloat(angleOffSet + startAngle), + endAngle: CGFloat(angleOffSet + endAngle), + clockwise: true) + lineColor.setStroke() + path.stroke() + UIGraphicsPopContext() + } + + /// Call this function after an animation returned by `nextCircleAnimation()` has been added + /// has been added to the render tree. This will update the model layer values so that + /// its contents match the presentation layer's appearance for the current + /// state at the end of its animation. + func updateState() { + let previousStateId = stateId + let state = MKPogressLayer.stateWithId(stateId) + stateId = state.nextStateId + if(previousStateId == .Collapsing){ + angleOffSet = (angleOffSet + MKPogressLayer.maxAngle - MKPogressLayer.minAngle) % Float(2 * M_PI) + } + } + + private class func stateWithId(stateId:StateId) -> State { + switch(stateId){ + case .Collapsed: + return collapsedState + case .Expanding: + return expandingState + case .Expanded: + return expandedState + case .Collapsing: + return collapsingState + } + } + + private func implicitAnimationForKeypath(property:String) -> CAAnimation { + let animation = CABasicAnimation(keyPath: property) + let presentationLayer = self.presentationLayer() as! MKPogressLayer + animation.fromValue = presentationLayer.valueForKey(property) + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + return animation + } + + /// Returns the next circle animation to play. + /// Call `updateState()` after the returned animation has been added to + /// the render tree. + func nextCircleAnimation() -> CAAnimation { + return circleAnimationForState(MKPogressLayer.stateWithId(stateId)) + } + + private func circleAnimationForState(state:State) -> CAAnimation { + + let pathAnimation = CABasicAnimation(keyPath: "t") + pathAnimation.fromValue = 0 + pathAnimation.toValue = 1 + + let stateAnimation = CABasicAnimation(keyPath: "stateId") + stateAnimation.fromValue = state.id.rawValue + stateAnimation.toValue = state.id.rawValue + + let offSetAnimation = CABasicAnimation(keyPath: "angleOffSet") + offSetAnimation.fromValue = angleOffSet + offSetAnimation.toValue = angleOffSet + + let group = CAAnimationGroup() + + group.animations = [pathAnimation, offSetAnimation, stateAnimation] + group.duration = state.duration + group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + + return group + + } +} \ No newline at end of file diff --git a/Source/MKProgressView.swift b/Source/MKProgressView.swift new file mode 100644 index 0000000..05af8d0 --- /dev/null +++ b/Source/MKProgressView.swift @@ -0,0 +1,84 @@ +// +// MKProgressView.swift +// MaterialKit +// +// Created by Raza Robson on 2015-11-04. +// Copyright © 2015 Le Van Nghia. All rights reserved. +// + +import Foundation +import UIKit +import Darwin + +/// An indeterminate, animated circular progress View +@IBDesignable +class MKProgressView : UIView { + + private let shapeLayer = MKPogressLayer() + + /// Line Color. Animatable. + @IBInspectable var lineColor:UIColor { + get{ + return shapeLayer.lineColor + } set(color) { + shapeLayer.lineColor = color + } + } + + /// Line Width. Animatable. + @IBInspectable var lineWidth:CGFloat { + get{ + return shapeLayer.lineWidth + } set(lineWidth) { + shapeLayer.lineWidth = lineWidth + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + configure() + } + + private func configure() { + layer.addSublayer(shapeLayer) + } + + override func layoutSubviews() { + super.layoutSubviews() + shapeLayer.frame = bounds + } + + override func animationDidStop(anim: CAAnimation, finished: Bool) { + if(finished){ + startNextCircleAnimation() + } + } + + override func didMoveToWindow() { + if(self.window != nil){ + startNextCircleAnimation() + shapeLayer.addAnimation(rotationAnimation(), forKey: "rotationAnimation") + } + } + + private func startNextCircleAnimation() { + let circleAnimation = shapeLayer.nextCircleAnimation() + circleAnimation.delegate = self + shapeLayer.addAnimation(circleAnimation, forKey: "circleAnimation") + shapeLayer.updateState() + } + + private func rotationAnimation() -> CAAnimation { + let animation = CABasicAnimation(keyPath: "transform.rotation") + animation.fromValue = 0 + animation.toValue = 2 * M_PI + animation.duration = 2 + animation.repeatCount = Float.infinity + return animation + } +} \ No newline at end of file From 623a89c7ee175dcfce25f5497b05caf7c2f1d5ac Mon Sep 17 00:00:00 2001 From: raza robson Date: Wed, 18 Nov 2015 22:35:56 -0500 Subject: [PATCH 2/3] added MKProgressView example on the storyboard. --- .../MaterialKit/Base.lproj/Main.storyboard | 41 +++++++++++++++++-- Source/MKProgressView.swift | 2 - 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Example/MaterialKit/Base.lproj/Main.storyboard b/Example/MaterialKit/Base.lproj/Main.storyboard index c705180..f35c405 100644 --- a/Example/MaterialKit/Base.lproj/Main.storyboard +++ b/Example/MaterialKit/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -13,6 +13,7 @@ + @@ -36,6 +37,7 @@ + @@ -126,22 +132,27 @@ + - + + + + @@ -154,6 +165,7 @@ + @@ -180,14 +192,22 @@ + + + + + + + @@ -216,11 +236,13 @@ + + @@ -228,6 +250,7 @@ + @@ -235,24 +258,28 @@ + + + + @@ -284,6 +311,7 @@ + diff --git a/Source/MKProgressView.swift b/Source/MKProgressView.swift index 05af8d0..ba87884 100644 --- a/Source/MKProgressView.swift +++ b/Source/MKProgressView.swift @@ -16,7 +16,6 @@ class MKProgressView : UIView { private let shapeLayer = MKPogressLayer() - /// Line Color. Animatable. @IBInspectable var lineColor:UIColor { get{ return shapeLayer.lineColor @@ -25,7 +24,6 @@ class MKProgressView : UIView { } } - /// Line Width. Animatable. @IBInspectable var lineWidth:CGFloat { get{ return shapeLayer.lineWidth From 1f569c6ba1fa09a0ae51e9aa5d679a2c4b875452 Mon Sep 17 00:00:00 2001 From: raza robson Date: Thu, 19 Nov 2015 13:35:13 -0500 Subject: [PATCH 3/3] just made a few properties private. --- Source/MKProgressLayer.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Source/MKProgressLayer.swift b/Source/MKProgressLayer.swift index 42e3385..0dac068 100644 --- a/Source/MKProgressLayer.swift +++ b/Source/MKProgressLayer.swift @@ -9,7 +9,6 @@ import Foundation import UIKit -/// Layer that defines an animatable arc circle class MKPogressLayer: CALayer { /// Defines the different states of @@ -86,11 +85,11 @@ class MKPogressLayer: CALayer { ) }) - dynamic var angleOffSet: Float = 0 dynamic var lineColor = UIColor.MKColor.Blue dynamic var lineWidth: CGFloat = 8 - dynamic var stateId = StateId.Collapsed - dynamic var t:Float = 0 + private dynamic var angleOffSet: Float = 0 + private dynamic var stateId = StateId.Collapsed + private dynamic var t:Float = 0 private let path = UIBezierPath()