Tape is one of our most recent iOS apps at Yeti, and one we’re very proud of because it is our largest application written in Swift.
To preface, Tape is a social video editing app that allows users to record clips and “tape” them together into a longer video. Through the use of clip editing, filters, and music, our beta users have already made some very impressive Tapes.
One of the features that we had to implement were hashtags and mentions - if a user inputs a #hashtag or @mentions a user, that word would be highlighted, tappable, and take you to the appropriate page. This feature appears in multiple places throughout our app such as video description, comments, so it was important for us to make the code as reusable as possible.
Here's what this feature looks like in the comments section of Tape:
In order create this functionality, we wrote a custom class which inherits from UITextView.
The logic within our custom class does two main things: it sets the text attributes on the incoming string and handles the tap gesture. Let's talk about setting text attributes first. In our public setText function, we pass the string and the text attributes we want to set on it and add a tap gesture to the hashtag or mention. Here's our actual setText method in Tape:
public func setText(text: String, withHashtagColor hashtagColor: UIColor, andMentionColor mentionColor: UIColor, andCallBack callBack: (String, wordType) -> Void, font: UIFont) { self.callBack = callBack var attrString = NSMutableAttributedString(string: text) self.attrString = attrString var textString = NSString(string: text) self.textString = textString // Set initial font attributes for our string attrString.addAttribute(NSFontAttributeName, value: font, range: NSRange(location: 0, length: textString.length)) // Call a custom set Hashtag and Mention Attributes Function setAttrWithName("Hashtag", wordPrefix: "#", color: hashtagColor, text: text) setAttrWithName("Mention", wordPrefix: "@", color: mentionColor, text: text) // Add tap gesture that calls a function tapRecognized when tapped let tapper = UITapGestureRecognizer(target: self, action: "tapRecognized:") addGestureRecognizer(tapper)}
And in our custom setAttrWithName function:
func setAttrWithName(attrName: String, wordPrefix: String, color: UIColor, text: String) { let words = text.componentsSeparatedByString(" ") for word in words.filter({$0.hasPrefix(wordPrefix)}) { let range = textString!.rangeOfString(word) attrString.addAttribute(NSForegroundColorAttributeName, value: color, range: range) attrString.addAttribute(attrName, value: 1, range: range) attrString.addAttribute("Clickable", value: 1, range: range) } self.attributedText = attrString }
This is fairly straight forward code that converts the incoming string from a non-mutable string to mutable one and adds attributes on the string. Finally, it adds a gesture recognizer to the string. Something you might've noticed is that we've added an attribute name for “Hashtag” or “Mention” in our setText function - in the next section we'll explain why this is necessary.
After setting text attributes and adding our tap gesture, we handle the tap via our tapRecognized function (shown below). Because checking for our text with a .Word granularity omits special characters such as @ and #, we had to set an attribute name to the word so we can easily identify whether the word is a hashtag or a mention. Once identified, we run our callback function which handles what data will get displayed. If a user clicks on a hashtag, they will be presented with the search results for the hashtag, and if a user clicks on a mention, they will go to the mentioned user's profile page if it exists. The logic of which page to show the user is defined in our callback function, but since it is fairly straight forward, we have not shown that code here.
func tapRecognized(tapGesture: UITapGestureRecognizer) { // Gets the range of word at current position var point = tapGesture.locationInView(self) var position = closestPositionToPoint(point) let range = tokenizer.rangeEnclosingPosition(position, withGranularity: .Word, inDirection: 1) if range != nil { let location = offsetFromPosition(beginningOfDocument, toPosition: range!.start) let length = offsetFromPosition(range!.start, toPosition: range!.end) let attrRange = NSMakeRange(location, length) let word = attributedText.attributedSubstringFromRange(attrRange) // Checks the word's attribute, if any let isHashtag: AnyObject? = word.attribute("Hashtag", atIndex: 0, longestEffectiveRange: nil, inRange: NSMakeRange(0, word.length)) let isAtMention: AnyObject? = word.attribute("Mention", atIndex: 0, longestEffectiveRange: nil, inRange: NSMakeRange(0, word.length)) // Runs callback function if word is a Hashtag or Mention if isHashtag != nil && callBack != nil { callBack!(word.string, wordType.Hashtag) } else if isAtMention != nil && callBack != nil { callBack!(word.string, wordType.Mention) } }}
The code for our feature worked great for our app, but it isn't without flaws. For example, clicking on a #hashtag would detect and return the word ‘hashtag’, but clicking on the character ‘#’ itself would not register as a click while clicking on any part of the word itself would. This minor bug is unnoticable to users, but it's good to note.
Overall, working on a larger swift project has been as rewarding for me as it was challenging. Although there are many nuances to the language and relatively few code examples out there, one of the huge benefits of Swift was how readable it is. This was my first compiled language and one of the most complicated apps I've worked on to date, so I'm especially grateful to Swift for making large, complex codebase simpler. It's an exciting time to learn Swift and I can't wait to dive in deeper!
Thanks to our wonderful co-op John Kohler for providing the initial code.