你好,歡迎來到IOS教程網

WWDC

編輯:IOS開發基礎

QQ截圖20160621110945.png

本文授權轉載,作者:Prayer_(微博)

本文為 WWDC 2016 Session 419 的部分內容筆記。強烈推薦觀看。

設計師來需求了

在我們的 App 中,通常需要自定義一些視圖。例如下圖:

251995-fdba841f4f36ea3f.jpg

我們可能會在很多地方用到右邊為內容,左邊有個裝飾視圖的樣式,為了代碼的通用性,我們在 UITableViewCell 的基礎上,封裝了一層 DecoratingLayout,然後再讓子類繼承它,從而實現這一類視圖。

class DecoratingLayout : UITableViewCell {
    var content: UIView
    var decoration: UIView
    // Perform layout...
}

重構

但是代碼這樣組織的話,因為繼承自 UITableViewCell,所以對於其他類型的 view 就不能使用了。我們開始重構。

251995-570d228aa894ae61.jpg

我們需要讓視圖布局的功能獨立與具體的 view 類型,無論是 UITableViewCell、UIView、還是 SKNode(Sprite Kit 中的類型)

struct DecoratingLayout {
    var content: UIView
    var decoration: UIView
    mutating func layout(in rect: CGRect) {
        // Perform layout...
    }
}

這裡,我們使用結構體 DecoratingLayout 來表示這種 layout。相比於之前的方式,現在只要在具體的實現中,創建一個 DecoratingLayout 就可以實現布局的功能。代碼如下:

class DreamCell : UITableViewCell {
   ...
    override func layoutSubviews() {
        var decoratingLayout = DecoratingLayout(content: content, decoration: decoration)
        decoratingLayout.layout(in: bounds)
    }
}
class DreamDetailView : UIView {
   ...
    override func layoutSubviews() {
        var decoratingLayout = DecoratingLayout(content: content, decoration: decoration)
        decoratingLayout.layout(in: bounds)
    }
}

注意觀察上面的代碼,在 UITableViewCell 和 UIView 類型的 view 中,布局功能和具體的視圖已經解耦,我們都可以使用 struct 的代碼來完成布局功能。

通過這種方式實現的布局,對於測試來說也更加的方便:

func testLayout() {
    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(child1.frame, CGRect(x: 0, y: 5, width: 35, height: 30))
    XCTAssertEqual(child2.frame, CGRect(x: 35, y: 5, width: 70, height: 30))
}

我們的野心遠不止於此。這裡我們也想要在 SKNode 上使用上面的布局方式。看如下的代碼:

struct ViewDecoratingLayout {
    var content: UIView
    var decoration: UIView
    mutating func layout(in rect: CGRect) {
        content.frame = ...
        decoration.frame = ...
    }
}
struct NodeDecoratingLayout {
    var content: SKNode
    var decoration: SKNode
    mutating func layout(in rect: CGRect) {
        content.frame = ...
        decoration.frame = ...
    }
}

注意觀察上面的代碼,除了 content 和 decoration 的類型不一樣之外,其他的都是重復的代碼,重復就是罪惡

那麼我們如何才能消除這些重復代碼呢?在 DecoratingLayout 中,唯一用到 content 和 decoration 的地方,是獲取它的 frame 屬性,所以,如果這兩個 property 的類型信息中,能夠提供 frame 就可以了,於是我們想到了使用 protocol 作為類型(type)來使用。

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 = ...
    }
}

為了能夠在使用 DecoratingLayout 的時候傳入 UIView 和 SKNode,我們需要讓它們遵守 Layout 協議,只需要像下面這樣聲明一下就可以了,因為二者都已滿足協議的要求。

extension UIView: Layout {}
extension SKNode: Layout {}

這裡講一點我自己的理解,DreamCell 和 DreamDetailView 中能夠使用同一套布局代碼,是因為傳遞進去的 view 都擁有公共的父類 UIView,它提供了 frame 信息,而 UIView 和 SKNode 則不行,這裡我們使用 protocol 作為類型參數,可以很好的解決這一問題。

引入范型

然而,目前的代碼中是存在一個問題的,content 和 decoration 的具體類型信息在實際中可能是不一致的,因為這裡我們只要求了它們的類型信息中提供 frame 屬性,而並沒有規定它們是相同的類型,例如 content 可能是 UIView 而 decoration 是 SKNode 類型,這與我們的期望是不符的。

這裡我們可以通過引入范型來解決:

struct DecoratingLayout {
    var content: Child
    var decoration: Child
    mutating func layout(in rect: CGRect) {
        content.frame = ...
        decoration.frame = ...
    }
}

通過使用范型,我們就保證了 content 和 decoration 類型相同。

需求又來啦

設計師說,來,小伙子,完成下面的布局。

251995-81db74f85990f692.png

為了實現上圖的效果,我們仿照之前的寫法,實現如下代碼:

struct CascadingLayout {
    var children: [Child]
    mutating func layout(in rect: CGRect) {
        ...
    }
}

251995-b4f02a0c1995874e.png

struct DecoratingLayout {
    var content: Child
    var decoration: Child
    mutating func layout(in rect: CGRect) {
        content.frame = ...
        decoration.frame = ...
    }
}

這裡我又將前面的代碼拿了過來,方便查看。

我們將上面的兩種布局方式組合起來,就可以得到下面的效果:

251995-df4a560296f4ae6b.png

組合優於繼承

那麼如何才能將兩種布局方式組合起來呢?

來觀察我們之前定義的協議 Layout,其實我們關心的並不是 Layout 中的 frame,我們的目的是,讓 Layout 能夠在特定的上下文中進行相應的布局,所以我們來修改代碼:

protocol Layout {
    mutating func layout(in rect: CGRect)
}

這裡 Layout 的語義變成了:該類型能夠在特定的 CGRect 中進行相應的布局。

同時我們也需要修改代碼:

extension UIView: Layout { ... }
extension SKNode: Layout { ... }

這裡省略了使用 UIView 和 SKNode 的 frame 來進行布局的代碼。

於是我們的代碼變成了:

struct DecoratingLayout : Layout { ... }
struct CascadingLayout : Layout { ... }

看到這裡可能有點暈,其實代碼表達的意思是,DecoratingLayout 遵循 Layout 協議,而它的 content 和 decoration 兩個 property 也同樣遵循該協議,即可以在特定的 CGRect 中完成布局操作。而兩個結構體本身就包含 layout 操作,所以不需要任何其他的代碼,結構體做的事情就是,在自己進行 layout 操作的基礎上,將其傳遞給兩個 property 然後分別進行 layout,這就完成了組合

組合之後的執行代碼如下:

let decoration = CascadingLayout(children: accessories) // 左邊
var composedLayout = DecoratingLayout(content: content, decoration: decoration) // 整體
composedLayout.layout(in: rect) // 執行 layout 操作

On step further

251995-fdea172eeddd45cd.png

注意觀察上面的視圖,視圖是有層次結構的,所以我們需要在布局的時候,能夠拿到這個子視圖數組,之前的視實現方式中,只能布局單個的視圖,沒有辦法拿到整個視圖數組進行操作。

我們來修改 Layout 的代碼:

protocol Layout {
    mutating func layout(in rect: CGRect)
    var contents: [Layout] { get }
}

這裡增加了一個可讀屬性,返回一個 Layout 數組。同樣,這裡的代碼存在一個問題,contents 可以為不同的 Layout 類型,例如 [UIView(), SKNode()],所以為了讓 contents 中的類型一致,我們使用 associatedtype,將上面的代碼改寫為:

protocol Layout {
    mutating func layout(in rect: CGRect)
    associatedtype Content
    var contents: [Content] { get }
}

相應的 struct 改為:

struct ViewDecoratingLayout : Layout {
   ...
   mutating func layout(in rect: CGRect)
   typealias Content = UIView
   var contents: [Content] { get }
}
struct NodeDecoratingLayout : Layout {
   ...
   mutating func layout(in rect: CGRect)
   typealias Content = SKNode
   var contents: [Content] { get }
}

重復就是罪惡啊!可以看到,這裡唯一的不同只是 Content 的類型信息。這裡我們還是利用強大的范型來解決:

struct DecoratingLayout : Layout {
   ...
   mutating func layout(in rect: CGRect)
   typealias Content = Child.Content
   var contents: [Content] { get }
}

這裡,當 Child 范型確定的時候,Child.Content 的類型信息也相應地確定了,所以可以使用上面的代碼來消除重復。

范型牛逼!*3

別激動的太早,我們的代碼中還存在一個問題。目前我們的代碼長這樣:

struct DecoratingLayout : Layout {
    var content: Child
    var decoration: Child
    mutating func layout(in rect: CGRect)
    typealias Content = Child.Content
    var contents: [Content] { get }
}

這裡的 content 和 decoration 使用的是同樣的 layout 方式,這與我們的預期是不符的。我們的需求時視圖左邊和右邊使用不同的布局方式。然而我們又需要這個范型的方式來保證它們倆實際的數據類型是相同的,這裡需要使用兩個范型信息,但是限制它們的實際數據類型相同。修改後的代碼如下:

struct DecoratingLayout : Layout {
    var content: Child
    var decoration: Decoration
    mutating func layout(in rect: CGRect)
    typealias Content = Child.Content
    var contents: [Content] { get }
}

以上。

再一次,推薦你在寫 Swift 中定義新類型的時候,把 class 拋在腦後,嘗試著從 struct 和 protocol 開始。

Happy Hacking!

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved