diff --git a/FluidInterfaces/FluidInterfaces.xcodeproj/project.pbxproj b/FluidInterfaces/FluidInterfaces.xcodeproj/project.pbxproj index b50fb06..5c2a4df 100644 --- a/FluidInterfaces/FluidInterfaces.xcodeproj/project.pbxproj +++ b/FluidInterfaces/FluidInterfaces.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + BF3FF11B211C42C300EF8DF4 /* UISpringTimingParameters+DesignParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3FF11A211C42C300EF8DF4 /* UISpringTimingParameters+DesignParams.swift */; }; D806B89020F8275600740219 /* UIColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D806B88F20F8275600740219 /* UIColorExtensions.swift */; }; D806B89220F82A3700740219 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D806B89120F82A3700740219 /* UIViewExtensions.swift */; }; D8409D0A20FBDA8100C7DCD2 /* CGPointExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8409D0920FBDA8100C7DCD2 /* CGPointExtensions.swift */; }; @@ -29,6 +30,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + BF3FF11A211C42C300EF8DF4 /* UISpringTimingParameters+DesignParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISpringTimingParameters+DesignParams.swift"; sourceTree = ""; }; D806B88F20F8275600740219 /* UIColorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorExtensions.swift; sourceTree = ""; }; D806B89120F82A3700740219 /* UIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; D8409D0920FBDA8100C7DCD2 /* CGPointExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPointExtensions.swift; sourceTree = ""; }; @@ -98,6 +100,7 @@ D806B89120F82A3700740219 /* UIViewExtensions.swift */, D806B88F20F8275600740219 /* UIColorExtensions.swift */, D8409D0920FBDA8100C7DCD2 /* CGPointExtensions.swift */, + BF3FF11A211C42C300EF8DF4 /* UISpringTimingParameters+DesignParams.swift */, D8757E7520F124BD00D7EB4E /* Main.storyboard */, D8757E7820F124BF00D7EB4E /* Assets.xcassets */, D8757E7A20F124BF00D7EB4E /* LaunchScreen.storyboard */, @@ -180,6 +183,7 @@ D8976AB420F2B3A400E148CB /* InterfaceViewController.swift in Sources */, D806B89220F82A3700740219 /* UIViewExtensions.swift in Sources */, D806B89020F8275600740219 /* UIColorExtensions.swift in Sources */, + BF3FF11B211C42C300EF8DF4 /* UISpringTimingParameters+DesignParams.swift in Sources */, D8976ABC20F2D3DC00E148CB /* Spring.swift in Sources */, D8757E7420F124BD00D7EB4E /* MenuViewController.swift in Sources */, D8409D0C20FC010B00C7DCD2 /* GradientView.swift in Sources */, @@ -269,7 +273,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -323,7 +327,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; @@ -337,9 +341,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = PW5ET5HNB2; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = FluidInterfaces/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -356,9 +359,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = PW5ET5HNB2; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = FluidInterfaces/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/FluidInterfaces/FluidInterfaces/Spring.swift b/FluidInterfaces/FluidInterfaces/Spring.swift index 20e6e3f..bc22dc0 100644 --- a/FluidInterfaces/FluidInterfaces/Spring.swift +++ b/FluidInterfaces/FluidInterfaces/Spring.swift @@ -20,10 +20,9 @@ class SpringInterfaceViewController: InterfaceViewController { private lazy var dampingSliderView: SliderView = { let sliderView = SliderView() - sliderView.translatesAutoresizingMaskIntoConstraints = false - sliderView.title = "DAMPING (BOUNCINESS)" - sliderView.minValue = 0.1 - sliderView.maxValue = 1 + sliderView.valueFormatter = { String(format: "%i%%", Int($0 * 100)) } + sliderView.title = "DAMPING" + sliderView.range = 0.1...1 sliderView.value = dampingRatio sliderView.sliderMovedAction = { self.dampingRatio = $0 } sliderView.sliderFinishedMovingAction = { self.resetAnimation() } @@ -32,10 +31,9 @@ class SpringInterfaceViewController: InterfaceViewController { private lazy var frequencySliderView: SliderView = { let sliderView = SliderView() - sliderView.translatesAutoresizingMaskIntoConstraints = false - sliderView.title = "RESPONSE (SPEED)" - sliderView.minValue = 0.1 - sliderView.maxValue = 2 + sliderView.valueFormatter = { String(format: "%.2fs", $0) } + sliderView.title = "RESPONSE" + sliderView.range = 0.1...2 sliderView.value = frequencyResponse sliderView.sliderMovedAction = { self.frequencyResponse = $0 } sliderView.sliderFinishedMovingAction = { self.resetAnimation() } @@ -46,26 +44,37 @@ class SpringInterfaceViewController: InterfaceViewController { private var frequencyResponse: CGFloat = 1 private let margin: CGFloat = 30 + + private var leadingAnchor, trailingAnchor : NSLayoutConstraint! override func viewDidLoad() { super.viewDidLoad() view.addSubview(dampingSliderView) - dampingSliderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin).isActive = true - dampingSliderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin).isActive = true - dampingSliderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true + UIView.activate(constraints: [ + dampingSliderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin), + dampingSliderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin), + dampingSliderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0) + ]) view.addSubview(frequencySliderView) - frequencySliderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin).isActive = true - frequencySliderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin).isActive = true - frequencySliderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 140).isActive = true + UIView.activate(constraints: [ + frequencySliderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin), + frequencySliderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin), + frequencySliderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 140) + ]) view.addSubview(springView) - springView.heightAnchor.constraint(equalToConstant: 80).isActive = true - springView.widthAnchor.constraint(equalToConstant: 80).isActive = true - springView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin).isActive = true - springView.bottomAnchor.constraint(equalTo: dampingSliderView.topAnchor, constant: -80).isActive = true - + UIView.activate(constraints: [ + springView.heightAnchor.constraint(equalToConstant: 80), + springView.widthAnchor.constraint(equalToConstant: 80), + springView.bottomAnchor.constraint(equalTo: dampingSliderView.topAnchor, constant: -80) + ]) + self.leadingAnchor = springView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin) + self.leadingAnchor.isActive = true + self.trailingAnchor = springView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin) + self.trailingAnchor.isActive = false + animateView() } @@ -74,22 +83,28 @@ class SpringInterfaceViewController: InterfaceViewController { /// Repeatedly animates the view using the current `dampingRatio` and `frequencyResponse`. private func animateView() { + self.view.layoutIfNeeded() + + // swap the constraints that align the springView to the left/right - deactivation has to be done in the correct order - when both layout constraints are active at the same time, this creates a layout warning + if self.leadingAnchor.isActive { + self.leadingAnchor.isActive = false + self.trailingAnchor.isActive = true + } else { + self.trailingAnchor.isActive = false + self.leadingAnchor.isActive = true + } + let timingParameters = UISpringTimingParameters(damping: dampingRatio, response: frequencyResponse) animator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters) animator.addAnimations { - let translation = self.view.bounds.width - 2 * self.margin - 80 - self.springView.transform = CGAffineTransform(translationX: translation, y: 0) - } - animator.addCompletion { _ in - self.springView.transform = .identity - self.animateView() + self.view.layoutIfNeeded() } + animator.addCompletion { _ in self.animateView() } animator.startAnimation() } private func resetAnimation() { animator.stopAnimation(true) - self.springView.transform = .identity animateView() } @@ -110,22 +125,20 @@ class SliderView: UIView { } set { slider.value = Float(newValue) - valueLabel.text = String(format: "%.2f", newValue) - } - } - - public var minValue: CGFloat = 0 { - didSet { - slider.minimumValue = Float(minValue) + valueLabel.text = valueFormatter(newValue) } } - public var maxValue: CGFloat = 1 { + public var range: ClosedRange = 0...1 { didSet { - slider.maximumValue = Float(maxValue) + slider.minimumValue = range.lowerBound + slider.maximumValue = range.upperBound } } + /// Code to format the value to a string for the valueLabel + public var valueFormatter: (CGFloat) -> (String) = { String(format: "%.2f", $0) } + /// Code that's executed when the slider moves. public var sliderMovedAction: (CGFloat) -> () = { _ in } @@ -169,41 +182,31 @@ class SliderView: UIView { private func sharedInit() { addSubview(titleLabel) - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - titleLabel.topAnchor.constraint(equalTo: topAnchor).isActive = true + UIView.activate(constraints: [ + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + titleLabel.topAnchor.constraint(equalTo: topAnchor) + ]) addSubview(valueLabel) - valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - valueLabel.lastBaselineAnchor.constraint(equalTo: titleLabel.lastBaselineAnchor).isActive = true - + UIView.activate(constraints: [ + valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + valueLabel.lastBaselineAnchor.constraint(equalTo: titleLabel.lastBaselineAnchor) + ]) + addSubview(slider) - slider.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - slider.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - slider.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - slider.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20).isActive = true - + UIView.activate(constraints: [ + slider.leadingAnchor.constraint(equalTo: leadingAnchor), + slider.trailingAnchor.constraint(equalTo: trailingAnchor), + slider.bottomAnchor.constraint(equalTo: bottomAnchor), + slider.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20) + ]) + } @objc private func sliderMoved(slider: UISlider, event: UIEvent) { - valueLabel.text = String(format: "%.2f", slider.value) + valueLabel.text = valueFormatter(CGFloat(slider.value)) sliderMovedAction(CGFloat(slider.value)) if event.allTouches?.first?.phase == .ended { sliderFinishedMovingAction() } } } - -extension UISpringTimingParameters { - - /// A design-friendly way to create a spring timing curve. - /// - /// - Parameters: - /// - damping: The 'bounciness' of the animation. Value must be between 0 and 1. - /// - response: The 'speed' of the animation. - /// - initialVelocity: The vector describing the starting motion of the property. Optional, default is `.zero`. - public convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) { - let stiffness = pow(2 * .pi / response, 2) - let damp = 4 * .pi * damping / response - self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity) - } - -} diff --git a/FluidInterfaces/FluidInterfaces/UISpringTimingParameters+DesignParams.swift b/FluidInterfaces/FluidInterfaces/UISpringTimingParameters+DesignParams.swift new file mode 100644 index 0000000..0d0ed9d --- /dev/null +++ b/FluidInterfaces/FluidInterfaces/UISpringTimingParameters+DesignParams.swift @@ -0,0 +1,17 @@ +import UIKit + +extension UISpringTimingParameters { + + /// A design-friendly way to create a spring timing curve. + /// + /// - Parameters: + /// - damping: The 'bounciness' of the animation. Value must be between 0 and 1. + /// - response: The 'speed' of the animation. + /// - initialVelocity: The vector describing the starting motion of the property. Optional, default is `.zero`. + public convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) { + let stiffness = pow(2 * .pi / response, 2) + let damp = 4 * .pi * damping / response + self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity) + } + +}