swiftuiviewbuilder

Saving ViewBuilder parameters to be used outside of init


I'm creating a custom object called AsyncImageCached using the same init signature as found in AsyncImage. My question is, how do I define variables outside of init to save content and placeholder parameters to be used when my async await calls complete?

public struct AsyncImageCached<Content> : View where Content : View {

    private let content: ((Image) -> I)?   <--- Doesn't Work, Cannot find type 'I' in scope
    private let placeholder: (() -> P)?    <--- Doesn't work, Cannot find type 'P' in scope

    init<I, P>(url: URL?, scale: CGFloat = 1, 
        @ViewBuilder content: @escaping (Image) -> I,
        @ViewBuilder placeholder: @escaping () -> P)
        where Content == _ConditionalContent<I, P>, I : View, P : View {

        let content: (Image) -> I = content    <--- Works, but can't access outside of init
        let placeholder: () -> P = placeholder <--- Works, but can't access outside of init

        ...
    }
}

Moving I, P to the structure level will break the other inits and will not match Apples AsyncImage signatures.

There must be a way to make it work because the same signature is in AsyncImage. I don't want to change the init signature function because I already have the other inits working:

public init(url: URL?, scale: CGFloat = 1) where Content == Image
public init(url: URL?, scale: CGFloat = 1, @ViewBuilder content: @escaping (AsyncImagePhase) -> Content)

Any help would be greatly appreciated, I have spent two days on this and I can't find anything online that teaches how to use the ViewBuilder outside of simple examples, non that have a custom init like this.


Solution

  • Well, inspecting Swift interface file, we can see the following:

    public struct AsyncImage<Content> : SwiftUI.View where Content : SwiftUI.View {
        /* ... */
    
        @_alwaysEmitIntoClient public init<I, P>(url: Foundation.URL?, scale: CoreGraphics.CGFloat = 1, @SwiftUI.ViewBuilder content: @escaping (SwiftUI.Image) -> I, @SwiftUI.ViewBuilder placeholder: @escaping () -> P) where Content == SwiftUI._ConditionalContent<I, P>, I : SwiftUI.View, P : SwiftUI.View {
            self.init(url: url, scale: scale) { phase in
                if let i = phase.image {
                    content(i)
                } else {
                    placeholder()
                }
            }
        }
    
        /* ... */
    }
    

    So, it turns out it just calls another init itself, which doesn't require these generics!

    You can see the interface file at /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64.swiftinterface, replacing Xcode.app with whatever the name is (as betas have different names)