How can I limit a Custom Shape to safe area in SwiftUI? - swift

I have 2 Shapes CustomShape1 is limited to safeArea and it is okay, but CustomShape2 is ignores safeArea out of the box, which I am not okay with it. In CustomShape1 I could pass the origin and size to limit my Shape available Space to SafeArea, but I do not have or I do not see a way to pass origin or size for CustomShape2 which is just a Circle, so how can I solve this issue, that CustomShape2 respect safeArea as well right of the box?
I found out adding padding modifier would solve the issue not in the way I wanted, I do not want use view modifier to solve shape issue. As you can see adding .padding(.vertical, 0.1) would limit the view to safeArea, I do know should I call this a bug or what!
struct ContentView: View {
var body: some View {
ZStack {
CustomShape1()
.fill(Color.red)
CustomShape2()
.fill(Color.blue)
.background(Color.black.opacity(0.8))
//.padding(.vertical, 0.1) // Adding This Would solve the issue!
}
}
}
struct CustomShape1: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.addRect(CGRect(origin: rect.origin, size: rect.size))
}
}
}
struct CustomShape2: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: 50, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: true)
}
}
}

Set .clipped().
.background(Color.blue.opacity(0.8).clipped()) // <===Here

Related

How to adapt frame of half circle to its size

I am making an app where I need to use half circle. But I can't make its frame match the real size of the object. Right now I am trimming the circle but I don't really know you to actually crop the view.
Here is how I trimming the circle:
Circle()
.trim(from: 0, to: 0.5)
.fill(Color.blue)
.background(Color.red)
And I am getting colour red as background twice bigger the my new object.
It's probably better to make your own HalfCircle that conforms to Shape instead of trying to bash Circle into the shape you need. Let's go the extra mile and make it conform to InsettableShape, in case you want to use it to stroke a border.
import SwiftUI
struct HalfCircle: InsettableShape {
var _inset: CGFloat = 0
func inset(by amount: CGFloat) -> Self {
var copy = self
copy._inset += amount
return copy
}
func path(in rect: CGRect) -> Path {
var path = Path()
// This is a half-circle centered at the origin with radius 1.
path.addArc(
center: .zero,
radius: 1,
startAngle: .zero,
endAngle: .radians(.pi),
clockwise: false
)
path.closeSubpath()
// Since it's the bottom half of a circle, we only want
// to inset the left, right, and bottom edges of rect.
let rect = rect
.insetBy(dx: _inset, dy: 0.5 * _inset)
.offsetBy(dx: 0, dy: -(0.5 * _inset))
// This transforms bounding box of the path to fill rect.
let transform = CGAffineTransform.identity
.translatedBy(x: rect.origin.x + 0.5 * rect.size.width, y: 0)
.scaledBy(x: rect.width / 2, y: rect.height)
return path.applying(transform)
}
}
We can use it to draw this:
using this playground:
import PlaygroundSupport
PlaygroundPage.current.setLiveView(HalfCircle()
.inset(by: 20)
.fill(.black)
.background(.gray)
.aspectRatio(2, contentMode: .fit)
.frame(width: 200)
.padding()
)
You can mask it differently using a GeometryReader:
GeometryReader { proxy in
Circle()
.fill(.blue)
.offset(y: -proxy.size.height) // <- Shows bottom part
.frame(height: proxy.size.width) // <- Makes the circle match frame
.background(.red) // <- Just for testing
}
.aspectRatio(2, contentMode: .fit) // <- Makes the frame ration 2 : 1 (landscape rectangle)
.clipped() // <- Removes out of bound drawings

SwiftUI, create a circle for each CGpoint in Array

I have an array of CGPoint and I'm trying to draw a little circle for each CGPoint location in this array (Using SwiftUI).
I have tried different approaches but have not been successful, I'm looking for some help.
my array : CGPoint is called "allFaceArray"
first try(doesn't working) throws an error : Generic struct 'ForEach' requires that 'CGPoint' conform to 'Hashable'
struct FaceFigure : View {
#ObservedObject var ce : CameraEngine
var size: CGSize
var body: some View {
if !ce.allFaceArray.isEmpty{
ZStack{
ForEach(ce.allFaceArray, id: \.self) { point in
Circle().stroke(lineWidth: 2.0).fill(Color.green)
}
}
}
}
}
second try I make a custom shape
struct MakeCircle: Shape {
var arrayViso: [CGPoint]
func path(in rect: CGRect) -> Path {
var path = Path()
for punti in arrayViso{
let radius = 2
path.addArc(
center: punti,
radius: CGFloat(radius),
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 360),
clockwise: true)
}
return path
}
}
and I use it like this:
struct FaceFigure : View {
#ObservedObject var ce : CameraEngine
var size: CGSize
var body: some View {
if !ce.allFaceArray.isEmpty{
ZStack{
MakeCircle(arrayViso: ce.allFaceArray)
.stroke(lineWidth: 2.0)
.fill(Color.green)
}
}
}
}
but like this every points is connect each other with a line I don't know why...
thanks for the help
For your second approach, you can create a separate path for each point in your array and add them to the path you already defined as var path = Path(). This way your second approach would work fine
struct MakeCircle: Shape {
var arrayViso: [CGPoint]
func path(in rect: CGRect) -> Path {
var path = Path()
for punti in arrayViso{
var circle = Path()
let radius = 2
circle.addArc(
center: punti,
radius: CGFloat(radius),
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 360),
clockwise: true)
path.addPath(circle)
}
return path
}
}
When you find Generic struct 'ForEach' requires that 'CGPoint' conform to 'Hashable', making CGPoint conform to Hashable would be one way to solve it.
Please use this extension and re-try your first try:
extension CGPoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}
The first method can be used. Although you can't directly call ForEach() for CGPoint arrays. Instead you can use ForEach with a Range<T> like follows
struct ContentView: View {
var allFaceArray = [
CGPoint(x: 0, y: 0),
CGPoint(x: 30, y: 50),
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 500),
]
var body: some View {
ZStack {
ForEach(0..<allFaceArray.count) { x in //<- Passing the count of the array as a range
Circle().stroke(lineWidth: 2.0).fill(Color.green)
.position(allFaceArray[x])
}
}
}
}
The output of the above code is,

How to draw a radial slice of a circle in SwiftUI ? (ie. pie chart like)

I am trying to create a slice of a circle in SwiftUI.
I am playing around with trim but it behaves oddly.
It picks the amount starting from the outmost point of the area.
Such that 0.5 is half a circle ok. But 0.25 of a circle is not a quarter slice (as a naive programming newbie would guess) but rather the shape filled in a non logical way. There also doesnt seem a way to modify this.
It would be sensible to define a Circle by saying Circle(radius: x, angle: y) instead such that angle: 360 would be a full circle filled.
Does one need to program this oneself in SwiftUI?
import SwiftUI
struct CircleView: View {
var body: some View {
let trimValue: CGFloat = 0.25
Circle()
.trim(from: 0, to: trimValue)
.fill(Color.red)
}
}
struct CircleView_Previews: PreviewProvider {
static var previews: some View {
CircleView()
}
}
Using a Path with an arc is the normal way to do this:
struct PieSegment: Shape {
var start: Angle
var end: Angle
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: center)
path.addArc(center: center, radius: rect.midX, startAngle: start, endAngle: end, clockwise: false)
return path
}
}
struct ContentView: View {
#State var fileData: Data?
var body: some View {
PieSegment(start: .zero, end: .degrees(45))
}
}

Applying gradient to UIBezierPath in SwiftUI Shape

I'm trying to accomplish the following:
struct MultiSegmentPaths: Shape {
func path(in rect: CGRect) -> Path {
let path1 = UIBezierPath()
// apply gradient to path 1
let path2 = UIBezierPath()
// apply gradient to path 2
return Path(path1.append(path2).cgPath)
}
}
Now I thought I could wrap each segment with Path, but that returns some View which means I can't append the two together.
And applying a gradient directly to UIBezierPath requires having access to context - which isn't passed in via the path(CGRect) -> Path method.
Finally the reason for creating the two paths within the same Shape is because I need to scale them while maintaining their offsets to each other.
The UIBezierPath(UIKIt) and Path(SwiftUI) are different things... To create Path you have to use its own instruments. Next, the fill operation (w/ gradient or anything) is drawing time operation, not part of path/shape creation.
Here is some example of combining to shapes filled by gradients. Tested with Xcode 11.4.
struct DemoShape1: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.addRect(rect)
}
}
}
struct DemoShape2: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.height/2, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: true)
}
}
}
struct DemoCombinedShape: View {
var gradient1 = LinearGradient(gradient: Gradient(colors:[.blue, .yellow]), startPoint: .top, endPoint: .bottom)
var gradient2 = LinearGradient(gradient: Gradient(colors:[.red, .black]), startPoint: .leading, endPoint: .trailing)
var body: some View {
// both shapes are rendered in same coordinate space
DemoShape1().fill(gradient1)
.overlay(DemoShape2().fill(gradient2))
}
}
struct TestCombinedShape_Previews: PreviewProvider {
static var previews: some View {
DemoCombinedShape()
.frame(width: 400, height: 300)
}
}

How to Animate Path in SwiftUI

Being unfamiliar with SwiftUI and the fact that there is not much documentation on this new framework yet. I was wondering if anyone was familiar with how it would be possible to animate a Path in SwiftUI.
For example given a view, lets say this simple RingView:
struct RingView : View {
var body: some View {
GeometryReader { geometry in
Group {
// create outer ring path
Path { path in
path.addArc(center: center,
radius: outerRadius,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 360),
clockwise: true)
}
.stroke(Color.blue)
// create inner ring
Path { path in
path.addArc(center: center,
radius: outerRadius,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 180),
clockwise: true)
}
.stroke(Color.red)
.animation(.basic(duration: 2, curve: .linear))
}
}
.aspectRatio(1, contentMode: .fit)
}
}
What is displayed is:
Now I was wondering how we could go about animating the inner ring, that is the red line inside the blue line. The animation I'm looking to do would be a simple animation where the path appears from the start and traverses to the end.
This is rather simple using CoreGraphics and the old UIKit framework but it doesn't seem like adding a simple .animation(.basic(duration: 2, curve: .linear)) to the inner path and displaying the view with a withAnimation block does anything.
I've looked at the provided Apple tutorials on SwiftUI but they really only cover move/scale animations on more in-depth views such as Image.
Any guidance on how to animate a Path or Shape in SwiftUI?
Animation of paths is showcased in the WWDC session 237 (Building Custom Views with SwiftUI). The key is using AnimatableData. You can jump ahead to 31:23, but I recommend you start at least at minute 27:47.
You will also need to download the sample code, because conveniently, the interesting bits are not shown (nor explained) in the presentation: https://developer.apple.com/documentation/swiftui/drawing_and_animation/building_custom_views_in_swiftui
More documentation:
Since I originally posted the answer, I continued to investigate how to animate Paths and posted an article with an extensive explanation of the Animatable protocol and how to use it with Paths: https://swiftui-lab.com/swiftui-animations-part1/
Update:
I have been working with shape path animations. Here's a GIF.
And here's the code:
IMPORTANT: The code does not animate on Xcode Live Previews. It needs to run either on the simulator or on a real device.
import SwiftUI
struct ContentView : View {
var body: some View {
RingSpinner().padding(20)
}
}
struct RingSpinner : View {
#State var pct: Double = 0.0
var animation: Animation {
Animation.basic(duration: 1.5).repeatForever(autoreverses: false)
}
var body: some View {
GeometryReader { geometry in
ZStack {
Path { path in
path.addArc(center: CGPoint(x: geometry.size.width/2, y: geometry.size.width/2),
radius: geometry.size.width/2,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 360),
clockwise: true)
}
.stroke(Color.green, lineWidth: 40)
InnerRing(pct: self.pct).stroke(Color.yellow, lineWidth: 20)
}
}
.aspectRatio(1, contentMode: .fit)
.padding(20)
.onAppear() {
withAnimation(self.animation) {
self.pct = 1.0
}
}
}
}
struct InnerRing : Shape {
var lagAmmount = 0.35
var pct: Double
func path(in rect: CGRect) -> Path {
let end = pct * 360
var start: Double
if pct > (1 - lagAmmount) {
start = 360 * (2 * pct - 1.0)
} else if pct > lagAmmount {
start = 360 * (pct - lagAmmount)
} else {
start = 0
}
var p = Path()
p.addArc(center: CGPoint(x: rect.size.width/2, y: rect.size.width/2),
radius: rect.size.width/2,
startAngle: Angle(degrees: start),
endAngle: Angle(degrees: end),
clockwise: false)
return p
}
var animatableData: Double {
get { return pct }
set { pct = newValue }
}
}