Let’s Get Our Hand Dirty with Texture
Note: This is a series about Texture, To fully understand this chapter, you can first read this chapter Onboard to “Asynchronus” Layout API — Texture and How to Layout on Texture.
After long theory, let’s create our Texture. We will try to create simple app called TextureGram. We will only focus only on View Aspect. Download/Clone this starter project:
Post Header
Let’s create the Header, with this composition view, we can use Stack Horizontal to create the components. Go to Component > Post > HeaderNode.swift
Explanation
First we need to create the profile picture, username and location.
profilePicture = ASImageNode()
profilePicture.image = post.user.userPhoto
profilePicture.style.preferredSize = CGSize(width: 32, height: 32)
profilePicture.cornerRadius = 32/2
We set profilePicture size to 32x32 and set corner radius to 32/2 so we can get circle profile picture.
profileName = ASTextNode()
profileName.attributedText = NSAttributedString.bold(post.user.username)
Next create the profile name label, to set text on ASTextNode, we use attributedText.
if let location = post.location {
postLocation = ASTextNode()
postLocation?.attributedText = NSAttributedString.subtitle(location)
}else{
postLocation = nil
}
Location is Optional, so we only create the location label only if exist.
On UIKit, to add the view to rootview, we use view.addSubView, on Texture, we got a similar one, node.addSubNode. So to add the node we would do like
self.addSubnode(profilePicture)
self.addSubnode(profileName)
self.addSubnode(postLocation)
But on Texture, we can remove that repetitive code with
self.automaticallyManagesSubnodes = true
What this mean is, our supernode will automatically add the childnode when supernode detect that childnode will be added to the supernode.
Finally we will setting up how our layouting will look like
var leftStackComponents = [ASLayoutElement]()
leftStackComponents.append(profileName)if let postLocation = self.postLocation {
leftStackComponents.append(postLocation)
}
As we have an optional view component (post location), we need to check whether we would display the node or not. First we create an array to hold the left stack components. This left stack will consist of profileName and postLocation (if available).
let leftStack = ASStackLayoutSpec(direction: .vertical, spacing: 0, justifyContent: .start, alignItems: .start, children: leftStackComponents)
Then we create a ASStackLayoutSpec, with vertical direction and use leftStackComponents array to create this stack.
let mainStack = ASStackLayoutSpec(direction: .horizontal, spacing: 6, justifyContent: .start, alignItems: .center, children: [profilePicture, leftStack])
Finally we create the main stack, with horizontal direction that consist of profile picture and left stack.
let padding = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 8, left: 8, bottom: 4, right: 8), child: mainStack)return padding
We also want to add inset on our node, so we set the ASInsetLayoutSpec.
As you can see, we save our node reference on class level, this do to prevent blinking as texture redraw the view, instead creating new one, if we save the reference, texture will use the reference one, so blinking is reduced.
That’s it, we finish the headerNode.
Action
Let’s create the Action, with this composition view, we can also use Stack Horizontal to create the components. Go to Component > Post > ActionNode.swift
Explanation
We will create ASImageNode to create the action, but in this tutorial, we will not add any action, just create and layouting the view. In real work solution, you can use ASButtonNode and use SetImage on that to utilize Action Event.
self.loveButton = ASImageNode()
loveButton.image = imageLiteral(resourceName: "love")
loveButton.style.preferredSize = CGSize(width: 16, height: 16)self.commentButton = ASImageNode()
commentButton.image = imageLiteral(resourceName: "comment")
commentButton.style.preferredSize = CGSize(width: 16, height: 16)self.shareButton = ASImageNode()
shareButton.image = imageLiteral(resourceName: "share")
shareButton.style.preferredSize = CGSize(width: 16, height: 16)
We create the image, with size 16x16
let mainStack = ASStackLayoutSpec(direction: .horizontal, spacing: 16, justifyContent: .start, alignItems: .start, children: [loveButton,commentButton, shareButton])let padding = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), child: mainStack)return padding
On our layoutspec, we will create ASStackLayout horizontally with spacing 16 each item with our image as the children. We also add padding.
Post Cell
Let’s create the PostCell. Go to Component > Post > PostCell.swift
We will combine all the previous component to create the Post Cell, this is a ASCellNode class that will be used on table to represent post on our app.
Explanation
headerNode = HeaderNode(post: post)
We init our HeaderNode, we previously created and past the post value.
postImage = ASImageNode()
postImage.contentMode = .scaleAspectFit
postImage.style.width = ASDimension(unit: .fraction, value: 1)
postImage.image = post.image
For our Post image, we will create ASImageNode. For real work solution, we can use ASNetworkImageNode. This class is provided by Texture to load image by url. We just parse url to ASNetworkImageNode, and it will do the job for us. It also provide caching and progressive image right out the box.
If you see we set style.width. This is the way Texture determine sizing. ASDimension is the class that will set the size.
In this case, i want to make sure our image will fill all the way to screen width. Fraction means percentage, range from 0 to 1, this means i want to set the width to 100% respective to parents.
ASDimension provide 2 way to determine manual size, by fraction or points.
actionNode = ActionNode()
We init our actionNode.
postLikes = ASTextNode()
postLikes.attributedText = NSAttributedString.bold("\(post.likeCount) Likes")
For post likes, we create ASTextNode with bold attributed String.
postDescription = ASTextNode()
postDescription.maximumNumberOfLines = 0
postDescription.attributedText = NSAttributedString.normal(post.description)
Because our description can be multiple line, we use maximumNumberOfLines, same as numberOfLines on UILabel.
Let’s talk about our layouting
let postImageWithRatio = ASRatioLayoutSpec(ratio: 1/1.5, child: postImage)
For our post image, i want to use ratio. Previously if you remember, i set post image to allocate its width 100% to parent.for the height i want to use ratio, i want the height ratio to be 1.5 respective to width, if the width 100 then the height will be 150.
let postLikesWithPadding = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8), child: self.postLikes)let postDescriptionWithPadding = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 4, left: 8, bottom: 0, right: 8), child: self.postDescription)
We warp the post likes and description with padding on left and right.
let mainStack = ASStackLayoutSpec(direction: .vertical, spacing: 0, justifyContent: .start, alignItems: .start, children: [headerNode, postImageWithRatio, actionNode, postLikesWithPadding, postDescriptionWithPadding])return mainStack
Finally we wrap all our component to vertical stack.
Post Node
Let’s create the Post Node.Post Node will be a ASTableNode that will utilize Post Cell we create previously.we will integrate all our components into this class. Go to Component > Post > PostNode.swift
Our post node is just a ASTableNode, and if you can see it shares a lot of similiarity to UITableView.
self.delegate = self
self.dataSource = self
We set delegate and datasource. ASTableNode use ASTableDataSource and ASTableDelegate and looks really familiar to UIKit.
You can set number of section by numberOfSection, or how much row in section by
func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int
For setting cell for each row, ASTableNode provide 2 ways to create this, either use ASCellNode or ASCellNodeBlock. The different between two of it, ASCellNodeBlock will utilize background thread that will decrease main thread load.
In this example, we use ASCellNodeBlock.
func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { guard posts.count > indexPath.row else { return { ASCellNode() } } let post = posts[indexPath.row] // this may be executed on a background thread - it is important to make sure it is thread safe let cellNodeBlock = { () -> ASCellNode in
return PostCell(post: post)
} return cellNodeBlock}
It is important to save the data you want to pass to cellNode block inside the function, so in this example we create post and then pass that into our cell. This is because, the value/data will be accessed from background thread, and to make sure nothing breaks, we capture that inside the function.
self.style.flexShrink = 1
If you see, there’s also flexShrink, what is flexShrink ?
This means, the higher the number, more prioritize that view to be smaller
For example, if there is a stack vertical with 2 text, the one with higher flexShrink will get more less space compare to the other.
There is also FlexGrow, which just conterpart of flexshrink, the higher the number , more likely the node will get more space.
If you familiar with prioritize auto layout on UIKit, it kinda like that but more simple.
Story Cell
Now we move to Story, it’s almost done. Go to Components > Story > StoryCell.swift
We need to create image and username, and stack it vertically
userPicture = ASImageNode()
userPicture.image = story.user.userPhoto
userPicture.style.preferredSize = CGSize(width: 55, height: 55)
userPicture.cornerRadius = 55/2userName = ASTextNode()
userName.attributedText = NSAttributedString.subtitle(story.user.username)
userName.maximumNumberOfLines = 1
We create ASImageNode with fix size 55x55 to hold the user photo and ASTextNode to hold username.
let mainStack = ASStackLayoutSpec(direction: .vertical, spacing: 8, justifyContent: .center, alignItems: .center, children: [userPicture, userName])
For our layouting, we use stack vertically with spacing 8. In this stack we set justifyContent and alightItems to .center, what it means is the distribution on stack horizontally and vertically will be based on center.
Story Node
We will place all of the Story Cell on ASCollectionNode, this will look really familiar with UICollectionView
Go to Components > Story > StoryNode.swift
First we create the flow layout to determine how our collection will look like
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 6
layout.minimumInteritemSpacing = 0
It’s basically use the same flow layout as UICollectionView
self.style.width = ASDimension(unit: .fraction, value: 1)
self.style.height = ASDimension(unit: .points, value: 75)
We will set width of this collection to 100% to parent and height fix at 75
For delegate and datasource, it also shares a lot of similarities to UICollectionView. ASCollectionNode use ASCollectionDataSource and ASCollectionDelegate as datasource and delegate.
That’s it. Now it’s time to integrate all of it.
Integrate All of the Components
Now it’s time to use all components we create previously.
Go to viewController.swift on root of the project.
First we create our story and post node.
self.storyNode = StoryNode(stories: Story.generateDummyStory())
self.postNode = PostNode(posts: Post.generateDummyPosts())
In this example , there’s already a dummy data for story and post. Pass that value into our node.
self.node.layoutSpecBlock = {_,_ in let storyNodeWithPadding = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 0), child: self.storyNode) let mainStack = ASStackLayoutSpec(direction: .vertical,spacing: 0, justifyContent: .start, alignItems: .stretch, children: [storyNodeWithPadding, self.postNode]) return mainStack
}
Did you notice the different ? on our node , when setting up our layouting, we always put our code layoutSpecThatFits, in this case we put it on node.layoutSpecBlock, why ?
Because on previous example, we setting layout for ASDisplayNode class. In this example, we set our layouting node for the ASViewController, and precisely for rootNode inside ASViewController, think about UIViewController have view.
So UIViewController.view is kinda similliar with ASViewController.node. Setting layouting will always targeting node. So in ASViewController, we will access ASViewController node and set the layoutSpec by accessing, self.node.layoutSpecBlock
In the layoutSpec, we just create a vertical stack, with our story and post node.
It is wrap up, run your app and see your TextureGram.
Conclusion
Man, that’s quite a long tutorial, but here we are at the finish line running our TextureGram 💪. As we finished this tutorial, i hope you guys get a brief introduction about Texture. There’s a lot of other thing about Texture worth to explore, and it’s your job to explore it. You can check Texture Website, lots of documentations, example or contribute to its repository.
Check completed TextureGram project here:
Until then, keep Texturing and thanks for your time.