6

I'm trying to create a resizable button that has an evenly dotted circle border around it. If you simply put:

Circle()
  .stroke(color, style: StrokeStyle(lineWidth: 3, lineCap: .butt, dash: [3, radius / 3.82]))
  .frame(width: radius * 2, height: radius * 2)

There might be an uneven distribution of dots as you can see in the below image: enter image description here

Here's a related question with a solution which I tried adapting from UIKit into a SwiftUI struct but also failed.

Can someone please help me either find a way to adapt that "dash" value to create an evenly dashed stroked border with dependence on the radius OR create a custom shape instead?

Nimantha
  • 6,405
  • 6
  • 28
  • 69
danylo.net
  • 253
  • 3
  • 7
  • Does this answer your question? [How to draw an arc with SwiftUI?](https://stackoverflow.com/questions/57034383/how-to-draw-an-arc-with-swiftui) – lorem ipsum Jul 01 '21 at 22:52
  • Thanks for your suggestion, I just tried it but unfortunately the .strokedPath function has that same issue that I'm trying to avoid :/ – danylo.net Jul 01 '21 at 23:16

2 Answers2

11

I have an answer in pure SwiftUI. I think the issue you are running into is simply that you are drawing for part and skipping for part, and you have to take both into account.

Therefore, you need to come up with the circumference, divide it into the segments of 1 drawn part + 1 undrawn part. The drawn part is simply what looks good to you, so the undrawn part is the segment less the drawn part. You then plug both of those values into the StrokeStyle.dash and you get evenly spaced dots.

import SwiftUI

struct ContentView: View {
    let radius: CGFloat = 100
    let pi = Double.pi
    let dotCount = 10
    let dotLength: CGFloat = 3
    let spaceLength: CGFloat

    init() {
        let circumerence: CGFloat = CGFloat(2.0 * pi) * radius
        spaceLength = circumerence / CGFloat(dotCount) - dotLength
    }
    
    var body: some View {
        Circle()
            .stroke(Color.blue, style: StrokeStyle(lineWidth: 2, lineCap: .butt, lineJoin: .miter, miterLimit: 0, dash: [dotLength, spaceLength], dashPhase: 0))
            .frame(width: radius * 2, height: radius * 2)
    }
}
Yrb
  • 8,103
  • 2
  • 14
  • 44
  • 1
    HUZZAH! Thank you so much, your solution is very straightforward. It took me a second to understand that circumference/dotCount creates the distance that each dot has around it and subtracting the size of the dot itself creates the leftover space. 'Tis so satisfying to see such perfect proportions. – danylo.net Jul 02 '21 at 01:36
  • 2
    It was a fun little exercise. Intrinsically, I knew the problem, but I couldn't state it directly. For a while there, I thought I would need something more than the simple circumference to solve it. It was late, and I hadn't eaten dinner yet. I had dinner, and it was a lot easier to finish up. – Yrb Jul 02 '21 at 01:56
  • amazing solution!!! – Chris Dec 12 '21 at 17:20
1

using some of the code in circle with dash lines uiview

I got this test code working for Swiftui:

    import SwiftUI
    
    @main
    struct TestApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    struct ContentView: View {

    @State var color = Color.blue
    @State var radius = CGFloat(128)
    @State var painted = CGFloat(6)
    @State var unpainted = CGFloat(6)
    
    let count: CGFloat = 30
    let relativeDashLength: CGFloat = 0.25

    var body: some View {
        Circle()
            .stroke(color, style: StrokeStyle(lineWidth: 3, lineCap: .butt, dash: [painted, unpainted]))
          .frame(width: radius * 2, height: radius * 2)
          .onAppear {
              let dashLength = CGFloat(2 * .pi * radius) / count
              painted = dashLength * relativeDashLength
              unpainted = dashLength * (1 - relativeDashLength)
          }
    }
}