Session 2 - Layouting with LayoutSpec
Session 3 - Create a List of Product Cards
Session 4 - Usage Comparison with UIKit
The session covers the basic of Texture
that is the introduction of ASDisplayNode
and how to use it along with node container
.
Also we will learn how to build a simple layout with layoutSpecBlock
This session is labeled under Start
inside the Session 1 - Basic
folder in the Xcode
project example. We will code together on the subsection Rendering Component in Texture Way
.
Before we learn the basics of Texture
, it is better we take a look at how UIKit
structuring the views to rendering component on screen first.
- Run the project
- Tap on
Basic UIKit
.
On the screen, you can see a colored box centering on the page. Open the BasicUIKitViewController.swift
to see the implementation. You may be familiar with the code, how to set up the viewcontroller
, the child view, and the constraints. We will create something similar to that by using Texture
, so let's get started!
The file will look like this:
import UIKit
class BasicTextureViewController: UIViewController {
}
The BasicTextureViewController
still inherit the UIViewController
so we need to change the subclass
Before we change the subclass, we need to import the framework Texture
by adding import AsyncDisplayKit
. Next we change the subclass viewController
into ASDKViewController<ASDisplayNode>
. Why we do have a generic on our ViewController
?. This generic used to determined which type of root node
we want to use. In this case we just need a plain ASDisplayNode
as our root node
.
import AsyncDisplayKit
import UIKit
class BasicTextureViewController: ASDKViewController<ASDisplayNode> {
}
Note: In the Create Product Card List Session
we will configure the root node
with a different type of ASDisplayNode
Next, we need to create an initializer for our viewController
. We can start by adding an empty init()
but we need to add the override
keyword before the init. The compiler will throw an error that we need to satisfy by adding required init?(coder: NSCoder)
......
class BasicTextureViewController: ASDKViewController<ASDisplayNode> {
override init() {
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
We have successfully changed our ViewController
to ASDKViewController
but the viewController
shows a blank view with a black color background. That means our root node
has still not been configured yet, let's start by changing the init into super.init(node: ASDisplayNode)
.
The parameter node
means that we can use any kind of ASDisplayNode
as our root node
but let's stick with a simple ASDisplayNode
for now.
We can change the background color or any property of the root node
by accessing the node.
property in our ViewController
. It similar to when we are accessing the view.
property in UIViewController
.
Let's change the background color to white
color
......
class BasicTextureViewController: ASDKViewController<ASDisplayNode> {
override init() {
super.init(node: ASDisplayNode())
node.backgroundColor = .white
}
......
}
Now the fun part begins, we will create a rectangle box as the child of ViewController
. Create the box by initialize ASDisplayNode()
.To make the box see clearly on the screen we can add some color and set the size of the box.
For the size, we can use frame
property for now and let's start by adding a simple size by adding subNode.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
.
After that, add your box into your viewController
by using node.addSubNode(subNode)
, It similar to UIKit
view.addSubview.
......
class BasicTextureViewController: ASDKViewController<ASDisplayNode> {
override init() {
super.init(node: ASDisplayNode())
node.backgroundColor = .white
let subNode = ASDisplayNode()
subNode.backgroundColor = .orange
subNode.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
node.addSubnode(subNode)
}
......
}
The box is showing up, but what are the advantages of layouting in Texture
? it looks similar to UIKit
.
Introduction: LayoutSpecBlock
LayoutSpecBlock
is a closure where you play around with the layout. This is where the Flexbox
concept is applied. The LayoutSpecBlock
has 2 parameters:
node
: thenode
itselfconstrainedSize
: the size of the rootnode
We don't need those 2 parameters for now so just omit them using _, _
. The block needs a return type of ASLayoutSpec
which is the kind of layout we want to use. There are many types of layout that we will learn in this workshop but for now, we will use ASCenterLayout
to center the node.
......
class BasicTextureViewController: ASDKViewController<ASDisplayNode> {
override init() {
super.init(node: ASDisplayNode())
node.backgroundColor = .white
let subNode = ASDisplayNode()
subNode.backgroundColor = .orange
subNode.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
node.addSubnode(subNode)
node.layoutSpecBlock = { _, _ in
return ASCenterLayoutSpec(centeringOptions: .XY,
sizingOptions: [],
child: subNode)
}
}
......
}
The ASCenterLayout
has 3 parameters:
centeringOptions
: the position of the child, We use the.XY
value that means we will center the child onHorizontal
andVertical
position.sizingOptions
: how much space the center spec will take up. We don't need this configuration right now instead we can use an empty value[]
.child
: the node that will be centered, in this case, issubNode
object.
The result is not the same as we not expected, the child node covers the whole screen, to fix that we can go to the next step.
Why did the child node
cover the whole screen? the answer is we haven't set the size yet. We already set the size before on .frame
but because right now we are using layoutSpecBlock
the frame
value is no longer valid. The goal of layoutSpecBlock
is to apply the FlexBox
concept which means we don't need to calculate the position & size manually. So let's fix the size.
By adding the style.preferredSize
, we specified a constant size to our child node
.
......
class BasicTextureViewController: ASDKViewController<ASDisplayNode> {
override init() {
super.init(node: ASDisplayNode())
node.backgroundColor = .white
let subNode = ASDisplayNode()
subNode.backgroundColor = .orange
// Change this
// subNode.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
// Into this
subNode.style.preferredSize = CGSize(width: 300, height: 300)
node.addSubnode(subNode)
node.layoutSpecBlock = { _, _ in
return ASCenterLayoutSpec(centeringOptions: .XY,
sizingOptions: [],
child: subNode)
}
}
......
}
Cool! Finally, we have successfully created a simple component using Texture
by creating a node, plug it into viewController
, and configure the layout using layoutSpecBlock
. In the next session, we will explore various LayoutSpecs
!
This session covers the various types of LayoutSpec
that we can use to configure the layouting of elements in our ViewController
as well as creating a custom/reusable node of ASDisplayNode
type.
This session is labelled under ASLayoutSpec - Interactive
in the XCode project example.
The files used in this session is under Session 2 - InteractiveUI
.
- For each
LayoutSpec
(e.g.:ASInsetLayoutSpec
), there will be a folder containing 2 files. FooLayoutVC
contains the real implementation of theLayoutSpec
FooLayoutVCInteractive
contains a bordered sandbox which displays the view ofFooLayoutVC
scaled to the size of the sandboxFooLayoutVCInteractive
also contains a set of labelled buttons for the audience to configure the arguments used in the relativeLayoutSpec
We have created 2 ViewControllers
that implement a layout that uses a combination of different LayoutSpecs
.
In this session, we hope to provide the audience with layout examples that may relate more to real-life cases when developing an app, which may help the audience understand more about the behaviours of various LayoutSpecs
and how to achieve a specific layout by combining them.
The goal of the exercises in this session is for the audience to re-create the layout provided in the example (top-right button in navigation bar of every Exercise page
), all the elements (nodes
) are provided
- Exercise 1 - Learn how to create the layout using a combination of
ASStackLayoutSpec
andASInsetLayoutSpec
- Exercise 2 - Edit the layout from
Exercise 1
by addingASBackgroundLayoutSpec
to the layout - Exercise 3 - Learn how to create a reusable custom node of
ASDisplayNode
type and achieve the same result asExercise 2
, but only instantiatingCustomNode
instead of a bunch of elements in theViewController
- Cheats for
Exercise 1
,Exercise 2
, andExercise 3
are provided underSession 2 - InteractiveUI/Components/Cheats
- It is recommended that the audience do not look at the cheats until the exercises are complete or they are completely stuck
This session breaks down the process of creating a component using Texture
.
Product Card
is one of the most used components in Tokopedia App
. It is used to represent the description of a product. However, every product has different amount of information. Thus, the presentation of each Product Card
may vary.
After this session, you are expected to learn three things using Texture
:
As stated earlier, not every Product Card
displays the same amount of information. This session shows how to create a Product Card
based on the information provided.
This session shows how to display a collection of products using List
format.
This session shows how easy it is to update the UI of a Texture
component.
I. Create Live Update on Component
This session is covered in the folder labelled as Session 3 - Create Product Card
Before we start, to have a better picture on what we are going to create, do the followings.
- Run the project
- Tap on
Create Product Card
. - Then, tap on
Expected
Also, take a look at the information model.
- Go back to Xcode
- Open
Product.swift
Now, let's start!
This is the main class of the Product Card component.
Let's put product image, name and price first.
-
Add the nodes as properties
let imageNode = ASNetworkImageNode() let nameNode = ASTextNode() let priceNode = ASTextNode()
-
Setup the nodes
imageNode.url = model.imageUrl imageNode.style.preferredSize = CGSize(width: 80, height: 80) nameNode.attributedText = NSAttributedString( string: description.name, attributes: [ NSAttributedString.Key.font : UIFont.systemFont(ofSize: 12, weight: .regular) ] ) priceNode.attributedText = NSAttributedString( string: description.price, attributes: [ .font: UIFont.systemFont(ofSize: 14, weight: .bold) ] )
-
Arrange it in
LayoutSpecThatFits
let verticalStack = ASStackLayoutSpec( direction: .vertical, spacing: 10, justifyContent: .start, alignItems: .start, children: [ nameNode, priceNode ] ) let mainStack = ASStackLayoutSpec( direction: .horizontal, spacing: 8, justifyContent: .start, alignItems: .start, children: [ imageNode, verticalStack ] ) let mainInset = ASInsetLayoutSpec( insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), child: mainStack ) return mainInset
-
Run the project and see the result
Before we continue, it is better to refactor the right hand side of the Product Card
into a new class. Else, we might have a bloated code like this.
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let shopStack = ASStackLayoutSpec(
direction: .horizontal,
spacing: 4,
justifyContent: .start,
alignItems: .start,
children: [
badgeNode,
nameNode
]
)
let verticalStack = ASStackLayoutSpec(
direction: .vertical,
spacing: 10,
justifyContent: .start,
alignItems: .start,
children: [
nameNode,
priceNode,
shopNode
]
)
let mainStack = ASStackLayoutSpec(
direction: .horizontal,
spacing: 8,
justifyContent: .start,
alignItems: .start,
children: [
imageNode,
verticalStack
]
)
let mainInset = ASInsetLayoutSpec(
insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8),
child: mainStack
)
return mainInset
}
So, let's move the vertical stack
-
Cut this
let nameNode = ASTextNode() let priceNode = ASTextNode()
-
This
nameNode.attributedText = NSAttributedString( string: description.name, attributes: [ NSAttributedString.Key.font : UIFont.systemFont(ofSize: 12, weight: .regular) ] ) priceNode.attributedText = NSAttributedString( string: description.price, attributes: [ .font: UIFont.systemFont(ofSize: 14, weight: .bold) ] )
-
And This
let verticalStack = ASStackLayoutSpec( direction: .vertical, spacing: 10, justifyContent: .start, alignItems: .start, children: [ nameNode, priceNode ] )
-
Then paste it to
Start/Components/ProductCardDescriptionNode.swift
-
Add this node into
Start/ProductCardNode.swift
let descriptionNode: ProductCardDescriptionNode
descriptionNode = ProductCardDescriptionNode(description: model.description)
let mainStack = ASStackLayoutSpec( direction: .horizontal, spacing: 8, justifyContent: .start, alignItems: .start, children: [ imageNode, descriptionNode ] )
Phew, now that we got that out of the way, let's... WAIT! One more thing. This shop row is a bit different from name row and price row, because the badge may or may not be displayed based on the shop type.
Please take a look at
Start/CreateProductCardViewController.swift
. That is the data that we will feed into the product card. Let's change the shop type from.officialStore
to.none
. After that, run the project again and take a look at the expectedProduct Card
. Something is missing...
Okay now, let's continue :).
-
Open
Start/Components/ProductCardShopNode
We will create the shop row here -
Add the nodes as properties
let badgeNode: ASImageNode? let nameNode = ASTextNode()
You might notice the optional, what is that for? Well, remember that the badge can disappear?
-
Setup the nodes
switch shop.type { case .officialStore: badgeNode = ASImageNode() badgeNode.image = UIImage(named: "official_store") case .powerMerchant: badgeNode = ASImageNode() badgeNode.image = UIImage(named: "power_merchant") case .none: badgeNode = nil } badgeNode?.style.preferredSize = CGSize(width: 16, height: 16) nameNode.attributedText = NSAttributedString( string: shop.name, attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .regular), .foregroundColor: UIColor.darkGray ] )
-
Arrange it in
LayoutSpecThatFits
return ASStackLayoutSpec( direction: .horizontal, spacing: 4, justifyContent: .start, alignItems: .start, children: [badgeNode, nameNode] )
Oh no, there is an error! The reason is that
children
expects[ASLayoutElement]
not[ASLayoutElement?]
. What do we do? We may define achildren
property before adding it into the stack.let children: [ASLayoutElement] if let badgeNode = badgeNode { // safe unwrap children = [badgeNode, nameNode] } else { children = [nameNode] } return ASStackLayoutSpec( direction: .horizontal, spacing: 4, justifyContent: .start, alignItems: .start, children: children )
Or we can take advantage of Swift's High Order Function
compactMap
.return ASStackLayoutSpec( direction: .horizontal, spacing: 4, justifyContent: .start, alignItems: .start, children: [badgeNode, nameNode].compactMap { $0 } )
Basically,
compactMap
filters all nil values out of a collection. -
Add this node into
Start/Components/ProductCardDescriptionNode.swift
let shopNode: ProductCardShopNode
shopNode = ProductCardShopNode(shop: description.shop)
return ASStackLayoutSpec( direction: .vertical, spacing: 10, justifyContent: .start, alignItems: .start, children: [ nameNode, priceNode, shopNode ] )
-
Run the project and see the result
One thing you might notice about this row is that the stars are stacked horizontally, and then the star stack is stacked again with review count text. This time, there is no need to move the star stack into a new class because the layout is relatively simple.
-
Open
Start/Components/ProductCardReviewNode.swift
We will create the review row here -
Add the nodes as properties
let starNodes: [ASImageNode] let reviewCountNode = ASTextNode()
-
Setup the nodes
var tempStarNodes = [ASImageNode]() for index in 1...5 { let starNode = ASImageNode() starNode.image = UIImage(named: index >= review.rating ? "active_star" : "inactive_star") starNode.style.preferredSize = CGSize(width: 11, height: 11) tempStarNodes.append(starNode) } starNodes = tempStarNodes reviewCountNode.attributedText = NSAttributedString( string: "(\(review.reviewCount))", attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .regular), .foregroundColor: UIColor.gray ] )
-
Arrange it in
LayoutSpecThatFits
let starStack = ASStackLayoutSpec( direction: .horizontal, spacing: 1, justifyContent: .start, alignItems: .start, children: starNodes ) let mainStack = ASStackLayoutSpec( direction: .horizontal, spacing: 2, justifyContent: .start, alignItems: .center, children: [starStack, reviewCountNode] ) return mainStack
-
Add this node into
Start/Components/ProductCardDescriptionNode.swift
let reviewNode: ProductCardReviewNode
reviewNode = ProductCardReviewNode(review: description.review)
return ASStackLayoutSpec( direction: .vertical, spacing: 10, justifyContent: .start, alignItems: .start, children: [ nameNode, priceNode, shopNode, reviewNode ] )
-
Run the project and see the result
This row is optional, as free shipping badge may only be shown if isFreeShipping == true
(refer to Start/CreateProductCardViewController.swift
). Also, this row only contains one image node so there is no need to create a new node class.
-
Add this node into
Start/ProductCardDescriptionNode.swift
let freeShippingNode: ASImageNode?
freeShippingNode = description.isFreeShipping? ASImageNode() : nil freeShippingNode?.image = UIImage(named: "free_shipping") freeShippingNode?.style.preferredSize = CGSize(width: 67, height: 16)
return ASStackLayoutSpec( direction: .vertical, spacing: 10, justifyContent: .start, alignItems: .start, children: [ nameNode, priceNode, shopNode, reviewNode, freeShippingNode ].compactMap { $0 } )
-
Run the project and see the result
-
What if we want to add more spacing between reviewNode and freeShippingNode?
freeShippingNode?.style.spacingBefore = 4 // there is also spacing after if you need it
in this part, we will refactor the ImageNode to Class ProductCardImageNode
, and we will attach a wishlist in the top corner(inside) of the image
-
Open
ProductCardNode
. -
Move
imageNode
intoProductCardImageNode
.
let imageNode: ASNetworkImageNode
// in init
imageNode = ASNetworkImageNode()
imageNode.url = imageUrl
imageNode.style.preferredSize = CGSize(width: 80, height: 80)
imageNode.cornerRadius = 6
-
Add
ProductCardWishlistNode
let wishlistNode = ProductCardWishlistNode()
-
Create layout for
ProductCardImageNode
let wishlistInset = ASInsetLayoutSpec(
insets: UIEdgeInsets(top: 4, left: 4, bottom: .infinity, right: .infinity),
child: wishlistNode
)
return ASOverlayLayoutSpec(child: imageNode, overlay: wishlistInset)
-
Add
ProductCardImageNode
intoProductCardNode
let imageNode: ProductCardImageNode
imageNode = ProductCardImageNode(imageUrl: model.imageUrl)
let mainStack = ASStackLayoutSpec( direction: .horizontal, spacing: 8, justifyContent: .start, alignItems: .start, children: [ imageNode, descriptionNode ] )
-
Run the project and see the result
Well, there you go. Easy, isn't it? In a nutshell, there are three steps to create a component using Texture
:
- Create properties of all nodes
- Setup the nodes
- Arrange it in
LayoutSpecThatFits.
Now let's move on to the next step: Creating a List of Product Card.
In this part, we will show how to create lists using a Table node with auto height.
Next, we will create a List of Product Card
using TableNode.
- Change
ProductCardNode
subclass toASCellNode
- Open
ProductCardStarterViewController
- Change the
ASDKViewController<ASDisplayNode>
generic toASDKViewController<ASTableNode>
andsuper.init
to confirm withASTableNode
- For use
TableNode
we needdata source
to give information what data we will render
node.dataSource = self
then we need to implement two method numberOfRowsInSection
this method
give the detail to how many data we will render and nodeForRowAt
to give
the information which cell will render
extension ProductCardViewController: ASTableDataSource {
func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
return model.count
}
func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode {
let data = model[indexPath.row]
return ProductCardFinalNode(model: data)
}
}
- Build and run the app now our
ProductCardList
seems good with different detail of height but we still find a separtor now we will delete it to delete it we need implement methodviewDidLoad
because we will acess thetableNode.view
override func viewDidLoad() {
super.viewDidLoad()
node.view.separatorStyle = .none
}
In this part, we will perform a live update component on wishlist refer to product card expected wishlist could toggle on and off.
- Open
start/ProductCardWishlistNode
. - Add action to activeNode and inactiveNode.
activeNode.addTarget(self, action: #selector(changeWishlistState), forControlEvents: .touchUpInside)
inactiveNode.addTarget(self, action: #selector(changeWishlistState), forControlEvents: .touchUpInside)
- Move to line create a new property to handle state wishlist.
var isWishlist = false
- Move to function
changeWishlistState
add toogle on and toogle off for wishlist
@objc func changeWishlistState() {
// change the state vice versa
isWishlist = !isWishlist
setNeedsLayout()
}
by calling setNeedsLayout()
we trigger the layoutSpecThatFits
to layout again.
5. Move to layoutSpecThatFits
this function will return an active node because we want to toggle on/off the wishlist so that we will add.
var child: ASImageNode
if isWishlist {
child = activeNode
} else {
child = inactiveNode
}
let wishlistInset = ASInsetLayoutSpec(
insets: UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2),
child: child
)
- Build and run
In this part, you can see that our lists of product cards
are working fine. You can toggle the wishlist,
but we have an inset issue from our product cards, so the challenge is to fixing the card inset.
- Clue: background and insets
- Cheat: you can checkout branch
BeautyProductCard
.
In this session, we will compare with the usage of UIKit for the same UI component, which is Wish List Product Card
, from the implementation and performance perspective.
In UIKit, we usually use Auto Layout
and managing Constraints
to dynamically calculate the size and position of all the views. Sometimes it can be cumbersome to manage the Constraints
if we had a lot of UI components, and these components need to be optionally placed on the view hierarchy. In this example, we use Constraints
to manage UI components' size and position.
We already created an example of UIKit implementation for Wish List Product Card
. The view itself located in the InterfaceBuilder
folder group, and we use XIB
to construct it. Also, we had created a separated sub-component for the Rating View
in the Components
folder group to make it easy to manage.
Next, open the ProductCardWishlistTableViewCell.xib
file. We declared this UI component as UITableViewCell
because we want to put it on UITableView
. If you dig deeper into the child-views, you can see these components heavily-dependent on Constraints
to manage its size and position.
With implementation like this, how to hide the view components
that not need to be shown in a certain condition? To achieve that, usually, we can manage Constraints
by:
- Managing it's
constant
property - Managing it's
isActive
state
In this file, we already implement the code for us. We see that we have Constraint Outlets
section. We will heavily depend on that.
In the current implementation, to change the Shop Badge
image, we can change the shopBadgeImage.image
property to the preferred image. But for managing the hidden state, we still only change the shopBadgeImage.isHidden
property. If you run this code, this implementation only hiding the view itself and leaving blank space on it.
But, if we want to hide the view and also remove the blank space, how to do that?
In the refreshUi()
method, you can see the switch case section for shop type
. In each case, you can try to enable: showShopBadge(with:)
or hideShopBadge()
code and make the rest disabled by commenting them out. After that, let's try to run it.
You can see, the new implementation can make the Shop Badge
image hidden and eliminate the blank space. If you see the showShopBadge(with:)
and hideShopBadge()
code, we managing constant
property of shopBadgeWidthConstraint
and shopBadgeGap
Constraints to hide the blank space.
By this implementation, the sizing can be wrong, which can be caused by human error if we need to change the view size in the XIB
but we forget to change in the .swift
too. This error can be minimized if we implementing full UI implementation programmatically without using XIB
.
If we see the Managing Shop Badge Image
implementation, the implementation looks simple. But it can be otherwise if we need to handle multiple Views'
state.
We have already run the previous implementation. If we look at the screen, we can see there's blank space too while hiding the Rating
and Bebas Ongkir
view.
To fix that we can see in the ProductCardWishlistTableViewCell.swift
file, on the refreshUI()
method, there's a section called #2 Extra Cases: Hiding both rating bar and free shipping
.
Below that section, we need to disable the codes that have commented on it by comment it out. Also, we need to enable other codes below of previous code, which has commented on it too, by uncommenting it. Try to run the code.
As we can see, now the UI can be shown properly by removing blanks space if the Rating
and/or Free Shipping
image was hidden. If you see the codes that you enable by uncommenting-out before, we heavily managing the Constraints
state there. We need to turn-off the un-needed Constraints
first and enable what we need. If you wrongly manage it, the Constraints
can be broken and the UI can be glitched because the OS
will attempt to fix that broken Constraints
and sometimes it fixed in the wrong way.
Imagine if we had many View components
that need to be dynamically shown based on a certain state.
In some years ago, Apple introduced UIStackView
that we can use to easily manage the View components
state. If we hide a view
in the stack, UIStackView
will automatically manage for removing blank space of that view
.
But in some conditions, it can be painful to control UIStackView
if we had deep-nested UIStackView
and some child-view
have a different size than the other. Also for spacing between each element, it should be of the same size. For handling that, usually we need to use dummy UIView
that acts as spacer
.
As we know, Texture
has the benefit of its UI Calculation
because it is done in the background thread, contrary to UIKit
that the calculation is done in the main thread.
We already made a video showing the performance of both, that shown in FPS
metrics. In that video, we showing the implementation of iPhone 5
with iOS 9
. In that video, it clearly shows that UIKit
implementation have performance drop. It has an average of 30++ fps compared to Texture
that has an average of 50++ fps.
We can improve the UIKit
implementation by implementing shadowPath
for showing the shadow. Also, we already made a video showing the performance while using shadowPath
. The FPS
increased to average 40++ fps, but still can't on par with Texture
.
https://bit.ly/twOct2020Supplementary
So far we talk about past and present, and now this time we will talk about future. We've talk about the advantages of using Texture, and what make we more confident to use Texture is by looking at some big companies like Pinterest and Telegram are using Texture in their iOS apps.
All of our workshop session show lot of benefit of using Texture, but if you check at the syntax of Texture, it still have lot of code and not "declarative" like SwiftUI. For SwiftUI, we can't used it yet on our main app because we still need to support iOS < 13, and SwiftUI is "a bit" buggy on iOS 13 as well. This is how our Texture layout code looks like:
let vStack = ASStackLayoutSpec(
direction: .vertical,
spacing: 16,
justifyContent: .start,
alignItems: .start,
children: [tokopediaText, tokopediaButtonNode]
)
return ASStackLayoutSpec(
direction: .horizontal,
spacing: 16,
justifyContent: .start,
alignItems: .start,
children: [vStack, goBtnNode]
)
Compare to SwiftUI code, the Texture code still look much more code than the SwiftUI one.
HStack {
VStack {
Text("Hello World")
Button("Tokopedia") {}
}
Button("Go!") {}
}
That's where the TextureSwiftSupport
shines. This library can made your Texture layouting code more like SwiftUI code by leveraging function builder swift feature that introduced in Swift version 5.1.
That texture code can be converted to SwiftUI like this:
LayoutSpec {
HStackLayout(spacing: 16) {
tokopediaText
tokopediaButtonNode
}
goBtnNode
}
And by the way, if you follow the Tokopedia iOS hotest news in early October, we've launch our first Widget! Built exclusively using SwiftUI. and our devs said that learning Texture make it easier for them to learn SwiftUI!
For final word, this is our quote of the day:
One step closer to learn SwiftUI but with UIKit foundation in a simpler way