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

SwiftUI: How to draw filled and stroked shape?

在 UIKit 中绘制描边和填充 path/shape 非常简单。

例如,下面的代码绘制了一个带有蓝色描边的红色圆圈。

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 做到这一点?

Swift UI好像支持:

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 两个圆圈吗?好像有点傻。

目前似乎是 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)
            }
        }

    }

}

输出:


查看层次结构:

您可以画一个带有描边边框的圆

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

为了将来参考,@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)
    }
}

我根据上面的回答整理了下面的包装器。它使这更容易一些,代码更易于阅读。

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

希望 Apple 将来能找到一种方法同时支持形状上的填充和描边。

我的解决方法:

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

结果:

如果我们想要一个带有 no moved 边框效果的圆圈,我们可以使用 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
    }
}

输出(选中和未选中状态):

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

用于 Shape 对象:

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)

您也可以结合使用 strokeBorderbackground

代码:

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

结果:

另一个更简单的选项,就是使用 ZStack 将笔画堆叠在填充之上

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

有几种方法可以实现“填充和描边”效果。这是其中三个:

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

此外,ZStack在某些情况下使用两种形状(描边和填充)对我来说是个不错的主意。

如果您想使用命令式方法,这里有一个 Canvas 视图的小 Playground 示例。权衡是您不能将手势附加到 Canvas 上绘制的形状和对象,只能附加到 Canvas 本身。

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

我的 2 美分用于抚摸和着色“来自 Apple 的花朵样本” (// 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 )
    }
}

图片:

这是我用来填充和描边形状的扩展。 None 的其他答案允许完全自定义填充和描边样式。

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