π 2019.12.15 (SUN)
WWDC2016 Session : Swift Category :
π
Protocol and Value Oriented Programming in UIKit Apps - WWDC 2016 - Videos - Apple Developer
β When you look at the code right in front of you, you don't have to think about how the rest of your code interacts with that one function.
- Model stores data
- View presents that data
- Controller coordinates between the two
Think Different Protocol-Oriented Programming in Swift(2015) Building Better Apps with Value Types in Swift(2015)
https://developer.apple.com/library/archive/LucidDreams/Introduction/Intro.html
// Reference Semantics
class Dream {
var descriptions: String
var creature: Creature
var effects: Set<Effect>
...
}
var dream1 = Dream(...)
var dream2 = dream1
dream2.description = "Unicorn all over"
Classes have reference semantics, meaning that references to the same instance share their storage and that sharing is implicit.
if someone tries to modify dream2's description, If we only care about dream1, we may be surprised the variable's value changed from underneath our control.
And this really hurts local reasoning.
β Just test and try the dream type on its own! By making dream type a struct which has value semantics.
// Value Semantics
struct Dream {
var description: String
var creature: Creature
var effects: Set<Effect>
...
}
var dream1 = Dream(...)
var dream2 = dream1
dream2.description = "Unicorns all over"
This means each variable has independent storage. So changing the value in one doesn't change the value in the other.
This guarantees us that dreams aren't involved in the complicated relationships that we saw earlier. And so this really improves our ability to reason locally because no code can change the value we're using from underneath our control.
DecorationLayoutCell
We couldn't reuse our layout cell outside of tableView.
DecoratinLayoutCell
// View Layout
struct DecoratingLayout {
var content: UIView
var descoration: UIView
mutating func layout(in rect: CGRect) {
// Perform layout...
}
}
class DreamCell: UITableViewCell {
...
override func layoutSubviews() {
var descoratingLayout = DecoratingLayout(content: content, decoration: decoration)
decoratingLayout.layout(in: bounds)
}
}
```Swift
class DreamDetailView: UIView {
...
override func layoutSubviews() {
var decoratingLayout = DecoratingLayout(content: content, decoration: decoration) {
decoratingLayout.layou(in: bounds)
}
}
```
Layout logic is decoupled from tableViewCells, we can use it in any UIView.
Layout can be used in isolation, it's really easy for unit test.
func testLayout() {
let child1 = UIView()
let child2 = UIView()
var layout = DecorationLayout(content: child1, decoration: child2)
layout.layout(in: CGRect(x: 0, y: 0, width: 120, height: 40))
XCTAssertEqual(child1.frame, CGRect(x: 0, y: 5, width: 35, height: 30))
XCTAssertEqual(child2.frame, CGRect(x: 0, y: 5, width: 70, height: 30))
}
// Layout
struct DecorationLayout<Child: Layout> {
var conetent: Child
var decoration: Child
mutating func layou(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
protocol Layout {
var frame: CGRect { get set }
}
extension UIView: Layout {}
extension SKNode: Layout {}
We can use retroactive modeling to make UIView
and SKNode
conform to our new protocol.
add this functionality to unrelated types to use both of them.
// SpriteKit Layout
struct NodeDecoratingLayout {
var content: SKNode
var decoration: SKNode
mutating func layou(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
- More control over types
- Can be optimized more at compile time
Related: Understanding Swift Performance(2016)
How can we share code instead?
inheritance
β But with inheritance, you have both your code, also have to consider what your superclass might be doing and what your subclasses might want to change or override.
You mind has to pull together a large amount of code that's spread across your app. A lot of the time you also inherit from a framework class, like UIView
or ViewController
and there's orders of magnitude more code there.
Composition
Share code without reducing local reasoning
When composing, you can understand those independent pieces in isolation.
Also enforce encapsulation without worrying about subclasses or superclasses poking holes in your abstractions.
- Classes instances are expensive! When you make another object, you have an extra heap allocation and this is even worse with view.
- Structs are cheap
- Composition is better with value semantics
- Local reasoning with value types
- Generic type for fast, safe polymorhpism
- Composition of values
// DreamListViewController - Isolating the Model
class DreamListViewController: UITableViewController {
var model: Model
...
}
struct Model: Equtable {
var dreams:[Dreams]
var favoriteCreature: Creature
}
In the first undo step, we're going to remove the dream that the user just added and then we're going to delete the row in that tableView.
And we can continue with the next undo step.
This approach of mutating individual model properties and updating our view independently is really easy to get wrong and that's because you need to match the change in the model the change in the view, precisely.
How we going to update UI?
And so in our ViewController whenever a model changes, we're going to call modelDidChange
method.
// DreamListViewController: Isoalting the Model
class DreamListViewController: UITableViewController {
...
func modelDidChange(old: Model, new: Model) {
if olf.favoriteCreature != new.favoriteCreature {
// Reload table view section for favorite creature.
tableView.realoadSections(...)
}
...
undoManager?.registerUndo(withTargets: self, handler: { target in
target.model = old
})
}
}
- Single code path
- Better local reasoning
- Values compose well with other values
Model β Making the Dream type a struct. And this makes it easier for us to locally reason about our code because there's no implicit sharing of our dream variables.
View β Components took advantage of protocol in generics to make sure that the generic components were reusable with views, SpriteKit nodes, and image rendering. And all of this led to better local reasoning since each of these types were small, testable, and isolated value types.
- Customization through coposition
- Protocols for generic, reusable code
- Taking advantage of value semantics
- Local reasoning