5

Edit

This is what I want visualised (ignore the ugly red line, it just indicates the movement of the UIView):

enter image description here

I want to have a UIView that is initialised in the middle of the screen. After that, I want to give it a push upwards and the gravity pulls it down till it is off the screen. My old question works with a UIPushBehaviour, UIDynamicBehaviour and a UIGravityBehaviour (see below). Matt pointed out a UIPushBehaviour is maybe not the right choice, since it not work out well across every screen size available on iOS.

I can do this with a UIView.animate function, but it is really static and does not look natural. With the UIPushBehaviour, UIDynamicBehaviour and UIGravityBehaviour, it looks really nice but the UIPushBehaviour's magnitude can not be calculated across every screen size to give the same ending point of the UIView's x and y position.

Question

How can I initialise a UIView in the middle of the screen, 'pull up' that UIView (with some change in the x position) and let the gravity (or something else) pulls it down until it is off the screen? It is important that the change in the x and y position will be the same on every screen size.

Below is my old question

I have a UIPushBehaviour with instantaneous as mode in which I push some UIViews around. The greater the screen size, the less it pushes.
I also have a UIDynamicItemBehavior with resistance set to 1, I think this is one the main reasons it is different in each screen size (correct me if I am wrong).

I want a function that will push the UIView to the same ending point, with the same speed, duration and ending point regardless of the screen size.

I tried to make a relative magnitude without any luck:

For the iPhone 5S, let's say a magnitude of 0.5 would touch a UIView from the middle to the top. I wanted to calculate the magnitude across all devices like this:

let y = 0.5 / 520 // 5S screen height
magnitude = self.view.frame.height * y

For the iPhone 8, it has a very different output and is not working. When reading the docs, I thought I would understand it. I thought 1 magnitude represents 100 pixels, but it is clearly not that case.
Is there any way I can calculate a magnitude to, for example, move a UIView from the middle to the right?

I made a project here. There is a black UIView that get's pushed to the edges on an iPhone 5, but not on the iPhone 8.

J. Doe
  • 12,159
  • 9
  • 60
  • 114
  • A push behavior doesn't push _to_ a _place_; it pushes _by_ a certain _amount_. No matter what iPhone this is, you are pushing the same distance, which is all that matters. If that's not what you want, you've chosen the wrong behavior. – matt Oct 07 '17 at 19:16
  • @matt O I did not know that but can that amount not be relative to screensize? I just want to push around a UIView and add some gravity to it. I got the push behaviour from this question: https://stackoverflow.com/questions/45921609/launch-and-drop-a-uiview. When changing the origin with a UIView.animate function, the gravity only applies after the animation, that is not what I want. A UIBezierPath looks to much of a hassle since the UIView can be animated anywhere and I need to create a lot of UIBezierPath's. What approach would be correct to have a UIView with gravity to push around? – J. Doe Oct 07 '17 at 19:30
  • What is the end result that you want? There may be an easier solution. – nathangitter Oct 07 '17 at 20:06
  • Why not pull the view into place with a field? It's unclear what you're trying to do (because you have not explained). As it stands, this is an x-y question: you made a false assumption about how to accomplish a goal and now you're hoping it can be made into a true assumption. Instead, tell us what the _goal_ is. – matt Oct 07 '17 at 21:25
  • @nathan Maybe I was not clear. I edited my question with a picture. If anything is unclear I will edit my question. – J. Doe Oct 08 '17 at 03:00
  • @matt Thank you again for your response. I edited my question to make things clear. If anything is not clear I edit my question again. Thanks. – J. Doe Oct 08 '17 at 03:01
  • @J.Doe I posted an answer below. Did it solve your problem? Let me know if you're still having issues. – nathangitter Oct 11 '17 at 16:05
  • @nathan Hi, thank you for providing an answer. Your code works with iPhone's with fixed dimensions on the UIView. My views are however dynamic in size and they have a resistance to 1 with a UIDynamicItemBehavior, this does not work well with your answer. Also, on iPad's the result is not the same. – J. Doe Oct 11 '17 at 16:16
  • @J.Doe I edited my answer with some ideas to fix those issues. Based on your exact needs, you will need to adjust the constants slightly to get the effect you want. – nathangitter Oct 11 '17 at 16:24
  • @nathan have you tested it with iPad in your code? – J. Doe Oct 11 '17 at 16:40
  • @J.Doe Yep, the constants just need to be adjusted slightly, since the iPad in landscape has a "taller" aspect ratio than the phones. The general solution still works. – nathangitter Oct 11 '17 at 16:44
  • 1
    @nathan Ok thank you, I will have a look tomorrow :) don't have the project with me now – J. Doe Oct 11 '17 at 16:48

1 Answers1

4

Solution

You need to scale the push amount relative to the size of the screen so your view always ends in the same place. To do this, adjusting the UIPushBehavior's pushDirection vector works quite well. In this case, I set the push direction to be proportional to the bounds of the view, and scaled it down by a constant factor.

let push = UIPushBehavior(items: [pushView], mode: .instantaneous)
let pushFactor: CGFloat = 0.01
push.pushDirection = CGVector(dx: -view.bounds.width * pushFactor, dy: -view.bounds.height * pushFactor)
animator.addBehavior(push)

You may need to adjust some constants to get the exact animation you want. The constants you can adjust are:

  • Gravity magnitude (currently 0.3)
  • Push factor (currently 0.01)

Depending on your needs, you may need to scale the gravity magnitude proportional to the size of the screen as well.

Note: These constants will need to change based on the size of your animated view, since UIKit Dynamics treats the size of the view as its mass. If your view needs to be dynamically sized, you will need to scale your constants according to the size of the animated view.

Edit regarding comments on the original question:

  • Views of varying sizes: Like I mentioned in my note above, you'll need to apply an additional factor to account for the "mass" of the views. Something like view.frame.height * view.frame.width * someConstant should work well.

  • iPad screen size: Currently the pushFactor is applied to both the dx and dy components of the vector. Because iPads have a different aspect ratio, you'll need to split this into two constants, maybe xPushFactor and yPushFactor, which can account for the differences in aspect ratio.

Examples

iPhone 8

gif of solution on iPhone 8 screen size

iPhone SE

gif of solution on iPhone SE size

Full Playground Source Code

Copy and paste this code into a Swift playground to see it in action. I've included the sizes of various iPhone screens, so just uncomment the size you want to easily test the animation on different device sizes. Most of the interesting/relevant code is in viewDidAppear.

import UIKit
import PlaygroundSupport

class ViewController: UIViewController {

    let pushView = UIView()
    var animator: UIDynamicAnimator!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.frame = CGRect(x: 0, y: 0, width: 568, height: 320) // iPhone SE
//        view.frame = CGRect(x: 0, y: 0, width: 667, height: 375) // iPhone 8
//        view.frame = CGRect(x: 0, y: 0, width: 736, height: 414) // iPhone 8+
//        view.frame = CGRect(x: 0, y: 0, width: 812, height: 375) // iPhone X

        view.backgroundColor = .white
        let pushViewSize = CGSize(width: 200, height: 150)
        pushView.frame = CGRect(x: view.bounds.midX - pushViewSize.width / 2, y: view.bounds.midY - pushViewSize.height / 2, width: pushViewSize.width, height: pushViewSize.height)
        pushView.backgroundColor = .red
        view.addSubview(pushView)

        animator = UIDynamicAnimator(referenceView: self.view)
        let dynamic = UIDynamicItemBehavior()
        dynamic.resistance = 1
        animator.addBehavior(dynamic)

    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let gravity = UIGravityBehavior(items: [pushView])
        gravity.magnitude = 0.3
        animator.addBehavior(gravity)

        let push = UIPushBehavior(items: [pushView], mode: .instantaneous)
        let pushFactor: CGFloat = 0.01
        push.pushDirection = CGVector(dx: -view.bounds.width * pushFactor, dy: -view.bounds.height * pushFactor)
        animator.addBehavior(push)

    }

}

let vc = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = vc.view
nathangitter
  • 9,607
  • 3
  • 33
  • 42