Giter Club home page Giter Club logo

adjustable's Introduction

This package provides property wrappers that can be used on properties for any value conforming to ClosedRange to allow for super fast iteration of UIs and interactions without the need to wait to recompile / relaunch an application. It does so by automatically adding an interactive slider for any property that you annotate with @Adjustable that sits on top of your app's window so you can dynamically adjust properties and see them update live. It's powered by a lot of the stuff I experimented with to make SetNeedsDisplay.

This is perfect for adjusting animation parameters on-the-fly, tweaking constants, or refining things until they feel just right without needing to constantly recompile your app and allowing you to focus on making things feel great.

It uses a lot of neat runtime tricks to pull out the name of the variable so it can appropriately name the relevant slider and also features a collapsible menu that you can tuck away when you don't need it (powered by Motion!).

Note

This is designed to only be used when developing applications and shouldn't be left in production builds. I would also not recommend annotating everything in your codebase with @Adjustable as this project isn't really built to scale (yet).

Warning

This code contains some private Swift API stuff that powers @Published so there's a strong likelihood this will break in the future. I'd like to figure out ways to make the API a lot nicer in general, so if you have any ideas, let me know!

Usage

Annotate your property of a type that conforms to ClosedRange like so:

class MyView: UIView {

    // A slider from `0.0` to `100.0` starting at `20.0` will be created.
    // Anytime the slider is changed, `invalidateForAdjustable()` is called on the enclosing class.
    @Adjustable(0.0...100.0) var someCustomProperty: Double = 20.0

    // A slider from `0.0` to `100.0` starting at `20.0` will be created.
    // Anytime the slider is changed, the `valueChanged` block is called with an instance of `self` that you can reference as well as the new value.
    @Adjustable(0.0...100.0, valueChanged: { enclosingSelf, newValue in
      // access `self` via `enclosingSelf`
      // do what you want with `newValue
    }) var someOtherCustomProperty: Double = 20.0

    override func invalidateForAdjustable() {
      print("someCustomProperty: \(someCustomProperty)")
      print("someOtherCustomProperty: \(someOtherCustomProperty)")
    }

}

You can also use @Published!

class MyCoolClass {
  
  @Adjustable(0.0...100.0) var somePublishedProperty: Double = 20.0

  var publishedCancellable: AnyCancellable?

  init() {
    self.publishedCancellable = $someProperty.sink { newValue in 
      print("somePublishedProperty: \(somePublishedProperty)")
    }
  }
  
}

It also works in SwiftUI!

class Model: ObservableObject {

  @Adjustable(0.0...255.0) var somePublishedProperty: Double = 20.0

}

struct MyView: View {
  
  @StateObject var model = Model()

  var body: some View {
    Rectangle()
      .foregroundColor(Color(hue: model.colourOffset / 255.0, saturation: 1.0, brightness: 1.0))
  }
  
}

Anytime any slider is adjusted, invalidateForAdjustable will be called on the enclosing class. This by default calls setNeedsLayout on UIView and UIViewController's view, but this can be overridden and used to update the view's state or perform any action, really. There's also an inline block that can be supplied that contains an instance of the enclosing class (enclosingSelf) as well as the new value from the slider. ObservableObject invalidation is also supported and it will automatically call the correct objectWillChange event.

If you wish to override the default invalidation behaviour of UIView or UIViewController you can adopt the AdjustableInvalidation protocol:

import Motion

class MyView: UIView {

  @Adjustable(0.0...1.0) private var springResponse: Double = 0.5

  var springAnimation = SpringAnimation<CGFloat>(response: 0.5, damping: 1.0)

  func invalidateForAdjustable() {
      springAnimation.configure(response: springResponse, damping: 1.0)
  }

}

I've saved a lot of time using Adjustable in conjunction with Motion for rapid iteration of animation constants (e.g. springs). I'll try an animation, tweak it, try it again, and tweak it some more etc. Once finished, I'll update the constant inline and remove the @Adjustable. This saves so much time and allows you to focus on adjusting interactions to feel as best they can be (instead of waiting for Xcode to build and run again).

Another thing I use it for is layout constants that are referenced in layoutSubviews. Adjusting the slider will change the value, invalidate layout automatically (via setNeedsLayout), and call layoutSubviews which makes iteration really easy.

Note

Similar ideas for "hot-reloading" projects exist: InjectionIII / Inject, as well as SwiftUI Previews, and while they're great tools, I felt like adjusting via sliders allows you to really focus on the feel of things moreso than waiting for delta compilations / injections (as well as more stable than relying on injection of dylibs to not break things haha).

Installation

Requirements

  • iOS 17+
  • Swift 5.9 or higher

Currently Adjustable supports Swift Package Manager.

Swift Package Manager

Add the following to your Package.swift (or add it via Xcode's GUI):

.package(url: "https://github.com/b3ll/Adjustable", from: "0.0.1")

License

Adjustable is licensed under the BSD 2-clause license.

Thanks

Thanks to @harlanhaskins and @hollyborla for helping point me in the right direction and explain the complexity that this sort of solution entails.

More info here.

Contact Info

Feel free to follow me on Mastodon: @b3ll!

adjustable's People

Contributors

b3ll avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.