UICollectionView in Swift 3 without interface builder.

Collection views are similar to table views, they show a collection of cells defined by a custom layout and like table views, they have to conform to protocols in order to display data and perform actions. In this tutorial, we are going to create a collection view that looks like a grid.

Ok, let’s dive in…

Let’s start creating a new project and this time select Swift. Now, let me start introducing to you the class that will handle the layout of the collectionView,  UICollectionViewFlowLayout.

Create a new Cocoa Touch file and make it subclass of  UICollectionViewFlowLayout, add this full implementation and I will explain it…

let innerSpace: CGFloat = 1.0
let numberOfCellsOnRow: CGFloat = 3

override init() {
super.init()
self.minimumLineSpacing = innerSpace
self.minimumInteritemSpacing = innerSpace
self.scrollDirection = .vertical
}

required init?(coder aDecoder: NSCoder) {
//fatalError("init(coder:) has not been implemented")
super.init(coder: aDecoder)
}

func itemWidth() -> CGFloat {
return (collectionView!.frame.size.width/self.numberOfCellsOnRow)-self.innerSpace
}

override var itemSize: CGSize {
set {
self.itemSize = CGSize(width:itemWidth(), height:itemWidth())
}
get {
return CGSize(width:itemWidth(),height:itemWidth())
}
}

We want to create a grid Layout with a 1 point of space between cells horizontally and vertically, that’ts why we have a constant with that value and it’s assigned to the ninimumLineSpacing and minimumInteritemSpacing properties.

fatal

Right now, I bet that all your attention is on the “fatalerror” line, looks really scary but the method where this “spooky” line is declared, is just a failable initializer, I won’t go deep on this subject but you can see more about it here, also the aDecoder: NSCoder will not be triggered because we are going to do all in code, and this gets triggered only if you make the implementation of the collectionView on interface builder.

The itemWidth function returns the full width divided but how many cells in a “row” you want, minus the amount of the Innerspace.

And finally, we override the itemSize property by returning a new CGSize.

That’s all for the layout now let’s jump to the ViewController file, add this two variables…

var gridCollectionView: UICollectionView! 
var gridLayout: GridLayout!

Inside the viewDidLoad method let’s implement the collectionView with our new GridLayout class like this…

gridLayout = GridLayout()
gridCollectionView = UICollectionView.init(frame: CGRect.zero, collectionViewLayout: gridLayout)
gridCollectionView.backgroundColor = UIColor.orange
gridCollectionView.showsVerticalScrollIndicator = false
gridCollectionView.showsHorizontalScrollIndicator = false
self.view.addSubview(gridCollectionView)

Now, we need to set a size and a position for the collectionView, copy and paste…

override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        var frame = gridCollectionView.frame
        frame.size.height = self.view.frame.size.height
        frame.size.width = self.view.frame.size.width
        frame.origin.x = 0
        frame.origin.y = 0
        gridCollectionView.frame = frame
    }

It’s time to create a custom Cell, create a new cocoa touch file subclass of UICollectionviewCell and name it ImageCell, copy and paste…


class ImageCell: UICollectionViewCell {
    
    var imageView: UIImageView!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.isUserInteractionEnabled = false
        contentView.addSubview(imageView)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        var frame = imageView.frame
        frame.size.height = self.frame.size.height
        frame.size.width = self.frame.size.width
        frame.origin.x = 0
        frame.origin.y = 0
        imageView.frame = frame
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Here we add an ImageView to it and that’s it for the cell.
In viewDidLoad let’s register the class cell for our collectionView and also set the delegate and datasource, add this lines in the implementation of the gridCollectionView.

gridCollectionView!.register(ImageCell.self, forCellWithReuseIdentifier: "cell")
gridCollectionView.dataSource = self
gridCollectionView.delegate = self

Now, your compiler is complaining and that’s because we need to conform the protocols, normally we will add it next to the name of the class but Swift gives us a better way to do it, and there is where extensions shine. Extensions let us put all the logic of a protocol without collapsing our class implementation, it looks like Apple is taking readability very serious and that’s awesome for us. Outside your class copy and paste this…

extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 50
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ImageCell
        cell.imageView.image = UIImage.init(named: "puppy")
        return cell
    }
}

Of course, you can change the logic so you show a collection of images from a restApi for example, but for simplicity, we are just using one image in my case I added a Puppy image to my assets, please go ahead and find a cool image for you, Run the app and you will see a full grid layout with your image!

simulator-screen-shot-dec-13-2016-22-24-35

Lets’s go a little bit further, and add some basic animation, play with alpha values to create a dissolve effect and also use a UIGesturRecoginzer.

Start adding a UIImageView to the project in viewDidLoad and set it’s alpha value to 0

fullImageView = UIImageView()
fullImageView.contentMode = .scaleAspectFit
fullImageView.backgroundColor = UIColor.lightGray
fullImageView.isUserInteractionEnabled = true
fullImageView.alpha = 0
self.view.addSubview(fullImageView)

Layout the fullImageView with the same dimensions and position of the collectionView add this line in the ViewWillLayoutSubViews method…

fullImageView.frame = gridCollectionView.frame

If you run the app now, you won’t see the fullImageView, and that’s because the alpha value is 0, the alpha property value goes from 0 to 1, O invisible and goes up until 1 completely visible.
Let’s create a method that will change the alpha back to completely visible and use CGAffineTransform to make a cool animation, copy and paste…

func showFullImage(of image:UIImage) {
        fullImageView.transform = CGAffineTransform(scaleX: 0, y: 0)
        fullImageView.contentMode = .scaleAspectFit
        UIView.animate(withDuration: 0.5, delay: 0, options: [], animations:{
            self.fullImageView.image = image
            self.fullImageView.alpha = 1
            self.fullImageView.transform = CGAffineTransform(scaleX: 1, y: 1)
        }, completion: nil)
}

Go to your extension, and add this…

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let cell = collectionView.cellForItem(at: indexPath) as! ImageCell
        if let image = cell.imageView.image {
            self.showFullImage(of: image)
        } else {
            print("no photo")
        } 
}

Now if you run the app, it will show you the fullImageView when you tap in any cell, but we need to dismiss that view and to accomplish that we are going to add a UITapGestureRecognizer to it; add this lines in viewDidLoad…

let dismissWihtTap = UITapGestureRecognizer(target: self, action: #selector(hideFullImage))
        fullImageView.addGestureRecognizer(dismissWihtTap)

Finally, let’s implement that method so we shut up the compiler error, let’s play with the alpha again by creating a “dissolve effect” changing back the alpha to 0 in 0.5 seconds, of course, you can customize the duration of it as you want, here is the function…

func hideFullImage() {
        UIView.animate(withDuration: 0.5, delay: 0, options: [], animations:{[unowned self] in
            self.fullImageView.alpha = 0
        }, completion: nil)
    }

Run the app, tap in a cell to show a full image and then tap the image to dismiss it. Now, you have a first approach to different useful tools like UICollectionViews, extensions, CGAffineTransform, playing with the properties of the views like changing alpha in animation blocks to create cool effects and using a UITapGestureRecognizer to perform actions.

g.jpg

Hope you find it helpful here is the full project in Swift and here in Objective-C.

Peace!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s