I would like to be able to get an image like this, with the word "File" in the center.
But I only managed to do this, you know how to give me a hand.
import SwiftUI
import CoreGraphics
struct File: Shape {
func path(in rect: CGRect) -> Path {
let topsx = CGPoint(x: 0, y: rect.minY)
let topdx = CGPoint(x: rect.maxX/1.5, y: 0)
let bottomsx = CGPoint(x: 0, y: rect.maxY)
let bottomdx = CGPoint(x: rect.maxX, y: rect.maxY)
var path = Path()
path.move(to: bottomdx)
path.addLine(to: bottomdx)
path.addLine(to: bottomsx)
path.addLine(to: topsx)
path.addLine(to: topdx)
return path
}
}
struct ContentView: View {
var body: some View {
ZStack {
File().fill(Color.black)
Text("File")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Personally I would not try to draw the icon with code.
Here's a simplified solution with a SF Symbol, but you can just find a nice document icon, download it, add it to your assets and use that instead.
ZStack {
Image(systemName: "doc.fill").resizable()
.scaledToFit()
.foregroundColor(.gray)
.overlay {
Text("File").font(.title).foregroundColor(.white).shadow(color: .black, radius: 10, x: 5, y: 5).offset(x:0, y: 20)
}
}.frame(height:200)
Related
I am making an app where I am generating points with random position. I want to connect first point with the second one with the line, and that the second with the third, etc.
My issue is that it is connecting second with the third but in the same time the first is being connected with the third one. And I don't want that. Please help.
This is my code:
struct Dot: Hashable {
var x: CGFloat
var y: CGFloat
}
struct TestShit: View {
#State var lastx = 0.0
#State var lasty = 0.0
#State var dots = [Dot]()
var body: some View {
VStack{
ZStack{
ForEach(dots, id: \.self){ dot in
Circle()
.frame(width: 5, height: 5)
.position(x: dot.x, y: dot.y)
Path { path in
path.move(to: CGPoint(x: lastx, y: lasty))
path.addLine(to: CGPoint(x: dot.x, y: dot.y))
}.stroke(.green, lineWidth: 5)
}
}
Spacer()
Button {
let randx = CGFloat.random(in: 100...300)
let randy = CGFloat.random(in: 100...300)
dots.append(Dot(x: randx, y: randy))
lastx = randx
lasty = randy
} label: {
Text("Button")
}
}
}
}
Here I only maintain 1 path, and append to it each time rather than more than 1 path in the ForEach
import Combine
import SwiftUI
struct Dot: Hashable {
var x: CGFloat
var y: CGFloat
}
class Model: ObservableObject {
#Published
var dots: [Dot] = []
#Published
var path: UIBezierPath?
func addDot(x: CGFloat, y: CGFloat) {
dots.append(Dot(x: x, y: y))
let randomPoint = CGPoint(x: x, y: y)
guard let path = path else {
path = UIBezierPath()
path?.move(to: randomPoint)
path?.lineWidth = 2
return
}
path.addLine(to: CGPoint(x: x, y: y))
}
}
struct TestIt: View {
#ObservedObject
var model: Model
var body: some View {
VStack{
ZStack{
if let path = model.path {
Path(path.cgPath)
.stroke(.green, lineWidth: 5)
}
ForEach(model.dots, id: \.self){ dot in
Circle()
.frame(width: 5, height: 5)
.position(x: dot.x, y: dot.y)
}
}
Spacer()
Button {
let randx = CGFloat.random(in: 100...300)
let randy = CGFloat.random(in: 100...300)
model.addDot(x: randx, y: randy)
} label: {
Text("Button")
}
}
}
}
You might want something a little nicer looking like
Path(path.cgPath.copy(strokingWithWidth: 5, lineCap: .round, lineJoin: .round, miterLimit: 0))
.stroke(.green, lineWidth: 5)
The problem is that with this loop:
Path { path in
path.move(to: CGPoint(x: lastx, y: lasty))
path.addLine(to: CGPoint(x: dot.x, y: dot.y))
}.stroke(.green, lineWidth: 5)
you are creating multiple line segments by moving to the same point, and then adding a line to a different point.
What you want to do is:
set a "start" point
move to that point
for each dot, add a line to the next dot
Take a look at the difference here:
import SwiftUI
struct Dot: Hashable {
var x: CGFloat
var y: CGFloat
}
struct DotsView: View {
// initialize start point
var startX = CGFloat.random(in: 100...300)
var startY = CGFloat.random(in: 100...300)
#State var dots = [Dot]()
var body: some View {
VStack{
ZStack{
Path { path in
// move to start point
path.move(to: CGPoint(x: startX, y: startY))
dots.forEach { dot in
// add line to each dot
path.addLine(to: CGPoint(x: dot.x, y: dot.y))
}
}.stroke(.green, lineWidth: 5)
// draw the start point circle in red
Circle()
.fill(Color.red)
.frame(width: 5, height: 5)
.position(x: startX, y: startY)
// draw each dot circle in blue
ForEach(dots, id: \.self){ dot in
Circle()
.fill(Color.blue)
.frame(width: 5, height: 5)
.position(x: dot.x, y: dot.y)
}
}
Spacer()
Button {
let randx = CGFloat.random(in: 100...300)
let randy = CGFloat.random(in: 100...300)
// add a dot point
dots.append(Dot(x: randx, y: randy))
} label: {
Text("Button")
}
}
}
}
How to make the lower right corner sharp? the result is shown in the screenshot
here is my code now:
struct Test123: View {
var body: some View {
Text("Hello, World!")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.black)
}
}
The possible solution is by using .clipShape by preparing any custom shape you want.
Here a demo (prepared with Xcode 13 / iOS 15)
struct SharpShape: 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: 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))
}
}
}
struct Test123: View {
var body: some View {
Text("Hello, World!")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.black)
.clipShape(SharpShape())
}
}
I've made a SemiRoundedRectangle shape which I'm using to clipShape a side menu. I need to flip it if the user is in an RTL language, but not sure the best way of achieving this.
I've tried .flipsForRightToLeftLayoutDirection(true) but this flips the actual Arabic text too. When I try rotating the shape it no longer conforms to the Shape protocol and so I can no longer use it in .clipShape. Everything else in SwiftUI just magically flips itself when I switch into Arabic, is there something I could add to my shape to give it these magic powers too?
Thanks for your help :)
import SwiftUI
struct SemiRoundedRectangle: Shape {
var cornerRadius: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX+cornerRadius, y: rect.maxY))
path.addArc(center: CGPoint(x: cornerRadius, y: rect.height - cornerRadius),
radius: cornerRadius,
startAngle: .degrees(90),
endAngle: .degrees(180), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: cornerRadius))
path.addArc(center: CGPoint(x: cornerRadius, y: cornerRadius),
radius: cornerRadius,
startAngle: .degrees(180),
endAngle: .degrees(270), clockwise: false)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}
}
struct TestView {
var body: some View {
HStack {
Text("ايه الأخبار؟")
.padding()
.background(Color.green)
.clipShape(SemiRoundedRectangle(cornerRadius: 10.0))
Spacer()
}
}
}
Try attaching the clipShape to the Color.green instead:
struct TestView: View {
var body: some View {
HStack {
Text("ايه الأخبار؟")
.padding()
.background(
Color.green /// here!
.clipShape(SemiRoundedRectangle(cornerRadius: 10.0))
.flipsForRightToLeftLayoutDirection(true)
)
Spacer()
}
}
}
Result:
English
RTL Language
There is a generic solution:
extension Shape {
func flipped(_ axis: Axis = .horizontal, anchor: UnitPoint = .center) -> ScaledShape<Self> {
switch axis {
case .horizontal:
return scale(x: -1, y: 1, anchor: anchor)
case .vertical:
return scale(x: 1, y: -1, anchor: anchor)
}
}
}
Use for this case:
struct TestView: View {
var body: some View {
HStack {
Text("ايه الأخبار؟")
.padding()
.background(Color.green)
.clipShape(SemiRoundedRectangle(cornerRadius: 20.0).flipped())
Spacer()
}
.padding()
}
}
I need to create a single dashed line. I tried going about it by creating a Rectangle view with a dashed stroke. However, when setting the height of the rectangle to 1, it results in a double line as its showing both the top and bottom borders of the view.
This is the code:
Rectangle()
.fill(Color.clear)
.frame(height: 1, alignment: .bottom)
.overlay(
RoundedRectangle(cornerRadius: 0)
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
.foregroundColor(Color(UIColor.blue))
)
Change the dash value to increase or decrease the number of dash in the line.
struct ContentView: View {
var body: some View {
Line()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
.frame(height: 1)
}
}
struct Line: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
Result:
Depending on what you want to do, you can do something like this:
VStack {
Path{ path in
path.move(to: CGPoint(x: 20, y: 300))
path.addLine(to: CGPoint(x: 200, y: 300))
}
.stroke(style: StrokeStyle( lineWidth: 10, dash: [5]))
.foregroundColor(Color(UIColor.blue))
}
You will get something like this:
Improved #kazi.munshimun solution. Vetical line and horizontal line:
struct VLine: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
}
}
}
struct HLine: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
}
}
}
Usage:
VLine().stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
HLine().stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
Here is an ultimate way for you to add and draw lines with more easier options:
struct CustomLineShapeWithAlignment: Shape {
let stratPoint: Alignment
let endPoint: Alignment
init(stratPoint: Alignment, endPoint: Alignment) {
self.stratPoint = stratPoint
self.endPoint = endPoint
}
private func cgPointTranslator(alignment: Alignment, rect: CGRect) -> CGPoint {
switch alignment {
case .topLeading: return CGPoint(x: rect.minX, y: rect.minY)
case .top: return CGPoint(x: rect.midX, y: rect.minY)
case .topTrailing: return CGPoint(x: rect.maxX, y: rect.minY)
case .leading: return CGPoint(x: rect.minX, y: rect.midY)
case .center: return CGPoint(x: rect.midX, y: rect.midY)
case .trailing: return CGPoint(x: rect.maxX, y: rect.midY)
case .bottomLeading: return CGPoint(x: rect.minX, y: rect.maxY)
case .bottom: return CGPoint(x: rect.midX, y: rect.maxY)
case .bottomTrailing: return CGPoint(x: rect.maxX, y: rect.maxY)
default: return CGPoint(x: rect.minX, y: rect.minY)
}
}
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: cgPointTranslator(alignment: stratPoint, rect: rect))
path.addLine(to: cgPointTranslator(alignment: endPoint, rect: rect))
}
}
}
use case:
struct ContentView: View {
var body: some View {
CustomLineShapeWithAlignment(stratPoint: .top, endPoint: .bottom)
.stroke(style: StrokeStyle(lineWidth: 1.0, dash: [5]))
.background(Color.red)
ZStack {
CustomLineShapeWithAlignment(stratPoint: .top, endPoint: .bottom)
.stroke(style: StrokeStyle(lineWidth: 1.0, dash: [5]))
.frame(width: 1.0)
CustomLineShapeWithAlignment(stratPoint: .leading, endPoint: .trailing)
.stroke(style: StrokeStyle(lineWidth: 1.0, dash: [5]))
.frame(height: 1.0)
}
.background(Color.gray)
CustomLineShapeWithAlignment(stratPoint: .topLeading, endPoint: .bottomTrailing)
.stroke(style: StrokeStyle(lineWidth: 1.0, dash: [5]))
.background(Color.blue)
}
}
result:
import SwiftUI
public struct DashedDivider: View {
private let overlayColor: Color
public init(_ overlayColor: Color = Color(UIColor.systemGray)) {
self.overlayColor = overlayColor
}
public var body: some View {
HLine()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
.foregroundColor(overlayColor)
.frame(height: 1)
}
}
struct HLine: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}
}
struct DashedDivider_Previews: PreviewProvider {
static var previews: some View {
DashedDivider()
}
}
So I've got the following code:
import SwiftUI
struct ContentView : View {
#State private var draggingLocation = CGPoint.zero
#State private var startLocation = CGPoint.zero
#State private var dragging = false
var body: some View {
let GR = DragGesture(minimumDistance: 10, coordinateSpace: .global)
.onEnded { value in
self.dragging = false
self.draggingLocation = CGPoint.zero
self.startLocation = CGPoint.zero
}
.onChanged { value in
if !self.dragging {
self.dragging = true
}
if self.startLocation == CGPoint.zero {
self.startLocation = value.startLocation
}
self.draggingLocation = value.location
}
return ZStack {
if self.dragging {
Path { path in
path.move(to: CGPoint(x: self.startLocation.x-5, y: self.startLocation.y-5))
path.addLine(to: CGPoint(x: self.draggingLocation.x-5, y: self.draggingLocation.y+5))
path.addLine(to: CGPoint(x: self.draggingLocation.x+5, y: self.draggingLocation.y-5))
path.addLine(to: CGPoint(x: self.startLocation.x+5, y: self.startLocation.y+5))
}
.fill(Color.black)
}
Circle()
.fill(self.dragging ? Color.blue : Color.red)
.frame(width: 100, height: 100)
.gesture(GR)
.offset(
x: 75,
y: 75
)
Circle()
.fill(self.dragging ? Color.blue : Color.red)
.frame(width: 100, height: 100)
.gesture(GR)
.offset(
x: -75,
y: -75
)
}
.frame(width: 400, height: 400)
.background(Color.gray)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Which results in this behavior:
I'd like to be able to drag the edge out from one circle and into the other, the problem of course is that the coordinate space of the Path is relative to the gray box (ContentView) and not global. A Path has a property coordinateSpace in the documentation but there's very little information how to use it, and googling the term with SwiftUI literally returns three results, all of which are really just links to Apple's currently sparse docs. Anyone have an idea on how to best approach this?
Coordinate spaces come in three flavours: .local, .global and .named. The first two are obvious. The third, named coordinate spaces, are extremely useful in cases like yours. They are also useful in combination with GeometryReader. For more details on that, check https://swiftui-lab.com/geometryreader-to-the-rescue/
Named coordinate spaces let you express a coordinate of one view, in the coordinate space of another. For that, SwiftUI let you specify a name for a view's coordinate space. Then, in other places of your code, you can make a reference of it. In your example, naming the coordinate of your ZStack is the way to go.
Here's the refactored code:
Note: I moved the Path below, only so that it draws in front of the circles. And also, pay attention to the first Path, which is only there to prevent what I think is a bug in ZStack.
import SwiftUI
struct ContentView : View {
#State private var draggingLocation = CGPoint.zero
#State private var startLocation = CGPoint.zero
#State private var dragging = false
var body: some View {
let GR = DragGesture(minimumDistance: 10, coordinateSpace: .named("myCoordinateSpace"))
.onEnded { value in
self.dragging = false
self.draggingLocation = CGPoint.zero
self.startLocation = CGPoint.zero
}
.onChanged { value in
if !self.dragging {
self.dragging = true
}
if self.startLocation == CGPoint.zero {
self.startLocation = value.startLocation
}
self.draggingLocation = value.location
}
return ZStack(alignment: .topLeading) {
Circle()
.fill(self.dragging ? Color.blue : Color.red)
.frame(width: 100, height: 100)
.overlay(Text("Circle 1"))
.gesture(GR)
.offset(x: 75, y: 75)
Circle()
.fill(self.dragging ? Color.blue : Color.red)
.frame(width: 100, height: 100)
.overlay(Text("Circle 2"))
.gesture(GR)
.offset(x: 200, y: 200)
if self.dragging {
Path { path in
path.move(to: CGPoint(x: self.startLocation.x-5, y: self.startLocation.y-5))
path.addLine(to: CGPoint(x: self.draggingLocation.x-5, y: self.draggingLocation.y+5))
path.addLine(to: CGPoint(x: self.draggingLocation.x+5, y: self.draggingLocation.y-5))
path.addLine(to: CGPoint(x: self.startLocation.x+5, y: self.startLocation.y+5))
}
.fill(Color.black)
}
}
.coordinateSpace(name: "myCoordinateSpace")
.frame(width: 400, height: 400, alignment: .topLeading)
.background(Color.gray)
}
}