J. Edgell
J. Edgell

Reputation: 1865

How can I create a SwiftUI view with a phased protocol similar to AsyncImage

I want to have an object just like AsyncImage where different states can be displayed using a "phase in" variable as in the AsyncImage example below. Can someone point me to an example on how to write an object like this? I need two features, one is using the phase variable to set the states, another is using the "let image" parameter to pass a view (in this case, an image) to the parent view. I'm not asking about the AsyncImage object, I'm using this as an example of the protocol I want to use for a custom object.

AsyncImage(
        url: url,
        transaction: Transaction(animation: .easeInOut)
    ) { phase in
        switch phase {
        case .empty:
            ProgressView()
        case .success(let image):
            image
                .resizable()
                .transition(.scale(scale: 0.1, anchor: .center))
        case .failure:
            Image(systemName: "wifi.slash")
        @unknown default:
            EmptyView()
        }
    }

Upvotes: 1

Views: 251

Answers (1)

George
George

Reputation: 30391

Here is a demo of my own version of AsyncImage. It's definitely no-where near the full implementation, as Apple makes lots of optimisations. I also didn't add a transaction parameter for simplicity's sake.

Code:

struct FakeAsyncImage<Content: View>: View {
    enum Failure: Error {
        case missingURL
        case invalidImage
    }

    @State private var phase: AsyncImagePhase = .empty
    private let url: URL?
    private let scale: CGFloat
    private let content: (AsyncImagePhase) -> Content

    init(url: URL?, scale: CGFloat = 1, @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
        self.url = url
        self.scale = scale
        self.content = content
    }

    var body: some View {
        content(phase)
            .task {
                await makeRequest()
            }
    }

    private func makeRequest() async {
        guard let url = url else {
            phase = .failure(Failure.missingURL)
            return
        }

        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            guard let uiImage = UIImage(data: data, scale: scale) else {
                phase = .failure(Failure.invalidImage)
                return
            }

            phase = .success(Image(uiImage: uiImage))
        } catch {
            phase = .failure(error)
        }
    }
}

Example usage:

struct ContentView: View {
    var body: some View {
        FakeAsyncImage(url: URL(string: "https://www.gravatar.com/avatar/e527532a57601f0f368f6643317be841?s=256&d=identicon&r=PG&f=1")) { phase in
            switch phase {
            case .empty:
                ProgressView()
            case .success(let image):
                image
                    .resizable()
                    .transition(.scale(scale: 0.1))
            case .failure:
                Image(systemName: "wifi.slash")
            @unknown default:
                EmptyView()
            }
        }
    }
}

Upvotes: 1

Related Questions