Я создал собственное представление как подкласс UIView. Теперь я хотел бы установить ограничения для этого представления в моем ViewController вместо установки фрейма. Это настраиваемый класс представления:

PullUpView.swift

//
//  PullUpView.swift
//  
//

import UIKit

final internal class PullUpView: UIView, UIGestureRecognizerDelegate {

    // MARK: - Variables
    private var animator                : UIDynamicAnimator!
    private var collisionBehavior       : UICollisionBehavior!
    private var snapBehaviour           : UISnapBehavior?
    private var dynamicItemBehavior     : UIDynamicItemBehavior!
    private var gravityBehavior         : UIGravityBehavior!
    private var panGestureRecognizer    : UIPanGestureRecognizer!
    internal var delegate               : PullUpViewDelegate?


    // MARK: - Setup the view when bounds are defined.
    override var bounds: CGRect {
        didSet {
            print(self.frame)
            print(self.bounds)
            setupStyle()
            setupBehavior()
            setupPanGesture()
        }
    }

    override var frame: CGRect {
        didSet {
            print(self.frame)
            print(self.bounds)
            setupStyle()
            setupBehavior()
            setupPanGesture()
        }
    }
}


// MARK: - Behavior (Collision, Gravity, etc.)
private extension PullUpView {
    final private func setupBehavior() {
        guard let superview = superview else { return }
        animator = UIDynamicAnimator(referenceView: superview)
        dynamicItemBehavior = UIDynamicItemBehavior(items: [self])
        dynamicItemBehavior.allowsRotation = false
        dynamicItemBehavior.elasticity = 0

        gravityBehavior = UIGravityBehavior(items: [self])
        gravityBehavior.gravityDirection = CGVector(dx: 0, dy: 1)

        collisionBehavior = UICollisionBehavior(items: [self])

        configureContainer()

        animator.addBehavior(gravityBehavior)
        animator.addBehavior(dynamicItemBehavior)
        animator.addBehavior(collisionBehavior)
    }

    final private func configureContainer() {
        let boundaryWidth = UIScreen.main.bounds.size.width
        let boundaryHeight = UIScreen.main.bounds.size.height

        collisionBehavior.addBoundary(withIdentifier: "upper" as NSCopying, from: CGPoint(x: 0, y: 0), to: CGPoint(x: boundaryWidth, y: 0))

        collisionBehavior.addBoundary(withIdentifier: "lower" as NSCopying, from: CGPoint(x: 0, y: boundaryHeight + self.frame.height - 66), to: CGPoint(x: boundaryWidth, y: boundaryHeight + self.frame.height - 66))
    }

    final private func snapToBottom() {
        gravityBehavior.gravityDirection = CGVector(dx: 0, dy: 2.5)
    }

    final private func snapToTop() {
        gravityBehavior.gravityDirection = CGVector(dx: 0, dy: -2.5)
    }
}


// MARK: - Style (Corner-radius, background, etc.)
private extension PullUpView {
    final private func setupStyle() {
        self.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0)
        let blurEffect = UIBlurEffect(style: .regular)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = self.bounds
        blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        self.addSubview(blurEffectView)

        self.layer.masksToBounds = true
        self.clipsToBounds = true
        self.isUserInteractionEnabled = true

        let dragBar = UIView()
        dragBar.backgroundColor = .gray
        dragBar.frame = CGRect(x: (self.bounds.width / 2) - 5, y: self.bounds.origin.y + 5, width: 10, height: 2)
        self.addSubview(dragBar)
    }
}



// MARK: - Pan Gesture Recognizer and Handler Functions
internal extension PullUpView {
    final private func setupPanGesture() {
        panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        panGestureRecognizer.cancelsTouchesInView = false
        self.addGestureRecognizer(panGestureRecognizer)
    }

    @objc final private func handlePanGesture(_ panGestureRecognizer: UIPanGestureRecognizer) {
        guard let superview = superview else { return }
        if let viewCanMove: Bool = delegate?.viewCanMove?(pullUpView: self) {
            if !viewCanMove {
                return
            }
        }

        let velocity = panGestureRecognizer.velocity(in: superview).y

        var movement = self.frame
        movement.origin.x = 0
        movement.origin.y = movement.origin.y + (velocity * 0.05)

        if panGestureRecognizer.state == .ended {
            panGestureEnded()
        } else if panGestureRecognizer.state == .began {
            snapToBottom()
        } else {
            animator.removeBehavior(snapBehaviour ?? UIPushBehavior())
            snapBehaviour = UISnapBehavior(item: self, snapTo: CGPoint(x: movement.midX, y: movement.midY))
            animator.addBehavior(snapBehaviour ?? UIPushBehavior())
        }
    }

    final private func panGestureEnded() {
        animator.removeBehavior(snapBehaviour ?? UIPushBehavior())

        let velocity = dynamicItemBehavior.linearVelocity(for: self)

        if fabsf(Float(velocity.y)) > 250 {
            if velocity.y < 0 {
                moveViewIfPossible(direction: .up)
            } else {
                moveViewIfPossible(direction: .down)
            }
        } else {
            if let superviewHeight = self.superview?.bounds.size.height {
                // User scrolled over half of the screen...
                if self.frame.origin.y > superviewHeight / 2 {
                    moveViewIfPossible(direction: .down)
                } else {
                    moveViewIfPossible(direction: .up)
                }
            }
        }
    }

    final private func moveViewIfPossible(direction: PullUpViewPanDirection) {
        if let viewCanMove: Bool = delegate?.viewCanSnap?(pullUpView: self, direction: direction) {
            if viewCanMove {
                delegate?.viewWillMove?(pullUpView: self, direction: direction)
                direction == .up ? snapToTop() : snapToBottom()
            }
        } else {
            delegate?.viewWillMove?(pullUpView: self, direction: direction)
            direction == .up ? snapToTop() : snapToBottom()
        }
    }
}


// MARK: - PullUpViewPanDirection declared inside
internal extension PullUpView {
    /// An enum that defines whether the view moves up or down.
    @objc
    internal enum PullUpViewPanDirection: Int {
        case
        up,
        down
    }
}

Но когда я устанавливаю ограничения, я всегда получаю кадр с отрицательным началом:

ViewController.swift

pullUpView.translatesAutoresizingMaskIntoConstraints = false
pullUpView.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
pullUpView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
pullUpView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
pullUpView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true

Рамка :
( -160.0 , -252.0 , 320.0, 504.0)

Границы :
(0,0, 0,0, 320,0, 504,0)

Странно то, что если я закомментирую функцию setupBehavior () , представление будет отображаться правильно (но оно завершится ошибкой, как только сработает обработчик жестов панорамирования).

Вместо этого, если я установил рамку :

pullUpView.frame = CGRect(x: 0, y: self.view.bounds.height - 66, width: self.view.bounds.width, height: self.view.bounds.height)

Работает нормально.

Спасибо за любую помощь заранее!

0
j3141592653589793238 17 Фев 2018 в 18:33

1 ответ

Лучший ответ

Глядя на ваши ограничения, которые вы пытаетесь установить, кажется, что вы хотите, чтобы контроллер представления - назовем его MainViewController - имел настраиваемое представление в качестве полноэкранного представления.

Первое, что вам следует сделать при использовании автоматического макета, - это забыть о фреймах. В большинстве случаев забывают о границах. Затем я бы использовал жизненный цикл представления и забыл об использовании didSet на границах и фреймах. Вот как бы я это сделал:

Установите все, что вы можете в viewDidLoad - и в вашем случае это все:

override func viewDidLoad() {
    super.viewDidLoad()

    pullUpView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(pullUpView)

    pullUpView.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
    pullUpView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
    pullUpView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
    pullUpView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
}

Вот и все! Соответствующие примечания:

  • Вы правильно установили маску автоматического изменения размера на false. Отличная работа. Если вы этого не сделаете, вы получите конфликт ограничений. Что еще более важно, если вы это делаете , вам не нужно беспокоиться о границах или фреймах.
  • Для маски автоматического изменения размера автоматически устанавливается значение false, если вы используете IB.
  • Если вы используете якоря и хотите, чтобы ваш пользовательский вид имел либо (а) границу из 10 пунктов, либо (б) ширину / высоту 50%, используйте constant: 10 или multiplier: 0.5.
  • Не требуется, но это помогает держаться подальше от размера контроллера представления - переместите ограничения в его собственную подпрограмму (я называю свою setUpConstraints() и делаю ее extension для вашего контроллера представления.

Последний пункт, и из того, что я почерпнул из электронного письма от Apple, iTunesConnect будет требоваться в апреле, используйте «безопасную зону» для всех ваших устройств. Вот что я делаю:

let safeAreaView = UIView()

safeAreaView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(safeAreaView)

if #available(iOS 11, *) {
    let guide = view.safeAreaLayoutGuide
    safeAreaView.topAnchor.constraintEqualToSystemSpacingBelow(guide.topAnchor, multiplier: 1.0).isActive = true
    safeAreaView.bottomAnchor.constraintEqualToSystemSpacingBelow(guide.bottomAnchor, multiplier: 1.0).isActive = true
} else {
    safeAreaView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
    safeAreaView.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
}
safeAreaView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
safeAreaView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true

Есть и другие способы сделать это (это создает UIView), но теперь вы устанавливаете все свои ограничения на safeAreaView вместо self.view.

Последнее замечание: если вы должны ссылаться на фрейм, сделайте это как можно позже в жизненном цикле представления контроллера представления, скажем viewDidLayoutSubview. Хотя границы могут быть известны еще до viewDidLoad, фреймы - нет.

Готов поспорить, что если вы проследите, сколько раз происходил кадр didSet при средней загрузке изображения или изменении ориентации, вы обнаружите, что это больше, чем вы думали.

2
dfd 17 Фев 2018 в 19:51