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

28

我正在玩新的Xcode 12 beta和SwiftUi 2.0。 .matchedGeometryEffect() 修饰符非常适合进行英雄动画。 SwiftUI中引入了一个新属性@Namespace,非常酷炫,工作得非常好。

我只是想知道是否有可能将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提取为子视图。

enter image description here

我收到了一个赞美Cannot find 'namespace' in scope

是否有一种方法可以在多个视图之间传递命名空间?

4个回答

47

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

10
一个不会有警告的将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)
    }
}

4

虽然接受的答案能够正常工作,但如果您希望初始化程序干净简洁并跨多个嵌套子视图共享命名空间的话,那么这会变得有点烦人。在这种情况下,使用环境值可能更好:

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

1
从理论上来说,这是一个不错的方法,但在实践中它并不能按预期工作。正如文档所建议的那样,您应该使用@Namespace属性包装器创建一个命名空间。在视图范围之外初始化命名空间,将无法在转换期间绑定正确的视图和属性,因此它不起作用。相反,应该在视图的初始化时分配它,就像正确的答案建议的那样。 - Jan Cássio
不清楚您的意思,但我在多个项目中使用过这个而没有问题。您是指默认值吗?那从来没有被使用过。 - Rad'Val
@JanCássio 是正确的。上述解决方案会对 defaultValue 产生运行时错误。 - mickben
1
@mickben,确实,如果您使用默认值:想法是设置.namespace。另外,可以使用@EnvironmentObject,但如果您没有先设置它,那么它将遇到相同的问题,并且会导致致命错误。 - Rad'Val

1
一个对 @mickben 答案的微小改进。
我们将使用一个 Namespaces 对象来保存多个 Namespace.ID 实例。 我们将把它作为环境对象注入 - 并提供一种简单的方法来配置预览。
首先 - Namespaces 包装器。
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    
}

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

因此,一个带有图片和标题的项目...

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

在这里,您可以看到多个命名空间的优势。我可以使用相同的item.id作为图像和标题动画的“自然”id。

请注意,使用.environmentObject(Namespaces())在此处构建预览非常容易。

如果我们在实际应用程序中使用Namespaces()来创建命名空间,则会收到警告。

在View.body之外读取Namespace属性。这将导致永远不匹配任何其他标识符的标识符。

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

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

我喜欢将我的命名空间创建放在一个变量中,因为这样可以将属性包装器和初始化程序放在一起。这样很容易根据需要添加新的命名空间。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接