diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index b594cfcce2..bc980bba13 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -94,13 +94,12 @@ - (void)setNeedsLayout { - ASDisplayNodeAssertThreadAffinity(self); CGSize oldSize = self.calculatedSize; [super setNeedsLayout]; if (_layoutDelegate != nil && self.isNodeLoaded) { - BOOL sizeChanged = !CGSizeEqualToSize(oldSize, self.calculatedSize); ASPerformBlockOnMainThread(^{ + BOOL sizeChanged = !CGSizeEqualToSize(oldSize, self.calculatedSize); [_layoutDelegate nodeDidRelayout:self sizeChanged:sizeChanged]; }); } diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index e9ebbab682..d874378617 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -628,7 +628,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize completion:(void(^)())completion { - ASDisplayNodeAssertThreadAffinity(self); ASDN::MutexLocker l(_propertyLock); if (![self __shouldSize]) return nil; @@ -1868,7 +1867,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize { - ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); if (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) { ASLayoutSpec *layoutSpec = [self layoutSpecThatFits:constrainedSize]; layoutSpec.isMutable = NO; @@ -1895,25 +1894,25 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { - ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); return _preferredFrameSize; } - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { - ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); return nil; } - (ASLayout *)calculatedLayout { - ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); return _layout; } - (CGSize)calculatedSize { - ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); return _layout.size; } @@ -1944,7 +1943,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)invalidateCalculatedLayout { - ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); // This will cause -measureWithSizeRange: to actually compute the size instead of returning the previously cached size _flags.isMeasured = NO; } diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index 6cbbb6c518..86c04ba4e2 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -335,9 +335,9 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { _pendingVie { _bridge_prologue_write; _setToLayer(opaque, newOpaque); - - // FIXME: Would like to setNeedsDisplay if opaqueness changed, but - // not safe to read old value in background. + // NOTE: If we're in the background, then when the pending state + // is applied to the view on main, we will call `setNeedsDisplay` if + // the new opaque value doesn't match the one on the layer. } - (BOOL)isUserInteractionEnabled @@ -539,8 +539,9 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { _pendingVie { _bridge_prologue_write; _setToLayer(backgroundColor, newBackgroundColor.CGColor); - // FIXME: Would like to setNeedsDisplay if background color changed, but - // not safe to read old color in background. + // NOTE: If we're in the background, then when the pending state + // is applied to the view on main, we will call `setNeedsDisplay` if + // the new background color doesn't match the one on the layer. } - (UIColor *)tintColor diff --git a/AsyncDisplayKit/Private/_ASPendingState.m b/AsyncDisplayKit/Private/_ASPendingState.m index 0e522e9f28..09bb916597 100644 --- a/AsyncDisplayKit/Private/_ASPendingState.m +++ b/AsyncDisplayKit/Private/_ASPendingState.m @@ -557,6 +557,13 @@ static UIColor *defaultTintColor = nil; - (void)applyToLayer:(CALayer *)layer { ASPendingStateFlags flags = _flags; + + if (flags.needsDisplay + || (flags.setOpaque && opaque != layer.opaque) + || (flags.setBackgroundColor && !CGColorEqualToColor(backgroundColor, layer.backgroundColor))) { + [layer setNeedsDisplay]; + } + if (flags.setAnchorPoint) layer.anchorPoint = anchorPoint; @@ -629,9 +636,6 @@ static UIColor *defaultTintColor = nil; if (flags.setEdgeAntialiasingMask) layer.edgeAntialiasingMask = edgeAntialiasingMask; - if (flags.needsDisplay) - [layer setNeedsDisplay]; - if (flags.needsLayout) [layer setNeedsLayout]; @@ -658,6 +662,12 @@ static UIColor *defaultTintColor = nil; CALayer *layer = view.layer; ASPendingStateFlags flags = _flags; + if (flags.needsDisplay + || (flags.setOpaque && opaque != view.opaque) + || (flags.setBackgroundColor && !CGColorEqualToColor(backgroundColor, layer.backgroundColor))) { + [view setNeedsDisplay]; + } + if (flags.setAnchorPoint) layer.anchorPoint = anchorPoint; @@ -752,9 +762,6 @@ static UIColor *defaultTintColor = nil; if (flags.setEdgeAntialiasingMask) layer.edgeAntialiasingMask = edgeAntialiasingMask; - if (flags.needsDisplay) - [view setNeedsDisplay]; - if (flags.needsLayout) [view setNeedsLayout]; diff --git a/examples/BackgroundPropertySetting/BackgroundPropertySetting.xcodeproj/project.pbxproj b/examples/BackgroundPropertySetting/BackgroundPropertySetting.xcodeproj/project.pbxproj index 1e3b07f3c0..a90b8ebb7b 100644 --- a/examples/BackgroundPropertySetting/BackgroundPropertySetting.xcodeproj/project.pbxproj +++ b/examples/BackgroundPropertySetting/BackgroundPropertySetting.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ CCD3736F1C751C8A00AB7199 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCD3736E1C751C8A00AB7199 /* ViewController.swift */; }; CCD373741C751C8A00AB7199 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CCD373731C751C8A00AB7199 /* Assets.xcassets */; }; CCD373771C751C8A00AB7199 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CCD373751C751C8A00AB7199 /* LaunchScreen.storyboard */; }; + CCD3737F1C7520AB00AB7199 /* DemoCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCD3737E1C7520AB00AB7199 /* DemoCellNode.swift */; }; + CCD373811C75228900AB7199 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCD373801C75228900AB7199 /* Utilities.swift */; }; FE56E788869496B3522E8AE2 /* Pods.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D966CA4D089E4178A58E447C /* Pods.framework */; }; /* End PBXBuildFile section */ @@ -23,6 +25,8 @@ CCD373731C751C8A00AB7199 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; CCD373761C751C8A00AB7199 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; CCD373781C751C8A00AB7199 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CCD3737E1C7520AB00AB7199 /* DemoCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoCellNode.swift; sourceTree = ""; }; + CCD373801C75228900AB7199 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; D966CA4D089E4178A58E447C /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -78,6 +82,8 @@ children = ( CCD3736C1C751C8A00AB7199 /* AppDelegate.swift */, CCD3736E1C751C8A00AB7199 /* ViewController.swift */, + CCD373801C75228900AB7199 /* Utilities.swift */, + CCD3737E1C7520AB00AB7199 /* DemoCellNode.swift */, CCD373731C751C8A00AB7199 /* Assets.xcassets */, CCD373751C751C8A00AB7199 /* LaunchScreen.storyboard */, CCD373781C751C8A00AB7199 /* Info.plist */, @@ -206,7 +212,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CCD3737F1C7520AB00AB7199 /* DemoCellNode.swift in Sources */, CCD3736F1C751C8A00AB7199 /* ViewController.swift in Sources */, + CCD373811C75228900AB7199 /* Utilities.swift in Sources */, CCD3736D1C751C8A00AB7199 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/examples/BackgroundPropertySetting/BackgroundPropertySetting/DemoCellNode.swift b/examples/BackgroundPropertySetting/BackgroundPropertySetting/DemoCellNode.swift new file mode 100644 index 0000000000..63524a540d --- /dev/null +++ b/examples/BackgroundPropertySetting/BackgroundPropertySetting/DemoCellNode.swift @@ -0,0 +1,87 @@ +// +// DemoCellNode.swift +// BackgroundPropertySetting +// +// Created by Adlai Holler on 2/17/16. +// Copyright © 2016 Adlai Holler. All rights reserved. +// + +import UIKit +import AsyncDisplayKit + +final class DemoCellNode: ASCellNode { + let childA = ASDisplayNode() + let childB = ASDisplayNode() + var state = State.Right + + override init() { + super.init() + usesImplicitHierarchyManagement = true + } + + override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec { + let specA = ASRatioLayoutSpec(ratio: 1, child: childA) + specA.flexBasis = ASRelativeDimensionMakeWithPoints(1) + specA.flexGrow = true + let specB = ASRatioLayoutSpec(ratio: 1, child: childB) + specB.flexBasis = ASRelativeDimensionMakeWithPoints(1) + specB.flexGrow = true + let children = state.isReverse ? [ specB, specA ] : [ specA, specB ] + let direction: ASStackLayoutDirection = state.isVertical ? .Vertical : .Horizontal + return ASStackLayoutSpec(direction: direction, + spacing: 20, + justifyContent: .SpaceAround, + alignItems: .Center, + children: children) + } + + override func animateLayoutTransition(context: ASContextTransitioning!) { + childA.frame = context.initialFrameForNode(childA) + childB.frame = context.initialFrameForNode(childB) + let tinyDelay = drand48() / 10 + UIView.animateWithDuration(0.5, delay: tinyDelay, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.5, options: .BeginFromCurrentState, animations: { () -> Void in + self.childA.frame = context.finalFrameForNode(self.childA) + self.childB.frame = context.finalFrameForNode(self.childB) + }, completion: { + context.completeTransition($0) + }) + } + + enum State { + case Right + case Up + case Left + case Down + + var isVertical: Bool { + switch self { + case .Up, .Down: + return true + default: + return false + } + } + + var isReverse: Bool { + switch self { + case .Left, .Up: + return true + default: + return false + } + } + + mutating func advance() { + switch self { + case .Right: + self = .Up + case .Up: + self = .Left + case .Left: + self = .Down + case .Down: + self = .Right + } + } + } +} diff --git a/examples/BackgroundPropertySetting/BackgroundPropertySetting/Utilities.swift b/examples/BackgroundPropertySetting/BackgroundPropertySetting/Utilities.swift new file mode 100644 index 0000000000..5aedf39722 --- /dev/null +++ b/examples/BackgroundPropertySetting/BackgroundPropertySetting/Utilities.swift @@ -0,0 +1,15 @@ +// +// Utilities.swift +// BackgroundPropertySetting +// +// Created by Adlai Holler on 2/17/16. +// Copyright © 2016 Adlai Holler. All rights reserved. +// + +import UIKit + +extension UIColor { + static func random() -> UIColor { + return UIColor(red: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0) + } +} diff --git a/examples/BackgroundPropertySetting/BackgroundPropertySetting/ViewController.swift b/examples/BackgroundPropertySetting/BackgroundPropertySetting/ViewController.swift index cd92abb4cf..1573b9ebeb 100644 --- a/examples/BackgroundPropertySetting/BackgroundPropertySetting/ViewController.swift +++ b/examples/BackgroundPropertySetting/BackgroundPropertySetting/ViewController.swift @@ -9,56 +9,87 @@ import UIKit import AsyncDisplayKit -final class ViewController: ASViewController, ASTableDelegate, ASTableDataSource { +final class ViewController: ASViewController, ASCollectionDelegate, ASCollectionDataSource { + let itemCount = 1000 - var tableNode: ASTableNode { - return node as! ASTableNode + let itemSize: CGSize + let padding: CGFloat + var collectionNode: ASCollectionNode { + return node as! ASCollectionNode } init() { - super.init(node: ASTableNode(style: .Plain)) - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Update", style: .Plain, target: self, action: "didTapUpdateButton") - tableNode.delegate = self - tableNode.dataSource = self - title = "Background Node Updating Demo" + let layout = UICollectionViewFlowLayout() + (padding, itemSize) = ViewController.computeLayoutSizesForMainScreen() + layout.minimumInteritemSpacing = padding + layout.minimumLineSpacing = padding + super.init(node: ASCollectionNode(collectionViewLayout: layout)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Color", style: .Plain, target: self, action: "didTapColorsButton") + navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Layout", style: .Plain, target: self, action: "didTapLayoutButton") + collectionNode.delegate = self + collectionNode.dataSource = self + title = "Background Updating" } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - let rowCount = 20 - func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rowCount + // MARK: ASCollectionDataSource + + func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return itemCount } - func tableView(tableView: ASTableView, nodeBlockForRowAtIndexPath indexPath: NSIndexPath) -> ASCellNodeBlock { + func collectionView(collectionView: ASCollectionView, nodeBlockForItemAtIndexPath indexPath: NSIndexPath) -> ASCellNodeBlock { return { - let node = ASCellNode() - node.backgroundColor = getRandomColor() + let node = DemoCellNode() + node.backgroundColor = UIColor.random() + node.childA.backgroundColor = UIColor.random() + node.childB.backgroundColor = UIColor.random() return node } } - @objc private func didTapUpdateButton() { - let currentlyVisibleNodes = tableNode.view.visibleNodes() + func collectionView(collectionView: ASCollectionView, constrainedSizeForNodeAtIndexPath indexPath: NSIndexPath) -> ASSizeRange { + return ASSizeRangeMake(itemSize, itemSize) + } + + // MARK: Action Handling + + @objc private func didTapColorsButton() { + let currentlyVisibleNodes = collectionNode.view.visibleNodes() let queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) dispatch_async(queue) { - for case let node as ASCellNode in currentlyVisibleNodes { - node.backgroundColor = getRandomColor() + for case let node as DemoCellNode in currentlyVisibleNodes { + node.backgroundColor = UIColor.random() } } } -} - -func getRandomColor() -> UIColor{ - - let randomRed:CGFloat = CGFloat(drand48()) - - let randomGreen:CGFloat = CGFloat(drand48()) - - let randomBlue:CGFloat = CGFloat(drand48()) - - return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0) - + + @objc private func didTapLayoutButton() { + let currentlyVisibleNodes = collectionNode.view.visibleNodes() + let queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) + dispatch_async(queue) { + for case let node as DemoCellNode in currentlyVisibleNodes { + node.state.advance() + node.setNeedsLayout() + } + } + } + + // MARK: Static + + static func computeLayoutSizesForMainScreen() -> (padding: CGFloat, itemSize: CGSize) { + let numberOfColumns = 4 + let screen = UIScreen.mainScreen() + let scale = screen.scale + let screenWidth = Int(screen.bounds.width * screen.scale) + let itemWidthPx = (screenWidth - (numberOfColumns - 1)) / numberOfColumns + let leftover = screenWidth - itemWidthPx * numberOfColumns + let paddingPx = leftover / (numberOfColumns - 1) + let itemDimension = CGFloat(itemWidthPx) / scale + let padding = CGFloat(paddingPx) / scale + return (padding: padding, itemSize: CGSize(width: itemDimension, height: itemDimension)) + } }