tl;dr
I’m going to make a simple tableview with a custom cell type, use the new UIVisualEffectView from iOS 8 and then show off a bit of the power of Swift. It should take you about ~30 minutes to go through all the steps. Enjoy!
if you are looking for a simpler tutorial for tableviews check out this one
Create a new project
Open Xcode 6, create a new “Single Page Application” and select Swift as the programming language.
Add a table view property
Open the ViewController.swift
class and add a new tableview instance variable below the class declaration. Add the @IBOutlet
Interface Builder declaration attribute to expose the tableView
property.
“Interface Builder attributes are declaration attributes used by Interface Builder to synchronize with Xcode. Swift provides the following Interface Builder attributes: IBAction, IBDesignable, IBInspectable, and IBOutlet. These attributes are conceptually the same as their Objective-C counterparts.”
Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/ro/jEUH0.l
class ViewController: UIViewController {
@IBOutlet var tableView: UITableView!
...
}
Conform to the UITableViewDelegate and UITableViewDataSource protocols
To conform to the UITableViewDelegate
and UITableViewDataSource
protocol, just add them separated by commas afterUIViewController
in the class declaration. (more about protocols in Apple’s Docs)
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
}
Add a table view in your view controller interface
Open Main.storyboard
and drag a UITableView from the library (in the lower right corner) into the ViewController view.
Connect the Interface Builder outlets
Connect the dataSource
, delegate
and tableView
outlets in interface builder. Just right click on the table view and then connect them.
Create the custom cell class
Create a new class above your ViewController
code. Your custom cell class should inherit from UITableViewCell
! Add outlets for the backgroundImage
and titleLabel
.
class CustomTableViewCell : UITableViewCell {
@IBOutlet var backgroundImage: UIImageView
@IBOutlet var titleLabel: UILabel
}
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
}
Create the custom cell interface
Right click on your applications directory and select new file.
Select User Interface
and then the Empty
template. Select iPhone
and name it CustomTableViewCell
.
Open CustomTableViewCell.xib
and add a UITableViewCell
in it from the component library. Select the Table View Cell
and change it’s class to CustomTableViewCell
After that the table view cell should change its name to Custom Table View Cell
, the backgroundImage
and titleLabel
outlets should be visible now.
Add an image view
and a label
in the cell.
Resize the cell to 320 x 320
using the size inspector. And set the row height to 320
Connect the cell outlets to the CustomTableCellViewCell
. Notice that custom view bind outlets to the view object and custom view controllers bind them to the File's Owner
Add the loadItem method
In a real life application you usualy have more than one type of cell in a table view (or collection view). By keeping the initialization logic in the cell we can avoid code duplication or spaghetti code.
This cell displays an image stored on the device and has a string title. All we need to do during the cell initialization is to set the image and the tile.
class CustomTableViewCell : UITableViewCell {
@IBOutlet var backgroundImage: UIImageView!
@IBOutlet var titleLabel: UILabel!
func loadItem(#title: String, image: String) {
backgroundImage.image = UIImage(named: image)
titleLabel.text = title
}
}
Obs: For you Objective-C folks in Swift you do not need to call properties using the self
keyword! Use the self
keyword only when you have a parameter named like your property, so that the compiler can understand your code.
Note: This function uses a shorthand external parameter name. If the method declaration was func loadItem(title: String, image: String)
(without #
symbol) to call it we would have to write cell.loadItem("We❤Swift", image: "someimage.jpeg")
. Instead, with the #
symbol, to call loadItem
we would write cell.loadItem(title: "We❤Swift", image: "someimage.jpeg")
. I think the second method is clearer.
From Apple’s Docs:
Shorthand External Parameter Names
If you want to provide an external parameter name for a function parameter, and the local parameter name is already an appropriate name to use, you do not need to write the same name twice for that parameter. Instead, write the name once, and prefix the name with a hash symbol (#). This tells Swift to use that name as both the local parameter name and the external parameter name.
Add some data to display
Download the swifts and add them in your project. Unarchive the zip file and drag the files in you Xcode Navigator. Make sure to check Copy items if needed
.
For each custom cell we need a title and a image name, we are going to store them in an Array of Tuples.
From Apple’s Docs:
A tuple pattern is a comma-separated list of zero or more patterns, enclosed in parentheses. Tuple patterns match values of corresponding tuple types.
…
A pattern represents the structure of a single value or a composite value. For example, the structure of a tuple (1, 2) is a comma-separated list of two elements. Because patterns represent the structure of a value rather than any one particular value, you can match them with a variety of values. For instance, the pattern (x, y) matches the tuple (1, 2) and any other two-element tuple. In addition matching a pattern with a value, you can extract part or all of a composite value and bind each part to a constant or variable name.
In Swift, patterns occur in variable and constant declarations (on their left-hand side), in for-in statements, and in switch statements (in their case labels). Although any pattern can occur in the case labels of a switch statement, in the other contexts, only wildcard patterns, identifier patterns, and patterns containing those two patterns can occur. …
The first value will represent the title
and the second one the imageName
.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
var items: [(String, String)] = [
("❤", "swift 1.jpeg"),
("We", "swift 2.jpeg"),
("❤", "swift 3.jpeg"),
("Swift", "swift 4.jpeg"),
("❤", "swift 5.jpeg")
]
}
Set the number of rows
Implement tableView(_:numberOfRowsInSection:)
and return the number of items
.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count;
}
}
Note: In iOS the number of sections is 1
by default, so we do not need to set a value for that. In case you want to create a table with multiple sections just implement the numberOfSectionsInTableView(_:)
method.
Register the Nib
Load the CustomTableViewCell
interface file into a UINib
object and then tell the table view to use it for the customCell
reuse identifier.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
override func viewDidLoad() {
...
var nib = UINib(nibName: "CustomTableViewCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "customCell")
}
...
}
Create the cell
Now when you will ask for a cell from the table view with the reuse identifier customCell
, the tableView will look for any unused cells with that reuse identifier or just create one using the CustomTableViewCell
nib.
The tableView will call the tableView(_:cellForRowAtIndexPath:)
method on the dataSource
whenever it need a specific cell. The location of the cell is stored in an NSIndexPath
that has a row
and section
property.
To create a cell all we need to do is ask for one using the dequeueReusableCellWithIdentifier(_:)
method. After we have a cell we need to load the title and image and then return it.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:CustomTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("customCell") as CustomTableViewCell
// this is how you extract values from a tuple
var (title, image) = items[indexPath.row]
cell.loadItem(title: title, image: image)
return cell
}
}
Handle Table Selection
When a cell is selected the table view will call the tableView(_:didSelectRowAtIndexPath:)
method on the delegate
. To handle table view selection all you need to do is implement that method.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
println("You selected cell #\(indexPath.row)!")
}
}
If you run the project it should look like this:
Add a blur view
In iOS 8 we now have a easy way of recreating the blur effect used throughout the system. UIVisualEffectView
is a subclass ofUIView
that provides a simple abstraction over complex visual effects. UIKit
has two implemented effects UIBlurEffect
andUIVibrancyEffect
.
Let’s create a UIVisualEffectView
and add it to the main view.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
override func viewDidLoad() {
...
addEffect()
...
}
func addEffect() {
var effect = UIBlurEffect(style: UIBlurEffectStyle.Light)
var effectView = UIVisualEffectView(effect: effect)
effectView.frame = CGRectMake(0, 0, 320, 100)
view.addSubview(effectView)
}
...
}
There are 3 type of blur effects. If we send the effect
and offset
as a parameters to the addEffect
method we can reuse the code and see all three blur effects at once.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
override func viewDidLoad() {
super.viewDidLoad()
addEffect(UIBlurEffect(style: UIBlurEffectStyle.Light), offset: 0)
addEffect(UIBlurEffect(style: UIBlurEffectStyle.Dark), offset: 50)
addEffect(UIBlurEffect(style: UIBlurEffectStyle.ExtraLight), offset: 100)
var nib = UINib(nibName: "CustomTableViewCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "customCell")
}
func addEffect(effect: UIVisualEffect, offset: CGFloat) {
var effectView = UIVisualEffectView(effect: effect)
effectView.frame = CGRectMake(0, offset, 320, 50)
view.addSubview(effectView)
}
...
}
Extend the Array class
In ruby the Array
class two nifty methods each
and eachWithIndex
. The each method takes a function as a parameter and calls it with each element of the array in order, eachWithIndex
takes a function as a parameter and calls it with the tuple (element, index)
for each element.
We can extend a class using the extension
keyword. The implementation of each
and eachWithIndex
in Swift would look like this:
extension Array {
func each(callback: T -> ()) {
for item in self {
callback(item)
}
}
func eachWithIndex(callback: (T, Int) -> ()) {
var index = 0
for item in self {
callback(item, index)
index += 1
}
}
}
Putting it all together
Now we have 3 method calls that look pretty similar. Two things change: the style and offset.
override func viewDidLoad() {
...
addEffect(UIBlurEffect(style: UIBlurEffectStyle.Light), offset: 0)
addEffect(UIBlurEffect(style: UIBlurEffectStyle.Dark), offset: 50)
addEffect(UIBlurEffect(style: UIBlurEffectStyle.ExtraLight), offset: 100)
...
}
We can rewrite this code using eachWithIndex
:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
override func viewDidLoad() {
...
addEffects()
...
}
func addEffects() {
[
UIBlurEffect(style: UIBlurEffectStyle.Light),
UIBlurEffect(style: UIBlurEffectStyle.Dark),
UIBlurEffect(style: UIBlurEffectStyle.ExtraLight)
].eachWithIndex { (effect, index) in
var effectView = UIVisualEffectView(effect: effect)
effectView.frame = CGRectMake(0, CGFloat(50 * index), 320, 50)
self.view.addSubview(effectView)
}
}
...
}
We can do one thing here and use the map
function:
func addEffects() {
[
UIBlurEffectStyle.Light,
UIBlurEffectStyle.Dark,
UIBlurEffectStyle.ExtraLight
].map {
UIBlurEffect(style: $0)
}.eachWithIndex { (effect, index) in
var effectView = UIVisualEffectView(effect: effect)
effectView.frame = CGRectMake(0, CGFloat(50 * index), 320, 50)
self.view.addSubview(effectView)
}
}
This is called method chaining. You can read more about closures and higher order functions in Silviu’s post here.
You can get the code here.
Challenges:
- use more than one type of cell in the same table view
- implement more of the dataSource and delegate methods and see what you can do with them. You could start with
numberOfSectionsInTableView(_:)
,tableView(_:titleForHeaderInSection:)
,sectionIndexTitlesForTableView(_:)
,tableView(_:heightForRowAtIndexPath:)
- make a Contact Book App (hint: finish the second challenge first)
I’ve been using similar patterns in Objective-C but they were always a pain because of the syntax and limitations of blocks. I Hope you got a bit excited about functional programming and Swift.
If you found this useful, please take a moment and share it with your friends
great guide! thanks a lot!
Are you sure that your explanation of “shorthand external parameter name” is correct?
It’s incomplete. You could also use
func doubleAnInt(shortName verboseNameThatWillBeUsedInsideTheFunction: Int) {
return verboseNameThatWillBeUsedInsideTheFunction * 2
}
doubleAnInt(shortName: 2) // 4
Hi, thanks for an awesome quick tutorial. I’m having trouble applying the blur effect, the result I’m getting is not similar to your. My blur view is not very transparent (so you almost can’t see the content behind). This is replicated in playground using this code:
`let imageView = UIImageView(image: UIImage(named: “/Users/Buusmann/bg.jpg”))
imageView.frame = CGRectMake(0, 0, 1024, 768)
var effectView = UIVisualEffectView(effect: UIBlurEffect(style: .Light))
effectView.frame = CGRectMake(0, 0, 1024, 768)
imageView.addSubview(effectView)
`
Have you got any suggestions as to why this would be?
Thanks in advance
Chris
I managed to replicate the behaviour you described. Just add the visual effect view in the view that contains imageView and it should work.
* view
|
*- Image View
*- Visual Effect
I tried this, and still no luck unfortunately. Can it have something to do with an alpha value?
Hello. good tutorial!
I am having a problem though. After attaching everything I am getting “fatal error: Can’t unwrap Optional.None” in my loadItem because the compiler thinks my background and label doesn’t exist. But they are hooked up in the nib file! Please help.
backgroundImage.image = UIImage(named: image)
titleLabel.text = title
I can’t tell without seeing your code. Maybe this helps http://stackoverflow.com/questions/24093904/custom-uitableviewcell-shows-fatal-error-cant-unwrap-optional-none-issue-in
Very efficiently written post. It will be valuable to anyone who usess it, as well as myself. Keep doing what you are doing i will definitely read more posts.
about that eachwithindex method… look into the enumerate function
It was just an example
I love it when individuals come together and share opinions.
Great website, keep it up!
when i run the project (done until registering the nib and handling selection) it throws an error at line
tableView.registerNib(nib, forCellReuseIdentifier: “customCell”)
says : EXC_BAD_INSTRUCTION.
any idea why ?
does tableView or nib have an optional type?
and in the console it says
can’t unwrap optional.none
any help is appreciated.
Thanks for your great article!
I found other blogs use registerClass to load custom table view. Something like this:
self.tableView.registerClass(CustomTableViewCell.self, forCellReuseIdentifier: “customCell”)
Would it be an alternative way to load custom Table View Class? What is the use case for this method?
the registerNib/registerClass method tells the tableView how to create a cell for a cell reuse identifier. You can use any method that solves your problem
Hi Andrei, great tutorial. When I’m running the project I’m getting a giant grey rectangle that is overlaying the image. I can only see the left most edge of the image so I can tell its there. I’ve downloaded your source code and compared all of the properties and everything seems to be the same. Any idea what it can be? (btw using beta 2 – don’t know if that makes a difference but thought I would throw it out there)
I’m getting the same thing. Using beta 3. With the 3d exploded view, i can see all images are there stacked on top of one another with several small UITableViewCellSeparatorView instances on the very top of the stack of images.
Figured it out! Add function:
func tableView(tableView:UITableView!, heightForRowAtIndexPath indexPath:NSIndexPath)->CGFloat
{
return 320
}
This also corrected my issue. Any chance we can have this added into the main article with an explanation?
Hi
This is a nice tutorial but this blur effect why dont work with only text? I see the text dissapear or dont add the blur effect
My understanding (based on watching WWDC videos) is that the blur effect that Apple designed intentionally handles text differently (ignores it) to prevent distraction.
I added another line of code for the purpose of my app, and getting a “property ‘self.nameLabel’ not initialized at super.init call” error message, however I followed the steps exactly how you have showed. I checked twice for any possible error that could’ve happened on my part. Is it just the swift beta 3 acting buggy?
nevermind!! Figured it out.
This worked! Couldn’t get custom cells to work any other way. Long live the xib!
I think I must’ve connected some of the Table View outlets wrong — I’m pretty new to Xcode, and that part wasn’t very specific — but on the line declaring the ViewController class, I get an error: “ViewController does not conform to protocol “UITableViewDataSource”
Anybody know what I’m doing wrong?
The tableView parameter is now an implicitly unwrapped optional now. UITableView -> UITableView!
Now I’m having a problem where cells don’t want to change size to match their contents — each table row is the default system size, and the images just seem to overlap them (and also appear to be simply displaying as each images’ actual size, instead of filling the cell area).
Constraint issue, maybe? Not sure how to fix this.
Current Environment: Xcode 6.1, iOS 8.1
Your code is now obsolete. It doesn’t compile …violates protocol, etc.
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:12:5: ‘IBOutlet’ property has non-optional type ‘UIImageView’
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:13:5: ‘IBOutlet’ property has non-optional type ‘UILabel’
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:39:5: ‘IBOutlet’ property has non-optional type ‘UITableView’
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:11:7: Class ‘CustomTableViewCell’ has no initializers
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:12:1: ‘required’ initializer ‘init(coder:)’ must be provided by subclass of ‘UITableViewCell’
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:37:7: Class ‘ViewController’ has no initializers
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:38:1: ‘required’ initializer ‘init(coder:)’ must be provided by subclass of ‘UIViewController’
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:37:1: Type ‘ViewController’ does not conform to protocol ‘UITableViewDataSource’
/tmp/UIVisualEffectView/UIVisualEffectView/ViewController.swift:59:31: Value of optional type ‘UINib?’ not unwrapped; did you mean to use ‘!’ or ‘?’?
I updated the code and the article. It should work now. There where some changes in the delegate method declarations.
Bravo Andrei, a really nicely structured tutorial with good explanations and extension of concepts. It must be a nightmare to offer this sort of thing right now, as Swift keeps changing. Good on you for sharing.
Great Tutorial, with a bit of persistence I was able to figure out your tutorial (First time coding iOS apps ever). Just a question: how come the height of the tableView row height is 320 and the custom cell height is also 320 is there anyway to make it flexible aka say for different image heights
use the tableView:heightForRowAtIndexPath: method for that
Thank you for great explanation. You tutorial saved a lot of hours for me.
Thanks Andrei great article, I am new to Swift and I am having some problems trying to access the elements inside the custom cell, this is my code for didDeselectRowAtIndexPath.
I want to pass the value of the current cell to another controller but xCode keeps complaining about it can’t find the label inside my custom cell.
Do I have to make IBOutlets publics inside our custom cell class in some way?
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
categoriesTableView.deselectRowAtIndexPath(indexPath, animated: true)
println(indexPath.row)
let currentCell = categoriesTableView.cellForRowAtIndexPath(indexPath)! as UITableViewCell
let currentCategory = currentCell.categoryLabel.text <— got error here!
performSegueWithIdentifier("toSubCategories", sender: self)
//navigationController?.pushViewController(ProductListViewController(), animated: true)
}
Thanks for some tips!
Try let currentCategory = currentCell.categoryLabel?.text
Thanks but it didn’t work for me, at the end I solve it reading the model using the indexPath and passing the data to the second controller.
Great Tutorial!
This is exactly what i was looking for, to fully custom my Cell construction.
Great website as well, keep it this way Andrei.
THX
Following this tutorial to get custom tableview cell, getting following error on line –
var nib = UINib(nibName: “CustomTableViewCell”, bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: “customCell”)<——-
fatal error: unexpectedly found nil while unwrapping an Optional value
did you connect the IBOutlet?
Hey and thank you very much for the tuto!
I’m getting an issue while trying to register the nib.
Xcode keeps telling me ‘(UITableView, numberOfRowsInSection: Int) -> Int’ does not have a member named ‘registerNib’
I searched a lot on google but nobody seems to have this issue…
Thank’s again.
Best,
Anatole
Code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var nib = UINib(nibName: “CommentTableViewCell”, bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: “commentCell”)
}
Hey again and thank’s for reading me!
I resolved this issue but now I have many problems:
EXC_BAD_INSTRUCTION one the registerNib line when launching the application
self.tableView.registerNib(nib, forCellReuseIdentifier: “commentCell”)
If on that line I use tableView? instead of tableView, then it’s ok but
SIG ABORT on the dequeue line when launching the application
var cell:CommentTableViewCell = self.tableView?.dequeueReusableCellWithIdentifier(“commentCell”, forIndexPath: indexPath) as CommentTableViewCell
Thank’s again.
Best,
Anatole
Did you connect the tableView IBOutlet ?