swift - 如何快速将 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)
}
}
解决方案
因为 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) } }
}
推荐阅读
- php - 版本控制软件
- php - 在 Windows 10 中启用 cURL
- java - 使用Jsoup顺序连接两个URL
- javascript - 如何从 Odoo 10 中的 JS 函数更新 One2many 字段?
- javascript - 如何停止渲染器 keydown 事件以传递给 Angular 中的其他组件?
- c# - 可以在 WPF 中绘制圆形 RegularPolygon - Blend
- c++ - HEX 值到 wchar_t 字符 (UTF-8) 的转换
- json - karate- checking whole response with jason schema getting error gherkin.lexer.LexingError
- intellij-idea - 代号1:添加外部jar“dropbox-core-sdk-3.0.10.jar (IntelliJ)”
- go - encoding of Urdu text in Go