Transform a view by dragging its edge (Swift) - swift

I try to make an app where the user can draw or add an arrow, and transform this arrow (translate, rotate, ...).
For the moment, I manage to draw the arrow and make all the transformations on it, but what I would like to do now is to be able to modify the arrow by dragging its edges.
To draw an arrow, I just create a UIView with a height of 20px (it is the thickness of the arrow), and a width of 400 (length of the arrow).
func drawArrow(frame: CGRect) {
let thicknessArrow = 20
let viewHorizontalArrow = UIView()
viewHorizontalArrow.frame.origin = CGPoint(x: 100, y: 600)
viewHorizontalArrow.frame.size = CGSize(width: 400, height: thicknessArrow)
drawDoubleArrow(viewHorizontalArrow, startPoint: CGPoint(x: 0, y: thicknessViewWithArrow/2), endPoint: CGPoint(x: viewHorizontalArrow.frame.width, y: thicknessViewWithArrow/2), lineWidth: 10, color: UIColor.blackColor())
}
After that, I transform the UIView thanks to the pan, pinch and rotate gesture.
The function "drawDoubleArrow" create an arrow with BezierPath and add it to the layer of the UIView.
I hope these explanations are clear enough :).
Could you help me to find a solution ?
Thanks !

UIBezierPath on it's own cannot easily be manipulated by gestures because it doesn't recognize them. But a UIView can recognize gestures. Combined with a CALayer's ability to be transformed and it's hitTest() method, I think you can achieve what you want.
// I'm declaring the layer as a shape layer, but depending on how your error thick is, you may need a rectangular one
let panGesture = UIPanGestureRecognizer()
var myLayer = CAShapeLayer()
viewDidLoad:
// I'm assuming you've created myBezierPath, an arrow path
myLayer.path = myBezierPath.cgPath
// Adding the pan gesture to the view's controller
panGesture.addTarget(self, action: #selector(moveLayer))
self.addGestureRecognizer(panGesture)
moveLayer:
func moveLayer(_ recognizer:UIPanGestureRecognizer) {
let p = recognizer.location(in: self)
// You will want to loop through all the layers you wish to transform
if myLayer.hitTest(p) != nil {
myLayer.position = p
}
}

Related

How do I determine the arcCenter for a UIBezierPath relative to its parent view?

I am using a CAShapeLayer with UIBezierPath to make a circular progress bar. The code requires a center value based on a CGPoint.
let circleLayer = CAShapeLayer()
let circlePath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
I have set the value manually as follows:
let center = CGPoint(x: 70, y: 70)
However those values are relative to the superView and I need to make it relative to its parent UIView.
All other elements in the UIView are constrained using programmatic UI code like this:
sampleText.translatesAutoresizingMaskIntoConstraints = false
sampleText.topAnchor.constraint(equalTo: directionsTitle.bottomAnchor, constant: 0).isActive = true
sampleText.leadingAnchor.constraint(equalTo: timerFooter.leadingAnchor, constant: 8).isActive = true
sampleText.trailingAnchor.constraint(equalTo: timerFooter.trailingAnchor, constant: -8).isActive = true
Fixed values may work fine on one device but will be out of alignment on a larger or smaller device. My question is, how do I determine the values for center of the UIView that will meet the values required of CGPoint?
I have tried this:
let centerX = view.centerXAnchor
let centerY = view.centerYAnchor
let center = CGPoint(x: centerX, y: centerY)
However, that throws an error: No exact matches in call to initializer
The following image reflects where the shape placement is relative to. The blue box indicates the desired UIView area where I want to position the circle.
Advice from wiser minds would be greatly appreciated.
Ah, ok, I think I understand what the problem is that you're facing.
I think your issue is that you're trying to use the CAShapeLayer like it is a UIView. When in reality they don't behave like that at all.
What you should be doing is creating a UIView subclass and putting the CAShapeLayer in there. Now you only need to worry about how the CAShapeLayer fits into its own UIView subclass.
To "position the layer" into some other view you actually don't need to worry about that at all. You just place the new UIView subclass and the layer will go along with it.
e.g.
class MyProgressView: UIView {
let circleLayer: CAShapeLayer
// add the circleLayer
// add properties for customising the circle layer
// Position the circle layer in the centre of the MyProgressView frame
// I'd suggest using `layoutSubviews` or something for this.
// etc...
}
Now... to position your "CircleLayer" you actually don't need to. You just position the MyProgressView where you want it to be and the layer is part of that so will go along for the ride.
From your screenshot it looks like you want the view with the blue rectangle to contain the circleLayer. If that is the case then make the blue rectangle view your custom UIView subclass and ... done. It will now contain the circle view.
RayWenderlich has some good stuff about customising views with CALayer... https://www.raywenderlich.com/10317653-calayer-tutorial-for-ios-getting-started#toc-anchor-002
It sounds like you want the midpoint of the UIView's bounds I think that's yourView.bounds.midX and yourView.bounds.midY
A view's bounds is its own coordinate space. A view's frame is its location in its parent space.
UIView has methods like convertPoint:toCoordinateSpace: and friends for converting points and rects between those spaces.

How to detect mouse event outside current NSView in MacOS?

Dues to I am using CALayer's mask attribute to draw and clip the content, the mask's size is like a NSView with a 10 pixel border, some of my content is larger than the current NSView's size with 10 pixel. But currently the mouse move event seems only detected when mouse move into the NSView area, I am trying to use NSTrackingArea the negative origin but seems not work?
Does anybody know how to detect the mouse move event outside current view?
Thanks,
Eric
More detail of my question:
I need to draw some content outside my view (which is inside a bigger view) and detect the mouse
event on my content and this is my current limitation that I can't use a larger view to draw my whole content, just consider it a special type of shadow and I need to detect mouse event on the shadow, so I use a mask to clip the whole content. Here is my code used to apple the mask. The margin is how much pixel I want to extend from my view. (apply this function to a NSView directly)
func applyMaskToView(src: NSView, margin: CGFloat) {
src.wantsLayer = true
let mask = CALayer()
mask.backgroundColor = NSColor.black.cgColor
let maskFrame = CGRect(x: -margin,
y: -margin,
width: src.frame.size.width + 2*margin,
height: src.frame.size.height + 2*margin )
mask.frame = maskFrame
src.layer?.masksToBounds = false
src.layer?.mask = mask
return
}
The code I used to apply tracking area is here (inside the NSView subclass):
override func updateTrackingAreas() {
var trackingArea: NSTrackingArea?
let options1: NSTrackingArea.Options = [.mouseEnteredAndExited,
.activeAlways,
.mouseMoved,
.enabledDuringMouseDrag]
let options2: NSTrackingArea.Options = [options1, .inVisibleRect]
for area in self.trackingAreas {
self.removeTrackingArea(area)
}
if let activeBound = self.layer?.mask?.frame {
trackingArea = NSTrackingArea.init(rect: activeBound, options: options1, owner: self, userInfo: nil)
}
else {
trackingArea = NSTrackingArea.init(rect: self.bounds, options: options2, owner: self, userInfo: nil)
}
self.addTrackingArea(trackingArea!)
super.updateTrackingAreas()
}
If I don't use tracking area, I can't receive the mouse event in the margin area mention in the above code. However I can receive "mouse move" only now.

Using particle effects or animated sks files to create filled letters

I have one .png image of a single star. I'd like to use this image to create animated star-filled letters. This is what I'd like to do (but the stars would be sort of animated within this, which I imagine can be done with particle effects):
Could I do this by using potentially several sks files for each letter and then loading them into one larger scene? In addition, if I just wanted to fill the label node with a static texture of several stars, is there an alternate way of doing this?
Not ideal, but an easy to achieve:
override func didMove(to view: SKView) {
if let nodeToMask = SKEmitterNode(fileNamed: "firelfies") {
backgroundColor = .black
let cropNode = SKCropNode()
cropNode.position = CGPoint(x: frame.midX, y: frame.midY)
cropNode.zPosition = 1
let mask = SKLabelNode(fontNamed: "ArialMT")
mask.text = "MASK"
mask.fontColor = .green
mask.fontSize = 185
cropNode.maskNode = mask
nodeToMask.position = CGPoint(x: 0, y: 0)
nodeToMask.name = "character"
cropNode.addChild(nodeToMask)
addChild(cropNode)
}
}
I think the code is self-explanatory, but basically, you just use text as a mask of a crop node, and you mask an emitter. Here is the result:
The thing with this implementation is that sparkles don't go outside of the letter bounds.

Swift 3 - Custom UIButton radius deletes constraints?

My view controller possesses a stack view with 3 buttons. One fixed in the center with a fixed width and the stackview set to fill proportionally such that the other two buttons will be symmetrical.
However I am also customizing the corner radius of the buttons and as soon as the application loads the button resizes in an undesired fashion.
Ive attempted numerous stackview distribution and fill settings. Removing the buttons from the stackview and simply trying to contraint them to edges on a normal UIView to no avail as it seems, but uncertain if, the constraints get deleted.
Visually the button will be located at the bottom right hand corner of the screen with 0 space between the edge and the button. Currently it gets laid out in a manner where there is no constraint it seems on multiple devices causing it to have a space on larger displays, and exit the screen on small displays within the simulator.
Attempted coding efforts to round the desired corners:
#IBOutlet fileprivate weak var button: UIButton! { didSet {
button.round(corners: [.topRight, .bottomLeft], radius: 50 , borderColor: UIColor.blue, borderWidth: 5.0)
button.setNeedsUpdateConstraints()
button.setNeedsLayout()
} }
override func viewDidLayoutSubviews() {
button.layoutIfNeeded()
}
ovverride func updateViewContraints() {
button.updateConstraintsIfNeeded()
super.updateViewConstraints()
}
The UIView extension that is being used can be found here:
https://stackoverflow.com/a/35621736/5434541
What solution is available to properly adjust the buttons corner radius' and allow the constraints to update the button to as it should be.
Without seeing your full code, I suspect you're getting this because it's drawing multiple layers with the border, and never removing any! If the buttons get resized, the old layers are still there. Here's a snip that shows removing the old layer on redraw that you should be able to adapt. I tested this inside a stack view, and it also behaves correctly on rotation:
class RoundedCorner: UIButton {
var lastBorderLayer:CAShapeLayer?
override func layoutSubviews() { setup() }
func setup() {
let r = self.bounds.size.height / 2
let path = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.topLeft, .bottomRight],
cornerRadii: CGSize(width: r, height: r))
let mask = CAShapeLayer()
mask.path = path.cgPath
addBorder(mask: mask, borderColor: UIColor.blue, borderWidth: 5)
self.layer.mask = mask
}
func addBorder(mask: CAShapeLayer, borderColor: UIColor, borderWidth: CGFloat) {
let borderLayer = CAShapeLayer()
borderLayer.path = mask.path
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = borderColor.cgColor
borderLayer.lineWidth = borderWidth
borderLayer.frame = bounds
if let last = self.lastBorderLayer, let index = layer.sublayers?.index(of: last) {
layer.sublayers?.remove(at: index)
}
layer.addSublayer(borderLayer)
self.lastBorderLayer = borderLayer
}
}
You also noted in your comment about the app lag. I'm not surprised, since layout can get called many times per update, and you're creating a new layer every single time. You can avoid this by saving the last dimensions of the botton frame and comparing. If it's identical, don't recreate the layer.

CornerRadius exactly UIView

I want to clip the bounds of my UIView perfectly to interact as a circle, but however I set the corner radius, mask and clip to bounds and it shows correctly, it moves as a square, as you can see in the image:
The code I have used is:
let bubble1 = UIView(frame: CGRectMake(location.x, location.y, 128, 128))
bubble1.backgroundColor = color2
bubble1.layer.cornerRadius = bubble1.frame.size.width/2
bubble1.clipsToBounds = true
bubble1.layer.masksToBounds = true
What is wrong there that does still keeping the edges of the view?
PD: All the views moves dynamically, so when it moves and hit each other, it shows these empty space, acting as an square instead of as an circle
Finally, after all I found what to implement, and was just that class instead of UIView:
class SphereView: UIView {
// iOS 9 specific
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .Ellipse
}
}
Seen here: https://objectcoder.com/2016/02/29/variation-on-dropit-demo-from-lecture-12-dynamic-animation-cs193p-stanford-university/