'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.
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()
}
}
}
}
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 |