Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
226 views
in Technique[技术] by (71.8m points)

ios - Center SwiftUI view in top-level view

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: current (incorrect) UI

This is how the UI should look like: desired UI

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)
    }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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)

enter image description here

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...