How can I use ASTextNode with TTTAttributedLabel

1.5k views Asked by At

I have built a ASCellNode and it works perfectly. However when I used a traditional UICollectionViewCell I used an TTTAttributedLabel with links.

I don't know how should I replicate this with AsyncDisplayKit

I can assign the attriubtedText from the TTTAttributedLabel to an ASTextNode but of course it doesn't keep the links. How could I efficiently do this. Bellow the example of my ASCellNode.

protocol EmailSentDelegator : class {
    func callSegueFromCell(data object: JSON)
}

class EmailCellNode: ASCellNode, TTTAttributedLabelDelegate {

    let cardHeaderNode: ASTextNode
    var frameSetOrNil: FrameSet?

    init(mailData: JSON) {
        // Create Nodes
        cardHeaderNode = ASTextNode()

        super.init()

        // Set Up Nodes

        cardHeaderNode.attributedString = createAttributedLabel(mailData, self).attributedText

        // Build Hierarchy
        addSubnode(cardHeaderNode)
    }

    override func calculateSizeThatFits(constrainedSize: CGSize) -> CGSize {
        var cardSize = CGSizeZero
        cardSize.width = UIScreen.mainScreen().bounds.size.width - 16

        // Measure subnodes
        let cardheaderSize = cardHeaderNode.measure(CGSizeMake(cardSize.width - 56, constrainedSize.height))
        cardSize.height = max(cardheaderSize.height,40) + subjectLabelSize.height + timeStampSize.height + emailAbstractSize.height  + 30

        // Calculate frames
        frameSetOrNil = FrameSet(node: self, calculatedSize: cardSize)
        return cardSize
    }

    override func layout() {
        if let frames = frameSetOrNil {
            cardHeaderNode.frame = frames.cardHeaderFrame
        }
    }

    func attributedLabel(label: TTTAttributedLabel!, didSelectLinkWithTransitInformation components: [NSObject : AnyObject]!) {
            self.delegate.callSegueFromCell(data: mailData)
    }

    func createAttributedLabel(mailData: JSON, cell: EmailCellNode) -> TTTAttributedLabel{
        let senderName = mailData["From"]["Name"].string!
        var recipients:[String] = []

        for (key: String, subJson: JSON) in mailData["To"] {
            if let recipientName = subJson["Name"].string {
                recipients.append(recipientName)
            }
        }
        var cardHeader = TTTAttributedLabel()
        cardHeader.setText("")
        cardHeader.delegate = cell
        cardHeader.userInteractionEnabled = true

        // Add sender to attributed string and save range

        var attString = NSMutableAttributedString(string: "\(senderName) to")
        let senderDictionary:[String:String] = ["sender": senderName]
        let rangeSender : NSRange = (attString.string as NSString).rangeOfString(senderName)

        // Check if recipients is nil and add undisclosed recipients
        if recipients.count == 0 {
            attString.appendAttributedString(NSAttributedString(string: " undisclosed recipients"))
            let rangeUndisclosed : NSRange = (attString.string as NSString).rangeOfString("undisclosed recipients")
            attString.addAttribute(NSFontAttributeName, value: UIFont(name: "SourceSansPro-Semibold", size: 14)!, range: rangeUndisclosed)
            attString.addAttribute(NSForegroundColorAttributeName, value: UIColor.grayColor(), range: rangeUndisclosed)
        } else {

            // Add recipients (first 5) to attributed string and save ranges for each

            var index = 0
            for recipient in recipients {
                if (index == 0) {
                    attString.appendAttributedString(NSAttributedString(string: " \(recipient)"))
                } else if (index == 5){
                    attString.appendAttributedString(NSAttributedString(string: ", and \(recipients.count - index) other"))
                    break
                } else {
                    attString.appendAttributedString(NSAttributedString(string: ", \(recipient)"))
                }
                index = index + 1
            }
        }
        cardHeader.attributedText = attString

        // Adding recipients and sender links with recipient object to TTTAttributedLabel
        cardHeader.addLinkToTransitInformation(senderDictionary, withRange: rangeSender)

        if recipients.count != 0 {
            var index = 0
            var position = senderName.length + 2
            for recipient in recipients {
                let recipientDictionary:[String: AnyObject] = ["recipient": recipient,"index": index ]
                let rangeRecipient : NSRange = (attString.string as NSString).rangeOfString(recipient, options: nil, range: NSMakeRange(position, attString.length-position))
                cardHeader.addLinkToTransitInformation(recipientDictionary, withRange: rangeRecipient)
                index = index + 1
                if (index == 5) {
                    let recipientsDictionary:[String: AnyObject] = ["recipients": recipients]
                    let rangeRecipients : NSRange = (attString.string as NSString).rangeOfString("and \(recipients.count - index) other")
                    cardHeader.addLinkToTransitInformation(recipientsDictionary, withRange: rangeRecipients)
                }
                position = position + rangeRecipient.length
            }
        }
        return cardHeader
    }
}

extension EmailCellNode {
    class FrameSet {
        let cardHeaderFrame: CGRect
        init(node: EmailCellNode, calculatedSize: CGSize) {
            var calculatedcardHeaderFrame = CGRect(origin: CGPointMake(senderPhotoFrame.maxX + 8, senderPhotoFrame.minY) , size: node.cardHeaderNode.calculatedSize)
            cardHeaderFrame = calculatedcardHeaderFrame.integerRect.integerRect
        }
    }
}
3

There are 3 answers

0
Jonas Romain Wiesel On BEST ANSWER

I ended up using solely ASTextNode it doesn't have as many features at TTTAttributedLabel but enough for my need. Further more since it's a heavy ASCollectionView it was better to go fully Async. Bellow the an exemple of how I did this in a ASCellNode with the creation of the complexe ASTextNode.

here is the final result with clickable name passing JSON data through a segue.

enter image description here

here is a simplified version of building the NSAttributedString

func createAttributedLabel(mailData: JSON) -> NSAttributedString{ var recipients:[String] = []

for (key: String, subJson: JSON) in mailData["To"] {
    if let recipientName = subJson["Name"].string {
        recipients.append(recipientName)
    }
}
// Add recipients to attributed string and save ranges for each
    var index = 0
    var position = senderName.length + 2
    for recipient in recipients {
            let recipientDictionary:[String: AnyObject] = ["recipient": recipient,"index": index ]
            let recipientJSON = mailData["To"][index]
            attString.appendAttributedString(NSAttributedString(string: ", \(recipient)"))
            let rangeRecipient : NSRange = (attString.string as NSString).rangeOfString(recipient, options: nil, range: NSMakeRange(position, attString.length-position))
            attString.addAttributes([
                kLinkAttributeName: recipientJSON.rawValue,
                NSForegroundColorAttributeName: UIColor.blackColor(),
                NSFontAttributeName:  UIFont(name: "SourceSansPro-Semibold", size: 14)!,
                NSUnderlineStyleAttributeName: NSUnderlineStyle.StyleNone.rawValue],
                range: rangeRecipient)
    }
    index = index + 1
return attString

}

end then to detect link taps. I had to transform my JSON in raw value to pass the data across.

func textNode(textNode: ASTextNode!, tappedLinkAttribute attribute: String!, value: AnyObject!, atPoint point: CGPoint, textRange: NSRange) {

//    The node tapped a link; open it if it's a valid URL
    if  (value.isKindOfClass(NSDictionary)) {
        var jsonTransferred = JSON(rawValue: value as! NSDictionary)
        self.delegate.callSegueFromCellUser(data: jsonTransferred!)
    } else {
        var jsonTransferred = JSON(rawValue: value as! NSArray)
        self.delegate.callSegueFromCellRecipients(data: jsonTransferred!)
    }
}
1
Jonathan Hersh On

I am not familiar with AsyncDisplayKit, but there are some issues with your use of TTTAttributedLabel:

  • You are initializing the label with TTTAttributedLabel(), which calls init. You must instead use the designated initializers initWithFrame: or initWithCoder:, as init will not properly initialize the links array and various other internal properties. In the latest release of TTTAttributedLabel, init is marked as unavailable.

  • You are assigning to the attributedText property. Please see this note in TTTAttributedLabel.h:

    @bug Setting attributedText directly is not recommended, as it may cause a crash when attempting to access any links previously set. Instead, call setText:, passing an NSAttributedString.

    You should never assign to the attributedText property.

2
Scott Goodson On

I'm one of the primary maintainers of ASDK, and would love to help you address any challenges — feel free to open a GitHub issue on the project (even just for questions).

What features is ASTextNode lacking that you love about TTT? It does handle links, complete with centroid-based tap disambiguation between multiple, disjoint & line-wrapping links. There are probably missing features, but since the project is so widely used, I bet other developers would find it useful to add any that you find a need for.

That said, if you don't need the significant performance gains that come with moving text layout and rendering off the main thread, you can wrap TTT in an initWithViewBlock — or simply create & add the view directly, without node wrapping, inside of any node's -didLoad method. Normally with ASDK, wrapping views is not required (which is why I asked about ASTextNode).

Good luck with your work!