ValueType怎样用到UIView中?
如果两个类没有相同的父类,我们又想对它们写一个相同的逻辑,怎么办?我们可以让它们遵守同一个协议,该协议中有它们共有的属性,然后就可以复用了。这样我们就依赖Protocol而不是SuperClassl来建立一个多态。
分享代码不必使用继承了:
UIView的组合不是最优的,因为UIView的创建需要很长的时间,并且需要开辟堆空间,需要处理事件。
我们接下来看看怎样利用ValueType将Layout和具体的类进行分开。因为我们可能会用UIView
也可能用SKNode
来做其ContentView。
按照之前的情况,我们可能这样写:
class DecoratingLayoutCell : UITableViewCell {
var content: UIView
var decoration: UIView
// Perform layout...
}
为了将具体的Cell和其Layout分开,我们创建一个Struct:
struct DecoratingLayout {
var content: UIView
var decoration: UIView
mutating func layout(in rect: CGRect) {
// Perform layout...
}
}
然后在Cell中我们就可以这样写:
class DreamCell : UITableViewCell {
...
override func layoutSubviews() {
var decoratingLayout = DecoratingLayout(content: content, decoration:decoration)
decoratingLayout.layout(in:bounds)
}
同时,我们可以对UIView也用同样的Layout(封装变化)。
class DreamDetailView : UIView {
...
override func layoutSubviews() {
var decoratingLayout = DecoratingLayout(content: content, decoration:decoration)
decoratingLayout.layout(in:bounds)
} }
将Layout分开之后还非常有利于单元测试,比如我们想要测试我们的Layout创建出来的两个View是否符合我们的预期,我们可以这样:
let child1 = UIView()
let child2 = UIView()
var layout = DecoratingLayout(content: child1, decoration: child2)
layout.layout(in: CGRect(x: 0, y: 0, width: 120, height: 40))
}
XCTAssertEqual(child2.frame, CGRect(x: 0, y: 5, width: 35, height: 30))
XCTAssertEqual(child2.frame, CGRect(x: 35, y: 5, width: 70, height: 30))
因为我们的Layout代码量很小并且和其它代码是隔离的(没有继承,不是Reference Type),那么这样我们就可以很好的做局部分析。
有一天,我们要对SKNode也做同样的操作。所以我们也需要NodeDecoratingLayout
:
struct ViewDecoratingLayout {
var content: UIView
var decoration: UIView
mutating func layout(in rect: CGRect) {
// Perform layout...
}
}
struct NodeDecoratingLayout {
var content: SKNode
var decoration: SKNode
mutating func layout(in rect: CGRect) {
// Perform layout...
content.frame = ....
decoration.frame = ...
}
}
我们可以看到这和VIewDecoratingLayout
是一样的。所以我们要将它封装起来,怎么封装?UIView和SKNode没有共同的父类。因为这两个结构体都有一个属性,那么我们就用一个Layout的Protocol
来解决这个问题:
protocol Layout {
var frame: CGRect {get set}
}
struct DecoratingLayout {
var content: Layout
var decoration: Layout
mutating func layout(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
然后为了让UIView
和SKNode
都是用这个Layout
,我们只用Protocol Extension
。
extension UIView : Layout{}
extension SKNode : Layout{}
我们用Protocol和Extension实现了多态,而没有使用继承!
这样我们的Layout就不必依赖于UIKit了。
但是上面的代码有一个Bug,我们不能保证content
和decoration
是相同的类型,他们可能一个是UIView
,一个是SKNode
。我们可以使用泛型来解决这个问题。我们可以给DecoratingLayout
添加泛型:
struct DecoratingLayout<Child: Layout> {
var content: Child
var decoration: Child
mutating func layout(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
这样我么就可以保证了,content和decoration有相同的类型。使用泛型,可以让编译器对代码有一个更好的理解,然后会对代码做很多的优化。
但是如果出现了更加相似的布局,我们该怎样重用我们的代码呢?比如:
重用代码,我们最先想到的是继承,但是继承会造成Override及变量的变更,这样我们很难从子类中就明确的推断出代码的含义,那么我们用组合来做这个事情,面对上面的第二张图,我们通常的做法是,创建两个UIView:
但是这样做有弊端的:类对象是很消耗性能的:开辟堆空间,内存管理,除此之外,UIView还有:绘制,事件处理。然而Struct是几乎不消耗性能,并且由于其实Value Type,所以它可以做更好的封装,你不需要关系别人对其进行修改。
我们可以使用如下方式:
struct CascadingLayout<Child : Layout> {
var children: [Child]
mutating func layout(in rect: CGRect) {
... }
}
struct DecoratingLayout<Child : Layout> {
var content: Child
var decoration: Child
mutating func layout(in rect: CGRect) {
content.frame = ...
decoration.frame = ...
}
}
这两种方式组合,这里我们不需要Layout
协议有frame属性,我们只需要其有layout()
方法即可
protocol Layout {
mutating func layout(in rect: CGRect)
}
然后,我们的DecoratingLayout
和CascadingLayout
遵守该协议即可:
struct DecoratingLayout<Child : Layout, ...> : Layout {}
struct CascadingLayout<Child : Layout> : Layout {}
这之后我们就可以用组合的方式实现上文提到的UI效果了:
// Composition of Values
let decoration = CascadingLayout(children: accessories)
var composedLayout = DecoratingLayout(content: content, decoration: decoration)
composedLayout.layout(in: rect)
为了实现这种UIView的叠加效果,我们给Layout协议添加contents
属性:
protocol Layout {
mutating func layout(in rect: CGRect)
var contents:[Layout]{get} // 这里可以是UIView或者SKNode
}
但是这回出现和之前所说的一样的Bug,我们没有办法保证这个contents属性里是否既有UIView还有SKNode,为了解决这个问题,我们加上一个关联属性:
protocol Layout {
mutating func layout(in rect: CGRect)
associatedtype Content
var contents:[Content]{get} // 这里可以是UIView或者SKNode
}
然后在DecoratingLayout
中,我们这样做:
struct DecoratingLayout<Child : Layout> : Layout {
...
mutating func layout(in rect: CGRect)
typealias Content = Child.Content
var contents: [Content] { get }
如果我们要对其中的内容做以限制,那么我们可以这样做:
struct DecoratingLayout<Child:Layout, Decoration:Layout where Child.Content == Decoration.Content> : Layout {
var content: Child
var decoration: Decoration
mutating func layout(in rect: CGRect)
typealias Content = Child.Content
var contents: [Content] { get }
}
这样我们的单元测试就不必依赖于具体的UIView或者是SKNode:
func testLayout() {
let child1 = TestLayout()
let child2 = TestLayout()
var layout = DecoratingLayout(content: child1, decoration: child2)
layout.layout(in: CGRect(x: 0, y: 0, width: 120, height: 40))
XCTAssertEqual(layout.contents[0].frame, CGRect(x: 0, y: 5, width: 35, height: 30))
XCTAssertEqual(layout.contents[1].frame, CGRect(x: 35, y: 5, width: 70, height: 30))
}
struct TestLayout : Layout {
var frame: CGRect
...
}
ValueType怎样用到Controller中
我们增加了Favoriate功能之后,我们的撤销功能消失了。为了隔离我们的Model,我们将其变为一个Struct,并且拥有之前的属性:
class DreamListViewController : UITableViewController {
var model: Model
...
}
struct Model : Equatable {
var dreams: [Dream]
var favoriteCreature: Creature
}
然后将Model和View隔离的做法极其容易出bug,因为我们的Model的任何改变都要对应相应View的位置。
我们可以将其合并起来:
/// Diffs the model changes and updates the UI based on the new model.
private func modelDidChange(diff: Model.Diff) {
// Check to see if we need to update any rows that present a dream.
if diff.hasAnyDreamChanges {
switch diff.dreamChange {
case .inserted?:
let indexPath = IndexPath(row: diff.from.dreams.count, section: Section.dreams.rawValue)
tableView.insertRows(at: [indexPath], with: .automatic)
case .removed?:
let indexPath = IndexPath(row: diff.from.dreams.count - 1, section: Section.dreams.rawValue)
tableView.deleteRows(at: [indexPath], with: .automatic)
case .updated(let indexes)?:
let indexPaths = indexes.map { IndexPath(row: $0, section: Section.dreams.rawValue) }
tableView.reloadRows(at: indexPaths, with: .automatic)
case nil: break
}
}
如果我们的UI有各种状态,并且我们要处理这种状态,这时,该怎样处理才比较好呢?比如我们的这个App中有:展示、选择、分享三种状态。这时如果遇到状态的切换就会遇到问题,因为Cell是复用的,所以就会出现忘记清除前一种Cell的状态而引起的Bug。
这种情况出现的原因就是我们的ViewController持有了这些不同的状态,如果状态逐渐变多,那么就会有非常复杂的业务逻辑需要处理。
class DreamListViewController : UITableViewController {
var isInViewingMode: Bool
var sharingDreams: [Dream]?
var selectedRows: IndexSet?
...
}
因为UI只能出现一种状态,所以每次我们切换状态,我们就需要给其它的两种状态进行置空,这时就很容易引起Bug,为了解决这个问题,我们可以使用Enum
来讲这些状态进行封装。
enum State {
case viewing
case sharing(dreams:[Dream])
case selecting(selectedRows: IndexSet)
}
这样我们就可以避免因为状态转换而产生的Bug了。