Swift:比 UICollectionView更好用的IGListKit教程

原文:IGListKit Tutorial: Better UICollectionViews
作者:Ryan Nystrom
譯者:kmyhy

每個(gè) app 都以同樣的方式開始:幾個(gè)界面,幾顆按鈕,一兩個(gè) list。但隨著進(jìn)度的進(jìn)行以及 app 膨脹,功能開始發(fā)生變化。你簡(jiǎn)單的數(shù)據(jù)源開始在工期和產(chǎn)品經(jīng)理的壓力下變得支離破碎。再過一久,你留下一堆龐大得難以維護(hù)的 view controller。今天,IGListKit 來拯救你了!

IGListKit 專門用于解決在使用 UICollectionView 時(shí)出現(xiàn)的功能蔓延(需求蔓延)和 view controller 膨脹的問題。用 IGListKit 創(chuàng)建列表,你可以使用非耦合組件來構(gòu)建 app,飛快的刷新,支持任何類型的數(shù)據(jù)。

本教程中,你將重構(gòu)一個(gè) UICollectionView 成 IGListKit,然后擴(kuò)展 app,讓它超凡脫俗!

開始

如果你是 NASA 的頂尖軟件工程師,正在從事最新的火星載人飛行任務(wù)。開發(fā)團(tuán)隊(duì)已經(jīng)做好了第一版的 MarsLink app,你可以在[這里](https://koenig-media.raywenderlich.com/uploads/2016/12/Marslink_Starter.zip)下載。下載完這個(gè)項(xiàng)目,打開 Marslink.xcworkspace 并運(yùn)行 app。

image

這個(gè) app 簡(jiǎn)單地列出了宇航員的飛行日志。
你的任務(wù)是當(dāng)團(tuán)隊(duì)需要新功能時(shí),添加新功能給這個(gè) app。打開 Marslink\ViewControllers\ClassicFeedViewController.swift 隨便看看,熟悉一下項(xiàng)目。如果你用過 UICollectionView,你會(huì)發(fā)現(xiàn)它非常普通:

  • ClassicFeedViewController 繼承了 UIViewController ,并用一個(gè)擴(kuò)展實(shí)現(xiàn) 了 UICollectionViewDataSource 協(xié)議。
  • viewDidLoad() 中創(chuàng)建了一個(gè) UICollectionView, 注冊(cè)了 cell,設(shè)置了數(shù)據(jù)源,然后將它添加到視圖樹中。
  • loader.entries 數(shù)組保存了幾個(gè) section,每個(gè) section 中有兩個(gè) cell(一個(gè)日期,一個(gè)文字)。
  • 日期 cell 顯示陽歷的日期,文本 cell 顯示日志內(nèi)容。
  • collectionView(_:layout:sizeForItemAt:) 方法返回一個(gè)固定的大小用于日期 cell,以及一個(gè)根據(jù)字符串大小計(jì)算出來的 size 給文本 cell。

每件事情都很完美,但是項(xiàng)目 leader 帶來了一個(gè)緊急的產(chǎn)品升級(jí)需求:

在火星上,一名宇航員擱淺了。我們需要添加一個(gè)天氣預(yù)報(bào)模塊和實(shí)時(shí)聊天。你只有48小時(shí)的時(shí)間。

image

JPL(噴氣推進(jìn)實(shí)驗(yàn)室,Jet Propulsion Laboratory) 的工程師要用到這些功能,但需要你將他們放到這個(gè) app 中來。

如果把一名宇航員帶回家的壓力還不夠大的話,NASA 的首席設(shè)計(jì)師還有一個(gè)需求,app 中每個(gè)子系統(tǒng)的升級(jí)必須是的平滑的,也就是不能 reloadData()。

你怎么會(huì)以為將這些模塊集成到一個(gè)偉大的 app 中并創(chuàng)建所有的轉(zhuǎn)換動(dòng)畫?這名宇航員已經(jīng)沒有多少土豆了!

IGListKit 介紹

UICollectionView 是一個(gè)極其強(qiáng)大的工具,與其強(qiáng)大一起的是負(fù)有同樣大的責(zé)任。保持你的數(shù)據(jù)源和視圖同步是極其重要的,通常崩潰就是因?yàn)檫@里沒搞好。

IGListKit 是一個(gè)數(shù)據(jù)驅(qū)動(dòng)的 UICollectionView 框架,有 Instagram 團(tuán)隊(duì)編寫。使用這個(gè)框架,你提供一個(gè)對(duì)象數(shù)組用于顯示到 UICollectionView 中。對(duì)于每種類型的對(duì)象,需要?jiǎng)?chuàng)建一個(gè) adapter,也叫做 section controller,里面包含了所有創(chuàng)建 cell 所需要的細(xì)節(jié)。

image

IGListKit 自動(dòng)識(shí)別你的對(duì)象并在任何東西發(fā)生變化時(shí)在 UICollectonView 上執(zhí)行批量動(dòng)畫刷新。這樣,你就永遠(yuǎn)不需要編寫 batch update 語句,避免這里列出的警告。

將 UICollectionView 換成 IGListKit

IGListKit 負(fù)責(zé)所有識(shí)別 collection 中發(fā)生變化,并以動(dòng)畫方式刷新對(duì)應(yīng)的行。它還能夠輕易處理針對(duì)不同的 section 使用不同的 data 和 UI 的情況??紤]到這一點(diǎn),它能夠完美解決當(dāng)前需求——讓我們開始吧!

在 Marslink.xcworkspace 打開的情況下,右擊 ViewControllers 文件夾并選擇 New File…。新建一個(gè) Cocoa Touch Class 繼承于 UIViewController 并命名為 FeedViewController。
打開 AppDelegate.swift 找到 application(_:didFinishLaunchingWithOptions:) 方法。找到將ClassicFeedViewController() push 到 navigation controller 的行,將它換成:

nav.pushViewController(FeedViewController(), animated: false)

FeedViewController 現(xiàn)在成為了 root view controller。你可以保留 ClassicFeedViewController.swift 作為參考,但 FeedViewController 將作為你使用 IGListKit 實(shí)現(xiàn)一個(gè) collection view 的地方。

運(yùn)行程序,確保你能看到一個(gè)嶄新的、空白的 view controller shows。

image

添加 Journal loader

打開 FeedViewController.swift 在 FeedViewController 頂部添加屬性:

let loader = JournalEntryLoader()

JournalEntryLoader 是一個(gè)類,用于加載一個(gè)硬編碼的日志記錄到一個(gè)數(shù)組中。

在 viewDidLoad() 最后一行添加:

loader.loadLatest()

loadLatest() 是 JournalEntryLoader 中的方法,加載最新的日志記錄。

加入 collection view

現(xiàn)在來添加某些 IGListKit 的特殊控件到 view controller 中了。在這樣做之前,你需要引入這個(gè)框架。在 FeedViewController.swift 頂部加入 import 語句:

import IGListKit

注意:本示例項(xiàng)目使用 CocoaPods 管理依賴。IGListKit 是 Objective-C 些的,因此需要在橋接頭文件中用 #import 手動(dòng)添加到你的項(xiàng)目。

在 FeedViewController 頂部添加一個(gè) collectionView 常量:

// 1
let collectionView: IGListCollectionView = {
  // 2
  let view = IGListCollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
  // 3
  view.backgroundColor = UIColor.black
  return view
}()
  1. IGListKit 使用了 IGListCollectionView, 這是一個(gè) UICollectionView 的子類,添加了某些功能并修復(fù)了某些缺陷。
  2. 一開始用一個(gè)大小為 0 的 frame,因?yàn)?view 都還沒創(chuàng)建。它也使用了 UICollectionViewFlowLayout ,就像 ClassicFeedViewController 一樣。
  3. 背景色設(shè)為 NASA-認(rèn)可的黑色。

在 viewDidLoad() 方法最后一句加入:

view.addSubview(collectionView)

這將新的 collectionView 添加到 controller 的 view。
在 viewDidLoad() 下面加入:

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  collectionView.frame = view.bounds
}

viewDidLayoutSubviews() 是一個(gè)覆蓋方法,將 collectionView的 frame 設(shè)為view 的 bounds。

IGListAdapter 和數(shù)據(jù)源

使用 UICollectionView,你需要某個(gè)數(shù)據(jù)源實(shí)現(xiàn) UICollectionViewDataSource 協(xié)議。它的作用是返回 section 和 row 的數(shù)目以及每個(gè) cell。
在 IGListKit 中,你使用一個(gè) GListAdapter 來控制 collection view。你仍然需要一個(gè)數(shù)據(jù)源來實(shí)現(xiàn) IGListAdapterDataSource 協(xié)議,但不是返回?cái)?shù)字或 cell,你需要提供數(shù)組和 controllers(后面會(huì)細(xì)講)。

首先,在 FeedViewController.swift 在頭部加入:

lazy var adapter: IGListAdapter = {
  return IGListAdapter(updater: IGListAdapterUpdater(), viewController: self, workingRangeSize: 0)
}()

這創(chuàng)建了一個(gè)延遲加載的 IGListAdapter 變量。這個(gè)初始化方法有 3 個(gè)參數(shù):

  1. updater 是一個(gè)實(shí)現(xiàn)了 IGListUpdatingDelegate 協(xié)議的對(duì)象, 它負(fù)責(zé)處理 row 和 section 的刷新。IGListAdapterUpdater 有一個(gè)默認(rèn)實(shí)現(xiàn),剛好給我們用。
  2. viewController 是一個(gè) UIViewController ,它擁有這個(gè) adapter。 這個(gè) view controller 后面會(huì)用于導(dǎo)航到別的 view controllers。
  3. workingRangeSize 是 warking range 的大小。允許你為那些不在可見范圍內(nèi)的 section 準(zhǔn)備內(nèi)容。

注意:working range 是另一個(gè)高級(jí)主題,本教程不會(huì)涉及。但是在 IGListKit 的代碼庫中有它豐富的文檔甚至一個(gè)示例 app。

在 viewDidLoad() 方法最后一行添加:

adapter.collectionView = collectionView
adapter.dataSource = self

這會(huì)將 collectionView 和 adapter 聯(lián)系在一起。還將 self 設(shè)置為 adapter 的數(shù)據(jù)源——這會(huì)報(bào)一個(gè)錯(cuò)誤,因?yàn)槟氵€沒有實(shí)現(xiàn) IGListAdapterDataSource 協(xié)議。

要解決這個(gè)錯(cuò)誤,聲明一個(gè) FeedViewController 擴(kuò)展以實(shí)現(xiàn) IGListAdapterDataSource 協(xié)議。在文件最后添加:

extension FeedViewController: IGListAdapterDataSource {
  // 1
  func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] {
    return loader.entries
  }

  // 2
  func listAdapter(_ listAdapter: IGListAdapter, sectionControllerFor object: Any) -> IGListSectionController {
    return IGListSectionController()
  }

  // 3
  func emptyView(for listAdapter: IGListAdapter) -> UIView? { return nil }
}

FeedViewController 現(xiàn)在采用了 IGListAdapterDataSource 協(xié)議并實(shí)現(xiàn)了 3 個(gè)必須的方法:

  1. objects(for:) 返回一個(gè)數(shù)據(jù)對(duì)象組成的數(shù)組,這些對(duì)象將顯示在 collection view。這里返回了loader.entries,因?yàn)樗巳罩居涗洝?/li>
  2. 對(duì)于每個(gè)數(shù)據(jù)對(duì)象,listAdapter(_:sectionControllerFor:) 方法必須返回一個(gè)新的 section conroller 實(shí)例?,F(xiàn)在,你返回了一個(gè)空的 IGListSectionController以解除編譯器的抱怨——等會(huì),你會(huì)修改這里,返回一個(gè)自定義的日志的 section controller。
  3. emptyView(for:) 返回一個(gè) view,它將在 List 為空時(shí)顯示。NASA 給的時(shí)間比較倉促,他們沒有為這個(gè)功能做預(yù)算。

創(chuàng)建第一個(gè) Section Controller

一個(gè) section controller 是一個(gè)抽象的對(duì)象,指定一個(gè)數(shù)據(jù)對(duì)象,它負(fù)責(zé)配置和管理 CollectionView 中的一個(gè) section 中的 cell。這個(gè)概念類似于一個(gè)用于配置一個(gè) view 的 view-model:數(shù)據(jù)對(duì)象就是 view-model,而 cell 則是 view,section controller 則是二者之間的粘合劑。

在 IGListKit 中,你根據(jù)不同類型的數(shù)據(jù)的類型和特性創(chuàng)建不同的 section controller。JPL 的工程師已經(jīng)放入了一個(gè) JournalEntry model,你只需要?jiǎng)?chuàng)建能夠處理這個(gè) Model 的 section controller 就行了。

在 SectionController 文件夾上右擊,選擇 New File…,創(chuàng)建一個(gè) Cocoa Touch Class 名為 JournalSectionController ,繼承 IGListSectionController。

image

Xcode 不會(huì)自動(dòng)引入第三方框架,因此在 JournalSectionController.swift 需要添加:

import IGListKit

為 JournalSectionController 添加如下屬性:

var entry: JournalEntry!
let solFormatter = SolFormatter()

JournalEntry 是一個(gè) model 類,在實(shí)現(xiàn)數(shù)據(jù)源時(shí)你會(huì)用到它。SolFormatter 類提供了將日期轉(zhuǎn)換為太陽歷格式的方法。很快你會(huì)用到它們。

在 JournalSectionController 中,覆蓋 init() 方法:

override init() {
  super.init()
  inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
}

如果不這樣做,section 中的 cell 會(huì)一個(gè)緊挨著一個(gè)。這個(gè)方法在每個(gè) JournalSectionController 對(duì)象的下方增加 15 個(gè)像素的間距。

你的 section controller需要實(shí)現(xiàn) IGListSectionType 協(xié)議才能被 IGListKit 所用。在文件最后添加一個(gè)擴(kuò)展:

extension JournalSectionController: IGListSectionType {
  func numberOfItems() -> Int {
    return 2
  }

  func sizeForItem(at index: Int) -> CGSize {
    return .zero
  }

  func cellForItem(at index: Int) -> UICollectionViewCell {
    return UICollectionViewCell()
  }

  func didUpdate(to object: Any) {
  }

  func didSelectItem(at index: Int) {}
}

注意: IGListKit 非常依賴 required 協(xié)議方法。但在這些方法中你可以空實(shí)現(xiàn),或者返回 nil,以免收到“缺少方法”的警告或運(yùn)行時(shí)報(bào)錯(cuò)。這樣,在使用 IGListKit 時(shí)就不容易出錯(cuò)。

你實(shí)現(xiàn)了 IGListSectionType 協(xié)議的 4 個(gè) required 方法。

所有方法都是無存根的實(shí)現(xiàn),除了 numberOfItems() 方法— 返回了一個(gè) 2 ,表示一個(gè)日期和一個(gè)文本字符串。你可以回到 ClassicFeedViewController.swift 看看,在collectionView( _:numberOfItemsInSection:) 方法中你返回的也是 2。這兩個(gè)方法基本上是一樣的。

在 didUpdate(to:)方法中加入:

entry = object as? JournalEntry

didUpdate(to:) 用于將一個(gè)對(duì)象傳給 section controller。注意在任何 cell 協(xié)議方法之前調(diào)用。這里,你把接收到的 object 參數(shù)賦給 entry。

注意:在一個(gè) section controller 的生命周期中,對(duì)象有可能會(huì)被改變多次。這只會(huì)在啟用了 IGListKit 的更高級(jí)的特性時(shí)候發(fā)生,比如自定義模型的 Diffing 算法。在本教程中你不需要擔(dān)心 Diffing。

現(xiàn)在你有一些數(shù)據(jù)了,你可以開始配置你的 cell 了。將 cellForItem(at:) 方法替換為:

// 1
let cellClass: AnyClass = index == 0 ? JournalEntryDateCell.self : JournalEntryCell.self
// 2
let cell = collectionContext!.dequeueReusableCell(of: cellClass, for: self, at: index)
// 3
if let cell = cell as? JournalEntryDateCell {
  cell.label.text = "SOL \(solFormatter.sols(fromDate: entry.date))"
} else if let cell = cell as? JournalEntryCell {
  cell.label.text = entry.text
}
return cell

cellForItem(at:) 方法詢問到 section 的某個(gè) cell(指定的 Index)時(shí)調(diào)用。以上代碼解釋如下:

  1. 如果 index 是第一個(gè),返回 JournalEntryDateCell 單元格,否則返回 JournalEntryCell 單元格。日志數(shù)據(jù)總是先顯示日期,然后才是文本。
  2. 從緩存中取出一個(gè) cell,dequeue 時(shí)需要指定 cell 的類型,一個(gè) section controller 對(duì)象,以及 index。
  3. 根據(jù) cell 的類型,用你先前在 didUpdate(to objectd:)方法中設(shè)置的 entry 來配置 cell。

然后,將 sizeForItem(at:) 方法替換為:

// 1
guard let context = collectionContext, let entry = entry else { return .zero }
// 2
let width = context.containerSize.width
// 3
if index == 0 {
  return CGSize(width: width, height: 30)
} else {
  return JournalEntryCell.cellSize(width: width, text: entry.text)
}

collectionContext 是一個(gè)弱引用,同時(shí)是 nullabel 的。雖然它永遠(yuǎn)不可能為空,但最好是做一個(gè)前置條件判斷,使用 Swift 的 guard 語句就行了。

IGListCollectionContext 是一個(gè)上下文對(duì)象,保存了這個(gè) section view 中用到的 adapter、collecton view、以及 view controller。這里我們需要獲取容器 container 的寬度。

如果是第一個(gè) index(即日期 cell),返回一個(gè)寬度等于 container 寬度,高度等 30 像素的 size。否則,使用 cell 的助手方法根據(jù) cell 文本計(jì)算 size。

最后一個(gè)方法是 didSelectItem(at:),這個(gè)方法在點(diǎn)擊某個(gè) cell 時(shí)調(diào)用。這是一個(gè) required 方法,你必須實(shí)現(xiàn)它,但如果你不想進(jìn)行任何處理的話,可以空實(shí)現(xiàn)。

這種 dequeue 不同類型的 cell、對(duì) cell 進(jìn)行不同配置和并返回不同 size 的套路和你之前使用 UICollectionView 的套路并無不同。你可以回去 ClassicFeedViewController 看看,這些代碼中有許多都很相似!

現(xiàn)在你擁有了一個(gè) section controller,它接收一個(gè) JournalEntry 對(duì)象,并返回連個(gè) cell 和 size。接下來我們就來使用它。

打開 FeedViewController.swift, 將 listAdapter(_:sectionControllerFor:) 方法替換為:

return JournalSectionController()

現(xiàn)在,這個(gè)方法返回了新的 Journal Section Controller 對(duì)象。

運(yùn)行程序,你將看到一個(gè)航空日志的列表!

image

添加消息

JPL 工程師很高興你能這么快就完成了修改,但他們還需要和那個(gè)倒霉的宇航員建立聯(lián)系。他們要你盡快將消息模塊也集成進(jìn)去。

在添加任何視圖之前,首先的一件事情就是數(shù)據(jù)。
打開 FeedViewController.swift 添加一個(gè)屬性:

let pathfinder = Pathfinder()

PathFinder() 扮演了消息系統(tǒng),并代表了火星上宇航員的探路車。

在 IGListAdapterDataSource 擴(kuò)展中找到 objects(for:) ,將內(nèi)容修改為:

var items: [IGListDiffable] = pathfinder.messages
items += loader.entries as [IGListDiffable]
return items

你可能想起來了,這個(gè)方法負(fù)責(zé)將數(shù)據(jù)源對(duì)象提供給 IGListAdapter。這里進(jìn)行了一些修改,將 pathfinder.messages 添加到 items 中,以便為新的 section controller 提供消息數(shù)據(jù)。

注意:你必須轉(zhuǎn)換消息數(shù)組以免編譯器報(bào)錯(cuò)。這些對(duì)象已經(jīng)實(shí)現(xiàn)了 IGListDiffable 協(xié)議。

在 SectionControllers 文件夾上右擊,創(chuàng)建一個(gè)新的 IGListSectionController 子類名為 MessageSectionController。在文件頭部引入 IGListKit:

import IGListKit

讓編譯器不報(bào)錯(cuò)之后,保持剩下的內(nèi)容不變。
回到 FeedViewController.swift 修改 IGListAdapterDataSource 擴(kuò)展中的 listAdapter(_:sectionControllerFor:) 方法為:

if object is Message {
  return MessageSectionController()
} else {
  return JournalSectionController()
}

現(xiàn)在,如果數(shù)據(jù)對(duì)象的類型是 Message,,我們會(huì)返回一個(gè)新的 Message Secdtion Controller。

JPL 團(tuán)隊(duì)需要你在創(chuàng)建 MessageSectionController 時(shí)滿足下列需求:

  • 接收 Message 消息
  • 底部間距 15 像素
  • 通過 MessageCell.cellSize(width:text:) 函數(shù)返回一個(gè) cell 的 size
  • dequeue 并配置一個(gè) MessageCell,并用 Message 對(duì)象的 text 和 user.name 屬性填充 Label。

試試看!如果你需要幫助,JPL 團(tuán)隊(duì)也在下面的提供了參考答案:

答案: MessageSectionController

import IGListKit

class MessageSectionController: IGListSectionController {

  var message: Message!

  override init() {
    super.init()
    inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
  }
}

extension MessageSectionController: IGListSectionType {
  func numberOfItems() -> Int {
    return 1
  }

  func sizeForItem(at index: Int) -> CGSize {
    guard let context = collectionContext else { return .zero }
    return MessageCell.cellSize(width: context.containerSize.width, text: message.text)
  }

  func cellForItem(at index: Int) -> UICollectionViewCell {
    let cell = collectionContext?.dequeueReusableCell(of: MessageCell.self, for: self, at: index) as! MessageCell
    cell.messageLabel.text = message.text
    cell.titleLabel.text = message.user.name.uppercased()
    return cell
  }

  func didUpdate(to object: Any) {
    message = object as? Message
  }

  func didSelectItem(at index: Int) {}
}

當(dāng)你寫完時(shí),運(yùn)行 app,看看將消息集成后的效果!

image

火星天氣預(yù)報(bào)

我們的宇航員需要知道當(dāng)前天氣以便避開某些東西比如沙塵暴。JPL 編寫了一個(gè)顯示當(dāng)前天氣的模塊。但是那個(gè)信息有點(diǎn)多,因此他們要求只有在用戶點(diǎn)擊之后才顯示天氣信息。

image

編寫最后一個(gè) sectioncontroller,名為 WeatherSecdtionController?,F(xiàn)在這個(gè)類中定義一個(gè)構(gòu)造函數(shù)和幾個(gè)變量:

import IGListKit

class WeatherSectionController: IGListSectionController {
  // 1
  var weather: Weather!
  // 2
  var expanded = false

  override init() {
    super.init()
    // 3
    inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
  }
}

這個(gè) section controller 會(huì)從 didUpdate(to:) 方法中接收到一個(gè) Weather 對(duì)象。
expanded 是一個(gè)布爾值,用于保存天氣 section 是否被展開。默認(rèn)為 false,這樣它下面的 cell 一開始是折疊的。

和另外幾個(gè) section 一樣,底部 inset 設(shè)置為 15 像素。

加一個(gè) IGListSectionType 擴(kuò)展,實(shí)現(xiàn) 3 個(gè) required 方法:

extension WeatherSectionController: IGListSectionType {
  // 1
  func didUpdate(to object: Any) {
    weather = object as? Weather
  }

  // 2
  func numberOfItems() -> Int {
    return expanded ? 5 : 1
  }

  // 3
  func sizeForItem(at index: Int) -> CGSize {
    guard let context = collectionContext else { return .zero }
    let width = context.containerSize.width
    if index == 0 {
      return CGSize(width: width, height: 70)
    } else {
      return CGSize(width: width, height: 40)
    }
  }
}
  1. 在 didUpdate(to:) 方法中,你保存了傳入的 Weather 對(duì)象。
  2. 如果天氣被展開,numberOfItems() 返回 5 個(gè) cell,這樣它會(huì)包含天氣數(shù)據(jù)的每個(gè)部分。如果不是展開狀態(tài),只返回一個(gè)用于顯示占位內(nèi)容的 cell。
  3. 第一個(gè) cell 會(huì)比其他 cell 大一點(diǎn),因?yàn)樗且粋€(gè) Header。沒有必要判斷展開狀態(tài),因?yàn)?Header cell 只會(huì)顯示在第一個(gè) cell。

然后你需要實(shí)現(xiàn) cellForItem(at:)方法來配置 weather cell。有幾個(gè)細(xì)節(jié)需要注意:

  • 第一個(gè) cell 是 WeatherSummaryCell 類型,其他 cell 是 WeatherDetailCell 類型。

  • 通過 cell.setExpanded(_:) 方法來配置 WeatherSummaryCell。

  • 配置 4 個(gè)不同的 WeatherDetailCell 用下列 title 和 detail 標(biāo)簽:

    1. “Sunrise” - weather.sunrise
    2. “Sunset” - weather.sunset
    3. “High” - “(weather.high) C”
    4. “Low” - “(weather.low) C”

試著配置一下這個(gè) cell! 參考答案如下。

func cellForItem(at index: Int) -> UICollectionViewCell {
  let cellClass: AnyClass = index == 0 ? WeatherSummaryCell.self : WeatherDetailCell.self
  let cell = collectionContext!.dequeueReusableCell(of: cellClass, for: self, at: index)
  if let cell = cell as? WeatherSummaryCell {
    cell.setExpanded(expanded)
  } else if let cell = cell as? WeatherDetailCell {
    let title: String, detail: String
    switch index {
    case 1:
      title = "SUNRISE"
      detail = weather.sunrise
    case 2:
      title = "SUNSET"
      detail = weather.sunset
    case 3:
      title = "HIGH"
      detail = "\(weather.high) C"
    case 4:
      title = "LOW"
      detail = "\(weather.low) C"
    default:
      title = "n/a"
      detail = "n/a"
    }
    cell.titleLabel.text = title
    cell.detailLabel.text = detail
  }
  return cell
}

最后還有最后一件事情,當(dāng) cell 被點(diǎn)擊時(shí),切換 section 的展開狀態(tài)并刷新 cell。在 IGListSectionType 擴(kuò)展后實(shí)現(xiàn)這個(gè) required 協(xié)議方法:

func didSelectItem(at index: Int) {
  expanded = !expanded
  collectionContext?.reload(self)
}

reload() 方法重新加載整個(gè) section。當(dāng) section controller 中的 cell 的數(shù)目或者內(nèi)容發(fā)生變化時(shí),你可以調(diào)用這個(gè)方法。因此我們通過 numberOfItems() 方法切換 section 的展開狀態(tài),在這個(gè)方法中根據(jù) expanded 的值來添加或減少 cell 的數(shù)目。

回到 FeedViewController.swift, 在頭部加入屬性:

let wxScanner = WxScanner()

WxScanner 是一個(gè)用于天氣情況的模型對(duì)象。
然后,修改 IGListAdapterDataSource 擴(kuò)展中的 objects(for:) 方法:

// 1
var items: [IGListDiffable] = [wxScanner.currentWeather]
items += loader.entries as [IGListDiffable]
items += pathfinder.messages as [IGListDiffable]
// 2
return items.sorted(by: { (left: Any, right: Any) -> Bool in
  if let left = left as? DateSortable, let right = right as? DateSortable {
    return left.date > right.date
  }
  return false
})

我們修改了數(shù)據(jù)源方法,讓它增加 currentWeather 的數(shù)據(jù)。代碼解釋如下:

  1. 將 currentWeather 添加到 items 數(shù)組。
  2. 讓所有數(shù)據(jù)實(shí)現(xiàn) DataSortable 協(xié)議,以便用于排序。這樣數(shù)據(jù)會(huì)按照日期前后順序排列。

最后,修改 listAdapter(_:sectionControllerFor:) 方法:

if object is Message {
  return MessageSectionController()
} else if object is Weather {
  return WeatherSectionController()
} else {
  return JournalSectionController()
}

現(xiàn)在,當(dāng) object 是 Weather 類型時(shí),返回一個(gè) WeatherSectionController。

運(yùn)行 app。你會(huì)在頂部看到新的天氣對(duì)象。點(diǎn)擊這個(gè) section,展開和收起它!

image

更新操作

JPL 對(duì)你的進(jìn)度相當(dāng)?shù)臐M意!當(dāng)你在工作時(shí),NASA 的 director 組織了對(duì)宇航員的營(yíng)救工作,要求他起飛并攔截另一艘飛船!這是一次復(fù)雜的起飛,他起飛的時(shí)間必須十分精確。

JPL 工程師擴(kuò)展了消息模塊,加入了實(shí)時(shí)聊天功能,要求你集成它。
打開 FeedViewController.swift 在 viewDidLoad() 方法最后一行加入:

pathfinder.delegate = self
pathfinder.connect()

這個(gè) Pathfinder 模塊增加了實(shí)時(shí)聊天支持。你需要做的僅僅是連接這個(gè)模塊并處理委托事件。

在文件底部增加新的擴(kuò)展:

extension FeedViewController: PathfinderDelegate {
  func pathfinderDidUpdateMessages(pathfinder: Pathfinder) {
    adapter.performUpdates(animated: true)
  }
}

FeedViewController 現(xiàn)在實(shí)現(xiàn)了 PathfinderDelegate 協(xié)議。只有一個(gè) performUpdates(animated:completion:) 方法,用于告訴 IGListAdapter 查詢數(shù)據(jù)源中的新對(duì)象并刷新UI。這個(gè)方法用于處理對(duì)象被刪除、更新、移動(dòng)或插入的情況。

運(yùn)行 app,你會(huì)看到標(biāo)題上消息正在刷新!你只不過是為 IGListKit 添加了一個(gè)方法,用于說明數(shù)據(jù)源發(fā)生了什么變化,并在收到新數(shù)據(jù)時(shí)執(zhí)行修改動(dòng)畫。

image

現(xiàn)在,你所需要做的僅僅是將最新版本發(fā)給宇航員,他就能回家了!干得不錯(cuò)!

結(jié)束

這里下載最后完成的項(xiàng)目。

在幫助一位擱淺的宇航員回家的同時(shí),你學(xué)習(xí)了 IGListKit 的基本功能:section controller、adapter、以及如何將它們組合在一起。還有其他重要的功能,比如 supplementary view 和 display 事件。

你可以閱讀 Instagram 放在 Realm 上關(guān)于為什么要編寫 IGListKit 的討論。這個(gè)討論中提到了許多在編寫 app 時(shí)經(jīng)常遇到在 UICollecitonView 中出現(xiàn)的問題。

如果你對(duì)參加 IGListKit 有興趣,開發(fā)團(tuán)隊(duì)為了便于讓你開始,在 Github 上創(chuàng)建了一個(gè) starter-task 的tag。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,240評(píng)論 3 119
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,756評(píng)論 4 61
  • 六種常見的關(guān)系以及耦合強(qiáng)度:依賴 < 關(guān)聯(lián) < 聚合 < 組合 < 實(shí)現(xiàn) <繼承 1、依賴關(guān)系(Dependen...
    雙魚子曰1987閱讀 857評(píng)論 0 1
  • 感恩物流公司的老板耐心的幫我查貨單,打了幾次電話也沒有不耐煩,雖然發(fā)往鄭州的東西第六天了還沒到,因?yàn)槭鞘称?,里面?..
    莀寶貝閱讀 258評(píng)論 0 1

友情鏈接更多精彩內(nèi)容