Wednesday, May 31, 2023
HomeiOS Developmentios - UIViewPropertyAnimator: "Quick" timing parameters and/or low animation length causes scrubbing...

ios – UIViewPropertyAnimator: “Quick” timing parameters and/or low animation length causes scrubbing to snap between values at the beginning


I have been enjoying round with the superior UIViewPropertyAnimator, however are observing one thing throughout scrubbing I do not fairly perceive.

Linearly scrubbing a UIViewPropertyAnimator configured with “quick” timing parameters and/or low animation length, causes the animated properties to snap between (bigger) values at the beginning of scrubbing.

This may be made seen with a easy backside sheet instance (recorded on iPhone 14 Professional with iOS 16.4.1):

Small/”quick” timing parameters: Massive/”sluggish” timing parameters
enter image description here

The UIViewPropertyAnimator is instantiated as follows (the difficulty additionally noticed with different initializers):

// length set to 0.25 provides the undesirable snapping at the beginning of scrubbing:
UIViewPropertyAnimator(length: 0.25, dampingRatio: 1)

// length set to 1.25 provides the anticipated clean scrubbing.
UIViewPropertyAnimator(length: 1.25, dampingRatio: 1)

Any concepts about what is going on on right here? I might count on the scrubbing to be clean impartial of what parameters are given? It is particularly odd that the snapping solely occurs at the beginning of the scrubbing…

Additionally added the complete instance code:

//
//  BottomSheetAnimationViewController.swift
//

import UIKit

// MARK: BottomSheetView

last class BottomSheetView: UIView {
    personal let handleViewHeight: CGFloat = 8

    personal lazy var handleView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .tertiarySystemBackground
        view.layer.cornerRadius = handleViewHeight / 2
        view.clipsToBounds = true
        return view
    }()

    init() {
        tremendous.init(body: .zero)

        addSubview(handleView)

        NSLayoutConstraint.activate([
            handleView.centerXAnchor.constraint(equalTo: centerXAnchor),
            handleView.centerYAnchor.constraint(equalTo: topAnchor, constant: handleViewHeight),
            handleView.heightAnchor.constraint(equalToConstant: handleViewHeight),
            handleView.widthAnchor.constraint(equalToConstant: handleViewHeight * 10)
        ])

        backgroundColor = .secondarySystemBackground
        layer.cornerRadius = 16
        layer.maskedCorners = [
            .layerMinXMinYCorner,
            .layerMaxXMinYCorner
        ]
        clipsToBounds = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been applied")
    }
}

// MARK: BottomSheetAnimationViewController

class BottomSheetAnimationViewController: UIViewController {
    personal lazy var bottomSheetView: UIView = {
        let bottomSheetView = BottomSheetView()
        bottomSheetView.translatesAutoresizingMaskIntoConstraints = false
        return bottomSheetView
    }()

    personal lazy var panGestureRecognizer = UIPanGestureRecognizer(
        goal: self, motion: #selector(onPan)
    )

    personal let animator: BottomSheetAnimator = BottomSheetAnimator()

    override func loadView() {
        let view = UIView()

        view.addSubview(bottomSheetView)

        let bottomConstraint = bottomSheetView.bottomAnchor.constraint(
            equalTo: view.bottomAnchor
        )

        animator.bottomConstraint = bottomConstraint

        NSLayoutConstraint.activate([
            bottomSheetView.heightAnchor.constraint(
                equalTo: view.heightAnchor, multiplier: 0.66
            ),
            bottomSheetView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            bottomSheetView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            bottomConstraint,
        ])

        view.backgroundColor = .systemBackground

        self.view = view
    }

    override func viewDidLoad() {
        tremendous.viewDidLoad()

        bottomSheetView.addGestureRecognizer(panGestureRecognizer)
    }

    @objc
    personal func onPan(_ panGestureRecognizer: UIPanGestureRecognizer) {
        guard let view = panGestureRecognizer.view else {
            return
        }

        let translation = panGestureRecognizer.translation(in: view.superview)

        swap panGestureRecognizer.state {
            case .attainable:
                break
            case .failed:
                break
            case .started:
                animator.begin(animating: view)
            case .modified:
                animator.transfer(view, basedOn: translation)
            default: // .ended, .cancelled, @unknown
                let velocity = panGestureRecognizer.velocity(in: view.superview)
                animator.cease(animating: view, with: velocity)
        }
    }
}

// MARK: BottomSheetAnimator

class BottomSheetAnimator {
    var bottomConstraint: NSLayoutConstraint?

    // Seize the sheet's preliminary top.
    personal var initialSheetHeight: CGFloat = .zero

    personal var offsetAnimator: UIViewPropertyAnimator?

    personal func makeOffsetAnimator(
        animating view: UIView,
        to offset: CGFloat
    ) -> UIViewPropertyAnimator {
        let propertyAnimator = UIViewPropertyAnimator(
            length: 0.25, // Low values makes the scrubbing snap between values at the beginning.
            dampingRatio: 1
        )

        propertyAnimator.addAnimations {
            self.bottomConstraint?.fixed = offset

            view.superview?.layoutIfNeeded()
        }

        propertyAnimator.addCompletion { place in
            self.bottomConstraint?.fixed = place == .finish ? offset : 0
        }

        return propertyAnimator
    }
}

extension BottomSheetAnimator {
    func begin(animating view: UIView) {
        initialSheetHeight = view.body.top

        offsetAnimator = makeOffsetAnimator(
            animating: view, to: initialSheetHeight
        )
    }

    func transfer(_ view: UIView, basedOn translation: CGPoint) {
        let fractionComplete = min(max(translation.y, 0) / initialSheetHeight, 1)

        offsetAnimator?.fractionComplete = fractionComplete
    }

    func cease(animating view: UIView, with velocity: CGPoint) {
        let fractionComplete = offsetAnimator?.fractionComplete ?? 0

        offsetAnimator?.isReversed = fractionComplete < 0.5

        offsetAnimator?.continueAnimation(
            withTimingParameters: nil,
            durationFactor: 1
        )
    }
}

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments