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

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

问题描述

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

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

这是UI的外观:

我尝试使用 GeometryReader .frame(in.global)修改 CenteredLoadingView body >来获取全局屏幕尺寸,但是我已经实现了,现在我的 loadingView 根本不可见.

 变量主体:某些视图{GeometryReader< AnyView>{地理位置让屏幕= geo.frame(在:.global)让stack = ZStack {self.rootViewself.loadingView.position(x:screen.midX,y:screen.midY)//偏移也不起作用//.offset(x:-screen.origin.x,y:-screen.origin.y)}//确保将"AnimatedLoadingView"显示在所有其他视图上方,默认情况下,其"zIndex"将高于"rootView".zIndex(.infinity)返回AnyView(堆栈)}} 

解决方案

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

在Xcode 12/iOS 14上进行了测试(但与SwiftUI 1.0兼容)

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

  struct CenteredLoadingView< RootView:View> ;:视图{私人让rootView:RootView@Binding var isActive:布尔init(rootView:RootView,isActive:Binding< Bool>){self.rootView = rootViewself._isActive = isActive}var body:some View {rootView.background(Activator(showLoading:$ isActive))}struct Activator:UIViewRepresentable {@Binding var showLoading:布尔@State private var myWindow:UIWindow?=无func makeUIView(context:Context)->UIView {让view = UIView()DispatchQueue.main.async {self.myWindow = view.window}返回视图}func updateUIView(_ uiView:UIView,context:Context){守卫让holder = myWindow?.rootViewController?.view else {return}如果showLoading&&context.coordinator.controller == nil {context.coordinator.controller = UIHostingController(rootView:loadingView)让view = context.coordinator.controller!.viewview?.backgroundColor = UIColor.black.withAlphaComponent(0.8)view?.translatesAutoresizingMaskIntoConstraints = falseholder.addSubview(view!)owner.isUserInteractionEnabled = falseview?.leadingAnchor.constraint(equalTo:holder.leadingAnchor).isActive = trueview?.trailingAnchor.constraint(equalTo:holder.trailingAnchor).isActive = trueview?.topAnchor.constraint(equalTo:holder.topAnchor).isActive = trueview?.bottomAnchor.constraint(equalTo:holder.bottomAnchor).isActive = true},如果!showLoading {context.coordinator.controller?.view.removeFromSuperview()context.coordinator.controller =无holder.isUserInteractionEnabled = true}}func makeCoordinator()->协调员{协调人()}班级协调员{var控制器:UIViewController?=无}私人var loadingView:一些视图{VStack {白颜色.frame(宽度:48,高度:72)文字(正在加载").foregroundColor(.white)}.frame(宽度:142,高度:142).background(Color.primary.opacity(0.7)).cornerRadius(10)}}}struct CenterView:查看{@State私有var isLoading = falsevar body:some View {返回VStack {颜色.灰色HStack {CenteredLoadingView(rootView:list,isActive:$ isLoading)其他列表}按钮(演示",操作:加载)}.onAppear(执行:加载)}func load(){self.isLoading = trueDispatchQueue.main.asyncAfter(最后期限:.now()+ 2){self.isLoading =假}}var列表:某些视图{列表 {ForEach(1 ..< 6){文字($ 0.description)}}}var otherList:some View {列表 {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天全站免登陆