{"id":18545896,"url":"https://github.com/olmps/revolutionary","last_synced_at":"2025-04-09T19:32:17.944Z","repository":{"id":62452900,"uuid":"146148088","full_name":"olmps/Revolutionary","owner":"olmps","description":"Create your circular progress/stopwatch/countdown animations with ease!","archived":false,"fork":false,"pushed_at":"2020-04-24T13:05:51.000Z","size":703,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-06T07:16:12.684Z","etag":null,"topics":["circular-progress","countdown","ios","progress-circle","stopwatch","swift","timer"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/olmps.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-08-26T03:23:56.000Z","updated_at":"2024-03-06T01:59:44.000Z","dependencies_parsed_at":"2022-11-01T23:46:30.028Z","dependency_job_id":null,"html_url":"https://github.com/olmps/Revolutionary","commit_stats":null,"previous_names":["matuella/revolutionary"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olmps%2FRevolutionary","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olmps%2FRevolutionary/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olmps%2FRevolutionary/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olmps%2FRevolutionary/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/olmps","download_url":"https://codeload.github.com/olmps/Revolutionary/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248097942,"owners_count":21047341,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["circular-progress","countdown","ios","progress-circle","stopwatch","swift","timer"],"created_at":"2024-11-06T20:22:46.318Z","updated_at":"2025-04-09T19:32:17.413Z","avatar_url":"https://github.com/olmps.png","language":"Swift","readme":"\u003ch1 align=\"center\"\u003e\n  \u003cbr\u003e\n  Revolutionary\n  \u003cbr\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/matuella/Revolutionary/master/Resources/icon.png\" alt=\"Revolutionary Icon\" width=\"300\"\u003e\n  \u003cbr\u003e\n\u003c/h1\u003e\n\n\u003ch4 align=\"center\"\u003eCreate your circular/progress/timer/stopwatch/countdown animations with ease!\u003c/h4\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://travis-ci.org/matuella/Revolutionary\"\u003e\n    \u003cimg src=\"https://travis-ci.org/matuella/Revolutionary.svg?branch=master)\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"http://cocoadocs.org/docsets/Revolutionary\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/docs-API-lightgrey.svg\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/matuella/Revolutionary/blob/master/LICENSE\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/license/matuella/Revolutionary.svg\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/matuella/Revolutionary/releases\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/release/matuella/Revolutionary.svg\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://docs.swift.org/swift-book/\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/swift%20version-4.2-red.svg\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/Carthage/Carthage\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/carthage-compatible-4BC51D.svg?style=flat\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"http://cocoapods.org/pods/Revolutionary\"\u003e\n    \u003cimg src=\"https://img.shields.io/cocoapods/p/Revolutionary.svg\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://saythanks.io/to/matuella\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg\" alt=\"Say Thanks!\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"http://makeapullrequest.com\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=shields\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e  \n  \u003ca href=\"#features\"\u003eFeatures\u003c/a\u003e |\n  \u003ca href=\"#roadmap\"\u003eRoadmap\u003c/a\u003e |\n  \u003ca href=\"#installing\"\u003eInstalling\u003c/a\u003e |\n  \u003ca href=\"#how-to-use\"\u003eHow To Use\u003c/a\u003e |\n  \u003ca href=\"#contributing\"\u003eContributing\u003c/a\u003e |\n  \u003ca href=\"#changelog\"\u003eChangelog\u003c/a\u003e |\n  \u003ca href=\"#license\"\u003eLicense\u003c/a\u003e\n\u003c/p\u003e\n\n## Description\n\nRevolutionary 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][watchkitImagesTutorial] (and call it on [WKInterfaceImage][appleDocWKII] 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 `SKAction`s.\n\nWith all of this in mind, an **API** was created to manage a `SKNode`, which basically control the UI behavior and do the necessary callbacks.\u003c!-- TODO: You can see the below screenshots and gifs to visually understand if **Revolutionary** fits your use-case. --\u003e\n\n**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.\n\n\u003c!--\n### Screenshots and GIFs\nTODO\n--\u003e\n\n## Features\n\n- [x] iOS support \u003c!--TODO: watchOS, tvOS, macOS --\u003e\n- [x] Fully customizable UI properties of the drawn arcs\n\u003c!-- TODO: - [x] Add Textures to the arcs; --\u003e\n- [x] Manage a Progress behavior\n- [x] Manage a Stopwatch/Countdown behavior\n\n## Roadmap\n\nFeatures **implemented/planned** for 2019 (in order of priority):\n\n- [x] Framework for iOS\n- [x] Examples for iOS\n- [x] Support Carthage\n- [x] Support CocoaPods\n- [x] Add TravisCI\n- [x] Repository description + how to use\n- [ ] Implement Textures on the `SKNode`s\n- [ ] iOS Showcases + Improved iOS Examples\n- [ ] Add Swiftlint\n- [ ] Add Jazzy Docs\n- [ ] Support watchOS\n- [ ] Examples for watchOS\n- [ ] Expose more properties to ease the customization of the `Revolutionary.swift` + improve the iOS examples with it\n- [ ] Find a better way to replicate the commentaries on the Builder (not just copy pasting the docs from `Revolutionary.swift`)\n- [ ] Add Tests (+ support with some check tool, like Coveralls)\n- [ ] Support/Examples/Showcases for watchOS\n- [ ] Support/Examples/Showcases for macOS\n- [ ] Support/Examples/Showcases for tvOS\n\n## Installing\n\n- **Carthage**: add `github \"matuella/Revolutionary\" ~\u003e 0.3.0` to your `Cartfile`;\n- **CocoaPods**: add `pod 'Revolutionary'` to your `Podfile`;\n- **Manual**: copy all the files in the [Revolutionary][revolutionary_folder] folder to your project and you're good to go.\n\n## How to Use\n\n### Instantiating `RevolutionaryView`\n\nBecause this framework is *UI-heavy*, it uses a [Builder pattern][designPatternBuilder] - required in the classes `init` -, so you can explicitly set the desired parameters with a much more clear and concise syntax.\n\nExample of creating a `RevolutionaryBuilder`:\n\n```swift\nlet revolutionaryBuilder = RevolutionaryBuilder { builder in\n    //Customize properties here\n    //I.E.:\n    builder.mainArcColor = .coolPurple\n    builder.mainArcWidth = 10\n    builder.backgroundArcWidth = 10\n\n    builder.displayStyle = .percentage(decimalPlaces: 2)\n}\n```\n\n---\n\n**Using [Interface Builder][appleDocsIB]**:\n\n```swift\n\n`@IBOutlet private weak var myWrapperView: UIView!`\n`private var revolutionary: Revolutionary!`\n\nprivate func viewDidLoad() {\n  super.viewDidLoad()\n  let myBuilder = RevolutionaryBuilder { builder in\n    builder.mainArcColor = .black\n  }\n        \n  let revolutionaryView = RevolutionaryView(revolutionaryBuilder, frame: myWrapperView.bounds)\n\n  //or by calling a default init with its default properties\n  //let revolutionaryView = RevolutionaryView(frame: myWrapperView.bounds)\n\n  //glueing the revolutionary view to my wrapper view\n  revolutionaryView.translatesAutoresizingMaskIntoConstraints = false\n  revolutionaryViewWrapper.addSubview(revolutionaryView)\n  revolutionaryView.leadingAnchor.constraint(equalTo: revolutionaryViewWrapper.leadingAnchor).isActive = true\n  revolutionaryView.trailingAnchor.constraint(equalTo: revolutionaryViewWrapper.trailingAnchor).isActive = true\n  revolutionaryView.topAnchor.constraint(equalTo: revolutionaryViewWrapper.topAnchor).isActive = true\n  revolutionaryView.bottomAnchor.constraint(equalTo: revolutionaryViewWrapper.bottomAnchor).isActive = true\n\n  //Because Revolutionary is a SKNode, we must stay with its reference to manipulate its state\n  revolutionary = revolutionaryView.rev\n\n  //If you don't want to create a custom `SKLabel` on the builder, just customize the default one after instantiation. I.e:\n  revolutionary.displayLabel.fontColor = .purple\n\n  //If you don't want to use the builder, just instante the RevolutionaryView with default values (just passing the frame)\n  //and set the same properties that you would've passed in the RevolutionaryBuilder\n  revolutionary.mainArcColor = .cyan\n}\n```\n\n---\n\n**Using Programmatically-created UI**:\n\nTo exemplify, we will center the `Revolutionary` in the middle of the screen:\n\n```swift\nprivate var revolutionary: Revolutionary!\n\nprivate func viewDidLoad() {\n  super.viewDidLoad()\n  let myBuilder = RevolutionaryBuilder { builder in\n    builder.mainArcColor = .black\n  }\n  \n  let revolutionaryViewFrame = CGRect(x: 0, y: 0, width: 200, height: 200)\n  \n  let revolutionaryView = RevolutionaryView(revolutionaryBuilder, frame: revolutionaryViewFrame)\n\n  //or by calling a default init with its default properties\n  //let revolutionaryView = RevolutionaryView(frame: revolutionaryViewFrame)\n\n  let centeredView = UIView()\n  centeredView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)\n  centeredView.translatesAutoresizingMaskIntoConstraints = false\n  centeredView.addSubview(revView)\n\n  revolutionaryView.topAnchor.constraint(equalTo: centeredView.topAnchor).isActive = true\n  revolutionaryView.bottomAnchor.constraint(equalTo: centeredView.bottomAnchor).isActive = true\n  revolutionaryView.leadingAnchor.constraint(equalTo: centeredView.leadingAnchor).isActive = true\n  revolutionaryView.trailingAnchor.constraint(equalTo: centeredView.trailingAnchor).isActive = true\n\n  view.addSubview(centeredView)\n  centeredView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true\n  centeredView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true\n\n  //Because Revolutionary is a SKNode, we must stay with its reference to manipulate its state\n  revolutionary = revolutionaryView.rev\n\n  //If you don't want to create a custom `SKLabel` on the builder, just customize the default one after instantiation. I.e:\n  revolutionary.displayLabel.fontColor = .purple\n\n  //If you don't want to use the builder, just instante the RevolutionaryView with default values (just passing the frame)\n  //and set the same properties that you would've passed in the RevolutionaryBuilder\n  revolutionary.mainArcColor = .cyan\n}\n```\n\n---\n\n**Alternatively - instantiating the `SpriteKit` classes directly:**\n\nIf you intend to use the `SpriteKit` classes directly (like the `Revolutionary` which is a `SKNode`, or the `RevolutionaryScene` which is a `SKScene`):\n\n`RevolutionaryScene` - `SKScene`:\n```swift\nlet revolutionarySize = CGSize(width: 100, height: 100)\nlet myRevolutionaryScene = RevolutionaryScene(size: revolutionarySize)\n\n//or with builder\n//let myBuilder = RevolutionaryBuilder { builder in\n//  builder.mainArcColor = .black\n//}\n//let myRevolutionaryScene = RevolutionaryScene(myBuilder, size: revolutionarySize)\n\nlet revolutionary = myRevolutionaryScene.rev\n```\n\n`Revolutionary` - `SKNode`:\n```swift\nlet myRevolutionary = Revolutionary(withRadius: 50)\n\n//or with builder\n//let myBuilder = RevolutionaryBuilder { builder in\n//  builder.mainArcColor = .black\n//}\n//let myRevolutionary = Revolutionary(withRadius: 50, builder: myBuilder)\n\nlet revolutionary = myRevolutionaryScene.rev\n```\n\n**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`.\nTo 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\".\n\n\n---\n\n### `Revolutionary` usage\n\nNow that we have a reference to our `Revolutionary` node, we can call the necessary functions, given our use-case.\n\n**Progress usage**:\n\nUsed 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.\n\n```swift\nlet progressAnimationDuration = 3.5\n\n//Animating the new progress - in terms of 0-100% - to 50%.\n//Important to notice that 0%/0 degress means `CGFloat = 0` and 100%/360 degrees means `CGFloat = 1`\nlet newProgress: CGFloat = 0.5\n\nrevolutionary.run(toProgress: newProgress, withDuration: progressAnimationDuration) {\n  print(\"Completed Progress in\")\n}\n```\n\n---\n\n**Countdown usage**:\n\nThere'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.\n\n*Definite countdown*:\n```swift\n//This is in seconds. Meaning half of a second for each revolution in this case\nlet countdownDuration = 0.5\n//Total revolution times\nlet totalRevolutions = 5\nrevolutionary.runCountdown(withRevolutionDuration: countdownDuration, amountOfRevolutions: revolutionsAmount) {\n  print(\"The countdown finished in \\(countdownDuration * Double(totalRevolutions))\")\n}\n```\n\n*Indefinite countdown*:\n```swift\n//This is in seconds. Meaning half of a second for each revolution in this case\nlet countdownDuration = 0.5\nrevolutionary.runCountdownIndefinitely(withRevolutionDuration: countdownDuration)\n```\n\n---\n\n**Stopwatch usage**:\n\nJust like the **Countdown**, the **Stopwatch** use the same indefinite/definite separation.\n\n*Definite stopwatch*:\n```swift\n//This is in seconds. Meaning half of a second for each revolution in this case\nlet stopwatchDuration = 0.5\n//Total revolution times\nlet totalRevolutions = 5\nrevolutionary.runStopwatch(withRevolutionDuration: stopwatchDuration, amountOfRevolutions: revolutionsAmount) {\n  print(\"The stopwatch finished in \\(stopwatchDuration * Double(totalRevolutions))\")\n}\n```\n\n*Indefinite stopwatch*:\n```swift\n//This is in seconds. Meaning half of a second for each revolution in this case\nlet countdownDuration = 0.5\nrevolutionary.runStopwatchIndefinitely(withRevolutionDuration: countdownDuration)\n```\n\n---\n\n**Managing the state**:\n\n*Resetting*: \n```swift\n//Completed in this case, means if it should reset to the full arc (360 degrees) / `true` or no arc (0 degress) / `false`\nrevolutionary.reset(completed: true)\n```\n\n*Pausing*:\n```swift\nrevolutionary.pause()\n```\n*Resuming*:\n```swift\nrevolutionary.resume()\n```\n\n## Contributing\n\nIf you have any suggestion, issue or idea, please contribute with what you've in your mind. Also, read [CONTRIBUTING][contributing].\n\n## Changelog\n\nThe version history and meaningful changes will all be available in the [CHANGELOG][changelog].\n\n## License \n\nRevolutionary is licensed under MIT - [LICENSE][license].\n\n\n\u003c!--- Links ---\u003e\n\n\n[watchkitImagesTutorial]:https://www.natashatherobot.com/watchkit-animate/\n[appleDocWKII]:https://developer.apple.com/documentation/watchkit/wkinterfaceimage\n\n[designPatternBuilder]:https://github.com/ochococo/Design-Patterns-In-Swift#-builder\n[appleDocsIB]:https://developer.apple.com/xcode/interface-builder/\n\n[revolutionary_folder]:https://github.com/matuella/Revolutionary/tree/master/Revolutionary\n[changelog]:https://github.com/matuella/Revolutionary/blob/master/CHANGELOG.md\n[contributing]:https://github.com/matuella/Revolutionary/blob/master/CONTRIBUTING.md\n[license]:https://github.com/matuella/Revolutionary/blob/master/LICENSE\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folmps%2Frevolutionary","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Folmps%2Frevolutionary","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folmps%2Frevolutionary/lists"}