iOSアプリのUX改善! FacebookのAsyncDisplayKitで60FPSのハイパフォーマンスなiOSアプリを作る
この記事は Eureka Advent Calendar 2016 14日目 の記事です。
13日目は 香取さん の「今日から始めるDeep Learning」でした。
こんにちは。Couples事業部でiOSアプリの開発を担当している丹です!
今回はFacebookとPinterestがオープンソースとして公開しているAsyncDisplayKitをCouplesで使ってみたので、導入方法を紹介したいと思います。
AsyncDisplayKitはViewのレイアウトを非同期に扱うことで、60FPSのスムーズなUIを実現するためのライブラリです。先日(2016年12月9日)、バージョン2.0になり、安定してきたようなので本格的に導入しても良いと思っています。Swiftのサンプルコードはまだ少ないので、参考になると嬉しいです。
facebook/AsyncDisplayKit
環境
- AsyncDisplayKit 2.0
- Swift 2.3
- Xcode 8.1(8B62)
AsyncDisplayKitの特徴
AsyncDisplayKitではNodeと呼ばれるViewを抽象化したオブジェクトを扱います。ベースのクラスはASDisplayNode
です。UIView
はメインスレッドでしか動作しませんが、ASDisplayNode
はスレッドセーフでバックグラウンドスレッドでも動作します。そのおかげでメインスレッドをブロックしないスムーズなUIを実現することができます。
NodeとNode Container
NodeはNode Containerの中で扱う必要があります。NodeやNode ContainerはUIKitとの対応関係を知ると分かりやすいと思うので、以下に一覧を載せておきます。
Node – AsyncDisplayKit | Node Subclasses
- ASDisplayNode / UIView
- ASCellNode / UITableViewCell & UICollectionViewCell
- ASScrollNode / UIScrollView
- ASEditableTextNode / UITextView
- ASTextNode / UILabel
- ASImageNode & ASNetworkImageNode & ASMultiplexImageNode / UIImage
- ASVideoNode / AVPlayerLayer
- ASVideoPlayerNode / UIMoviePlayer
- ASControlNode / UIControl
- ASButtonNode / UIButton
- ASMapNode / MKMapView
Node Container – AsyncDisplayKit | Node Containers
- ASViewController / UIViewController
- ASNavigationController / UINavigationController
- ASTabBarController / UITabBarController
- ASPagerNode / UIPageViewController
- ASCollectionNode / UICollectionView
- ASTableNode / UITableView
AsyncDisplayKitのレイアウト方法
AsyncDisplayKitはStoryboardやInterface Builderを使用せず、レイアウトをすべてコードで書く必要があります。ドキュメントを全部読んだ感想としては、AsyncDisplayKitの導入はレイアウトの組み方をマスターできるかにかかっています。コードはあとで紹介するので、ここでは概要だけ説明します。
AsyncDisplayKitではASLayoutSpec
というオブジェクトを使って、レイアウトを組みます。ASLayoutSpec
を使ったレイアウトの計算は、以下の2点の理由からAutoLayoutよりも断然速くなります。
- マニュアルレイアウトと同等の速度(複雑なレイアウトではAutoLayoutは遅くなります)
- バックグラウンドかつ並列の計算
ASLayoutSpec
はASLayoutElement
プロトコルを採用しているASLayoutSpec
とASDisplayNode
を扱うことができます。つまり、以下のような入れ子構造が可能になります。
ASLayoutSpec
|- ASLayoutSpec
|- ASDisplayNode
ASLayoutSpec
には以下のサブクラスが用意されています。詳しくは AsyncDisplayKit | Layout Specsをご覧ください。
- ASInsetLayoutSpec
- ASOverlayLayoutSpec
- ASBackgroundLayoutSpec
- ASCenterLayoutSpec
- ASRatioLayoutSpec
- ASStackLayoutSpec
- ASAbsoluteLayoutSpec
Couplesのお知らせ画面をAsyncDisplayKitに置き換える
百聞は一見に如かずということで、実際のコードを見ていきます。今回適用する画面はこちらのお知らせ画面です。シンプルなTableViewです。
ViewControllerを書く
ASViewController
とUIViewController
、ASTableNode
とUITableView
は似たインターフェースを持っています。まずは、ViewControllerのイニシャライザとライフサイクルのコードを書きます。
// ASNotificationViewController.swift
import UIKit
import AsyncDisplayKit
// ASViewControllerのサブクラスにします。
final class ASNotificationViewController: ASViewController {
// クラス内で扱いやすくするため、nodeをASTableNodeにキャストします。
var tableNode: ASTableNode {
return node as! ASTableNode
}
// ASTableNodeでnodeを初期化します。
// init内ではself.view, self.node.viewにアクセスしてはいけません。
init() {
super.init(node: ASTableNode())
tableNode.dataSource = self // ASTableDataSource
tableNode.delegate = self // ASTableDataSource
}
override func viewDidLoad() {
super.viewDidLoad()
// メインスレッドなので、ここでViewのセットアップをしましょう。
// tableNode.viewでASTableView(UITableViewのサブクラス)にアクセスできます。
tableNode.view.tableFooterView = UIView()
tableNode.view.backgroundColor = ...
}
DataSourceを書く
ASTableDataSource
もUITableViewDataSource
と似たインターフェースを持っています。ASCellNodeBlock
を返すメソッドは注意事項がたくさんあるので、気をつけてください。
// ASNotificationViewController.swift
extension ASNotificationViewController: ASTableDataSource {
func numberOfSections(in tableNode: ASTableNode) -> Int {
return 1
}
func tableNode(tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
// CoreDataでフェッチ済みのオブジェクト数を返します。
return notifications.numberOfObjects()
}
// tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)に当たります。
// ちなみに、ASCellNodeは再利用されません。
func tableNode(tableNode: ASTableNode, nodeBlockForRowAtIndexPath indexPath: NSIndexPath) -> ASCellNodeBlock {
// CoreDataのNotificationオブジェクトを取得します。
let notification: Notification = ...
// nodeに渡す際にスレッドセーフなオブジェクトに変換してあげる必要があります。NotificationViewModelはstructです。
let viewModel = NotificationViewModel(notification)
// typealias ASCellNodeBlock = () -> ASCellNode
// このブロックはバックグラウンドスレッドで実行されます。
// ブロック実行時にindexPathが無効になっている可能性があるので、ブロック内でindexPathにアクセスすべきではありません。
let block: ASCellNodeBlock = { ASNotificationCellNode(viewModel: viewModel) }
return block
}
}
上記のコードに出てきたViewModelのインターフェースです。
struct NotificationViewModel {
let body: String
let date: String
let imageURL: String
let isRead: Bool
}
ASNotificationCellNodeを書く
お知らせのセルは、アイコン画像、お知らせの本文、時刻、未読の丸いマークの4つを含んでいます。
既読時
![Couplesお知らせ画面のセル既読時]()
// ASNotificationCellNode.swift
import UIKit
import AsyncDisplayKit
final class ASNotificationCellNode: ASCellNode {
let viewModel: NotificationViewModel
private let iconNode = ASNetworkImageNode()
private let messageNode = ASTextNode()
private let dateNode = ASTextNode()
private let unreadNode = ASImageNode()
// ASCellNodeBlock内で呼ばれるため、initはバックグラウンドスレッドで動作します。
init(viewModel: NotificationViewModel) {
self.viewModel = viewModel
super.init()
// trueにするとnodeの追加などを自動でやってくれます。
automaticallyManagesSubnodes = true
// iconNode: アイコン画像
iconNode.URL = NSURL(string: viewModel.imageURL)!
iconNode.layerBacked = true // タッチをハンドリングしないnodeはtrueにすることでパフォーマンスが向上します。
// 画像自体を丸くする処理を書きます。このブロックはバックグラウンドスレッドで実行されます。
iconNode.imageModificationBlock = { image in
let modifiedImage: UIImage
let rect = CGRect(origin: .zero, size: image.size)
UIGraphicsBeginImageContextWithOptions(image.size, false, 0)
UIBezierPath(roundedRect: rect, cornerRadius: 25 * UIScreen.mainScreen().scale).addClip()
image.drawInRect(rect)
modifiedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return modifiedImage
}
// bodyNode: お知らせの本文
bodyNode.attributedText = NSAttributedString.couplesAttributedString(viewModel.body, color: UIColor.couplesColor000, fontSize: 14, bold: viewModel.isRead)
bodyNode.layerBacked = true
bodyNode.maximumNumberOfLines = 0
// dateNode: 時刻
dateNode.attributedText = NSAttributedString.couplesAttributedString(viewModel.date, color: UIColor.couplesColor005, fontSize: 12, bold: false)
dateNode.layerBacked = true
dateNode.maximumNumberOfLines = 1
// unreadNode: 未読の丸いマーク
unreadNode.layerBacked = true
}
}
UIKitオブジェクトの設定をする場合は、didLoad
メソッド内で行いましょう。メインスレッドで呼ばれます。
// ASNotificationCellNode.swift
extension ASNotificationCellNode {
override func didLoad() {
super.didLoad()
backgroundColor = viewModel.isRead ? UIColor.whiteColor() : UIColor.couplesColorBackground
// 丸い画像を作るextensionもAsyncDisplayKitには用意されています。
unreadNode.image = UIImage.as_resizableRoundedImageWithCornerRadius(5, cornerColor: UIColor.clearColor(), fillColor: UIColor.couplesColor200)
}
}
ASNotificationCellNodeのレイアウトを書く
セルのレイアウトを組んでいきます。画像内の番号とコードの番号は対応しています。コードが分割されていますが、すべてlayoutSpecThatFits
メソッドの中身になります。
// ASNotificationCellNode.swift
extension ASNotificationCellNode {
// このメソッド内でレイアウトを決定します。
// バックグラウンドスレッドで呼ばれることに気をつけてください。
override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec {
// 1. 画像のサイズを指定します。
iconNode.style.preferredSize = CGSize(width: 50, height: 50)
// 2. テキストをVerticalに並べます。
let textLayout = ASStackLayoutSpec(
direction: .Vertical,
spacing: 4, // 画像の緑のマージンになります。
justifyContent: .SpaceBetween, // bodyNodeの上端と、dateNodeの下端がtextLayoutの上下端になります。
alignItems: .Start, // bodyNodeとdateNodeの左端を揃えます。
children: [bodyNode, dateNode]
)
// 縮んだり、伸びたりすることを防ぎ、TextNodeの文字がちょうど収まるようにレイアウトします。
textLayout.style.flexShrink = 1.0
textLayout.style.flexGrow = 1.0
// 3. Horizontalに並べます。
// このあと、HorizontalなStackLayoutSpecで囲んであげるので、
// textLayoutの左右のマージンになります。図の緑のマージンです。
textLayout.style.spacingBefore = 10.0 // 左のマージン
textLayout.style.spacingAfter = 16.0 // 右のマージン
// 既読と未読でレイアウトするnodeを分けます。
let horizontalNodes: [ASLayoutElement]
if viewModel.isRead {
horizontalNodes = [iconNode, textLayout]
}
else {
unreadNode.style.preferredSize = CGSize(width: 10, height: 10)
horizontalNodes = [iconNode, textLayout, unreadNode]
}
let horizontalStack = ASStackLayoutSpec(
direction: .Horizontal,
spacing: 0, // textLayout.style.spacingBefore等でマージンは指定してあるので、spacingは0です。
justifyContent: .SpaceBetween,
alignItems: .Center, // Vertical方向にセンタリングします。
children: horizontalNodes
)
// 4. 上下左右のinsetを指定します。
// layoutSpecThatFitsのメソッドではASLayoutSpecを返します。
return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 20), child: horizontalStack)
}
// layoutSpecThatFitsの終わり
}
ASTableDelegateでフェッチのロジックを書く
これが最後のパートになります。ASTableNodeはデフォルトで画面サイズの2画面分先までをプリフェッチするようになっています。その際に呼ばれるメソッドが2つあります。
// ASNotificationViewController.swift
extension ASNotificationViewController: ASTableDelegate {
// フェッチをするかどうかです。
// プリフェッチをする領域までスクロールした場合に、
// バックグラウンドスレッドで呼ばれます。
func shouldBatchFetchForTableNode(tableNode: ASTableNode) -> Bool {
return true
}
// フェッチの実行部分です。
// バックグラウンドスレッドで呼ばれます。
func tableNode(tableNode: ASTableNode, willBeginBatchFetchWithContext context: ASBatchContext) {
// Notificationをフェッチするコードをここに書きます。
fetchNotifications(completion: { () -> Void in
let insertedIndexPathes: [NSIndexPath]() = ...
// IndexPathの配列を渡して、Insertをします。
tableNode.insertRowsAtIndexPaths(insertedIndexPathes, withRowAnimation: .Fade)
// 最後にフェッチが完了したことを伝えます。trueは成功したことを意味します。
context.completeBatchFetching(true)
}
}
}
以上になります!
最後に
AsyncDisplayKitを使用することでフレームレートを向上させることができました。
また、レイアウトやプリフェッチのコードを簡単に書けることが分かりました。
AsyncDisplayKitの存在は知っているけど、手が出せていない方はぜひ挑戦してみてください。
ただし、AsyncDisplayKitで実現できないUIも存在するかもしれません。そのため、アプリ全体で採用していくのはリスクだと思います。
明日の記事は恩田さんの「Terraformを約1年運用して学んだトラブルパターン4選」になります!
エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!
この記事は Eureka Advent Calendar 2016 14日目 の記事です。
13日目は 香取さん の「今日から始めるDeep Learning」でした。
こんにちは。Couples事業部でiOSアプリの開発を担当している丹です!
今回はFacebookとPinterestがオープンソースとして公開しているAsyncDisplayKitをCouplesで使ってみたので、導入方法を紹介したいと思います。
AsyncDisplayKitはViewのレイアウトを非同期に扱うことで、60FPSのスムーズなUIを実現するためのライブラリです。先日(2016年12月9日)、バージョン2.0になり、安定してきたようなので本格的に導入しても良いと思っています。Swiftのサンプルコードはまだ少ないので、参考になると嬉しいです。
facebook/AsyncDisplayKit
環境
- AsyncDisplayKit 2.0
- Swift 2.3
- Xcode 8.1(8B62)
AsyncDisplayKitの特徴
AsyncDisplayKitではNodeと呼ばれるViewを抽象化したオブジェクトを扱います。ベースのクラスはASDisplayNode
です。UIView
はメインスレッドでしか動作しませんが、ASDisplayNode
はスレッドセーフでバックグラウンドスレッドでも動作します。そのおかげでメインスレッドをブロックしないスムーズなUIを実現することができます。
NodeとNode Container
NodeはNode Containerの中で扱う必要があります。NodeやNode ContainerはUIKitとの対応関係を知ると分かりやすいと思うので、以下に一覧を載せておきます。
Node – AsyncDisplayKit | Node Subclasses
- ASDisplayNode / UIView
- ASCellNode / UITableViewCell & UICollectionViewCell
- ASScrollNode / UIScrollView
- ASEditableTextNode / UITextView
- ASTextNode / UILabel
- ASImageNode & ASNetworkImageNode & ASMultiplexImageNode / UIImage
- ASVideoNode / AVPlayerLayer
- ASVideoPlayerNode / UIMoviePlayer
- ASControlNode / UIControl
- ASButtonNode / UIButton
- ASMapNode / MKMapView
Node Container – AsyncDisplayKit | Node Containers
- ASViewController / UIViewController
- ASNavigationController / UINavigationController
- ASTabBarController / UITabBarController
- ASPagerNode / UIPageViewController
- ASCollectionNode / UICollectionView
- ASTableNode / UITableView
AsyncDisplayKitのレイアウト方法
AsyncDisplayKitはStoryboardやInterface Builderを使用せず、レイアウトをすべてコードで書く必要があります。ドキュメントを全部読んだ感想としては、AsyncDisplayKitの導入はレイアウトの組み方をマスターできるかにかかっています。コードはあとで紹介するので、ここでは概要だけ説明します。
AsyncDisplayKitではASLayoutSpec
というオブジェクトを使って、レイアウトを組みます。ASLayoutSpec
を使ったレイアウトの計算は、以下の2点の理由からAutoLayoutよりも断然速くなります。
- マニュアルレイアウトと同等の速度(複雑なレイアウトではAutoLayoutは遅くなります)
- バックグラウンドかつ並列の計算
ASLayoutSpec
はASLayoutElement
プロトコルを採用しているASLayoutSpec
とASDisplayNode
を扱うことができます。つまり、以下のような入れ子構造が可能になります。
ASLayoutSpec |- ASLayoutSpec |- ASDisplayNode
ASLayoutSpec
には以下のサブクラスが用意されています。詳しくは AsyncDisplayKit | Layout Specsをご覧ください。
- ASInsetLayoutSpec
- ASOverlayLayoutSpec
- ASBackgroundLayoutSpec
- ASCenterLayoutSpec
- ASRatioLayoutSpec
- ASStackLayoutSpec
- ASAbsoluteLayoutSpec
Couplesのお知らせ画面をAsyncDisplayKitに置き換える
百聞は一見に如かずということで、実際のコードを見ていきます。今回適用する画面はこちらのお知らせ画面です。シンプルなTableViewです。
ViewControllerを書く
ASViewController
とUIViewController
、ASTableNode
とUITableView
は似たインターフェースを持っています。まずは、ViewControllerのイニシャライザとライフサイクルのコードを書きます。
// ASNotificationViewController.swift import UIKit import AsyncDisplayKit // ASViewControllerのサブクラスにします。 final class ASNotificationViewController: ASViewController { // クラス内で扱いやすくするため、nodeをASTableNodeにキャストします。 var tableNode: ASTableNode { return node as! ASTableNode } // ASTableNodeでnodeを初期化します。 // init内ではself.view, self.node.viewにアクセスしてはいけません。 init() { super.init(node: ASTableNode()) tableNode.dataSource = self // ASTableDataSource tableNode.delegate = self // ASTableDataSource } override func viewDidLoad() { super.viewDidLoad() // メインスレッドなので、ここでViewのセットアップをしましょう。 // tableNode.viewでASTableView(UITableViewのサブクラス)にアクセスできます。 tableNode.view.tableFooterView = UIView() tableNode.view.backgroundColor = ... }
DataSourceを書く
ASTableDataSource
もUITableViewDataSource
と似たインターフェースを持っています。ASCellNodeBlock
を返すメソッドは注意事項がたくさんあるので、気をつけてください。
// ASNotificationViewController.swift extension ASNotificationViewController: ASTableDataSource { func numberOfSections(in tableNode: ASTableNode) -> Int { return 1 } func tableNode(tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { // CoreDataでフェッチ済みのオブジェクト数を返します。 return notifications.numberOfObjects() } // tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)に当たります。 // ちなみに、ASCellNodeは再利用されません。 func tableNode(tableNode: ASTableNode, nodeBlockForRowAtIndexPath indexPath: NSIndexPath) -> ASCellNodeBlock { // CoreDataのNotificationオブジェクトを取得します。 let notification: Notification = ... // nodeに渡す際にスレッドセーフなオブジェクトに変換してあげる必要があります。NotificationViewModelはstructです。 let viewModel = NotificationViewModel(notification) // typealias ASCellNodeBlock = () -> ASCellNode // このブロックはバックグラウンドスレッドで実行されます。 // ブロック実行時にindexPathが無効になっている可能性があるので、ブロック内でindexPathにアクセスすべきではありません。 let block: ASCellNodeBlock = { ASNotificationCellNode(viewModel: viewModel) } return block } }
上記のコードに出てきたViewModelのインターフェースです。
struct NotificationViewModel { let body: String let date: String let imageURL: String let isRead: Bool }
ASNotificationCellNodeを書く
お知らせのセルは、アイコン画像、お知らせの本文、時刻、未読の丸いマークの4つを含んでいます。
既読時
// ASNotificationCellNode.swift import UIKit import AsyncDisplayKit final class ASNotificationCellNode: ASCellNode { let viewModel: NotificationViewModel private let iconNode = ASNetworkImageNode() private let messageNode = ASTextNode() private let dateNode = ASTextNode() private let unreadNode = ASImageNode() // ASCellNodeBlock内で呼ばれるため、initはバックグラウンドスレッドで動作します。 init(viewModel: NotificationViewModel) { self.viewModel = viewModel super.init() // trueにするとnodeの追加などを自動でやってくれます。 automaticallyManagesSubnodes = true // iconNode: アイコン画像 iconNode.URL = NSURL(string: viewModel.imageURL)! iconNode.layerBacked = true // タッチをハンドリングしないnodeはtrueにすることでパフォーマンスが向上します。 // 画像自体を丸くする処理を書きます。このブロックはバックグラウンドスレッドで実行されます。 iconNode.imageModificationBlock = { image in let modifiedImage: UIImage let rect = CGRect(origin: .zero, size: image.size) UIGraphicsBeginImageContextWithOptions(image.size, false, 0) UIBezierPath(roundedRect: rect, cornerRadius: 25 * UIScreen.mainScreen().scale).addClip() image.drawInRect(rect) modifiedImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return modifiedImage } // bodyNode: お知らせの本文 bodyNode.attributedText = NSAttributedString.couplesAttributedString(viewModel.body, color: UIColor.couplesColor000, fontSize: 14, bold: viewModel.isRead) bodyNode.layerBacked = true bodyNode.maximumNumberOfLines = 0 // dateNode: 時刻 dateNode.attributedText = NSAttributedString.couplesAttributedString(viewModel.date, color: UIColor.couplesColor005, fontSize: 12, bold: false) dateNode.layerBacked = true dateNode.maximumNumberOfLines = 1 // unreadNode: 未読の丸いマーク unreadNode.layerBacked = true } }
UIKitオブジェクトの設定をする場合は、didLoad
メソッド内で行いましょう。メインスレッドで呼ばれます。
// ASNotificationCellNode.swift extension ASNotificationCellNode { override func didLoad() { super.didLoad() backgroundColor = viewModel.isRead ? UIColor.whiteColor() : UIColor.couplesColorBackground // 丸い画像を作るextensionもAsyncDisplayKitには用意されています。 unreadNode.image = UIImage.as_resizableRoundedImageWithCornerRadius(5, cornerColor: UIColor.clearColor(), fillColor: UIColor.couplesColor200) } }
ASNotificationCellNodeのレイアウトを書く
セルのレイアウトを組んでいきます。画像内の番号とコードの番号は対応しています。コードが分割されていますが、すべてlayoutSpecThatFits
メソッドの中身になります。
// ASNotificationCellNode.swift extension ASNotificationCellNode { // このメソッド内でレイアウトを決定します。 // バックグラウンドスレッドで呼ばれることに気をつけてください。 override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { // 1. 画像のサイズを指定します。 iconNode.style.preferredSize = CGSize(width: 50, height: 50)
// 2. テキストをVerticalに並べます。 let textLayout = ASStackLayoutSpec( direction: .Vertical, spacing: 4, // 画像の緑のマージンになります。 justifyContent: .SpaceBetween, // bodyNodeの上端と、dateNodeの下端がtextLayoutの上下端になります。 alignItems: .Start, // bodyNodeとdateNodeの左端を揃えます。 children: [bodyNode, dateNode] ) // 縮んだり、伸びたりすることを防ぎ、TextNodeの文字がちょうど収まるようにレイアウトします。 textLayout.style.flexShrink = 1.0 textLayout.style.flexGrow = 1.0
// 3. Horizontalに並べます。 // このあと、HorizontalなStackLayoutSpecで囲んであげるので、 // textLayoutの左右のマージンになります。図の緑のマージンです。 textLayout.style.spacingBefore = 10.0 // 左のマージン textLayout.style.spacingAfter = 16.0 // 右のマージン // 既読と未読でレイアウトするnodeを分けます。 let horizontalNodes: [ASLayoutElement] if viewModel.isRead { horizontalNodes = [iconNode, textLayout] } else { unreadNode.style.preferredSize = CGSize(width: 10, height: 10) horizontalNodes = [iconNode, textLayout, unreadNode] } let horizontalStack = ASStackLayoutSpec( direction: .Horizontal, spacing: 0, // textLayout.style.spacingBefore等でマージンは指定してあるので、spacingは0です。 justifyContent: .SpaceBetween, alignItems: .Center, // Vertical方向にセンタリングします。 children: horizontalNodes )
// 4. 上下左右のinsetを指定します。 // layoutSpecThatFitsのメソッドではASLayoutSpecを返します。 return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 15, left: 12, bottom: 15, right: 20), child: horizontalStack) } // layoutSpecThatFitsの終わり }
ASTableDelegateでフェッチのロジックを書く
これが最後のパートになります。ASTableNodeはデフォルトで画面サイズの2画面分先までをプリフェッチするようになっています。その際に呼ばれるメソッドが2つあります。
// ASNotificationViewController.swift extension ASNotificationViewController: ASTableDelegate { // フェッチをするかどうかです。 // プリフェッチをする領域までスクロールした場合に、 // バックグラウンドスレッドで呼ばれます。 func shouldBatchFetchForTableNode(tableNode: ASTableNode) -> Bool { return true } // フェッチの実行部分です。 // バックグラウンドスレッドで呼ばれます。 func tableNode(tableNode: ASTableNode, willBeginBatchFetchWithContext context: ASBatchContext) { // Notificationをフェッチするコードをここに書きます。 fetchNotifications(completion: { () -> Void in let insertedIndexPathes: [NSIndexPath]() = ... // IndexPathの配列を渡して、Insertをします。 tableNode.insertRowsAtIndexPaths(insertedIndexPathes, withRowAnimation: .Fade) // 最後にフェッチが完了したことを伝えます。trueは成功したことを意味します。 context.completeBatchFetching(true) } } }
以上になります!
最後に
AsyncDisplayKitを使用することでフレームレートを向上させることができました。
また、レイアウトやプリフェッチのコードを簡単に書けることが分かりました。
AsyncDisplayKitの存在は知っているけど、手が出せていない方はぜひ挑戦してみてください。
ただし、AsyncDisplayKitで実現できないUIも存在するかもしれません。そのため、アプリ全体で採用していくのはリスクだと思います。
明日の記事は恩田さんの「Terraformを約1年運用して学んだトラブルパターン4選」になります!
エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!