Creating Upside Arrow - swift3

I am trying to show arrow of my selected page menu. But, the arrow which I draw didn't show as I expected. It needs to be up
Here is my code:
class ArrowView : UIView {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
var rect: CGRect!
var arrorBackgroundColor: UIColor?
var midX: CGFloat { return rect.midX }
var midY: CGFloat { return rect.midY }
var maxX: CGFloat { return rect.maxX }
var maxY: CGFloat { return rect.maxY }
override func draw(_ rect: CGRect) {
self.rect = rect
let ctx = UIGraphicsGetCurrentContext()
ctx?.beginPath()
ctx?.move(to: CGPoint(x: 0, y: 0))
// Old code which the arrow was down
// ctx?.addQuadCurve(to: CGPoint(x:maxX * 0.2 , y: maxY * 0.2), control: CGPoint(x: maxX * 0.12, y: 0))
// ctx?.addLine(to: CGPoint(x: midX - maxX * 0.05, y: maxY * 0.9))
// ctx?.addQuadCurve(to: CGPoint(x: midX + maxX * 0.05, y: maxY * 0.9), control: CGPoint(x: midX, y: maxY))
// ctx?.addLine(to: CGPoint(x: maxX * 0.8, y: maxY * 0.2))
// ctx?.addQuadCurve(to: CGPoint(x: maxX, y: 0), control: CGPoint(x: maxX * 0.88, y: 0))
ctx?.addLine(to: CGPoint(x: midX - maxX * 106.5, y: maxY * 65.5))
ctx?.addLine(to: CGPoint(x: maxX * 128.5, y: maxY * 88.5))
ctx?.addLine(to: CGPoint(x: 85.5, y: 88.5))
ctx?.closePath()
ctx?.setFillColor((arrorBackgroundColor?.cgColor)!)
ctx?.fillPath();
}
}
And here how it goes
Actually I am trying to customize this library : https://github.com/azurechen/ACTabScrollView
And trying to archive like this : https://puu.sh/viWwc/35a6bddb0d.png
Since I am beginner with UIBezierPath , any help is appreciated.

I don't understand the constants in your calls to addLine(to:) and I think the implementation of ArrowView is unnecessarily weird.
You seem to be confused about midX/maxX/... (or I'm just too dense to get it right now). The CGRect documentation should help.
I think the original author of ArrowView (from the github project you linked to) somehow mixed up bounds and frame when setting the arrow's position. Oh, and he also abused the rect parameter in draw(_:), see the docs. Specifically (emphasis mine):
rect
The portion of the view’s bounds that needs to be updated. The
first time your view is drawn, this rectangle is typically the entire
visible bounds of your view. However, during subsequent drawing
operations, the rectangle may specify only part of your view.
Also, ACTabScrollView seems to work only sometimes. In my shallow experiments, it messed up view positioning most of the time.
Anyways, to implement a simple UIView for an upwards pointing arrow, this works:
class ArrowView : UIView {
var arrorBackgroundColor: UIColor = UIColor.red
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// Sample to really fill the background before drawing the arrow.
ctx.setFillColor(UIColor.yellow.cgColor)
ctx.fill(bounds)
ctx.beginPath()
ctx.move(to: CGPoint(x: 0, y: bounds.maxY))
ctx.addLine(to: CGPoint(x: bounds.midX, y: bounds.minY))
ctx.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
ctx.addLine(to: CGPoint(x: 0, y: bounds.maxY))
ctx.closePath()
ctx.setFillColor(arrorBackgroundColor.cgColor)
ctx.fillPath()
}
}
The core is using the view's bounds to compute the coordinates.
From the UIView docs:
The geometry of a view is defined by its frame, bounds, and center
properties. The frame defines the origin and dimensions of the view in
the coordinate system of its superview and is commonly used during
layout to adjust the size or position of the view. The center property
can be used to adjust the position of the view without changing its
size. The bounds defines the internal dimensions of the view as it
sees them and is used almost exclusively in custom drawing code. The
size portion of the frame and bounds rectangles are coupled together
so that changing the size of either rectangle updates the size of
both.
I think arrorBackgroundColor should really be called arrowColor (because it is used as the color of the arrow) and you might have another property arrorBackgroundColor that really is for the background. I added two lines that fill the background with yellow as an example.

Related

How to scale Path in SwiftUI?

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-:

How to flip Shape horizontally in SwiftUI?

I have a Shape that need to be flipped along the x-axis. I'm looking for a way to solve this with CGAffineTransform, but I've only found a way to rotate the Shape, but not flipping along an axis. I need something like .rotation3DEffect(.degrees(180), axis: (x: 1, y: 0, z: 0))for Shapes
You can use the rotation3DEffect on a shape. Here's a simple example:
struct ContentView : View {
#State var degrees : Double = 0
var body: some View {
VStack {
MyShape()
.fill(Color.red)
.rotation3DEffect(.degrees(degrees), axis: (x: 1, y: 0, z: 0))
Button("Rotate") {
withAnimation {
degrees = degrees == 0 ? 180 : 0
}
}
}
}
}
struct MyShape : Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: .zero)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
}
}
}
Just to avoid confusion, I would say that your question title and your example of rotation3DEffect code and your title (flip shape "horizontally") conflict a little in my mind -- rotating 180 degrees with axis(x:1, y:0, z:0) performs what I would consider a vertical flip. For horizontal, you can change it to axis(x:0, y:1, z:0)
Here is way to get back a Shape (some Shape, I would say):
extension Shape {
func flipped -> ScaledShape<Self> {
scale(x: -1, y: 1, anchor: .center)
}
}
If some more complex path manipulation is needed:
struct FlippedShape<Content>: Shape where Content : Shape {
let content: Content
func path(in rect: CGRect) -> Path {
let flip = CGAffineTransform(scaleX: -1, y: 1)
let position = flip.concatenating(CGAffineTransform(translationX: rect.width, y: 0))
// some other path manipulation requiring `rect`
return content.path(in: rect).applying(position)
}
}
extension Shape {
func flipped() -> FlippedShape<Self> {
FlippedShape(content: self)
}
}

SwiftUI InsettableShape with .strokeBoarder

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.

Curves and Paths in SwiftUI

I am trying to create a curved path wrapped in a shape in order to attach to a tab bar but I’m having trouble get the desired look.
Here’s the look I am going for. Any ideas on how to create this shape?
I was able to answer my own question by using the a custom path and attaching it to the tab bar.
struct Arc: Shape {
var height: CGFloat = 20
var length: CGFloat = 80
var startX: CGFloat = 0
func path(in rect: CGRect) -> Path {
var path = Path()
let midPoint: CGFloat = (startX + length) / 2
let apex1: CGFloat = (startX + midPoint) / 2
let apex2: CGFloat = (midPoint + length) / 2
path.move(to: CGPoint(x: startX, y: height))
path.addCurve(to: CGPoint(x: midPoint, y: 0), control1: CGPoint(x: apex1, y:
height), control2: CGPoint(x: apex1, y: 0))
path.addCurve(to: CGPoint(x: startX + length, y: height), control1: CGPoint(x: apex2, y: 0), control2: CGPoint(x: apex2, y: height))
return path
}
}

SwiftUI Path: upside down and wrong Pivot in Scenekit

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...