skip to Main Content

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

2

Answers


  1. 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()
    }
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search