David Cordero

Implementing a TV Guide with SwiftUI

Published on 11 Mar 2024

One of the consequences of having worked in the TV industry for so many years, is that I had the opportunity to develop a TV Guide using multiple programming languages.

A TV Guide is often the most complex view component within such applications. That’s both in terms of navigation, with its multidirectionaly controls, and in terms of performance, with thousand of programs loaded asynchronously.

Throughout my career, I had the chance to implement TV Guides using Qt, JS Canvas and UIKit. The solution based on UIKit is most probably the most complex UI that I have ever created for the Apple ecosystem.

Even though the TV Guide solution at Zattoo has evolved significantly, adding a lot of navigation improvements to handle different type of remote controls, its initial implementation was based on the learnings from this great post by Kyle Andrews.

Solution based on SwiftUI

Having SwiftUI in the wild for some several years already, I could not resist creating a proof of concept for a TV Guide using this new UI library.

The basis for this proof of concept are similar to the one created by Kyle Andrews back in the days for UIKit. It consists on building a multidirectional grid of Programs. To make it more dynamic, each program can vary its size, being either narrow or wide.

You can find here below the code implementation for the view corresponding to each program:

struct ProgramView: View {
    
    var isNarrow: Bool
    
    @Environment(\.isFocused) var isFocused: Bool
    
    var body: some View {
        ZStack {
            Rectangle().foregroundColor(isFocused ? .pink : .secondary)
            Text("Program Title")
                .foregroundColor(.white)
                .lineLimit(1)
        }
        .frame(width: isNarrow ? 250 : 100, height: cellHeight)
    }
}

In addition to programs, we also need to show an initial column featuring all the Channels. In the following code you can find the implementation for the Channel view:

struct ChannelView: View {
    
    var channelNumber: Int
    
    var body: some View {
        ZStack {
            Rectangle().foregroundColor(.black)
            Text("Channel \(channelNumber)")
                .foregroundColor(.white)
                .lineLimit(1)
        }
        .frame(width: channelsWidth, height: cellHeight)
    }
}

With our components in place, we can proceed with the implementation of our TV Guide. As you can see right below, the implementation of a TV Guide using SwiftUI is quite straightforward using scrollable stacks:

import SwiftUI

private let numberOfRows = 100
private let visibleNumberOfRows = 5
private let cellHeight: CGFloat = 100

#if os(tvOS)
private var channelsWidth: CGFloat = 150
#else
private var channelsWidth: CGFloat = 100
#endif

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer()

            Text("The amazing SwiftUI Program Guide")
                .font(.largeTitle)

            Spacer()
        
            ScrollView(.vertical) {
                HStack(alignment: .top, spacing: 4) {
                    LazyVStack(alignment: .leading, spacing: 2) {
                        ForEach(0..<numberOfRows, id:\.self) {
                            rowIndex in
                            ChannelView(channelNumber: rowIndex)
                        }
                    }
                    .frame(width: channelsWidth)
                    
                    ScrollView(.horizontal) {
                        LazyVStack(alignment: .leading, spacing: 2) {
                            ForEach(0..<numberOfRows, id:\.self) {
                                rowIndex in
                                
                                LazyHStack(alignment: .top, spacing: 2) {
                                    ForEach(0..<30, id:\.self) {
                                        columnIndex in
                                        
                                        ZStack {
                                            ProgramView(isNarrow: (rowIndex + columnIndex).isMultiple(of: 2))
                                        }
                                        #if os(tvOS)
                                        .focusable()
                                        #endif
                                    }
                                }
                            }
                        }
                    }
                }
            }
            .frame(height: CGFloat(visibleNumberOfRows) * cellHeight)
            .edgesIgnoringSafeArea(.horizontal)
        }
        .frame(maxHeight: .infinity)
    }
}

Result

With this implementation, we have achieved a multiplatform TV Guide solution. In the following videos, you can observe its functionality on both iOS and tvOS.

Conclusion

I have to say that I am really impressed by the minimal code required to develop a functional TV Guide prototype with SwiftUI. This is by far the easiest framework that I had the chance to use for building this type of component.

You can easily compare this with the code required to build the same component using UIKit in the post by Kyle Andrews back in the days.

However, even though the prototype was that easy to build, I have a lot of concerns about its viability for a real-world applications. Unfortunately live data extends beyond just handling two different types of program durations. Dynamic data can bring a lot of edge cases requiring extensive optimizations. With SwiftUI having a higher level API, incorporating these necessary optimizations could be harder, if not unfeasible, without having to jump to a custom UIKit implementation.