SwiftUI: How to draw filled and stroked shape?

SwiftSwiftui

Swift Problem Overview


In UIKit drawing a stroked and filled path/shape is pretty easy.

Eg, the code below draws a red circle that is stroked in blue.

override func draw(_ rect: CGRect) {
    guard let ctx = UIGraphicsGetCurrentContext() else { return }
        
    let center = CGPoint(x: rect.midX, y: rect.midY)

    ctx.setFillColor(UIColor.red.cgColor)
    ctx.setStrokeColor(UIColor.blue.cgColor)
        
    let arc = UIBezierPath(arcCenter: center, radius: rect.width/2, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
        
    arc.stroke()
    arc.fill()
}

How does one do this with SwiftUI?

Swift UI seems to support:

Circle().stroke(Color.blue)
// and/or
Circle().fill(Color.red)

but not

Circle().fill(Color.red).stroke(Color.blue) // Value of type 'ShapeView<StrokedShape<Circle>, Color>' has no member 'fill'
// or 
Circle().stroke(Color.blue).fill(Color.red) // Value of type 'ShapeView<Circle, Color>' has no member 'stroke'

Am I supposed to just ZStack two circles? That seems a bit silly.

Swift Solutions


Solution 1 - Swift

You can also use strokeBorder and background in combination.

Code:

Circle()
    .strokeBorder(Color.blue,lineWidth: 4)
    .background(Circle().foregroundColor(Color.red))

Result:

https://i.stack.imgur.com/dZfXs.png" height="500">

Solution 2 - Swift

You can draw a circle with a stroke border

struct ContentView: View {
    var body: some View {
        Circle()
            .strokeBorder(Color.green,lineWidth: 3)
            .background(Circle().foregroundColor(Color.red))
   }
}

circle with stroked border

Solution 3 - Swift

My workaround:

import SwiftUI

extension Shape {
    /// fills and strokes a shape
    public func fill<S:ShapeStyle>(
        _ fillContent: S, 
        stroke       : StrokeStyle
    ) -> some View {
        ZStack {
            self.fill(fillContent)
            self.stroke(style:stroke)
        }
    }
}

Example:


struct ContentView: View {
    // fill gradient
    let gradient = RadialGradient(
        gradient   : Gradient(colors: [.yellow, .red]), 
        center     : UnitPoint(x: 0.25, y: 0.25), 
        startRadius: 0.2, 
        endRadius  : 200
    )
    // stroke line width, dash
    let w: CGFloat   = 6       
    let d: [CGFloat] = [20,10]
    // view body
    var body: some View {
        HStack {
            Circle()
                // ⭐️ Shape.fill(_:stroke:)
                .fill(Color.red, stroke: StrokeStyle(lineWidth:w, dash:d))
            Circle()
                .fill(gradient, stroke: StrokeStyle(lineWidth:w, dash:d))
        }.padding().frame(height: 300)
    }
}

Result:

fill and stroke circles

Solution 4 - Swift

Seems like it's either ZStack or .overlay at the moment.

The view hierarchy is almost identical - according to Xcode.

struct ContentView: View {

    var body: some View {

        VStack {
            Circle().fill(Color.red)
                .overlay(Circle().stroke(Color.blue))
            ZStack {
                 Circle().fill(Color.red)
                 Circle().stroke(Color.blue)
            }
        }

    }

}

Output:

enter image description here


View hierarchy:

enter image description here

Solution 5 - Swift

For future reference, @Imran's solution works, but you also need to account for stroke width in your total frame by padding:

struct Foo: View {
    private let lineWidth: CGFloat = 12
    var body: some View {
        Circle()
            .stroke(Color.purple, lineWidth: self.lineWidth)
        .overlay(
            Circle()
                .fill(Color.yellow)
        )
        .padding(self.lineWidth)
    }
}

enter image description here

Solution 6 - Swift

I put the following wrapper together based on the answers above. It makes this a bit more easy and the code a bit more simple to read.

struct FillAndStroke<Content:Shape> : View
{
  let fill : Color
  let stroke : Color
  let content : () -> Content
 
  init(fill : Color, stroke : Color, @ViewBuilder content : @escaping () -> Content)
  {
    self.fill = fill
    self.stroke = stroke
    self.content = content
  }
  
  var body : some View
  {
    ZStack
    {
      content().fill(self.fill)
      content().stroke(self.stroke)
    }
  }
}

It can be used like this:

FillAndStroke(fill : Color.red, stroke : Color.yellow)
{
  Circle()
}

Hopefully Apple will find a way to support both fill and stroke on a shape in the future.

Solution 7 - Swift

Another simpler option just stacking the stroke on top of the fill with the ZStack

    ZStack{
        Circle().fill()
            .foregroundColor(.red)
        Circle()
            .strokeBorder(Color.blue, lineWidth: 4)
    }

The result is

Solution 8 - Swift

If we want to have a circle with no moved border effect as we can see doing it by using ZStack { Circle().fill(), Circle().stroke }

I prepared something like below:

First step

We are creating a new Shape

struct CircleShape: Shape {
    
    // MARK: - Variables
    var radius: CGFloat
    
    func path(in rect: CGRect) -> Path {
        let centerX: CGFloat = rect.width / 2
        let centerY: CGFloat = rect.height / 2
        var path = Path()
        path.addArc(center: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: Angle(degrees: .zero)
            , endAngle: Angle(degrees: 360), clockwise: true)
        
        return path
    }
}

Second step

We are creating a new ButtonStyle

struct LikeButtonStyle: ButtonStyle {
        
        // MARK: Constants
        private struct Const {
            static let yHeartOffset: CGFloat = 1
            static let pressedScale: CGFloat = 0.8
            static let borderWidth: CGFloat = 1
        }
        
        // MARK: - Variables
        var radius: CGFloat
        var isSelected: Bool
        
        func makeBody(configuration: Self.Configuration) -> some View {
            ZStack {
                if isSelected {
                    CircleShape(radius: radius)
                        .stroke(Color.red)
                        .animation(.easeOut)
                }
                CircleShape(radius: radius - Const.borderWidth)
                    .fill(Color.white)
                configuration.label
                    .offset(x: .zero, y: Const.yHeartOffset)
                    .foregroundColor(Color.red)
                    .scaleEffect(configuration.isPressed ? Const.pressedScale : 1.0)
            }
        }
    }

Last step

We are creating a new View

struct LikeButtonView: View {
    
    // MARK: - Typealias
    typealias LikeButtonCompletion = (Bool) -> Void
    
    // MARK: - Constants
    private struct Const {
        static let selectedImage = Image(systemName: "heart.fill")
        static let unselectedImage = Image(systemName: "heart")
        static let textMultiplier: CGFloat = 0.57
        static var textSize: CGFloat { 30 * textMultiplier }
    }
    
    // MARK: - Variables
    @State var isSelected: Bool = false
    private var radius: CGFloat = 15.0
    private var completion: LikeButtonCompletion?
    
    init(isSelected: Bool, completion: LikeButtonCompletion? = nil) {
        _isSelected = State(initialValue: isSelected)
        self.completion = completion
    }
    
    var body: some View {
        ZStack {
            Button(action: {
                withAnimation {
                    self.isSelected.toggle()
                    self.completion?(self.isSelected)
                }
            }, label: {
                setIcon()
                    .font(Font.system(size: Const.textSize))
                
            })
                .buttonStyle(LikeButtonStyle(radius: radius, isSelected: isSelected))
        }
    }
    
    // MARK: - Private methods
    private func setIcon() -> some View {
        isSelected ? Const.selectedImage : Const.unselectedImage
    }
}

Output (Selected and unselected state):

enter image description here

enter image description here

Solution 9 - Swift

Building on the previous answer by lochiwei...

public func fill<S:ShapeStyle>(_ fillContent: S,
                                   opacity: Double,
                                   strokeWidth: CGFloat,
                                   strokeColor: S) -> some View
    {
        ZStack {
            self.fill(fillContent).opacity(opacity)
            self.stroke(strokeColor, lineWidth: strokeWidth)
        }
    }

Used on a Shape object:

struct SelectionIndicator : Shape {
    let parentWidth: CGFloat
    let parentHeight: CGFloat
    let radius: CGFloat
    let sectorAngle: Double
    
    
    func path(in rect: CGRect) -> Path { ... }
}

SelectionIndicator(parentWidth: g.size.width,
                        parentHeight: g.size.height,
                        radius: self.radius + 10,
                        sectorAngle: self.pathNodes[0].sectorAngle.degrees)
                    .fill(Color.yellow, opacity: 0.2, strokeWidth: 3, strokeColor: Color.white)

Solution 10 - Swift

my 2 cents for stroking and colouring the "flower sample from Apple (// https://developer.apple.com/documentation/quartzcore/cashapelayer) moved to SwiftUI

extension Shape {
    public func fill<Shape: ShapeStyle>(
        _ fillContent: Shape,
        strokeColor  : Color,
        lineWidth    : CGFloat

    ) -> some View {
        ZStack {
            self.fill(fillContent)
            self.stroke( strokeColor, lineWidth: lineWidth)

        }
    }

in my View:

struct CGFlower: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        let width = rect.width
        let height = rect.height

        stride(from: 0, to: CGFloat.pi * 2, by: CGFloat.pi / 6).forEach {
            angle in
            var transform  = CGAffineTransform(rotationAngle: angle)
                .concatenating(CGAffineTransform(translationX: width / 2, y: height / 2))
            
            let petal = CGPath(ellipseIn: CGRect(x: -20, y: 0, width: 40, height: 100),
                               transform: &transform)
            
            let p = Path(petal)
            path.addPath(p)
        }
        
        return path
        
    }
}

struct ContentView: View {
    var body: some View {
        
        CGFlower()
            .fill( .yellow, strokeColor: .red, lineWidth:  5 )
    }
}

img: enter image description here

Solution 11 - Swift

There are several ways to achieve "fill and stroke" result. Here are three of them:

struct ContentView: View {
    var body: some View {
        let shape = Circle()
        let gradient = LinearGradient(gradient: Gradient(colors: [.orange, .red, .blue, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing)
        VStack {
            Text("Most modern way (for simple backgrounds):")
            shape
                .strokeBorder(Color.green,lineWidth: 6)
                .background(gradient, in: shape) // Only `ShapeStyle` as background can be used (iOS15)
            Text("For simple backgrounds:")
            shape
                .strokeBorder(Color.green,lineWidth: 6)
                .background(
                    ZStack { // We are pretty limited with `shape` if we need to keep inside border
                       shape.fill(gradient) // Only `Shape` Views as background
                       shape.fill(.yellow).opacity(0.4) // Another `Shape` view
                       //Image(systemName: "star").resizable() //Try to uncomment and see the star spilling of the border
                    }
                )
            Text("For any content to be clipped:")
            shape
                .strokeBorder(Color.green,lineWidth: 6)
                .background(Image(systemName: "star").resizable()) // Anything
                .clipShape(shape) // clips everything
        }
    }
}

Also ZStack'ing two shapes (stroked and filled) for some cases is not a bad idea to me.

If you want to use an imperative approach, here is a small Playground example of Canvas view. The tradeoff is that you can't attach gestures to shapes and objects drawn on Canvas, only to Canvas itself.

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    let lineWidth: CGFloat = 8
    var body: some View {
        Canvas { context, size in
            let path = Circle().inset(by: lineWidth / 2).path(in: CGRect(origin: .zero, size: size))
            context.fill(path, with: .color(.cyan))
            context.stroke(path, with: .color(.yellow), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, dash: [30,20]))
        }
        .frame(width: 100, height: 200)
    }
}

PlaygroundPage.current.setLiveView(ContentView())

Solution 12 - Swift

Here are the extensions I use for filling and stroking a shape. None of the other answers allow full customization of the fill and stroke style.

extension Shape {
    
    /// Fills and strokes a shape.
    func style<F: ShapeStyle, S: ShapeStyle>(
        fill: F,
        stroke: S,
        strokeStyle: StrokeStyle
    ) -> some View {
        ZStack {
            self.fill(fill)
            self.stroke(stroke, style: strokeStyle)
        }
    }
    
    /// Fills and strokes a shape.
    func style<F: ShapeStyle, S: ShapeStyle>(
        fill: F,
        stroke: S,
        lineWidth: CGFloat = 1
    ) -> some View {
        self.style(
            fill: fill,
            stroke: stroke,
            strokeStyle: StrokeStyle(lineWidth: lineWidth)
        )
    }
    
}

extension InsettableShape {
    
    /// Fills and strokes an insettable shape.
    func style<F: ShapeStyle, S: ShapeStyle>(
        fill: F,
        strokeBorder: S,
        strokeStyle: StrokeStyle
    ) -> some View {
        ZStack {
            self.fill(fill)
            self.strokeBorder(strokeBorder, style: strokeStyle)
        }
    }
    
    /// Fills and strokes an insettable shape.
    func style<F: ShapeStyle, S: ShapeStyle>(
        fill: F,
        strokeBorder: S,
        lineWidth: CGFloat = 1
    ) -> some View {
        self.style(
            fill: fill,
            strokeBorder: strokeBorder,
            strokeStyle: StrokeStyle(lineWidth: lineWidth)
        )
    }
    
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionorjView Question on Stackoverflow
Solution 1 - SwiftBurak DizlekView Answer on Stackoverflow
Solution 2 - SwiftImranView Answer on Stackoverflow
Solution 3 - SwiftlochiweiView Answer on Stackoverflow
Solution 4 - SwiftMatteo PaciniView Answer on Stackoverflow
Solution 5 - SwiftmanmanView Answer on Stackoverflow
Solution 6 - SwiftjensrodiView Answer on Stackoverflow
Solution 7 - SwiftcoffeecoderView Answer on Stackoverflow
Solution 8 - SwiftPiterPanView Answer on Stackoverflow
Solution 9 - Swiftuser6902806View Answer on Stackoverflow
Solution 10 - SwiftingcontiView Answer on Stackoverflow
Solution 11 - SwiftPaul BView Answer on Stackoverflow
Solution 12 - SwiftPeter SchornView Answer on Stackoverflow