如何在不闪烁视图的情况下删除和添加 ArrangedSubview?

How to remove and addArrangedSubvew without view blinking?

我有一些视图来自一个堆栈视图。每 n 秒我都应该用来自服务器的数据更新这个视图。当新数据到来时,我清理 stackView(使用其子项的 removeFromSuperView 方法)并再次添加 arrangedSubviews 以更新 UI。有时,服务器发送与旧数据相同的数据。但是做这个更新操作,我的看法有点不寒而栗。每次我清理视图并将视图添加到我的 stackview 时,它有点拖拉和颤抖。当然,如果 oldData != newData,我只能更新我的 UI。但是这个比赛很难,很难找到正确的。那么,如何在不颤抖和不眨眼的情况下用新数据更新堆栈视图?

这是我的代码:

func configure(_ items: [Item]) {
    stackView.arrangedSubviews.forEach { [=10=].removeFromSuperview() }
    items.forEach { 
       let someView = SomeView()
       someView.configure([=10=])
       stackView.addArrangedSubview(someView)
    }
}

而不是删除/重新添加视图到堆栈视图...

  • 仅当 Item 多于当前排列的子视图时才添加新视图
  • 设置数据-configure()-每个排列的子视图
  • 如果超出需要,则隐藏现有的排列子视图

例如:

func configure(_ items: [Item]) {
    // if we have fewer arranged subviews than items
    //  add new ones
    while stackView.arrangedSubviews.count < items.count {
        stackView.addArrangedSubview(SomeView())
    }
    // hide any existing arranged subviews if we have too many
    for i in 0..<stackView.arrangedSubviews.count {
        stackView.arrangedSubviews[i].isHidden = i >= items.count
    }
    // update the existing arranged subviews with the new data
    for (thisItem, thisView) in zip(items, stackView.arrangedSubviews) {
        // unwrap the arranged subview
        guard let v = thisView as? SomeView else {
            continue
        }
        v.configure(thisItem)
    }
}

这是一个完整的示例 - “项目”的数量及其数据每 1.5 秒更改一次:

具有两个字符串的示例结构项目:

struct Item {
    var title: String = ""
    var desc: String = ""
}

带有两个标签的示例“SomeView”class:

class SomeView: UIView {
    let titleLabel = UILabel()
    let descLabel = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        backgroundColor = .systemYellow
        titleLabel.font = .boldSystemFont(ofSize: 18.0)
        descLabel.font = .italicSystemFont(ofSize: 15.0)
        [titleLabel, descLabel].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .white // so we can see the label frames at run-time
            addSubview(v)
        }
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),

            descLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4.0),

            descLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
            descLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
            descLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
        ])
    }
    
    func configure(_ item: Item) -> Void {
        titleLabel.text = item.title
        descLabel.text = item.desc
    }
}

使用堆栈视图示例“NeoView”class:

class NeoView: UIView {

    let stackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.spacing = 8
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        stackView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(stackView)
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
            stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
            stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
        ])
    }
    
    func configure(_ items: [Item]) {
        // if we have fewer arranged subviews than items
        //  add new ones
        while stackView.arrangedSubviews.count < items.count {
            stackView.addArrangedSubview(SomeView())
        }
        // hide any existing arranged subviews if we have too many
        for i in 0..<stackView.arrangedSubviews.count {
            stackView.arrangedSubviews[i].isHidden = i >= items.count
        }
        // update the existing arranged subviews with the new data
        for (thisItem, thisView) in zip(items, stackView.arrangedSubviews) {
            // unwrap the arranged subview
            guard let v = thisView as? SomeView else {
                continue
            }
            v.configure(thisItem)
        }
    }
}

示例控制器 class:

class NeoTestViewController: UIViewController {
    
    let neoView: NeoView = {
        let v = NeoView()
        v.backgroundColor = .systemTeal
        return v
    }()
    
    var items: [[Item]] = []
    var idx: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        neoView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(neoView)
        
        // respect safe area
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            neoView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            neoView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            neoView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            // no bottom constraint
        ])
        
        items = sampleData()
        
        var idx: Int = 0
        self.neoView.configure(self.items[idx % self.items.count])

        // update neoView with a new set of items every 1.5 seconds
        Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { timer in
            idx += 1
            self.neoView.configure(self.items[idx % self.items.count])
        }
        
    }
    
    func sampleData() -> [[Item]] {

        // we'll create 8 sets of [Item] arrays
        //  each with a different number of Items
        let numItems: [Int] = [
            4, 2, 6, 3, 7, 8, 3, 5,
        ]
        
        var items: [[Item]] = []
        var i: Int = 1
        numItems.forEach { n in
            var theseItems: [Item] = []
            for j in 1...n {
                let thisItem = Item(title: "Set \(i) item \(j) title.", desc: "Set \(i) item \(j) description.")
                theseItems.append(thisItem)
            }
            theseItems[0].title = "Set \(i) has \(n) items."
            items.append(theseItems)
            i += 1
        }

        return items
    }
    
}