如何将@namespace 传递给 SwiftUI 中的多个视图?

How to pass @namespace to multiple Views in SwiftUI?

我正在玩新的 Xcode 12 beta 和 SwiftUi 2.0。 .matchedGeometryEffect() 修改器非常适合制作英雄动画。 SwiftUI 中引入了一个新的 属性 @Namespace。它超级酷。工作很棒。

我只是想知道是否有可能将命名空间变量传递给多个视图?

这是我正在处理的示例,

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

它工作正常。

但是如果我想将 Vstack 提取为子视图,下图显示我已将第一个 VStack 提取到子视图中。

我得到了赞美Cannot find 'namespace' in scope

有没有办法跨多个视图传递命名空间?

@NamespaceNamespace.ID 的包装器,您可以将 Namespace.ID 参数传递给子视图。

这是一个可能的解决方案演示。用 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

虽然已接受的答案有效,但在多个嵌套子视图之间共享名称空间会有点烦人,尤其是如果您希望初始化程序简洁明了。在这种情况下使用环境值可能更好:

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)
    }
}

现在您可以在任何视图中创建命名空间并允许其所有后代使用它:

/// 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)
        }
    }
}

Namespace 注入 Environment 的无警告方法是创建一个 ObservableObject,名称类似于 NamespaceWrapper,以保存 Namespace 一旦它被创建。这可能看起来像:

class NamespaceWrapper: ObservableObject {
    var namespace: Namespace.ID

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

然后您可以像这样创建并传递 Namespace

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 的回答略有改进。

我们将使用命名空间 object 来保存多个 Namespace.ID 实例。 我们会将其作为环境注入 object - 并提供一种配置预览的简单方法。

首先 - 命名空间包装器

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    
}

我为不同的 object 使用不同的命名空间。

那么,带有图像和标题的项目...

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

您可以在这里看到多个命名空间的优势。我可以使用 asme item.id 作为图像和标题动画的 'natural' id。

注意这里使用 .environmentObject(Namespaces())

构建预览真的很容易

如果我们在实际应用中使用 Namespaces() 创建我们的命名空间,那么我们会收到警告。

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

根据您的设置 - 这可能不是真的,但我们可以通过使用显式初始化程序来解决

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)
    }
}

我喜欢将我的命名空间创建保存在一个 var 中,因为它将 属性 包装器和初始化器放在一起。很容易根据需要添加新的命名空间。