Search code examples
swiftswiftuiscaling

SwiftUI: Scaling a Shape in a View


I have a SubView perfectly designed for 320x320 :). I want to let it scale down to a minimum of 100x100. Now i am using a scaling factor in each Shape. Question: is there a more generic way of scaling Shapes (dimension, lines width and length and rectangles width and height) in a reusable SwiftUI View? Thank you.

View:

enter image description here

Code:

import SwiftUI

private struct SecondsFace: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let square = CGRect(x: rect.minX, y: rect.minY, width: min(rect.maxX-rect.minX,rect.maxY-rect.minY), height: min(rect.maxX-rect.minX,rect.maxY-rect.minY))
        let scaleFactor = CGFloat(square.width / 320.0)
        var seconds : Path = Path()
        seconds.addLines([CGPoint(x: (320.0/2.0-14.0)*scaleFactor, y: 0),CGPoint(x: (320.0/2-4.0)*scaleFactor, y: 0)])
        for i in 0...59 {
            path.addPath(seconds, transform: .init(rotationAngle: 2.0 * CGFloat.pi * CGFloat(i)/60.0))
        }
        return path.offsetBy(dx: (320.0/2.0)*scaleFactor, dy: (320.0/2.0)*scaleFactor)
    }
}

private struct HoursFace: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let square = CGRect(x: rect.minX, y: rect.minY, width: min(rect.maxX-rect.minX,rect.maxY-rect.minY), height: min(rect.maxX-rect.minX,rect.maxY-rect.minY))
        let scaleFactor = CGFloat(square.width / 320.0)
        var seconds : Path = Path()
        seconds.addLines([CGPoint(x: (320.0/2.0-16)*scaleFactor, y: 0),CGPoint(x: (320.0/2.0-2.0)*scaleFactor, y: 0)])
        for i in 0...11 {
            path.addPath(seconds, transform: .init(rotationAngle: 2.0 * CGFloat.pi * CGFloat(i)/12.0))
        }
        return path.offsetBy(dx: (320.0/2.0)*scaleFactor, dy: (320.0/2.0)*scaleFactor)
    }
}

struct ThreeHoursFace: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let square = CGRect(x: rect.minX, y: rect.minY, width: min(rect.maxX-rect.minX,rect.maxY-rect.minY), height: min(rect.maxX-rect.minX,rect.maxY-rect.minY))
        let scaleFactor = CGFloat(square.width / 320.0)
        let threehours : Path = Path(CGRect(x: (320.0/2.0-20.0)*scaleFactor, y: -2.0*scaleFactor, width: 20.0*scaleFactor, height: 4.0*scaleFactor))
        for i in 0...3 {
            path.addPath(threehours, transform: .init(rotationAngle: 2.0 * CGFloat.pi * CGFloat(i)/4.0))
            }
        return path.offsetBy(dx: (320.0/2.0)*scaleFactor, dy: (320.0/2.0)*scaleFactor)
    }
}

struct SubView: View {
    @Binding var color : ColorValue

    var body: some View {
        ZStack {
            SecondsFace()
            .stroke(Color.green, style: StrokeStyle(lineWidth: 1, lineCap: .square, lineJoin: .bevel))
            HoursFace()
            .stroke(Color.red, style: StrokeStyle(lineWidth: 3, lineCap: .square, lineJoin: .bevel))
            ThreeHoursFace()
            .fill(Color.blue)
        }
        .aspectRatio(1, contentMode: .fit)
        .clipped(antialiased: true)
        .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0))
    }
 }

struct SubViewPreview: PreviewProvider {

    @State static var currentColor : ColorValue = ColorValue(color: UIColor.red)

    static var previews: some View {
        SubView(color: $currentColor)
    }
}

Reused Views with scaling:

(There is still a small issue with scaling of linewidths ...)

enter image description here

Code:

import Foundation
import SwiftUI

struct MainView: View {

    @State var color = ColorValue(color: UIColor.red)

    var body: some View {
        VStack {
            HStack(spacing: 10) {
                SubView(color: $color)
            }
            .padding(10)
            HStack(spacing: 10) {
                SubView(color: $color)
                SubView(color: $color)
            }
            .padding(10)
            HStack(spacing: 10) {
                SubView(color: $color)
                SubView(color: $color)
                SubView(color: $color)
            }
            .padding(10)
        }
    }

}

struct ShapeView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}

Solution

  • I am using now applying(CGAffineTransform(a:, b: , c: , d:, tx: , ty: )) on the result path in Shape.

    Code:

    import SwiftUI
    
    private struct SecondsFace: Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            let seconds : Path = Path(CGRect(x: (320.0/2.0-14.0), y: -1.0, width: 10.0, height: 2.0))
            for i in 0...59 {
                path.addPath(seconds, transform: .init(rotationAngle: 2.0 * CGFloat.pi * CGFloat(i)/60.0))
            }
            let scaleFactor = CGFloat(min(rect.maxX-rect.minX,rect.maxY-rect.minY) / 320.0)
            return path.applying(CGAffineTransform(a: scaleFactor, b: 0.0, c: 0.0, d: scaleFactor, tx: (320.0/2.0)*scaleFactor, ty: (320.0/2.0)*scaleFactor))
        }
    }
    
    private struct HoursFace: Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            let seconds : Path = Path(CGRect(x: (320.0/2.0-16.0), y: -2.0, width: 14.0, height: 4.0))
            for i in 0...11 {
                path.addPath(seconds, transform: .init(rotationAngle: 2.0 * CGFloat.pi * CGFloat(i)/12.0))
            }
            let scaleFactor = CGFloat(min(rect.maxX-rect.minX,rect.maxY-rect.minY) / 320.0)
            return path.applying(CGAffineTransform(a: scaleFactor, b: 0.0, c: 0.0, d: scaleFactor, tx: (320.0/2.0)*scaleFactor, ty: (320.0/2.0)*scaleFactor))
        }
    }
    
    struct ThreeHoursFace: Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            let threehours : Path = Path(CGRect(x: (320.0/2.0-24.0), y: -4.0, width: 24.0, height: 8.0))
            for i in 0...3 {
                path.addPath(threehours, transform: .init(rotationAngle: 2.0 * CGFloat.pi * CGFloat(i)/4.0))
            }
            let scaleFactor = CGFloat(min(rect.maxX-rect.minX,rect.maxY-rect.minY) / 320.0)
            return path.applying(CGAffineTransform(a: scaleFactor, b: 0.0, c: 0.0, d: scaleFactor, tx: (320.0/2.0)*scaleFactor, ty: (320.0/2.0)*scaleFactor))
        }
    }
    
    struct SubView: View {
        @Binding var color : ColorValue
    
        var body: some View {
            ZStack {
                SecondsFace()
                .fill(Color.green)
                HoursFace()
                .fill(Color.red)
                ThreeHoursFace()
                .fill(Color.blue)
            }
            .aspectRatio(1, contentMode: .fit)
            .clipped(antialiased: true)
            .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 1.0))
        }
     }
    
    struct SubViewPreview: PreviewProvider {
    
        @State static var currentColor : ColorValue = ColorValue(color: UIColor.red)
    
        static var previews: some View {
            SubView(color: $currentColor)
        }
    }
    

    Look:

    enter image description here