【问题标题】:Center SwiftUI view in top-level view在顶级视图中居中 SwiftUI 视图
【发布时间】:2020-11-24 19:33:00
【问题描述】:

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

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

我尝试过使用View.offset.position 函数,但是,无论屏幕大小或任何部分,我都无法获得正确的输入以始终动态居中我的loadingView整个屏幕的rootView 占据。

请在下面找到该问题的最小可重现示例:

/// 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 应该是这样的:

我尝试使用GeometryReader.frame(in: .global) 修改CenteredLoadingViewbody 以获取全局屏幕尺寸,但我所取得的是现在我的loadingView 根本不可见。

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

【问题讨论】:

  • 应始终居中于其rootView 上方,但也应位于整个屏幕中 - 这是自相矛盾的要求。如果 rootView 是 10x10 点 topLeading - 那怎么能满足?我假设中心到根视图或全屏 - 不是两者。
  • @Asperi 不确定你从哪里得到的,这句话不在我的问题范围内。在任何情况下,加载视图都应该在整个屏幕中居中,如屏幕截图所示。
  • ;) 它在您对struct CenteredLoadingView 的评论中的第一个代码快照中...是您的代码吗?
  • @Asperi 我的错,忘记了文档评论并在页面上找到没有找到,但即使有该文档评论,描述和屏幕截图也应该很清楚。无论如何更新了评论,现在应该非常清楚我的要求是什么。

标签: ios swift swiftui


【解决方案1】:

这是一个可能的方法的演示。这个想法是使用注入的 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)
            }
        }
    }
}

【讨论】:

  • 感谢您的出色解决方案!但是,如果我确实有它在工作表中呢?现在它显示在它的背后
猜你喜欢
  • 2020-11-27
  • 2019-10-21
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
  • 2015-11-29
  • 1970-01-01
  • 2019-10-23
  • 2020-07-15
相关资源
最近更新 更多