Skip to content

Instantly share code, notes, and snippets.

@jeffersonsetiawan
Last active May 22, 2021 05:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeffersonsetiawan/c2d6835e4d7780dea85974e5bc995d80 to your computer and use it in GitHub Desktop.
Save jeffersonsetiawan/c2d6835e4d7780dea85974e5bc995d80 to your computer and use it in GitHub Desktop.
Create Slack like reaction using Texture and flexWrap
//
// MySlackVC.swift
// TextureWorkshop
//
// Created by Jefferson Setiawan on 22/05/21.
//
import AsyncDisplayKit
class MySlackVC: ASDKViewController<ASTableNode> {
var messages: [MessageState] = []
override init() {
super.init(node: ASTableNode())
node.dataSource = self
generateMessages(30)
node.backgroundColor = .white
node.reloadData()
}
func generateMessages(_ numberOfMessage: Int) {
messages = (1...numberOfMessage).map { index -> MessageState in
let reactionList = ["πŸ˜‚", "πŸŽ‰", "😨", "πŸ€”", "πŸ‘Ά", "😎", "πŸŽ„", "βœ…", "😊", "πŸ˜…", "⚠️", "πŸ™πŸ»"]
let numberOfReactions = Int.random(in: 0..<reactionList.count)
// simulate no reaction on first cell
let reactions: [ReactionState]
if index == 1 {
reactions = []
} else {
reactions = (0...numberOfReactions).map {
ReactionState(image: reactionList[$0], count: Int.random(in: 1...100), isActive: Bool.random())
}
}
return MessageState(id: index, name: "Person \(index % 3)", message: "Message number \(index)", reactions: reactions)
}
}
func updateEmoticon(index: Int, image: String) {
// For simplicity I use index
guard let reactionIndex = messages[index].reactions.firstIndex(where: { $0.image == image }) else { return }
if messages[index].reactions[reactionIndex].isActive {
messages[index].reactions[reactionIndex].count -= 1
} else {
messages[index].reactions[reactionIndex].count += 1
}
messages[index].reactions[reactionIndex].isActive.toggle()
node.reloadData() // you can do performBatchUpdate every changes made
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension MySlackVC: ASTableDataSource {
func numberOfSections(in tableNode: ASTableNode) -> Int {
return 1
}
func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
let message = messages[indexPath.row]
return { [weak self] in
let cell = MessageCellNode(state: message)
cell.onTap = { image in
self?.updateEmoticon(index: indexPath.row, image: image)
}
return cell
}
}
}
// MARK: MessageCell
struct MessageState {
var id: Int
var name: String
var message: String
var reactions: [ReactionState]
}
class MessageCellNode: ASCellNode {
private let state: MessageState
private let nameNode = ASTextNode()
private let messageNode = ASTextNode()
private var reactionNodes: [ReactionDisplayNode]? = nil
internal var onTap: ((String) -> ())?
init(state: MessageState) {
self.state = state
super.init()
automaticallyManagesSubnodes = true
nameNode.attributedText = NSAttributedString(string: state.name, attributes: [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 12)])
messageNode.attributedText = NSAttributedString(string: state.message)
if !state.reactions.isEmpty {
reactionNodes = state.reactions.map {
let node = ReactionDisplayNode(state: $0)
node.onTap = { [weak self] image in
self?.onTap?(image)
}
return node
}
}
}
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let reactionStack = ASStackLayoutSpec.horizontal()
reactionStack.children = reactionNodes
reactionStack.spacing = 4
reactionStack.flexWrap = .wrap
reactionStack.lineSpacing = 4
let mainStack = ASStackLayoutSpec.vertical()
mainStack.children = [nameNode, messageNode, reactionStack]
mainStack.spacing = 8
return mainStack
}
}
// MARK: ReactionNode
struct ReactionState {
var image: String
var count: Int
var isActive: Bool
}
class ReactionDisplayNode: ASDisplayNode {
private let iconNode = ASTextNode()
private let countNode = ASTextNode()
internal var onTap: ((String) -> ())?
private let state: ReactionState
init(state: ReactionState) {
self.state = state
super.init()
automaticallyManagesSubnodes = true
iconNode.attributedText = NSAttributedString(string: state.image)
countNode.attributedText = NSAttributedString(string: String(state.count))
backgroundColor = state.isActive ? .yellow : .lightGray
cornerRadius = 8
}
override func didLoad() {
super.didLoad()
let tapGesture = UITapGestureRecognizer()
tapGesture.addTarget(self, action: #selector(onTapEmoticon))
view.addGestureRecognizer(tapGesture)
}
@objc private func onTapEmoticon() {
onTap?(state.image)
}
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let stack = ASStackLayoutSpec.horizontal()
stack.children = [iconNode, countNode]
stack.spacing = 4
return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4), child: stack)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment