I would like to implement the logic to change the shape color on tap. I tried to modify the example for changing the shape on tap as follows:
import SwiftUI
struct PathView: View {
#State private var insetAmount: CGFloat = 50
#State private var fillColor: UIColor = UIColor.white
var body: some View {
GeometryReader {
geometry in
ZStack {
Trapezoid(insetAmount: insetAmount, fillColor: fillColor)
.frame(width: 200, height: 100)
.onTapGesture {
self.insetAmount = CGFloat.random(in: 10...90)
let demoColors = [UIColor.blue, UIColor.green, UIColor.red]
self.fillColor = demoColors.randomElement() ?? UIColor.white
}
}.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
}
}
}
struct Trapezoid: Shape {
var insetAmount: CGFloat
var fillColor: UIColor
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY))
path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: 0, y: rect.maxY))
path.closeSubpath()
fillColor.setFill()
UIColor.white.setStroke()
path.fill()
return path
}
}
The path, however, is black and the line path.fill() displays a warning:
Result of call to 'fill(style:)' is unused
Does anyone know how to set the color property of shape and change it on tap?
A fill should be applied to Shape.
Here is possible solution (tested with Xcode 12)
struct PathView: View {
#State private var insetAmount: CGFloat = 50
#State private var fillColor: UIColor = UIColor.white
var body: some View {
GeometryReader {
geometry in
ZStack {
Trapezoid(insetAmount: insetAmount)
.fill(Color(fillColor)) // << here !!
.frame(width: 200, height: 100)
.onTapGesture {
self.insetAmount = CGFloat.random(in: 10...90)
let demoColors = [UIColor.blue, UIColor.green, UIColor.red]
self.fillColor = demoColors.randomElement() ?? UIColor.white
}
}.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
}
}
}
struct Trapezoid: Shape {
var insetAmount: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY))
path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: 0, y: rect.maxY))
path.closeSubpath()
return path
}
}
Related
One of the views of my swiftui app require a dash line running through the background. I added this by creating a custom shape called MyLine that creates a path using the following code:
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
return path
I make it a dashed line using
.stroke(style: StrokeStyle(dash: [10]))
Finally, I add it to the blackground of my intended view using the background modifier:
.background(
MyLine()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [10]))
)
This all works great except for one problem: whenever ANY padding is added to the line the dashes become lopsided as shown below. Why is this happening and how do I fix it?
Before Padding:
After Padding
Minimum reproducible example:
import SwiftUI
struct MyLine: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
return path
}
}
struct MyLineTest: View{
var body: some View {
VStack{
MyLine()
.stroke(style: StrokeStyle(dash: [10]))
}.frame(height: 60)
.padding() //Remove this line to fix the dashes
}
}
struct MyLine_Previews: PreviewProvider {
static var previews: some View {
MyLineTest()
.foregroundColor(.black)
}
}
Just paste this code into an empty swift view file.
I'm trying to add a stroke to `CGPath` in a macOS app with the code below. The issue is that the stroked path can't be filled properly. With the current method, I'm adding the original path on top of the stroked path. This causes the overlapping parts to remain unfilled.
func getPreviewPath(path:CGPath, strokeWidth: CGFloat) ->CGPath{
let window = NSWindow()
let context = NSGraphicsContext(window: window).cgContext
context.setLineCap(CGLineCap(rawValue: 0)!)
context.setLineWidth(strokeWidth)
context.setLineJoin(.bevel)
context.addPath(path)
context.replacePathWithStrokedPath()
context.addPath(path)
return context.path!
}
I've read that this is caused by the even-odd fill rule. But there doesn't seem to be a way to change this.
Here's a minimal example using the stroke() method:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 50, y: 100))
path.addLine(to: CGPoint(x: 80, y: 20))
path.addLine(to: CGPoint(x: 120, y: 20))
}
.stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .bevel))
.padding()
}
}
And this is the result:
Update
Her is a slightly more complex example. It first creates a path containing a stroked path. This path is then filled. The result is the same.
struct ContentView: View {
var body: some View {
Path(createPath())
.fill(.black)
.padding()
}
func createPath() -> CGPath {
let path = CGMutablePath()
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 50, y: 100))
path.addLine(to: CGPoint(x: 80, y: 20))
path.addLine(to: CGPoint(x: 120, y: 20))
return path.copy(strokingWithWidth: 12.0, lineCap: .round, lineJoin: .bevel, miterLimit: 10)
}
}
I'm trying to draw a line with markers at each point by using a Shape view in SwiftUI. I want the line to have a filled circle at each CGPoint on the line. The closest to this that I've gotten is by adding an arc at each point. Instead of using the arc, how can I add a Circle() shape at each point? I'm open to other approaches to accomplish this. My only requirements are to use SwiftUI and have the ability to interact with the markers.
import SwiftUI
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
path.addArc(center: pt, radius: 8, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: false)
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.padding()
.frame(width: 400, height: 300)
}
}
You can make two different shapes, one for the line and one for the markers, and overlay them. Then you can also control their coloring individually:
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
}
return path
}
}
struct MarkersShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addEllipse(in: CGRect(x: pt.x - 8, y: pt.y - 8, width: 16, height: 16))
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.overlay(
MarkersShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.fill(.blue)
)
.frame(width: 350, height: 300)
}
}
According to previous posts and comments here is my solution:
First there is need to use extension for filling and setting shape stroke:
extension Shape {
public func fill<Shape: ShapeStyle>(_ fillContent: Shape, strokeColor: Color, lineWidth: CGFloat) -> some View {
ZStack {
self.fill(fillContent)
self.stroke(strokeColor, lineWidth: lineWidth)
}
}
}
Another thing is Scaler struct used to make scaling calculations:
struct Scaler {
let bounds: CGPoint
let maxVal: Double
let minVal: Double
let valuesCount: Int
var xFactor: CGFloat {
valuesCount < 2 ? bounds.x : bounds.x / CGFloat(valuesCount - 1)
}
var yFactor: CGFloat {
bounds.y / maxVal
}
}
Now it's time for real chart shape drawing:
struct ChartLineDotsShape: Shape {
let values: [Double]
let dotRadius: CGFloat
func path(in rect: CGRect) -> Path {
guard let maxVal = values.max(), let minVal = values.min() else { return Path() }
let scaler = Scaler(bounds: CGPoint(x: rect.width, y: rect.height), maxVal: maxVal, minVal: minVal, valuesCount: values.count)
return Path { path in
var valuesIterator = values.makeIterator()
let dx = scaler.xFactor
var x = 0.0
guard let y = valuesIterator.next() else { return }
path.move(to: CGPoint(x: x, y: calculate(y, scaler: scaler)))
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
while let y = valuesIterator.next() {
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
}
}
}
private func calculate(_ value: CGFloat, scaler: Scaler) -> CGFloat {
scaler.bounds.y - value * scaler.yFactor
}
private func draw(point: CGPoint, on path: inout Path) {
path.addLine(to: CGPoint(x: point.x, y: point.y))
path.addEllipse(in: CGRect(x: point.x - dotRadius * 0.5, y: point.y - dotRadius * 0.5, width: dotRadius, height: dotRadius))
path.move(to: CGPoint(x: point.x, y: point.y))
}
}
If there is single iteration loop lines dots drawing shape we can try to use it in some view:
public struct ChartView: View {
public var body: some View {
ChartLineDotsShape(values: [0.3, 0.5, 1.0, 2.0, 2.5, 2.0, 3.0, 6.0, 2.0, 1.5, 2.0], dotRadius: 4.0)
.fill(Color.blue, strokeColor: Color.blue, lineWidth: 1.0)
.shadow(color: Color.blue, radius: 2.0, x: 0.0, y: 0.0)
.frame(width: 320.0, height: 200.0)
.background(Color.blue.opacity(0.1))
.clipped()
}
}
Finally preview setup:
struct ChartView_Previews: PreviewProvider {
static var previews: some View {
ChartView()
.preferredColorScheme(.dark)
}
}
and viola:
Problem :
I cannot draw a shape on another shape.
What I am trying to achieve :
Draw circles on the line.
Anyway, the circle is shifting the line. I didn't find a way to make it as swift UI seems relatively new. I am currently learning swift and I prefer swift UI rater than storyboard.
If circle and line are different struct, this is because I want to reuse the shape later on.
There is the code :
import SwiftUI
public var PointArray = [CGPoint]()
public var PointArrayInit:Bool = false
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
var centerCustom:CGPoint
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: 20, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct CurveCustomInit: Shape {
private var Divider:Int = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let xStep:CGFloat = DrawingZoneWidth / CGFloat(Divider)
let yStep:CGFloat = DrawingZoneHeight / 2
var xStepLoopIncrement:CGFloat = 0
path.move(to: CGPoint(x: 0, y: yStep))
for _ in 0...Divider {
let Point:CGPoint = CGPoint(x: xStepLoopIncrement, y: yStep)
PointArray.append(Point)
path.addLine(to: Point)
xStepLoopIncrement += xStep
}
PointArrayInit = true
return (path)
}
}
struct TouchCurveBasic: View {
var body: some View {
if !PointArrayInit {
Arc(startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true, centerCustom: CGPoint(x: 50, y: 400))
.stroke(Color.blue, lineWidth: 4)
CurveCustomInit()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
} else {
}
}
}
struct TouchCurveBasic_Previews: PreviewProvider {
static var previews: some View {
TouchCurveBasic()
}
}
There is what I get :
Here is an other way for you, you can limit the size of drawing with giving a frame or you can use the available size of view without limiting it or even you can use the current limit coming from parent and updated it, like i did on drawing the Line. The method that I used was overlay modifier.
struct ContentView: View {
var body: some View {
ArcView(radius: 30.0)
.stroke(lineWidth: 10)
.foregroundColor(.blue)
.frame(width: 60, height: 60)
.overlay(LineView().stroke(lineWidth: 10).foregroundColor(.red).frame(width: 400))
}
}
struct ArcView: Shape {
let radius: CGFloat
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: Angle(degrees: 0.0), endAngle: Angle(degrees: 360.0), clockwise: true)
}
}
}
struct LineView: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLines([CGPoint(x: rect.minX, y: rect.midY), CGPoint(x: rect.maxX, y: rect.midY)])
}
}
}
result:
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
I have provided an image of how I would like it to appear but have no idea how to do it. Also if possible I would like to know how to constrain such awkward looking buttons to look good on all displays. Thank you all very much!!
enum CustomButtonType {
case topLeft, topRight, bottom
}
let triangelHeight:CGFloat = 75
class CustomButton: UIButton {
private var bgColor:UIColor = .white
private var type:CustomButtonType = .topLeft
init(frame:CGRect, bgColor:UIColor, type:CustomButtonType) {
super.init(frame: frame)
self.bgColor = bgColor
self.type = type
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.beginPath()
switch type {
case .topLeft:
context.move(to: CGPoint(x: rect.minX, y: rect.minY))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
context.addLine(to: CGPoint(x: (rect.maxX), y: rect.maxY - triangelHeight))
context.addLine(to: CGPoint(x: (rect.minX), y: rect.maxY))
break
case .topRight:
context.move(to: CGPoint(x: rect.minX, y: rect.minY))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
context.addLine(to: CGPoint(x: (rect.maxX), y: rect.maxY ))
context.addLine(to: CGPoint(x: (rect.minX), y: rect.maxY - triangelHeight))
break
case .bottom:
context.move(to: CGPoint(x: rect.minX, y: rect.minY + triangelHeight))
context.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + triangelHeight))
context.addLine(to: CGPoint(x: (rect.maxX), y: rect.maxY ))
context.addLine(to: CGPoint(x: (rect.minX), y: rect.maxY ))
break
}
context.closePath()
context.setFillColor(bgColor.cgColor)
context.fillPath()
}
}
I have created buttons with frame you can use Autolayout.
let topBtnHeight:CGFloat = 400
let aBtn = CustomButton(frame: .init(x: 0, y: 0, width: view.frame.width / 2, height: topBtnHeight), bgColor: .green, type: .topLeft)
view.addSubview(aBtn)
let bBtn = CustomButton(frame: .init(x: view.frame.width / 2, y: 0, width: view.frame.width / 2, height: topBtnHeight), bgColor: .red, type: .topRight)
view.addSubview(bBtn)
let cBtn = CustomButton(frame: .init(x: 0, y: topBtnHeight - triangelHeight, width: view.frame.width , height: view.frame.height - topBtnHeight + triangelHeight), bgColor: .yellow, type: .bottom)
view.addSubview(cBtn)