25

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?

Azhagusundaram Tamil
  • 2,053
  • 3
  • 21
  • 36

4 Answers4

40

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()
            }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
8

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)
    }
}
mickben
  • 385
  • 4
  • 11
4

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)
        }
    }
}
Rad'Val
  • 8,895
  • 9
  • 62
  • 92
  • 1
    It's a nice approach in theory, but it won't work as expected in practice. As the documentation suggest, you should create a `Namespace` by using `@Namespace` property wrapper. Initializing namespace outside of View scope, will not bind the correct views and properties during the transition so it won't work. Instead it should be assigned at view's initialization as the correct answer suggest. – Jan Cássio Jan 20 '22 at 20:36
  • It's not clear what you mean, but I've used this without a problem in multiple projects. Do you mean the default value? That's never used. – Rad'Val Jan 20 '22 at 23:28
  • @JanCássio is correct. The above solution will produce a runtime error for the `defaultValue`. – mickben Feb 07 '22 at 23:04
  • 1
    @mickben, indeed, *if* you use the default value: the idea is to set the `.namespace`. Alternatively, `@EnvironmentObject` can be used, but suffers from the same problem, if you don't first set it first, you'll get a fatal error. – Rad'Val Feb 08 '22 at 01:30
0

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.

Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49