你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> 用 AsyncDisplayKit 開發響應式 iOS App

用 AsyncDisplayKit 開發響應式 iOS App

編輯:IOS開發綜合

原文:Using AsyncDisplayKit to Develop Responsive UIs in IOS
作者:ZIAD TAMIM
譯者:kmyhy

在 2011 年,我認識了一位十分聰明的傢伙,叫做 Mike Matas on Ted 。他介紹了在電子書用到的一種增強用戶體驗的新辦法,能夠創建令人驚歎的用戶體驗。這個 App 所達到的流暢水平讓人無法置信這是一個手機 app。同年的晚些時候,這個 App 所屬的公司被 Facebook 收購,並將這種技術用在自己的產品中,從而使數億萬用戶獲得這種傑出的體驗。

我對於這個被“大公司”運用並維護著的、需求項目中全體開發者花費少量的時間和分歧努力的庫不斷感到很獵奇。

用 AsyncDisplayKit 開發響應式 IOS App用 AsyncDisplayKit 開發響應式 IOS App

示例 App 概覽

在本教程中,我們將創建一個簡單的 app,叫“BrowseMeetup”,它會運用 Meetup 的私有 API。假如你不知道 Meetup,那麼你只需求知道它是全球最大的中央人群網絡就好了。你可以免費運用 Meetup 發起一個本地群,或許從成千上萬個已有群中查找一個你想和人們面對面相遇的群。和其他社交網絡一樣,它提供了能夠從 app 中訪問的私有 API。

BrowseMeetup 運用 Meetup 的 web 服務查找最近的群。App 會獲取當前坐標並自動加載左近的 Meetup 群組,並通過 AsyncDisplayKit 來進行優化和響應式設計。我們會介紹 AsyncDisplayKit 的一些根本概念,並用這些概念來對 app 進行輕量化設計。

用 AsyncDisplayKit 開發響應式 iOS App

App 的結構

在開始編寫程式碼之前,我建議你先下載 app 的最終專案。這將有助於你對後續內容的了解。

[ecko_alert color=”gray”]留意: 在閱讀本教程之前,我強烈建議你看一下我們以前的一篇教程 如何用 iOS SDK 抓取和分析 JSON 數據。我假設你熟習 Swift。假如不,請閱讀 我們的 Swift 教程 以熟習這門語言。[/ecko_alert]

下圖顯示了 app 的結構:

用 AsyncDisplayKit 開發響應式 iOS App

View Controller、Table 節點、代理和數據源

在 UIKit 中,數據經常用 Table View 來進行顯示。對於 AsyncDisplayKit,根本顯示單位是“節點”。它是位於 UIView 之上的一種籠統。 ASTableNode 則類似於某種 UIView。它的大局部辦法都會有一個節點的“版本”。假如你熟習 UIView 或 Table View,你也就明白怎樣運用“節點”。

Table 節點對功能進行了高度優化,它十分容易運用和實現。我們將在群組列表上用到 Table 節點。

一個 Table 節點通常和 ASViewController 一同運用,後者往往作為前者的數據源和代理。這樣往往會導致 View Controller 膨脹,因為它需求做的事情太多了,負責數據的展現、顯示視圖、導航到其它 View Controller。

顯然,應當將這些任務分給多個類來進行。因而,我們會用一個助手類負責 Table 節點的數據源。在 View Controller 和助手類之間的交互通過一個協議來進行。這是一種良好的實踐,也許後面我們會換一種更好的實現方式。

Table 節點 cell

看一眼我們的 app,群列表中有一張圖片、地位、日期、組織者的頭像,以及組織者的名字。Table 節點的 cell 應該只需求顯示這些數據。我們將用一個自定義的 Table 節點 cell 來實現它。

模型

App 的模型包括群組、組織者、交互對象、數據管理器,後者允許搜索左近的群。同時,控制器會詢問群組的交互對象以用於顯示。數據管理器負責運用 meetup 服務同時將 JSON 對象創建為群組對象。

老手總是在控制器中管理模型對象。這樣,在控制器中會援用到一個群組集合。這是不推薦的,因為假如我們要改變服務,我們就必須在控制器中去修正這些功用。要對這樣的類堅持記憶是困難的,因而這是一個導致 Bug 的誘因。

更簡單的做法是在界面和模型對象之間添加一層接口,這樣假如我們需求改變模型對象的管理方式時,控制器可以堅持不變。假如接口不需求改變時,甚至可以只替換整個模型層。

我們的開發戰略

在本教程中,我們從內到外來創建這個 app。一開是模型,然後編寫網絡和控制器。顯然這不是編寫 app 的獨一方式。我們將 app 依照層的方式進行分隔,而不是依照功用進行分隔,是因為這樣更容易繼續後面的任務並始終記住你將要做的事情是什麼。當在後面需求喚起你的記憶時,你更容易想起需求的信息。

從 Xcode 開始

現在,我們的旅程將從新建專案開始,這個專案我們用到 AsyncDisplayKit。

打開 Xcode ,新建一個 iOS 專案,採用 Single View Application 模板。在選項窗口,設置 product name 為 BrowseMeetup ,語言選擇 Swift ,Device 選擇 iPhone。

要配置專案使其能夠運用 AsyncDisplayKit,請在專案導航器中選擇 Main.Storyboard 並刪除它。在專案導航器中,打開 AppDelegate.swift 將程式碼替換為:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var Window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        let Window                      = UIWindow(frame: UIScreen.main.bounds)
        window.backgroundColor          = UIColor.white

        let feedVC  = MeetupFeedViewController()
        let feedNavCtrl = UINavigationController(rootViewController: feedVC)
        window.rootViewController  = feedNavCtrl

        window.makeKeyAndVisible()
        self.window = window

        return true
    }
}

在這個辦法中,我們簡單地初始化一個 AsyncDisplayKit 容器作為我們的主 View Controller,以免你直接將節點添加到已原有的視圖樹中。這十分重要,因為這才干讓節點在渲染的時候失掉刷新。

程式碼現在不能通過編譯,因為 Xcode 還不認識 MeetupFeeController。你需求創建這個文件,首先在專案導航器中點擊 BrowseMeetup 群組。點開菜單 File | New | File…,選擇 iOS | Source | Swift File 模板,然後點 next。在 Save As 欄,填入類名 MeetupFeedViewController.swift,點擊 Create。

打開 MeetupFeedViewController.swift 編寫如下程式碼:

import AsyncDisplayKit

final class MeetupFeedViewController: ASViewController {

    var _tableNode: ASTableNode

    init() {
        _tableNode = ASTableNode()
        super.init(node: _tableNode)
                setupInitialState()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

有時候,初始化 View Controller 的方式簡單一點就好。例如我們的 MeetupFeedViewController,它運用了一個指定初始化函數,在這個辦法中初始化一個 ASViewController,並指定它的節點為一個 ASTableNode。你能夠不知道什麼是 ASTableNode,只需求把它看成是 UIKit 的 UITableView 就好了,把它當成 UITableView 就是了。

再次編譯。編譯依然不能通過,因為你的專案中還沒有導入 AsyncDisplayKit。

關閉 Xcode,打開終端。我們用 CocoaPods 來進行安裝。切換到 iOS 專案所在的目錄,執行 pod init 命令,創建一個空的 Podfile 檔案。

$ cd path/to/project
$ pod init

[ecko_alert class=”green”]留意: 假如你不熟習 CocoaPods,請閱讀我們的 這篇指南。[/ecko_alert]

現在來編寫 Podfile,在檔案中寫入下述內容:

target 'BrowseMeetup' do
  use_frameworks!

  pod 'AsyncDisplayKit', ' 2.0'
end

然後,運行 pod install 以便安裝這個框架。安裝能夠會花幾分鐘時間,具體就要看你的網速了。當安裝完畢,打開剛剛生成的BrowseMeetup.xcworkspace ,而不是原先的 BrowseMeetup.xcodeproj

$ pod install
$ open BrowseMeetup.xcworkspace
運用 Meetup APIs

在運用 Meetup API 之前,需求有一個 Meetup 賬號。進入 APIs Doc ,點擊 “Request to join Meetup group” 按鈕,根據屏幕上的提示進行註冊,並參加一個群。當註冊完後,你就可以用那個群對 API 進行沙盒測試。

用 AsyncDisplayKit 開發響應式 iOS App

為了能夠訪問 Meetup APIs,你需求擁有一個 API key。在 dashboard 中,點擊 API 標籤欄,你可以點擊小鎖圖標以檢查你的 API key。

用 AsyncDisplayKit 開發響應式 iOS App

我們會用到一個 Meetup APIs (即 https://api.meetup.com/find/groups) 來搜索某個坐標左近的 Meetup 群組。運用時需求開發者指定一個經緯度坐標。Meetup 的 dashboard 中提供了一個控制台,允許你通過控制台來測試 APIs,點擊 Console,然後輸入 find/groups 試試看。

用 AsyncDisplayKit 開發響應式 iOS App

例如,假如請求 https://api.meetup.com/find/groups?&lat=51.509980&lon=-0.133700&page=1&key=1f5718c16a7fb3a5452f45193232,則失掉一個 JSON 格式的響應:

[
    {
            score: 1,
            id: 10288002,
            name: "Virtual Java User Group",
            link: "https://www.meetup.com/virtualJUG/",
            urlname: "virtualJUG",
            description: "

If you don't live near an active Java User Group, or just yearn for more high quality technical sessions, The Virtual JUG is for you! If you live on pl.net Earth you can join. Actually even if you don't you can still join! Our aim is to get the greatest minds and speakers of the Java industry giving talks and presentations for this community, in the form of webinars and JUG session streaming from JUG f2f meetups. If you're a Java enthusiast and you want to learn more about Java and surrounding technologies, join and see what we have to offer!

", created: 1379344850000, city: "London", country: "GB", localized_country_name: "United Kingdom", state: "17", join_mode: "open", visibility: "public", lat: 51.5, lon: -0.14, members: 10637, organizer: { id: 13374959, name: "Simon Maple", bio: "", photo: { id: 210505562, highres_link: "http://photos2.meetupstatic.com/photos/member/6/3/d/a/highres_210505562.jpeg", photo_link: "http://photos2.meetupstatic.com/photos/member/6/3/d/a/member_210505562.jpeg", thumb_link: "http://photos2.meetupstatic.com/photos/member/6/3/d/a/thumb_210505562.jpeg", type: "member", base_url: "http://photos2.meetupstatic.com" } }, who: "vJUGers", group_photo: { id: 454745514, highres_link: "http://photos4.meetupstatic.com/photos/event/1/5/8/a/highres_454745514.jpeg", photo_link: "http://photos4.meetupstatic.com/photos/event/1/5/8/a/600_454745514.jpeg", thumb_link: "http://photos4.meetupstatic.com/photos/event/1/5/8/a/thumb_454745514.jpeg", type: "event", base_url: "http://photos4.meetupstatic.com" }, key_photo: { id: 454577629, highres_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/highres_454577629.jpeg", photo_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/600_454577629.jpeg", thumb_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/thumb_454577629.jpeg", type: "event", base_url: "http://photos1.meetupstatic.com" }, timezone: "Europe/London", next_event: { id: "235903314", name: "The JavaFX Ecosystem", yes_rsvp_count: 261, time: 1484154000000, utc_offset: 0 }, category: { id: 34, name: "Tech", shortname: "Tech", sort_name: "Tech" }, photos: [ { id: 454577629, highres_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/highres_454577629.jpeg", photo_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/600_454577629.jpeg", thumb_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/thumb_454577629.jpeg", type: "event", base_url: "http://photos1.meetupstatic.com" }, { id: 454577652, highres_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/4/highres_454577652.jpeg", photo_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/4/600_454577652.jpeg", thumb_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/4/thumb_454577652.jpeg", type: "event", base_url: "http://photos1.meetupstatic.com" }, { id: 454577660, highres_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/c/highres_454577660.jpeg", photo_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/c/600_454577660.jpeg", thumb_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/c/thumb_454577660.jpeg", type: "event", base_url: "http://photos1.meetupstatic.com" }, { id: 454579647, highres_link: "http://photos4.meetupstatic.com/photos/event/4/c/b/f/highres_454579647.jpeg", photo_link: "http://photos2.meetupstatic.com/photos/event/4/c/b/f/600_454579647.jpeg", thumb_link: "http://photos2.meetupstatic.com/photos/event/4/c/b/f/thumb_454579647.jpeg", type: "event", base_url: "http://photos2.meetupstatic.com" } ] } ]
實現 Group 結構

我們的 BrowserMeetup app 需求一個模型,用於保管群組信息。這需求新建一個檔案,用於編寫實現程式碼。打開 專案導航器,添加一個 Swift 檔案,名為 Group.swift。通過之前的 app 截圖,我們知道,它需求存儲創建日期、照片、城市、國家和創建者。

struct Group {
    let createdAt: Double!
    let photoUrl: URL!
    let city: String!
    let country: String!
    let organizer: Organizer!

    var timeInterval: String {
        let date = Date(timeIntervalSince1970: createdAt)
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .medium
        dateFormatter.timeStyle = .none

        return dateFormatter.string(from: date)
    }
}

在結構定義中,我們添加了一個助手辦法將 time interval 通過 date formatter 轉換成人類可讀的日期類型,只取 date 局部,疏忽 time 局部。

這段代碼無法編譯,因為 Organizer 類型未知。要解決這個問題,我們需求再創建一個名為 Organizer.swift 的 Swift 檔案。並編輯它的內容為如下程式碼。

struct Organizer {

}

現在,能夠順利編譯通過了。

實現 Organizer 結構

是上一節,我們用一個結構保管創建者的信息。接下來需求為它添加一些屬性。打開 OpenOrganizer.swift,編輯為如下內容:

struct Organizer {
    let name: String!
    let thumbUrl: URL!
}

每個創建者都有一個名字和頭像 URL,所以我們創建了兩個屬性來保管它們。

實現 MeetupService 類

後面說過,我們知道怎樣用 Meetup API 來查找左近的群。即這個 URL https://api.meetup.com/find/groups ,它需求 3 個參數: 緯度、經度和關鍵字(你可以運用這個例子中的關鍵字,也可以用你自己的關鍵字來測試你的程式碼)。這個 API 前往一個 JSON 對象,其中包括了我們 app 需求的信息。

我們會創建一個 MeetupService 類,用於連接 API 並進行 JSON 解析。現在,添加一個新的 Swift 檔案 MeetupService.swift。在其中編寫如下程式碼:

typealias JSONDictionary = Dictionary<String, Any>
let MEETUP_API_KEY = "1f5718c16a7fb3a5452f45193232"

final class MeetupService {

    var baseUrl: String = "https://api.meetup.com/"
    lazy var session: URLSession = URLSession.shared

    func fetchMeetupGroupInLocation(latitude: Double, longitude: Double, completion: @escaping (_ results: [JSONDictionary]?, _ error: Error?) -> ()) {
        guard let url = URL(string: "\(baseUrl)find/groups?&lat=\(latitude)&lon=\(longitude)&page=10&key=\(MEETUP_API_KEY)") else {
            fatalError()
        }

        session.dataTask(with: url) { (data, response, error) in
            DispatchQueue.main.async(execute: {
                do {
                    let results = try JSONSerialization.jsonObject(with: data!) as? [JSONDictionary]
                    completion(results, nil);

                } catch let underlyingError {
                    completion(nil, underlyingError);
                }
            })
            }.resume()
    }
}

我假定你非常熟習 web 服務和 JSON 解析,就不細講這段程式了。簡單來說,我們運用了一個 URLSession 去調用 web API,然後向服務器請求數據。針對所前往的 JSON 數據,我們運用 JSONSerialization 進行分析,並將結果前往給 completion 塊進行處理。

實現 LocationService 類

要調用 Meetup API 需求我們用 CoreLocation 獲取一個坐標。這局部內容略微超出了本文的範疇,因而我們不再介紹,關於如何運用 Core Location,你可以參考我們的教程 How To Get the Current User Location 或許 這本書 。現在,新建一個 Swift 檔案,叫做 LocationService.swift 然後參加以下程式碼:

import Foundation
import CoreLocation

final class LocationService {

    var coordinate: CLLocationCoordinate2D? = CLLocationCoordinate2D(latitude: 51.509980, longitude: -0.133700)
}

這裡,我們宣告了一個 CLLocationCoordinate2D 實例,將 latitudelongitude 設置為位於倫敦的某個坐標即我們的興趣點。處於演示的目的,我們硬編碼了這個坐標。

實現 DataManager 類

MeetupBrowse app 會顯示一張列表,列出左近的群組。這個列表受 MeetupFeedDataManager 類管理。再創建一個新的 Swift 檔案,叫做MeetupFeedDataManager.swift

編輯這個文件的內容如下:

final class MeetupFeedDataManager {

    fileprivate var _meetupService: MeetupService?
    fileprivate var _locationService: LocationService?

    init(meetupService: MeetupService, locationService: LocationService) {
       _meetupService = meetupService
       _locationService = locationService
    }

    func searchForGroupNearby(completion: @escaping ( _ groups: [Group]?, _ error: Error?) -> ()) {
        let coordinate = _locationService?.coordinate

        _meetupService?.fetchMeetupGroupInLocation(latitude: coordinate!.latitude, longitude: coordinate!.longitude, completion: { (results, error) in
            guard error == nil else { completion(nil, error); return }

            let groups = results?.flatMap(self.groupItemFromJSONDictionary)
            completion(groups, nil)
        })
    }
}

MeetupFeedDataManager 中,提供一個承受 MeetupServiceLocationService 對象的初始化辦法是一個不錯的做法,這就是所謂的依賴注入,通過這種設計形式可以使我們的類更容易被管理和測試。

從 JSON 中提取數據

searchForGroupNearby 辦法調用 MeetupServicefetchMeetupGroupInLoaction 辦法來獲取最近的群組列表。這些結果需求從 JSON 格式轉換成 app 領域中的某種對象,也就是我們新近宣告的模型類。

要將 JSON 對象轉換成 Group 對象,需求編寫一個 groupItemFromJSONDictionary 辦法,這個辦法用一個 JSONDictionary 對象作參數,並將 JSON 對象中的值抽取到 Group 對象的屬性中。在 MeetupFeedDataManager.swift 中參加下列程式碼:

func groupItemFromJSONDictionary(_ entry: JSONDictionary) -> Group? {
        guard let created = entry["created"] as? Double, let city = entry["city"] as? String, let country = entry["country"] as? String, let keyPhoto = entry["key_photo"] as? JSONDictionary, let photoUrl = keyPhoto["photo_link"] as? String, let organizerJSON = entry["organizer"] as? JSONDictionary, let organizer = organizerItemFromJSONDictionary(organizerJSON) else {
            return nil
        }

        return Group(createdAt: created, photoUrl: URL(string: photoUrl), city: city, country: country, organizer: organizer)
    }

這裡,JSONDictionary 中的每個值都會用可空綁定和 as? 類型轉換操作的方式提取到常量中。然後用這些值創建出 Group 對象。

下面的程式碼無法編譯,因為 organizerItemFromJSONDictionary 辦法未知。要解決這個問題,在同一個類中參加以下程式碼:

 
func organizerItemFromJSONDictionary(_ entry: JSONDictionary) -> Organizer? {
    guard let name = entry["name"] as? String, let photo = entry["photo"] as? JSONDictionary, let thumbUrl = photo["thumb_link"] as? String else {
        return nil
    }

    return Organizer(name: name, thumbUrl: URL(string: thumbUrl))
}

Swift 內置的語言特功能夠讓我們很容易地運用 Foundation API 就可以對 JSON 數據進行解碼並抽取其中的數值,完全不需求藉助任何第三方框架和庫。

實現 Interactor 類

現在,我們已經實現了 JSON 數據的分析,我們需求創建另一個類來處理和數據(實體)或網絡相關的業務邏輯,比方創建新的實體實例或許從服務器抓取實體。

新建一個 Swift 檔案,叫做 MeetupFeedInteractorIO.swift,參加以下程式碼:

protocol MeetupFeedInteractorInput {
    func findGroupItemsNearby ()
}

protocol MeetupFeedInteractorOutput {
    func foundGroupItems (_ groups: [Group]?, error: Error?)
}

這些協議用於處理用戶輸入,以及處理需求顯示的內容。這種別離是基於單一責任准繩。在我們的 app 中這次要體現在顯示左近的群。 上面是在 MeetupFeedInteractor.swift 中的實現:

final class MeetupFeedInteractor: MeetupFeedInteractorInput {

    var dataManager: MeetupFeedDataManager?

    var output: MeetupFeedInteractorOutput?

    func findGroupItemsNearby() {
        dataManager?.searchForGroupNearby(completion: output!.foundGroupItems)
    }
}

現在,我們的類運用 MeetupFeedInteractorInput 協議從用戶交互中採集輸入,這樣當它通過 findGroupItemsNearby 辦法獲得結果後會重新繪製 UI。

實現 MeetupFeedViewController

我們繼續來實現 MeetupFeedViewController 類,這個類負責顯示左近的群組。它也是用戶在 app 啟動後看到的第一個視圖。

在 專案導航器 中打開 MeetupFeedViewController.swift,將 viewDidLoad辦法修正為這樣:

override func viewDidLoad() {
        super.viewDidLoad()
        _activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
        _activityIndicatorView.hidesWhenStopped = true
        _activityIndicatorView.sizeToFit()

        var refreshRect = _activityIndicatorView.frame
        refreshRect.origin = CGPoint(x: (view.bounds.size.width - _activityIndicatorView.frame.width) / 2.0, y: _activityIndicatorView.frame.midY)

        _activityIndicatorView.frame = refreshRect
        view.addSubview(_activityIndicatorView)

        _tableNode.view.allowsSelection = false
        _tableNode.view.separatorStyle = UITableViewCellSeparatorStyle.none

        _activityIndicatorView.startAnimating()
        handler?.findGroupItemsNearby()
    }

同時,需求在類中宣告 handler_activityIndicatorView 屬性:

var handler: MeetupFeedInteractorInput?
var _activityIndicatorView: UIActivityIndicatorView!

我們宣告了一個新的 UIActivityIndicatorView 物件,並將它添加到 subview 中,以便在加載數據的過程中顯示一個旋轉的小菊花。同時禁用了交互,因為 app 只要一個 View Controller。然後,應用 handler(MeetupFeedInteractor)來查找左近的群組。

然後,我們會創建一個辦法來初始化 View Controller 的狀態。在這個辦法中,我們會定義控制器的數據提供者和數據源。在後面,我們會創建一個 MeetupFeedTableDataProvider 的類來處理數據。現在,先來編寫這個辦法:

    
func setupInitialState() {
    title = "Browse Meetup"

    _dataProvider = MeetupFeedTableDataProvider()
    _dataProvider._tableNode = _tableNode
    _tableNode.dataSource = _dataProvider
}

同時需求宣告一個屬性,用做我們的數據提供者:

var _dataProvider: MeetupFeedTableDataProvider!

還記得我們曾經定義過一個 MeetupFeedInteractorOutput 協議嗎?在 MeetupFeedInteractor 中會呼叫這個協議辦法:

func findGroupItemsNearby() {
    dataManager?.searchForGroupNearby(completion: output!.foundGroupItems)
}

但是,我們還沒有實現這個辦法(即 foundGroupItems)。我們在 MeetupFeedViewController 類中實現它。因而,將類的定義修正為:

final class MeetupFeedViewController: ASViewController, MeetupFeedInteractorOutput

然後實現這個辦法:

func foundGroupItems(_ groups: [Group]?, error: Error?) {
    guard error == nil else { return }

    _dataProvider.insertNewGroupsInTableView(groups!)
    _activityIndicatorView.stopAnimating()
}

當這個辦法被呼叫時,我們會將處理 groups 並將它拔出到 Table View 中。我們運用了數據提供者中實現的 insertNewGroupsInTableView 辦法,這個辦法將實體對象出入到 Table 節點,後面解釋。同時需求將 Activity Indicator 中止,因為我們不需求它了。

實現 MeetupFeedTableDataProvider

在上一節,我們創建了一個類充當 Table 節點的數據源。在這一節,我們來實現它的屬性和辦法。

創建一個新文件 MeetupFeedTableDataProvider.swift,修正它的內容為:

import Foundation
import AsyncDisplayKit

class MeetupFeedTableDataProvider: NSObject, ASTableDataSource {

    var _groups: [Group]?
    weak var _tableNode: ASTableNode?

    ///--------------------------------------
    // MARK - Table data source
    ///--------------------------------------

    func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
        return _groups?.count ?? 0
    }

    func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {

        let group = _groups![indexPath.row]
        let cellNodeBlock = { () -> ASCellNode in
            return GroupCellNode(group: group)
        }
        return cellNodeBlock
    }

    ///--------------------------------------
    // MARK - Helper Methods
    ///--------------------------------------

    func insertNewGroupsInTableView(_ groups: [Group]) {
        _groups = groups

        let section = 0
        var indexPaths = [IndexPath]()
        groups.enumerated().forEach { (row, group) in
            let path = IndexPath(row: row, section: section)
            indexPaths.append(path)
        }
        _tableNode?.insertRows(at: indexPaths, with: .none)
    }
}

好像之前說的,要運用 AsyncDisplayKit 中的 Table View,我們必須實現 ASTableDataSource 協議。這裡,我們創建了一個新類實現這個協議並讓它作為數據提供者。

這些辦法和你熟習的 UITableViewDataSource 協議辦法十分像。 在第一個辦法中,我們前往了需求顯示在 Table View 中的群組的總數。

tableNode(_:nodeForRowAtIndexPath:) 辦法中,我們獲取了與 indexPath.row 所指定的行相對應的 Group 物件。然後,創建了一個 GroupCellNode,它是一個 ASCellNode 的籠統。最後,我們將 Group 物件和這個 cell 綁定並返。

建議你運用這些辦法的節點塊版本,這樣你的集合節點就能夠同步地準備和顯示它的 cell。這還意味著一切子節點的初始化辦法能夠在後台執行。

MeetupFeedTableDataProvider 中的 insertNewGroupsInTableView 辦法,用於將群組拔出到 Table 節點中。這裡,我們呼叫了 Table 節點的 insertRows 辦法來拔出行。需求留意的是,這個辦法必須從主線程中呼叫。

實現 GroupCell

假如你曾經自定義過 Table Cell,那麼你就知道需求為 Table Cell 創建一個自定義類。同樣,在運用 AsyncDisplayKit 時,我們需求創建一個自定義的 Cell 節點,並延伸 ASCellNode 以呈現自定義數據。這裡,我們會創建一個 GroupCellNode 類,用於堅持住對 Label 和 Image 的援用。

再新建一個檔案,命名為 GroupCellNode。修正它的內容為:

import AsyncDisplayKit

fileprivate let SmallFontSize: CGFloat = 12
fileprivate let FontSize: CGFloat = 12
fileprivate let OrganizerImageSize: CGFloat = 30
fileprivate let HorizontalBuffer: CGFloat = 10

final class GroupCellNode: ASCellNode {

    fileprivate var _organizerAvatarImageView: A.networkImageNode!
    fileprivate var _organizerNameLabel: ASTextNode!
    fileprivate var _locationLabel: ASTextNode!
    fileprivate var _timeIntervalSincePostLabel: ASTextNode!
    fileprivate var _photoImageView: ASNetworkImageNode!

    init(group: Group) {
        super.init()

        _organizerAvatarImageView = ASNetworkImageNode()
        _organizerAvatarImageView.cornerRadius = OrganizerImageSize/2
        _organizerAvatarImageView.clipsToBounds = true
        _organizerAvatarImageView?.url = group.organizer.thumbUrl


        _organizerNameLabel = createLayerBackedTextNode(attributedString: NSAttributedString(string: group.organizer.name, attributes: [NSFontAttributeName: UIFont(name: "Avenir-Medium", size: FontSize)!, NSForegroundColorAttributeName: UIColor.darkGray]))

        let location = "\(group.city!), \(group.country!)"
        _locationLabel = createLayerBackedTextNode(attributedString: NSAttributedString(string: location, attributes: [NSFontAttributeName: UIFont(name: "Avenir-Medium", size: SmallFontSize)!, NSForegroundColorAttributeName: UIColor.blue]))

        _timeIntervalSincePostLabel = createLayerBackedTextNode(attributedString: NSAttributedString(string: group.timeInterval, attributes: [NSFontAttributeName: UIFont(name: "Avenir-Medium", size: FontSize)!, NSForegroundColorAttributeName: UIColor.lightGray]))

        _photoImageView = ASNetworkImageNode()
        _photoImageView?.url = group.photoUrl

        automaticallyManagesSubnodes = true
    }

    fileprivate func createLayerBackedTextNode(attributedString: NSAttributedString) -> ASTextNode {
        let textNode = ASTextNode()
        textNode.isLayerBacked = true
        textNode.attributedText = attributedString

        return textNode
    }
}

這個節點會下載和顯示 Meetup 群組的縮略圖。AsyncDisplay 有一個類,叫做 ASNetworkImageNode,它會下載並顯示遠程圖片。你所需求做的僅僅是設置圖片的 URL 地址給它的 url 屬性。這個圖片就會異步加載並同步地顯示。

對於文字,我們運用 ASTextNode 來進行顯示。一個文字節點和我們常用的 UILabel 類似。它添加了富文本支持並延伸了 ASControlNode 類。

助手辦法(即 createLayerBackedTextNode 辦法)用於將創建 Label 時重複的程式碼放在一個辦法裡。

AsyncDisplayKit 的自動佈局基於 CSS 盒子模型。和 UIKit 的佈局約束相比,它的效率更高,更容易調試、更明晰、機構化,能構造複雜和可重用的佈局。

現在來編寫佈局辦法:

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {

        _locationLabel.style.flexShrink = 1.0
        _organizerNameLabel.style.flexShrink = 1.0

        let headerSubStack = ASStackLayoutSpec.vertical()
        headerSubStack.children = [_organizerNameLabel, _locationLabel]

        _organizerAvatarImageView.style.preferredSize = CGSize(width: OrganizerImageSize, height: OrganizerImageSize)

        let spacer = ASLayoutSpec()
        spacer.style.flexGrow = 1.0

        let avatarInsets = UIEdgeInsets(top: HorizontalBuffer, left: 0, bottom: HorizontalBuffer, right: HorizontalBuffer)
        let avatarInset = ASInsetLayoutSpec(insets: avatarInsets, child: _organizerAvatarImageView)

        let headerStack = ASStackLayoutSpec.horizontal()
        headerStack.alignItems = ASStackLayoutAlignItems.center
        headerStack.justifyContent = ASStackLayoutJustifyContent.start
        headerStack.children = [avatarInset, headerSubStack, spacer, _timeIntervalSincePostLabel]

        let headerInsets = UIEdgeInsets(top: 0, left: HorizontalBuffer, bottom: 0, right: HorizontalBuffer)
        let headerWithInset = ASInsetLayoutSpec(insets: headerInsets, child: headerStack)

        let cellWidth = constrainedSize.max.width

        _photoImageView.style.preferredSize = CGSize(width: cellWidth, height: cellWidth)
        let photoImageViewAbsolute = ASAbsoluteLayoutSpec(children: [_photoImageView]) //ASStaticLayoutSpec(children: [_photoImageView])

        let verticalStack = ASStackLayoutSpec.vertical()
        verticalStack.alignItems = ASStackLayoutAlignItems.stretch
        verticalStack.children = [headerWithInset, photoImageViewAbsolute]

        return verticalStack
    }

上述程式碼運用了一個十分強大的佈局規範叫做 ASStackLayoutSpec。它包括了少量屬性,你可以用來實現你想要實現的任何效果。同時,我們還運用了 ASInsetLayoutSpec 來添加一些 padding。

簡單說,這段程式碼創建了一個垂直的 Stack 佈局,包括了兩個節點和另一個程度 Stack,這個程度 Stack 位於頂端,自己又包括了另外 3 個節點用於顯示創建者的頭像、創建者名字和群組的地位。最後,我們將整個節點包裝成一個垂直的 ASStackLayoutSpec 前往。

ASLayoutSpec 當成小間隔來用。

[ecko_alert color=”green”]補充說明: 你可以參考 官方文檔 以理解 AsyncDisplayKit 中的自動佈局。[/ecko_alert]

總裝

在後面,我們已經分別用 AsyncDisplayKit 實現了 app 的各個局部。現在,我們來將它們組裝成一個完好的 app。

打開專案導航器,選擇 AppDelegate.swift。修正 application(:didFinishLaunchingWithOptions:) 辦法為:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    let window                      = UIWindow(frame: UIScreen.main.bounds)
    window.backgroundColor          = UIColor.white

    let feedVC  = MeetupFeedViewController()
    let locationService = LocationService()
    let meetupService = MeetupService()

    let dataManager = MeetupFeedDataManager(meetupService: meetupService, locationService: locationService)
    let interactor = MeetupFeedInteractor()
    interactor.dataManager = dataManager
    interactor.output = feedVC

    feedVC.handler = interactor

    let feedNavCtrl = UINavigationController(rootViewController: feedVC)
    window.rootViewController  = feedNavCtrl

    window.makeKeyAndVisible()
    self.window = window

    return true
}

我們添加了幾句話,包括初始化 Feed View Controller、數據管理器和 Interactor。現在,你可以運行 app 進行測試了。app 會從 meetup.com 下載和顯示 Meetup 群組。但是,圖片無法顯示。假如你看一眼控制台,你會發現報錯了:

App Transport Security has blocked a cleartext HTTP (http://) 

App 傳輸平安(ATS) 是從 iOS 9 開始引入的,它規定 app 優先運用 HTTPS 平安網絡連接。假如你的 app 要訪問 HTTP 遠程資源,你就會看到這個錯誤。

關閉 ATS

要解決這個問題,你可以在 Info.plist 檔案中關閉 ATS。在專案導航器中選擇 Info.plist 並編輯它的這個中央:

用 AsyncDisplayKit 開發響應式 iOS App

Allow Arbitrary Loads 選項設置為 YES 可以關閉 ATS。然後再次運行 app。這次,你可以看到群組圖片了。

用 AsyncDisplayKit 開發響應式 iOS App

結束

祝賀你,app 完成了!在本文,我帶你理解了 AsyncDisplayKit 的根本知識。你現在知道如何在自己的專案中通過 AsyncDisplayKit 來創建響應式 UI 了。更多補充內容,我建議你閱讀 官方文檔。

你可以 從 GitHub 下載完成的專案 。

你對本文和 AsyncDisplayKit 框架作何感想?假如你想聽我繼續介紹這個令人驚歎的框架,請告訴我。

譯者簡介

楊宏焱,男,中國大陸籍人士,CSDN 博客專家(個人博客 http://blog.csdn.net/kmyhy)。2009 年開始學習蘋果 iOS 開發,通曉 O-C/Swift 和 Cocoa Touch 框架,開發有多個商店應用和企業 App。熱愛寫作,著有和翻譯有多本技術專著,包括:《企業級 iOS 應用實戰》、《iPhone & iPad 企業移動應用開發秘笈》、《iOS8 Swift 編程指南》,《寫給大忙人看的 Swift》、《iOS Swift 遊戲開發經典實例》等

【用 AsyncDisplayKit 開發響應式 iOS App】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!

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