'How to pass @namespace to multiple Views in SwiftUI?

I'm playing with the new Xcode 12 beta & SwiftUi 2.0. .matchedGeometryEffect() modifier is great to do Hero animations. There is a new property @Namespace is introduced in SwiftUI. Its super cool. working awesome.

I was just wondering if there is any possibility to pass a Namespace variable to multiple Views?

Here is an example I'm working on,

struct HomeView: View {
    @Namespace var namespace
    @State var isDisplay = true
    
    var body: some View {
        ZStack {
            if isDisplay {
                VStack {
                    Image("share sheet")
                        .resizable()
                        .frame(width: 150, height: 100)
                        .matchedGeometryEffect(id: "img", in: namespace)
                    Spacer()
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.blue)
                .onTapGesture {
                    withAnimation {
                        self.isDisplay.toggle()
                    }
                }
            } else {
                VStack {
                    Spacer()
                    Image("share sheet")
                        .resizable()
                        .frame(width: 300, height: 200)
                        .matchedGeometryEffect(id: "img", in: namespace)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.red)
                .onTapGesture {
                    withAnimation {
                        self.isDisplay.toggle()
                    }
                }
            }
        }
    }
}

It is working fine.

But if I want to extract the Vstack as a SubView, Below picture shows that I have extracted the first VStack into a subview.

enter image description here

I'm getting a compliment Cannot find 'namespace' in scope

Is there a way to pass namespace across multiple Views?



Solution 1:[1]

The @Namespace is a wrapper for Namespace.ID, and you can pass Namespace.ID in argument to subviews.

Here is a demo of possible solution. Tested with Xcode 12 / iOS 14

struct HomeView: View {
    @Namespace var namespace
    @State var isDisplay = true

    var body: some View {
        ZStack {
            if isDisplay {
                View1(namespace: namespace, isDisplay: $isDisplay)
            } else {
                View2(namespace: namespace, isDisplay: $isDisplay)
            }
        }
    }
}

struct View1: View {
    let namespace: Namespace.ID
    @Binding var isDisplay: Bool
    var body: some View {
        VStack {
            Image("plant")
                .resizable()
                .frame(width: 150, height: 100)
                .matchedGeometryEffect(id: "img", in: namespace)
            Spacer()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.blue)
        .onTapGesture {
            withAnimation {
                self.isDisplay.toggle()
            }
        }
    }
}

struct View2: View {
    let namespace: Namespace.ID
    @Binding var isDisplay: Bool
    var body: some View {
        VStack {
            Spacer()
            Image("plant")
                .resizable()
                .frame(width: 300, height: 200)
                .matchedGeometryEffect(id: "img", in: namespace)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.red)
        .onTapGesture {
            withAnimation {
                self.isDisplay.toggle()
            }
        }
    }
}

backup

Solution 2:[2]

While the accepted answer works, it gets a bit annoying to share the namespace across multiple nested subviews, especially if you'd like your initialisers clean and to the point. Using environment values might be better in this case:

struct NamespaceEnvironmentKey: EnvironmentKey {
    static var defaultValue: Namespace.ID = Namespace().wrappedValue
}

extension EnvironmentValues {
    var namespace: Namespace.ID {
        get { self[NamespaceEnvironmentKey.self] }
        set { self[NamespaceEnvironmentKey.self] = newValue }
    }
}

extension View {
    func namespace(_ value: Namespace.ID) -> some View {
        environment(\.namespace, value)
    }
}

Now you can create a namespace in any view and allow all its descendants to use it:

/// Main View
struct PlaygroundView: View {
    @Namespace private var namespace

    var body: some View {
        ZStack {
           SplashView()
...
        }
        .namespace(namespace)
    }
}

/// Subview
struct SplashView: View {
    @Environment(\.namespace) var namespace

    var body: some View {
        ZStack(alignment: .center) {
            Image("logo", bundle: .module)
                .matchedGeometryEffect(id: "logo", in: namespace)
        }
    }
}

Solution 3:[3]

A warning free approach to inject the Namespace into the Environment is to create an ObservableObject, named something like NamespaceWrapper, to hold the Namespace once it's been created. This could look something like:

class NamespaceWrapper: ObservableObject {
    var namespace: Namespace.ID

    init(_ namespace: Namespace.ID) {
        self.namespace = namespace
    }
}

You would then create and pass the Namespace like so:

struct ContentView: View {
    @Namespace var someNamespace

    var body: some View {
        Foo()
            .environmentObject(NamespaceWrapper(someNamespace))
    }
}

struct Foo: View {
    @EnvironmentObject var namespaceWrapper: NamespaceWrapper
    
    var body: some View {
        Text("Hey you guys!")
            .matchedGeometryEffect(id: "textView", in: namespaceWrapper.namespace)
    }
}

Solution 4:[4]

A slight evolution on @mickben 's answer.

We'll use a Namespaces object to hold multiple Namespace.ID instances. We'll inject that as an environment object - and also provide an easy way to configure Previews.

Firstly - the Namespaces wrapper

class Namespaces:ObservableObject {
    internal init(image: Namespace.ID = Namespace().wrappedValue,
                  title: Namespace.ID = Namespace().wrappedValue) {
        self.image = image
        self.title = title
    }
    
    let image:Namespace.ID
    let title:Namespace.ID    
}

I use different namespaces for different objects.

So, an Item with an image & title...

struct Item:Identifiable {
    let id = UUID()
    var image:UIImage = UIImage(named:"dummy")!
    var title = "Dummy Title"
}

struct ItemView: View {
    @EnvironmentObject var namespaces:Namespaces
    
    var item:Item
    
    var body: some View {
        VStack {
            Image(uiImage: item.image)
                .matchedGeometryEffect(id: item.id, in: namespaces.image)
            
            Text(item.title)
                .matchedGeometryEffect(id: item.id, in: namespaces.title)
        }
    }
}

struct ItemView_Previews: PreviewProvider {
    static var previews: some View {
        ItemView(item:Item())
            .environmentObject(Namespaces())
    }
}

You can see the advantage of multiple namespaces here. I can use the asme item.id as the 'natural' id for both the image and title animations.

Notice how the preview is really easy to construct here using .environmentObject(Namespaces())

If we use Namespaces() to create our namespaces in the actual app, then we'll get a warning.

Reading a Namespace property outside View.body. This will result in identifiers that never match any other identifier.

Depending on your setup - this may not be true, but we can work around by using the explicit initialiser

struct ContentView: View {
     
    var body: some View {
        ItemView(item: Item())
            .environmentObject(namespaces)
    }
    
    @Namespace var image
    @Namespace var title
    var namespaces:Namespaces {
        Namespaces(image: image, title: title)
    }
}

I like to keep my namespace creation in a var as it keeps the property wrappers and initialiser together. It's easy to add new namespaces as appropriate.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Rad'Val
Solution 3 mickben
Solution 4 Confused Vorlon