Last active
May 22, 2021 05:16
-
-
Save jeffersonsetiawan/c2d6835e4d7780dea85974e5bc995d80 to your computer and use it in GitHub Desktop.
Create Slack like reaction using Texture and flexWrap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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