Last time at SLUG, we were excited to welcome NatashaTheRobot at our latest meetup, graciously hosted by Eventbrite. TableViews are the foundation of many iOS applications, and Swift provides several unexplored patterns for approaching TableViews. In this talk, Natasha goes over the basics of TableViews and how to apply “Swift-thinking” when approaching them in iOS8. Natasha is an iOS engineer who’s currently blogging about her experience learning Swift, and has started sending out a weekly Swift newsletter!
As usual, video & slides are synchronized. The video is also subtitled. You can find a blog version of the talk (with code samples) below, as well as source code here.
TableView Basics (0:00)
Natasha’s talk was a walkthrough for a simple Seinfeld quotes app built using table views. Every cell in view has two labels: one for the quote content, and another for the scene in the show. When you create a new file for a project, use the Cocoa touch option. If you use the Swift file, the boiler plate code won’t be populated. In addition, make sure you have your XCode project set to Swift, with the right target selected.
Data Source (2:06)
The data source is a quote object, which has the two string objects for a content and a scene. In this particular app, the quote object is not going to be optional because it should have content and a scene. These two properties are declared with the LET keyword, indicating they can’t be mutated in the future.
Self Sizing Cells (4:23)
Now is the time to use auto layout. Sizes for your table view or your view controllers are abstracted in XCode 6, and so the nice thing about auto layout is that it makes it really easy to create self sizing table view cells. In iOS7 and below, the method for calculating height is especially annoying when the labels can be different sizes based on the content. By specifying the real height as UITableViewAutomaticDimension, the height will be calculated for you and the estimated row height is good for optimization.
import UIKit
class QuotesTableViewController: UITableViewController {
/*...*/
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 89
tableView.rowHeight = UITableViewAutomaticDimension
}
/*...*/
}
Privacy & Dynamic Type (12:19)
Swift no longer has separate .h and .m files, as there were in Objective-C. Therefore, now you have to ensure that everything is private that should be private. In Objective-C, the labels/IBOutletes were always in the .m file and were thus by default private. In Swift, by default they are not private, so make sure to add in the PRIVATE keyword.
import UIKit
class TwoLabelTableViewCell: UITableViewCell {
@IBOutlet weak private var headerLabel: UILabel!
@IBOutlet weak private var subheadLabel: UILabel!
func configure(#headerText: String, subheadText: String) {
headerLabel.text = headerText
headerLabel.accessibilityLabel = headerText
subheadLabel.text = "- \(subheadText)"
subheadLabel.accessibilityLabel = subheadText
}
}
In Swift, you can identify everything that you’ll be using at the top of the file; otherwise, everything should be initialized in the init method if not an optional. At dequeues, by default you need to typecast the cell to label TableViewCell, the custom cell. If you want any cell configuration, you would have to typecast by using the AS keyword. The best thing about auto layout & self sizing cells is wheny ou rotate the table view, the sizes are automatically calculated for you. WIth different device sizes, it’s important to have a dynamic app that can adapt to any size. Dynamic typing is a feature for users who have a hard time seeing or just want bigger text. Annoying to configure in iOS7, dynamic typing is now very easy to use - just set the text style from the storyboard and it will work in your app. This only works in iOS8; in iOS7, there’s a lot more involved with calculating the different heights and listening.
class TwoLabelTableViewCell: UITableViewCell {
@IBOutlet weak private var headerLabel: UILabel!
@IBOutlet weak private var subheadLabel: UILabel!
func configure(#headerText: String, subheadText: String) {
headerLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
subheadLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
/*...*/
}
/*...*/
}
Hide Nav Bar on Scroll (13:19)
Another neat feature in iOS8 is the ability to hie the navigation and tab bar when swiping. You can set this when the user scrolls very easily with just one property, hidesBarsOnSwipe, and thus make sure that the entire screen shows just your content text.
class QuotesTableViewController: UITableViewController {
// ** //
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.hidesBarsOnSwipe = true
// ** //
}
// ** //
}
Default Parameter Values & Optionals (14:06)
One of my favourite Swift features is default parameter values, which are very useful for table views. In previous versions of XCode, you would have to add additional functions to change colours for labels that were added later. You’d have to go back to the other places in your code where you use that and refactor. With default parameter values, the other places in code won’t be broken or need to be fixed. This is great for backwards compatibility when you’re adding features to your table view.
func configure(#headerText: String, subheadText: String,
textColor: UIColor = UIColor.blackColor()) {
headerLabel.textColor = textColor
subheadLabel.textColor = textColor
/* ... */
}
MVVM (16:08)
Optionals are a new feature in Swift that can be difficult when you need to unwrap them. MVVM, a Model View ViewModel, works really well with optionals. With the following create quote screen, the quote content and scene discription are required but can be null while the user is creating a quote.
class Quote {
let content: String
let scene: String
/* ... */
init(content: String, scene: String) {
self.content = content
self.scene = scene
}
}
This ViewModel allows the content and scene to be optional or null while the user creates the quote, but maintains quote integrity otherwise. In the view model, quotes are variables that change as the user types. Again, the view controller created from this view model is private to prevent anyone outside of the view controller to access the view model.
class QuoteViewModel {
var quoteContent: String?
var quoteScene: String?
let quoteContentPlaceholder = "Quote Content"
let quoteScenePlaceholder = "Scene Description"
init(quoteContent: String? = nil, quoteScene: String? = nil) {
self.quoteContent = quoteContent
self.quoteContent = quoteScene
}
}
class CreateQuoteTableViewController: UITableViewController {
private let quoteViewModel = QuoteViewModel()
/* ... */
}
Alternate Data Source Structures (19:43)
Enums (21:31)
Another neat structure in Swift is enumerations. They’re not from Objective-C; in them, you can add functions. From writing this, I’ve learned that enums do not have access to your instance variables from your class, so anything that you do want to change from your class must be passed in. Once you have enums, you can also unwrap optionals easily through an IF LET statement. Instead of having multiple switch statements, you have code that looks much cleaner.
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell =
tableView.dequeueReusableCellWithIdentifier(textCellIdentifier) as TextInputTableViewCell
if let quoteField = QuoteField.fromRaw(indexPath.row) {
let text = quoteField.text(quoteViewModel: quoteViewModel)
let placeholder = quoteField.placeholder(quoteViewModel: quoteViewModel)
/* ... */
}
return cell
}
Closures (23:26)
Functions and closures are kind of like first class citizens in Swift. You can actually assign a function to a variable and then execute it later. If you’re not used to functional programming or a few of its concepts, I recommend reading this book, written by our [previous speaker](http://realm.io/news/functional-programming-swift-chris-eidhof/, Chris Eidhof. In my app, I try to use closures and functions instead of protocols. One thing that can be done is that each text field is listening for content changes, and this can be handled in the view controller by setting it as a delegate. The view controller can pass in a handler, and when the text field content changes, it can automatically execute that handler. One thing to note: when you have delegates, simply separate it with a comma and add it in.
import UIKit
class TextInputTableViewCell: UITableViewCell, UITextFieldDelegate {
@IBOutlet weak private var textField: UITextField!
typealias textFieldChangedHandlerType = (String) -> Void
private var textFieldChangedHander: textFieldChangedHandlerType?
override func awakeFromNib() {
textField.delegate = self
}
func configure(#text: String?, placeholder: String,
textFieldChangedHandler: textFieldChangedHandlerType?) {
textField.placeholder = placeholder
textField.text = text
textField.accessibilityLabel = placeholder
textField.accessibilityValue = text
self.textFieldChangedHander = textFieldChangedHandler
}
// MARK: Text Field Delegate
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {
if let textFieldChangedHandler = textFieldChangedHander {
textFieldChangedHandler(textField.text)
}
return true
}
}
For those of you from Objective-C, you want to pass in a weak self that could be executed anytime. In Swift, you can use weak self or unowned self. The syntax is within the closure, rather than having an extra line where weak self equals self. If you’re using weak self, you have to unwrap because self may or may not be there. If we say, if let strong self equals self, we can use the enum to update the content of the cell and use the view model to update it.
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell =
tableView.dequeueReusableCellWithIdentifier(textCellIdentifier) as TextInputTableViewCell
if let quoteField = QuoteField.fromRaw(indexPath.row) {
let text = quoteField.text(quoteViewModel: quoteViewModel)
let placeholder = quoteField.placeholder(quoteViewModel: quoteViewModel)
cell.configure(text: text, placeholder: placeholder,
textFieldChangedHandler: { [weak self] (newText) in
if let strongSelf = self {
quoteField.updateQuoteWithText(newText, quoteViewModel: strongSelf.quoteViewModel)
}
})
}
return cell
}
Q&A (30:17)
Q: What’s the best JSON handling that you’ve found?
Natasha: There are a few out there, but I’m personally not happy with most of them. I think the one I’ve used was Swift to JSON, on GitHub. Because Swift is new, it’s going to evolve and maybe Apple will release something to help us since it is a big issue. Chris Eidhof’s book it great for functional programming and you can see how he does it. Swift is also new, so there’s plenty of opportunities to create open source libraries yourself.
Q: What uses for closures did you find in Swift with iOS programming?
Natasha: Similar to examples I showed, closures are similar to Objective-C blocks where you have completion handling. I still have a lot more to explore myself, but I want to experiment with passing in a function name and have it be executed later.
Q: Did you find that it was easier to handle events by passing the function into the closure so you can attach a function to an event?
Natasha: I do like that you can attach it right there when you configure the function, versus if you’re using target, action and you’re doing it later with the code for how you’re handling it somehwere else in your code.
Q: When building table views in Objective-C, I find myself using a lot of multiple threads. I noticed you didn’t, would you deal with that through blocking?
Natasha: Swift works really nicely with Objective-C, so you can use the same libraries as with Objective-C or CocoaPod. It works seamlessly if you just import and start using it. I think right now, pure Swift doesn’t support multithreading so you do have to use GCD.
Receive news and updates from Realm straight to your inbox