12

I am using this code now to spin bottle on button tap:

@IBAction func spinButton(sender: AnyObject) {
        let rotateView = CABasicAnimation()
        let randonAngle = arc4random_uniform(360) + 720
        rotateView.fromValue = 0
        rotateView.toValue = Float(randonAngle) * Float(M_PI) / 180.0
        rotateView.duration = 3
        rotateView.delegate = self
        rotateView.repeatCount = 0
        rotateView.removedOnCompletion = false
        rotateView.fillMode = kCAFillModeForwards
        rotateView.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
        bottleImageView.layer.addAnimation(rotateView, forKey: "transform.rotation.z")
    }

But how can I rotate the button using gesture? So the harder/faster I move my finger, the faster the bottle will spin

Roduck Nickes
  • 1,021
  • 2
  • 15
  • 41

3 Answers3

2

The simple answer to this is... use a UIScrollView.

From my question here... Loop UIScrollView but continue decelerating

Translating it to Swift is trivial but here goes...

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    //make the content size really big so that the targetOffset of the deceleration will never be met.

    scrollView.contentSize = CGSize(width: CGRectGetWidth(scrollView.frame) * 100.0, height: CGRectGetHeight(scrollView.frame))
    //set the contentOffset of the scroll view to a point in the center of the contentSize.
    scrollView.setContentOffset(CGPoint(CGRectGetWidth(scrollView.frame) * 50, 0), animated: false)
}

func rotateImageView() {
    //Calculate the percentage of one "frame" that is the current offset.

    // each percentage of a "frame" equates to a percentage of 2 PI Rads to rotate
    let minOffset = CGRectGetWidth(scrollView.frame) * 50.0
    let maxOffset = CGRectGetWidth(scrollView.frame) * 51.0

    let offsetDiff = maxOffset - minOffset

    let currentOffset = scrollView.contentOffset.x - minOffset

    let percentage = currentOffset / offsetDiff

    arrowView.transform = CGAffineTransformMakeRotation(M_PI * 2 * percentage)
}

func scrollViewDidScroll(scrollView: UIScrollView) {
    //the scrollView moved so update the rotation of the image
    rotateImageView()
}

func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    //the scrollview stopped moving.
    //set the content offset back to be in the middle
    //but make sure to keep the percentage (used above) the same
    //this ensures the arrow is pointing in the same direction as it ended decelerating

    let diffOffset = scrollView.contentOffset.x

    while diffOffset >= CGRectGetWidth(scrollView.frame) {
        diffOffset = diffOffset - CGRectGetWidth(scrollView.frame)
    }

    scrollView.setContentOffset(CGPoint(x: CGRectGetWidth(scrollView.frame) * 50 + diffOffset, y: 0), animated:false)
}

In this example my spinning view is the arrowView. The scrollView and arrowView should both be subviews of the view controller's view. The scrollView should not have anything in it.

N.B. This is done in the browser so there may be some syntax problems. You may also have to cast some of the numbers to CGFloat etc...

Also, being able to translate from Objective-C is essential for any iOS dev. Millions of apps are written using Objective-C. The syntax may be slightly different but all of the APIs are the same.

Learning how to do this is something that every iOS dev should be doing.

Community
  • 1
  • 1
Fogmeister
  • 76,236
  • 42
  • 207
  • 306
  • Do you have any sample code or anything on how I would perform this? – Roduck Nickes Jun 22 '16 at 21:52
  • @RoduckNickes the sample code in my blog can be used in the same way. All you need to do is convert the contentOffset.x into an angle. Maybe 0=0 and 200=2*Pi rotation. Then 400=4*Pi etc... That way, every 200 points of scroll is equal to a full rotation. – Fogmeister Jun 22 '16 at 21:54
  • @RoduckNickes I had a look for the code I did for this but I couldn't find it. Sorry. I think it was just an experiment I worked on. – Fogmeister Jun 22 '16 at 21:55
  • Could you please provide some code I could play around with to get this spin bottle thing? – Roduck Nickes Jun 24 '16 at 23:23
  • @RoduckNickes check the blog I linked to. It has code. Which bit do you not understand? You have a number... 0-200 for example. You need to make it into another number... 0-2*pi. That is simple enough. That's literally it. That is all you need. Look at my blog. Tell me which bit you're stuck on. – Fogmeister Jun 24 '16 at 23:25
  • Ugh, this is not going to work for me :( can you please share the code with me? And code is Obj-C, I only know Swift. – Roduck Nickes Jun 27 '16 at 23:17
  • `UIScrollView` is for scrollable content, there's nothing to scroll here, using scrollView here will be just a hack (which will cause you problems too). – farzadshbfn Jun 28 '16 at 09:45
  • @farzadshbfn absolutely wrong. UIScrollView is one of the best ways of creating things that animate and decelerate. If you disagree then I'd urge you to watch some of the WWDC videos on UIScrollView. That's where I originally got the idea from. – Fogmeister Jun 28 '16 at 09:46
  • @farzadshbfn an invisible scroll view can still be scrolled and the effect of the content offset change can then be used to make things spin. – Fogmeister Jun 28 '16 at 09:47
  • @Fogmeister I'm not saying it's not possible, I'm saying it's not what it's made for. – farzadshbfn Jun 28 '16 at 09:49
  • @farzadshbfn your answer is implementing everything that UIScrollView gives you but doing it manually and you still don't get the deceleration. It may not have been designed for this but that is certainly no reason to not use it. Nothing about the UIScrollView says that it must only ever be used for scrolling content. – Fogmeister Jun 28 '16 at 09:50
  • @RoduckNickes ok, I'll add some code for you. Will have to do it at lunch though. Haha :D – Fogmeister Jun 28 '16 at 09:58
  • @RoduckNickes I edited to show my question that I asked about the same subject. – Fogmeister Jun 28 '16 at 10:02
  • @Fogmeister If you could translate to swift it would be awesome :) – Roduck Nickes Jun 29 '16 at 01:41
  • 1
    @RoduckNickes done. You should learn how to translate between Objective-C and Swift. It is trivial and essential. Millions of apps and years worth of tutorials are written in Objective-C. – Fogmeister Jun 29 '16 at 10:58
  • 1
    @RoduckNickes I also recommend against just copying and pasting this code. You will learn nothing by doing that and it's lazy. Look at the code. Learn what it is doing and why and try to rewrite it yourself. – Fogmeister Jun 29 '16 at 11:00
  • I added a scrollView and added another UIView inside the scrollView, but now they doesn't even show up? – Roduck Nickes Jun 30 '16 at 23:58
  • @RoduckNickes so, where I said that the scroll view should be empty...? – Fogmeister Jul 01 '16 at 00:06
  • @Fogmeister What do you mean? Now you say i should have something in it? I have the UIView inside the scrollView, and nothing shows up.. – Roduck Nickes Jul 01 '16 at 00:22
  • @RoduckNickes no. Nothing should be in the scroll view. The scroll view should be empty. You are not scrolling anything. The scroll view is being used to control the rotation. – Fogmeister Jul 01 '16 at 00:24
  • So where in my `UIViewController` do I put the `scrollView`, and where do I put the `arrowView `? – Roduck Nickes Jul 01 '16 at 00:26
  • @RoduckNickes put them both in the main view of the controller. The arrow view goes wherever you want it to go. The scroll view will be over it and will probably go from one side of the screen to the other. (Wherever you want the user to be able to swipe). Make it at least as tall as the arrow. – Fogmeister Jul 01 '16 at 00:28
  • Weird, the `arrowView` doesn't move at all. Does this code looks correct: http://pastebin.com/Hs5pqLia - and here is a image of how my `UIViewController` looks like: https://s31.postimg.org/a2p2jw8kr/Screen_Shot_2016_07_01_at_02_31_29.png - the `scrollView` is over the `arrowView` – Roduck Nickes Jul 01 '16 at 00:34
  • @RoduckNickes yes. That looks correct. Did you set the delegate of the scroll view? – Fogmeister Jul 01 '16 at 08:36
  • @RoduckNickes did you get any further with this? – Fogmeister Jul 03 '16 at 16:19
  • @Fogmeister Yes, sorry - I've been away :) It worked, yes. Thanks! Another question. Is it possible to implement to detect when the bottle stops spinning, and when the finger release to start the spin? – Roduck Nickes Jul 04 '16 at 21:40
  • Yes. Stopped spinning is the method "didEndDecelerating" and I believe there is a method something like "targetOffsetForProposedTargetOffset" or something that will be called when the user ends the swipe to start it spinning. – Fogmeister Jul 04 '16 at 21:42
  • @RoduckNickes don't forget to accept an answer if it solves your problem :) Glad you got it working. – Fogmeister Jul 05 '16 at 09:04
  • Accepted! Thanks :-) – Roduck Nickes Jul 06 '16 at 23:52
2

I have managed to create a sample app that has a view in the center which you can spin with UIRotationGestureRecognizer and spin speed will be affected based on rotation speed. I've used UIDynamics for this:

class ViewController: UIViewController {

@IBOutlet weak var sampleView: UIView!

var animator: UIDynamicAnimator?

override func viewDidLoad() {
    super.viewDidLoad()

    setupRotationGesture()

}

override func viewDidAppear(animated: Bool) {

    animator = UIDynamicAnimator(referenceView: self.view)

    let sampleViewBehavior = UIDynamicItemBehavior(items: [self.sampleView])
    sampleViewBehavior.allowsRotation = true // view can rotate
    animator?.addBehavior(sampleViewBehavior)

    let anchoredSuperViewBehavior = UIDynamicItemBehavior(items: [self.view])
    anchoredSuperViewBehavior.anchored = true
    animator?.addBehavior(anchoredSuperViewBehavior)

    // Attachment between the sample view and super view at center anchor point.

    let attachment = UIAttachmentBehavior.pinAttachmentWithItem(self.sampleView, attachedToItem: self.view, attachmentAnchor: CGPointMake(self.view.center.x + 1, self.view.center.y + 1))
    animator?.addBehavior(attachment)
}


// MARK: - Rotation Gesture -

func setupRotationGesture(){

    let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(handleRotationGesture(_:)))
    self.sampleView.addGestureRecognizer(rotationGesture)
}

func handleRotationGesture(gesture: UIRotationGestureRecognizer){

    if gesture.state == .Ended {

        let push = UIPushBehavior(items: [self.sampleView], mode: .Instantaneous)

        push.magnitude = abs(50.5 * gesture.velocity)
        let transform = self.sampleView.transform
        push.angle = atan2(transform.b, transform.a);
        animator?.addBehavior(push)
    }

}
 }

When you run you will be able to spin the view based on speed of rotation gesture.

Hossam Ghareeb
  • 7,063
  • 3
  • 53
  • 64
  • 1
    Thanks, but it only spins to the right side, and it does not move when I move my finger. I have to use two fingers, and it takes way to long untill it stops spinning.? Any ideas? – Roduck Nickes Jun 30 '16 at 23:51
  • 1
    You can use the angle of rotation in the rotation gesture so it can work in both sides. If you want to use one finger, you can use swipe gesture and I think its easy. For taking too long to stop spinning, you can change the magnitude value `UIPushBehavior` I gave you the while idea and how to do it. Its just needs some tweaks from your side :) – Hossam Ghareeb Jul 01 '16 at 10:12
1

Use UIPanGestureRecognizer. Create a panGestureRecognizer and add it to your bottle's superview and implement it's delegate. It has variables such as velocityInView, translationInView. When user finishes panning, Use them to calculate the angle, speed and duration of your spin.

farzadshbfn
  • 2,710
  • 1
  • 18
  • 38
  • 1
    OK, so you create your pan gesture recogniser (which UIScrollView has for free) you create your methods to capture the velocity (which UIScrollView has for free) you update the rotation based on the translation in view (which UIScrollView has for free with content offset) then how do you calculate the deceleration based on the velocity at which the user released their tap? UIScrollView does this for you. All of the calculation and hard work which you have described here is done by UIScrollView. All you have to do is take the offset and rotate the image. – Fogmeister Jun 28 '16 at 09:49
  • 1
    @Fogmeister How much do you set your `contentSize` ? 1million? it doesn't sound right. – farzadshbfn Jun 28 '16 at 09:51
  • 1
    How I did it was to set the content size to something arbitrarily large. This was to ensure that the scroll view never got to the end in a single scroll. However, when the scrollview is no longer scrolling it can be reset back to 0 (+- the offset based on the angle) so that each time the rotation stops it goes back. Effectively making it scrollable infinitely. – Fogmeister Jun 28 '16 at 09:52
  • 1
    Ok, let's say you've cracked it. one day (say next couple of years) you want to check something out, or you give your code to someone else, it reads your code and he's just like, why scrollView? is it scrolling? where is the scrollView?, That's what i'm saying. – farzadshbfn Jun 28 '16 at 09:54
  • 1
    @Fogmeister well there was a large limit on it as i remember (99k i think). But if reaches the end of the content, your view will not function correctly anymore (until you set it to zero again) – farzadshbfn Jun 28 '16 at 09:55
  • 1
    That's a very weak argument. With a scrollview you have a single method that controls the rotation of your image view. Call the scroll view something that makes sense (`rotationControllingScrollView`). Add comments. It will be much easier for someone to read and maintain than a load of custom gesture recogniser code and deceleration calculations. – Fogmeister Jun 28 '16 at 09:56
  • 1
    Yes, that is correct. Which is why I said that it should be reset to 0 (+- the content offset based on the rotation) when the deceleration stops. You are correct that it will eventually stop animating if the user continuously scrolls the view without stopping. But it would take them a while to get to the maximum offset. Plus you could account for that by having an edge case that catches the offset if it gets too big and resets it to 0 again. Still much easier than writing everything with custom gesture recognisers. – Fogmeister Jun 28 '16 at 09:57
  • 1
    I'm saying, it sounds like a good hack. But I'd avoid using such irregularities in my code. because by my understanding, `UIScrollView` was designed for scrollable contents. sure it supports lots of things to work with, but I'll stick with classes main purposes. And no, no one said (and there's no rule or sth) that you can't use them :) – farzadshbfn Jun 28 '16 at 10:01
  • 1
    OK, well know that you are going against what Apple's own engineers in WWDC sessions have recommended. – Fogmeister Jun 28 '16 at 10:03
  • 1
    Take a look at this. This is my question from a few years ago... http://stackoverflow.com/questions/14632754/loop-uiscrollview-but-continue-decelerating – Fogmeister Jun 28 '16 at 10:04