Animal Browser version 2 - UIStackView, UIScrollView and Swift

Back in October I wrote a short article about using a static UITableView as a device for laying out a page dedicated to a single bit of data. I created a small sample app that allowed you to browse a list of animals and read some details about each one with a picture too.

WWDC 2015 (just last week) announced a new control in the iOS arsenal - UIStackView. Not only that but they recommended that UIStackView should be the first port of call for creating new interface layouts and AutoLayout constraints should only be used in cases where stack view cannot help.

It felt like a perfect opportunity to dive in and update my app to use UIStackView but also super power it with some nifty Swift stuff. I had a few niggles on the way which I think were mainly due to using 1st release beta software but I got there.

Here's the new app!

Screen shot of the app in landscape with the Fox selected.
Look at the cute fox!

The data model

Starting with a SplitView template in Swift on Xcode I first wanted to create the object model and try something a bit more Swifty. In the new version of the app I wanted to sort the list of animals alphabetically...

The Animal model is a very simple Struct...

struct Animal {
    let name: String
    let latinName: String
    let text: String
    let imageName: String? // some animals don't have photos
    let authorName: String
    let authorPhotoName: String
}

Now, in order to make sorting very simple I just need to make Animals comparable...

extension Animal: Comparable {}

func ==(lhs: Animal, rhs: Animal) -> Bool {
    return lhs.name == rhs.name
}

func <=(lhs: Animal, rhs: Animal) -> Bool {
    return lhs.name <= rhs.name
}

func <(lhs: Animal, rhs: Animal) -> Bool {
    return lhs.name < rhs.name
}

func >=(lhs: Animal, rhs: Animal) -> Bool {
    return lhs.name >= rhs.name
}

That's pretty much it.

To create my array of Animals I load the plist into an array. Map the array to an array of Animals and then sort it using the sort method (thanks to the comparable protocol).

var animals: [Animal] = {
    guard let path = NSBundle.mainBundle().pathForResource("Animals", ofType: "plist"),
        animalArray = NSArray(contentsOfFile: path) as? [[String: String]] else {return []}

    return animalArray.map {
        a in
        return Animal(name: a["name"]!,
            latinName: a["species"]!,
            text: a["text"]!,
            imageName: a["photo"],
            authorName: a["authorName"]!,
            authorPhotoName: a["authorPhoto"]!)
    }.sort{$0 < $1}
}()

UIStackView in the UITableViewCells

Now for the interesting part. I wanted to go all in with UIStackView and try to use it as much as possible. The first thing to do was the UITableViewCell layout. This was done with nested stack views and one supplementary constraint.

Three stack views. Blue is horizontal and red is vertical.
Three stack views. Blue is horizontal and red is vertical.

The layout above comprises of three stack views. The inner most contains the two labels arranged vertically with a space. The next one contains only the label stack view and is arrange horizontally. By centring this one it will arrange the labels neatly around the centre line. The last one contains the image view and the second stack view arranged horizontally with a fill alignment.

The one additional constraint I needed to add was a 1:1 aspect ratio on the image view. This gave me everything I wanted. I just had to pin the outset stack view to the cell and everything was sorted.

UITableViewCell laid out entirely with UIStackView.
UITableViewCell laid out entirely with UIStackView.

UIStackView in a UIScrollView

Next I wanted to lay out the detail view of the app. I already had the layout from the previous version so I just needed to implement this with a UIStackView. I quickly learned that the easiest way to work with UIStackView is to work from the inside out.

Layout 1
Start with the smallest elements. In this case the name labels and the author detail. These are both aligned centre.
Layout 2
Next add the main image and the text view and stack them all together. This is aligned fill.
Layout 3
Embed the whole thing in a scroll view, pin the stack view to the four edges of the scroll view and add constraints from the scroll view to the superview.
Layout 4
Update the frames to check your amazing layout!
Layout 5
Next add the placeholder images and everything is sorted.

I struggled more than I thought I should with this trying to build it from the outside in but once I changed tack everything fell into place... literally.

The missing images!

Oh no! I couldn't find an image of the Red Squirrel on the internet... OK, I didn't search very hard...

As you saw from earlier the imageName property of the is optional which means it's possible that an image sometimes doesn't exist. This is what I really love about using UIStackView. With this code...

if let imageName = animal.imageName {
    animalImageView.hidden = false
    animalImageView.image = UIImage(named: imageName)
} else {
    animalImageView.hidden = true
}

... I check if the imageName exists and load an image if it does. If it doesn't... I just hide the imageView. Because the image view is arranged by a UIStackView the stack view intelligently removes it from the arrangement and reorganises the other views around it.

The image view has been removed and the space between the labels and the text has been set accordingly.
The image view has been removed and the space between the labels and the text has been set accordingly.

All the shiny things!

There has been so much new stuff from Apple this last week. I'm like a child with a new toy.

I've added a lot of stuff here for what is only a very small project. If you'd like to have a look at the code and see how it all works take a look at my GitHub repo.

2 Comments

  1. zaid

    can you explain more about the constrains you used in your app because im having some difficulties with it and how to make the TextView shows all the text in them

    thanks

Leave a Reply

Your email address will not be published. Required fields are marked *