diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 974b390886..f93552b3c0 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -13,13 +13,19 @@ Pod::Spec.new do |spec| 'AsyncDisplayKit/*.h', 'AsyncDisplayKit/Details/**/*.h', 'AsyncDisplayKit/Layout/*.h', - 'AsyncDisplayKit/TextKit/*.h', - 'Base/*.h' + 'Base/*.h', + 'AsyncDisplayKit/TextKit/ASTextNodeTypes.h' ] spec.source_files = [ 'AsyncDisplayKit/**/*.{h,m,mm}', - 'Base/*.{h,m}' + 'Base/*.{h,m}', + + # Most TextKit components are not public because the C++ content + # in the headers will cause build errors when using + # `use_frameworks!` on 0.39.0 & Swift 2.1. + # See https://github.com/facebook/AsyncDisplayKit/issues/1153 + 'AsyncDisplayKit/TextKit/*.h', ] spec.frameworks = 'AssetsLibrary' diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index 63df8deed9..2f2c7136ba 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -165,28 +165,24 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell - (void)tableView:(UITableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - sleep(1); - dispatch_async(dispatch_get_main_queue(), ^{ - - // populate a new array of random-sized kittens - NSArray *moarKittens = [self createLitterWithSize:kLitterBatchSize]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + // populate a new array of random-sized kittens + NSArray *moarKittens = [self createLitterWithSize:kLitterBatchSize]; - NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; - - // find number of kittens in the data source and create their indexPaths - NSInteger existingRows = _kittenDataSource.count + 1; - - for (NSInteger i = 0; i < moarKittens.count; i++) { - [indexPaths addObject:[NSIndexPath indexPathForRow:existingRows + i inSection:0]]; - } + NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; + + // find number of kittens in the data source and create their indexPaths + NSInteger existingRows = _kittenDataSource.count + 1; + + for (NSInteger i = 0; i < moarKittens.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:existingRows + i inSection:0]]; + } - // add new kittens to the data source & notify table of new indexpaths - [_kittenDataSource addObjectsFromArray:moarKittens]; - [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + // add new kittens to the data source & notify table of new indexpaths + [_kittenDataSource addObjectsFromArray:moarKittens]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; - [context completeBatchFetching:YES]; - }); + [context completeBatchFetching:YES]; }); } diff --git a/examples/Swift/Podfile b/examples/Swift/Podfile index 6c012e3c04..d76ea6718b 100644 --- a/examples/Swift/Podfile +++ b/examples/Swift/Podfile @@ -1,3 +1,6 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' + +use_frameworks! + pod 'AsyncDisplayKit', :path => '../..' diff --git a/examples/Swift/Sample.xcodeproj/project.pbxproj b/examples/Swift/Sample.xcodeproj/project.pbxproj index 6675b3b34c..18f8aba889 100644 --- a/examples/Swift/Sample.xcodeproj/project.pbxproj +++ b/examples/Swift/Sample.xcodeproj/project.pbxproj @@ -10,9 +10,10 @@ 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7319D22E19004363C2 /* AppDelegate.swift */; }; 050E7C7619D22E19004363C2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050E7C7519D22E19004363C2 /* ViewController.swift */; }; 05DDD8DB19D2336300013C30 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */; }; - 4690009EF79C47BBA8FDBAD4 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2ACC614D420B4E90B7EE3BCE /* libPods.a */; }; 6C5053DB19EE266A00E385DE /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053D919EE266A00E385DE /* Default-667h@2x.png */; }; 6C5053DC19EE266A00E385DE /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */; }; + 92E46E91A7D47AEC5B2B2F55 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FC29F18AE7C8C204A5CD4F2 /* Pods.framework */; }; + CCB01CAB1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB01CAA1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -21,11 +22,11 @@ 050E7C7319D22E19004363C2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 050E7C7519D22E19004363C2 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; - 05DDD8DC19D2341D00013C30 /* AsyncDisplayKit-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AsyncDisplayKit-Bridging-Header.h"; sourceTree = ""; }; - 2ACC614D420B4E90B7EE3BCE /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; 6C5053D919EE266A00E385DE /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; 6C5053DA19EE266A00E385DE /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + 7FC29F18AE7C8C204A5CD4F2 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 841652076B3E9351337AA7C7 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + CCB01CAA1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TailLoadingCellNode.swift; sourceTree = ""; }; E3EE87D12CE3EF73FAE2EF02 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -34,7 +35,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4690009EF79C47BBA8FDBAD4 /* libPods.a in Frameworks */, + 92E46E91A7D47AEC5B2B2F55 /* Pods.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -65,6 +66,7 @@ 050E7C7019D22E19004363C2 /* Sample */ = { isa = PBXGroup; children = ( + CCB01CAA1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift */, 050E7C7319D22E19004363C2 /* AppDelegate.swift */, 050E7C7519D22E19004363C2 /* ViewController.swift */, 050E7C7119D22E19004363C2 /* Supporting Files */, @@ -75,7 +77,6 @@ 050E7C7119D22E19004363C2 /* Supporting Files */ = { isa = PBXGroup; children = ( - 05DDD8DC19D2341D00013C30 /* AsyncDisplayKit-Bridging-Header.h */, 050E7C7219D22E19004363C2 /* Info.plist */, 05DDD8DA19D2336300013C30 /* Default-568h@2x.png */, 6C5053D919EE266A00E385DE /* Default-667h@2x.png */, @@ -87,7 +88,7 @@ 092C2001FE124604891D6E90 /* Frameworks */ = { isa = PBXGroup; children = ( - 2ACC614D420B4E90B7EE3BCE /* libPods.a */, + 7FC29F18AE7C8C204A5CD4F2 /* Pods.framework */, ); name = Frameworks; sourceTree = ""; @@ -113,6 +114,7 @@ 050E7C6B19D22E19004363C2 /* Frameworks */, 050E7C6C19D22E19004363C2 /* Resources */, 941C5E41C54B4613A2D3B760 /* Copy Pods Resources */, + 1F5A9F09F5875F61862D0783 /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -131,7 +133,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0600; + LastUpgradeCheck = 0720; ORGANIZATIONNAME = Facebook; TargetAttributes = { 050E7C6D19D22E19004363C2 = { @@ -171,6 +173,21 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1F5A9F09F5875F61862D0783 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 941C5E41C54B4613A2D3B760 /* Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -208,6 +225,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CCB01CAB1C5FEA6E00CA64C4 /* TailLoadingCellNode.swift in Sources */, 050E7C7619D22E19004363C2 /* ViewController.swift in Sources */, 050E7C7419D22E19004363C2 /* AppDelegate.swift in Sources */, ); @@ -236,6 +254,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -300,8 +319,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = Sample/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Sample/AsyncDisplayKit-Bridging-Header.h"; }; name = Debug; }; @@ -312,8 +331,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = Sample/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Sample/AsyncDisplayKit-Bridging-Header.h"; }; name = Release; }; diff --git a/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme index 8d7f73e325..f7f575e824 100644 --- a/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme +++ b/examples/Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -38,15 +38,18 @@ ReferencedContainer = "container:Sample.xcodeproj"> + + @@ -62,10 +65,10 @@ diff --git a/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata b/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/Swift/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/Swift/Sample/AppDelegate.swift b/examples/Swift/Sample/AppDelegate.swift index a2b00b1727..3a1dac68c1 100644 --- a/examples/Swift/Sample/AppDelegate.swift +++ b/examples/Swift/Sample/AppDelegate.swift @@ -19,7 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { let window = UIWindow(frame: UIScreen.mainScreen().bounds) window.backgroundColor = UIColor.whiteColor() - window.rootViewController = ViewController(nibName: nil, bundle: nil) + window.rootViewController = ViewController() window.makeKeyAndVisible() self.window = window return true diff --git a/examples/Swift/Sample/AsyncDisplayKit-Bridging-Header.h b/examples/Swift/Sample/AsyncDisplayKit-Bridging-Header.h deleted file mode 100644 index e5488e4ee6..0000000000 --- a/examples/Swift/Sample/AsyncDisplayKit-Bridging-Header.h +++ /dev/null @@ -1,12 +0,0 @@ -/* This file provided by Facebook is for non-commercial testing and evaluation - * purposes only. Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN - * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#import diff --git a/examples/Swift/Sample/Info.plist b/examples/Swift/Sample/Info.plist index 35d842827b..fb4115c84c 100644 --- a/examples/Swift/Sample/Info.plist +++ b/examples/Swift/Sample/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/examples/Swift/Sample/TailLoadingCellNode.swift b/examples/Swift/Sample/TailLoadingCellNode.swift new file mode 100644 index 0000000000..e6b4d333f0 --- /dev/null +++ b/examples/Swift/Sample/TailLoadingCellNode.swift @@ -0,0 +1,53 @@ +// +// TailLoadingCellNode.swift +// Sample +// +// Created by Adlai Holler on 2/1/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +import AsyncDisplayKit +import UIKit + +final class TailLoadingCellNode: ASCellNode { + let spinner = SpinnerNode() + let text = ASTextNode() + + override init() { + super.init() + addSubnode(text) + text.attributedString = NSAttributedString( + string: "Loading…", + attributes: [ + NSFontAttributeName: UIFont.systemFontOfSize(12), + NSForegroundColorAttributeName: UIColor.lightGrayColor(), + NSKernAttributeName: -0.3 + ]) + addSubnode(spinner) + } + + override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { + return ASStackLayoutSpec( + direction: .Horizontal, + spacing: 16, + justifyContent: .Center, + alignItems: .Center, + children: [ text, spinner ]) + } +} + +final class SpinnerNode: ASDisplayNode { + var activityIndicatorView: UIActivityIndicatorView { + return view as! UIActivityIndicatorView + } + + override init() { + super.init(viewBlock: { UIActivityIndicatorView(activityIndicatorStyle: .Gray) }, didLoadBlock: nil) + preferredFrameSize.height = 32 + } + + override func didLoad() { + super.didLoad() + activityIndicatorView.startAnimating() + } +} \ No newline at end of file diff --git a/examples/Swift/Sample/ViewController.swift b/examples/Swift/Sample/ViewController.swift index 037e0965e8..dec8710f86 100644 --- a/examples/Swift/Sample/ViewController.swift +++ b/examples/Swift/Sample/ViewController.swift @@ -10,57 +10,136 @@ */ import UIKit +import AsyncDisplayKit -class ViewController: UIViewController, ASTableViewDataSource, ASTableViewDelegate { +final class ViewController: ASViewController, ASTableDataSource, ASTableDelegate { - var tableView: ASTableView + struct State { + var itemCount: Int + var fetchingMore: Bool + static let empty = State(itemCount: 20, fetchingMore: false) + } + enum Action { + case BeginBatchFetch + case EndBatchFetch(resultCount: Int) + } - // MARK: UIViewController. + var tableNode: ASTableNode { + return node as! ASTableNode + } - override required init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { - self.tableView = ASTableView() + private(set) var state: State = .empty - super.init(nibName: nil, bundle: nil) - - self.tableView.asyncDataSource = self - self.tableView.asyncDelegate = self + init() { + super.init(node: ASTableNode()) + tableNode.delegate = self + tableNode.dataSource = self } required init?(coder aDecoder: NSCoder) { fatalError("storyboards are incompatible with truth and beauty") } - override func viewDidLoad() { - super.viewDidLoad() - self.view.addSubview(self.tableView) - } - - override func viewWillLayoutSubviews() { - self.tableView.frame = self.view.bounds - } - override func prefersStatusBarHidden() -> Bool { return true } - // MARK: ASTableView data source and delegate. - func tableView(tableView: ASTableView!, nodeForRowAtIndexPath indexPath: NSIndexPath!) -> ASCellNode! { - let patter = NSString(format: "[%ld.%ld] says hello!", indexPath.section, indexPath.row) + func tableView(tableView: ASTableView, nodeForRowAtIndexPath indexPath: NSIndexPath) -> ASCellNode { + // Should read the row count directly from table view but + // https://github.com/facebook/AsyncDisplayKit/issues/1159 + let rowCount = self.tableView(tableView, numberOfRowsInSection: 0) + + if state.fetchingMore && indexPath.row == rowCount - 1 { + return TailLoadingCellNode() + } + let node = ASTextCellNode() - node.text = patter as String + node.text = String(format: "[%ld.%ld] says hello!", indexPath.section, indexPath.row) return node } - func numberOfSectionsInTableView(tableView: UITableView!) -> Int { + func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 } - func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int { - return 20 + func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + var count = state.itemCount + if state.fetchingMore { + count += 1 + } + return count } + func tableView(tableView: ASTableView, willBeginBatchFetchWithContext context: ASBatchContext) { + /// This call will come in on a background thread. Switch to main + /// to add our spinner, then fire off our fetch. + dispatch_async(dispatch_get_main_queue()) { + let oldState = self.state + self.state = ViewController.handleAction(.BeginBatchFetch, fromState: oldState) + self.renderDiff(oldState) + } + + ViewController.fetchDataWithCompletion { resultCount in + let action = Action.EndBatchFetch(resultCount: resultCount) + let oldState = self.state + self.state = ViewController.handleAction(action, fromState: oldState) + self.renderDiff(oldState) + context.completeBatchFetching(true) + } + } + + private func renderDiff(oldState: State) { + let tableView = tableNode.view + tableView.beginUpdates() + + // Add or remove items + let rowCountChange = state.itemCount - oldState.itemCount + if rowCountChange > 0 { + let indexPaths = (oldState.itemCount.. Void) { + let time = dispatch_time(DISPATCH_TIME_NOW, Int64(NSTimeInterval(NSEC_PER_SEC) * 0.5)) + dispatch_after(time, dispatch_get_main_queue()) { + let resultCount = Int(arc4random_uniform(20)) + completion(resultCount) + } + } + + private static func handleAction(action: Action, var fromState state: State) -> State { + switch action { + case .BeginBatchFetch: + state.fetchingMore = true + case let .EndBatchFetch(resultCount): + state.itemCount += resultCount + state.fetchingMore = false + } + return state + } }