NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

Custom containers

Paul Hudson    @twostraws   

Although it’s not something you’re likely to do often, I want to at least show you that it’s perfectly possible to create custom containers in your SwiftUI apps. This takes more advanced Swift knowledge because it leverages some of Swift’s power features, so it’s OK to skip this if you find it too much.

To try it out, we’re going to make a new type of stack called a GridStack, which will let us create any number of views inside a grid. What we want to say is that there is a new struct called GridStack that conforms to the View protocol and has a set number of rows and columns, and that inside the grid will be lots of content cells that themselves must conform to the View protocol.

In Swift we’d write this:

struct GridStack<Content: View>: View {
    let rows: Int
    let columns: Int
    let content: (Int, Int) -> Content

    var body: some View {
        // more to come
    }
}

The first line – struct GridStack<Content: View>: View – uses a more advanced feature of Swift called generics, which in this case means “you can provide any kind of content you like, but whatever it is it must conform to the View protocol.” After the colon we repeat View again to say that GridStack itself also conforms to the View protocol.

Take particular note of the let content line – that defines a closure that must be able to accept two integers and return some sort of content we can show.

We need to complete the body property with something that combines multiple vertical and horizontal stacks to create as many cells as was requested. We don’t need to say what’s in each cell, because we can get that by calling our content closure with the appropriate row and column.

So, we might fill it in like this:

var body: some View {
    VStack {
        ForEach(0..<rows, id: \.self) { row in
            HStack {
                ForEach(0..<self.columns, id: \.self) { column in
                    self.content(row, column)
                }
            }
        }
    }
}

Tip: When looping over ranges, SwiftUI can use the range directly only if we know for sure the values in the range won’t change over time. Here we’re using ForEach with 0..<rows and 0..<columns, both of which are values that can change over time – we might add more rows, for example. In this situation, we need to add a second parameter to ForEach, id: \.self, to tell SwiftUI how it can identify each view in the loop. We’ll go into more detail on this in project 5.

Now that we have a custom container, we can write a view using it like this:

struct ContentView: View {
    var body: some View {
        GridStack(rows: 4, columns: 4) { row, col in
            Text("R\(row) C\(col)")
        }
    }
}

Our GridStack is capable of accepting any kind of cell content, as long as it conforms to the View protocol. So, we could give cells a stack of their own if we wanted:

GridStack(rows: 4, columns: 4) { row, col in
    HStack {
        Image(systemName: "\(row * 4 + col).circle")
        Text("R\(row) C\(col)")
    }
}

Want to go further?

For more flexibility we could leverage one of SwiftUI’s features called view builders, which allows us to send in several views and have it form an implicit stack for us.

To use this, we need to create a custom initializer for our GridStack struct, so we can mark the content closure as using SwiftUI’s view builders system:

init(rows: Int, columns: Int, @ViewBuilder content: @escaping (Int, Int) -> Content) {
    self.rows = rows
    self.columns = columns
    self.content = content
}

That is mostly just copying the parameters directly into the struct’s properties, but notice the @ViewBuilder attribute is there. You’ll also see the @escaping attribute, which allows us to store closures away to be used later on.

With that in place SwiftUI will now automatically create an implicit horizontal stack inside our cell closure:

GridStack(rows: 4, columns: 4) { row, col in
    Image(systemName: "\(row * 4 + col).circle")
    Text("R\(row) C\(col)")
}

Both options work, so do whichever you prefer.

Hacking with Swift is sponsored by Instabug

SPONSORED Are you tired of wasting time debugging your Swift app? Instabug’s SDK is here to help you minimize debugging time by providing you with complete device details, network logs, and reproduction steps with every bug report. All data is attached automatically, and it only takes a line of code to setup. Start your free trial now and get 3 months off exclusively for the Hacking with Swift Community.

Start your free trial!

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.6/5

Link copied to your pasteboard.