如何为 UIView 的自定义子类设置约束?

Posted

技术标签:

【中文标题】如何为 UIView 的自定义子类设置约束?【英文标题】:How to set constraints for custom subclass of UIView? 【发布时间】:2018-02-17 15:33:30 【问题描述】:

我构建了一个自定义视图作为 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(),视图显示正确(但一旦触发平移手势处理程序就会失败)。

相反,如果我设置 frame

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

它工作正常。

提前感谢您的帮助!

【问题讨论】:

我认为您的第一个错误是在使用自动布局时考虑框架。简而言之,它们不能一起工作。其次,只是使用视图生命周期——而不是滥用它。对于override 边界和框架来说,这可能看起来像是可靠的逻辑,但为什么呢?如果您想使用约束,只需接受操作系统会这样做。在viewDidLoad 中设置您的约束,而不用担心边界和框架...接受到时间viewWillLayoutSubviewsviewDidLayoutSubviews 事情会在那里。在边界和框架上覆盖didSet?使用自动布局时完全错误的事情。 @dfd 但是如果我不覆盖框架或边界,我将如何以及在哪里调用 setup***() 函数?你能写一个答案吗? 我不确定您所说的“setup***() 函数”是什么意思。我会给你一个例子作为答案 - 降价可能会有所帮助 - 如果答案没有帮助,请告诉我我会删除它。 谢谢!我在谈论 setupStyle()、setupBehavior() 和 setupPanGesture()。它们在 didSet() 中被调用。 由于您的问题标题,我专注于“约束”。但据我所知,我认为大多数事情都可以简单地在 viewDidLoad 中完成,因为你真的想一次而不是多次做这些事情。 【参考方案1】:

查看您尝试设置的约束,您似乎想要一个视图控制器 - 我们称之为 MainViewController - 拥有一个自定义视图,因为它是全屏视图。

使用自动布局时,您应该做的第一件事是忘记框架。在大多数情况下,忘记边界。接下来,我会 use the view life cycle 并忘记在边界和框架上使用 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。做得好。如果你不这样做,你会得到约束冲突。更重要的是,如果您这样做,您基本上无需担心边界或框架。 如果您使用 IB,自动调整大小掩码会自动设置为 false。 如果您使用锚点并希望自定义视图具有 (a) 10 点边框或 (b) 50% 宽度/高度,请使用 constant: 10multiplier: 0.5。 不是必需的,但它有助于避免视图控制器的大小 - 将您的约束移动到它自己的子例程中(我调用我的 setUpConstraints() 并将其设置为 extension 到您的视图控制器。

最后一点,根据我从 Apple iTunesConnect 的电子邮件中收集到的内容,4 月份将要求对所有设备使用“安全区域”。这是我的工作:

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

最后说明:如果您必须引用一个框架,请在视图控制器视图生命周期中尽可能晚地执行此操作,例如viewDidLayoutSubviewbounds 可能早在 viewDidLoad 就已为人所知,而 frames 则不是。

我敢打赌,如果您跟踪帧 didSet 在您的平均视图负载或方向更改时发生了多少次,您会发现它比您想象的要多。

【讨论】:

以上是关于如何为 UIView 的自定义子类设置约束?的主要内容,如果未能解决你的问题,请参考以下文章

如何为我的自定义 UICollectionViewCell 设置属性

为啥设置 backgroundColor 对我的自定义 UIView 没有明显影响?

自定义UIView类约束,具体取决于ParentView

自定义 UITableViewCell 中的自定义 UIView

以编程方式约束 Dynamic ViewController 中的自定义 UIView

如何为通过 SwiftUI 中的自定义结构传递的 @State 值设置动画