Blog

SwiftUI: Loading Data on Demand

In our previous post, we showed a way to load data from the network in SwiftUI.

The approach we use leaves some outstanding issues. First of all, because view structs can get reconstructed many times over the course of an app (typically, they get constructed much more often than UIViews), the construction of a view should be as efficient as possible. In our example, we reload data from the network every time the ContentView is created. For a root view, this might not be a problem, but in general, it's a bad idea.

Furthermore, the view might get constructed even when it's not going to be on screen. For example, a NavigationButton constructs its destination view immediately. This is unlike most UIKit apps, where you typically construct view controllers lazily, just before they get pushed onto the navigation stack.

To delay the loading of the resource until the view is on screen, we experimented with two approaches. In the first approach, we could evaluate the view on demand, by wrapping it in a LazyView wrapper:

								struct LazyView<Content: View>: View {
    let build: () -> Content
    init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}

							

This creates the view on-demand when SwiftUI wants to render the body of the lazy view. This can be useful when you work with a NavigationButton.

A second approach is to change the way our Resource works. Instead of loading the data in the Resource's initializer, we can load the data once SwiftUI starts subscribing to the object. This is a bit more complicated. We achieve this by using the handleEvents method on our PassthroughSubject. Once somebody subscribes (in practice SwiftUI is the only subscriber), we start loading the data.

								
final class Resource<A>: BindableObject {    
    // Workaround for initialization
    private(set) var didChange: AnyPublisher<A?, Never> = Publishers.Empty().eraseToAnyPublisher()

    let subject = PassthroughSubject<A?, Never>()
    let endpoint: Endpoint<A>
    var firstSubscriber: Bool = true

    var value: A? {
        didSet {
            DispatchQueue.main.async {
                self.subject.send(self.value)
            }
        }
    }

    init(endpoint: Endpoint<A>) {
        self.endpoint = endpoint
        didChange = subject.handleEvents(receiveSubscription: { [weak self] sub in
            guard let s = self, s.firstSubscriber else { return }
            s.firstSubscriber = false
            s.reload()
        }).eraseToAnyPublisher()
    }

    func reload() {
        URLSession.shared.load(endpoint) { result in
            self.value = try? result.get()
        }
    }
}

							

Both of the approaches above feel like workarounds, and at the moment of writing, there's no documentation about how this should be done. Likewise, they both rely on implementation assumptions: the lazy wrapper assumes that the underlying views get displayed once a view's body is evaluated, and the Resource assumes that a subscriber to didChange means that the view is displayed.

In either case, issues remain. While they both delay the loading of a URL until the view is displayed, neither solves a second problem: every time a view gets recreated, the URL gets loaded again. We'll try to solve that problem in a future post.

If you'd like to follow our experiments with SwiftUI, we'll be adding to our Swift Talk Collection over the coming weeks. In the latest series, we use SwiftUI to build a Swift Talk app — the first episode is free to watch.


  • See the Swift Talk Collection

  • Watch the Full Episode

  • See the Swift Talk Collection

Stay up-to-date with our newsletter or follow us on Twitter .

Back to the Blog

Recent Posts