Dans UIKit, dessiner un chemin/une forme caressé et rempli est assez facile.
Par exemple, le code ci-dessous dessine un cercle rouge qui est caressé en bleu.
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()
}
Comment faire cela avec SwiftUI?
L'interface utilisateur rapide semble prendre en charge:
Circle().stroke(Color.blue)
// and/or
Circle().fill(Color.red)
mais non
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'
Suis-je censé simplement ZStack deux cercles? Cela semble un peu idiot.
Il semble que ce soit ZStack
ou .overlay
en ce moment.
La hiérarchie des vues est presque identique - selon 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)
}
}
}
}
Sortie :
Afficher la hiérarchie :
Pour référence future, la solution de @ Imran fonctionne, mais vous devez également prendre en compte la largeur de trait dans votre cadre total par rembourrage:
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)
}
}
J'ai assemblé le wrapper suivant sur la base des réponses ci-dessus. Cela rend cela un peu plus facile et le code un peu plus simple à lire.
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)
}
}
}
Il peut être utilisé comme ceci:
FillAndStroke(fill : Color.red, stroke : Color.yellow)
{
Circle()
}
Espérons que Apple trouvera un moyen de prendre en charge à la fois le remplissage et le contour d'une forme à l'avenir.
Ma solution:
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)
}
}
}
Exemple:
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)
}
}
Résultat:
S'appuyant sur la réponse précédente de 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)
}
}
Utilisé sur un objet 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)
Si nous voulons avoir un cercle avec un effet de bordure no moved
Comme nous pouvons le voir en utilisant ZStack { Circle().fill(), Circle().stroke }
j'ai préparé quelque chose comme ci-dessous:
Premier raide
Nous créons un nouveau 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
}
}
Deuxième raide
Nous créons un nouveau 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)
}
}
}
Dernière pente
Nous créons un nouveau 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
}
}
Sortie: état sélectionné et non sélectionné