首页 > 解决方案 > 如何快速将 UILabel 从一个 UIStackView 移动到另一个 UIStackView

问题描述

我有 3 个水平 UIStackViews(我们称它们为 labelStackViews),其中包含许多 UILabel。所有 3 个视图都在另一个水平 StackView 内(我们称之为 mainStackView)。

我希望能够通过用手指拖动标签将标签从一个 labelStackView 移动到另一个。

我使用 createPanGestureRecognizer 使每个标签都可移动。因此,如果我在另一个 labelStackView 的边界内拖动一个标签,我希望它切换位置。

但是我在从一个视图到另一个视图中删除和添加标签时遇到问题。

每个堆栈视图内的标签图像。“-3x”、“+2x”是stackview 1中的标签,“=”在stackview 2中,“2”、“+3”、“+4”在stackview 3中

图片

这是我使用的代码,当我观察到标签放在另一个 labelStackView 上方时:

lableStackView1.removeArrangedSubview(label1)
labelStackView1.setNeedsLayout()
labelStackView1.layoutIfNeeded()
labelStackView3.addArrangedSubview(label1)
labelStackView3.setNeedsLayout()

结果是 label1 完全从屏幕上消失了。并且 labelStackView1 中的其他标签按比例放大以填充整个 labelStackView 1。

我试图在 DispatchQueue.main.async {} 中移动这些代码行。但这没有帮助。

我在另一个 StackView 中有这 3 个 labelStackViews 的原因是自动缩放每个标签并将标签很好地设置在彼此旁边。

如果它有所作为,这是我对 labelStackViews 的限制:

labelStackView.axis = NSLayoutConstraint.Axis.horizontal
labelStackView.distribution = UIStackView.Distribution.fillProportionally
labelStackView.alignment = UIStackView.Alignment.center
labelStackView.spacing = 5.0

在 UILabels 上:

label.textAlignment = .center
label.layer.masksToBounds = true
label.numberOfLines = 1
label.adjustsFontSizeToFitWidth = true
label.sizeToFit()
label.layoutIfNeeded()

提前感谢您的帮助,我尝试了很多不同的东西,但我对 swift 有点陌生,所以也许我遗漏了一些明显的东西。

...这是完整的代码:

import UIKit

class imageViewController: UIViewController {
@IBOutlet weak var upperLabel: UILabel!
@IBOutlet weak var lowerLabel: UILabel!
@IBOutlet weak var screenStackView: UIStackView!

let labelStackView1 = UIStackView()
let labelStackView2 = UIStackView()
let labelStackView3 = UIStackView()

let label1 = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
let label2 = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
let label3 = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
let equalSign = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
var labels: [UILabel] = []

var oldPos: [CGPoint] = [CGPoint(x: 0,y: 0), CGPoint(x: 0,y: 0), CGPoint(x: 0,y: 0)]
var oldPosInView: [CGPoint] = [CGPoint(x: 0,y: 0), CGPoint(x: 0,y: 0), CGPoint(x: 0,y: 0)]
var side: [String] = []
var equalPos: CGPoint = CGPoint(x: 0,y: 0)

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = hexStringToUIColor(hex: "#93DDFA")
    
    labels = [label1, label2, label3]
    
    //Add first stackview to screen and then add labels to stackview
    initStackView(sview: labelStackView1)
    initLabel(label: labels[0], text: "2x", stackView: labelStackView1)
    side.append("Left")
    initLabel(label: labels[1], text: "+3", stackView: labelStackView1)
    side.append("Left")
    
    initStackView(sview: labelStackView2)
    initLabel(label: equalSign, text: "=", stackView: labelStackView2)
    
    initStackView(sview: labelStackView3)
    initLabel(label: labels[2], text: "-4x", stackView: labelStackView3)
    side.append("Right")
    
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    updatePositions(labelsInStackView: labels, stackViewScreen: screenStackView)
    createPanGestureRecognizer(labels: labels)
    
}

func updatePositions(labelsInStackView: [UILabel], stackViewScreen: UIStackView) {
    for (ind, label) in labels.enumerated() {
        oldPos[ind] = getConvertedPoint(label, baseView: view)
        oldPosInView[ind] = label.frame.origin
    }
    equalPos = getConvertedPoint(equalSign, baseView: view)
}

//Create moving gesture for all objects
func createPanGestureRecognizer(labels: [UILabel]) {
    for label in labels {
        let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(panGesture:)))
        label.addGestureRecognizer(gesture)
        label.isUserInteractionEnabled = true
    }
}

//Move the object and deside what to do when the action ends
@objc func handlePanGesture(panGesture: UIPanGestureRecognizer) {
    // get translation
    let translation = panGesture.translation(in: view)
    panGesture.setTranslation(CGPoint(x: 0.0, y: 0.0), in: view)
    if let myView = panGesture.view {
        myView.center = CGPoint(x: myView.center.x + translation.x, y: myView.center.y + translation.y)
        myView.isUserInteractionEnabled = true
    }
    
    //Move objects back to equation when action ends
    if (panGesture.state == UIGestureRecognizer.State.ended) {
        for (ind, label) in labels.enumerated() {
            let newPos = getConvertedPoint(label, baseView: view)
            if (newPos.x != oldPos[ind].x) ||  (newPos.y != oldPos[ind].y) {
                //If the object moved side then change sign and move other objects
                if didLabelMoveSide(ind: ind) {
                    print("It moved side")
                    labelMovedSide(leftStackView: labelStackView1, middleStackView: labelStackView2, rightStackView: labelStackView1, view: view, ind: ind)
                    
                    break
                } else {
                    print("It did not move side")
                    labelNotMovedSide(leftStackView: labelStackView1, middleStackView: labelStackView2, rightStackView: labelStackView3, view: view, ind: ind)
                }
            }
        }
        updatePositions(labelsInStackView: labels, stackViewScreen: screenStackView)
    }
}
}

extension imageViewController {
    func didLabelMoveSide(ind: Int) -> Bool {
        let newP = getConvertedPoint(labels[ind], baseView: view)
        let equalSignPos = equalPos.x
        if (newP.x > equalSignPos &&
            oldPos[ind].x <= equalSignPos) ||
        (newP.x <= equalSignPos && oldPos[ind].x > equalSignPos) {
            return true
        } else {
           return false
        }
    }

func labelNotMovedSide(leftStackView: UIStackView, middleStackView: UIStackView, rightStackView: UIStackView, view: UIView, ind: Int) {
    self.labels[ind].frame.origin = oldPosInView[ind]
    }

func labelMovedSide(leftStackView: UIStackView, middleStackView: UIStackView, rightStackView: UIStackView, view: UIView, ind: Int) {
        //Laben "ind" is moving side
        //Remove it from on view and add it to another
        DispatchQueue.main.async {
            if self.side[ind] == "Left" {
            self.labelStackView1.removeArrangedSubview(self.labels[ind])
            self.labelStackView1.setNeedsLayout()
            self.labelStackView1.layoutIfNeeded()
            self.labelStackView3.addArrangedSubview(self.labels[ind])
            self.labelStackView3.setNeedsLayout()
            self.side[ind] = "Right"
        } else {
            self.labelStackView3.removeArrangedSubview(self.labels[ind])
            self.labelStackView3.setNeedsLayout()
            self.labelStackView3.layoutIfNeeded()
            self.labelStackView1.addArrangedSubview(self.labels[ind])
            self.labelStackView1.setNeedsLayout()
            self.side[ind] = "Left"
        }
            
    }
}


func initLabel(label: UILabel, text: String, stackView: UIStackView) {
    label.text = text
    label.font = UIFont(name: "San Francisco", size: 40)
    label.textColor = hexStringToUIColor(hex: "#1C3294")
    label.font = UIFont.boldSystemFont(ofSize: K.textSize)
    label.textAlignment = .center
    label.layer.masksToBounds = true
    label.frame.size.width = label.intrinsicContentSize.width
    label.frame.size.height = label.intrinsicContentSize.height
    label.layer.borderColor = UIColor.white.cgColor
    label.layer.borderWidth = 6.0
    label.layer.masksToBounds = true
    label.numberOfLines = 1
    label.adjustsFontSizeToFitWidth = true
    label.sizeToFit()
    label.layoutIfNeeded()
    stackView.addArrangedSubview(label)
}

func initStackView(sview: UIStackView) {
    sview.axis = NSLayoutConstraint.Axis.horizontal
    sview.distribution = UIStackView.Distribution.fillProportionally
    sview.alignment = UIStackView.Alignment.center
    sview.spacing = 5.0
    screenStackView.addArrangedSubview(sview)
    
}

func getConvertedPoint(_ targetView: UIView, baseView: UIView)->CGPoint{
    var pnt = targetView.frame.origin
    if nil == targetView.superview{
        return pnt
    }
    var superView = targetView.superview
    while superView != baseView{
        pnt = superView!.convert(pnt, to: superView!.superview)
        if nil == superView!.superview{
            break
        }else{
            superView = superView!.superview
        }
    }
    return superView!.convert(pnt, to: baseView)
}

}

标签: swiftuilabeluistackview

解决方案


因为 stackViews 使用自动布局来排列和调整其子视图的大小,所以您将在尝试按照您尝试的方式设置帧位置进行无休止的战斗。

我将建议一种不同的方法,仅使用“容器”视图和每个标签作为子视图。

在初始布局中,我们将使用标签框架和“间隙”值来定位它们,以及设置容器的大小。

当我们“拖放”标签时,我们将:

  • 找到 drop 相对于其他标签的位置
  • 重新排列标签数组
  • 以新顺序重新布置标签

这是一个完整的例子。它不使用任何@IBOutlet@IBAction连接,因此您可以简单地创建一个新的视图控制器并将其自定义类分配给ArrangeLabelsViewController

class ArrangeLabelsViewController: UIViewController {
    
    var parts: [String] = [
        "-3x", "+2x", "=", "2", "+3", "+4",
    ]
    
    // array to hold labels
    var labels: [UILabel] = []
    
    // used in viewDidLayoutSubviews so we don't create infinite recursion
    var labelsCount: Int = 0
    
    // gap between labels
    let labelHGap: CGFloat = 5.0
    
    // gap for top and bottom of labels in container
    let labelVGap: CGFloat = 8.0

    // view to hold the labels
    let containerView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // very light gray for container background
        containerView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        view.addSubview(containerView)
        
        parts.forEach { s in
            let v = UILabel()
            v.font = .systemFont(ofSize: 30.0)
            v.text = s
            v.backgroundColor = .yellow
            v.layer.borderWidth = 1.0
            v.layer.borderColor = UIColor.red.cgColor
            v.translatesAutoresizingMaskIntoConstraints = false
            if s != "=" {
                let pg = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(panGesture:)))
                v.addGestureRecognizer(pg)
                v.isUserInteractionEnabled = true
            }
            labels.append(v)
            containerView.addSubview(v)
        }
        
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // only execute this if number of labels has changed
        if labelsCount != labels.count {
            labelsCount = labels.count
        
            // tell labels to size themselves
            labels.forEach { v in
                v.sizeToFit()
            }
            // get sum of label widths (uses Sequence extension)
            var totalWidth: CGFloat = labels.sum(\.frame.width)
            // add gaps
            totalWidth += CGFloat(labels.count + 1) * labelHGap
            // set container frame size, based on
            //  totalWidth and label height + heightGap
            containerView.frame.size = CGSize(width: totalWidth, height: labels[0].frame.height + labelVGap * 2)
            // center container in view
            containerView.center = view.center
            // on initial layout, we'll "hide" the labels by positioning them
            //  outside the container
            labels.forEach { v in
                v.frame.origin.x = -(v.frame.size.width + 2.0)
                v.center.y = containerView.bounds.midY
            }

            // start with container view clipping to its bounds, so we can
            //  animate the labels into view in their initial positions
            containerView.clipsToBounds = true
            
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updatePositions()
    }
    
    func updatePositions() -> Void {
        var x: CGFloat = labelHGap
        UIView.animate(withDuration: 0.3, animations: {
            self.labels.forEach { v in
                v.frame.origin.x = x
                v.center.y = self.containerView.bounds.midY
                x += v.frame.width + self.labelHGap
            }
        }, completion: { b in
            // allow labels to be dragged outside the bounds of the container view
            self.containerView.clipsToBounds = false
        })
    }
    
    // just for readability (as opposed to using "left" "right" strings
    enum LeftOrRightSide {
        case left, right
    }
    
    //Move the object and deside what to do when the action ends
    @objc func handlePanGesture(panGesture: UIPanGestureRecognizer) {
        
        // unwrap gesture view, its superview,
        // moving label index and equals label index
        guard let movingView = panGesture.view as? UILabel,
              let superV = movingView.superview,
              let mvIDX = labels.firstIndex(of: movingView),
              let equalsIDX = labels.firstIndex(where: { $0.text == "=" })
        else {
            return
        }
        
        let startingSide: LeftOrRightSide = mvIDX < equalsIDX ? .left : .right
        
        // if we're trying to move the only view left of the equals sign
        //  or
        // we're trying to move the only view right of the equals sign
        //  don't allow the move
        if (startingSide == .left && equalsIDX == 1) || (startingSide == .right && equalsIDX == labels.count - 2) {
            return
        }

        // get translation
        let translation = panGesture.translation(in: view)
        panGesture.setTranslation(CGPoint(x: 0.0, y: 0.0), in: view)
        
        // bring the moving view to the front
        superV.bringSubviewToFront(movingView)
        
        // move it
        movingView.center = CGPoint(x: movingView.center.x + translation.x, y: movingView.center.y + translation.y)
        
        // re-position objects when action ends
        if (panGesture.state == .ended) {
            // remove the moving view from the array
            labels.remove(at: mvIDX)
            var newIDX: Int = -1
            // find the first label to the right of where we're dropping
            if let n = labels.firstIndex(where: { $0.center.x > movingView.center.x}) {
                newIDX = n
            } else {
                newIDX = labels.count
            }
            if newIDX == labels.count {
                // we dropped right of the last label
                labels.append(movingView)
            } else {
                // we dropped left of a label
                labels.insert(movingView, at: newIDX)
            }
            let endingSide: LeftOrRightSide = newIDX < equalsIDX ? .left : .right
            if startingSide != endingSide {
                print("Changed Side")
            } else {
                print("Did NOT Change Side")
            }
            updatePositions()
        }
    }

}

// extension to get sum of a property of objects in array
extension Sequence  {
    func sum<T: AdditiveArithmetic>(_ predicate: (Element) -> T) -> T { reduce(.zero) { $0 + predicate($1) } }
}

推荐阅读