Skip to content

Create your circular progress/stopwatch/countdown animations with ease!

License

Notifications You must be signed in to change notification settings

olmps/Revolutionary

Repository files navigation


Revolutionary
Revolutionary Icon

Create your circular/progress/timer/stopwatch/countdown animations with ease!

Say Thanks!

Features | Roadmap | Installing | How To Use | Contributing | Changelog | License

Description

Revolutionary was built due to a personal need - in essence, the intuit was to create a circle that would behave like a countdown and a stopwatch, but on watchOS. One of the "problems" is that we don't have Core Animation, so we may eventually try using a bunch of images (and call it on WKInterfaceImage in our assets folder), which is completely fine - if the animation is not complex -, but if you want something more detailed (more fluid without a ton of assets) and "controllable", you will probably end ask for help to our beloved SpriteKit and its SKActions.

With all of this in mind, an API was created to manage a SKNode, which basically control the UI behavior and do the necessary callbacks.

Relevant info.: Because the same behavior was needed in both iOS and tvOS, "class helpers" were created to be instantiated directly - a SKView and/or a SKScene - so we can manipulate the Revolutionary SKNode without other SpriteKit UI elements creating "noise" over the instantiation of our main SKNode - the Revolutionary.swift. These helpers make this framework works seamlessly on any platform.

Features

  • iOS support
  • Fully customizable UI properties of the drawn arcs
  • Manage a Progress behavior
  • Manage a Stopwatch/Countdown behavior

Roadmap

Features implemented/planned for 2019 (in order of priority):

  • Framework for iOS
  • Examples for iOS
  • Support Carthage
  • Support CocoaPods
  • Add TravisCI
  • Repository description + how to use
  • Implement Textures on the SKNodes
  • iOS Showcases + Improved iOS Examples
  • Add Swiftlint
  • Add Jazzy Docs
  • Support watchOS
  • Examples for watchOS
  • Expose more properties to ease the customization of the Revolutionary.swift + improve the iOS examples with it
  • Find a better way to replicate the commentaries on the Builder (not just copy pasting the docs from Revolutionary.swift)
  • Add Tests (+ support with some check tool, like Coveralls)
  • Support/Examples/Showcases for watchOS
  • Support/Examples/Showcases for macOS
  • Support/Examples/Showcases for tvOS

Installing

  • Carthage: add github "matuella/Revolutionary" ~> 0.3.0 to your Cartfile;
  • CocoaPods: add pod 'Revolutionary' to your Podfile;
  • Manual: copy all the files in the Revolutionary folder to your project and you're good to go.

How to Use

Instantiating RevolutionaryView

Because this framework is UI-heavy, it uses a Builder pattern - required in the classes init -, so you can explicitly set the desired parameters with a much more clear and concise syntax.

Example of creating a RevolutionaryBuilder:

let revolutionaryBuilder = RevolutionaryBuilder { builder in
    //Customize properties here
    //I.E.:
    builder.mainArcColor = .coolPurple
    builder.mainArcWidth = 10
    builder.backgroundArcWidth = 10

    builder.displayStyle = .percentage(decimalPlaces: 2)
}

Using Interface Builder:

`@IBOutlet private weak var myWrapperView: UIView!`
`private var revolutionary: Revolutionary!`

private func viewDidLoad() {
  super.viewDidLoad()
  let myBuilder = RevolutionaryBuilder { builder in
    builder.mainArcColor = .black
  }
        
  let revolutionaryView = RevolutionaryView(revolutionaryBuilder, frame: myWrapperView.bounds)

  //or by calling a default init with its default properties
  //let revolutionaryView = RevolutionaryView(frame: myWrapperView.bounds)

  //glueing the revolutionary view to my wrapper view
  revolutionaryView.translatesAutoresizingMaskIntoConstraints = false
  revolutionaryViewWrapper.addSubview(revolutionaryView)
  revolutionaryView.leadingAnchor.constraint(equalTo: revolutionaryViewWrapper.leadingAnchor).isActive = true
  revolutionaryView.trailingAnchor.constraint(equalTo: revolutionaryViewWrapper.trailingAnchor).isActive = true
  revolutionaryView.topAnchor.constraint(equalTo: revolutionaryViewWrapper.topAnchor).isActive = true
  revolutionaryView.bottomAnchor.constraint(equalTo: revolutionaryViewWrapper.bottomAnchor).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 = .purple

  //If you don't want to use the builder, just instante the RevolutionaryView with default values (just passing the frame)
  //and set the same properties that you would've passed in the RevolutionaryBuilder
  revolutionary.mainArcColor = .cyan
}

Using Programmatically-created UI:

To exemplify, we will center the Revolutionary in the middle of the screen:

private var revolutionary: Revolutionary!

private func viewDidLoad() {
  super.viewDidLoad()
  let myBuilder = RevolutionaryBuilder { builder in
    builder.mainArcColor = .black
  }
  
  let revolutionaryViewFrame = CGRect(x: 0, y: 0, width: 200, height: 200)
  
  let revolutionaryView = RevolutionaryView(revolutionaryBuilder, frame: revolutionaryViewFrame)

  //or by calling a default init with its default properties
  //let revolutionaryView = RevolutionaryView(frame: revolutionaryViewFrame)

  let centeredView = UIView()
  centeredView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
  centeredView.translatesAutoresizingMaskIntoConstraints = false
  centeredView.addSubview(revView)

  revolutionaryView.topAnchor.constraint(equalTo: centeredView.topAnchor).isActive = true
  revolutionaryView.bottomAnchor.constraint(equalTo: centeredView.bottomAnchor).isActive = true
  revolutionaryView.leadingAnchor.constraint(equalTo: centeredView.leadingAnchor).isActive = true
  revolutionaryView.trailingAnchor.constraint(equalTo: centeredView.trailingAnchor).isActive = true

  view.addSubview(centeredView)
  centeredView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
  centeredView.centerYAnchor.constraint(equalTo: view.centerYAnchor).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 = .purple

  //If you don't want to use the builder, just instante the RevolutionaryView with default values (just passing the frame)
  //and set the same properties that you would've passed in the RevolutionaryBuilder
  revolutionary.mainArcColor = .cyan
}

Alternatively - instantiating the SpriteKit classes directly:

If you intend to use the SpriteKit classes directly (like the Revolutionary which is a SKNode, or the RevolutionaryScene which is a SKScene):

RevolutionaryScene - SKScene:

let revolutionarySize = CGSize(width: 100, height: 100)
let myRevolutionaryScene = RevolutionaryScene(size: revolutionarySize)

//or with builder
//let myBuilder = RevolutionaryBuilder { builder in
//  builder.mainArcColor = .black
//}
//let myRevolutionaryScene = RevolutionaryScene(myBuilder, size: revolutionarySize)

let revolutionary = myRevolutionaryScene.rev

Revolutionary - SKNode:

let myRevolutionary = Revolutionary(withRadius: 50)

//or with builder
//let myBuilder = RevolutionaryBuilder { builder in
//  builder.mainArcColor = .black
//}
//let myRevolutionary = Revolutionary(withRadius: 50, builder: myBuilder)

let revolutionary = myRevolutionaryScene.rev

IMPORTANT: As you can see, the init of both RevolutionaryView and RevolutionaryScene requires a padding: CGFloat, which defaults to 16, but this is basically the padding in which the Revolutionary will draw its circle. This is needed because the UIBezierPath which will draw the arcs may get out of the SKScene. To clarify, lets say you need a circle of radius = 100. If you set the padding = 8, you'll need a frame of 116 of height/width, because the padding will be 8 points in each "side".


Revolutionary usage

Now that we have a reference to our Revolutionary node, we can call the necessary functions, given our use-case.

Progress usage:

Used when you need to manage the arc state, like a download progress, a completion ratio of some arbitrary in-game progress, a progress of a onboarding, etc.

let progressAnimationDuration = 3.5

//Animating the new progress - in terms of 0-100% - to 50%.
//Important to notice that 0%/0 degress means `CGFloat = 0` and 100%/360 degrees means `CGFloat = 1`
let newProgress: CGFloat = 0.5

revolutionary.run(toProgress: newProgress, withDuration: progressAnimationDuration) {
  print("Completed Progress in")
}

Countdown usage:

There's basically two modes when running Countdown: indefinite and definite. This means to pick if you want the animation to keep going until stopped (indefinite) or using predetermined duration/amounts of revolutions.

Definite countdown:

//This is in seconds. Meaning half of a second for each revolution in this case
let countdownDuration = 0.5
//Total revolution times
let totalRevolutions = 5
revolutionary.runCountdown(withRevolutionDuration: countdownDuration, amountOfRevolutions: revolutionsAmount) {
  print("The countdown finished in \(countdownDuration * Double(totalRevolutions))")
}

Indefinite countdown:

//This is in seconds. Meaning half of a second for each revolution in this case
let countdownDuration = 0.5
revolutionary.runCountdownIndefinitely(withRevolutionDuration: countdownDuration)

Stopwatch usage:

Just like the Countdown, the Stopwatch use the same indefinite/definite separation.

Definite stopwatch:

//This is in seconds. Meaning half of a second for each revolution in this case
let stopwatchDuration = 0.5
//Total revolution times
let totalRevolutions = 5
revolutionary.runStopwatch(withRevolutionDuration: stopwatchDuration, amountOfRevolutions: revolutionsAmount) {
  print("The stopwatch finished in \(stopwatchDuration * Double(totalRevolutions))")
}

Indefinite stopwatch:

//This is in seconds. Meaning half of a second for each revolution in this case
let countdownDuration = 0.5
revolutionary.runStopwatchIndefinitely(withRevolutionDuration: countdownDuration)

Managing the state:

Resetting:

//Completed in this case, means if it should reset to the full arc (360 degrees) / `true` or no arc (0 degress) / `false`
revolutionary.reset(completed: true)

Pausing:

revolutionary.pause()

Resuming:

revolutionary.resume()

Contributing

If you have any suggestion, issue or idea, please contribute with what you've in your mind. Also, read CONTRIBUTING.

Changelog

The version history and meaningful changes will all be available in the CHANGELOG.

License

Revolutionary is licensed under MIT - LICENSE.