How to create connected sliders in Swift? - ios

I'm trying to create a control with 3 vertical sliders. I want the draggable dots on the sliders to have lines connecting them. Any advice on how to go about making this?

You can create UIBezierPath, use that path in a CAShapeLayer, and add that layer as a sublayer to your view's layer.
The only issue is figuring out the start and end points for your lines, and that can be done by looking at the sliders' thumbRectForBounds. And assuming you used a standard UISliderView and simply rotated it, you should make sure to convert the point to the parent's coordinate system:
class ViewController: UIViewController {
#IBOutlet weak var slider1: UISlider!
#IBOutlet weak var slider2: UISlider!
#IBOutlet weak var slider3: UISlider!
lazy var lineLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.lineWidth = 3
layer.fillColor = UIColor.clearColor().CGColor
layer.strokeColor = UIColor.redColor().CGColor
return layer
}()
override func viewDidLoad() {
super.viewDidLoad()
[slider1, slider2, slider3].forEach { slider in
slider.transform = CGAffineTransformMakeRotation(CGFloat(-M_PI_2))
}
view.layer.insertSublayer(lineLayer, atIndex: 0)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
lineLayer.frame = view.bounds
updatePathBetweenSliders()
}
#IBAction func didChangeValue(sender: UISlider) {
updatePathBetweenSliders()
}
private func updatePathBetweenSliders() {
let path = UIBezierPath()
path.moveToPoint(slider1.thumbCenter())
path.addLineToPoint(slider2.thumbCenter())
path.addLineToPoint(slider3.thumbCenter())
lineLayer.path = path.CGPath
}
}
Where:
extension UISlider {
func thumbCenter() -> CGPoint {
let thumbRect = thumbRectForBounds(bounds, trackRect: trackRectForBounds(bounds), value: value)
let thumbCenter = CGPoint(x: thumbRect.midX, y: thumbRect.midY)
return convertPoint(thumbCenter, toView: superview)
}
}
That yields:
If you want to animate the sliders at any point (like I do at the end of that animated GIF), you'll probably have to resort to a CADisplayLink to update the values:
let animationDuration = 0.5
var originalValues: [Float]!
var targetValues: [Float]!
var startTime: CFAbsoluteTime!
#IBAction func didTapResetButton(sender: UIButton) {
let sliders = [slider1, slider2, slider3]
originalValues = sliders.map { $0.value }
targetValues = sliders.map { _ in Float(0.5) }
startTime = CFAbsoluteTimeGetCurrent()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func handleDisplayLink(displayLink: CADisplayLink) {
let percent = Float((CFAbsoluteTimeGetCurrent() - startTime) / animationDuration)
let sliders = [slider1, slider2, slider3]
if percent < 1 {
for (index, slider) in sliders.enumerate() {
slider.value = (targetValues[index] - originalValues[index]) * percent + originalValues[index]
}
} else {
for (index, slider) in sliders.enumerate() {
slider.value = targetValues[index]
}
displayLink.invalidate()
}
updatePathBetweenSliders()
}

Related

Not all pixels clipped when using UIView.layer.clipsToBounds = true in conjunction with CABasicAnimation

What I'm doing:
I am creating a music app. Within this music app, I have a music player that links with Apple Music and Spotify. The music player displays the current song's album art on a UIImageView. Whenever the song is playing, the UIImageView rotates (like a record on a record player).
What my problem is:
The album art for the UIImageView is square by default. I am rounding the UIImageView's layer's corners and setting UIImageView.clipsToBounds equal to true to make it appear as a circle.
Whenever the UIImageView rotates, some of the pixels outside of the UIImageView's layer (the part that is cut off after rounding the image) are bleeding through.
Here is what the bug looks like: https://www.youtube.com/watch?v=OJxX5PQc7Jo&feature=youtu.be
My code:
The UIImageView is rounded by setting its layer's cornerRadius equal to UIImageView.frame.height / 2 and setting UIImageView.clipsToBounds = true:
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.clipsToBounds = true
//I've also tried the following code, and am getting the same behavior:
/*
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.layer.masksToBounds = true
albumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
*/
}
}
Whenever a button is pressed, the UIImageView begins to rotate. I've used the following extension to make UIView's rotate:
extension UIView {
func rotate(duration: Double = 1, startPoint: CGFloat) {
if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = startPoint
rotationAnimation.toValue = (CGFloat.pi * 2.0) + startPoint
rotationAnimation.duration = duration
rotationAnimation.repeatCount = Float.infinity
layer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
}
}
}
I also have the following extension to end the rotation:
extension UIView {
...
func stopRotating(beginTime: Double!, startingAngle: CGFloat) -> CGFloat? {
if layer.animation(forKey: UIView.kRotationAnimationKey) != nil {
let animation = layer.animation(forKey: UIView.kRotationAnimationKey)!
let elapsedTime = CACurrentMediaTime() - beginTime
let angle = elapsedTime.truncatingRemainder(dividingBy: animation.duration)/animation.duration
layer.transform = CATransform3DMakeRotation((CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle, 0.0, 0.0, 1.0)
layer.removeAnimation(forKey: UIView.kRotationAnimationKey)
return (CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle
} else {
return nil
}
}
}
This is how these extension functions are used in the context of my view controller:
class MyViewController: UIViewController {
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
...
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}
So, all together, MyViewController looks something like this:
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.clipsToBounds = true
}
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}
I copy your code into the project and I can reproduce this issue. But if you add the animation to another CALayer, that seems to resolve the issue.
extension UIView {
static var kRotationAnimationKey: String {
return "kRotationAnimationKey"
}
func makeAnimationLayer() -> CALayer {
let results: [CALayer]? = layer.sublayers?.filter({ $0.name ?? "" == "animationLayer" })
let animLayer: CALayer
if let sublayers = results, sublayers.count > 0 {
animLayer = sublayers[0]
}
else {
animLayer = CAShapeLayer()
animLayer.name = "animationLayer"
animLayer.frame = self.bounds
animLayer.contents = UIImage(named: "imageNam")?.cgImage
layer.addSublayer(animLayer)
}
return animLayer
}
func rotate(duration: Double = 1, startPoint: CGFloat) {
if layer.animation(forKey: UIView.kRotationAnimationKey) == nil {
let animLayer = makeAnimationLayer()
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = startPoint
rotationAnimation.toValue = (CGFloat.pi * 2.0) + startPoint
rotationAnimation.duration = duration
rotationAnimation.repeatCount = Float.infinity
animLayer.add(rotationAnimation, forKey: UIView.kRotationAnimationKey)
}
}
func stopRotating(beginTime: Double!, startingAngle: CGFloat) -> CGFloat? {
let animLayer = makeAnimationLayer()
if animLayer.animation(forKey: UIView.kRotationAnimationKey) != nil {
let animation = animLayer.animation(forKey: UIView.kRotationAnimationKey)!
let elapsedTime = CACurrentMediaTime() - beginTime
let angle = elapsedTime.truncatingRemainder(dividingBy: animation.duration)/animation.duration
animLayer.transform = CATransform3DMakeRotation(CGFloat(angle) * (2 * CGFloat.pi) + startingAngle, 0.0, 0.0, 1.0)
animLayer.removeAnimation(forKey: UIView.kRotationAnimationKey)
return (CGFloat(angle) * (2 * CGFloat.pi)) + startingAngle
}
else {
return nil
}
}
Clip to bounds does not include rounded corners. Try using corner masking instead.
class MyViewController: UIViewController {
#IBOutlet var albumArtImageView: UIImageView()
var songBeginTime: Double!
var currentRecordAngle: CGFloat = 0.0
var isPlaying = false
override func viewDidLoad() {
super.viewDidLoad()
albumArtImageView.layer.cornerRadius = albumArtImageView.frame.height / 2
albumArtImageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner,.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
#IBAction func playButtonPressed(_ sender: Any) {
if isPlaying {
if let angle = albumArtImageView.stopRotating(beginTime: songBeginTime, startingAngle: currentRecordAngle) {
currentRecordAngle = angle
}
songBeginTime = nil
} else {
songBeginTime = CACurrentMediaTime()
albumArtImageView.rotate(duration: 3, startPoint: currentRecordAngle)
}
}
}

Swipe slide and move imageView position

in my code some time it increases a lot and some time let the image position increase and decrease.
IBOutlet weak var bgImage: UIImageView!
IBOutlet weak var sliders: UISlider!
IBOutlet weak var imgViews: UIImageView!
var location = CGPoint(x: 0, y: 0)
var valueOFButterFly = 54.5
var lastValueOfSlider = Float()
#IBAction func sliderValue(_ sender: UISlider) {
print(sender.value)
if lastValueOfSlider >= sender.value {
valueOFButterFly = valueOFButterFly - 1
print("new value \(valueOFButterFly)")
imgViews.center = CGPoint(x: valueOFButterFly, y: 81.5)
}
else if (lastValueOfSlider < sender.value) {
valueOFButterFly = valueOFButterFly + 1
print("new value \(valueOFButterFly)")
imgViews.center = CGPoint(x: valueOFButterFly, y: 81.5)
}
else {
print("else if")
}
self.lastValueOfSlider = sender.value
}
In case you want to match the imageView's centre with that of the slider value, you need to set the slider's currentValue as imageView's centre instead of incrementing or decrementing by 1.
Also, update the imageView.center in valueOFButterFly's didSet property observer instead of updating it multiple times throughout the code.
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var slider: UISlider!
var lastValueOfSlider = Float()
var valueOFButterFly = 54.5 {
didSet {
imageView.center = CGPoint(x: self.valueOFButterFly, y: 81.5)
}
}
override func viewDidLoad() {
super.viewDidLoad()
slider.minimumValue = 0
slider.maximumValue = Float(UIScreen.main.bounds.width)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
imageView.center = CGPoint(x: self.valueOFButterFly, y: 81.5)
}
#IBAction func sliderValue(_ sender: UISlider) {
print(sender.value)
valueOFButterFly = Double(sender.value)
self.lastValueOfSlider = sender.value
}
}
In the above code, I've used
slider.minimumValue = 0
slider.maximumValue = Float(UIScreen.main.bounds.width)
You can configure the above properties as per your requirement.

How to attach multiple UIDynamicItems to each other

I am trying to implement circles attached to each other like in Apple's Music App via UIDynamicAnimator. I need to attach circles to each other and to view center. I was trying to implement this via UIAttachmentBehavior, but seems to it's not supporting multiple attachments. In result, circles overlaps on each other :)
let attachment = UIAttachmentBehavior(item: circle, attachedToAnchor: CGPoint(x: view.center.x, y: view.center.y))
attachment.length = 10
animator?.addBehavior(attachment)
let push = UIPushBehavior(items: [circle], mode: .continuous)
collision.addItem(circle)
animator?.addBehavior(push)
What I am doing wrong?
I don't think the apple music genre picker thing uses UIAttachmentBehavior which is closer to attaching two views with a pole or a rope. But, it seems like the problem you're experiencing might be that all of the views are added at the same location which has the effect of placing them on top of each other and with the collision behavior causes them to be essentially be stuck together. One thing to do is to turn on UIDynamicAnimator debugging by calling animator.setValue(true, forKey: "debugEnabled").
For recreating the above circle picker design, I would look into using UIFieldBehavior.springField().
For example:
class ViewController: UIViewController {
lazy var animator: UIDynamicAnimator = {
let animator = UIDynamicAnimator(referenceView: view)
return animator
}()
lazy var collision: UICollisionBehavior = {
let collision = UICollisionBehavior()
collision.collisionMode = .items
return collision
}()
lazy var behavior: UIDynamicItemBehavior = {
let behavior = UIDynamicItemBehavior()
behavior.allowsRotation = false
behavior.elasticity = 0.5
behavior.resistance = 5.0
behavior.density = 0.01
return behavior
}()
lazy var gravity: UIFieldBehavior = {
let gravity = UIFieldBehavior.springField()
gravity.strength = 0.008
return gravity
}()
lazy var panGesture: UIPanGestureRecognizer = {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.didPan(_:)))
return panGesture
}()
var snaps = [UISnapBehavior]()
var circles = [CircleView]()
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(panGesture)
animator.setValue(true, forKey: "debugEnabled")
addCircles()
addBehaviors()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
gravity.position = view.center
snaps.forEach {
$0.snapPoint = view.center
}
}
func addCircles() {
(1...30).forEach { index in
let xIndex = index % 2
let yIndex: Int = index / 3
let circle = CircleView(frame: CGRect(origin: CGPoint(x: xIndex == 0 ? CGFloat.random(in: (-300.0 ... -100)) : CGFloat.random(in: (500 ... 800)), y: CGFloat(yIndex) * 200.0), size: CGSize(width: 100, height: 100)))
circle.backgroundColor = .red
circle.text = "\(index)"
circle.textAlignment = .center
view.addSubview(circle)
gravity.addItem(circle)
collision.addItem(circle)
behavior.addItem(circle)
circles.append(circle)
}
}
func addBehaviors() {
animator.addBehavior(collision)
animator.addBehavior(behavior)
animator.addBehavior(gravity)
}
#objc
private func didPan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: sender.view)
switch sender.state {
case .began:
animator.removeAllBehaviors()
fallthrough
case .changed:
circles.forEach { $0.center = CGPoint(x: $0.center.x + translation.x, y: $0.center.y + translation.y)}
case .possible, .cancelled, .failed:
break
case .ended:
circles.forEach { $0.center = CGPoint(x: $0.center.x + translation.x, y: $0.center.y + translation.y)}
addBehaviors()
#unknown default:
break
}
sender.setTranslation(.zero, in: sender.view)
}
}
final class CircleView: UILabel {
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .ellipse
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height * 0.5
layer.masksToBounds = true
}
}
For more information I would watch What's New in UIKit Dynamics and Visual Effects from WWDC 2015

How do I draw lines over an image within a ScrollView - Swift 3

I'm creating an app that allows the user to draw 2 rectangles over an image, contained within a scroll view. However, at the moment, the line does not appear to be drawn.
Here is the UIViewController that handles the drawing:
import UIKit
import CoreGraphics
import CoreData
class PhotoZoomController: UIViewController {
//Photo View's Variables
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var photoImageView: UIImageView!
#IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!
#IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!
#IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!
#IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!
var photo: Photo!
var indicatorPoints: [CGPoint]!
var objectPoints: [CGPoint]!
var context: NSManagedObjectContext!
var brushWidth: CGFloat = 10.0;
var brushColor: CGColor = UIColor.cyan.cgColor
override func viewDidLoad() {
super.viewDidLoad()
photoImageView.image = photo.image
photoImageView.sizeToFit()
scrollView.contentSize = photoImageView.bounds.size
updateZoomScale()
updateConstraintsForSize(view.bounds.size)
view.backgroundColor = .black
}
var minZoomScale: CGFloat {
let viewSize = view.bounds.size
let widthScale = viewSize.width/photoImageView.bounds.width
let heightScale = viewSize.height/photoImageView.bounds.height
return min(widthScale, heightScale)
}
func updateZoomScale() {
scrollView.minimumZoomScale = minZoomScale
scrollView.zoomScale = minZoomScale
}
func updateConstraintsForSize(_ size: CGSize) {
let verticalSpace = size.height - photoImageView.frame.height
let yOffset = max(0, verticalSpace/2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset
let xOffset = max(0, (size.width - photoImageView.frame.width)/2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
}
#IBAction func tappedInside(_ sender: Any) {
guard let sender = sender as? UITapGestureRecognizer else {
return
}
let touch = sender.location(in: self.photoImageView)
for i in 0...3 {
if indicatorPoints[i] == CGPoint() {
indicatorPoints[i] = touch
return
}
}
for i in 0...3 {
if objectPoints[i] == CGPoint() {
objectPoints[i] = touch
return
}
}
//This part isn't finished, but it's supposed to modify previous points
}
}
extension PhotoZoomController {
I feel like this is where my problems start. I know this code runs, because I've put a print statement in here and it ran.
func drawLine(between points: [CGPoint]) {
UIGraphicsBeginImageContextWithOptions(self.photoImageView.bounds.size, false, 0)
photoImageView.image?.draw(in: self.photoImageView.bounds)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.setLineCap(.round)
context.setLineWidth(brushWidth)
context.setStrokeColor(brushColor)
context.addLines(between: points)
context.strokePath()
UIGraphicsEndImageContext()
}
}
extension PhotoZoomController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return photoImageView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
updateConstraintsForSize(view.bounds.size)
if scrollView.zoomScale < minZoomScale {
dismiss(animated: true, completion: nil)
}
}
}
This is what the UIViewController currently looks like (maybe there's something wrong with the order of views?):
Storyboard picture of the ZoomController (This UIViewController)
You are saying
UIGraphicsBeginImageContextWithOptions
and
UIGraphicsEndImageContext
and in between them, while the context exists, you draw into it.
So far, so good.
But nowhere do you say
UIGraphicsGetImageFromCurrentImageContext
Therefore, the context, containing the result of your drawing, is just thrown away.

What is this? UIView's border line or shadow? And how to remove it from the background?

I'm creating a dynamic animation system(UIKit Dynamics) with an object orbiting around a point (the center of the orbit). Radial gravity and Vortex, 2 simultaneous forces make the blue object orbit around the yellow object. They are UIFieldBehavior both.
lazy var animator: UIDynamicAnimator = {
return UIDynamicAnimator(referenceView: self.view)
}()
lazy var center: CGPoint = {
return CGPoint(x: self.view.frame.midX, y: self.view.frame.midY)
}()
lazy var radialGravity: UIFieldBehavior = {
let radialGravity: UIFieldBehavior = UIFieldBehavior.radialGravityField(position: self.center)
radialGravity.region = UIRegion(radius: 300.0)
radialGravity.strength = 1.5
radialGravity.falloff = 4.0
radialGravity.minimumRadius = 50.0
return radialGravity
}()
lazy var vortex: UIFieldBehavior = {
let vortex: UIFieldBehavior = UIFieldBehavior.vortexField()
vortex.position = self.center
vortex.region = UIRegion(radius: 200.0)
vortex.strength = 0.005
return vortex
}()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animator.setValue(true, forKey: "debugEnabled")
let itemBehavior = UIDynamicItemBehavior(items: [orbitingView])
itemBehavior.density = 0.5
animator.addBehavior(itemBehavior)
vortex.addItem(orbitingView)
radialGravity.addItem(orbitingView)
animator.addBehavior(radialGravity)
animator.addBehavior(vortex)
}
I'm using the property observer to change the view layer properties and render a blue circle. And view's background blue border line is still there. How to remove it so there'll be only the circle?
#IBOutlet weak var orbitingView: UIView! {
willSet {
newValue.layer.cornerRadius = newValue.frame.width / 2.0
newValue.layer.borderWidth = 0.0
newValue.layer.backgroundColor = UIColor.blue.cgColor
}
}

Resources