视图如何更新视图控制器?

9

我从CS193P课程学习了Swift语言。它推荐以下API用于ViewController FaceViewController更新其视图FaceView

var expression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) {
    didSet {
        updateUI() // Model changed, so update the View
    }
}

然而,我还没有看到这个概念的扩展,即视图更新自己的模型。例如,以下内容没有意义:
// Implementing an imaginary delegate UIFaceViewDelegate
func faceView(_ faceView: FaceView, didEpdateExpressionTo expression: FacialExpression {
    self.expression = expression
    // This triggers another update to the view, and possibly infinite recursion
}

在Objective-C中,这非常简单,因为您可以使用getter和setter作为公共API,使用后备存储作为私有状态。Swift也可以使用计算变量来使用此方法,但我认为Swift的设计者有不同的想法。
那么,在响应视图更新时,表示状态更改的适当方式是什么,同时还向其他人公开合理的读/写API以检查其状态?

你的意思是像 layoutSubviews() 中那样在视图控制器中更新视图吗? - MohyG
为什么 self.expression = expression 没有意义?它是如何创建递归的? - Sulthan
4个回答

8
我还观看了2017年冬季的cs193p视频。对于FaceIt应用程序,需要将mdoel翻译为如何在视图上显示的内容。这不是1对1的翻译,而更像是3对2或者其他类似的比例。这就是为什么我们需要帮助方法updateUI(_:)

至于视图控制器如何根据视图中的变化更新模型的问题,在这个示例中,我们无法更新模型,因为我们需要弄清楚如何将2个值映射到3个值。如果我们想要持久性,我们可以将视图状态存储在core data或userDefaults中。

在更一般的情况下,当模型改变需要更新视图且视图改变需要更新模型时,我们需要有间接操作以避免您设想的循环。例如,由于FacialExpression是一个值类型,我们可以有类似以下的东西:

private var realExpression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk)
var expression: FacialExpression  {
    get { return realExpression } 
    set { 
         realExpression = newValue 
         updateUI() // Model changed, so update the View
    }  
}

那么在您的 虚拟代理UIFaceViewDelegate 中,我们可以拥有以下内容:

// Implementing an imaginary delegate UIFaceViewDelegate
func faceView(_ faceView: FaceView, didEpdateExpressionTo expression: FacialExpression {
    self.realExpression = expression
// This WILL NOT triggers another update to the view, and AVOID THE possibly of infinite recursion

}


对于简单且一对一的情况,其中模型是一个数字,视图可以更新该数字(例如通过滑动),这种方法仍然有意义吗? - William Entriken
只要是“值类型”(例如数字、字符串、数组、字典、结构体、枚举),我上面编写的示例代码仍然可以工作。 - Hange

3
以下是我的测试代码:
class SubView:UIView{

}

class TestVC: UIViewController {
    var testView : SubView = SubView.init(frame: CGRect.zero)  {
        didSet{
            print("testView didSet")
        }
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

        var testBtn = UIButton.init(frame: CGRect(x: 0, y: 0, width: 264, height: 45))
        testBtn.backgroundColor = .red
        testBtn.addTarget(self, action: #selector(clickToUpdateTestView), for: UIControlEvents.touchUpInside)
        self.view.addSubview(testBtn)
    }

    func clickToUpdateTestView() -> Void {
        self.testView = SubView.init(frame: CGRect.zero)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

但是当我点击按钮时,控制台输出"testView didSet"。你的实现方式有什么不同之处?

这与问题无关。@Full 的意思是,如果您以某种方式更新 UI,然后通过委托通知底层模型进行更新,则可能会引入无限递归(因为模型更改也会通过委派再次通知视图)。 - Gero
我并不是试图修复问题,而是试图解释didSet应该被调用。因此应该调用“updateUI()”。因此问题应该在“updateUI()”中得到解决,你认为在“updateUI()”中添加bool值也可以吗?这是关于实现逻辑的问题,而不是语言问题。@Gero - simalone
我理解你的意图,只是与问题无关。可能是语言障碍(无意冒犯)。 :) 我只是想帮助你看到:这不是一个“我的代码有问题,请帮我找出来”的问题,而是一个“最佳实践是什么”的问题。你举了一个例子,说明如何完全改变视图属性的反应,这与 OP 想知道的无关。顺便说一句,我不是在抱怨,只是想澄清误解。 - Gero

2
Hange's解决方案不错,尽管它对于引用类型无效,正如他们所说。 它还引入了另一个基本上是多余的(私有)变量,模仿Objective-C区分属性和备份成员变量的方式。这是一种风格问题,个人而言,我大多数时候会尽量避免这样做(但我也做过Hange建议的同样的事情)。
我的理由是,对于引用类型,你需要以不同的方式处理,并且我尝试避免跟随太多不同的编码模式(或者拥有太多多余的变量)。
以下是另一个建议:
重要的是在某些点打破循环依赖。 我通常遵循“只有在实际更改数据时才通知代理”的原则。 您可以在视图自身中执行此操作(出于渲染性能的原因,许多人都这样做),但并非始终如此。 视图控制器并不是进行此检查的坏地方,因此我会将您的观察员调整为:
var expression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) {
    didSet {
        if updateIsNecessary() {
            updateUI() // Model changed, so update the View
        }
    }
}
updateIsNecessary()明显决定了视图是否需要被改变,它可能依赖于oldValue以及在模型和视图数据之间的任何映射关系。如果更改实际上是从视图发起的(通知视图控制器,再通知模型,然后再次通知视图控制器),那么就没有必要更新任何内容,因为视图是首先进行更改的。
你可能会认为这会引入不必要的开销,但我怀疑性能损失实际上并不大,因为通常只涉及一些简单的检查。出于同样的原因,当模型被更新时,我通常也会有类似的检查。

1
给变量expression赋值应该与FaceView的表示同步,这意味着即使FaceView是从除我们的expression之外的输入设置的,expression也应该具有正确的值,反之亦然。您只需确保当expression的新值与旧值不同时才调用updateUI,这将避免从FaceView到expression再到updateUI再返回FaceView的递归调用。
var expression: FacialExpression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) {
    didSet {
        if expression != oldValue {
            updateUI()
        }
    }
}

这意味着FacialExpression应符合Equatable,您可以通过重载==运算符来实现。
public extension FacialExpression: Equatable {
    static func ==(lhs: FacialExpression, rhs: FacialExpression) -> Bool {
        return lhs == rhs // TODO: Logic to compare FacialExpression
    }
}

如果我写错了,请原谅,因为我没有使用适当的文本编辑器

编辑:

当第一次使用虚构委托设置不同值的表情时,FaceView将进行一次无必要的更新,但之后不会再重复更新,因为表情将保持同步。

为了避免这种情况,您可以将表情与FaceView中保存当前表情的另一个表情属性进行比较。

var expression: FacialExpression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) {
    didSet {
        if expression != faceView.currentExpression {
            updateUI()
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接