Центрировать представление SwiftUI в представлении верхнего уровня - PullRequest
1 голос
/ 04 августа 2020

Я создаю индикатор загрузки в SwiftUI, который всегда должен располагаться по центру в представлении верхнего уровня иерархии представлений (т.е. по центру всего экрана в полноэкранном приложении). Это было бы легко в UIKit, но SwiftUI центрирует представления относительно их родительского представления, и я не могу получить позиции родительских представлений родительского представления.

К сожалению, мое приложение не полностью основано на SwiftUI, поэтому я не могу легко установить свойства в моих root представлениях, к которым я мог бы получить доступ в моем представлении загрузки - мне нужно, чтобы это представление было центрировано независимо от того, как выглядит иерархия представлений (смешанные родительские представления UIKit - SwiftUI). Вот почему ответы типа SwiftUI set position to center of different view не работают для моего варианта использования, поскольку в этом примере вам нужно изменить представление, в котором вы хотите центрировать свое дочернее представление.

Я пробовал поиграть с функциями .offset и .position из View, однако мне не удалось получить правильные входные данные, чтобы всегда динамически центрировать мой 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)
            }
        }
    }
}

Вот как выглядит результат: current (incorrect) UI

This is how the UI should look like: желаемый UI

Я попытался изменить body из CenteredLoadingView, используя GeometryReader и .frame(in: .global), чтобы получить глобальный размер экрана, но я добился того, что теперь мой 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)
    }
}

1 Ответ

1 голос
/ 05 августа 2020

Вот демонстрация возможного подхода. Идея состоит в том, чтобы использовать внедренный UIView для доступа к UIWindow, а затем показать представление загрузки как вид сверху окна root viewcontroller.

Протестировано с Xcode 12 / iOS 14 (но совместимо с SwiftUI 1.0)

введите описание изображения здесь

Примечание: анимация, эффекты и т. Д. c. возможны, но выходят за рамки простоты

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