在顶级视图中居中 SwiftUI 视图 [英] Center SwiftUI view in top-level view

查看:55
本文介绍了在顶级视图中居中 SwiftUI 视图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

遗憾的是,我的应用程序并不完全基于 SwiftUI,因此我无法轻松地在我的根视图上设置属性,然后我可以在加载视图中访问这些属性 - 无论视图层次结构如何,我都需要将此视图居中(混合 UIKit- SwiftUI 父视图).这就是为什么像

UI 应该是这样的:

我尝试使用 GeometryReader.frame(in: .global) 修改 CenteredLoadingViewbody> 获取全局屏幕大小,但我已经实现的是现在我的 loadingView 根本不可见.

var body: some View {GeometryReader{ 地理输入让屏幕 = geo.frame(in: .global)让堆栈 = ZStack {self.rootViewself.loadingView.position(x: screen.midX, y: screen.midY)//偏移也不起作用//.offset(x: -screen.origin.x, y: -screen.origin.y)}//确保 `AnimatedLoadingView` 显示在所有其他视图之上,默认情况下其 `zIndex` 将高于 `rootView`.zIndex(.无穷大)返回 AnyView(堆栈)}}

解决方案

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

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

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

struct CenteredLoadingView: View {私人让 rootView: RootView@Binding var isActive: Boolinit(rootView: RootView, isActive: Binding) {self.rootView = rootViewself._isActive = isActive}var主体:一些视图{根视图.background(Activator(showLoading: $isActive))}结构激活器:UIViewRepresentable {@Binding var showLoading: Bool@State private var myWindow:UIWindow?= 零func makeUIView(context: Context) ->界面视图{让视图 = UIView()DispatchQueue.main.async {self.myWindow = view.window}返回视图}func updateUIView(_ uiView: UIView, context: Context) {守卫让持有人 = myWindow?.rootViewController?.view else { return }if showLoading &&context.coordinator.controller == nil {context.coordinator.controller = UIHostingController(rootView: loadingView)让视图 = context.coordinator.controller!.viewview?.backgroundColor = UIColor.black.withAlphaComponent(0.8)查看?.translatesAutoresizingMaskIntoConstraints = falseholder.addSubview(查看!)holder.isUserInteractionEnabled = false视图?.leadingAnchor.constraint(equalTo:holder.leadingAnchor).isActive = true查看?.trailingAnchor.constraint(equalTo:holder.trailingAnchor).isActive = true查看?.topAnchor.constraint(equalTo:holder.topAnchor).isActive = true视图?.bottomAnchor.constraint(equalTo: holder.bottomAnchor).isActive = true} 否则如果 !showLoading {context.coordinator.controller?.view.removeFromSuperview()context.coordinator.controller = nilholder.isUserInteractionEnabled = true}}func makeCoordinator() ->协调员{协调员()}班级协调员{var 控制器:UIViewController?= 零}私有变量加载视图:一些视图{虚拟堆栈{白颜色.frame(宽:48,高:72)文本(加载").foregroundColor(.white)}.frame(宽:142,高:142).background(Color.primary.opacity(0.7)).cornerRadius(10)}}}结构中心视图:查看{@State 私有变量 isLoading = falsevar主体:一些视图{返回 VStack {颜色.灰色堆栈{CenteredLoadingView(rootView: list, isActive: $isLoading)其他列表}按钮(演示",动作:加载)}.onAppear(执行:加载)}功能加载(){self.isLoading = trueDispatchQueue.main.asyncAfter(deadline: .now() + 2) {self.isLoading = false}}变量列表:一些视图{列表 {ForEach(1..<6) {文字($0.description)}}}var otherList:一些视图{列表 {ForEach(6..<11) {文字($0.description)}}}}

I am creating a loading indicator in SwiftUI that should always be centered in the top-level view of the view hierarchy (i.e centered in the whole screen in a fullscreen app). This would be easy in UIKit, but SwiftUI centres views relative to their parent view only and I am not able to get the positions of the parent views of the parent view.

Sadly my app is not fully SwiftUI based, so I cannot easily set properties on my root views that I could then access in my loading view - I need this view to be centered regardless of what the view hierarchy looks like (mixed UIKit - SwiftUI parent views). This is why answers like SwiftUI set position to centre of different view don't work for my use case, since in that example, you need to modify the view in which you want to centre your child view.

I have tried playing around with the .offset and .position functions of View, however, I couldn't get the correct inputs to always dynamically centre my loadingView regardless of screen size or regardless of what part of the whole screen rootView takes up.

Please find a minimal reproducible example of the problem below:

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

View above which the loading view should be displayed:

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

This is what the result looks like:

This is how the UI should look like:

I have tried modifying the body of CenteredLoadingView using a GeometryReader and .frame(in: .global) to get the global screen size, but what I've achieved is that now my loadingView is not visible at all.

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

解决方案

Here is a demo of possible approach. The idea is to use injected UIView to access UIWindow and then show loading view as a top view of window's root viewcontroller view.

Tested with Xcode 12 / iOS 14 (but SwiftUI 1.0 compatible)

Note: animations, effects, etc. are possible but are out scope for simplicity

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

这篇关于在顶级视图中居中 SwiftUI 视图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆