I learned Swift from the CS193P class. It recommends the following API for a ViewController FaceViewController
to update its view FaceView
:
var expression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) { didSet { updateUI() // Model changed, so update the View } }
However, I have not seen an extension of this concept for when a view updates its own model. For example this does not make sense:
// 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 }
In Objective-C, this was very straightforward because you could use getters and setters as your public API and the backing store as your private state. Swift can use calculated variables to use this approach as well but I believe the Swift designers have something different in mind.
So, what is an appropriate way for a view controller to represent state changes in response to view updates, while also exposing a reasonable read/write API for others to inspect its state?
4 Answers
Answers 1
I also watched the cs193p
Winter 2017 videos. For the FaceIt
app, the mdoel
need to be translated to how it will be displayed on the view
. And it's not 1 to 1
translation, but more like 3 to 2
or something like that. That's why we have helper method updateUI(_:)
.
As for the question on how the view controller
would update model
based on the change in the view
. In this example, we couldn't update the model
as we need to figure out how to map 2 values to 3 values
? If we want persistence, we could just stored the view state
in core data
or userDefaults
.
In a more general settings, where the model change need to update the view
and view change need to update the model
, then we'll need to have indirection
to avoid the cycle
like you envision.
For example, since the FacialExpression
is a value type. We could have something like:
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 } }
}
Then in your imaginary delegate UIFaceViewDelegate
we could have the following:
// 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
}
Answers 2
Following is my test code:
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") } }
But I get "testView didSet" in console output when button is clicked. What's the difference with your implement?
Answers 3
Hange's solution is good, although it does not work for reference types, as they said. It also introduces another, basically redundant (private) variable, mimicking Objective-C's way of distinguishing between properties and backing member variables. This is a matter of style, personally I would mostly try to avoid that (but I have done the same thing Hange suggests, too).
My reason is that for reference types you need to do this differently anyways and I try to avoid following too many different coding patterns (or having too many redundant variables).
Here's a different proposal:
The important point is to break the cycle at some point. I usually go by "only inform your delegates if you actually did change something about your data". You can do so either in the view's themselves (many do so anyways for rendering performance reasons), but that's not always the case. The view controller isn't a bad place for this check, so I would adapt your observer like this:
var expression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) { didSet { if updateIsNecessary() { updateUI() // Model changed, so update the View } } }
updateIsNecessary()
obviously determines whether the view actually needs to be changed or not and might rely on oldValue
and/or whatever mapping you have between model and view data. Now if the change actually originated from the view in the first place (which informed the view controller, who informed the model, who now informs the view controller again) there shouldn't be anything necessary to update, as the view was the one making a change in the first place.
You might argue that this introduces unnecessary overhead, but I doubt the performance hit is actually big, as it's usually just some easy checks. I usually have a similar check when the model gets updated, too, for the same reasons.
Answers 4
Giving the variable expression should be in sync with the FaceView representation meaning expression should have the correct value even if FaceView was set from an input other than our expression and vice versa you can simply make sure updateUI is called iff expression 's newValue is different from its oldValue. This will avoid the recursive call from FaceView to expression to updateUI and back to FaceView
var expression: FacialExpression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) { didSet { if expression != oldValue { updateUI() } } }
This means FacialExpression should conform to Equatable which you can do by overloading == operator.
public extension FacialExpression: Equatable { static func ==(lhs: FacialExpression, rhs: FacialExpression) -> Bool { return lhs == rhs // TODO: Logic to compare FacialExpression } }
I didn't use a proper text editor so pardon me if I've made typos
EDIT:
There will be one unnecessary update of FaceView with the same expression value when expression is set from the imaginary delegate first time with a different value but there won't be anymore repetition since expression will then be in sync.
To avoid even that, you can compare expression with another expression property in FaceView that holds the current expression.
var expression: FacialExpression = FacialExpression(eyes: .Closed, eyeBrows: .Relaxed, mouth: .Smirk) { didSet { if expression != faceView.currentExpression { updateUI() } } }
0 comments:
Post a Comment