diff --git a/AsyncDisplayKit/ASControlNode.mm b/AsyncDisplayKit/ASControlNode.mm index 41b41b09..4d543c2e 100644 --- a/AsyncDisplayKit/ASControlNode.mm +++ b/AsyncDisplayKit/ASControlNode.mm @@ -9,6 +9,9 @@ #import "ASControlNode.h" #import "ASControlNode+Subclasses.h" #import "ASThread.h" +#import "ASDisplayNode+FrameworkPrivate.h" +#import "ASLayoutSpec+Debug.h" +#import "ASLayoutableInspectorNode.h" // UIControl allows dragging some distance outside of the control itself during // tracking. This value depends on the device idiom (25 or 70 points), so @@ -87,9 +90,26 @@ static BOOL _enableHitTestDebug = NO; // As we have no targets yet, we start off with user interaction off. When a target is added, it'll get turned back on. self.userInteractionEnabled = NO; + return self; } +- (void)inspectElement +{ + [ASLayoutableInspectorNode sharedInstance].layoutableToEdit = self; +} + +- (void)setHierarchyState:(ASHierarchyState)hierarchyState +{ + [super setHierarchyState:hierarchyState]; + + // FIXME: handle disabling hierarchy state on nodes that had previously enabled it (remove target) + if (ASHierarchyStateIncludesVisualizeLayoutSpecs(hierarchyState)) { + [self addTarget:self action:@selector(inspectElement) forControlEvents:ASControlNodeEventTouchUpInside]; +// NSLog(@"%@", self); + } +} + - (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled { [super setUserInteractionEnabled:userInteractionEnabled]; @@ -236,6 +256,7 @@ static BOOL _enableHitTestDebug = NO; { NSParameterAssert(action); NSParameterAssert(controlEventMask != 0); + ASDisplayNodeAssert(!self.isLayerBacked, @"ASControlNode is layer backed, will never be able to call target in target:action: pair."); ASDN::MutexLocker l(_controlLock); diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 9e80d973..5cff4877 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1838,6 +1838,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) [ASLayoutSpec setShouldVisualizeLayoutSpecs2:YES]; } + ASStaticLayoutSpec *staticSpec = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[[self layoutSpecThatFits:constrainedSize]]]; ASLayoutSpec *layoutSpec = staticSpec; diff --git a/AsyncDisplayKit/ASLayoutSpec+Debug.m b/AsyncDisplayKit/ASLayoutSpec+Debug.m index f2331870..8ccff429 100644 --- a/AsyncDisplayKit/ASLayoutSpec+Debug.m +++ b/AsyncDisplayKit/ASLayoutSpec+Debug.m @@ -33,10 +33,11 @@ static BOOL __shouldVisualizeLayoutSpecs = NO; { self = [super init]; if (self) { + self.layer.borderWidth = 2; self.layoutSpec = layoutSpec; self.usesImplicitHierarchyManagement = YES; self.layer.borderColor = [[UIColor redColor] CGColor]; - self.layer.borderWidth = 2; + [self addTarget:self action:@selector(layoutMagicNodeTapped:) forControlEvents:ASControlNodeEventTouchUpInside]; } return self; @@ -47,16 +48,35 @@ static BOOL __shouldVisualizeLayoutSpecs = NO; ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; // FIXME: need to auto pass properties to children insetSpec.neverShouldVisualize = YES; self.layoutSpec.neverShouldVisualize = YES; - UIEdgeInsets insets = UIEdgeInsetsZero; //UIEdgeInsetsMake(10, 10, 10, 10); + CGFloat insetFloat = [ASLayoutableInspectorNode sharedInstance].vizNodeInsetSize; + UIEdgeInsets insets = UIEdgeInsetsMake(insetFloat, insetFloat, insetFloat, insetFloat); +// UIEdgeInsets insets = UIEdgeInsetsZero; // propogate child's layoutSpec properties to the inset that we are adding - insetSpec.flexGrow = _layoutSpec.flexGrow; + insetSpec.flexGrow = _layoutSpec.flexGrow; // FIXME: insetSpec.flexShrink = _layoutSpec.flexShrink; - insetSpec.alignSelf = _layoutSpec.alignSelf; + insetSpec.alignSelf = _layoutSpec.alignSelf; + insetSpec.insets = insets; + insetSpec.child = self.layoutSpec; - insetSpec.insets = insets; - insetSpec.child = self.layoutSpec; - return self.layoutSpec; + return insetSpec; //self.layoutSpec; +} + +- (void)setLayoutSpec:(ASLayoutSpec *)layoutSpec +{ + _layoutSpec = layoutSpec; + +// self.flexGrow = _layoutSpec.flexGrow; +// self.flexShrink = _layoutSpec.flexShrink; +// self.alignSelf = _layoutSpec.alignSelf; + + if ([layoutSpec isKindOfClass:[ASInsetLayoutSpec class]]) { + self.layer.borderColor = [[UIColor redColor] CGColor]; + + } else if ([layoutSpec isKindOfClass:[ASStackLayoutSpec class]]) { + self.layer.borderColor = [[UIColor greenColor] CGColor]; + + } } - (void)layoutMagicNodeTapped:(UIGestureRecognizer *)sender @@ -64,5 +84,10 @@ static BOOL __shouldVisualizeLayoutSpecs = NO; [[ASLayoutableInspectorNode sharedInstance] setLayoutableToEdit:self.layoutSpec]; } +- (NSString *)description +{ + return [self.layoutSpec description]; // FIXME: expand on layoutSpec description (e.g. have StackLayoutSpec return horz/vert) +} + @end diff --git a/AsyncDisplayKit/ASLayoutableInspectorNode.h b/AsyncDisplayKit/ASLayoutableInspectorNode.h index a1a5f39e..7c16cf4f 100644 --- a/AsyncDisplayKit/ASLayoutableInspectorNode.h +++ b/AsyncDisplayKit/ASLayoutableInspectorNode.h @@ -11,7 +11,7 @@ @protocol ASLayoutableInspectorNodeDelegate - (void)shouldShowMasterSplitViewController; - +- (void)toggleVizualization:(BOOL)toggle; @end @@ -19,6 +19,7 @@ @property (nonatomic, strong) id layoutableToEdit; @property (nonatomic, strong) id delegate; +@property (nonatomic, assign) CGFloat vizNodeInsetSize; + (instancetype)sharedInstance; diff --git a/AsyncDisplayKit/ASLayoutableInspectorNode.m b/AsyncDisplayKit/ASLayoutableInspectorNode.m index dc78f6bb..2b5e8679 100644 --- a/AsyncDisplayKit/ASLayoutableInspectorNode.m +++ b/AsyncDisplayKit/ASLayoutableInspectorNode.m @@ -19,6 +19,8 @@ // Navigate layout hierarchy ASButtonNode *_parentNodeNavBtn; ASButtonNode *_siblingNodeRightNavBtn; + ASButtonNode *_addNodeNavBtn; + ASButtonNode *_addLayoutSpecNavBtn; ASButtonNode *_siblingNodeLefttNavBtn; ASButtonNode *_childNodeNavBtn; @@ -48,7 +50,12 @@ // LayoutSpec properties ASTextNode *_layoutSpecPropertiesSectionTitle; - + + // debug help + ASTextNode *_debugSectionTitle; + ASTextNode *_vizNodeInsetSizeTitle; + ASButtonNode *_vizNodeInsetSizeBtn; + ASButtonNode *_vizNodeBordersBtn; } #pragma mark - class methods @@ -72,6 +79,8 @@ self.usesImplicitHierarchyManagement = YES; + _vizNodeInsetSize = 0; + _itemDescription = [[ASTextNode alloc] init]; _itemPropertiesSectionTitle = [[ASTextNode alloc] init]; @@ -80,6 +89,10 @@ _layoutablePropertiesSectionTitle.attributedString = [self attributedStringFromString:@" Properties"]; _layoutSpecPropertiesSectionTitle = [[ASTextNode alloc] init]; _layoutSpecPropertiesSectionTitle.attributedString = [self attributedStringFromString:@" Properties"]; + _debugSectionTitle = [[ASTextNode alloc] init]; + _debugSectionTitle.attributedString = [self attributedStringFromString:@"debugging help"]; + _vizNodeInsetSizeTitle = [[ASTextNode alloc] init]; + _vizNodeInsetSizeTitle.attributedString = [self attributedStringFromString:@"inset ASLayoutSpecs"]; _flexGrowBtn = [self makeBtnNodeWithTitle:@"flexGrow"]; [_flexGrowBtn addTarget:self action:@selector(setFlexGrowValue:) forControlEvents:ASControlNodeEventTouchUpInside]; @@ -101,11 +114,28 @@ _itemBackgroundColorBtn = [self makeBtnNodeWithTitle:@"node color"]; [_itemBackgroundColorBtn addTarget:self action:@selector(changeColor:) forControlEvents:ASControlNodeEventTouchUpInside]; + _vizNodeBordersBtn = [self makeBtnNodeWithTitle:@"visualize ASLayoutSpecs"]; + [_vizNodeBordersBtn addTarget:self action:@selector(setVizNodeBorders:) forControlEvents:ASControlNodeEventTouchUpInside]; + _vizNodeBordersBtn.selected = YES; + + _vizNodeInsetSizeBtn = [self makeBtnNodeWithTitle:@"overlap ASLayoutSpecs"]; + [_vizNodeInsetSizeBtn addTarget:self action:@selector(setVizNodeInsets:) forControlEvents:ASControlNodeEventTouchUpInside]; + _vizNodeInsetSizeBtn.selected = YES; + _parentNodeNavBtn = [self makeBtnNodeWithTitle:@"parent\nnode"]; _siblingNodeRightNavBtn = [self makeBtnNodeWithTitle:@"sibling\nnode"]; + _addNodeNavBtn = [self makeBtnNodeWithTitle:@"add\nnode"]; + _addLayoutSpecNavBtn = [self makeBtnNodeWithTitle:@"add\nlayoutSpec"]; _siblingNodeLefttNavBtn = [self makeBtnNodeWithTitle:@"sibling\nnode"]; _childNodeNavBtn = [self makeBtnNodeWithTitle:@"child\nnode"]; +// _parentNodeNavBtn.selected = YES; +// _siblingNodeRightNavBtn.selected = YES; +// _addNodeNavBtn.selected = YES; +// _addLayoutSpecNavBtn.selected = YES; +// _siblingNodeLefttNavBtn.selected = YES; +// _childNodeNavBtn.selected = YES; + _slider = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{ UISlider *slider = [[UISlider alloc] init]; return slider; @@ -131,16 +161,21 @@ { // navigate layout hierarchy + _parentNodeNavBtn.alignSelf = ASStackLayoutAlignSelfCenter; + _childNodeNavBtn.alignSelf = ASStackLayoutAlignSelfCenter; + ASStackLayoutSpec *horizontalStackNav = [ASStackLayoutSpec horizontalStackLayoutSpec]; horizontalStackNav.flexGrow = YES; + horizontalStackNav.alignSelf = ASStackLayoutAlignSelfCenter; horizontalStackNav.children = @[_siblingNodeLefttNavBtn, _siblingNodeRightNavBtn]; ASStackLayoutSpec *horizontalStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; horizontalStack.flexGrow = YES; ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; + spacer.flexGrow = YES; horizontalStack.children = @[_flexGrowBtn, spacer]; - _flexGrowValue.alignSelf = ASStackLayoutAlignSelfEnd; // FIXME: framework give a warning if you use ASAlignmentBottom!!!!! + _flexGrowValue.alignSelf = ASStackLayoutAlignSelfEnd; // FIXME: make framework give a warning if you use ASAlignmentBottom!!!!! ASStackLayoutSpec *horizontalStack2 = [ASStackLayoutSpec horizontalStackLayoutSpec]; horizontalStack2.flexGrow = YES; @@ -153,7 +188,7 @@ _flexBasisValue.alignSelf = ASStackLayoutAlignSelfEnd; ASStackLayoutSpec *itemDescriptionStack = [ASStackLayoutSpec verticalStackLayoutSpec]; - itemDescriptionStack.children = @[_itemDescription, _itemBackgroundColorBtn,]; + itemDescriptionStack.children = @[_itemDescription]; itemDescriptionStack.spacing = 5; itemDescriptionStack.flexGrow = YES; @@ -166,11 +201,16 @@ layoutSpecStack.children = @[_layoutSpecPropertiesSectionTitle, _alignItemsBtn]; layoutSpecStack.spacing = 5; layoutSpecStack.flexGrow = YES; + + ASStackLayoutSpec *debugHelpStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + debugHelpStack.children = @[_debugSectionTitle, _vizNodeInsetSizeBtn, _vizNodeBordersBtn]; + debugHelpStack.spacing = 5; + debugHelpStack.flexGrow = YES; ASStackLayoutSpec *verticalLayoutableStack = [ASStackLayoutSpec verticalStackLayoutSpec]; verticalLayoutableStack.flexGrow = YES; verticalLayoutableStack.spacing = 20; - verticalLayoutableStack.children = @[_parentNodeNavBtn, horizontalStackNav, _childNodeNavBtn, itemDescriptionStack, layoutableStack, layoutSpecStack]; + verticalLayoutableStack.children = @[_parentNodeNavBtn, horizontalStackNav, _childNodeNavBtn, itemDescriptionStack, layoutableStack, layoutSpecStack, debugHelpStack]; verticalLayoutableStack.alignItems = ASStackLayoutAlignItemsStretch; // stretch headerStack to fill horizontal space ASLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(100, 10, 10, 10) child:verticalLayoutableStack]; @@ -202,28 +242,18 @@ // _flexBasisBtn.selected = self.layoutableToEdit.flexShrink; // _flexBasisValue.attributedString = [self attributedStringFromString: (_flexBasisBtn.selected) ? @"YES" : @"NO"]; + NSUInteger alignSelfValue = [self.layoutableToEdit alignSelf]; - _alignSelfBtn.selected = alignSelfValue ? YES : NO; NSString *newTitle = [@"alignSelf:" stringByAppendingString:[self alignSelfName:alignSelfValue]]; [_alignSelfBtn setAttributedTitle:[self attributedStringFromString:newTitle] forState:ASControlStateNormal]; -// _alignItemsBtn.selected = YES; -// if ([[self layoutSpec] isKindOfClass:[ASStackLayoutSpec class]]) { -// NSUInteger alignItemsValue = [(ASStackLayoutSpec *)[self layoutSpec] alignItems]; -// newTitle = [@"alignItems:" stringByAppendingString:[self typeDisplayNameItems:alignItemsValue]]; + if ([self layoutSpec]) { + _alignItemsBtn.enabled = YES; +// NSUInteger alignItemsValue = [[self layoutSpec] alignItems]; +// newTitle = [@"alignItems:" stringByAppendingString:[self alignSelfName:alignItemsValue]]; // [_alignItemsBtn setAttributedTitle:[self attributedStringFromString:newTitle] forState:ASControlStateNormal]; -// } -// -// if ([layoutable isKindOfClass:[ASLayoutSpec class]]) { -// return [self attributedStringFromString:[(ASLayoutSpec *)layoutable asciiArtString]]; -// } else if ([layoutable isKindOfClass:[ASDisplayNode class]]) { -// return [self attributedStringFromString:[(ASControlNode *)layoutable asciiArtString]]; -// } - + } - - - [self setNeedsLayout]; } @@ -261,7 +291,7 @@ _alignSelfBtn.enabled = NO; _spacingBeforeBtn.enabled = NO; _spacingAfterBtn.enabled = NO; - _alignItemsBtn.enabled = NO; + _alignItemsBtn.enabled = YES; } } @@ -378,6 +408,34 @@ [self updateInspectorWithLayoutable]; } +- (void)setVizNodeInsets:(ASButtonNode *)sender +{ + BOOL newState = !sender.selected; + + if (newState == YES) { + self.vizNodeInsetSize = 0; + [self.delegate toggleVizualization:NO]; // FIXME + [self.delegate toggleVizualization:YES]; // FIXME + _vizNodeBordersBtn.selected = YES; + + } else { + self.vizNodeInsetSize = 10; + [self.delegate toggleVizualization:NO]; // FIXME + [self.delegate toggleVizualization:YES]; // FIXME + } + + sender.selected = newState; +} + +- (void)setVizNodeBorders:(ASButtonNode *)sender +{ + BOOL newState = !sender.selected; + + [self.delegate toggleVizualization:newState]; // FIXME + + sender.selected = newState; +} + // diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.mm b/AsyncDisplayKit/Layout/ASLayoutSpec.mm index 9a3256db..c7746293 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.mm @@ -129,7 +129,7 @@ static NSString * const kDefaultChildrenKey = @"kDefaultChildrenKey"; { if ([child isKindOfClass:[ASLayoutSpec class]]) { [(ASLayoutSpec *)child setShouldVisualize:self.shouldVisualize]; - NSLog(@"%@ %@ %d", self, child, self.shouldVisualize); +// NSLog(@"%@ %@ %d", self, child, self.shouldVisualize); } ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); self.layoutChildren[identifier] = [self layoutableToAddFromLayoutable:child]; @@ -143,7 +143,7 @@ static NSString * const kDefaultChildrenKey = @"kDefaultChildrenKey"; for (id child in children) { if ([child isKindOfClass:[ASLayoutSpec class]]) { [(ASLayoutSpec *)child setShouldVisualize:self.shouldVisualize]; - NSLog(@"%@ %@ %d", self, child, self.shouldVisualize); +// NSLog(@"%@ %@ %d", self, child, self.shouldVisualize); } [finalChildren addObject:[self layoutableToAddFromLayoutable:child]]; } diff --git a/examples/ASLayoutSpecPlayground/Sample.xcodeproj/project.pbxproj b/examples/ASLayoutSpecPlayground/Sample.xcodeproj/project.pbxproj index 09c24560..b24ae845 100644 --- a/examples/ASLayoutSpecPlayground/Sample.xcodeproj/project.pbxproj +++ b/examples/ASLayoutSpecPlayground/Sample.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 3EC0CDCBA10D483D9F386E5E /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */; }; 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; + 7602C7651CA4F83100D0D917 /* Utilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 7602C7641CA4F83100D0D917 /* Utilities.m */; }; + 7602C7671CA4FB5300D0D917 /* resizeHandle.png in Resources */ = {isa = PBXBuildFile; fileRef = 7602C7661CA4FB5300D0D917 /* resizeHandle.png */; }; 76466F321C9DFFC4006C4D2D /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76466F2B1C9DFFC4006C4D2D /* AppDelegate.m */; }; 76466F331C9DFFC4006C4D2D /* ColorNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 76466F2D1C9DFFC4006C4D2D /* ColorNode.m */; }; 76466F341C9DFFC4006C4D2D /* PlaygroundNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 76466F2F1C9DFFC4006C4D2D /* PlaygroundNode.m */; }; @@ -29,6 +31,9 @@ 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + 7602C7631CA4F83100D0D917 /* Utilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utilities.h; sourceTree = ""; }; + 7602C7641CA4F83100D0D917 /* Utilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Utilities.m; sourceTree = ""; }; + 7602C7661CA4FB5300D0D917 /* resizeHandle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = resizeHandle.png; sourceTree = ""; }; 76466F2A1C9DFFC4006C4D2D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 76466F2B1C9DFFC4006C4D2D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 76466F2C1C9DFFC4006C4D2D /* ColorNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ColorNode.h; sourceTree = ""; }; @@ -92,6 +97,8 @@ 76466F2F1C9DFFC4006C4D2D /* PlaygroundNode.m */, 76466F2C1C9DFFC4006C4D2D /* ColorNode.h */, 76466F2D1C9DFFC4006C4D2D /* ColorNode.m */, + 7602C7631CA4F83100D0D917 /* Utilities.h */, + 7602C7641CA4F83100D0D917 /* Utilities.m */, 05E2128419D4DB510098F589 /* Supporting Files */, ); path = Sample; @@ -102,6 +109,7 @@ children = ( 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 7602C7661CA4FB5300D0D917 /* resizeHandle.png */, 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, 05E2128519D4DB510098F589 /* Info.plist */, 05E2128619D4DB510098F589 /* main.m */, @@ -189,6 +197,7 @@ 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + 7602C7671CA4FB5300D0D917 /* resizeHandle.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -249,6 +258,7 @@ files = ( 76466F321C9DFFC4006C4D2D /* AppDelegate.m in Sources */, 05E2128719D4DB510098F589 /* main.m in Sources */, + 7602C7651CA4F83100D0D917 /* Utilities.m in Sources */, 76466F331C9DFFC4006C4D2D /* ColorNode.m in Sources */, 76466F341C9DFFC4006C4D2D /* PlaygroundNode.m in Sources */, 76F58D5C1C9E15C1004512CC /* PlaygroundContainerNode.m in Sources */, diff --git a/examples/ASLayoutSpecPlayground/Sample/AppDelegate.m b/examples/ASLayoutSpecPlayground/Sample/AppDelegate.m index b0400f8c..24128857 100644 --- a/examples/ASLayoutSpecPlayground/Sample/AppDelegate.m +++ b/examples/ASLayoutSpecPlayground/Sample/AppDelegate.m @@ -43,6 +43,7 @@ splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; splitViewController.viewControllers = [NSArray arrayWithObjects:masterNav, detailNav, nil]; splitViewController.delegate = detailViewController; + splitViewController.maximumPrimaryColumnWidth = 200; detailViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem; diff --git a/examples/ASLayoutSpecPlayground/Sample/PlaygroundContainerNode.m b/examples/ASLayoutSpecPlayground/Sample/PlaygroundContainerNode.m index 5da5d0ce..abfb975c 100644 --- a/examples/ASLayoutSpecPlayground/Sample/PlaygroundContainerNode.m +++ b/examples/ASLayoutSpecPlayground/Sample/PlaygroundContainerNode.m @@ -14,7 +14,7 @@ @implementation PlaygroundContainerNode { PlaygroundNode *_playgroundNode; - ASDisplayNode *_resizeHandle; + ASImageNode *_resizeHandle; } - (instancetype)init @@ -28,8 +28,9 @@ _playgroundNode = [[PlaygroundNode alloc] init]; - _resizeHandle = [[ASDisplayNode alloc] init]; - _resizeHandle.backgroundColor = [UIColor greenColor]; + _resizeHandle = [[ASImageNode alloc] init]; + _resizeHandle.image = [UIImage imageNamed:@"resizeHandle"]; + _resizeHandle.userInteractionEnabled = YES; [self.view addSubnode:_resizeHandle]; UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(resizePlayground:)]; @@ -43,7 +44,7 @@ return self; } -#define RESIZE_HANDLE_SIZE 10 +#define RESIZE_HANDLE_SIZE 30 - (void)layout { [super layout]; @@ -62,7 +63,8 @@ _playgroundNode.flexGrow = YES; _playgroundNode.flexShrink = YES; - return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[_playgroundNode]]; + UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 10, 10); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:_playgroundNode]; } - (void)resizePlayground:(UIGestureRecognizer *)sender diff --git a/examples/ASLayoutSpecPlayground/Sample/PlaygroundNode.m b/examples/ASLayoutSpecPlayground/Sample/PlaygroundNode.m index b0c69660..008690cc 100644 --- a/examples/ASLayoutSpecPlayground/Sample/PlaygroundNode.m +++ b/examples/ASLayoutSpecPlayground/Sample/PlaygroundNode.m @@ -10,15 +10,24 @@ #import "ColorNode.h" #import "AsyncDisplayKit+Debug.h" #import "ASLayoutableInspectorNode.h" +#import "Utilities.h" + +#define USER_IMAGE_HEIGHT 60 +#define HORIZONTAL_BUFFER 10 +#define VERTICAL_BUFFER 5 +#define FONT_SIZE 20 @implementation PlaygroundNode { - NSArray *_colorNodes; - ASDisplayNode *_individualColorNode; - ASTextNode *_textNode1; - ASTextNode *_textNode2; - ASTextNode *_textNode3; + ASNetworkImageNode *_userAvatarImageView; + ASNetworkImageNode *_photoImageView; + ASTextNode *_userNameLabel; + ASTextNode *_photoLocationLabel; + ASTextNode *_photoTimeIntervalSincePostLabel; + ASTextNode *_photoLikesLabel; + ASTextNode *_photoDescriptionLabel; } + #pragma mark - Lifecycle - (instancetype)init @@ -26,95 +35,153 @@ self = [super init]; if (self) { - + + self.backgroundColor = [UIColor whiteColor]; self.usesImplicitHierarchyManagement = YES; -// self.clipsToBounds = YES; // make outside bounds semi-transparent - ColorNode *node = [[ColorNode alloc] init]; - ColorNode *node2 = [[ColorNode alloc] init]; - ColorNode *node3 = [[ColorNode alloc] init]; - _colorNodes = @[node, node2, node3]; - - _individualColorNode = [[ColorNode alloc] init]; - _individualColorNode.backgroundColor = [UIColor orangeColor]; + _userAvatarImageView = [[ASNetworkImageNode alloc] init]; + _userAvatarImageView.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/avatars/503h_1458880322_140.jpg"]; - // user interaction off by default - _textNode1 = [[ASTextNode alloc] init]; - _textNode1.attributedString = [[NSAttributedString alloc] initWithString:@"test"]; - _textNode1.backgroundColor = [UIColor greenColor]; - _textNode1.userInteractionEnabled = YES; - [_textNode1 addTarget:self action:@selector(textTapped:) forControlEvents:ASControlNodeEventTouchUpInside]; + // FIXME: autocomplete for this line seems broken + [_userAvatarImageView setImageModificationBlock:^UIImage *(UIImage *image) { + CGSize profileImageSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + return [image makeCircularImageWithSize:profileImageSize]; + }]; - _textNode2 = [[ASTextNode alloc] init]; - _textNode2.attributedString = [[NSAttributedString alloc] initWithString:@"Hhhhhhhhhheeeeeeeeeelllllloooooooo"]; - _textNode2.backgroundColor = [UIColor greenColor]; - _textNode2.userInteractionEnabled = YES; - [_textNode2 addTarget:self action:@selector(textTapped:) forControlEvents:ASControlNodeEventTouchUpInside]; + _userNameLabel = [[ASTextNode alloc] init]; + _userNameLabel.attributedString = [self usernameAttributedStringWithFontSize:FONT_SIZE]; - _textNode3 = [[ASTextNode alloc] init]; - _textNode3.attributedString = [[NSAttributedString alloc] initWithString:@"another test text node"]; - _textNode3.backgroundColor = [UIColor greenColor]; - _textNode3.userInteractionEnabled = YES; - [_textNode3 addTarget:self action:@selector(textTapped:) forControlEvents:ASControlNodeEventTouchUpInside]; + _photoLocationLabel = [[ASTextNode alloc] init]; + _photoLocationLabel.maximumNumberOfLines = 1; + _photoLocationLabel.attributedString = [self locationAttributedStringWithFontSize:FONT_SIZE]; + + _photoTimeIntervalSincePostLabel = [[ASTextNode alloc] init]; + _photoTimeIntervalSincePostLabel.attributedString = [self uploadDateAttributedStringWithFontSize:FONT_SIZE]; + + _photoImageView = [[ASNetworkImageNode alloc] init]; + _photoImageView.URL = [NSURL URLWithString:@"https://s-media-cache-ak0.pinimg.com/564x/9f/5b/3a/9f5b3a35640bc7a5d484b66124c48c46.jpg"]; + + _photoLikesLabel = [[ASTextNode alloc] init]; + _photoLikesLabel.attributedString = [self likesAttributedStringWithFontSize:FONT_SIZE]; + + _photoDescriptionLabel = [[ASTextNode alloc] init]; + _photoDescriptionLabel.attributedString = [self descriptionAttributedStringWithFontSize:FONT_SIZE]; + _photoDescriptionLabel.maximumNumberOfLines = 3; } return self; } -- (void)textTapped:(UIGestureRecognizer *)sender -{ - [ASLayoutableInspectorNode sharedInstance].layoutableToEdit = (ASTextNode *)sender; -} - - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { - NSMutableArray *children = [[NSMutableArray alloc] init]; - for (ASDisplayNode *node in _colorNodes) { - UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 10, 10); - node.flexGrow = YES; - ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:node]; - insetSpec.flexGrow = YES; - [children addObject:insetSpec]; + // username / photo location header vertical stack + + _userNameLabel.flexShrink = YES; + _photoLocationLabel.flexShrink = YES; + + ASStackLayoutSpec *headerSubStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + headerSubStack.flexShrink = YES; + + if (_photoLocationLabel.attributedString) { + [headerSubStack setChildren:@[_userNameLabel, _photoLocationLabel]]; + } else { + [headerSubStack setChildren:@[_userNameLabel]]; } - [children addObject:_textNode1]; - [children addObject:_textNode2]; - [children addObject:_textNode3]; + // header stack - _textNode1.flexShrink = YES; - _textNode2.flexShrink = YES; - _textNode3.flexShrink = YES; - - ASStackLayoutSpec *innerStack = [ASStackLayoutSpec verticalStackLayoutSpec]; - innerStack.children = children; - innerStack.flexGrow = YES; - innerStack.flexShrink = YES; - -// _individualColorNode.preferredFrameSize = CGSizeMake(100, 600); - _individualColorNode.flexGrow = YES; - _individualColorNode.flexShrink = YES; + _userAvatarImageView.preferredFrameSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + _photoTimeIntervalSincePostLabel.spacingBefore = HORIZONTAL_BUFFER; // hack to remove double spaces around spacer - ASStackLayoutSpec *outerStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; - outerStack.flexGrow = YES; - outerStack.flexShrink = YES; - outerStack.children = @[innerStack, _individualColorNode]; - outerStack.alignItems = ASStackLayoutAlignItemsStretch; + UIEdgeInsets avatarInsets = UIEdgeInsetsMake(HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER, HORIZONTAL_BUFFER); + ASInsetLayoutSpec *avatarInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:avatarInsets child:_userAvatarImageView]; - return outerStack; + ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init]; + spacer.flexGrow = YES; + spacer.flexShrink = YES; + + ASStackLayoutSpec *headerStack = [ASStackLayoutSpec horizontalStackLayoutSpec]; + headerStack.alignItems = ASStackLayoutAlignItemsCenter; // center items vertically in horizontal stack + headerStack.justifyContent = ASStackLayoutJustifyContentStart; // justify content to the left side of the header stack + headerStack.flexShrink = YES; + headerStack.flexGrow = YES; + + [headerStack setChildren:@[avatarInset, headerSubStack, spacer, _photoTimeIntervalSincePostLabel]]; + + // header inset stack + + UIEdgeInsets insets = UIEdgeInsetsMake(0, HORIZONTAL_BUFFER, 0, HORIZONTAL_BUFFER); + ASInsetLayoutSpec *headerWithInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:headerStack]; + headerWithInset.flexShrink = YES; + headerWithInset.flexGrow = YES; + + // footer stack + + ASStackLayoutSpec *footerStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + footerStack.spacing = VERTICAL_BUFFER; + + [footerStack setChildren:@[_photoLikesLabel, _photoDescriptionLabel]]; + + // footer inset stack + + UIEdgeInsets footerInsets = UIEdgeInsetsMake(VERTICAL_BUFFER, HORIZONTAL_BUFFER, VERTICAL_BUFFER, HORIZONTAL_BUFFER); + ASInsetLayoutSpec *footerWithInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:footerInsets child:footerStack]; + + // vertical stack + + CGFloat cellWidth = constrainedSize.max.width; + _photoImageView.preferredFrameSize = CGSizeMake(cellWidth, cellWidth); // constrain photo frame size + + ASStackLayoutSpec *verticalStack = [ASStackLayoutSpec verticalStackLayoutSpec]; + verticalStack.alignItems = ASStackLayoutAlignItemsStretch; // sretch headerStack to fill horizontal space + [verticalStack setChildren:@[headerWithInset, _photoImageView, footerWithInset]]; + verticalStack.flexShrink = YES; + + return verticalStack; } -//- (ASSizeRange)playgroundConstrainedSize -//{ -// if (ASRangeIsEmpty(_playgroundConstrainedSize)) { -// CGSize maxSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); -// _playgroundConstrainedSize = ASSizeRangeMake(maxSize, maxSize); -// } -// return _playgroundConstrainedSize; -//} -// -//- (ASSizeRange)nodeConstrainedSize -//{ -// return self.playgroundConstrainedSize; -//} +#pragma mark - helper methods + +- (NSAttributedString *)usernameAttributedStringWithFontSize:(CGFloat)size +{ + return [NSAttributedString attributedStringWithString:@"hannahmbanana" + fontSize:size + color:[UIColor darkBlueColor] + firstWordColor:nil]; +} + +- (NSAttributedString *)locationAttributedStringWithFontSize:(CGFloat)size +{ + return [NSAttributedString attributedStringWithString:@"San Fransisco, CA" + fontSize:size + color:[UIColor lightBlueColor] + firstWordColor:nil]; +} + +- (NSAttributedString *)uploadDateAttributedStringWithFontSize:(CGFloat)size +{ + return [NSAttributedString attributedStringWithString:@"30m" + fontSize:size + color:[UIColor lightGrayColor] + firstWordColor:nil]; +} + +- (NSAttributedString *)likesAttributedStringWithFontSize:(CGFloat)size +{ + return [NSAttributedString attributedStringWithString:@"♥︎ 17 likes" + fontSize:size + color:[UIColor darkBlueColor] + firstWordColor:nil]; +} + +- (NSAttributedString *)descriptionAttributedStringWithFontSize:(CGFloat)size +{ + NSString *string = [NSString stringWithFormat:@"hannahtroisi check out this cool pic from the internet!"]; + NSAttributedString *attrString = [NSAttributedString attributedStringWithString:string + fontSize:size + color:[UIColor darkGrayColor] + firstWordColor:[UIColor darkBlueColor]]; + return attrString; +} @end diff --git a/examples/ASLayoutSpecPlayground/Sample/Utilities.h b/examples/ASLayoutSpecPlayground/Sample/Utilities.h new file mode 100644 index 00000000..111c237d --- /dev/null +++ b/examples/ASLayoutSpecPlayground/Sample/Utilities.h @@ -0,0 +1,39 @@ +// +// Utilities.h +// Flickrgram +// +// Created by Hannah Troisi on 3/9/16. +// Copyright © 2016 Hannah Troisi. All rights reserved. +// + +#import +#import + +@interface UIColor (Additions) + ++ (UIColor *)darkBlueColor; ++ (UIColor *)lightBlueColor; + +@end + +@interface UIImage (Additions) + ++ (UIImage *)followingButtonStretchableImageForCornerRadius:(CGFloat)cornerRadius following:(BOOL)followingEnabled; + +- (UIImage *)makeCircularImageWithSize:(CGSize)size; + +@end + +@interface NSString (Additions) + +// returns a user friendly elapsed time such as '50s', '6m' or '3w' ++ (NSString *)elapsedTimeStringSinceDate:(NSString *)uploadDateString; + +@end + +@interface NSAttributedString (Additions) + ++ (NSAttributedString *)attributedStringWithString:(NSString *)string fontSize:(CGFloat)size + color:(UIColor *)color firstWordColor:(UIColor *)firstWordColor; + +@end \ No newline at end of file diff --git a/examples/ASLayoutSpecPlayground/Sample/Utilities.m b/examples/ASLayoutSpecPlayground/Sample/Utilities.m new file mode 100644 index 00000000..ac0c93c3 --- /dev/null +++ b/examples/ASLayoutSpecPlayground/Sample/Utilities.m @@ -0,0 +1,188 @@ +// +// Utilities.m +// Flickrgram +// +// Created by Hannah Troisi on 3/9/16. +// Copyright © 2016 Hannah Troisi. All rights reserved. +// + +#import "Utilities.h" +#import + +#define StrokeRoundedImages 0 + +@implementation UIColor (Additions) + ++ (UIColor *)darkBlueColor +{ + return [UIColor colorWithRed:18.0/255.0 green:86.0/255.0 blue:136.0/255.0 alpha:1.0]; +} + ++ (UIColor *)lightBlueColor +{ + return [UIColor colorWithRed:0.0 green:122.0/255.0 blue:1.0 alpha:1.0]; +} + +@end + +@implementation UIImage (Additions) + ++ (UIImage *)followingButtonStretchableImageForCornerRadius:(CGFloat)cornerRadius following:(BOOL)followingEnabled +{ + CGSize unstretchedSize = CGSizeMake(2 * cornerRadius + 1, 2 * cornerRadius + 1); + CGRect rect = (CGRect) {CGPointZero, unstretchedSize}; + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius]; + + // create a graphics context for the following status button + UIGraphicsBeginImageContextWithOptions(unstretchedSize, NO, 0); + + [path addClip]; + + if (followingEnabled) { + + [[UIColor whiteColor] setFill]; + [path fill]; + + path.lineWidth = 3; + [[UIColor lightBlueColor] setStroke]; + [path stroke]; + + } else { + + [[UIColor lightBlueColor] setFill]; + [path fill]; + } + + UIImage *followingBtnImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + UIImage *followingBtnImageStretchable = [followingBtnImage stretchableImageWithLeftCapWidth:cornerRadius + topCapHeight:cornerRadius]; + return followingBtnImageStretchable; +} + +- (UIImage *)makeCircularImageWithSize:(CGSize)size +{ + // make a CGRect with the image's size + CGRect circleRect = (CGRect) {CGPointZero, size}; + + // begin the image context since we're not in a drawRect: + UIGraphicsBeginImageContextWithOptions(circleRect.size, NO, 0); + + // create a UIBezierPath circle + UIBezierPath *circle = [UIBezierPath bezierPathWithRoundedRect:circleRect cornerRadius:circleRect.size.width/2]; + + // clip to the circle + [circle addClip]; + + // draw the image in the circleRect *AFTER* the context is clipped + [self drawInRect:circleRect]; + + // create a border (for white background pictures) +#if StrokeRoundedImages + circle.lineWidth = 1; + [[UIColor darkGrayColor] set]; + [circle stroke]; +#endif + + // get an image from the image context + UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext(); + + // end the image context since we're not in a drawRect: + UIGraphicsEndImageContext(); + + return roundedImage; +} + +@end + +@implementation NSString (Additions) + +// Returns a user-visible date time string that corresponds to the +// specified RFC 3339 date time string. Note that this does not handle +// all possible RFC 3339 date time strings, just one of the most common +// styles. ++ (NSDate *)userVisibleDateTimeStringForRFC3339DateTimeString:(NSString *)rfc3339DateTimeString +{ + NSDateFormatter * rfc3339DateFormatter; + NSLocale * enUSPOSIXLocale; + + // Convert the RFC 3339 date time string to an NSDate. + + rfc3339DateFormatter = [[NSDateFormatter alloc] init]; + + enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + + [rfc3339DateFormatter setLocale:enUSPOSIXLocale]; + [rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ssZ'"]; + [rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + + return [rfc3339DateFormatter dateFromString:rfc3339DateTimeString]; +} + ++ (NSString *)elapsedTimeStringSinceDate:(NSString *)uploadDateString +{ + // early return if no post date string + if (!uploadDateString) + { + return @"NO POST DATE"; + } + + NSDate *postDate = [self userVisibleDateTimeStringForRFC3339DateTimeString:uploadDateString]; + + if (!postDate) { + return @"DATE CONVERSION ERROR"; + } + + NSDate *currentDate = [NSDate date]; + + NSCalendar *calendar = [NSCalendar currentCalendar]; + + NSUInteger seconds = [[calendar components:NSCalendarUnitSecond fromDate:postDate toDate:currentDate options:0] second]; + NSUInteger minutes = [[calendar components:NSCalendarUnitMinute fromDate:postDate toDate:currentDate options:0] minute]; + NSUInteger hours = [[calendar components:NSCalendarUnitHour fromDate:postDate toDate:currentDate options:0] hour]; + NSUInteger days = [[calendar components:NSCalendarUnitDay fromDate:postDate toDate:currentDate options:0] day]; + + NSString *elapsedTime; + + if (days > 7) { + elapsedTime = [NSString stringWithFormat:@"%luw", (long)ceil(days/7.0)]; + } else if (days > 0) { + elapsedTime = [NSString stringWithFormat:@"%lud", (long)days]; + } else if (hours > 0) { + elapsedTime = [NSString stringWithFormat:@"%luh", (long)hours]; + } else if (minutes > 0) { + elapsedTime = [NSString stringWithFormat:@"%lum", (long)minutes]; + } else if (seconds > 0) { + elapsedTime = [NSString stringWithFormat:@"%lus", (long)seconds]; + } else if (seconds == 0) { + elapsedTime = @"1s"; + } else { + elapsedTime = @"ERROR"; + } + + return elapsedTime; +} + +@end + +@implementation NSAttributedString (Additions) + ++ (NSAttributedString *)attributedStringWithString:(NSString *)string fontSize:(CGFloat)size + color:(nullable UIColor *)color firstWordColor:(nullable UIColor *)firstWordColor +{ + NSDictionary *attributes = @{NSForegroundColorAttributeName: color ? : [UIColor blackColor], + NSFontAttributeName: [UIFont systemFontOfSize:size]}; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string]; + [attributedString addAttributes:attributes range:NSMakeRange(0, string.length)]; + + if (firstWordColor) { + NSRange firstSpaceRange = [string rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]]; + NSRange firstWordRange = NSMakeRange(0, firstSpaceRange.location); + [attributedString addAttribute:NSForegroundColorAttributeName value:firstWordColor range:firstWordRange]; + } + + return attributedString; +} + +@end diff --git a/examples/ASLayoutSpecPlayground/Sample/ViewController.m b/examples/ASLayoutSpecPlayground/Sample/ViewController.m index 2d4a6b73..4cedff91 100644 --- a/examples/ASLayoutSpecPlayground/Sample/ViewController.m +++ b/examples/ASLayoutSpecPlayground/Sample/ViewController.m @@ -62,4 +62,11 @@ [self.view setNeedsLayout]; } +- (void)toggleVizualization:(BOOL)toggle +{ + NSLog(@"shouldVisualizeLayoutSpecs:%d", toggle); + [self.node shouldVisualizeLayoutSpecs:toggle]; + [self.view setNeedsLayout]; +} + @end diff --git a/examples/ASLayoutSpecPlayground/Sample/resizeHandle.png b/examples/ASLayoutSpecPlayground/Sample/resizeHandle.png new file mode 100644 index 00000000..86404bcb Binary files /dev/null and b/examples/ASLayoutSpecPlayground/Sample/resizeHandle.png differ