1

I am trying to follow the design created for an app which has some objects placed in the middle of the screen.

The objects should have a size and padding proportional to the device's screen size, meaning they should appear bigger if the screen is bigger than the screen we take as a base in the design (the base is an iPhone 11 screen in this case). In addition, these objects have more objects inside, which should also be proportional to the screen size. For example: a Text view placed whithin the borders of a RoundedRectangle for which the font should grow if the screen is bigger than the screen used as a base; or an image inside another image. In these examples, the object and the objects inside of it should all be proportional to the screen size.

So far, we are using GeometryReader to accomplish this. The way we are doing it needs us to use GeometryReader in each file we have defined for a screen and its views. Once we have GeometryReader data, we use the Scale struct to get the correct proportions for the objects.

Here is the sample code:

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

The question / request

I would like to simplify this code and store the GeometryReader data (the Scale struct with its info) in an ObservedObject or an EnvironmentObject so that we can use it in different views and files all over the project. The problem with this is that we cannot get GeometryReader data until the view is loaded, and once the view is loaded I believe we cannot declare ObservedObject or EnvironmentObject anymore (is that correct?).

I know there could be a way to get the screen size without using GeometryReader as shown here: How to get the iPhone's screen width in SwiftUI?. But if I used GeometryReader to get the size of a view that is inside another view, I would like to have its information stored as well.

The goal would be not to use this code inside each view that needs to use scale:

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

and instead use ObservedObject or EnvironmentObject to get the scale data from the views that need it. Therefore, how can I use ObservedObject or EnvironmentObject to store GeometryReader data?

Tomas
  • 153
  • 1
  • 1
  • 12

1 Answers1

2

I tend to think that you're fighting the general principals of SwiftUI a little by doing this (ie basing things on screen sizes rather than using the built-in SwiftUI layout principals that are screen size independent like padding). Assuming you want to go forward with the plan, though, I'd recommend using an @Envrionment value. I don't think it needs to be an @EnvironmentObject, since Scale is a struct and there's no compelling reason to have a reference-type to box the value.

Here's a simple example:

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))")
    }
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Thank you for your reply! This solution works for me. And regarding the principles of SwiftUI and how this defeats them, how would you do in SwiftUI if given a base screen size you had to make a design with a rectangular box (of a given size) with text (of a given font size, let us say 20) inside. If we have a device with a bigger screen size than the base's screen, how could we do to increase the size of the objects inside proportionaly? If these objects are not increased, the design of the screen would not be respected for all screen sizes. – Tomas Sep 17 '21 at 12:55
  • In regards to the box, I'd use `padding` to size it relative to its surroundings. You're probably right that for the text size, you'd have to use the scale. But, I'd personally try to avoid that, as it will break a user's accessibility settings for font sizes. – jnpdx Sep 17 '21 at 17:02
  • You can accept the answer using the green checkmark. If you found it helpful, you can also consider using the arrow to upvote it. – jnpdx Sep 17 '21 at 17:03
  • Thanks! Accepted! I think for the text we will end up using scaling. It is not ideal, but the designs we were given by graphic designers were like that. – Tomas Sep 17 '21 at 18:06