Reputation: 13504
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.
Upvotes: 90
Views: 100394
Reputation:
Update from https://www.hackingwithswift.com/quick-start/swiftui/how-to-fill-and-stroke-shapes-at-the-same-time by Paul Hudson:
In iOS 17 and later, you can fill and stroke shapes at the same time just by stacking the modifiers, like this:
Circle() .stroke(.red, lineWidth: 3) .fill(.orange) .frame(width: 150, height: 150)
iOS 16 (same source, slightly modified)
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))
}
}
Upvotes: 67
Reputation: 997
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)
)
}
}
Upvotes: 2
Reputation: 634
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)
}
Upvotes: 9
Reputation: 11666
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 )
}
}
Upvotes: 5
Reputation: 5125
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())
Upvotes: 0
Reputation: 5145
You can also use strokeBorder
and background
in combination.
Circle()
.strokeBorder(Color.blue,lineWidth: 4)
.background(Circle().foregroundColor(Color.red))
Upvotes: 110
Reputation: 1802
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):
Upvotes: 3
Reputation: 280
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)
Upvotes: 3
Reputation: 1358
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:
Upvotes: 20
Reputation: 700
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.
Upvotes: 4
Reputation: 5113
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)
}
}
Upvotes: 6
Reputation: 22856
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:
View hierarchy:
Upvotes: 16