Please note, this blog entry is from a previous course. You might want to check out the current one.
When a user clicks on a Tweet, segue to a new UITableViewController which has four sections showing the “mentions” in the Tweet: Images, URLs, Hashtags and Users. The first section displays (one per row) any images attached to the Tweet (found in the media variable in the Tweet class). The last three show the items described in Required Task 1 (again, one per row).
Add a new table view controller to story board and a new table-view-controller class which you link to the new controller. Add a segue from the tweet cell too the new controller (don’t forget to add a segue identifier).
To show that the tweet cell has a segue/action associated enable its disclosure indicator. You will get a warning that the views need an update. … and there will be a concurrency of the constraints. To cope, change reduce the priority of the trailing constraints of the labels.
In the new view controller set the style of the existing cell to basic and provide a sensible identifier (it will hold the keywords).
Add a new cell (including a new cell class). Make it a custom cell and provide another identifier (it will hold the images). Add an image view and an activity indicator including outlets to the class … and don’t forget to set constraints for auto layout:
When updating the twitter cell, enable or disable the disclosure indicator depending on if there are details to show:
func updateUI() { ... if tweet.hashtags.count + tweet.urls.count + tweet.userMentions.count + tweet.media.count > 0 { accessoryType = UITableViewCellAccessoryType.DisclosureIndicator } else { accessoryType = UITableViewCellAccessoryType.None } } }
Strangely, adding the disclosure indicator breaks the automatically calculated cell height, when the cells are filled the first time, but it starts working after scrolling the table view. A work around for this “feature” is to force a reload of all sections in addition to reload just the data.
Additionally, set the current search text as title:
@IBAction func refresh(sender: UIRefreshControl?) { ... self.tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(0, self.tableView.numberOfSections())), withRowAnimation: .None) self.title = self.searchText ... }
Above we removed the disclosure indicator if there are not details to show. Now we need also to prevent segues from such tweets to fire:
private struct Storyboard { ... static let MentionsIdentifier = "Show Mentions" } override func shouldPerformSegueWithIdentifier(identifier: String?, sender: AnyObject?) -> Bool { if identifier == Storyboard.MentionsIdentifier { if let tweetCell = sender as? TweetTableViewCell { if tweetCell.tweet!.hashtags.count + tweetCell.tweet!.urls.count + tweetCell.tweet!.userMentions.count + tweetCell.tweet!.media.count == 0 { return false } } } return true }
When we fire a segue, set the current tweet for the new table view controller:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if let identifier = segue.identifier { if identifier == Storyboard.MentionsIdentifier { if let mtvc = segue.destinationViewController as? MentionsTableViewController { if let tweetCell = sender as? TweetTableViewCell { mtvc.tweet = tweetCell.tweet } } } } }
Hint #7 asks for a special data structure. We will store the data for the table view controller in a new array of this new data structure. The structure contains a title (which we will use in the next task as section title) and an array of the individual mentions. Those will either be keywords – plain strings – or images – at least their URLs and aspect ratios:
var mentions: [Mentions] = [] struct Mentions: Printable { var title: String var data: [MentionItem] var description: String { return "\(title): \(data)" } } enum MentionItem: Printable { case Keyword(String) case Image(NSURL, Double) var description: String { switch self { case .Keyword(let keyword): return keyword case .Image(let url, _): return url.path! } } }
The data structure is filled when the tweet data arrives (and we also set the title at this point), where we map the data from the tweet into the the data structure:
var tweet: Tweet? { didSet { title = tweet?.user.screenName if let media = tweet?.media { mentions.append(Mentions(title: "Images", data: media.map { MentionItem.Image($0.url, $0.aspectRatio) })) } if let urls = tweet?.urls { mentions.append(Mentions(title: "URLs", data: urls.map { MentionItem.Keyword($0.keyword) })) } if let hashtags = tweet?.hashtags { mentions.append(Mentions(title: "Hashtags", data: hashtags.map { MentionItem.Keyword($0.keyword) })) } if let users = tweet?.userMentions { mentions.append(Mentions(title: "Users", data: users.map { MentionItem.Keyword($0.keyword) })) } } }
The number of sections equal the number of mention groups:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return mentions.count }
The number of rows in a section equals the number of entries of the particular mention group:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return mentions[section].data.count }
Creating the cells, we just set the text for keywords, and set the URL for images:
private struct Storyboard { static let KeywordCellReuseIdentifier = "Keyword Cell" static let ImageCellReuseIdentifier = "Image Cell" } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let mention = mentions[indexPath.section].data[indexPath.row] switch mention { case .Keyword(let keyword): let cell = tableView.dequeueReusableCellWithIdentifier( Storyboard.KeywordCellReuseIdentifier, forIndexPath: indexPath) as UITableViewCell cell.textLabel?.text = keyword return cell case .Image(let url, let ratio): let cell = tableView.dequeueReusableCellWithIdentifier( Storyboard.ImageCellReuseIdentifier, forIndexPath: indexPath) as MentionsTableViewCell cell.imageUrl = url return cell } }
Same with the height of the cells. We let them calculate automatically for keywords, and calculate it from the aspect ratio for images:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { let mention = mentions[indexPath.section].data[indexPath.row] switch mention { case .Image(_, let ratio): return tableView.bounds.size.width / CGFloat(ratio) default: return UITableViewAutomaticDimension } }
For the image-table-view cell, start the spinner before downloading the image asynchronously, and stop it when the image is set:
var imageUrl: NSURL? { didSet { updateUI() } } func updateUI() { tweetImage?.image = nil if let url = imageUrl { spinner?.startAnimating() dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) { let imageData = NSData(contentsOfURL: url) dispatch_async(dispatch_get_main_queue()) { if url == self.imageUrl { if imageData != nil { self.tweetImage?.image = UIImage(data: imageData!) } else { self.tweetImage?.image = nil } self.spinner?.stopAnimating() } } } } }
The complete code for task #2 is available on GitHub.
The way you use your data structure (hint #7) is brilliant!
I re-used that principle in my own apps, where my cellForRowAtIndexPath was cluttered by if / else if / else / endif checks. That’s much more legible!
In your didSet{} for tweet. Don’t you also need to check that media.count > 0, urls.count > 0, etc? One of the requirements was *not* to include a section if it was empty.
This might be a good time to throw in a new Swift-ism:
if let media = tweet?.media where media.count > 0 { .... }
Of course, you are right, good point!
However, I structured the solutions following the tasks. Not-showing empty sections is a requirement of task #4 … you will find it there.
Second, the code is from March, this particular Swift-ism was introduced with Swift 1.2. Rewriting the code for Swift 1.2. would also get rid of lots of pyramids of death 😉
Sorry, and thanks for the quick reply. I had not yet gotten to the post, and didn’t realize you were doing things item by item.
These posts are wonderful. I really enjoy solving the problems and then seeing how someone else solves them. You also bring up issues that I haven’t thought about.
You are welcome, I like to get feedback on how other people solved the problems. But because I am a nerd, I tend to defend my solutions 😉
Excellent! I am learning so much!