0

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

GarySabo
  • 5,806
  • 5
  • 49
  • 124

2 Answers2

1

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.

  • On every lineWidth inside the StrokeStyle, I changed the value from 20 to geometry.size.width * multiplierAmount. This makes the line width also scale with the GeometryReader.
  • I added an optional variable, multiplierAmount, to the top of the twoRingGuageStyle. This allows you to optionally configure the Circle width. The default value is 0.045.
  • While I left the frame on the outer Circle at height: geometry.size.height * 0.82, as I mentioned in the comment on that line,

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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
0

I've run your code myself, and I've checked that the raw value of the green Circle() is incorrect.

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.70)  // 0.82 -> 0.70
    }
    .padding()
}
.aspectRatio(contentMode: .fit)

And, as @lorem ipsum said, to solve overlapping problems, you need to modify the code so that lineWidth: works as percentage.

GeometryReader { geometry in
    ZStack {
        Circle()
            .stroke(Color(.lightGray).opacity(0.2), style: StrokeStyle(lineWidth: geometry.size.height * 0.10))
            .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.height * 0.10, 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: geometry.size.height * 0.10, lineCap: .round, lineJoin: .round))
            .rotationEffect(.degrees(270))
            .frame(height: geometry.size.height * 0.70)
    }
    .padding()
}
Cheolhyun
  • 169
  • 1
  • 7
  • Thanks sorry I wasn't clear top picture is how I would like it to look, i.e. the green ring outside of the inner orange ring, but when it scales down it overlaps as in the bottom picture. – GarySabo Mar 14 '23 at 02:22
  • I'm sorry. I totally misunderstood. I revised the original text, so please check it again. – Cheolhyun Mar 14 '23 at 02:42