I am trying to create SwiftUI Custom picker with Arrow Shape like
What I have done so far is :
struct SItem: Identifiable, Hashable {
var text: String
var id = UUID()
}
struct BreadCumView: View {
var items: [SItem] = [SItem(text: "Item 1"), SItem(text: "Item 2"), SItem(text: "Item 3")]
#State var selectedIndex: Int = 0
var body: some View {
HStack {
ForEach(self.items.indices) { index in
let tab = self.items[index]
Button(tab.text) {
selectedIndex = index
}.buttonStyle(CustomSegmentButtonStyle())
}
}
}
}
struct CustomSegmentButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration
.label
.padding(8)
.background(
Color(red: 0.808, green: 0.831, blue: 0.855, opacity: 0.9)
)
}
}
How to create Shape style for Background?
With Selected Stats.
Here's an arrow Shape using #Asperi's comment:
struct ArrowShape: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
let chunk = rect.height * 0.5
path.move(to: .zero)
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: max(rect.width - chunk, rect.width / 2), y: 0))
path.addLine(to: CGPoint(x: rect.width), y: chunk)
path.addLine(to: CGPoint(x: max(rect.width - chunk, rect.width / 2), y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
}
}
}
Related
I would like to connect two SwiftUI Views with an arrow, one pointing to the other. For example, in this View:
struct ProfileIconWithPointer: View {
var body: some View {
VStack {
Image(systemName: "person.circle.fill")
.padding()
.font(.title)
Text("You")
.offset(x: -20)
}
}
}
I have a Text view and an Image view. I would like to have an arrow pointing from one to the other, like this:
However, all the solutions that I could find rely on knowing the position of each element apriori, which I'm not sure SwiftUI's declarative syntax allows for.
you can use the following code to display an arrow.
struct CurvedLine: Shape {
let from: CGPoint
let to: CGPoint
var control: CGPoint
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get {
AnimatablePair(control.x, control.y)
}
set {
control = CGPoint(x: newValue.first, y: newValue.second)
}
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: from)
path.addQuadCurve(to: to, control: control)
let angle = atan2(to.y - control.y, to.x - control.x)
let arrowLength: CGFloat = 15
let arrowPoint1 = CGPoint(x: to.x - arrowLength * cos(angle - .pi / 6), y: to.y - arrowLength * sin(angle - .pi / 6))
let arrowPoint2 = CGPoint(x: to.x - arrowLength * cos(angle + .pi / 6), y: to.y - arrowLength * sin(angle + .pi / 6))
path.move(to: to)
path.addLine(to: arrowPoint1)
path.move(to: to)
path.addLine(to: arrowPoint2)
return path
}
}
you can use this in your code as follows.
struct ContentView: View {
var body: some View {
VStack {
Text("You")
.alignmentGuide(.top) { d in
d[.bottom] - 30
}
Image(systemName: "person.circle.fill")
.padding()
.font(.title)
}
.overlay(
CurvedLine(from: CGPoint(x: 50, y: 50),
to: CGPoint(x: 300, y: 200),
control: CGPoint(x: 100, y: -300))
.stroke(Color.blue, lineWidth: 4)
)
}
}
This will looks like as follows.
Depend on your scenario modify the code. Hope this will helps you.
I hope you are all doing good.
I'm very new to this community and I hope we can help each other grow.
I'm currently building a MacOS App with swiftUI and I have this capsule shape, that has a water animation inside. The water animation are just waves that move horizontally created with animatable data and path. My problem is, I have two other buttons on the screen, a play button and a stop button. They are supposed to start filling the capsule with the water and stop doing it respectively, which they do, but they are supposed to do it with an animation, and it's not.
Below is my code for further details. Thanks in advance.
GeometryReader { geometry in
VStack {
Spacer()
BreathingWave(progress: $progressValue, phase: phase)
.fill(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
.clipShape(Capsule())
.border(Color.gray, width: 3)
.frame(width: geometry.size.width / 12)
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.phase = .pi * 2
}
}
HStack {
Spacer()
Image(systemName: "play.circle.fill")
.resizable()
.renderingMode(.template)
.frame(width: 50, height: 50)
.foregroundColor(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
.scaleEffect(scalePlay ? 1.25 : 1)
.onHover(perform: { scalPlay in
withAnimation(.linear(duration: 0.1)) {
scalePlay = scalPlay
}
})
.onTapGesture {
withAnimation(
.easeInOut(duration: duration)
.repeatForever(autoreverses: true)) {
if(progressValue < 1) {
progressValue += 0.1
}
else {
progressValue = progressValue
}
}
}
Spacer()
Image(systemName: "stop.circle.fill")
.resizable()
.renderingMode(.template)
.frame(width: 50, height: 50)
.foregroundColor(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
.scaleEffect(scaleStop ? 1.25 : 1)
.onHover(perform: { scalStop in
withAnimation(.linear(duration: 0.1)) {
scaleStop = scalStop
}
})
.onTapGesture {
withAnimation(.linear) {
progressValue = 0.0
}
}
Spacer()
}
.padding(.bottom, 50)
}
}
And this is the code of the BreathingWave
struct BreathingWave: Shape {
#Binding var progress: Float
var applitude: CGFloat = 10
var waveLength: CGFloat = 20
var phase: CGFloat
var animatableData: CGFloat {
get { phase }
set { phase = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.width
let height = rect.height
let minWidth = width / 2
let progressHeight = height * (1 - CGFloat(progress))
path.move(to: CGPoint(x: 0, y: progressHeight))
for x in stride(from: 0, to: width + 5, by: 5) {
let relativeX = x / waveLength
let normalizedLength = (x - minWidth) / minWidth
let y = progressHeight + sin(phase + relativeX) * applitude * normalizedLength
path.addLine(to: CGPoint(x: x, y: y))
}
path.addLine(to: CGPoint(x: width, y: progressHeight))
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.addLine(to: CGPoint(x: 0, y: progressHeight))
return path
}
}
The problem you are running into is simply that you never told BreathingWave how to animate for progress. You set a single dimension animation, when you really want it to animate in two dimensions. The fix is straightforward: use AnimatablePair to supply the variables to animatableData.
struct BreathingWave: Shape {
var applitude: CGFloat = 10
var waveLength: CGFloat = 20
// progress is no longer a Binding. Using #Binding never did anything
// as you were not changing the value to be used in the parent view.
var progress: Float
var phase: CGFloat
var animatableData: AnimatablePair<Float, CGFloat> { // <- AnimatablePair here
get { AnimatablePair(progress, phase) } // <- the getter returns the pair
set { progress = newValue.first // <- the setter sets each individually from
phase = newValue.second // the pair.
}
}
func path(in rect: CGRect) -> Path {
...
}
}
Lastly, you now call it like this:
BreathingWave(progress: progressValue, phase: phase)
One final thing. Please provide a Minimal Reproducible Example (MRE) in the future. There was a lot of code that was unnecessary to debug this, and I had to implementment the struct header to run it. Strip as much out as possible, but give us something that we can copy, paste and run.
I am currently working on a graph view for a widget and I don't like the way the edges looks on my graph atm. I would like to make the edges on my graph line rounded instead of sharp (Graph).
I've tried with .cornerRadius(5) and .addQuadCurve() but nothing seems to work.
My code looks like this.
import SwiftUI
#available(iOS 14.0.0, *)
struct Graph: View {
var styling = ViewStyling()
var stockPrices: [CGFloat]
var body: some View {
ZStack {
LinearGradient(gradient:
Gradient(colors: [styling.gradientColor, styling.defaultWhite]), startPoint: .top, endPoint: .bottom)
.clipShape(LineGraph(dataPoints: stockPrices.normalized, closed: true))
LineGraph(dataPoints: stockPrices.normalized)
.stroke(styling.graphLine, lineWidth: 2)
//.cornerRadius(5)
}
}
}
#available(iOS 14.0.0, *)
struct LineGraph: Shape {
var dataPoints: [CGFloat]
var closed = false
func path(in rect: CGRect) -> Path {
func point(at ix: Int) -> CGPoint {
let point = dataPoints[ix]
let x = rect.width * CGFloat(ix) / CGFloat(dataPoints.count - 1)
let y = (1 - point) * rect.height
return CGPoint(x: x, y: y)
}
return Path { p in
guard dataPoints.count > 1 else { return }
let start = dataPoints[0]
p.move(to: CGPoint(x: 0, y: (1 - start) * rect.height))
//p.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY), control: CGPoint(x: rect.maxX, y: rect.maxY))
for index in dataPoints.indices {
p.addLine(to: point(at: index))
}
if closed {
p.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
p.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
p.closeSubpath()
}
}
}
}
extension Array where Element == CGFloat {
// Return the elements of the sequence normalized.
var normalized: [CGFloat] {
if let min = self.min(), let max = self.max() {
return self.map{ ($0 - min) / (max - min) }
}
return []
}
}
How the joins between line segments are rendered is controlled by the lineJoin property of StrokeStyle, You're stroking with a color and a line width here:
.stroke(styling.graphLine, lineWidth: 2)
but you want something more like:
.stroke(styling.graphline, StrokeStyle(lineWidth: 2, lineJoin: .round))
Swift 5, iOS 13
Using addArc to draw a semi-circle and then using rotation3DEffect to rotate it. On the y axis it works perfectly, but in the x axis is seems broken. Am I doing something wrong here.
I got this code to work. But it is a kludge.
I want to us the x axis on the MyArc2 and not have to use an offset+rotationEffect. The first image is what you see with this code as is.
However if I do use x axis it doesn't draw it correctly.
import SwiftUI
struct ContentView: View {
#State var turn:Double = 0
var body: some View {
return VStack {
Image(systemName: "circle")
.foregroundColor(Color.blue)
.onTapGesture {
withAnimation(.linear(duration: 36)) {
self.turn = 360
}
}
Globe2View(turn: $turn)
} // VStack
}
}
struct Globe2View: View {
struct MyArc : Shape {
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(center: CGPoint(x: UIScreen.screenWidth/2, y:UIScreen.screenHeight/2), radius: 64, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true)
return p
}
}
struct MyArc2 : Shape {
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(center: CGPoint(x: UIScreen.screenWidth/2, y:UIScreen.screenHeight/2), radius: 64, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true)
return p
}
}
#Binding var turn:Double
var body: some View {
ZStack {
ForEach(0..<12) { loop in
MyArc()
.stroke(Color.blue, lineWidth: 2)
.rotation3DEffect(.degrees(self.turn + Double((loop * 30))), axis: (x: 0, y: -1, z: 0), anchor: UnitPoint.center, anchorZ: 0, perspective: 0)
}
ForEach(0..<12) { loop in
MyArc2()
.stroke(Color.green, lineWidth: 2)
.rotation3DEffect(.degrees(self.turn + Double((loop * 30))), axis: (x: 0, y: -1, z: 0), anchor: UnitPoint.center, anchorZ: 0, perspective: 0)
.rotationEffect(.degrees(90))
.offset(x: 20, y: 20)
}
}
}
}
If I change the last few lines so they look like this...
struct MyArc2 : Shape {
func path(in rect: CGRect) -> Path {
var p = Path()
p.addArc(center: CGPoint(x: UIScreen.screenWidth/2, y:UIScreen.screenHeight/2), radius: 64, startAngle: .degrees(0), endAngle: .degrees(180), clockwise: true)
return p
}
}
ForEach(0..<12) { loop in
MyArc2()
.stroke(Color.green, lineWidth: 2)
.rotation3DEffect(.degrees(self.turn + Double((loop * 30))), axis: (x: 1, y: 0, z: 0), anchor: UnitPoint.center, anchorZ: 0, perspective: 0)
// .rotationEffect(.degrees(90))
// .offset(x: 20, y: 20)
}
It is not clear why it is needed MyArc2 as it is the same, but the following, with simple fix in shape, just works as expected (to my mind).
Tested with Xcode 11.4 / iOS 13.4
struct Globe2View: View {
struct MyArc : Shape {
func path(in rect: CGRect) -> Path {
var p = Path()
// used provided dynamic rect instead of hardcoded NSScreen
p.addArc(center: CGPoint(x: rect.width/2, y:rect.height/2), radius: 64, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true)
return p
}
}
#Binding var turn:Double
var body: some View {
ZStack {
ForEach(0..<12) { loop in
MyArc()
.stroke(Color.blue, lineWidth: 2)
.rotation3DEffect(.degrees(self.turn + Double((loop * 30))), axis: (x: 0, y: -1, z: 0), anchor: UnitPoint.center, anchorZ: 0, perspective: 0)
}
ForEach(0..<12) { loop in
MyArc()
.stroke(Color.green, lineWidth: 2)
.rotation3DEffect(.degrees(self.turn + Double((loop * 30))), axis: (x: 0, y: -1, z: 0), anchor: UnitPoint.center, anchorZ: 0, perspective: 0)
.rotationEffect(.degrees(90))
}
}
}
}
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 rounded rect:
let rect = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius)
and I need to this drawing path add 4 dashes lines on each corner
Expected result:
I need in swift 5 and add to this path, because by this path I draw general mask for view
I use this path for CAShapeLayer, and I need this dashes just for this CAShapeLayer
Hey so I decided to go nuts and create an extension for UIView for what you are looking for. It's as follows:
extension UIView {
private struct Properties {
static var _radius: CGFloat = 0.0
static var _color: UIColor = .red
static var _strokeWidth: CGFloat = 1.0
static var _length: CGFloat = 20.0
}
private var radius: CGFloat {
get {
return Properties._radius
}
set {
Properties._radius = newValue
}
}
private var color: UIColor {
get {
return Properties._color
}
set {
Properties._color = newValue
}
}
private var strokeWidth: CGFloat {
get {
return Properties._strokeWidth
}
set {
Properties._strokeWidth = newValue
}
}
private var length: CGFloat {
get {
return Properties._length
}
set {
Properties._length = newValue
}
}
func drawCorners(radius: CGFloat? = nil, color: UIColor? = nil, strokeWidth: CGFloat? = nil, length: CGFloat? = nil) {
if let radius = radius {
self.radius = radius
}
if let color = color {
self.color = color
}
if let strokeWidth = strokeWidth {
self.strokeWidth = strokeWidth
}
if let length = length {
self.length = length
}
createTopLeft()
createTopRight()
createBottomLeft()
createBottomRight()
}
private func createTopLeft() {
let topLeft = UIBezierPath()
topLeft.move(to: CGPoint(x: strokeWidth/2, y: radius+length))
topLeft.addLine(to: CGPoint(x: strokeWidth/2, y: radius))
topLeft.addQuadCurve(to: CGPoint(x: radius, y: strokeWidth/2), controlPoint: CGPoint(x: strokeWidth/2, y: strokeWidth/2))
topLeft.addLine(to: CGPoint(x: radius+length, y: strokeWidth/2))
setupShapeLayer(with: topLeft)
}
private func createTopRight() {
let topRight = UIBezierPath()
topRight.move(to: CGPoint(x: frame.width-radius-length, y: strokeWidth/2))
topRight.addLine(to: CGPoint(x: frame.width-radius, y: strokeWidth/2))
topRight.addQuadCurve(to: CGPoint(x: frame.width-strokeWidth/2, y: radius), controlPoint: CGPoint(x: frame.width-strokeWidth/2, y: strokeWidth/2))
topRight.addLine(to: CGPoint(x: frame.width-strokeWidth/2, y: radius+length))
setupShapeLayer(with: topRight)
}
private func createBottomRight() {
let bottomRight = UIBezierPath()
bottomRight.move(to: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-radius-length))
bottomRight.addLine(to: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-radius))
bottomRight.addQuadCurve(to: CGPoint(x: frame.width-radius, y: frame.height-strokeWidth/2), controlPoint: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-strokeWidth/2))
bottomRight.addLine(to: CGPoint(x: frame.width-radius-length, y: frame.height-strokeWidth/2))
setupShapeLayer(with: bottomRight)
}
private func createBottomLeft() {
let bottomLeft = UIBezierPath()
bottomLeft.move(to: CGPoint(x: radius+length, y: frame.height-strokeWidth/2))
bottomLeft.addLine(to: CGPoint(x: radius, y: frame.height-strokeWidth/2))
bottomLeft.addQuadCurve(to: CGPoint(x: strokeWidth/2, y: frame.height-radius), controlPoint: CGPoint(x: strokeWidth/2, y: frame.height-strokeWidth/2))
bottomLeft.addLine(to: CGPoint(x: strokeWidth/2, y: frame.height-radius-length))
setupShapeLayer(with: bottomLeft)
}
private func setupShapeLayer(with path: UIBezierPath) {
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = color.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = strokeWidth
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
}
}
Then you can use it on any UIView. So once you've created a view, you can then use it as follows:
let myView = UIView(frame: CGRect(x: 50, y: 50, width: 300, height: 300))
myView.drawCorners(radius: 30.0, color: .red, strokeWidth: 10.0, length: 30)
And that's all you need to do! The output on a blue background is as follows: