iosswiftswiftuigaugegeometryreader

Swifui: How to use GeometryReader to be able to scale this view to fit any size parent?


This nested ring UI works well but how can I code it so it scales whether its parent is very small or very large?

import SwiftUI

struct CustomGaugeStyleView: View {
    
    @State private var innerRingFill = 6.5
   
      var body: some View {
          Gauge(value: innerRingFill, in: 0...10) {
              Image(systemName: "gauge.medium")
                  .font(.system(size: 50.0))
          } currentValueLabel: {
              Text("\(innerRingFill.formatted(.number))")
   
          }
          .gaugeStyle(twoRingGaugeStyle(outerRingMin: 5.5, outerRingMax: 7.5))
         
      }
}

struct CustomGaugeStyleView_Previews: PreviewProvider {
    static var previews: some View {
        CustomGaugeStyleView()
    }
}



struct twoRingGaugeStyle: GaugeStyle {
    
    var outerRingMin: Double
    var outerRingMax: Double
    
    func makeBody(configuration: Configuration) -> some View {
        
        GeometryReader { geometry in
            
            ZStack {
                Circle()
                    .stroke(Color(.lightGray).opacity(0.2), style: StrokeStyle(lineWidth: 20))
                    .frame(height: geometry.size.height * 0.70)
                Circle()
                    .trim(from: 0, to: 0.75 * configuration.value)
                    .stroke(Color.orange.gradient, style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                    .rotationEffect(.degrees(270))
                    .frame(height: geometry.size.height * 0.70)
                Circle()
                    .trim(from: outerRingMin / 10, to: outerRingMax / 10)
                    .stroke(Color.green.gradient, style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
                    .rotationEffect(.degrees(270))
                   .frame(height: geometry.size.height * 0.82)
            }
            .padding()
        }
        .aspectRatio(contentMode: .fit)
    }
    
}

the first image is the view without any frame size, the second view is with adding .frame(height: 100) to the Gauge.

without a frame size

with a frame height of 100


Solution

  • As @loremipsum mentioned in the comments, if you want this UI to scale for any screen size, then your lineWidth needs to be a percentage.

    Here is an example implementation:

    struct CustomGuageStyleView: View {
        
        @State private var innerRingFill = 6.5
       
          var body: some View {
              Gauge(value: innerRingFill, in: 0...10) {
                  Image(systemName: "gauge.medium")
                      .font(.system(size: 50.0))
              } currentValueLabel: {
                  Text("\(innerRingFill.formatted(.number))")
       
              }
              .gaugeStyle(twoRingGaugeStyle(outerRingMin: 5.5, outerRingMax: 7.5))
             
          }
    }
    
    struct twoRingGaugeStyle: GaugeStyle {
        
        var outerRingMin: Double
        var outerRingMax: Double
        
        //This is not strictly necessary but it gives you an option
        var multiplierAmount: Double = 0.045
        
        func makeBody(configuration: Configuration) -> some View {
            
            GeometryReader { geometry in
                
                ZStack {
                    Circle()
                        .stroke(Color(.lightGray).opacity(0.2), style: StrokeStyle(lineWidth: geometry.size.width * multiplierAmount)) ///<<<--- HERE!! We are now making this variable.
                        .frame(height: geometry.size.height * 0.70)
                    Circle()
                        .trim(from: 0, to: 0.75 * configuration.value)
                        .stroke(Color.orange.gradient, style: StrokeStyle(lineWidth: geometry.size.width * multiplierAmount, lineCap: .round, lineJoin: .round)) ///<<<--- HERE too.
                        .rotationEffect(.degrees(270))
                        .frame(height: geometry.size.height * 0.70)
                    Circle()
                        .trim(from: outerRingMin / 10, to: outerRingMax / 10)
                        .stroke(Color.green.gradient, style: StrokeStyle(lineWidth: geometry.size.width * multiplierAmount, lineCap: .round, lineJoin: .round)) ///<<<--- HERE!! We are now making this variable.
                        .rotationEffect(.degrees(270))
                        .frame(height: geometry.size.height * 0.78) // <<<--- Here, I changed the value to 0.75 - this will make the rings slightly closer together, which works better with the new scaling.
                    //- NOTE: I might add a `minHeight` above, or the circle will end up eventually having not enough of a change between the inner value to appear separated.
                }
                .padding()
            }
            .aspectRatio(contentMode: .fit)
        }
        
    }
    

    Explanation

    There are only a few changes to the code here, and all of them are inside the twoRingGuageStyle.

    I might add a minHeight above, or the circle will end up eventually having not enough of a change between the inner value to appear separated.

    Otherwise, at very small screen sizes, your circles will appear to not be spaced out enough.

    Screenshots

    The circles at a larger size.

    The circles at a smaller size.

    Note

    This code was tested with Xcode 14.2 and macOS 13.1. It may require minute adjustments of the values for the UI to be of the exact look needed.