我从CS193P课程中学到了Swift。它建议ViewController FaceViewController
使用以下API来更新其视图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以检查其状态?
答案 0 :(得分:8)
我还观看了cs193p
2017年冬季视频。对于FaceIt
应用,mdoel
需要转换为how it will be displayed on the view
。它不是1 to 1
翻译,而是更像3 to 2
或类似的东西。这就是为什么我们有辅助方法updateUI(_:)
关于view controller
如何根据model
中的更改更新view
的问题。在此示例中,我们无法更新model
,因为我们需要找出how to map 2 values to 3 values
?如果我们想要持久性,我们可以将view state
存储在core data
或userDefaults
中。
在model change need to update the view
和view change need to update the model
的更常规设置中,我们需要indirection
来避免您想象的cycle
。
例如,因为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
}
}
}
然后在imaginary delegate 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
}
答案 1 :(得分: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 "在单击按钮时在控制台输出中。与您的工具有什么不同?
答案 2 :(得分:2)
Hange的解决方案很好,虽然它不适用于参考类型,正如他们所说的那样。 它还引入了另一个基本冗余(私有)变量,模仿Objective-C区分属性和后备成员变量的方式。这是一个风格问题,我个人会主要试图避免这种情况(但我也做过Hange所说的同样的事情)。
我的理由是,对于引用类型,您需要以不同的方式执行此操作,并且我尽量避免使用太多不同的编码模式(或者有太多冗余变量)。
这是一个不同的提议:
重要的是要在某个时刻打破这个循环。我通常会“只告知你的代表你是否真的改变了你的数据”。你可以在视图中自己这样做(很多时候为了渲染性能原因而这样做),但情况并非总是如此。视图控制器不是这个检查的坏地方,所以我会像这样调整你的观察者:
var expression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) {
didSet {
if updateIsNecessary() {
updateUI() // Model changed, so update the View
}
}
}
updateIsNecessary()
显然确定视图是否实际需要更改,并且可能依赖于oldValue
和/或模型和视图数据之间的任何映射。现在,如果更改实际上源自视图(通知视图控制器,谁通知模型,谁现在再次通知视图控制器)应该没有任何必要更新,因为视图是一个制作首先是改变。
您可能会认为这会带来不必要的开销,但我怀疑性能影响实际上很大,因为它通常只是一些简单的检查。出于同样的原因,我通常也会在模型更新时进行类似的检查。
答案 3 :(得分:1)
赋予变量表达式应该与FaceView表示同步,这意味着表达式应该具有正确的值,即使FaceView是从我们的以外的输入设置的表达式,反之亦然,您可以简单地确保调用 updateUI iff 表达式的newValue与其oldValue不同。这将避免从FaceView到表达式到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()
}
}
}