【问题标题】:SwiftUI: How to draw filled and stroked shape?SwiftUI:如何绘制填充和描边的形状?
【发布时间】:2019-11-09 04:30:39
【问题描述】:

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

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 两个圆圈吗?这似乎有点傻。

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    您还可以组合使用strokeBorderbackground

    代码:

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

    结果:

    【讨论】:

    • 这可能是我见过的最简单的解决方案。不知道我是如何错过 iOS 13 中的 strokeBorder 的。仍然认为让 stroke 和 fill 一起工作会是一个更好的 API。 :)
    • 非常聪明!这样,我也可以在视图修饰符中使用三元条件,并用一行解决我巨大的 if-else 语句:)
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      我的解决方法:

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

      结果:

      【讨论】:

        【解决方案4】:

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

        输出


        查看层次结构

        【讨论】:

        • 是的,目前这似乎是唯一的方法。我向 Apple 提供了“反馈”,认为这并不理想,并认为这是 API 的疏忽。
        【解决方案5】:

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

        【讨论】:

        • 谢谢,除了填充应该是 lineWidth 的一半以使边缘接触。
        【解决方案6】:

        我根据上面的答案将以下包装放在一起。它使这更容易,代码更易于阅读。

        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 能够找到一种方法来支持形状的填充和描边。

        【讨论】:

          【解决方案7】:

          另一个更简单的选择是使用 ZStack 将笔画堆叠在填充顶部

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

          【讨论】:

          • 超级有帮助!谢谢!
          【解决方案8】:

          如果我们想要一个带有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
              }
          }
          

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

          【讨论】:

            【解决方案9】:

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

            【讨论】:

              【解决方案10】:

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

              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'ing 两个形状(描边和填充)对我来说不是一个坏主意。

              如果您想使用命令式方法,这里有一个 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())
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-01-04
                • 1970-01-01
                • 1970-01-01
                • 2011-07-27
                相关资源
                最近更新 更多