A Swift application is more than just an Objective-C app translated into Swift. We need to embrace the features and philosophy of the Swift language. In this talk from try! Swift, we begin with a standard Model-View-Controller Table View application translated into Swift, and apply ideas from Functional Programming, Object Oriented Programming, Design Patterns, and Protocol Oriented Programming to turn it into idiomatic Swift.
Starter Swift (0:00)
You usually start in Swift by taking existing Objective-C code and just translating it over. You then move to a place where you introduce cultures we’ve known about from the past: object-oriented programming, functional programming, and protocol-oriented programming, and blending it all in.
I’ll start with a Table View example you were raised with. If you open Xcode and you do the template (shame on you), you start with code where you have a controller, amodel, and a view. Initially, I translate this into Swift, so I create my view or the storyboard as we do. For the model, I’m going to use a deck of cards and deal out five cards, and I’ll call that my “hand”.
My hand has ranks and suits, so I’ll represent those somehow, and I have to manage the model and the view with a controller. I’m going to use a HandViewController
, which is a subclass of UITableViewController
. We’re going to start typically, the way we all do, meaning that the view controller knows a lot about the model. (That’s not a good thing).
Let’s look at the code.
Basic view controller (1:56)
Let’s start with the view controller. My controller class is this hand view controller which I call HandVC
, and it is a subclass of UITableViewController
.
// HandVC.swift
import UIKit
class HandVC: UITableViewController {
private let hand = Hand()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.leftBarButtonItem = self.editButtionItem()
}
@IBAction private func addNewCard(sender: UIBarButtionItem) {
if hand.numberOfCards < 5 {
hand.addNewCardAtIndex(0)
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Fade)
}
}
One of the the things I love so much about Swift is how we have a property. There’s no declaring it somewhere and initializing it somewhere else. We just say, “there’s my property,” and so the view controller knows about the model. Then when I push the plus button, this method is called, and my action is addNewCard(_:)
.
If we have fewer than five cards, then we’re allowed to add one more. We add another card, as always, by first adding to the model and then to the view. I tend to take this code and pull it out for the view, so my action is very simple.
@IBAction private func addNewCard(sender: UIBarButtionItem) {
if hand.numberOfCards < 5 {
hand.addNewCardAtIndex(0)
insertTopRow()
}
}
private func insertTopRow() {
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Fade)
}
I see that it’s model and then view. I pull out the view code, and you’ll see that this allows us to pull this method somewhere else. I’m keeping my methods very short.
// MARK: - Table View Data Source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return hand.numberOfCards
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cardCell", forIndexPath: indexPath)
let card = hand.cardAtPosition(indexPath.row)
cell.textLabel?.text = card.rank.description
cell.textLabel?.textColor = card.color
cell.detailTextLabel?.text = card.suit.description
return cell
}
These are our two table view data source methods. This first one figures out how many rows are there in the section by turning to the model and saying, “Hey model, how many cards do you have?”
The other one is used to fill in the table view. For each row in the table view, we fill each cell and we get the corresponding model object, then we set the rank and color. AWe display the suit. Now, if it’s time for us to delete a card, we implement this method, and just as when we added, we delete from the model. Then we delete from the view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
hand.deleteCardAtIndex(indexPath.row)
deleteRowAtIndexPath(indexPath)
}
}
private func deleteRowAtIndexPath(indexPath: NSIndexPath) {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
We delete from the view, and we put that deleting from the view code in a separate function,. Notice it’s private, because that’s nobody’s business.
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
hand.moveCard(fromIndexPath.row, toIndex: toIndexPath.row)
}
To move a row, we don’t have to do the GUI code. Apple does that for us. All we have to do is take our model code and say, “Here’s where I’m moving the cards from and to.” This is our view controller. Let’s see how the model is implemented.
Basic model (4:45)
import UIKit
class Hand {
private let deck = Deck()
private var cards = [Card]()
It’s a hand. The hand has a deck of cards that it deals from. The model is a class, and you might notice that when we come to Swift, we’re taught that classes are bad. We should do everything in structs, and we will change this to a struct in awhile, but we’re starting with an Objective-C mindset.
Our Hand
class has a deck of cards and it keeps your hand in an array that holds items of type Card
.
In order to back up those methods that fill the table view, I need to know how many cards there are, and what card is at this position.
var numberOfCards: Int {
return cards.count
}
func cardAtPosition(index: Int) -> Card {
return cards[index]
}
I do the first one with a computed property, and I do the second one with a method that returns the card so I know which card is at a given position. We can support our table view data source. We need the methods that support adding, deleting, and moving cards.
func addNewCardAtIndex(index: Int) {
insertCard(deck.nextCard(), atIndex: index)
}
private func inserCard(card: Card, atIndex index: Int) {
cards.insert(card, atIndex: index)
}
func deleteCardAtIndex(index: Int) {
cards.removeAtIndex(index)
}
func moveCard(fromIndex: Int, toIndex: Int) {
let cardToMove = cards[fromIndex]
deleteCardAtIndex(fromIndex)
insertCard(cardToMove, atIndex: toIndex)
}
You could stop here and be happy, and I would forgive you. However, Swift is moving, and we need to move with it.
Think about it this way: when Swift was introduced, we didn’t know the right way to do things. The community is learning together and trying these things.
Turning Class to Struct (6:40)
First, I want to change the model view class from a class to a struct. To be more Swift-like, it’s going to be a value type instead of a reference type. We can keep better track of what changes in an app by saying reference types. More than one thing can be pointing at it, so more than one thing can be changing it. With a value type, that can’t happen, so I’m going to change my hand from a class to a struct.
Unfortunately, if you have a struct, all those methods that change the value of properties have to mutate. The hand
variable that we set in the view controller must now be a var, because it’s changing.
That’s different with structs and classes. If Hand
were still a class and we changed its properties, hand
could still be a let
, and its properties could change out from under it. However, with value semantics, hand
itself has to be a var as well.
Next, I’m going to eliminate mutating in an effort to make this more functional. It’s the “fun” in functional that we’re after. If we’re no longer changing the value of hand
, then these methods have to return a new instance of Hand
, so addNewCard(_:)
now returns a new instance of Hand
.
func addNewCardAtIndex(index: Int) -> Hand {
return insertCard(deck.nextCard(), atIndex: index)
}
insertCard
, our private method, must no longer be mutating either, but returning a new instance of hand
.
private func insertCard(card: Card, atIndex index: Int) -> Hand {
var mutableCards = cards
mutableCards.insert(card, atIndex: index)
return Hand(deck: deck, cards: mutableCards)
}
This is typical when getting rid of mutating. We’re going to create a local mutable copy of our immutable cards or immutable array by inserting the card we’re adding to the array, and then creating a new instance of Hand
that uses the same deck we’ve been using, but uses this new mutable array that we’ve added the card to.
Editing the view controller (9:37)
If we’re adding changing hand
, then we have to go back to the view controller which has to store this new value. I have to assign hand
to be this new instance of Hand
. Instead of it being a class or a mutating object that changes, I have to assign this new instance of Hand
back to my hand
variable.
When I add a new card, I’ll do the same exact thing. Let’s go back to our model and see what delete looks like.
func deleteCardAtIndex(index: Int) -> Hand {
var mutableCards = cards
mutableCards.removeAtIndex(index)
return Hand(deck: deck, cards: mutableCards)
}
delete
does the same thing that add
did. We create and change a mutable local copy, then we create a new instance of Hand
with the same deck, but the cards that have that element deleted, that’s what we return. add
and delete
look very parallel.
move
, however, is different. Let’s start with the non-functional version.
mutating func moveCard(fromIndex: Int, toIndex: Int) {
let cardToMove = cards[fromIndex]
deleteCardAtIndex(fromIndex)
insertCard(cardToMove, atIndex: toIndex)
}
This looks a lot like when you have to swap two things in an array in Objective-C. You have to store one of them somewhere else, do the swap, and then take the one you set aside. We take the card we’re moving and store it somewhere temporarily. Then we delete that card, then insert the card we saved back into the new position.
There’s nothing wrong with the non-functional way, but here’s the functional approach:
func moveCard(fromIndex: Int, toIndex: Int) -> Hand {
return deleteCardAtIndex(fromIndex).insertCard(cards[fromIndex], atIndex: toIndex)
}
Here, I have this moveCard
that’s going to return a new instance of Hand
. It is still going to use deleteCardAtIndex
and insertCard
, but notice it chained them together with .
– that dot means we’re going to delete the card, give us a new instance of Hand
, and then ask that instance to insert a card. We’re not saving anything anywhere.
Why not? deleteCardAtIndex
doesn’t change our current set of cards; it creates a new hand. Our current set of cards still exists, so I create this new hand, and then I ask it, “Hey, can you insert a card?” It says, “What card?” Fortunately, we still have our set of cards, and we say, “This card.” If that doesn’t make you smile, you can go back to Ruby or Java.
Let’s push this example in another direction by changing the way our UI looks by making a custom cell.
Creating a Custom Cell (12:52)
This will do the same thing, but it allows us to think about creating the custom cell that backs that up. Our custom cell is going to be called CardCell
. CardCell
is a subclass of UITableViewCell
. You’re often reaching for classes when you have to work in the UIKit
hierarchy, because you want to get behavior that you inherit for free. Our UITableViewCell
feels like it should be view code, but I find when I write code for these things, it’s really controller code. I’m doing the view stuff in the storyboard, and even though this is inside the custom card cell class the code that I write here is more controller code.
I need to be able to talk to my two labels. There is the one that holds the suit and the one that holds the rank. We’re using a modified MVVM.
At first, it looks like things have gotten worse, because our hand view controller has to talk to the table view, and it has to talk to the custom cell. However, it’s darkest before the dawn.
Here inside of my custom cell is a fillWith(_:)
method.
func fillWith(card: Card) {
rankLabel.textColor = card.color
rankLabel.text = card.rank.description
suitLabel.text = card.suit.description
}
Remember, the controller’s job is to talk to the model and to the view. The controller is going to say to this object, “here’s the card you’re being filled with. I want you to take this card and draw the rank and the suit in the right colors.” This card cell knows how to fill the cells.
My view controller has to make sure that it’s talking to the right type of cell object. Inside of a guard let
, which we all love, all I’m doing when I dequeue my reusable cell is making sure that it is a cell of the right type, and that it can be filled with these things.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier("cardCell", forIndexPath: indexPath) as? CardCell else {
fatalError("Could not create CardCell")
}
cell.fillWith(hand.cardAtPosition(indexPath.row))
return cell
}
This is a small thing, but that fillWith
argument looks long and gunky, and I have to read through it and see what cardAtPosition
means. If you go back over to your model and implement a subscript, you can simplify the look of that code a little bit:
subscript(index: Int) -> Card {
return cards[index]
}
I’m filling with this element of the hand, and I can see that this maps over really nicely. If you’re not using subscript, this is a nice place to reach for.
cell.fillWith(hand.cardAtPosition(indexPath.row))
// turn into:
cell.fillWith(hand[indexPath.row])
Extracting the Data Source (16:39)
To push this in another direction, I’m going to take the data source out of the table view controller.
UITableViewController
automatically comes with a template where it implements the UITableViewDataSource
and UITableViewDelegate
, but you don’t have to leave it there.
We’re going to take the data source out of the table view controller. We’ll isolate what things know about the model, so that at some point we have a design where if you don’t want to use this table view to show deck of cards, you can use it to show anything else.
My data source is a subclass of NSObject
that conforms to the protocol UITableViewDataSource
. Unfortunately, it must be a subclass of NSObject
to do what we need to do; it can’t be a base class or a struct. It has the handle to the model object. That’s how we’ve pushed our knowledge of the model down, and all of these methods we push down as well.
These are the methods that used to be in the table view, and now they’ve been table view controller, and now we push it down here to our data source, and we’re feeling pretty good. All this stuff is down here.
class DataSource: NSObject, UITableViewDataSource {
private var hand = Hand()
func addItemTo(tableView: UITableView) {}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {}
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {}
func insertTopRowIn(tableView: UITableView) {}
func deleteRowAtIndexPath(indexPath: NSIndexPath, from tableView: UITableView) {}
}
It runs perfectly just as it used to. If we look at our view controller, there’s not a lot left there.
class HandVC: UITableViewController {
private varDataSource = DataSource()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataSource
self.navigationItem.leftBarButtonItem = self.editButtonItem()
}
// MARK: - Action
@IBAction private func addNewCard(sender: UIBarButtonItem) {
dataSource.addItemTo(tableView)
}
}
There’s a handle to the data source, because it needs to pass things on. We set our table view’s data source in our viewDidLoad()
, and the only other thing we do is when someone hits the plus button, we pass that message on to our data source. We also have to remind the data source who its table view is, because we are connected to the table view at this level, but the data source isn’t. We have to pass that in as a parameter.
The nice thing is that if we look back at our data source, every one of these methods accepts a table view. It just needs to know what table view it’s talking to.
The bottom two methods are where we pulled out our visual code. The bottom two methods are a little different, so I want to introduce a protocol and I want to pull them out of here.
func insertTopRowIn(tableView: UITableView) {}
func deleteRowAtIndexPath(indexPath: NSIndexPath, from tableView: UITableView) {}
You may think, “You just pushed them down here. Now you’re pulling them up.” Yes, but we’re pulling them up somewhere different. Let’s introduce a protocol.
Introducing Protocols (19:20)
I’m going to call it SourceType
. I’ll pull out the GUI code. My protocol source type conforms to my UITableViewDataSource
, and it holds my visual methods. I declare them here, and it’s a protocol extension. T(hank goodness for Swift 2 where we got to actually implement these methods.) Inside my extension for this protocol, I can implement these methods.
protocol SourceType: UITableViewDataSource {
func insertTopRowIn(tableView: UITableView)
func deleteRowAtIndexPath(indexPath: NSIndexPath, from tableView: UITableView)
}
extension SourceType {
func insertTopRowIn(tableView: UITableView) {
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Fade)
}
func deleteRowAtIndexPath(indexPath: NSIndexPath, from tableView: UITableView) {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
}
Now I have my data source. It is a subclass of NSObject
, it conforms to UITableViewDataSource
, and it conforms to SourceType
. Since it conforms to the protocol, I’m now getting the behavior I had before.
class DataSource: NSObject, UITableViewDataSource, SourceType {}
This word “card” is everywhere. It seems to spread this news of my model object too far. I want to look inside of Hand.swift
, and everywhere I use the word “card”, I want to replace it with the word “item”. Now that I’ve taken the model out of here, I can capture this in a protocol.
In a protocol called DataType
, I’m going to capture that and pull up all those public methods. This is wonderful, because if you look at data type, it captures all of the things that my model has to do.
My model has to be able to tell the table view data source how many items it has. It has to be able to add items. It has to be able to delete items, and it has to be able to move items. This protocol does what protocols are supposed to do, and it says, “Here’s what this sort of thing, here’s what this type of thing has to be able to support.”
protocol DataType {
var numberOfItems: Int {get}
func addNewItemAtIndex(index: Int) -> Self
func deleteItemAtIndex(index: Int) -> Self
func moveItem(fromIndex: Int, toIndex: Int) -> Self
}
Once I have my data types, I have to use a little trick that we have in protocols, which is these things used to return Hand
, they now have to return Self
. So, if Hand
conforms to the protocol, they will return Hand
. Whatever is conforming to the data type will return one of themselves back, so we capture that with Self
.
struct Hand: DataType {}
Because I’ve conformed to this, I can use data type. Instead of source type or data type knowing about Hand
, they can all talk to a data type instead. I can eliminate Hand
throughout my codebase.
protocol SourceType: UITableViewDataSource {
var dataObject: DataType {get set}
In my SourceType.swift
I have a dataObject
, which is a DataType
. In the case of a Hand
, that’s just going to be a Hand
. I declare that it has a getter and a setter, so in my DataSource.swift
, my dataObject
is going to be an element of type Hand
.
class DataSource: NSObject, UITableViewDataSource, SourceType {
var dataObject: DataType = Hand()
func addItemTo(tableView: UITableView) {
if dataObject.numberOfItems < 5 {
dataObject = dataObject.addNewItemAtIndex(0)
insertTopRowIn(tableView)
}
}
}
Still doing okay? You can always take a break.
Instead of using the word “card”, we use the word “item.” I add a new item at index 0. Again, I know that my type of cell is the right type, because we tested for that earlier. We also add to our guard let
a second check to make sure that the data object I get back is actually an element of type hand, because before I tried to fill that type of cell with this type of model, I have to make sure that it’s the right type of cell and the right type of model. This guard let
checks both, and if they both pass, I go ahead and fill the data.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier("cardCell", forIndexPath: indexPath) as? CardCell else {
fatalError("Could not create CardCell")
}
hand = dataObject as? Hand else {
fatalError("Could not create Card Cell or Hand instance")
}
cell.fillWith(hand.cardAtPosition(indexPath.row))
return cell
}
I’m having so much fun that I look to see what else I can pull out of the data source.
if dataObject.numberOfItems < 5 {}
I remember from design patterns that we have the template pattern. The template pattern allows us to do something and then go down to the specific case and come back up to finish it.
protocol SourceType: UITableViewDataSource {
var dataObject: DataType {get set}
var conditionForAdding: Bool {get}
Here in my SourceType
, I can add a computed property called conditionForAdding
, which could be specifically implemented at are the numberOfItems < 5
, but that allows me to go back to my SourceType
, and I can use this condition for adding inside add item to in my extension, because it’s declared in the protocol.
var conditionForAdding: Bool {
return dataObject.numberOfItems < 5
}
extension SourceType {
func addItemTo(tableView: UITableView) {
if conditionForAdding {
dataObject = dataObject.addNewItemAtIndex(0)
insertTopRowIn(tableView)
}
}
}
Every element must have this condition for adding. I’d love to move everything else up into the protocol too, but I can’t, and at this point you might get angry because you’ve forgotten, we’ve done so much Swift we’ve forgotten that we can still introduce subclasses. We’re allowed to do that. I know they’re a little bad, but we can do that when appropriate.
If we have this now, and we introduce a subclass of DataSource
, and we push all of our code down that needs to be pushed down, my HandDataSource
extends DataSource
.
class HandDataSource: DataSource {}
In my hand view controller, I need to be able to create an instance of my subclass:
class HandVC: UITableViewController {
private var dataSource = HandDataSource()
Now I push all my hand references down into the subclass, and I have to do that by overriding my conditions for adding, so I’m going to have to stub that out the super class, and I override my table view, so I’ll have to stub that out in super class:
class HandDataSource: DataSource {
init() {
super.init(dataObject: Hand())
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier("cardCell", forIndexPath: indexPath) as? CardCell, hand = dataObject as? Hand else {
return UITableViewCell()
}
cell.fillWith(hand[indexPath.row])
return cell
}
override var conditionForAdding: Bool {
return dataObject.numberOfItems < 5
}
}
What does that look like in the super class?
var conditionForAdding: Bool {
return false
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
fatalError("This method must be overriden")
}
If you ask me can I add something? I’m going to stub it out as “no you can’t”. If you don’t override the table view, I’m going to throw a fatal error. You have to override these two methods to get anything done.
How do I create an instance? I’ve got my dataObject
which is a DataType
, so I have to initialize it. We’ve come this far without generics and I feel pretty good, but I have to introduce generics too, because what we’re going to initialize has to be of type DataType
. I have to do it this way where I specify that your dataObject
can be any class as long as it conforms to the DataType
and then my actual image here called super init, and passes in a hand object, and hand object conforms to DataType
.
class DataSource: NSObject, UITableViewDataSource, SourceType {
var dataObject: DataType
init<A: DataType>(dataObject: A) {
self.dataObject = dataObject
}
}
class HandDataSource: DataSource {
init() {
super.init(dataObject: Hand())
}
}
Recap (27:18)
We started with the table view, and we ended up with code that might seem more complicated, but really we’ve simplified it in many ways.
We’ve factored out the code that doesn’t change, that is reusable in any table view that you handle. You don’t have to do it this way, but I’m showing you how we push things around, and what’s left is the code that does change. These are the only things that you have to change for different sorts of table views. We did this by taking advantage of these three worlds: object-oriented programming, functional programming, and protocol-oriented programming.
Don’t limit yourself by saying, “I only do objects. I only do functional. I only do protocols.” You’ve got these wonderful tools, and you live at the union of them. Use them!
Receive news and updates from Realm straight to your inbox