I am creating a simple path between two coordinates ([100, 100], [300, 300]) and I want the frame of that path to be only the size it needs to be (i.e., 200 x 200). For some reason, while the frame shrinks to the desired 200 x 200 size, the path starts being drawn in the middle of the frame and therefore overflows.
How can I centre the path within a frame? I tried assigning alignment: .topLeading on the path frame parameter but that didn't change anything.
Code:
struct RelationView: View {
let startPoint = CGPoint(x: 100, y: 100)
let endPoint = CGPoint(x: 300, y: 300)
var body: some View {
Path({ path in
path.addLines([
startPoint,
CGPoint(x: (startPoint.x + endPoint.x) / 2, y: startPoint.y),
CGPoint(x: (startPoint.x + endPoint.x) / 2, y: endPoint.y),
endPoint
])
})
.stroke()
.frame(width: 200, height: 200)
}
}
Result:
Related
I've found an animated custom Tab bar tutorial and tried to reproduce it. But now I'm stuck on the moment where I need to place the center of my custom shape in the center of the button every time it is pressed. Would be very appreciate if somebody could explain to me why does midX is located between buttons?
Here is expecting result :
And here is what I get when taping on the button with code below:
Code:
struct TabBar: View{
#State private var selectedTab: Tabs.RawValue = Tabs.home.rawValue
#State private var xAxisForCurve: CGFloat = 80
let screenWidth: CGFloat
private var tabs : [String]{
var _tabs: [String] = []
Tabs.allCases.forEach { tab in
_tabs.append(tab.rawValue)
}
return _tabs
}
var body: some View{
HStack{
ForEach(tabs, id: \.self) { tab in
GeometryReader { button in
Button(action: {
withAnimation(.spring()){
self.selectedTab = tab
self.xAxisForCurve = button.frame(in: .global).midX
}
}) {
Image(systemName: tab)
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.foregroundColor(self.selectedTab == tab ? Color.blue : Color.gray)
}
}
.frame(width: 28, height: 28)
if tab != tabs.last{
Spacer().frame(width: 65)
}
}
}
.frame(width: screenWidth * multiplayerForTabFrameOnDifferentDevices())
.padding(.vertical)
.background(Color.white.clipShape(MyShape(xAxis: self.xAxisForCurve)))
.cornerRadius(20)
}
}
struct MyShape: Shape{
var xAxis: CGFloat
func path(in rect: CGRect) -> Path {
Path{ path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
let upperLeftCorner = CGPoint(x: self.xAxis - 50 , y: rect.minY)
let upperRightCorner = CGPoint(x: self.xAxis + 50, y: rect.minY)
let curveCenter = CGPoint(x: self.xAxis, y: rect.height * 0.55)
let control1 = CGPoint(x: self.xAxis - 25 , y: 0)
let control2 = CGPoint(x: self.xAxis - 25 , y: 35)
let control4 = CGPoint(x: self.xAxis + 25 , y: 0)
let control3 = CGPoint(x: self.xAxis + 25 , y: 35)
path.move(to: upperLeftCorner)
path.addCurve(to: curveCenter, control1: control1, control2: control2)
path.addCurve(to: upperRightCorner, control1: control3, control2: control4)
}
}
}
You're taking global frame with frame(in: .global): it's frame related to the device screen.
But your shape needs position related to your TabBar bounds.
You can solve it by adding a named coordinate space:
.frame(width: screenWidth * multiplayerForTabFrameOnDifferentDevices())
.coordinateSpace(name: "container")
And getting button frame in this coordinate space:
xAxisForCurve = button.frame(in: .named("container")).midX
p.s. from your code it looks like you're trying to animate your shape. You need to add the following lines to your shape to make it animatable. More info can be found here
var animatableData: CGFloat {
get { xAxis }
set { xAxis = newValue }
}
I have some problems to scale a Path accordingly to my purpose. I'm getting from an API some SVG data that I would like to draw in my view. In order to make it simple let's say this is what I get from the API:
extension UIBezierPath {
static var rectangle: UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 200, y: 100))
path.addLine(to: CGPoint(x: 100, y: 300))
path.addLine(to: CGPoint(x: 300, y: 300))
path.addLine(to: CGPoint(x: 200, y: 100))
return path
}
}
When I display this path in my view everything works fine.
Since the SVG coordinates from the API have an offset and are sometimes bigger than the display screen of the iPhone, I need to scale them down/up to a specific size and center them in my view. In order to do that I tried this transformation:
struct ScaledShapeView: Shape {
let bezier: UIBezierPath
func path(in rect: CGRect) -> Path {
let bounds = bezier.bounds
let scaleX = rect.size.width/bounds.size.width
let scaleY = rect.size.height/bounds.size.height
let scale = max(scaleX, scaleY)
return Path(bezier.cgPath).applying(CGAffineTransform(scaleX: scale, y: scale))
}
}
and uses it in my view:
struct TestView: View {
var body: some View {
ScaledShapeView(bezier: .rectangle)
.frame(width: 100, height: 100)
}
}
But this is my result:
I'm not sure what exactly the problem is. I would like to achieve that the shape is in the middle of the frame.
#SwiftPunk:
When I try
var body: some View {
Path(UIBezierPath.logo1.cgPath)
.frame(width: 100, height: 100)
.scaleEffect(CGSize(width: 0.5, height: 0.5))
}
I get this result:
Normalized each X and Y position for drawing the points between 0 and 1, where 0 means the top edge or the leading edge, and 1 means the bottom edge or the trailing edge.
Also,we’re going to find the minimum of width and height so we can scale our Bezier path proportionally, so it stays the same shape as it grows.
import SwiftUI
extension UIBezierPath{
static var rectangle: UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0.5, y: 0.2))
path.addLine(to: CGPoint(x: 0.2, y: 0.8))
path.addLine(to: CGPoint(x: 0.8, y: 0.8))
path.addLine(to: CGPoint(x: 0.5, y: 0.2))
return path
}
}
struct ScaledShapeView:Shape{
var bezier:UIBezierPath
func path(in rect: CGRect) -> Path {
let path = Path(bezier.cgPath)
let multiplier = min(rect.width,rect.height)
let transform = CGAffineTransform(scaleX:multiplier , y: multiplier)
return path.applying(transform)
}
}
struct TestView: View {
#State private var endAmount: CGFloat = 0
var body: some View {
ScaledShapeView(bezier: UIBezierPath.rectangle)
.frame(width: 300, height: 300)
}
}
Output-:
I have created two triangle shapes and I am trying to color each of them individually when I click on it. The #State variable is created inside PaintView which creates a Shape that contains a single path.
The behavior I’m seeing is that whenever I click on a shape, it re-creates both shapes, instead of only the one I’m tapping on (I can observe this based on the print that I've added to BuildShape).
Now, my understanding is that whenever a state changes, SwiftUI will re-render the entire View that contains that #State, and its child views, but since my shapes are in different views that don't relate, why is SwiftUI re-rendering both of them?
This diagram shows how the views related to each other:
For example, this is what happens when I tap on the bottom triangle:
When I tap on the bottom triangle for the first time, SwiftUI re-creates the bottom triangle twice, and the top triangle once
Path: 0 0 m 100 100 l 200 0 l
Path: 100 100 m 0 200 l 200 200 l
Path: 100 100 m 0 200 l 200 200 l
When I tap on the bottom triangle again (and for as many other times), SwiftUI re-creates the bottom triangle once, and the top triangle once
Path: 0 0 m 100 100 l 200 0 l
Path: 100 100 m 0 200 l 200 200 l
This is the code I'm using:
import SwiftUI
import UIKit
struct BuildShape: Shape {
let path: Path
func path(in rect: CGRect) -> Path {
print("Path:", path)
return path
}
}
struct PaintView: View {
let path: Path
#State var color = Color.black
var body: some View {
BuildShape(path: path)
.fill(self.color)
.onTapGesture(count: 1) {
self.color = Color.blue
}
}
}
struct DrawView: View {
var triangle1Path: Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: 200, y: 0))
return path
}
var triangle2Path: Path {
var path = Path()
path.move(to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: 0, y: 200))
path.addLine(to: CGPoint(x: 200, y: 200))
return path
}
var body: some View {
ZStack {
PaintView(path: triangle1Path)
PaintView(path: triangle2Path)
}
}
}
The views containing triangle shapes are overlap (because they are in ZStack), and as views by default are not opaque, changing one can affects resulting appearance so redrawn all in ZStack.
ZStack {
PaintView(path: triangle1Path)
PaintView(path: triangle2Path)
}
Put them for example into VStack and you'll see that only tapped is redrawn, because rendering engine knows now that views independent.
VStack {
PaintView(path: triangle1Path)
PaintView(path: triangle2Path)
}
Can someone please explain why my self made "myRectangle" that I have conformed to InsettableShape doesn't work with .strokeBorder but the built in Rectangle() does?
Here is myRectangle code;
struct myRectangle: InsettableShape {
var insetAmount: CGFloat = 0
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX * 1.2, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX * 1.2, y: rect.midY * 0.6))
path.addLine(to: CGPoint(x: rect.midX * 0.8, y: rect.midY * 0.6))
path.addLine(to: CGPoint(x: rect.midX * 0.8, y: rect.maxY))
return path
}
func inset(by amount: CGFloat) -> some InsettableShape {
var rectangle = self
rectangle.insetAmount -= amount
return rectangle
}
}
then modifying it with the .strokeBorder;
struct ColorCyclingRectangle: View {
var amount = 0.0
var steps = 100
var body: some View {
ZStack {
ForEach(0..<steps) { value in
myRectangle()
.inset(by: CGFloat(value))
.strokeBorder(self.color(for: value, brightness: 1), lineWidth: 2)
}
}
.drawingGroup()
}
func color(for value: Int, brightness: Double) -> Color {
var targetHue = Double(value) / Double(self.steps) + self.amount
if targetHue > 1 {
targetHue -= 1
}
return Color(hue: targetHue, saturation: 1, brightness: brightness)
}
}
I am expecting the rainbow effect but just get a gradient line.
I have tried modifying my path to - insetAmount but I just get a weird shape and the rainbow border is straight, not square
Am I misunderstanding somthing or does this modifyer only work on the default shapes?
Though this is an older questions I will make an attempt to answer it in hopes the OP is still interested or that it may help others.
You can most definitely use the strokeBorder() modifier on custom types that conform to the InsettableShape protocol noting that it is your responsibility to write the code that insets - reduces size - of your custom shape accordingly.
The first item I noticed in your code is that you are hardcoding some values within your path(in rect: CGRect) method which is why your output is not a square. Two options for this are:
Add width and height variables to myRectangle and use them in your path(in rect: CGRect) to create the shape
Create the shape using the entire size of rect and utilize the .frame() modifier on the container where you are creating your custom shape or on the shape directly to control its dimensions.
I am not sure which is preferable, but I will use option 2 as it seems this is how the built-in shapes work - in relation to width and height at least - and it makes it easier to comprehend the insetting code as follows
struct myRectangle: InsettableShape {
var insetAmount: CGFloat = 0
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX + insetAmount, y: rect.maxY - insetAmount))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.maxY - insetAmount))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY + insetAmount))
path.addLine(to: CGPoint(x: rect.minX + insetAmount, y: rect.minY + insetAmount))
path.addLine(to: CGPoint(x: rect.minX + insetAmount, y: rect.maxY - insetAmount))
return path
}
func inset(by amount: CGFloat) -> some InsettableShape {
var rectangle = self
rectangle.insetAmount += amount
return rectangle
}
}
In the above you will note:
I removed the 1.2, 0.8 and 0.6 and instead utilized minX, maxX, minY and maxY to create the shape utilizing the entirety of rect size
To inset a rectangle you need to bring each of the edge points inwards by the inset amount taking account that SwiftUI measures coordinates from top-left, so you can say top-left would be (0,0)
I have used the insetAmount variable within the code in path(in rect: CGRect) that creates the path. For example path.move(to: CGPoint(x: rect.minX + insetAmount, y: rect.maxY - insetAmount)) is the bottom-left point. To inset this point you need to move the X to the right rect.minX + insetAmount and Y upwards rect.maxY - insetAmount - we are subtracting from Y to move upwards per my point 2.
In your ColorCyclingRectangle custom View I only added a .frame(width: 300, height: 300) modifier to the ZStack container that you are creating your custom shape in so that I can control its size.
struct ColorCyclingRectangle: View {
var amount = 0.0
var steps = 100
var body: some View {
ZStack {
ForEach(0..<steps) { value in
myRectangle()
.inset(by: CGFloat(value))
.strokeBorder(self.color(for: value, brightness: 1), lineWidth: 2)
}
}
.drawingGroup()
.frame(width: 300, height: 300)
}
func color(for value: Int, brightness: Double) -> Color {
var targetHue = Double(value) / Double(self.steps) + self.amount
if targetHue > 1 {
targetHue -= 1
}
return Color(hue: targetHue, saturation: 1, brightness: brightness)
}
}
With this I was able to get the rainbow effect.
Hopefully this resolves your issue.
Problem:
When I parse a SwiftUI Path to Scenekit, it i upside down and the pivot is outside the bounding box.
I tried:
Setting SwiftUI .anchorPreference to center. Didn't make any difference.
Goal:
How can I make the pivot the center bottom of the bounding box?
Important: I need to have a correct SwiftUI Path -> SCN Pivot translation, and not manually offsetting the pivot, as this would affect all objects pivot on scene
import SwiftUI
struct PatternScene: View {
let idx: Int
let pattern = Path { path in
path.move(to: CGPoint(x: 50, y: 50))
path.addLine(to: CGPoint(x: 250, y: 50))
path.addLine(to: CGPoint(x: 250, y: 250))
path.addLine(to: CGPoint(x: 50, y: 250))
path.addLine(to: CGPoint(x: 100, y: 100))
}
var body: some View {
self.pattern
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.anchorPreference(key: MyTextPreferenceKey.self, value: .bounds, transform: { [MyTextPreferenceData(viewIdx: self.idx, bounds: $0)] })
}
}
struct PatternScene_Previews: PreviewProvider {
static var previews: some View {
PatternScene(idx: 0)
}
}
//Connect PatternScene with Scenekit
let scnpath = PatternScene(idx: 0).pattern.cgPath
let shape = SCNShape(path: UIBezierPath(cgPath: scnpath), extrusionDepth: 1)
let material = SCNMaterial()
//material.diffuse.contents = UIColor.red
material.isDoubleSided = true
// shape.firstMaterial = material
let shapeNode = SCNNode(geometry: shape)
scene.rootNode.addChildNode(shapeNode)
shapeNode.position = SCNVector3(x: 0, y: 0, z: 0)
shapeNode.scale = SCNVector3(x: 1, y: 1, z: 1)
shapeNode.pivot = SCNMatrix4MakeTranslation(0, 0, 0)
UIKit and SwiftUI use a flipped geometry where y values increase as you move from the top to the bottom of the canvas.
Scenekit does not use flipped geometry. So you have to "flip" the y values...
On a layer you can even set this value...https://developer.apple.com/documentation/quartzcore/calayer/1410960-geometryflipped ...but i think there is not such a value for scenekit...