Home > Enterprise >  How can I scale my ticks proportionally in view? Swiftui
How can I scale my ticks proportionally in view? Swiftui

Time:11-14

I would like to make my view scalable. However, when I change the size, the size of the ticks remain. The ticks should also scale down to fit the circle proportionally.

Is there a best practice approach here?

this is how it looks in large

and here in small

as we can see, the ticks remain the same size.

here is my code which I have used:

struct TestView: View {

var body: some View {
    
    GeometryReader { geometry in
        ZStack {
            Circle()
                .fill(Color.gray)
            
                ForEach(0..<60*4) { tick in
                    Ticks.tick(at: tick)
            }
        }
    }.frame(height: 100)
    }
}

struct Ticks{
    static func tick(at tick: Int) -> some View {
        VStack {
            Rectangle()
                .fill(Color.primary)
                .opacity(tick % 20 == 0 ? 1 : 0.4)
                .frame(width: 2, height: tick % 4 == 0 ? 15 : 7)
            Spacer()
    }.rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
}
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}

Thanks!


Additional code:

struct Hand: Shape {
    let inset: CGFloat
    let angle: Angle
    
    func path(in rect: CGRect) -> Path {
        let rect = rect.insetBy(dx: inset, dy: inset)
        var p = Path()
        p.move(to: CGPoint(x: rect.midX, y: rect.midY))
        p.addLine(to: position(for: CGFloat(angle.radians), in: rect))
        return p
    }
    
    private func position(for angle: CGFloat, in rect: CGRect) -> CGPoint {
        let angle = angle - (.pi/2)
        let radius = min(rect.width, rect.height)/2
        let xPos = rect.midX   (radius * cos(angle))
        let yPos = rect.midY   (radius * sin(angle))
        return CGPoint(x: xPos, y: yPos)
    }
}

struct TickHands: View {
    @State private var dateTime = Date()

    private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    
    var body: some View {
        ZStack {
            Hand(inset: 105, angle: dateTime.hourAngle)
                .stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
            Hand(inset: 70, angle: dateTime.minuteAngle)
                .stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
            Hand(inset: 40, angle: dateTime.secondAngle)
                .stroke(Color.orange, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
            Circle().fill(Color.orange).frame(width: 10)
        }
        .onReceive(timer) { (input) in
            self.dateTime = input
        }
    }
}

extension Date {
    var hourAngle: Angle {
        return Angle (degrees: (360 / 12) * (self.hour   self.minutes / 60))
    }
    var minuteAngle: Angle {
        return Angle(degrees: (self.minutes * 360 / 60))
    }
    var secondAngle: Angle {
        return Angle (degrees: (self.seconds * 360 / 60))
    }
}

extension Date {
    var hour: Double {
        return Double(Calendar.current.component(.hour, from: self))
    }
    var minutes: Double {
        return Double(Calendar.current.component(.minute, from: self))
    }
    var seconds: Double {
        return Double(Calendar.current.component(.second, from: self))
    }
}


struct TestView: View {
    var clockSize: CGFloat = 400
    
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                TickHands()
                ForEach(0..<60*4) { tick in
                    Ticks.tick(at: tick, scale: geometry.size.height / 200)
                }
            }
        }.frame(width: clockSize, height: clockSize)
    }
}

struct Ticks{
    static func tick(at tick: Int, scale: CGFloat) -> some View {
        VStack {
            Rectangle()
                .fill(Color.primary)
                .opacity(tick % 20 == 0 ? 1 : 0.4)
                .frame(width: 2 * scale, height: (tick % 4 == 0 ? 15 : 7) * scale)
            Spacer()
        }
        .rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
    }
}

CodePudding user response:

You can pass along a scale factor based on the GeometryReader that you already have in place.

struct ContentView: View {
    var body: some View {
        TestView()
    }
}

struct Hand: Shape {
    let inset: CGFloat
    let angle: Angle
    
    func path(in rect: CGRect) -> Path {
        let rect = rect.insetBy(dx: inset, dy: inset)
        var p = Path()
        p.move(to: CGPoint(x: rect.midX, y: rect.midY))
        p.addLine(to: position(for: CGFloat(angle.radians), in: rect))
        return p
    }
    
    private func position(for angle: CGFloat, in rect: CGRect) -> CGPoint {
        let angle = angle - (.pi/2)
        let radius = min(rect.width, rect.height)/2
        let xPos = rect.midX   (radius * cos(angle))
        let yPos = rect.midY   (radius * sin(angle))
        return CGPoint(x: xPos, y: yPos)
    }
}

struct TickHands: View {
    var scale: Double
    @State private var dateTime = Date()
    private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    
    var body: some View {
        ZStack {
            Hand(inset: 105 * scale, angle: dateTime.hourAngle)
                .stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
            Hand(inset: 70 * scale, angle: dateTime.minuteAngle)
                .stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
            Hand(inset: 40 * scale, angle: dateTime.secondAngle)
                .stroke(Color.orange, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
            Circle().fill(Color.orange).frame(width: 10)
        }
        .onReceive(timer) { (input) in
            self.dateTime = input
        }
    }
}

extension Date {
    var hourAngle: Angle {
        return Angle (degrees: (360 / 12) * (self.hour   self.minutes / 60))
    }
    var minuteAngle: Angle {
        return Angle(degrees: (self.minutes * 360 / 60))
    }
    var secondAngle: Angle {
        return Angle (degrees: (self.seconds * 360 / 60))
    }
}

extension Date {
    var hour: Double {
        return Double(Calendar.current.component(.hour, from: self))
    }
    var minutes: Double {
        return Double(Calendar.current.component(.minute, from: self))
    }
    var seconds: Double {
        return Double(Calendar.current.component(.second, from: self))
    }
}


struct TestView: View {
    var clockSize: CGFloat = 500
    
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                let scale = geometry.size.height / 200
                TickHands(scale: scale)
                ForEach(0..<60*4) { tick in
                    Ticks.tick(at: tick, scale: scale)
                }
            }
        }.frame(width: clockSize, height: clockSize)
    }
}

struct Ticks{
    static func tick(at tick: Int, scale: CGFloat) -> some View {
        VStack {
            Rectangle()
                .fill(Color.primary)
                .opacity(tick % 20 == 0 ? 1 : 0.4)
                .frame(width: 2 * scale, height: (tick % 5 == 0 ? 15 : 7) * scale)
            Spacer()
        }
        .rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
    }
}

Note that you may want to change how the scale effect changes the width vs the height -- for example, maybe you want the width to always be 2. Or, perhaps you want to use something like min(1, 2 * scale) to prevent the ticks from going above or below a certain size. But, the principal will be the same (ie using the scale factor). You can also adjust the 200 that I have to something that fits your ideal scaling algorithm.

  • Related