SwiftUI - 如何使用 ObservedObject 或 EnvironmentObject 来存储 GeometryReader 数据?

SwiftUI - How can I use ObservedObject or EnvironmentObject to store GeometryReader data?

我正在尝试遵循为应用程序创建的设计,该应用程序在屏幕中间放置了一些对象。

对象的大小和填充应与设备的屏幕大小成比例,这意味着如果屏幕大于我们在设计中作为基础的屏幕(基础是 iPhone 11屏幕在这种情况下)。另外,这些对象里面的对象比较多,也应该和屏幕大小成正比。例如:放置在 RoundedRectangle 边界内的文本视图,如果屏幕大于用作基础的屏幕,则字体应该增大;或另一个图像中的图像。在这些示例中,对象及其内部的对象都应与屏幕大小成比例。

到目前为止,我们正在使用 GeometryReader 来完成此操作。我们这样做的方式需要我们在为屏幕及其视图定义的每个文件中使用 GeometryReader。一旦我们有了 GeometryReader 数据,我们就使用 Scale 结构来获得对象的正确比例。

示例代码如下:

GeometryReaderSampleView.swift

import SwiftUI

struct GeometryReaderSampleView: View {
    var body: some View {
        NavigationView {
            GeometryReader { metrics in
                ZStack {
                    VStack {
                        LoginMainDecorationView(Scale(geometry: metrics))
                        Spacer()
                    }
                    
                    VStack {
                        HStack {
                            GreenSquareView(Scale(geometry: metrics))
                            Spacer()
                        }
                        .offset(x: 29, y: Scale(geometry: metrics).vertical(300.0))
                        Spacer()
                    }
                }
            }
        }
    }
}

struct GreenSquareView: View {
    let scale:Scale
    
    init (_ scale:Scale) {
        self.scale = scale
    }
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            RoundedRectangle(cornerRadius: scale.horizontal(30))
                .fill(Color.green)
                .frame(width: scale.horizontal(157), height: scale.horizontal(146))
            
            Text("Here goes\nsome text")
                .font(.custom("TimesNewRomanPS-ItalicMT", size: scale.horizontal(20)))
                .padding(.top, scale.horizontal(29))
                .padding(.leading, scale.horizontal(19))
            
            VStack {
                Spacer()
                HStack {
                    Spacer()
                    Image(systemName: "heart.circle")
                        .resizable()
                        .frame(width: scale.horizontal(20), height: scale.horizontal(20))
                        .offset(x: scale.horizontal(-20), y: scale.vertical(-17.0))
                }
            }.frame(width: scale.horizontal(157), height: scale.horizontal(146))
        }
    }
}

struct LoginMainDecorationView: View {
    let scale:Scale
    
    init (_ scale:Scale) {
        self.scale = scale
    }
    
    var body: some View {
            HStack {
                Image(systemName: "cloud.rain")
                    .resizable()
                    .frame(width: scale.horizontal(84), height: scale.horizontal(68), alignment: .leading)
                    .offset(x: 0, y: scale.vertical(200.0))
                Spacer()
                Image(systemName: "cloud.snow")
                    .resizable()
                    .frame(width: scale.horizontal(119), height: scale.horizontal(91), alignment: .trailing)
                    .offset(x: scale.horizontal(-20.0), y: scale.vertical(330.0))
            }
    }
}

struct GeometryReaderSampleView_Previews: PreviewProvider {
    static var previews: some View {
        GeometryReaderSampleView()
    }
}

Scale.swift

import SwiftUI

struct Scale {
    // Size of iPhone 11 Pro
    let originalWidth:CGFloat = 375.0
    let originalHeight:CGFloat = 734.0
    
    let horizontalProportion:CGFloat
    let verticalProportion:CGFloat
    
    init(screenWidth:CGFloat, screenHeight:CGFloat) {
        horizontalProportion =  screenWidth / originalWidth
        verticalProportion = screenHeight / originalHeight
    }
    
    init(geometry: GeometryProxy) {
        self.init(screenWidth: geometry.size.width, screenHeight: geometry.size.height)
    }
    
    func horizontal(_ value:CGFloat) -> CGFloat {
        return value * horizontalProportion
    }
    
    func vertical(_ value:CGFloat) -> CGFloat {
        return value * verticalProportion
    }
}

问题/要求

我想简化此代码并将 GeometryReader 数据(Scale 结构及其信息)存储在 ObservedObject 或 EnvironmentObject 中,以便我们可以在整个项目的不同视图和文件中使用它。这样做的问题是,在加载视图之前我们无法获取 GeometryReader 数据,一旦加载视图,我相信我们不能再声明 ObservedObject 或 EnvironmentObject(正确吗?)。

我知道有一种方法可以在不使用 GeometryReader 的情况下获取屏幕尺寸,如下所示:How to get the iPhone's screen width in SwiftUI?。但是,如果我使用 GeometryReader 获取另一个视图内的视图的大小,我也希望存储它的信息。

目标是不在每个需要使用比例的视图中使用此代码:

let scale:Scale
        
init (_ scale:Scale) {
    self.scale = scale
}

而是使用 ObservedObject 或 EnvironmentObject 从需要它的视图中获取比例数据。那么,如何使用 ObservedObject 或 EnvironmentObject 来存储 GeometryReader 数据呢?

我倾向于认为你通过这样做有点与 SwiftUI 的一般原则作斗争(即基于屏幕大小而不是使用与屏幕大小无关的内置 SwiftUI 布局原则 padding).不过,假设您想继续执行该计划,我建议您使用 @Envrionment 值。我认为它不需要是一个 @EnvironmentObject,因为 Scale 是一个 struct,并且没有令人信服的理由让引用类型来装箱值。

这是一个简单的例子:

private struct ScaleKey: EnvironmentKey {
  static let defaultValue = Scale(screenWidth: -1, screenHeight: -1)
}

extension EnvironmentValues {
  var scale: Scale {
    get { self[ScaleKey.self] }
    set { self[ScaleKey.self] = newValue }
  }
}

struct ContentView: View {
    
    var body: some View {
        GeometryReader { metrics in
            SubView()
                .environment(\.scale, Scale(geometry: metrics))
        }
    }
}

struct SubView : View {
    @Environment(\.scale) private var scale : Scale
    
    var body: some View {
        Text("Scale: \(scale.horizontal(1)) x \(scale.vertical(1))")
    }
}