首页 > 解决方案 > 在顶级视图中居中 SwiftUI 视图

问题描述

我在 SwiftUI 中创建了一个加载指示器,它应该始终位于视图层次结构的顶层视图的中心(即在全屏应用程序的整个屏幕中居中)。这在 UIKit 中很容易,但是 SwiftUI 仅相对于其父视图将视图居中,我无法获取父视图的父视图的位置。

遗憾的是,我的应用程序并非完全基于 SwiftUI,因此我无法轻松地在我的根视图上设置属性,然后我可以在加载视图中访问这些属性 - 无论视图层次结构如何,我都需要此视图居中(混合 UIKit - SwiftUI 父级)意见)。这就是为什么SwiftUI set position to center of different view 之类的答案不适用于我的用例,因为在该示例中,您需要修改要使子视图居中的视图。

我尝试过使用.offset和的.position功能,但是,无论屏幕大小或整个屏幕的哪个部分占用View,我都无法获得正确的输入以始终动态居中。loadingViewrootView

请找到以下问题的最小可重现示例:

/// Loading view that should always be centered in the whole screen on the XY axis and should be the top view in the Z axis
struct CenteredLoadingView<RootView: View>: View {
    private let rootView: RootView

    init(rootView: RootView) {
        self.rootView = rootView
    }

    var body: some View {
        ZStack {
            rootView
                loadingView
        }
             // Ensure that `AnimatedLoadingView` is displayed above all other views, whose `zIndex` would be higher than `rootView`'s by default
            .zIndex(.infinity)
    }

    private var loadingView: some View {
        VStack {
            Color.white
                .frame(width: 48, height: 72)
            Text("Loading")
                .foregroundColor(.white)
        }
            .frame(width: 142, height: 142)
            .background(Color.primary.opacity(0.7))
            .cornerRadius(10)
    }
}

应在其上方显示加载视图的视图:

struct CenterView: View {
    var body: some View {
        return VStack {
            Color.gray
            HStack {
                CenteredLoadingView(rootView: list)
                otherList
            }
        }
    }

    var list: some View {
        List {
            ForEach(1..<6) {
                Text($0.description)
            }
        }
    }

    var otherList: some View {
        List {
            ForEach(6..<11) {
                Text($0.description)
            }
        }
    }
}

结果如下所示: 当前(不正确的)用户界面

UI 应该是这样的: 所需的用户界面

我曾尝试修改body使用CenteredLoadingViewaGeometryReader.frame(in: .global)获得全局屏幕尺寸,但我所取得的成就是现在 myloadingView根本不可见。

var body: some View {
    GeometryReader<AnyView> { geo in
        let screen = geo.frame(in: .global)
        let stack = ZStack {
            self.rootView
            self.loadingView
                .position(x: screen.midX, y: screen.midY)
                // Offset doesn't work either
                //.offset(x: -screen.origin.x, y: -screen.origin.y)
        }
             // Ensure that `AnimatedLoadingView` is displayed above all other views, whose `zIndex` would be higher than `rootView`'s by default
            .zIndex(.infinity)
        return AnyView(stack)
    }
}

标签: iosswiftswiftui

解决方案


这是一个可能的方法的演示。这个想法是使用注入的 UIView 来访问 UIWindow,然后将加载视图显示为窗口的根视图控制器视图的顶视图。

使用 Xcode 12 / iOS 14 测试(但兼容 SwiftUI 1.0)

在此处输入图像描述

注意:动画、效果等是可能的,但为简单起见超出了范围

struct CenteredLoadingView<RootView: View>: View {
    private let rootView: RootView
    @Binding var isActive: Bool

    init(rootView: RootView, isActive: Binding<Bool>) {
        self.rootView = rootView
        self._isActive = isActive
    }

    var body: some View {
        rootView
            .background(Activator(showLoading: $isActive))
    }

    struct Activator: UIViewRepresentable {
        @Binding var showLoading: Bool
        @State private var myWindow: UIWindow? = nil

        func makeUIView(context: Context) -> UIView {
            let view = UIView()
            DispatchQueue.main.async {
                self.myWindow = view.window
            }
            return view
        }

        func updateUIView(_ uiView: UIView, context: Context) {
            guard let holder = myWindow?.rootViewController?.view else { return }

            if showLoading && context.coordinator.controller == nil {
                context.coordinator.controller = UIHostingController(rootView: loadingView)

                let view = context.coordinator.controller!.view
                view?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
                view?.translatesAutoresizingMaskIntoConstraints = false
                holder.addSubview(view!)
                holder.isUserInteractionEnabled = false

                view?.leadingAnchor.constraint(equalTo: holder.leadingAnchor).isActive = true
                view?.trailingAnchor.constraint(equalTo: holder.trailingAnchor).isActive = true
                view?.topAnchor.constraint(equalTo: holder.topAnchor).isActive = true
                view?.bottomAnchor.constraint(equalTo: holder.bottomAnchor).isActive = true
            } else if !showLoading {
                context.coordinator.controller?.view.removeFromSuperview()
                context.coordinator.controller = nil
                holder.isUserInteractionEnabled = true
            }
        }

        func makeCoordinator() -> Coordinator {
            Coordinator()
        }

        class Coordinator {
            var controller: UIViewController? = nil
        }

        private var loadingView: some View {
            VStack {
                Color.white
                    .frame(width: 48, height: 72)
                Text("Loading")
                    .foregroundColor(.white)
            }
                .frame(width: 142, height: 142)
                .background(Color.primary.opacity(0.7))
                .cornerRadius(10)
        }
    }
}

struct CenterView: View {
    @State private var isLoading = false
    var body: some View {
        return VStack {
            Color.gray
            HStack {
                CenteredLoadingView(rootView: list, isActive: $isLoading)
                otherList
            }
            Button("Demo", action: load)
        }
        .onAppear(perform: load)
    }

    func load() {
        self.isLoading = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.isLoading = false
        }
    }

    var list: some View {
        List {
            ForEach(1..<6) {
                Text($0.description)
            }
        }
    }

    var otherList: some View {
        List {
            ForEach(6..<11) {
                Text($0.description)
            }
        }
    }
}

推荐阅读