From e67d27d96b511c7ba6cb752d359e611d2a8cde24 Mon Sep 17 00:00:00 2001 From: Guilherme Matuella Date: Mon, 18 Feb 2019 19:14:02 -0300 Subject: [PATCH] Refactors Revolutionary approach (v0.2) + complete iOS Demo --- Demo/Extensions/CGFloat+Random.swift | 17 - Demo/Extensions/UIColor.swift | 19 - Demo/Storyboards/Base.lproj/Main.storyboard | 229 ------- .../CircularProgressScene.swift | 29 - .../CircularProgressViewController.swift | 91 --- .../CircularTimer/CircularTimerScene.swift | 29 - .../CircularTimerViewController.swift | 68 -- Revolutionary.xcodeproj/project.pbxproj | 172 +++-- .../{Demo.xcscheme => iOSDemo.xcscheme} | 16 +- Revolutionary/CircularProgress.swift | 413 ------------ Revolutionary/CircularTimer.swift | 67 -- Revolutionary/Extensions/CGFloat.swift | 19 - Revolutionary/Extensions/Typealias.swift | 11 - Revolutionary/Revolutionary.swift | 591 ++++++++++++++++++ Revolutionary/RevolutionaryBuilder.swift | 64 ++ Revolutionary/RevolutionaryView.swift | 82 +++ {Demo => iOSDemo}/AppDelegate.swift | 7 +- .../AppIcon.appiconset/Contents.json | 0 .../Resources/Assets.xcassets/Contents.json | 0 {Demo => iOSDemo}/Resources/Info.plist | 2 + .../Storyboards/Base.lproj/Main.storyboard | 303 +++++++++ .../Storyboards/LaunchScreen.storyboard | 0 iOSDemo/UIExtensions.swift | 77 +++ .../PropertiesViewController.swift | 153 +++++ .../RevolutionaryViewController.swift | 387 ++++++++++++ .../Showcases/ShowcasesViewController.swift | 13 + 26 files changed, 1758 insertions(+), 1101 deletions(-) delete mode 100644 Demo/Extensions/CGFloat+Random.swift delete mode 100644 Demo/Extensions/UIColor.swift delete mode 100644 Demo/Storyboards/Base.lproj/Main.storyboard delete mode 100644 Demo/Views/CircularProgress/CircularProgressScene.swift delete mode 100644 Demo/Views/CircularProgress/CircularProgressViewController.swift delete mode 100644 Demo/Views/CircularTimer/CircularTimerScene.swift delete mode 100644 Demo/Views/CircularTimer/CircularTimerViewController.swift rename Revolutionary.xcodeproj/xcshareddata/xcschemes/{Demo.xcscheme => iOSDemo.xcscheme} (90%) delete mode 100644 Revolutionary/CircularProgress.swift delete mode 100644 Revolutionary/CircularTimer.swift delete mode 100644 Revolutionary/Extensions/CGFloat.swift delete mode 100644 Revolutionary/Extensions/Typealias.swift create mode 100644 Revolutionary/Revolutionary.swift create mode 100644 Revolutionary/RevolutionaryBuilder.swift create mode 100644 Revolutionary/RevolutionaryView.swift rename {Demo => iOSDemo}/AppDelegate.swift (53%) rename {Demo => iOSDemo}/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {Demo => iOSDemo}/Resources/Assets.xcassets/Contents.json (100%) rename {Demo => iOSDemo}/Resources/Info.plist (96%) create mode 100644 iOSDemo/Storyboards/Base.lproj/Main.storyboard rename {Demo => iOSDemo}/Storyboards/LaunchScreen.storyboard (100%) create mode 100644 iOSDemo/UIExtensions.swift create mode 100644 iOSDemo/Views/Revolutionary/PropertiesViewController.swift create mode 100644 iOSDemo/Views/Revolutionary/RevolutionaryViewController.swift create mode 100644 iOSDemo/Views/Showcases/ShowcasesViewController.swift diff --git a/Demo/Extensions/CGFloat+Random.swift b/Demo/Extensions/CGFloat+Random.swift deleted file mode 100644 index d4d73a2..0000000 --- a/Demo/Extensions/CGFloat+Random.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CGFloat.swift -// RevolutionaryExamples -// -// Created by Guilherme Carlos Matuella on 25/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import UIKit - -extension CGFloat { - - /// Random CGFloat between 0 and 1. - static var random: CGFloat { - return CGFloat(Float(arc4random()) / Float(UINT32_MAX)) - } -} diff --git a/Demo/Extensions/UIColor.swift b/Demo/Extensions/UIColor.swift deleted file mode 100644 index 749efda..0000000 --- a/Demo/Extensions/UIColor.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// UIColor.swift -// Revolutionary -// -// Created by Guilherme Carlos Matuella on 24/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import UIKit - -extension UIColor { - - static var random: UIColor { - return UIColor(red: CGFloat.random, - green: CGFloat.random, - blue: CGFloat.random, - alpha: 1) - } -} diff --git a/Demo/Storyboards/Base.lproj/Main.storyboard b/Demo/Storyboards/Base.lproj/Main.storyboard deleted file mode 100644 index df5443c..0000000 --- a/Demo/Storyboards/Base.lproj/Main.storyboard +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Demo/Views/CircularProgress/CircularProgressScene.swift b/Demo/Views/CircularProgress/CircularProgressScene.swift deleted file mode 100644 index d15fab3..0000000 --- a/Demo/Views/CircularProgress/CircularProgressScene.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CircularProgressScene.swift -// RevolutionaryExamples -// -// Created by Guilherme Carlos Matuella on 25/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import SpriteKit - -class CircularProgressScene: SKScene { - - private var progressNode: CircularProgress! - - override func didMove(to view: SKView) { - let smallestSide = size.height > size.width ? size.height : size.width - progressNode = CircularProgress(withRadius: smallestSide / 3, width: smallestSide / 30, color: .random) - progressNode.position = CGPoint(x: frame.midX, y: frame.midY) - - addChild(progressNode) - } - - func animateProgress(withDuration duration: Int, progress: CGFloat) { - progressNode.circleColor = .random - - progressNode.updateProgress(progress, - duration: TimeInterval(duration)) - } -} diff --git a/Demo/Views/CircularProgress/CircularProgressViewController.swift b/Demo/Views/CircularProgress/CircularProgressViewController.swift deleted file mode 100644 index 8fa3c68..0000000 --- a/Demo/Views/CircularProgress/CircularProgressViewController.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// CircularProgressViewController.swift -// RevolutionaryExamples -// -// Created by Guilherme Carlos Matuella on 22/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import SpriteKit - -class CircularProgressViewController: UIViewController { - - private var duration: Int = 0 { didSet { updateDurationLabel() } } - @IBOutlet private weak var durationLabel: UILabel! - @IBOutlet private weak var durationStepper: UIStepper! - - private var progress: CGFloat = 0 { didSet { updateProgress() } } - @IBOutlet private weak var progressLabel: UILabel! - @IBOutlet private weak var progressTextField: UITextField! - - private var skview: SKView! - private var circularProgressScene: CircularProgressScene! - @IBOutlet private weak var skviewWrapper: UIView! - - override func viewDidLoad() { - super.viewDidLoad() - duration = Int(durationStepper.value) - progress = 0.5 - - let dismissSelector = #selector(CircularProgressViewController.dismissKeyboard) - let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, - action: dismissSelector) - view.addGestureRecognizer(tap) - } - - //Need to call on viewDidAppear because the `skviewWrapper` - //does not contains its correct contentSize yet. - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if skview == nil { - skview = SKView(frame: skviewWrapper.frame) - skviewWrapper.addSubview(skview) - - circularProgressScene = CircularProgressScene(size: skview.bounds.size) - skview.presentScene(circularProgressScene) - skview.showsDrawCount = true - skview.showsNodeCount = true - } - } - - @objc private func dismissKeyboard() { - view.endEditing(true) - } - - @IBAction private func durationStepperTapped(_ sender: UIStepper) { - duration = Int(sender.value) - } - - @IBAction private func progressEditingDidEnd(_ sender: UITextField) { - let commaFilteredInput = sender.text!.replacingOccurrences(of: ",", with: ".") - - guard let textAsNumber = Float(commaFilteredInput) else { - updateProgress() - return - } - if textAsNumber > 100 { - progress = 1 - } else if textAsNumber < 0 { - progress = 0 - } else { - progress = CGFloat(textAsNumber / 100) - } - } - - @IBAction private func animateTapped(_ sender: UIButton) { - circularProgressScene.animateProgress(withDuration: duration, - progress: progress) - } - - private func updateDurationLabel() { - durationLabel.text = "Duration: \(duration) sec." - } - - private func updateProgress() { - let printableProgress = String(format: "%.2f", - progress * 100) - progressLabel.text = "Progress: \(printableProgress)%" - - progressTextField.text = printableProgress - } -} diff --git a/Demo/Views/CircularTimer/CircularTimerScene.swift b/Demo/Views/CircularTimer/CircularTimerScene.swift deleted file mode 100644 index ee98d06..0000000 --- a/Demo/Views/CircularTimer/CircularTimerScene.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CircularTimerScene.swift -// RevolutionaryExamples -// -// Created by Guilherme Carlos Matuella on 25/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import SpriteKit - -class CircularTimerScene: SKScene { - - private var timerNode: CircularTimer! - - override func didMove(to view: SKView) { - let smallestSide = size.height > size.width ? size.height : size.width - timerNode = CircularTimer(withRadius: smallestSide / 3, width: smallestSide / 30, color: .random) - timerNode.position = CGPoint(x: frame.midX, y: frame.midY) - - addChild(timerNode) - } - - func animate(withDuration duration: Int, revolutions: Int, clockwise: Bool) { - timerNode.circleColor = .random - timerNode.play(withRevolutionTime: Double(duration), - amountOfRevolutions: revolutions, - clockwise: clockwise) - } -} diff --git a/Demo/Views/CircularTimer/CircularTimerViewController.swift b/Demo/Views/CircularTimer/CircularTimerViewController.swift deleted file mode 100644 index 09e92a2..0000000 --- a/Demo/Views/CircularTimer/CircularTimerViewController.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// CircularTimerViewController.swift -// RevolutionaryExamples -// -// Created by Guilherme Carlos Matuella on 25/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import SpriteKit - -class CircularTimerViewController: UIViewController { - - private var duration: Int = 0 { didSet { updateDurationLabel() } } - @IBOutlet private weak var durationLabel: UILabel! - - private var revolutions: Int = 0 { didSet { updateRevolutionsLabel() } } - @IBOutlet private weak var revolutionsLabel: UILabel! - - private var clockwise: Bool = true - - private var skview: SKView! - private var circularTimerScene: CircularTimerScene! - @IBOutlet private weak var skViewWrapper: UIView! - - //Need to call on viewDidAppear because the `skviewWrapper` - //does not contains its correct contentSize yet. - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if skview == nil { - duration = 6 - revolutions = 3 - - skview = SKView(frame: skViewWrapper.frame) - skViewWrapper.addSubview(skview) - - circularTimerScene = CircularTimerScene(size: skview.bounds.size) - skview.presentScene(circularTimerScene) - skview.showsDrawCount = true - skview.showsNodeCount = true - } - } - - @IBAction private func durationStepperTapped(_ sender: UIStepper) { - duration = Int(sender.value) - } - - @IBAction private func revolutionsStepperTapped(_ sender: UIStepper) { - revolutions = Int(sender.value) - } - - @IBAction private func clockwiseSwitchTapped(_ sender: UISwitch) { - clockwise = sender.isOn - } - - @IBAction private func animateTapped(_ sender: UIButton) { - circularTimerScene.animate(withDuration: duration, - revolutions: revolutions, - clockwise: clockwise) - } - - private func updateDurationLabel() { - durationLabel.text = "Rev. Duration: \(duration) sec." - } - - private func updateRevolutionsLabel() { - revolutionsLabel.text = "Revolutions: \(revolutions)x" - } -} diff --git a/Revolutionary.xcodeproj/project.pbxproj b/Revolutionary.xcodeproj/project.pbxproj index aac30f9..4329499 100644 --- a/Revolutionary.xcodeproj/project.pbxproj +++ b/Revolutionary.xcodeproj/project.pbxproj @@ -7,52 +7,43 @@ objects = { /* Begin PBXBuildFile section */ + 5209FEDC2219BFD000875E33 /* PropertiesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5209FEDB2219BFD000875E33 /* PropertiesViewController.swift */; }; + 524B4E28221582F40048A6C3 /* RevolutionaryBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F7128E2212C67900F781C4 /* RevolutionaryBuilder.swift */; }; + 524B4E2B22158FDF0048A6C3 /* ShowcasesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524B4E2A22158FDF0048A6C3 /* ShowcasesViewController.swift */; }; + 52A0601A221AED4E00A841D7 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A06019221AED4E00A841D7 /* UIExtensions.swift */; }; + 52F7128F2212C67900F781C4 /* RevolutionaryBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F7128E2212C67900F781C4 /* RevolutionaryBuilder.swift */; }; + 52F7473C2217530600778273 /* RevolutionaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F7473B2217530600778273 /* RevolutionaryView.swift */; }; + 52F7473D2217530600778273 /* RevolutionaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F7473B2217530600778273 /* RevolutionaryView.swift */; }; + 52F7473E2217530600778273 /* RevolutionaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F7473B2217530600778273 /* RevolutionaryView.swift */; }; DD41AEB32132504A00776D43 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DD41AEB22132504A00776D43 /* Assets.xcassets */; }; - DD41AEC52132508200776D43 /* CircularTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBC2132508100776D43 /* CircularTimer.swift */; }; - DD41AEC82132508200776D43 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBF2132508100776D43 /* CircularProgress.swift */; }; + DD41AEC82132508200776D43 /* Revolutionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBF2132508100776D43 /* Revolutionary.swift */; }; DD41AEC92132508200776D43 /* SKNode+SKAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC12132508100776D43 /* SKNode+SKAction.swift */; }; - DD41AECA2132508200776D43 /* Typealias.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC22132508100776D43 /* Typealias.swift */; }; - DD41AECB2132508200776D43 /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC32132508100776D43 /* CGFloat.swift */; }; - DD41AEE82132533B00776D43 /* CircularTimerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AED82132533B00776D43 /* CircularTimerViewController.swift */; }; - DD41AEE92132533B00776D43 /* CircularTimerScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AED92132533B00776D43 /* CircularTimerScene.swift */; }; - DD41AEEA2132533B00776D43 /* CircularProgressScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEDB2132533B00776D43 /* CircularProgressScene.swift */; }; - DD41AEEB2132533B00776D43 /* CircularProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEDC2132533B00776D43 /* CircularProgressViewController.swift */; }; + DD41AEEB2132533B00776D43 /* RevolutionaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEDC2132533B00776D43 /* RevolutionaryViewController.swift */; }; DD41AEEC2132533B00776D43 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEDD2132533B00776D43 /* AppDelegate.swift */; }; DD41AEED2132533B00776D43 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD41AEDF2132533B00776D43 /* LaunchScreen.storyboard */; }; DD41AEEE2132533B00776D43 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD41AEE02132533B00776D43 /* Main.storyboard */; }; - DD41AEEF2132533B00776D43 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEE32132533B00776D43 /* UIColor.swift */; }; - DD41AEF02132533B00776D43 /* CGFloat+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEE42132533B00776D43 /* CGFloat+Random.swift */; }; DD41AEF1213253A400776D43 /* SKNode+SKAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC12132508100776D43 /* SKNode+SKAction.swift */; }; - DD41AEF2213253A400776D43 /* Typealias.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC22132508100776D43 /* Typealias.swift */; }; - DD41AEF3213253A400776D43 /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC32132508100776D43 /* CGFloat.swift */; }; - DD41AEF4213253A400776D43 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBF2132508100776D43 /* CircularProgress.swift */; }; - DD41AEF5213253A400776D43 /* CircularTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBC2132508100776D43 /* CircularTimer.swift */; }; + DD41AEF4213253A400776D43 /* Revolutionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBF2132508100776D43 /* Revolutionary.swift */; }; DDDB6E782133941700A4C79C /* SKNode+SKAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC12132508100776D43 /* SKNode+SKAction.swift */; }; - DDDB6E792133941700A4C79C /* Typealias.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC22132508100776D43 /* Typealias.swift */; }; - DDDB6E7A2133941700A4C79C /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEC32132508100776D43 /* CGFloat.swift */; }; - DDDB6E7B2133941700A4C79C /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBF2132508100776D43 /* CircularProgress.swift */; }; - DDDB6E7C2133941700A4C79C /* CircularTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBC2132508100776D43 /* CircularTimer.swift */; }; + DDDB6E7B2133941700A4C79C /* Revolutionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41AEBF2132508100776D43 /* Revolutionary.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 5209FEDB2219BFD000875E33 /* PropertiesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertiesViewController.swift; sourceTree = ""; }; + 524B4E2A22158FDF0048A6C3 /* ShowcasesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowcasesViewController.swift; sourceTree = ""; }; + 52A06019221AED4E00A841D7 /* UIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = ""; }; + 52F7128E2212C67900F781C4 /* RevolutionaryBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevolutionaryBuilder.swift; sourceTree = ""; }; + 52F7473B2217530600778273 /* RevolutionaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevolutionaryView.swift; sourceTree = ""; }; DD41AE9A2132500800776D43 /* Revolutionary.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Revolutionary.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DD41AEA92132504800776D43 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DD41AEA92132504800776D43 /* iOSDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; DD41AEB22132504A00776D43 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DD41AEB72132504A00776D43 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DD41AEBC2132508100776D43 /* CircularTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularTimer.swift; sourceTree = ""; }; - DD41AEBF2132508100776D43 /* CircularProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = ""; }; + DD41AEBF2132508100776D43 /* Revolutionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Revolutionary.swift; sourceTree = ""; }; DD41AEC12132508100776D43 /* SKNode+SKAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKNode+SKAction.swift"; sourceTree = ""; }; - DD41AEC22132508100776D43 /* Typealias.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Typealias.swift; sourceTree = ""; }; - DD41AEC32132508100776D43 /* CGFloat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGFloat.swift; sourceTree = ""; }; - DD41AED82132533B00776D43 /* CircularTimerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularTimerViewController.swift; sourceTree = ""; }; - DD41AED92132533B00776D43 /* CircularTimerScene.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularTimerScene.swift; sourceTree = ""; }; - DD41AEDB2132533B00776D43 /* CircularProgressScene.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressScene.swift; sourceTree = ""; }; - DD41AEDC2132533B00776D43 /* CircularProgressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressViewController.swift; sourceTree = ""; }; + DD41AEDC2132533B00776D43 /* RevolutionaryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevolutionaryViewController.swift; sourceTree = ""; }; DD41AEDD2132533B00776D43 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DD41AEDF2132533B00776D43 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; DD41AEE12132533B00776D43 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - DD41AEE32132533B00776D43 /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; - DD41AEE42132533B00776D43 /* CGFloat+Random.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat+Random.swift"; sourceTree = ""; }; DDDB6E702133937900A4C79C /* Revolutionary_watchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Revolutionary_watchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -81,11 +72,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 524B4E2922158FC40048A6C3 /* Showcases */ = { + isa = PBXGroup; + children = ( + 524B4E2A22158FDF0048A6C3 /* ShowcasesViewController.swift */, + ); + path = Showcases; + sourceTree = ""; + }; DD41AE902132500800776D43 = { isa = PBXGroup; children = ( DD41AE9C2132500800776D43 /* Revolutionary */, - DD41AEAA2132504800776D43 /* Demo */, + DD41AEAA2132504800776D43 /* iOSDemo */, DD41AE9B2132500800776D43 /* Products */, ); sourceTree = ""; @@ -94,7 +93,7 @@ isa = PBXGroup; children = ( DD41AE9A2132500800776D43 /* Revolutionary.framework */, - DD41AEA92132504800776D43 /* Demo.app */, + DD41AEA92132504800776D43 /* iOSDemo.app */, DDDB6E702133937900A4C79C /* Revolutionary_watchOS.framework */, ); name = Products; @@ -104,30 +103,29 @@ isa = PBXGroup; children = ( DD41AEC02132508100776D43 /* Extensions */, - DD41AEBF2132508100776D43 /* CircularProgress.swift */, - DD41AEBC2132508100776D43 /* CircularTimer.swift */, + DD41AEBF2132508100776D43 /* Revolutionary.swift */, + 52F7128E2212C67900F781C4 /* RevolutionaryBuilder.swift */, + 52F7473B2217530600778273 /* RevolutionaryView.swift */, ); path = Revolutionary; sourceTree = ""; }; - DD41AEAA2132504800776D43 /* Demo */ = { + DD41AEAA2132504800776D43 /* iOSDemo */ = { isa = PBXGroup; children = ( - DD41AEE22132533B00776D43 /* Extensions */, DD41AED32132533B00776D43 /* Resources */, DD41AEDE2132533B00776D43 /* Storyboards */, DD41AED62132533B00776D43 /* Views */, DD41AEDD2132533B00776D43 /* AppDelegate.swift */, + 52A06019221AED4E00A841D7 /* UIExtensions.swift */, ); - path = Demo; + path = iOSDemo; sourceTree = ""; }; DD41AEC02132508100776D43 /* Extensions */ = { isa = PBXGroup; children = ( DD41AEC12132508100776D43 /* SKNode+SKAction.swift */, - DD41AEC22132508100776D43 /* Typealias.swift */, - DD41AEC32132508100776D43 /* CGFloat.swift */, ); path = Extensions; sourceTree = ""; @@ -144,28 +142,19 @@ DD41AED62132533B00776D43 /* Views */ = { isa = PBXGroup; children = ( - DD41AED72132533B00776D43 /* CircularTimer */, - DD41AEDA2132533B00776D43 /* CircularProgress */, + 524B4E2922158FC40048A6C3 /* Showcases */, + DD41AEDA2132533B00776D43 /* Revolutionary */, ); path = Views; sourceTree = ""; }; - DD41AED72132533B00776D43 /* CircularTimer */ = { - isa = PBXGroup; - children = ( - DD41AED92132533B00776D43 /* CircularTimerScene.swift */, - DD41AED82132533B00776D43 /* CircularTimerViewController.swift */, - ); - path = CircularTimer; - sourceTree = ""; - }; - DD41AEDA2132533B00776D43 /* CircularProgress */ = { + DD41AEDA2132533B00776D43 /* Revolutionary */ = { isa = PBXGroup; children = ( - DD41AEDB2132533B00776D43 /* CircularProgressScene.swift */, - DD41AEDC2132533B00776D43 /* CircularProgressViewController.swift */, + DD41AEDC2132533B00776D43 /* RevolutionaryViewController.swift */, + 5209FEDB2219BFD000875E33 /* PropertiesViewController.swift */, ); - path = CircularProgress; + path = Revolutionary; sourceTree = ""; }; DD41AEDE2132533B00776D43 /* Storyboards */ = { @@ -177,15 +166,6 @@ path = Storyboards; sourceTree = ""; }; - DD41AEE22132533B00776D43 /* Extensions */ = { - isa = PBXGroup; - children = ( - DD41AEE42132533B00776D43 /* CGFloat+Random.swift */, - DD41AEE32132533B00776D43 /* UIColor.swift */, - ); - path = Extensions; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -216,9 +196,9 @@ productReference = DD41AE9A2132500800776D43 /* Revolutionary.framework */; productType = "com.apple.product-type.framework"; }; - DD41AEA82132504800776D43 /* Demo */ = { + DD41AEA82132504800776D43 /* iOSDemo */ = { isa = PBXNativeTarget; - buildConfigurationList = DD41AEB82132504A00776D43 /* Build configuration list for PBXNativeTarget "Demo" */; + buildConfigurationList = DD41AEB82132504A00776D43 /* Build configuration list for PBXNativeTarget "iOSDemo" */; buildPhases = ( DD41AEA52132504800776D43 /* Sources */, DD41AEA62132504800776D43 /* Frameworks */, @@ -228,9 +208,9 @@ ); dependencies = ( ); - name = Demo; + name = iOSDemo; productName = Demo; - productReference = DD41AEA92132504800776D43 /* Demo.app */; + productReference = DD41AEA92132504800776D43 /* iOSDemo.app */; productType = "com.apple.product-type.application"; }; DDDB6E6F2133937900A4C79C /* Revolutionary-watchOS */ = { @@ -256,7 +236,7 @@ DD41AE912132500800776D43 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0940; + LastSwiftUpdateCheck = 1010; LastUpgradeCheck = 0940; ORGANIZATIONNAME = gmatuella; TargetAttributes = { @@ -286,7 +266,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - DD41AEA82132504800776D43 /* Demo */, + DD41AEA82132504800776D43 /* iOSDemo */, DD41AE992132500800776D43 /* Revolutionary */, DDDB6E6F2133937900A4C79C /* Revolutionary-watchOS */, ); @@ -318,11 +298,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DD41AECB2132508200776D43 /* CGFloat.swift in Sources */, - DD41AEC82132508200776D43 /* CircularProgress.swift in Sources */, + 52F7473D2217530600778273 /* RevolutionaryView.swift in Sources */, + DD41AEC82132508200776D43 /* Revolutionary.swift in Sources */, DD41AEC92132508200776D43 /* SKNode+SKAction.swift in Sources */, - DD41AEC52132508200776D43 /* CircularTimer.swift in Sources */, - DD41AECA2132508200776D43 /* Typealias.swift in Sources */, + 52F7128F2212C67900F781C4 /* RevolutionaryBuilder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -331,17 +310,14 @@ buildActionMask = 2147483647; files = ( DD41AEF1213253A400776D43 /* SKNode+SKAction.swift in Sources */, - DD41AEF2213253A400776D43 /* Typealias.swift in Sources */, - DD41AEF3213253A400776D43 /* CGFloat.swift in Sources */, - DD41AEF4213253A400776D43 /* CircularProgress.swift in Sources */, - DD41AEF5213253A400776D43 /* CircularTimer.swift in Sources */, + 524B4E2B22158FDF0048A6C3 /* ShowcasesViewController.swift in Sources */, + DD41AEF4213253A400776D43 /* Revolutionary.swift in Sources */, + 524B4E28221582F40048A6C3 /* RevolutionaryBuilder.swift in Sources */, DD41AEEC2132533B00776D43 /* AppDelegate.swift in Sources */, - DD41AEEF2132533B00776D43 /* UIColor.swift in Sources */, - DD41AEEB2132533B00776D43 /* CircularProgressViewController.swift in Sources */, - DD41AEF02132533B00776D43 /* CGFloat+Random.swift in Sources */, - DD41AEE92132533B00776D43 /* CircularTimerScene.swift in Sources */, - DD41AEEA2132533B00776D43 /* CircularProgressScene.swift in Sources */, - DD41AEE82132533B00776D43 /* CircularTimerViewController.swift in Sources */, + 5209FEDC2219BFD000875E33 /* PropertiesViewController.swift in Sources */, + 52F7473C2217530600778273 /* RevolutionaryView.swift in Sources */, + DD41AEEB2132533B00776D43 /* RevolutionaryViewController.swift in Sources */, + 52A0601A221AED4E00A841D7 /* UIExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -350,10 +326,8 @@ buildActionMask = 2147483647; files = ( DDDB6E782133941700A4C79C /* SKNode+SKAction.swift in Sources */, - DDDB6E792133941700A4C79C /* Typealias.swift in Sources */, - DDDB6E7A2133941700A4C79C /* CGFloat.swift in Sources */, - DDDB6E7B2133941700A4C79C /* CircularProgress.swift in Sources */, - DDDB6E7C2133941700A4C79C /* CircularTimer.swift in Sources */, + 52F7473E2217530600778273 /* RevolutionaryView.swift in Sources */, + DDDB6E7B2133941700A4C79C /* Revolutionary.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -496,8 +470,9 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -511,6 +486,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.gmatuella.Revolutionary; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; @@ -523,8 +499,9 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -538,6 +515,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.gmatuella.Revolutionary; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -549,16 +527,18 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = CV54LX9MG6; - INFOPLIST_FILE = "$(SRCROOT)/Demo/Resources/Info.plist"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/iOSDemo/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.gmatuella.Demo; + PRODUCT_BUNDLE_IDENTIFIER = com.gmatuella.iOSDemo; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -570,16 +550,18 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = CV54LX9MG6; - INFOPLIST_FILE = "$(SRCROOT)/Demo/Resources/Info.plist"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/iOSDemo/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.gmatuella.Demo; + PRODUCT_BUNDLE_IDENTIFIER = com.gmatuella.iOSDemo; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -664,7 +646,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DD41AEB82132504A00776D43 /* Build configuration list for PBXNativeTarget "Demo" */ = { + DD41AEB82132504A00776D43 /* Build configuration list for PBXNativeTarget "iOSDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( DD41AEB92132504A00776D43 /* Debug */, diff --git a/Revolutionary.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/Revolutionary.xcodeproj/xcshareddata/xcschemes/iOSDemo.xcscheme similarity index 90% rename from Revolutionary.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme rename to Revolutionary.xcodeproj/xcshareddata/xcschemes/iOSDemo.xcscheme index 9870cb1..6874706 100644 --- a/Revolutionary.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme +++ b/Revolutionary.xcodeproj/xcshareddata/xcschemes/iOSDemo.xcscheme @@ -15,8 +15,8 @@ @@ -33,8 +33,8 @@ @@ -56,8 +56,8 @@ @@ -75,8 +75,8 @@ diff --git a/Revolutionary/CircularProgress.swift b/Revolutionary/CircularProgress.swift deleted file mode 100644 index 459a4c2..0000000 --- a/Revolutionary/CircularProgress.swift +++ /dev/null @@ -1,413 +0,0 @@ -// -// CircularProgress.swift -// Revolutionary -// -// Created by Guilherme Carlos Matuella on 24/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import SpriteKit - -//TODO: Create class description -public class CircularProgress: SKNode { - - /// A Central display style to see the current state of the CircularProgress. - public enum DisplayStyle { - /// Nothing will be displayed - case none - - /** - The display will be formatted relative to the remaining time - and will probably the most simple scenario. - - ## Examples: - - 80 remaining seconds will output 80; - - 3666 remaining seconds will output 3666. - */ - case simpleRemainingTime - - /** - The display will be formatted relative to the remaining time - with a more compacted style. - - ## Examples: - - 80 remaining seconds will output 01:20; - - 3666 remaining seconds will output 01:01:06. - */ - case compactedRemainingTime - - /** - The display will be formatted relative to the remaining time - with a full description. - - ## Examples: - - 80 remaining seconds will output 00:01:15; - - 3666 remaining seconds will output 01:01:06. - */ - case fullRemainingTime - - /** - The current progress percentage. - */ - case percentage - } - - // MARK: UI Properties - - public var isAnimating: Bool { - return action(forKey: animationKey) != nil - } - - public let circleRadius: CGFloat - - public var circleColor: UIColor { - didSet { - displayedCircle?.strokeColor = circleColor - } - } - - public var circleLineWidth: CGFloat { - didSet { - if hasDefaultBackground { background?.lineWidth = circleLineWidth } - displayedCircle?.lineWidth = circleLineWidth - } - } - - public var lineCap: CGLineCap = .round { - didSet { - if hasDefaultBackground { background?.lineCap = lineCap } - displayedCircle?.lineCap = lineCap - } - } - - /// Node that follows the tip of the circular progress. - public var circleTip: SKShapeNode? { - willSet { - guard let currentCircleTip = circleTip else { return } - currentCircleTip.removeFromParent() - } - didSet{ - guard let newCircleTip = circleTip else { return } - addChild(newCircleTip) - - updateCircleTip() - } - } - - /** - Background SKShapeNode of the animation. - The default is a background circle with the same radius and lineWidth of the progress circle. - - If no background is needed, just set this to nil. - */ - public var background: SKShapeNode? { - willSet { - guard let currentBackground = background else { return } - currentBackground.removeFromParent() - } - didSet{ - guard let newBackground = background else { return } - hasDefaultBackground = false - - newBackground.zPosition = -1 - addChild(newBackground) - } - } - - /** - Display style of the current progress state. - - **Defaults to `.none`**. - */ - public var displayStyle: DisplayStyle = .none { - didSet { - if displayStyle != .none && displayLabel == nil { - displayLabel = SKLabelNode() - displayLabel?.verticalAlignmentMode = .center - displayLabel?.horizontalAlignmentMode = .center - } - } - } - - /** - Label that will be used to show the current `displayStyle`. - - **Defaults to nil**. - */ - public var displayLabel: SKLabelNode? { - willSet { - guard let currentTextualFeedback = displayLabel else { return } - currentTextualFeedback.removeFromParent() - } - didSet { - guard let newTextualFeedback = displayLabel else { return } - - addChild(newTextualFeedback) - updateDisplay() - } - } - - // MARK: Auxiliar Properties - - /** - Flag to help check when updates are made in the Circle properties, - so if there's a default background, it can be updated as well. - */ - private var hasDefaultBackground: Bool = true - - private var displayedCircle: SKShapeNode? - private let animationKey = "circularProgressAnimation" - - //TODO: Find the ratio between circleRadius and this value to make the animation seems fluid. - //Also describe the currentProgress + progressInCircles better. - //BTW: NumberOfCircles isn't needed, this could be just some "auxiliar variable" inside the updateProgress function. - private let numberOfCircles = 1000 - - /** - Should be between 0 and 1 - every other value will be truncated to the next "valid" value. - - To update the progress, call `updateProgress(_: duration: completion:)`. - */ - public private(set) var currentProgress: CGFloat = 0 - - private var progressInCircles = 0 - private var remainingDuration: TimeInterval = 0 - /** - - Parameters: - - radius: Radius of the CircularProgress. - - width: Width of the circle line. - - color: Color of the circle line. - */ - required public init(withRadius radius: CGFloat, - width: CGFloat, - color: UIColor) { - self.circleRadius = radius - self.circleLineWidth = width - self.circleColor = color - super.init() - - //Default Background - let background = circleNode(withStartAngle: 0, endAngle: 2 * CGFloat.pi) - background.strokeColor = .lightGray - background.zPosition = -1 - - addChild(background) - - self.background = background - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Public Functions - - /** - Changes the progress based on the current progress. - This means if `newProgress > currentProgress` the revolution orientation will be clockwise, - otherwise will be counterclockwise. - - - Parameters: - - newProgress: New value of the circular progress. Should be between 0 and 1. - - duration: Animation duration. Set to 0 if no animation is necessary. **Defaults to 1**. - */ - public func updateProgress(_ newProgress: CGFloat, duration: TimeInterval = 1, completion: (() -> Void)? = nil) { - let verifiedNewProgress = validateProgress(newProgress) - remainingDuration = duration - - //Find the diff between the newProgress and the currentOne - let progressDiff = (verifiedNewProgress - currentProgress).rounded(toPlaces: 3) - let progressDiffUnits = Int(progressDiff * CGFloat(numberOfCircles)) - if progressDiffUnits == 0 { return } - - //new "amount" of circles displayed - let newCirclesAmount = progressInCircles + progressDiffUnits - - //How much time does every animation will need (as it'll be put inside a sequence) - let animationTimespan = duration / Double(abs(progressDiffUnits)) - - let wait = SKAction.wait(forDuration: animationTimespan) - var fadesActions = [SKAction]() - - //Reversed means that the the progress is going "backwards" - let reversed = progressDiff < 0 - let orientationModifier = reversed ? -1 : 1 - - let initialAngle = CGFloat.pi / 2 - let circleUnit = (2 * CGFloat.pi) / CGFloat(numberOfCircles) - - while progressInCircles != newCirclesAmount { - //Building Current Circle - var showNewCircle: SKAction - if progressInCircles < (numberOfCircles + (reversed ? +1 : -1)) && progressInCircles > 0 { - showNewCircle = SKAction.run { - self.returnShapeToPool(self.displayedCircle!) - } - } else { - showNewCircle = SKAction.run { } - } - - //Building Next Circle - let nextIndex = progressInCircles + orientationModifier - - var hideCurrentCircle: SKAction - if nextIndex < numberOfCircles && nextIndex > 0 { - hideCurrentCircle = SKAction.run { - let nextCircle = self.circleNode(withStartAngle: initialAngle, - endAngle: initialAngle - (CGFloat(nextIndex) * circleUnit)) - nextCircle.strokeColor = self.circleColor - self.displayedCircle = nextCircle - self.addChild(nextCircle) - } - } else { - hideCurrentCircle = SKAction.run { } - } - - let updateProgress = SKAction.run { - let progressStep = CGFloat(orientationModifier) / CGFloat(self.numberOfCircles) - self.update(progress: progressStep, duration: animationTimespan) - } - - //Sequencing iteration respective actions - let animationSwapSequence = SKAction.sequence([showNewCircle, hideCurrentCircle, updateProgress, wait]) - fadesActions.append(animationSwapSequence) - progressInCircles += orientationModifier - } - - run(SKAction.sequence(fadesActions), withKey: animationKey) { - completion?() - } - } - - /** - Resets the progress (visibly and logically). - - When the property `isAnimating == true`, the reset will stop the current ongoing animation. - - Parameters: - - completed: If the desired reset state is completed or not. **Defaults to false**. - */ - public func reset(completed: Bool = false) { - removeAllActions() - displayedCircle?.removeFromParent() - - currentProgress = completed ? 1 : 0 - progressInCircles = completed ? numberOfCircles : 0 - - if completed { - let completedCircle = circleNode(withStartAngle: 0, endAngle: 2 * CGFloat.pi) - completedCircle.strokeColor = circleColor - - addChild(completedCircle) - - displayedCircle = completedCircle - } else { - displayedCircle = nil - } - - updateCircleTip() - updateDisplay() - } - - // MARK: Auxiliar Functions - - private func update(progress: CGFloat, duration: TimeInterval) { - currentProgress += progress - remainingDuration -= duration - updateCircleTip() - updateDisplay() - } - - private func validateProgress(_ progress: CGFloat) -> CGFloat { - switch progress { - case let value where value < 0: return 0 - case let value where value > 1: return 1 - default: return progress.rounded(toPlaces: 3) - } - } - - private func circleNode(withStartAngle startAngle: CGFloat, endAngle: CGFloat) -> SKShapeNode { - let node = pooledShapeNode - let nodePath = UIBezierPath.init(arcCenter: CGPoint.zero, - radius: circleRadius, - startAngle: startAngle, - endAngle: endAngle, - clockwise: false) - node.path = nodePath.cgPath - node.lineWidth = circleLineWidth - node.lineCap = lineCap - - return node - } - - // MARK: SKShapeNode Pooling - - private var shapePool = NSMutableArray() - - private var pooledShapeNode: SKShapeNode { - if shapePool.count > 0 { - let shape = shapePool[0] - shapePool.remove(shape) - - return shape as! SKShapeNode - } - - if shapePool.count > 3 { fatalError("Pool shouldn't be greater than 3: background node and two SKShapeNode being swapped.")} - - return SKShapeNode() - } - - private func returnShapeToPool(_ node: SKShapeNode) { - shapePool.add(node) - - node.path = nil - node.removeFromParent() - } -} - -// MARK: UI Auxiliar Functions - -extension CircularProgress { - //TODO: CircleTip - private func updateCircleTip() { - guard let availableCircleTip = circleTip else { return } - - //Logic to place the circle tip at the end of the last visible circle - } - - private func updateDisplay() { - guard let availableDisplay = displayLabel else { return } - let duration = Int(remainingDuration) + 1 - - switch displayStyle { - case .percentage: - let printableProgress = String(format: "%.2f", currentProgress * 100) - availableDisplay.text = "\(printableProgress)%" - - case .simpleRemainingTime: - availableDisplay.text = "\(duration)" - - case .compactedRemainingTime: - let hours = duration / 3600 - let hoursAvailable = hours > 0 - let minutes = (duration / 60) % 60 - let minutesAvailable = minutes > 0 - let seconds = duration % 60 - - if hoursAvailable { - availableDisplay.text = String(format: "%02i:%02i:%02i", hours, minutes, seconds) - } else if minutesAvailable { - availableDisplay.text = String(format: "%02i:%02i", minutes, seconds) - } else { - availableDisplay.text = String(format: "%02i", seconds) - } - - case .fullRemainingTime: - let hours = duration / 3600 - let minutes = (duration / 60) % 60 - let seconds = duration % 60 - availableDisplay.text = String(format: "%02i:%02i:%02i", hours, minutes, seconds) - - default: break - } - } -} diff --git a/Revolutionary/CircularTimer.swift b/Revolutionary/CircularTimer.swift deleted file mode 100644 index e802707..0000000 --- a/Revolutionary/CircularTimer.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// CircularTimer.swift -// Revolutionary -// -// Created by Guilherme Carlos Matuella on 24/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import SpriteKit - -//TODO: Create class description -public class CircularTimer: CircularProgress { - - //TODO: Create description - public private(set) var shouldRepeatForever: Bool = false - public private(set) var isClockwise: Bool = true - public private(set) var currentRevolution: Int = 0 - public private(set) var totalRevolutions: Int = 0 - - //TODO: Create description - public func play(withRevolutionTime revolutionTime: TimeInterval, - amountOfRevolutions: Int, - clockwise: Bool = true) { - currentRevolution = 0 - totalRevolutions = amountOfRevolutions - isClockwise = clockwise - - playRevolution(withDuration: revolutionTime) - } - - //TODO: Create description - public func playForever(withRevolutionTime revolutionTime: TimeInterval, - clockwise: Bool = true) { - playRevolution(withDuration: revolutionTime) - } - - public func stopTimer() { - shouldRepeatForever = false - reset() - } - - // MARK: Auxiliar Functions - - /** - Recursively calls the `updateProgress(_: duration: completion:)` of `CircularProgress` and manages its completion to play the next revolution. - - The recursion is stopped when `currentRevolution` is equals to `totalRevolutions`. - - `isClockwise` determines if the animation will start "completed" or not. - - If `shouldRepeatForever == true`, the recursion will only be stopped when `stopTimer()` is called. - - Parameters: - - duration: the amount of time spent in each revolution. - */ - private func playRevolution(withDuration duration: TimeInterval) { - reset(completed: !isClockwise) - - guard shouldRepeatForever || currentRevolution != totalRevolutions else { return } - currentRevolution += 1 - - let targetProgress: CGFloat = isClockwise ? 1 : 0 - - updateProgress(targetProgress, duration: duration) { - self.playRevolution(withDuration: duration) - } - } -} diff --git a/Revolutionary/Extensions/CGFloat.swift b/Revolutionary/Extensions/CGFloat.swift deleted file mode 100644 index 8d77ddb..0000000 --- a/Revolutionary/Extensions/CGFloat.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// CGFloat.swift -// Revolutionary -// -// Created by Guilherme Carlos Matuella on 24/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import UIKit - -extension CGFloat { - - /// Rounds the CGFloat to decimal places value - func rounded(toPlaces places: Int) -> CGFloat { - let divisor = pow(10.0, CGFloat(places)) - - return (self * divisor).rounded() / divisor - } -} diff --git a/Revolutionary/Extensions/Typealias.swift b/Revolutionary/Extensions/Typealias.swift deleted file mode 100644 index da4224f..0000000 --- a/Revolutionary/Extensions/Typealias.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// Typealias.swift -// Revolutionary -// -// Created by Guilherme Carlos Matuella on 24/08/18. -// Copyright © 2018 gmatuella. All rights reserved. -// - -import Foundation - -typealias Completion = (() -> Void) diff --git a/Revolutionary/Revolutionary.swift b/Revolutionary/Revolutionary.swift new file mode 100644 index 0000000..897bd01 --- /dev/null +++ b/Revolutionary/Revolutionary.swift @@ -0,0 +1,591 @@ +// +// Revolutionary.swift +// Revolutionary +// +// Created by Guilherme Carlos Matuella on 24/08/18. +// Copyright © 2018 gmatuella. All rights reserved. +// + +import SpriteKit +import os.log + +/// Wrapper dedicated to contain magic numbers or strings +private struct V { + + static let animationKey = "circularProgressAnimation" + + static let fullRevolution: CGFloat = .pi * 2 + + static let minProgress: CGFloat = 0 + static let maxProgress: CGFloat = 1 +} + +/** + The core class of the `Revolutionary` lib - clearly obvious enough. + + Its API is available in 3 different types of "flows" (or "runs", which was the prefix used before each "flow" execution): + + # 1. Animating a progress directly. + Explanation: Exemplifying, say you want to animate a percentage of something being downloaded, so you would call + `run(progress:, duration:, completion:)` and just pass the desired progress of your download, also informing the duration in which it should conclude + the animation). + + # 2. Animating a countdown or stopwatch with a specific amount of revolutions. + Explanation: You want to create a scenario where the arcs need to behave like a countdown/stopwatch, but its future is predetermined, so you would pass a + amount of revolutions and how much time each revolution should take. To create these scenarios, call both + `runCountdown(revolutionDuration:, amountOfRevolutions:, completion:)` and `runStopwatch(revolutionDuration:, amountOfRevolutions:, completion:)` + + # 3. Animating a countdown or stopwatch indefinitely. + Explanation: Cases where no specific amount of revolutions is needed, this behavior provides no completion, because it's intended to not stop until requested. + Just call both `runCountdownIndefinitely(revolutionDuration:)` and `runStopwatchIndefinitely(revolutionDuration:)`, and to stop it, call `stopRunGracefully()` + (which will stop on the completion of the next revolution) or `reset(completed:)`. + + It's important to know that what the countdown means here is just starting from 100% and finishing its revolution in 0%. The stopwatch is the contrary, starts + in 0% and goes to 100%. Both directions can be manipulated by `clockwise`. + + --- + # IMPORTANT + + These `run`/"flows" has UI properties exposed to be customized, and these properties will probably cover most of the scenarios. + If your scenario is not included, this is just a `SKNode` after all, so manipulate the positions, sizes, child nodes at will. + + It's important to specify one UI property in question, because it's the only complex one, and that is `displayLabel` which uses a `displayStyle` to manipulate + the text inside the `SKLabel` that is going to appear in the center of both `mainArc` and `backgroundArc`. + See `Revolutionary.DisplayStyle` to know more about the possibilities. + */ +public class Revolutionary: SKNode { + + /** + The content that is going to appear mid-animation, in the center of the `Revolutionary` arcs. + + If needed, the `displayLabel` - which the `displayStyle` modifies the text - is public and exposed to any UI modification. + + - FIXME: Both `elapsedTime` and `remainingTime` cases use a `DateComponentsFormatter` which has its API broken and mess it up when using `.nanosecond` in + its `allowedUnits` property. Also, a mix of `collapsesLargestUnit` and `allowsFractionalUnits` also does not work properly - really messy stuff! + Find a workaround or try to solve this API directly. + */ + public enum DisplayStyle: Equatable { + + /// Nothing will be displayed + case none + + /// The current progress percentage with the granularity based on the `decimalPlaces` + case percentage(decimalPlaces: Int) + + /// The elapsed time given the `DateComponentsFormatter` format + case elapsedTime(formatter: DateComponentsFormatter) + + /// The remaining time given the `DateComponentsFormatter` format + case remainingTime(formatter: DateComponentsFormatter) + + /// A custom formatted string + case custom(text: String) + } + + // MARK: - Main Arc Properties + + private lazy var mainArc: SKShapeNode = { + let mainArc = SKShapeNode() + + mainArc.zRotation = .pi / 2 + mainArc.strokeColor = mainArcColor + mainArc.lineWidth = mainArcWidth + mainArc.lineCap = mainArcLineCap + + return mainArc + }() + + /// Defaults to `white`. + public var mainArcColor: UIColor = .white { + didSet { mainArc.strokeColor = mainArcColor } + } + + /// Defaults to `5`. + public var mainArcWidth: CGFloat = 5 { + didSet { mainArc.lineWidth = mainArcWidth } + } + + /// Defaults to `round`. + public var mainArcLineCap: CGLineCap = .round { + didSet { mainArc.lineCap = mainArcLineCap } + } + + // MARK: - Background Arc Properties + + private lazy var backgroundArc: SKShapeNode = { + let backgroundArc = SKShapeNode() + backgroundArc.path = arcPath(withProgress: V.maxProgress) + + backgroundArc.zRotation = .pi / 2 + backgroundArc.strokeColor = backgroundArcColor + backgroundArc.lineWidth = backgroundArcWidth + backgroundArc.lineCap = backgroundArcLineCap + + return backgroundArc + }() + + /// Defaults to `lightGray`. + public var backgroundArcColor: UIColor = .lightGray { + didSet { backgroundArc.strokeColor = backgroundArcColor } + } + + /// Defaults to `5`. + public var backgroundArcWidth: CGFloat = 5 { + didSet { backgroundArc.lineWidth = backgroundArcWidth } + } + + /// Defaults to `round`. + public var backgroundArcLineCap: CGLineCap = .round { + didSet { backgroundArc.lineCap = backgroundArcLineCap } + } + + // MARK: - Display Properties + + /// Display style of the `displayLabel`. Defaults to `none`. + public var displayStyle: DisplayStyle = .none { + didSet { updateDisplay() } + } + + /// Shows the state of the `Revolutionary` given the current `displayStyle` format. + public var displayLabel: SKLabelNode = { + let displayLabel = SKLabelNode() + + displayLabel.fontName = UIFont.systemFont(ofSize: 0, weight: .semibold).fontName + displayLabel.fontSize = 30 + displayLabel.fontColor = .gray + displayLabel.verticalAlignmentMode = .center + displayLabel.horizontalAlignmentMode = .center + + return displayLabel + }() { + didSet { updateDisplay() } + } + + // MARK: - Other UI Properties + + /// Radius of both main and background arc. Defaults to `5`. + public var arcRadius: CGFloat { + didSet { updateArcs() } + } + + /// Manages the appearance of the background arc. Defaults to `true`. + public var hasBackgroundArc: Bool = true { + didSet { backgroundArc.isHidden = !hasBackgroundArc } + } + + /// Orientation of the progress arc. Defaults to `true`. + public var clockwise: Bool = true { + didSet { updateArcs() } + } + + public var isAnimating: Bool { return action(forKey: V.animationKey) != nil } + + // MARK: - State Management Properties + + /** + The "animation visual accuracy multiplier". + + The `animationMultiplier` will affect directly the number of animations (or `SKAction`) generated in an arbitrary `TimeInterval`. + + The greater this number, less "stutter" will be apparent in the animation. The contrary is likewise. + + Defaults to `1000`, which is a reasonable amount of `SKAction` given the fact that in most cases it does not loses the aspect of "continous" animation + and the keeping the performance at a decent level. + */ + public var animationMultiplier = 1000 + + /** + Should be between 0 and 1 - every other value will be truncated to the next "valid" value. + + To update the progress, call `run(progress: duration: completion:)`. + */ + public private(set) var currentProgress: CGFloat = 0 { + didSet { updateDisplay() } + } + + /// The remaining duration of the current run + private var remainingDuration: TimeInterval = 0 + + /// The elapsed duration of the current run + private var elapsedDuration: TimeInterval = 0 + + /// The remaining revolutions when the run is not endless (defined by a specific revolutions) + private var remainingRevolutions = 0 + + /// Flag that defines if the `Revolutionary` is in a endless state (running endlessly a countdown or a stopwatch) + private var endlessRun = false + + /// Control if an endlessRun should stop on its next cicle completion + private var shouldStopOnNextCicle = false + + // MARK: - Initializers + + /** + - Parameters: + - radius: radius of both arcs (main and background). + - builder: builder with the desired properties that should be overriden. + */ + required public init(withRadius radius: CGFloat, builder: RevolutionaryBuilder) { + self.arcRadius = radius + + if let mainArcColor = builder.mainArcColor { self.mainArcColor = mainArcColor } + if let mainArcWidth = builder.mainArcWidth { self.mainArcWidth = mainArcWidth } + if let mainArcLineCap = builder.mainArcLineCap { self.mainArcLineCap = mainArcLineCap } + + if let hasBackgroundArc = builder.hasBackgroundArc { self.hasBackgroundArc = hasBackgroundArc } + + if let backgroundArcColor = builder.backgroundArcColor { self.backgroundArcColor = backgroundArcColor } + if let backgroundArcWidth = builder.backgroundArcWidth { self.backgroundArcWidth = backgroundArcWidth } + if let backgroundArcLineCap = builder.backgroundArcLineCap { self.backgroundArcLineCap = backgroundArcLineCap } + + if let displayStyle = builder.displayStyle { self.displayStyle = displayStyle } + if let displayLabel = builder.displayLabel { self.displayLabel = displayLabel } + + if let clockwise = builder.clockwise { self.clockwise = clockwise } + if let animationMultiplier = builder.animationMultiplier { self.animationMultiplier = animationMultiplier } + + super.init() + + commonInit(startsCompleted: builder.startsCompleted) + } + + /** + - Parameters: + - radius: radius of both arcs (main and background). + - startsCompleted: initial "position" of the main arc. + */ + required public init(withRadius radius: CGFloat, startsCompleted: Bool = false) { + self.arcRadius = radius + super.init() + + commonInit(startsCompleted: startsCompleted) + } + + private func commonInit(startsCompleted: Bool) { + addChild(backgroundArc) + backgroundArc.isHidden = !hasBackgroundArc + + addChild(mainArc) + currentProgress = startsCompleted ? V.maxProgress : V.minProgress + updateArcs() + + addChild(displayLabel) + updateDisplay() + } + + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // MARK: - Core Behavior + + /** + Updates the current progress of the `Revolutionary`. + + The proposed solution goes like this: + 1. It starts dividing the `duration` the total amount of `animationMultipler`, that will be used to space out the path "animations". + 2. After this, the `progress` sent as a parameter is used to calculate a diff with the `currentProgress`, so the new arc path may be correctly calculated. + 3. Then the `SKAction`s are created to evenly space the path changes in a `SKAction.run` closure, that updates all of the necessary properties. + */ + private func update(progress: CGFloat, withDuration duration: TimeInterval, completion: (() -> Void)? = nil) { + //Variables to manage animations/properties state + let animationTimespan = duration / TimeInterval(animationMultiplier) + + let progressDiff = newProgressDiff(progress) + let parsedAnimationMultipler = CGFloat(animationMultiplier) + let progressChangeUnit = progressDiff / parsedAnimationMultipler + + //Animations + let modifyArcPath = SKAction.run { + self.elapsedDuration += animationTimespan + self.remainingDuration -= animationTimespan + self.currentProgress += progressChangeUnit + self.mainArc.path = self.arcPath(withProgress: self.currentProgress) + } + let waitNextModification = SKAction.wait(forDuration: animationTimespan) + let arcModificationSequence = SKAction.sequence([waitNextModification, modifyArcPath]) + let repeatArcModifications = SKAction.repeat(arcModificationSequence, count: animationMultiplier) + + run(repeatArcModifications, withKey: V.animationKey) { completion?() } + } + + /** + Validates if a sent `progress` is contained within the specified range (0 for minimum and 1 for maximum). + + - Returns: a "normalized" progress. + */ + private func validatedProgress(_ progress: CGFloat) -> CGFloat { + switch progress { + case let value where value < V.minProgress: + if #available(iOS 12.0, *) { + os_log(.fault, "Invalid progress sent to animate - Fixed by normalizing to %@. Requested: %@", V.minProgress, value) + } + return V.minProgress + case let value where value > V.maxProgress: + if #available(iOS 12.0, *) { + os_log(.fault, "Invalid progress sent to animate - Fixed by normalizing to %@. Requested: %@", V.maxProgress, value) + } + return V.maxProgress + default: return progress + } + } + + /** + Calculates the difference between the progress sent as parameter and the `currentProgress`. + + It's important to notice that the difference is not in absolute values, meaning that if the parameter is smaller than the current, it will return a + negative value. + */ + private func newProgressDiff(_ newProgresss: CGFloat) -> CGFloat { + let validatedProgress = self.validatedProgress(newProgresss) + + return validatedProgress - currentProgress + } + + /** + Uses the `progress` (from 0 to 1) to determine an arc that corresponds from 0 to 360 degress. + + - To build the arc radius, the property `arcRadius` is used. + - To determine the arc orientation, the property `clockwise` is used. + */ + private func arcPath(withProgress progress: CGFloat) -> CGPath { + let arcEndAngle = V.fullRevolution * progress + + //The "clockwise" is inverse in SpriteKit coordinates. See more at https://stackoverflow.com/a/36820789/8558606 + let clockwiseNormalizedEndAngle = clockwise ? arcEndAngle : -arcEndAngle + let arcBezierPath = UIBezierPath(arcCenter: .zero, radius: arcRadius, startAngle: 0, endAngle: clockwiseNormalizedEndAngle, clockwise: clockwise) + + return arcBezierPath.cgPath + } +} + +// MARK: - State Management + +public extension Revolutionary { + + /** + Resets the progress (this reset will also stop the current ongoing animation). + + - Parameters: + - completed: If the desired reset state is completed/`true` (progress = 1) or not/`false` (progress = 0). **Defaults to false**. + */ + public func reset(completed: Bool = false) { + removeAllActions() + currentProgress = completed ? V.maxProgress : V.minProgress + mainArc.path = arcPath(withProgress: currentProgress) + } + + /// Resumes animations (sets the `SKNode.isPaused` property to `false`) + public func resume() { isPaused = false } + + /// Pauses all ongoing animations (sets the `SKNode.isPaused` property to `true`) + public func pause() { isPaused = true } + + public func setProgress(_ progress: CGFloat) { + removeAllActions() + currentProgress = validatedProgress(progress) + mainArc.path = arcPath(withProgress: currentProgress) + } + + /// Resets the internal state of possible runs + private func resetState() { + endlessRun = false + remainingRevolutions = 0 + elapsedDuration = 0 + remainingDuration = 0 + } + + private func updateArcs() { + mainArc.path = arcPath(withProgress: currentProgress) + backgroundArc.path = arcPath(withProgress: V.maxProgress) + } +} + +// MARK: - Run Setups + +extension Revolutionary { + + private func setupStopwatchRun() { reset(completed: false) } + private func setupCountdownRun() { reset(completed: true) } + + /// Setups the run when it has a specific number of revolutions to end. + private func setupFiniteRun(withRevolutionDuration revolutionDuration: TimeInterval, revolutions: Int) { + //Reset state to ensure that if there is a running animation, it does not conflict + resetState() + + remainingRevolutions = revolutions + remainingDuration = revolutionDuration * Double(revolutions) + } + + /// Setups the run when it has no predetermined revolutions. + private func setupInfiniteRun(withRevolutionDuration revolutionDuration: TimeInterval) { + //Reset state to ensure that if there is a running animation, it does not conflict + resetState() + + endlessRun = true + shouldStopOnNextCicle = false + } +} + +// MARK: - Finite Behavior + +public extension Revolutionary { + + /** + Runs from the `currentProgress` to the `progress` sent as parameter. + + - Parameters: + - progress: the new progress. Accepted values are from 0 to 1 (representing the arc from 0 to 360 degress) + - duration: the amount of time spent to animate the progress. + - completion: an optional callback when the animation finishes. + */ + public func run(toProgress progress: CGFloat, withDuration duration: TimeInterval, completion: (() -> Void)? = nil) { + setupFiniteRun(withRevolutionDuration: duration, revolutions: 1) + + update(progress: progress, withDuration: duration, completion: completion) + } + + /** + Runs a countdown style animation in a total `amountOfRevolutions` with each lasting a `revolutionDuration`. + + If a stop - in the middle of the animation - is needed, call `reset(completed:)`. + */ + public func runCountdown(withRevolutionDuration revolutionDuration: TimeInterval, amountOfRevolutions: Int, completion: (() -> Void)? = nil) { + setupFiniteRun(withRevolutionDuration: revolutionDuration, revolutions: amountOfRevolutions) + setupCountdownRun() + + run(numberOfRevolutions: amountOfRevolutions, withDuration: revolutionDuration, finishesCompleted: false, completion: completion) + } + + /** + Runs a stopwatch style animation in a total `amountOfRevolutions` with each lasting a `revolutionDuration`. + + If a stop - in the middle of the animation - is needed, call `reset(completed:)`. + */ + public func runStopwatch(withRevolutionDuration revolutionDuration: TimeInterval, amountOfRevolutions: Int, completion: (() -> Void)? = nil) { + setupFiniteRun(withRevolutionDuration: revolutionDuration, revolutions: amountOfRevolutions) + setupStopwatchRun() + + run(numberOfRevolutions: amountOfRevolutions, withDuration: revolutionDuration, finishesCompleted: true, completion: completion) + } + + /** + Recursively calls `update(progress: duration: completion:)` with a total of `numberOfRevolutions` times. Each call lasts the `duration` sent as parameter. + + - Parameters: + - numberOfRevolutions: The total number of revolutions. + - duration: The amount of time that each revolution will take to go from 0% to 100% (or 100% to 0%). + - finishesCompleted: If it should finishes its last call by having the mainArc at 100% or 0%. + - completion: Callback after the specific `duration` * `numberOfRevolutions` are finished. + */ + private func run(numberOfRevolutions: Int, withDuration duration: TimeInterval, finishesCompleted: Bool, completion: (() -> Void)? = nil) { + let targetProgress = finishesCompleted ? V.maxProgress : V.minProgress + + update(progress: targetProgress, withDuration: duration) { + self.remainingRevolutions -= 1 + + if self.remainingRevolutions == 0 { + //If it's the last revolution, it should retain its state, so no need to reset + completion?() + } else { + //The reset should always be opposite to the current progress, otherwise there will be no difference between the current, + //and the one that is attributed in the targetProgress property. + self.reset(completed: !finishesCompleted) + self.run(numberOfRevolutions: numberOfRevolutions, withDuration: duration, finishesCompleted: finishesCompleted, completion: completion) + } + } + } +} + +// MARK: - Endless Behavior + +public extension Revolutionary { + + /// When finishing the next cicle, stops the current indefinite run. + public func stopRunGracefully() { shouldStopOnNextCicle = true } + + /** + Runs a countdown style animation without a predetermined time to stop. + + To stop with a decent animation behavior, call `stopRunGracefully()`. + If an instant reset is needed, call `reset(completed:)`. + */ + public func runCountdownIndefinitely(withRevolutionDuration revolutionDuration: TimeInterval) { + setupInfiniteRun(withRevolutionDuration: revolutionDuration) + setupCountdownRun() + + runIndefinitely(withRevolutionDuration: revolutionDuration, finishesCompleted: false) + } + + /** + Runs a stopwatch style animation without a predetermined time to stop. + + To stop with a decent animation behavior, call `stopRunGracefully()`. + If an instant reset is needed, call `reset(completed:)`. + */ + public func runStopwatchIndefinitely(withRevolutionDuration revolutionDuration: TimeInterval) { + setupInfiniteRun(withRevolutionDuration: revolutionDuration) + setupStopwatchRun() + + runIndefinitely(withRevolutionDuration: revolutionDuration, finishesCompleted: true) + } + + /** + Recursively calls `update(progress: duration: completion:)` infinitely. + Each call lasts the `duration` sent as parameter. + + - Parameters: + - duration: The amount of time that each revolution will take to go from 0% to 100% (or 100% to 0%). + - finishesCompleted: If it should finishes its last call by having the mainArc at 100% or 0%. + */ + private func runIndefinitely(withRevolutionDuration duration: TimeInterval, finishesCompleted: Bool) { + let targetProgress = finishesCompleted ? V.maxProgress : V.minProgress + + update(progress: targetProgress, withDuration: duration) { + //The reset should always be opposite to the current progress, otherwise there will be no difference between the current, + //and the one that is attributed in the targetProgress property. + self.reset(completed: !finishesCompleted) + + if self.shouldStopOnNextCicle { return } + self.runIndefinitely(withRevolutionDuration: duration, finishesCompleted: finishesCompleted) + } + } +} + +// MARK: - Display Functions + +public extension Revolutionary { + + /** + Updates the `displayLabel` using the `displayStyle`. + + If the `displayStyle` is using an invalid `DateComponentsFormatter`, this function will throw a `fatalError(:)`. + */ + private func updateDisplay() { + displayLabel.isHidden = displayStyle == .none + + switch displayStyle { + case .none: break + case .percentage(let decimalPlaces): + displayLabel.text = progressPercentage(withDecimalPlaces: decimalPlaces) + + case .elapsedTime(let formatter): + guard let elapsedTimeDescription = formatter.string(from: elapsedDuration) else { + fatalError("Bad `DateComponentsFormatter` sent to `Revolutionary.DisplayStyle.elapsedTime(formatter:)`") + } + + displayLabel.text = elapsedTimeDescription + + case .remainingTime(let formatter): + guard let remainingTimeDescription = formatter.string(from: remainingDuration) else { + fatalError("Bad `DateComponentsFormatter` sent to `Revolutionary.DisplayStyle.remainingTime(formatter:)`") + } + + displayLabel.text = remainingTimeDescription + case .custom(let text): + displayLabel.text = text + } + } + + private func progressPercentage(withDecimalPlaces decimalPlaces: Int) -> String { + return String(format: "%.\(decimalPlaces)f%%", currentProgress * 100) + } +} diff --git a/Revolutionary/RevolutionaryBuilder.swift b/Revolutionary/RevolutionaryBuilder.swift new file mode 100644 index 0000000..0c7a2fb --- /dev/null +++ b/Revolutionary/RevolutionaryBuilder.swift @@ -0,0 +1,64 @@ +// +// RevolutionaryBuilder.swift +// Revolutionary +// +// Created by Guilherme Carlos Matuella on 12/02/19. +// Copyright © 2019 gmatuella. All rights reserved. +// + +import SpriteKit + +/// Helper to instantiate the properties of a `Revolutionary` with less verbosity. +public class RevolutionaryBuilder { + + /// Initial position of the main arc. Defaults to `false`. + public var startsCompleted: Bool = false + + // MARK: - Shared Properties (Between Revolutionary and RevolutionaryBuilder) + + /// Defaults to `white`. + public var mainArcColor: UIColor? + + /// Defaults to `5`. + public var mainArcWidth: CGFloat? + + /// Defaults to `round`. + public var mainArcLineCap: CGLineCap? + + /// Manages the appearance of the background arc. Defaults to `true`. + public var hasBackgroundArc: Bool? + + /// Defaults to `lightGray`. + public var backgroundArcColor: UIColor? + + /// Defaults to `5`. + public var backgroundArcWidth: CGFloat? + + /// Defaults to `round`. + public var backgroundArcLineCap: CGLineCap? + + /// Display style of the `displayLabel`. Defaults to `none`. + public var displayStyle: Revolutionary.DisplayStyle? + + /// Shows the state of the `Revolutionary` given the current `displayStyle` format. + public var displayLabel: SKLabelNode? + + /// Orientation of the progress arc. Defaults to `true`. + public var clockwise: Bool? + + /** + The "animation visual accuracy multiplier". + + The `animationMultiplier` will affect directly the number of animations (or `SKAction`) generated in an arbitrary `TimeInterval`. + + The greater this number, less "stutter" will be apparent in the animation. The contrary is likewise. + + Defaults to `1000`, which is a reasonable amount of `SKAction` given the fact that in most cases it does not loses the aspect of "continous" animation + and the keeping the performance at a decent level. + */ + public var animationMultiplier: Int? + + public init(_ revolutionaryBuilder: (RevolutionaryBuilder) -> ()) { + revolutionaryBuilder(self) + } +} diff --git a/Revolutionary/RevolutionaryView.swift b/Revolutionary/RevolutionaryView.swift new file mode 100644 index 0000000..62f05f7 --- /dev/null +++ b/Revolutionary/RevolutionaryView.swift @@ -0,0 +1,82 @@ +// +// RevolutionaryView.swift +// Revolutionary +// +// Created by Guilherme Carlos Matuella on 15/02/19. +// Copyright © 2019 gmatuella. All rights reserved. +// + +import SpriteKit + +/** + Wrapper to quickly instantiate a `RevolutionaryScene` with a `Revolutionary` and put it inside UIKit. + + The proposed solution is to directly call the `RevolutionaryView.rev` property, otherwise all functions and comments would need to be replicated. + */ +public class RevolutionaryView: SKView { + + private var revScene: RevolutionaryScene! + + /// Acessor to the Revolutionary SKNode + public var rev: Revolutionary { + return revScene.rev + } + //TODO: ADD DESC + public init(_ builder: RevolutionaryBuilder, frame: CGRect, padding: CGFloat = 16) { + self.revScene = RevolutionaryScene(builder, size: frame.size, padding: padding) + super.init(frame: frame) + commonInit() + } + + //TODO: ADD DESC + public init(frame: CGRect, padding: CGFloat = 16) { + self.revScene = RevolutionaryScene(size: frame.size, padding: padding) + super.init(frame: frame) + commonInit() + } + + private func commonInit() { + allowsTransparency = true + presentScene(revScene) + } + + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } +} + +/** + Wrapper to quickly instantiate a `Revolutionary`. + + The proposed solution is to directly call the `RevolutionaryScene.rev` property, otherwise all functions and comments would need to be replicated. + */ +public class RevolutionaryScene: SKScene { + + /// Acessor to the Revolutionary SKNode + public let rev: Revolutionary + + //TODO: ADD DESC + public init(_ builder: RevolutionaryBuilder, size: CGSize, padding: CGFloat = 16) { + let arcRadius = (size.height / 2) - padding + self.rev = Revolutionary(withRadius: arcRadius, builder: builder) + super.init(size: size) + + commonInit() + } + + //TODO: ADD DESC + public init(size: CGSize, padding: CGFloat = 16) { + let arcRadius = (size.height / 2) - padding + self.rev = Revolutionary(withRadius: arcRadius) + super.init(size: size) + + commonInit() + } + + private func commonInit() { + backgroundColor = .clear + + rev.position = CGPoint(x: frame.midX, y: frame.midY) + addChild(rev) + } + + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } +} diff --git a/Demo/AppDelegate.swift b/iOSDemo/AppDelegate.swift similarity index 53% rename from Demo/AppDelegate.swift rename to iOSDemo/AppDelegate.swift index dd08d68..c501926 100644 --- a/Demo/AppDelegate.swift +++ b/iOSDemo/AppDelegate.swift @@ -1,6 +1,6 @@ // // AppDelegate.swift -// RevolutionaryExamples +// iOSDemo // // Created by Guilherme Carlos Matuella on 22/08/18. // Copyright © 2018 gmatuella. All rights reserved. @@ -13,9 +13,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { - return true - } } - diff --git a/Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOSDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Demo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to iOSDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Demo/Resources/Assets.xcassets/Contents.json b/iOSDemo/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Demo/Resources/Assets.xcassets/Contents.json rename to iOSDemo/Resources/Assets.xcassets/Contents.json diff --git a/Demo/Resources/Info.plist b/iOSDemo/Resources/Info.plist similarity index 96% rename from Demo/Resources/Info.plist rename to iOSDemo/Resources/Info.plist index 16be3b6..a670f14 100644 --- a/Demo/Resources/Info.plist +++ b/iOSDemo/Resources/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + iOSDemo CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/iOSDemo/Storyboards/Base.lproj/Main.storyboard b/iOSDemo/Storyboards/Base.lproj/Main.storyboard new file mode 100644 index 0000000..0f723b1 --- /dev/null +++ b/iOSDemo/Storyboards/Base.lproj/Main.storyboard @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Storyboards/LaunchScreen.storyboard b/iOSDemo/Storyboards/LaunchScreen.storyboard similarity index 100% rename from Demo/Storyboards/LaunchScreen.storyboard rename to iOSDemo/Storyboards/LaunchScreen.storyboard diff --git a/iOSDemo/UIExtensions.swift b/iOSDemo/UIExtensions.swift new file mode 100644 index 0000000..4ec1606 --- /dev/null +++ b/iOSDemo/UIExtensions.swift @@ -0,0 +1,77 @@ +// +// UIExtensions.swift +// iOSDemo +// +// Created by Guilherme Carlos Matuella on 18/02/19. +// Copyright © 2019 gmatuella. All rights reserved. +// + +import UIKit + +extension UIColor { + static var coolPurple: UIColor { + return UIColor(red: 121 / 255, green: 92 / 255, blue: 212 / 255, alpha: 1) + } +} + +extension UIView { + + convenience init(translatingAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints: Bool) { + self.init() + self.translatesAutoresizingMaskIntoConstraints = translatesAutoresizingMaskIntoConstraints + } + + func addSubviews(_ views: [UIView]) { views.forEach { addSubview($0) } } + + func addDismissKeyboard() { + let dismissSelector = #selector(dismissKeyboard) + let dismissTap = UITapGestureRecognizer(target: self, action: dismissSelector) + addGestureRecognizer(dismissTap) + } + + @objc private func dismissKeyboard() { endEditing(true) } +} + +extension UILabel { + + static func instanceWithDefaultProperties(withText text: String? = nil) -> UILabel { + let defaultLabel = UILabel() + defaultLabel.text = text + defaultLabel.textAlignment = .left + defaultLabel.font = .systemFont(ofSize: 18, weight: .semibold) + defaultLabel.textColor = .coolPurple + + defaultLabel.translatesAutoresizingMaskIntoConstraints = false + return defaultLabel + } +} + +extension UIStepper { + + static func instanceWithDefaultProperties() -> UIStepper { + let defaultStepper = UIStepper() + defaultStepper.tintColor = .coolPurple + defaultStepper.stepValue = 1 + defaultStepper.minimumValue = 1 + defaultStepper.value = 3 + + defaultStepper.translatesAutoresizingMaskIntoConstraints = false + return defaultStepper + } +} + +extension UITextField { + + static func instanceWithDefaultProperties() -> UITextField { + + let progressTextField = UITextField() + progressTextField.keyboardType = .decimalPad + progressTextField.textAlignment = .center + progressTextField.borderStyle = .roundedRect + progressTextField.textColor = .coolPurple + progressTextField.font = .systemFont(ofSize: 15, weight: .regular) + + progressTextField.translatesAutoresizingMaskIntoConstraints = false + return progressTextField + } +} diff --git a/iOSDemo/Views/Revolutionary/PropertiesViewController.swift b/iOSDemo/Views/Revolutionary/PropertiesViewController.swift new file mode 100644 index 0000000..2c8a116 --- /dev/null +++ b/iOSDemo/Views/Revolutionary/PropertiesViewController.swift @@ -0,0 +1,153 @@ +// +// PropertiesViewController.swift +// iOSDemo +// +// Created by Guilherme Carlos Matuella on 17/02/19. +// Copyright © 2019 gmatuella. All rights reserved. +// + +import UIKit + +/** + Mocked possible display styles, each with its own respective mocked data. + */ +enum MockedDisplayStyle: CaseIterable { + case none + case percentage + case elapsedTime + case remainingTime + case custom + + var associatedMockedValue: Revolutionary.DisplayStyle { + switch self { + case .none: return .none + case .percentage: + let mockedDecimalPlaces = 2 + + return .percentage(decimalPlaces: mockedDecimalPlaces) + case .elapsedTime: + let mockedDateFormatter = DateComponentsFormatter() + mockedDateFormatter.allowedUnits = [.minute, .second] + + return .elapsedTime(formatter: mockedDateFormatter) + case .remainingTime: + let mockedDateFormatter = DateComponentsFormatter() + mockedDateFormatter.allowedUnits = [.minute, .second] + + return .remainingTime(formatter: mockedDateFormatter) + case .custom: + let mockedCustomText = "Your Text" + + return .custom(text: mockedCustomText) + } + } + + var associatedDescription: String { + switch self { + case .none: return "None" + case .percentage: return "Current Percentage" + case .elapsedTime: return "Elapsed Time" + case .remainingTime: return "Remaining Time" + case .custom: return "Custom" + } + } + + static func index(of displayStyle: Revolutionary.DisplayStyle) -> Int { + switch displayStyle { + case .none: return 0 + case .percentage(_): return 1 + case .elapsedTime(_): return 2 + case .remainingTime(_): return 3 + case .custom(_): return 4 + } + } +} + +protocol PropertiesDelegate: class { + func properties(_ properties: PropertiesViewController, updatedStyle displayStyle: Revolutionary.DisplayStyle) + func properties(_ properties: PropertiesViewController, updatedClockwise clockwise: Bool) + func properties(_ properties: PropertiesViewController, updatedAnimationMultiplier animationMultiplier: Int) +} + +class PropertiesViewController: UIViewController { + + weak var delegate: PropertiesDelegate? + + private var animationMultiplier: Int! { + didSet { + if let animationMultiplierText = animationMultiplierText { + animationMultiplierText.text = "\(animationMultiplier!)" + delegate?.properties(self, updatedAnimationMultiplier: animationMultiplier!) + } + } + } + + @IBOutlet private weak var animationMultiplierText: UITextField! + + private var clockwise: Bool! { + didSet { delegate?.properties(self, updatedClockwise: clockwise) } + } + @IBOutlet private weak var clockwiseSwitch: UISwitch! + + private var displayStyle: Revolutionary.DisplayStyle! { + didSet { delegate?.properties(self, updatedStyle: displayStyle) } + } + @IBOutlet private weak var displayStylePicker: UIPickerView! + + override func viewDidLoad() { + super.viewDidLoad() + + view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + + displayStylePicker.dataSource = self + displayStylePicker.delegate = self + + //Updating the UI with the initial values + clockwiseSwitch.isOn = clockwise + + let selectedDisplayIndex = MockedDisplayStyle.index(of: displayStyle) + displayStylePicker.selectRow(selectedDisplayIndex, inComponent: 0, animated: false) + + animationMultiplierText.text = "\(animationMultiplier!)" + } + + @IBAction func clockwiseTapped(_ sender: UISwitch) { clockwise = sender.isOn } + + @IBAction func animationMultiplierEndedEditing(_ sender: UITextField) { + if let multiplierAsInt = Int(sender.text!), multiplierAsInt > 0 { + animationMultiplier = multiplierAsInt + } else { + //Attribute again because the textField might be dirty with invalid characters + animationMultiplierText.text = "\(animationMultiplier!)" + } + } + + func configureUI(withRevolutionaryState revolutionary: Revolutionary) { + clockwise = revolutionary.clockwise + displayStyle = revolutionary.displayStyle + animationMultiplier = revolutionary.animationMultiplier + } +} + + +extension PropertiesViewController: UIPickerViewDelegate { + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return MockedDisplayStyle.allCases[row].associatedDescription + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + displayStyle = MockedDisplayStyle.allCases[row].associatedMockedValue + } +} + +extension PropertiesViewController: UIPickerViewDataSource { + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return MockedDisplayStyle.allCases.count + } +} diff --git a/iOSDemo/Views/Revolutionary/RevolutionaryViewController.swift b/iOSDemo/Views/Revolutionary/RevolutionaryViewController.swift new file mode 100644 index 0000000..a51f3bd --- /dev/null +++ b/iOSDemo/Views/Revolutionary/RevolutionaryViewController.swift @@ -0,0 +1,387 @@ +// +// RevolutionaryViewController.swift +// iOSDemo +// +// Created by Guilherme Carlos Matuella on 22/08/18. +// Copyright © 2018 gmatuella. All rights reserved. +// + +import SpriteKit + +class RevolutionaryViewController: UIViewController { + + private enum State { + + case progress + case timer + } + + // MARK: - Shared Properties + + /// Property used to managed what's appearing in the VC, given the selected content on the segment control + private var state: State = .progress { + didSet { + switch state { + case .progress: + revolutionary.reset() + progressContentView.isHidden = false + timerContentView.isHidden = true + case .timer: + revolutionary.reset(completed: isCountdown) + progressContentView.isHidden = true + timerContentView.isHidden = false + } + } + } + + @IBOutlet private weak var contentSegmentedControl: UISegmentedControl! + + @IBOutlet private weak var animateButton: UIButton! + + @IBOutlet private weak var revolutionaryViewWrapper: UIView! + private var revolutionary: Revolutionary! + + // MARK: - Progress Content Properties + + private let progressContentView = UIView(translatingAutoresizingMaskIntoConstraints: false) + + private var duration = 3 { + didSet { updateDuration() } + } + private let durationLabel = UILabel.instanceWithDefaultProperties() + private let durationStepper = UIStepper.instanceWithDefaultProperties() + + private var progress: CGFloat = 0 { + didSet { updateProgress() } + } + private let progressLabel = UILabel.instanceWithDefaultProperties() + private let progressTextField = UITextField.instanceWithDefaultProperties() + + // MARK: - Timer Content Properties + + private let timerContentView = UIView(translatingAutoresizingMaskIntoConstraints: false) + + private var revolutionDuration = 3 { + didSet { updateRevolutionDuration() } + } + private let revolutionDurationLabel = UILabel.instanceWithDefaultProperties() + private let revolutionDurationStepper = UIStepper.instanceWithDefaultProperties() + + private var revolutionsAmount = 3 { + didSet { updateRevolutionsAmount() } + } + private let revolutionsAmountLabel = UILabel.instanceWithDefaultProperties() + private let revolutionsAmountStepper = UIStepper.instanceWithDefaultProperties() + + private var endless = false { + didSet { + revolutionsAmountStepper.isEnabled = !endless + + if endless { + revolutionsAmountLabel.alpha = 0.5 + revolutionsAmountStepper.alpha = 0.5 + } else { + revolutionsAmountLabel.alpha = 1 + revolutionsAmountStepper.alpha = 1 + } + } + } + private let endlessLabel = UILabel.instanceWithDefaultProperties(withText: "Endless") + private let endlessSwitch: UISwitch = { + let endlessSwitch = UISwitch() + endlessSwitch.onTintColor = .coolPurple + endlessSwitch.setOn(false, animated: false) + + endlessSwitch.translatesAutoresizingMaskIntoConstraints = false + return endlessSwitch + }() + + private var isCountdown = true { + didSet { revolutionary.reset(completed: isCountdown) } + } + private let timerStyleLabel = UILabel.instanceWithDefaultProperties(withText: "Style") + private let timerStyleSegment: UISegmentedControl = { + let timerStyleSegment = UISegmentedControl(items: ["Countdown", "Stopwatch"]) + timerStyleSegment.tintColor = .coolPurple + timerStyleSegment.selectedSegmentIndex = 0 + + timerStyleSegment.translatesAutoresizingMaskIntoConstraints = false + return timerStyleSegment + }() + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + navigationController?.navigationBar.barStyle = .black + view.addDismissKeyboard() + + duration = Int(durationStepper.value) + + setupRevolutionary() + setupProgressContent() + setupTimerContent() + + timerContentView.isHidden = true + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + let propertiesVC = segue.destination as! PropertiesViewController + propertiesVC.delegate = self + propertiesVC.configureUI(withRevolutionaryState: revolutionary) + } + + @IBAction private func swappedContent(_ sender: UISegmentedControl) { + state = sender.selectedSegmentIndex == 0 ? .progress : .timer + } +} + +// MARK - Revolutionary Content + +extension RevolutionaryViewController { + + private func setupRevolutionary() { + //You can instantiate the `Revolutionary` by using a builder + let revolutionaryBuilder = RevolutionaryBuilder { builder in + //Customize properties here + //I.E.: + builder.mainArcColor = .coolPurple + builder.mainArcWidth = 10 + builder.backgroundArcWidth = 10 + + builder.displayStyle = .percentage(decimalPlaces: 2) + } + + let revolutionaryView = RevolutionaryView(revolutionaryBuilder, frame: revolutionaryViewWrapper.bounds) + + //or by calling a default init with its default properties + //let revolutionaryView = RevolutionaryView(frame: revolutionaryViewWrapper.bounds) + + revolutionaryView.translatesAutoresizingMaskIntoConstraints = false + revolutionaryViewWrapper.addSubview(revolutionaryView) + revolutionaryView.leadingAnchor.constraint(equalTo: revolutionaryViewWrapper.leadingAnchor, constant: 0).isActive = true + revolutionaryView.trailingAnchor.constraint(equalTo: revolutionaryViewWrapper.trailingAnchor, constant: 0).isActive = true + revolutionaryView.topAnchor.constraint(equalTo: revolutionaryViewWrapper.topAnchor, constant: 0).isActive = true + revolutionaryView.bottomAnchor.constraint(equalTo: revolutionaryViewWrapper.bottomAnchor, constant: 0).isActive = true + + //Because Revolutionary is a SKNode, we must stay with its reference to manipulate its state + revolutionary = revolutionaryView.rev + + //If you don't want to create a custom `SKLabel` on the builder, just customize the default one after instantiation. I.e: + revolutionary.displayLabel.fontColor = .coolPurple + } + + @IBAction private func animateTapped(_ sender: UIButton) { + switch state { + case .progress: animateProgressState() + case .timer: animateTimerState() + } + } + + private func animateProgressState() { + revolutionary.run(toProgress: progress, withDuration: Double(duration)) { + print("Completed Progress") + } + } + + private func animateTimerState() { + let parsedRevDuration = Double(revolutionDuration) + + if endless { + if isCountdown { + revolutionary.runCountdownIndefinitely(withRevolutionDuration: parsedRevDuration) + } else { + revolutionary.runStopwatchIndefinitely(withRevolutionDuration: parsedRevDuration) + } + } else { + if isCountdown { + revolutionary.runCountdown(withRevolutionDuration: parsedRevDuration, amountOfRevolutions: revolutionsAmount) { + print("Completed Countdown revolutions") + } + } else { + revolutionary.runStopwatch(withRevolutionDuration: parsedRevDuration, amountOfRevolutions: revolutionsAmount) { + print("Completed Stopwatch revolutions") + } + } + } + } + + @IBAction private func pauseTapped(_ sender: UIButton) { + //Alternativelly, you could set `isPaused` directly. Just for clear use of the API, these redundant funcs were created. + if revolutionary.isPaused { + sender.setTitle("PAUSE", for: .normal) + revolutionary.resume() + } else { + sender.setTitle("RESUME", for: .normal) + revolutionary.pause() + } + } + + @IBAction private func resetTapped(_ sender: UIButton) { + if state == .timer { + revolutionary.reset(completed: isCountdown) + } else { + revolutionary.reset() + } + + progress = 0 + } +} + +extension RevolutionaryViewController: PropertiesDelegate { + + func properties(_ properties: PropertiesViewController, updatedStyle displayStyle: Revolutionary.DisplayStyle) { + revolutionary.displayStyle = displayStyle + } + + func properties(_ properties: PropertiesViewController, updatedClockwise clockwise: Bool) { + revolutionary.clockwise = clockwise + } + + func properties(_ properties: PropertiesViewController, updatedAnimationMultiplier animationMultiplier: Int) { + revolutionary.animationMultiplier = animationMultiplier + } +} + +// MARK - Progress Segment Content + +extension RevolutionaryViewController { + + private func setupProgressContent() { + view.addSubview(progressContentView) + progressContentView.addSubviews([durationLabel, durationStepper, progressLabel, progressTextField]) + + durationLabel.leadingAnchor.constraint(equalTo: progressContentView.leadingAnchor, constant: 0).isActive = true + durationLabel.trailingAnchor.constraint(equalTo: durationStepper.leadingAnchor, constant: 8).isActive = true + durationLabel.centerYAnchor.constraint(equalTo: durationStepper.centerYAnchor).isActive = true + + durationStepper.trailingAnchor.constraint(equalTo: progressContentView.trailingAnchor, constant: 0).isActive = true + durationStepper.topAnchor.constraint(equalTo: progressContentView.topAnchor, constant: 0).isActive = true + + progressLabel.leadingAnchor.constraint(equalTo: progressContentView.leadingAnchor, constant: 0).isActive = true + progressLabel.trailingAnchor.constraint(equalTo: progressTextField.leadingAnchor, constant: 8).isActive = true + progressLabel.centerYAnchor.constraint(equalTo: progressTextField.centerYAnchor).isActive = true + + progressTextField.leadingAnchor.constraint(equalTo: durationStepper.leadingAnchor, constant: 0).isActive = true + progressTextField.trailingAnchor.constraint(equalTo: progressContentView.trailingAnchor, constant: 0).isActive = true + progressTextField.topAnchor.constraint(equalTo: durationStepper.bottomAnchor, constant: 8).isActive = true + progressTextField.bottomAnchor.constraint(equalTo: progressContentView.bottomAnchor, constant: 0).isActive = true + + progressContentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + progressContentView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true + progressContentView.topAnchor.constraint(equalTo: contentSegmentedControl.bottomAnchor, constant: 16).isActive = true + + updateDuration() + updateProgress() + + durationStepper.addTarget(self, action: #selector(RevolutionaryViewController.durationStepperTapped), for: .primaryActionTriggered) + progressTextField.addTarget(self, action: #selector(RevolutionaryViewController.progressEditingDidEnd), for: .editingDidEnd) + } + + @objc private func durationStepperTapped() { + duration = Int(durationStepper.value) + } + + private func updateDuration() { + durationLabel.text = "Duration: \(duration) sec." + } + + @objc private func progressEditingDidEnd() { + let commaFilteredInput = progressTextField.text!.replacingOccurrences(of: ",", with: ".") + + guard let textAsNumber = Float(commaFilteredInput) else { + updateProgress() + return + } + + switch textAsNumber { + case let progressValue where progressValue > 100: progress = 1 + case let progressValue where progressValue < 0: progress = 0 + default: progress = CGFloat(textAsNumber / 100) + } + } + + private func updateProgress() { + let printableProgress = String(format: "%.2f", progress * 100) + progressLabel.text = "Progress: \(printableProgress)%" + + progressTextField.text = printableProgress + } +} + +// MARK: - Timer Segment Content + +extension RevolutionaryViewController { + + private func setupTimerContent() { + view.addSubview(timerContentView) + timerContentView.addSubviews([revolutionDurationLabel, revolutionDurationStepper, revolutionsAmountLabel, revolutionsAmountStepper, + endlessLabel, endlessSwitch, timerStyleLabel, timerStyleSegment]) + + revolutionDurationLabel.leadingAnchor.constraint(equalTo: timerContentView.leadingAnchor, constant: 0).isActive = true + revolutionDurationLabel.trailingAnchor.constraint(equalTo: revolutionDurationStepper.leadingAnchor, constant: 8).isActive = true + revolutionDurationLabel.centerYAnchor.constraint(equalTo: revolutionDurationStepper.centerYAnchor).isActive = true + + revolutionDurationStepper.trailingAnchor.constraint(equalTo: timerContentView.trailingAnchor, constant: 0).isActive = true + revolutionDurationStepper.topAnchor.constraint(equalTo: timerContentView.topAnchor, constant: 0).isActive = true + + revolutionsAmountLabel.leadingAnchor.constraint(equalTo: timerContentView.leadingAnchor, constant: 0).isActive = true + revolutionsAmountLabel.trailingAnchor.constraint(equalTo: revolutionsAmountStepper.leadingAnchor, constant: 8).isActive = true + revolutionsAmountLabel.centerYAnchor.constraint(equalTo: revolutionsAmountStepper.centerYAnchor).isActive = true + + revolutionsAmountStepper.trailingAnchor.constraint(equalTo: timerContentView.trailingAnchor, constant: 0).isActive = true + revolutionsAmountStepper.topAnchor.constraint(equalTo: revolutionDurationStepper.bottomAnchor, constant: 8).isActive = true + + endlessLabel.leadingAnchor.constraint(equalTo: timerContentView.leadingAnchor, constant: 0).isActive = true + endlessLabel.trailingAnchor.constraint(equalTo: endlessSwitch.leadingAnchor, constant: 8).isActive = true + endlessLabel.centerYAnchor.constraint(equalTo: endlessSwitch.centerYAnchor).isActive = true + + endlessSwitch.trailingAnchor.constraint(equalTo: timerContentView.trailingAnchor, constant: 0).isActive = true + endlessSwitch.topAnchor.constraint(equalTo: revolutionsAmountStepper.bottomAnchor, constant: 8).isActive = true + + timerStyleLabel.leadingAnchor.constraint(equalTo: timerContentView.leadingAnchor, constant: 0).isActive = true + timerStyleLabel.trailingAnchor.constraint(equalTo: timerStyleSegment.leadingAnchor, constant: 8).isActive = true + timerStyleLabel.centerYAnchor.constraint(equalTo: timerStyleSegment.centerYAnchor).isActive = true + + timerStyleSegment.trailingAnchor.constraint(equalTo: timerContentView.trailingAnchor, constant: 0).isActive = true + timerStyleSegment.topAnchor.constraint(equalTo: endlessSwitch.bottomAnchor, constant: 8).isActive = true + timerStyleSegment.bottomAnchor.constraint(equalTo: timerContentView.bottomAnchor, constant: 0).isActive = true + timerStyleSegment.setContentHuggingPriority(.required, for: .horizontal) + + timerContentView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + timerContentView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true + timerContentView.topAnchor.constraint(equalTo: contentSegmentedControl.bottomAnchor, constant: 16).isActive = true + + revolutionDurationStepper.addTarget(self, action: #selector(RevolutionaryViewController.revolutionDurationStepperTapped), for: .primaryActionTriggered) + revolutionsAmountStepper.addTarget(self, action: #selector(RevolutionaryViewController.revolutionsAmountStepperTapped), for: .primaryActionTriggered) + endlessSwitch.addTarget(self, action: #selector(RevolutionaryViewController.endlessSwitchTapped), for: .primaryActionTriggered) + timerStyleSegment.addTarget(self, action: #selector(RevolutionaryViewController.timerSegmentTapped), for: .primaryActionTriggered) + + updateRevolutionDuration() + updateRevolutionsAmount() + } + + @objc private func revolutionDurationStepperTapped() { + revolutionDuration = Int(revolutionDurationStepper.value) + } + + private func updateRevolutionDuration() { + revolutionDurationLabel.text = "Rev. Duration: \(revolutionDuration) sec." + } + + @objc private func revolutionsAmountStepperTapped() { + revolutionsAmount = Int(revolutionsAmountStepper.value) + } + + private func updateRevolutionsAmount() { + revolutionsAmountLabel.text = "Revolutions: \(revolutionsAmount)x" + } + + @objc private func endlessSwitchTapped() { + endless = endlessSwitch.isOn + } + + @objc private func timerSegmentTapped() { + isCountdown = timerStyleSegment.selectedSegmentIndex == 0 + } +} diff --git a/iOSDemo/Views/Showcases/ShowcasesViewController.swift b/iOSDemo/Views/Showcases/ShowcasesViewController.swift new file mode 100644 index 0000000..466e065 --- /dev/null +++ b/iOSDemo/Views/Showcases/ShowcasesViewController.swift @@ -0,0 +1,13 @@ +// +// ShowcasesViewController.swift +// iOSDemo +// +// Created by Guilherme Carlos Matuella on 14/02/19. +// Copyright © 2019 gmatuella. All rights reserved. +// + +import UIKit + +class ShowcasesViewController: UIViewController { + +}