SwiftUI:如何绘制填充和描边形状?

78
在UIKit中,绘制描边和填充的路径/形状非常容易。
例如,下面的代码会绘制一个红色圆,描边为蓝色。
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()
}

如何使用SwiftUI完成这个任务?

看起来SwiftUI支持以下功能:

Circle().stroke(Color.blue)
// and/or
Circle().fill(Color.red)
但不。
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'

我只需要在ZStack中堆叠两个圆形吗?这看起来有点傻。


请查看 https://www.swiftbysundell.com/articles/stroking-and-filling-a-swiftui-shape-at-the-same-time/。 - Robin Stewart
12个回答

102

您还可以将 strokeBorder background 结合使用。

代码:

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

结果:


4
这可能是我见过的最简单的解决方案。不确定我怎么错过了iOS 13中的strokeBorder。仍然认为让描边和填充一起工作会是一个更好的API。 :) - orj
非常聪明!这样,我也可以在视图修饰符中使用三元条件,并用一行解决我的庞大的if-else语句 :) - Sayonara
我真的不想在代码中重复形状两次。当我使用一个接受多个参数的自定义形状时,这非常不方便。这不能是最好的方法。 - Peter Schorn
可以了,谢谢!但是这样做有点疯狂,对于一个已经使用 fillstroke 的开发者来说,这种方式不太可组合。 - Robert Monfera
@orj同意。从语法上讲,我认为大多数开发人员也希望如此。 - chitgoks

59
更新: 在iOS 17及更高版本中,您可以通过堆叠修饰符来同时填充和描边形状,就像这样:
Circle()
    .stroke(.red, lineWidth: 3)
    .fill(.orange)
    .frame(width: 150, height: 150)

iOS 16:
你可以绘制带有描边的圆形。
struct ContentView: View {
    var body: some View {
        Circle()
            .strokeBorder(Color.green,lineWidth: 3)
            .background(Circle().foregroundColor(Color.red))
   }
}

circle with stroked border


20

我的解决方法:

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)
        }
    }
}

例子:


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)
    }
}

结果:

填充和描边的圆形


16

目前看起来要么是ZStack,要么是.overlay

根据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)
            }
        }

    }

}

输出:

enter image description here


视图层次结构:

enter image description here


3
目前看来这似乎是唯一的方法。我向苹果提供了“反馈”,认为这不是理想的解决方案,而且这在API中可能是一个疏忽。 - orj

8

另一个更简单的选项是使用ZStack将描边叠放在填充上方

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

The result is


非常有帮助!谢谢! - Lemon

6

供日后参考,@Imran的解决方案是可行的,但您还需要通过填充来考虑总框架中的描边宽度:

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


1
谢谢,除此之外,填充应该是线宽的一半,以使边缘相接触。 - William Grand

4

关于将来自苹果的 "花样例子"(// https://developer.apple.com/documentation/quartzcore/cashapelayer)迁移到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)

        }
    }

我的看法是:

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 )
    }
}

图像: 输入图像描述


4

我根据上面的答案编写了以下包装器。它使得事情更加容易并且使代码更加易于阅读。

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)
    }
  }
}

可以这样使用:
FillAndStroke(fill : Color.red, stroke : Color.yellow)
{
  Circle()
}

希望苹果公司未来能够找到一种支持形状的填充和描边的方法。

3
如果我们想要一个没有移动边框效果的圆形,就像使用ZStack { Circle().fill(), Circle().stroke }看到的那样,我们需要进行以下操作: 第一步:创建一个新的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
    }
}

第二步

我们正在创建一个新的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)
            }
        }
    }

最后一步

我们正在创建一个新的 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
    }
}

输出(已选择和未选择状态):

输入图片描述

输入图片描述


3

在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)
        }
    }

用于形状对象:

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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接