{"id":13522946,"url":"https://github.com/MacPaw/CocoaSprings","last_synced_at":"2025-04-01T00:30:26.401Z","repository":{"id":152622656,"uuid":"602929427","full_name":"MacPaw/CocoaSprings","owner":"MacPaw","description":"Interactive spring animations for macOS/iOS","archived":false,"fork":false,"pushed_at":"2024-10-20T09:26:27.000Z","size":5807,"stargazers_count":78,"open_issues_count":0,"forks_count":1,"subscribers_count":12,"default_branch":"main","last_synced_at":"2024-11-01T21:45:56.701Z","etag":null,"topics":[],"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/MacPaw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-02-17T08:48:00.000Z","updated_at":"2024-11-01T06:36:16.000Z","dependencies_parsed_at":"2023-07-06T14:16:06.840Z","dependency_job_id":null,"html_url":"https://github.com/MacPaw/CocoaSprings","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacPaw%2FCocoaSprings","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacPaw%2FCocoaSprings/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacPaw%2FCocoaSprings/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MacPaw%2FCocoaSprings/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MacPaw","download_url":"https://codeload.github.com/MacPaw/CocoaSprings/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":222688160,"owners_count":17023297,"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":[],"created_at":"2024-08-01T06:00:53.938Z","updated_at":"2024-11-02T07:30:58.287Z","avatar_url":"https://github.com/MacPaw.png","language":"Swift","funding_links":[],"categories":["Libs","UI and SwiftUI","Swift"],"sub_categories":["Animation"],"readme":"![CocoaSprings](https://github.com/MacPaw/CocoaSprings/blob/main/Screenshots/header.gif)\n\n## Contents\n- [About](#about)\n- [Installation](#installation)\n- [Usage](#usage)\n  - [SpringConfiguration](#springconfiguration)\n  - [SpringMotionLayer](#springmotionlayer)\n  - [SpringMotionView](#springmotionview)\n  - [SpringMotionWindow](#springmotionwindow)\n- [Demo](#demo)\n- [License](#license)\n\n## About\n\nCocoaSprings is a lightweight Swift package that simulates damped spring physics for basic AppKit \u0026 UIKit components. The package provides subclasses of `CALayer`, `UIView`, `NSView`, and `NSWindow`, which can be moved around the UI repeatedly without breaking the continuity of their motion. This allows for the implementation of fluid, interactive animations similar to this one:\n\n![About](https://github.com/MacPaw/CocoaSprings/blob/main/Screenshots/about.gif)\n\nThe math behind the animations is based on this [excellent blog post by Ryan Juckett](https://www.ryanjuckett.com/damped-springs/). In it, he explains the algorithm he uses for moving third-person cameras in video games, provided that a camera's motion should be smooth and continuous despite the possible abrupt changes in the player's movement. This is the exact effect we wanted to achieve for animating UI elements inside our apps, so we applied the same algorithm to move layers, views, and windows on the 2D plane of the user interface.\n\n## Installation\n\nCocoaSprings is available through SPM. Just add this repository as a dependency to your package or project.\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/MacPaw/CocoaSprings.git\", branch: \"main\")\n]\n```\n\n## Usage\n\n### SpringConfiguration\n\nThe physics of CocoaSprings components is configured via the `SpringConfiguration` struct. It has two properties: \n- `angularFrequency` controls how fast an object moves towards its destination. The higher the value, the faster an object moves. The default value is `7.5`.\n- `dampingRatio` controls how fast the spring motion decays. The lower the value, the less velocity is lost upon each oscillation. The value must range from 0 to 1; the default is `0.5`.\n\nSet the configuration on any component to adjust its physics:\n```swift\nlet layer = SpringMotionLayer()\nlayer.configuration = SpringConfiguration(angularFrequency: 10, dampingRatio: 0.7)\n```\n\n### SpringMotionLayer\n\n`SpringMotionLayer` is a `CALayer` subclass available for both iOS \u0026 macOS. \n\nAdd it as a sublayer to the desired parent layer and call the `move(to:)` method to update the layer's position with spring animation.\n\nBelow is an example of a macOS `NSView` subclass that hosts a `SpringMotionLayer` and moves it to whichever point inside it gets clicked:\n\n```swift\nimport AppKit\nimport CocoaSprings\n\nfinal class ClickableView: NSView {\n    \n    override init(frame frameRect: NSRect) {\n        super.init(frame: frameRect)\n        setup()\n    }\n    \n    required init?(coder: NSCoder) {\n        super.init(coder: coder)\n        setup()\n    }\n    \n    private lazy var rootLayer = CALayer()\n    \n    private lazy var springMotionLayer: SpringMotionLayer = {\n        let layer = SpringMotionLayer()\n        layer.backgroundColor = NSColor.systemRed.cgColor\n        layer.cornerRadius = 10\n        layer.frame.size = .init(width: 20, height: 20)\n        return layer\n    }()\n    \n    private func setup() {\n        layer = rootLayer\n        wantsLayer = true\n        rootLayer.addSublayer(springMotionLayer)\n    }\n    \n    override func mouseDown(with event: NSEvent) {\n        super.mouseDown(with: event)\n        springMotionLayer.move(to: convert(event.locationInWindow, from: nil))\n    }\n}\n```\n\n![Layer Example](https://github.com/MacPaw/CocoaSprings/blob/main/Screenshots/layer.gif)\n\n### SpringMotionView\n\n`SpringMotionView` is an `NSView`/`UIView` subclass, depending on the platform you're building for. \n\nDue to the fact that views are usually positioned using constraints, the client is responsible for updating the view's position during animation. On each frame of movement the view executes its `onUpdatePosition` closure, you must set it to update the relevant view's constraints.\n\nConsider an example iOS snippet below, assume we've set up `SpringMotionView` and its constraints via Interface Builder: \n\n```swift\nimport UIKit\nimport CocoaSprings\n\nfinal class ViewController: UIViewController {\n\n    @IBOutlet weak var springMotionView: SpringMotionView!\n    @IBOutlet weak var springMotionViewTopConstraint: NSLayoutConstraint!\n    @IBOutlet weak var springMotionViewLeftConstraint: NSLayoutConstraint!\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        // Here we set up how the view should update its position\n        // depending on the provided `point` parameter.\n        springMotionView.onPositionUpdate = { [weak self] point in\n            guard let self else { return }\n            let size = self.springMotionView.frame.size\n            self.springMotionViewLeftConstraint.constant = point.x - size.width / 2\n            self.springMotionViewTopConstraint.constant = point.y - size.height / 2\n        }\n    }\n\n    // UITapGestureRecognizer action\n    @IBAction func handleTap(_ sender: UITapGestureRecognizer) {\n        // Start moving to the tapped location\n        springMotionView.move(to: sender.location(in: view))\n    }\n}\n``` \n\n![View Example](https://github.com/MacPaw/CocoaSprings/blob/main/Screenshots/view.gif)\n\n### SpringMotionWindow\n\n`SpringMotionWindow` is an `NSWindow` subclass, available for macOS only.\n\n- Call `move(to:)` to start animating position to a point on screen.\n- Call `pinToWindow(_:offsetFromCenter:)` to make the window follow any other window on screen with spring animation (example below).\n- Call `unpinFromWindow()` to stop following a previously followed window.\n\n```swift\nimport AppKit\nimport CocoaSprings\n\nfinal class ViewController: NSViewController {\n\n    override func viewDidAppear() {\n        super.viewDidAppear()\n        \n        let springMotionWindow = SpringMotionWindow()\n        springMotionWindow.contentView = NSView()\n        springMotionWindow.contentView?.wantsLayer = true\n        springMotionWindow.contentView?.layer?.backgroundColor = NSColor.systemRed.cgColor\n        springMotionWindow.contentView?.layer?.cornerRadius = 10\n        springMotionWindow.setFrame(.init(origin: .zero, size: .init(width: 100, height: 100)), display: true)\n        springMotionWindow.makeKeyAndOrderFront(nil)\n        springMotionWindow.level = .mainMenu\n        springMotionWindow.styleMask = [.borderless, .fullSizeContentView]\n        springMotionWindow.backgroundColor = .clear\n        springMotionWindow.isMovableByWindowBackground = true\n        \n        if let mainWindow = view.window {\n            springMotionWindow.pinToWindow(mainWindow, offsetFromCenter: .init(x: 300, y: 100))\n        }\n    }\n}\n```\n\n![View Example](https://github.com/MacPaw/CocoaSprings/blob/main/Screenshots/window.gif)\n\n## Demo\nFor your convenience, most of the features described above are implemented in a demo project in this repository. Please refer to it for package usage examples.\n\n![View Example](https://github.com/MacPaw/CocoaSprings/blob/main/Screenshots/demo.png)\n\n## License\n\nCocoaSprings is available under the MIT license.\n\nSee the [LICENSE](https://github.com/MacPaw/CocoaSprings/blob/master/LICENSE) file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMacPaw%2FCocoaSprings","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMacPaw%2FCocoaSprings","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMacPaw%2FCocoaSprings/lists"}