在顶层视图中居中SwiftUI视图 [英] Center SwiftUI view in top-level view
问题描述
我正在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屋!