Draggable connected UIViews in Swift - ios

I am trying to create some draggable UIViews that are connected by lines. See image below:
I can create the draggable circles by creating a class that is a subclass of UIView and overriding the draw function
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: rect)
let circleColor:UIColor
switch group {
case .forehead:
circleColor = UIColor.red
case .crowsFeetRightEye:
circleColor = UIColor.green
case .crowsFeetLeftEye:
circleColor = UIColor.blue
}
circleColor.setFill()
path.fill()
}
and then add a pan gesture recognizer for the dragging
func initGestureRecognizers() {
let panGR = UIPanGestureRecognizer(target: self, action: #selector(DragPoint.didPan(panGR:)))
addGestureRecognizer(panGR)
}
#objc func didPan(panGR: UIPanGestureRecognizer) {
if panGR.state == .changed {
self.superview!.bringSubview(toFront: self)
let translation = panGR.translation(in: self)
self.center.x += translation.x
self.center.y += translation.y
panGR.setTranslation(CGPoint.zero, in: self)
}
}
However, I am totally stuck on how to go about the connecting lines and parenting the start/end points to its corresponding circle when dragged. Is there anybody who can help or point me in the right direction please?

You want to use CAShapeLayers with UIBezier paths to draw the lines between the circles and then change the paths when the user moves the views.
Here is a playground showing an implementation. You can copy and paste this into a playground to see it in action.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class CircleView : UIView {
var outGoingLine : CAShapeLayer?
var inComingLine : CAShapeLayer?
var inComingCircle : CircleView?
var outGoingCircle : CircleView?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = self.frame.size.width / 2
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func lineTo(circle: CircleView) -> CAShapeLayer {
let path = UIBezierPath()
path.move(to: self.center)
path.addLine(to: circle.center)
let line = CAShapeLayer()
line.path = path.cgPath
line.lineWidth = 5
line.strokeColor = UIColor.red.cgColor
circle.inComingLine = line
outGoingLine = line
outGoingCircle = circle
circle.inComingCircle = self
return line
}
}
class MyViewController : UIViewController {
let circle1 = CircleView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
let circle2 = CircleView(frame: CGRect(x: 100, y: 200, width: 50, height: 50))
let circle3 = CircleView(frame: CGRect(x: 100, y: 300, width: 50, height: 50))
let circle4 = CircleView(frame: CGRect(x: 100, y: 400, width: 50, height: 50))
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
circle1.backgroundColor = .red
view.addSubview(circle1)
circle2.backgroundColor = .red
view.addSubview(circle2)
circle3.backgroundColor = .red
view.addSubview(circle3)
circle4.backgroundColor = .red
view.addSubview(circle4)
circle1.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:))))
circle2.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:))))
circle3.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:))))
circle4.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(didPan(gesture:))))
view.layer.addSublayer(circle1.lineTo(circle: circle2))
view.layer.addSublayer(circle2.lineTo(circle: circle3))
view.layer.addSublayer(circle3.lineTo(circle: circle4))
}
#objc func didPan(gesture: UIPanGestureRecognizer) {
guard let circle = gesture.view as? CircleView else {
return
}
if (gesture.state == .began) {
circle.center = gesture.location(in: self.view)
}
let newCenter: CGPoint = gesture.location(in: self.view)
let dX = newCenter.x - circle.center.x
let dY = newCenter.y - circle.center.y
circle.center = CGPoint(x: circle.center.x + dX, y: circle.center.y + dY)
if let outGoingCircle = circle.outGoingCircle, let line = circle.outGoingLine, let path = circle.outGoingLine?.path {
let newPath = UIBezierPath(cgPath: path)
newPath.removeAllPoints()
newPath.move(to: circle.center)
newPath.addLine(to: outGoingCircle.center)
line.path = newPath.cgPath
}
if let inComingCircle = circle.inComingCircle, let line = circle.inComingLine, let path = circle.inComingLine?.path {
let newPath = UIBezierPath(cgPath: path)
newPath.removeAllPoints()
newPath.move(to: inComingCircle.center)
newPath.addLine(to: circle.center)
line.path = newPath.cgPath
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
PlaygroundPage.current.needsIndefiniteExecution = true

Related

How to make custom ripples like Square, Stare and other custom shapes in Swift 5?

I have facing issue to make ripples in Square and Stare figure like YRipple
Please help me and suggestion always welcome.
One easy way to achieve this is to use UIView animations. Each ripple is simply an instance of UIView. The shape can then be simply defined, drawn in one of many ways. I am using the override of draw rect method:
class RippleEffectView: UIView {
func addRipple(at location: CGPoint) {
let minRadius: CGFloat = 5.0
let maxRadius: CGFloat = 100.0
let startFrame = CGRect(x: location.x - minRadius, y: location.y - minRadius, width: minRadius*2.0, height: minRadius*2.0)
let endFrame = CGRect(x: location.x - maxRadius, y: location.y - maxRadius, width: maxRadius*2.0, height: maxRadius*2.0)
let view = ShapeView(frame: startFrame)
view.shape = .star(cornerCount: 5)
view.backgroundColor = .clear
view.contentMode = .redraw
view.strokeColor = .black
view.strokeWidth = 5.0
addSubview(view)
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.allowUserInteraction]) {
view.frame = endFrame
view.alpha = 0.0
} completion: { _ in
view.removeFromSuperview()
}
}
}
private class ShapeView: UIView {
var fillColor: UIColor?
var strokeColor: UIColor?
var strokeWidth: CGFloat = 0.0
var shape: Shape = .rectangle
override func draw(_ rect: CGRect) {
super.draw(rect)
let path = generatePath()
path.lineWidth = strokeWidth
if let fillColor = fillColor {
fillColor.setFill()
path.fill()
}
if let strokeColor = strokeColor {
strokeColor.setStroke()
path.stroke()
}
}
private func generatePath() -> UIBezierPath {
switch shape {
case .rectangle: return UIBezierPath(rect: bounds.insetBy(dx: strokeWidth*0.5, dy: strokeWidth*0.5))
case .oval: return UIBezierPath(ovalIn: bounds.insetBy(dx: strokeWidth*0.5, dy: strokeWidth*0.5))
case .anglesOnCircle(let cornerCount):
guard cornerCount > 2 else { return .init() }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height)*0.5 - strokeWidth*0.5
let path = UIBezierPath()
for index in 0..<cornerCount {
let angle = CGFloat(index)/CGFloat(cornerCount) * (.pi*2.0)
let point = CGPoint(x: center.x + cos(angle)*radius,
y: center.y + sin(angle)*radius)
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.close()
return path
case .star(let cornerCount):
guard cornerCount > 2 else { return .init() }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let outerRadius = min(bounds.width, bounds.height)*0.5 - strokeWidth*0.5
let innerRadius = outerRadius*0.7
let path = UIBezierPath()
for index in 0..<cornerCount*2 {
let angle = CGFloat(index)/CGFloat(cornerCount) * .pi
let radius = index.isMultiple(of: 2) ? outerRadius : innerRadius
let point = CGPoint(x: center.x + cos(angle)*radius,
y: center.y + sin(angle)*radius)
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.close()
return path
}
}
}
private extension ShapeView {
enum Shape {
case rectangle
case oval
case anglesOnCircle(cornerCount: Int)
case star(cornerCount: Int)
}
}
I used it in a view controller where I replaced main view with this ripple view in Storyboard.
class ViewController: UIViewController {
private var rippleView: RippleEffectView? { view as? RippleEffectView }
override func viewDidLoad() {
super.viewDidLoad()
rippleView?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
#objc private func onTap(_ recognizer: UIGestureRecognizer) {
let location = recognizer.location(in: rippleView)
rippleView?.addRipple(at: location)
}
}
I hope the code speaks for itself. It should be no problem to change colors. You could apply some rotation by using transform on each ripple view...
You could even use images instead of shapes. If image is set to be as templates you could even change colors using tint property on image view... So limitless possibilities.

How do you determine the drag and drop location in Swift 5?

I have mocked up a simple example of what I am trying to accomplish:
A ViewController contains 4 "drop zone" UIImageViews (e.g. dropZone1). A 5th UIImageView (playerCard) can be dragged and dropped onto any of the drop zones, but nowhere else.
I cannot figure out the way to determine which of the 4 drop zones is where the user has dragged and dropped the playerCard.
My thought was to set some sort of variable in dropInteraction canHandle and then use that in dropInteraction performDrop to take the appropriate action. But I can't figure out how to do it.
class ViewController: UIViewController {
let bounds = UIScreen.main.bounds
let imageViewWidth: CGFloat = 100
let imageViewHeight: CGFloat = 200
let inset: CGFloat = 40
var arrayDropZones = [DropZoneCard]()
var initialFrame: CGRect {
get {
return CGRect(x: bounds.width - imageViewWidth,
y: bounds.height - imageViewHeight,
width: imageViewWidth,
height: imageViewHeight
)
}
}
override func viewDidLoad() {
super.viewDidLoad()
addDropZones()
addNewCard()
}
}
extension ViewController {
func addDropZones() {
let dropZone1 = getDropZoneCard()
dropZone1.frame = CGRect(x: inset, y: inset, width: imageViewWidth, height: imageViewHeight)
let dropZone2 = getDropZoneCard()
let x = bounds.width - imageViewWidth - inset
dropZone2.frame = CGRect(x: x, y: inset, width: imageViewWidth, height: imageViewHeight)
let dropZone3 = getDropZoneCard()
let y = inset + imageViewHeight + inset
dropZone3.frame = CGRect(x: inset, y: y, width: imageViewWidth, height: imageViewHeight)
let dropZone4 = getDropZoneCard()
dropZone4.frame = CGRect(x: x, y: y, width: imageViewWidth, height: imageViewHeight)
[dropZone1, dropZone2, dropZone3, dropZone4].forEach {
view.addSubview($0)
self.arrayDropZones.append($0)
}
}
func getNewCard() -> UIImageView {
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
imageView.backgroundColor = .green
imageView.frame = initialFrame
let panGesture = UIPanGestureRecognizer(target: self, action:(#selector(handleGesture(_:))))
imageView.addGestureRecognizer(panGesture)
return imageView
}
func getDropZoneCard() -> DropZoneCard {
let dropZone = DropZoneCard()
dropZone.isUserInteractionEnabled = true
dropZone.backgroundColor = .yellow
return dropZone
}
func addNewCard() {
let imageView = getNewCard()
view.addSubview(imageView)
}
#objc func handleGesture(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
if let view = recognizer.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
if recognizer.state == .ended {
let point = view.center
for dropZone in arrayDropZones {
if dropZone.frame.contains(point) {
dropZone.append(card: view)
addNewCard()
return
}
}
view.frame = initialFrame
}
}
recognizer.setTranslation(.zero, in: view)
}
}
class DropZoneCard: UIImageView {
private(set) var arrayCards = [UIView]()
func append(card: UIView) {
arrayCards.append(card)
card.isUserInteractionEnabled = false
card.frame = frame
}
}

Dragging UIImageView only inside it's parent view

Let's say that I have:
An UIImageView called fox
A parent ImageView called fence
Master UIView embedded in ViewControllers by default
Now in other words, I want the fox to move only inside it's fence
In my viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(fence)
fence.addSubview(fox)
}
Now this part works fine, I figured to move the fox by subclassing UIImageView with a little bit of modifications:
class DraggableImageView: UIImageView {
var dragStartPositionRelativeToCenter : CGPoint?
override init(image: UIImage!) {
super.init(image: image)
self.isUserInteractionEnabled = true
addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePan(nizer:))))
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 3)
layer.shadowOpacity = 0.5
layer.shadowRadius = 2
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func handlePan(nizer: UIPanGestureRecognizer!) {
if nizer.state == UIGestureRecognizer.State.began {
let locationInView = nizer.location(in: superview)
dragStartPositionRelativeToCenter = CGPoint(x: locationInView.x - center.x, y: locationInView.y - center.y)
layer.shadowOffset = CGSize(width: 0, height: 20)
layer.shadowOpacity = 0.3
layer.shadowRadius = 6
return
}
if nizer.state == UIGestureRecognizer.State.ended {
dragStartPositionRelativeToCenter = nil
layer.shadowOffset = CGSize(width: 0, height: 3)
layer.shadowOpacity = 0.5
layer.shadowRadius = 2
return
}
let locationInView = nizer.location(in: superview)
UIView.animate(withDuration: 0.1) {
self.center = CGPoint(x: locationInView.x - self.dragStartPositionRelativeToCenter!.x,
y: locationInView.y - self.dragStartPositionRelativeToCenter!.y)
}
}}
Now I can drag the fox object anywhere i like, but; what if I wanted to only move the fox inside the fence object?, since it's a subview, I think it's possible.
In order to keep the image view inside its parent, I added a check before you update the center of the view that makes sure that the views frame will be in the parents frame. The center is only updated if the update would keep the view within its parent's frame.
I also updated the pan handler to use the translation (similar to the example in the pan gesture documentation) as opposed to the locationInView.
This makes the drag behave better.
I've tested this and I believe it behaves in the way you desire. Hope this helps:
#objc func handlePan(nizer: UIPanGestureRecognizer!) {
if nizer.state == UIGestureRecognizer.State.began {
dragStartPositionRelativeToCenter = self.center
layer.shadowOffset = CGSize(width: 0, height: 20)
layer.shadowOpacity = 0.3
layer.shadowRadius = 6
return
}
if nizer.state == UIGestureRecognizer.State.ended {
dragStartPositionRelativeToCenter = nil
layer.shadowOffset = CGSize(width: 0, height: 3)
layer.shadowOpacity = 0.5
layer.shadowRadius = 2
return
}
if let initialCenter = dragStartPositionRelativeToCenter {
let translation = nizer.translation(in: self.superview)
let newCenter = CGPoint(x: initialCenter.x + translation.x, y: initialCenter.y + translation.y)
if frameContainsFrameFromCenter(containingFrame: superview!.frame, containedFrame: self.frame, center: newCenter) {
UIView.animate(withDuration: 0.1) {
self.center = newCenter
}
}
}
}
func frameContainsFrameFromCenter(containingFrame: CGRect, containedFrame: CGRect, center: CGPoint) -> Bool {
let leftMargin = containedFrame.width / 2
let topMargin = containedFrame.height / 2
let testFrame = CGRect(
x: leftMargin,
y: topMargin,
width: containingFrame.width - (2*leftMargin),
height: containingFrame.height - (2*topMargin)
)
return testFrame.contains(center)
}

How to create a connecting line between drag-able UIViews in Swift 3? and measure the line angle

I need to have two views in screen for dragging (done whit UIPanGestureRecogniser) and a connecting line between , so when I move the views freely the line stay attached (like a rope between the views). and measure the angle of this line.
this is my code so far
var rect1:UIView!
var rect2:UIView!
override func viewDidLoad() {
super.viewDidLoad()
// create my two views
rect1 = UIView(frame: CGRect(x: 50, y: 150, width: 40, height: 40))
rect1.backgroundColor = UIColor.orange
self.view.addSubview(rect1)
rect2 = UIView(frame: CGRect(x: 200, y: 150, width: 40, height: 40))
rect2.backgroundColor = UIColor.yellow
self.view.addSubview(rect2)
// and the UIPanGestureRecognizer objects
let gesture1 = UIPanGestureRecognizer(target: self, action: #selector(dragView))
rect1.addGestureRecognizer(gesture1)
let gesture2 = UIPanGestureRecognizer(target: self, action: #selector(dragView))
rect2.addGestureRecognizer(gesture2)
// add mi connecting line between
func addLine(fromPoint start: rect1.center, toPoint end:rect2.center)
}
// create the func for moving the views
func dragView(_ sender: UIPanGestureRecognizer)
{
let point = sender.location(in: self.view)
let theDraggedView = sender.view!
theDraggedView.center = point
}
// and the func for line creation
func addLine(fromPoint start: CGPoint, toPoint end:CGPoint)
{
let line = CAShapeLayer()
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
line.path = linePath.cgPath
line.strokeColor = UIColor.red.cgColor
line.lineWidth = 2
line.lineJoin = kCALineJoinRound
self.view.layer.addSublayer(line)
}
}
and here I am stock!!
if I use the addline func again inside the dragView, creates hundreds of lines.
and don't know what to do next
Thats because you are not removing previous line when you call addLine function again and again.
For remove previous line declare var line = CAShapeLayer() at the top into the class so you can get previous line you have drawn and then whenever you call addLine function just remove previous line from superview at start like:
line.removeFromSuperlayer()
And your complete code will be:
import UIKit
class ViewController: UIViewController {
var rect1:UIView!
var rect2:UIView!
var line = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
rect1 = UIView(frame: CGRect(x: 50, y: 150, width: 40, height: 40))
rect1.backgroundColor = UIColor.orange
self.view.addSubview(rect1)
rect2 = UIView(frame: CGRect(x: 200, y: 150, width: 40, height: 40))
rect2.backgroundColor = UIColor.yellow
self.view.addSubview(rect2)
// and the UIPanGestureRecognizer objects
let gesture1 = UIPanGestureRecognizer(target: self, action: #selector(dragView))
rect1.addGestureRecognizer(gesture1)
let gesture2 = UIPanGestureRecognizer(target: self, action: #selector(dragView))
rect2.addGestureRecognizer(gesture2)
addLine(start: rect1.center, toPoint:rect2.center)
}
func dragView(_ sender: UIPanGestureRecognizer) {
let point = sender.location(in: self.view)
let theDraggedView = sender.view!
theDraggedView.center = point
addLine(start: rect1.center, toPoint:rect2.center)
}
func addLine(start: CGPoint, toPoint end:CGPoint) {
line.removeFromSuperlayer()
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
line.path = linePath.cgPath
line.strokeColor = UIColor.red.cgColor
line.lineWidth = 2
line.lineJoin = kCALineJoinRound
self.view.layer.addSublayer(line)
}
}
And your result will be:

Animate a circle from the center in Swift

I have a circle that I'm animating. It works except that the drawing is from the top left.. Can I animate it from the center? If so, any help would be appreciated..
My code for drawing the circle is:
class CircleView: UIView {
override func draw(_ rect: CGRect)
{
let prefs: UserDefaults = UserDefaults.standard
lineWidthFloat = prefs.value(forKey: "lineWidth") as! Float
let circleSize = Double(lineWidthFloat * 100)
let context = UIGraphicsGetCurrentContext()
context!.setLineWidth(10.0)
context!.setFillColor(UIColor.black.cgColor)
let rect = CGRect(x: 20, y: 20, width: circleSize, height: circleSize)
context!.addEllipse(inRect: rect)
context!.fillPath()
}
}
Thanks!
Details
Xcode 10.2.1 (10E1001), Swift 5
Full Sample
CircleView
class CircleView: UIView {
weak var circleView: UIView?
lazy var isAnimating = false
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setup() {
let rectSide = (frame.size.width > frame.size.height) ? frame.size.height : frame.size.width
let circleRect = CGRect(x: (frame.size.width-rectSide)/2, y: (frame.size.height-rectSide)/2, width: rectSide, height: rectSide)
let circleView = UIView(frame: circleRect)
circleView.backgroundColor = UIColor.yellow
circleView.layer.cornerRadius = rectSide/2
circleView.layer.borderWidth = 2.0
circleView.layer.borderColor = UIColor.red.cgColor
addSubview(circleView)
self.circleView = circleView
}
func resizeCircle (summand: CGFloat) {
guard let circleView = circleView else { return }
frame.origin.x -= summand/2
frame.origin.y -= summand/2
frame.size.height += summand
frame.size.width += summand
circleView.frame.size.height += summand
circleView.frame.size.width += summand
}
private func animateChangingCornerRadius (toValue: Any?, duration: TimeInterval) {
guard let circleView = circleView else { return }
let animation = CABasicAnimation(keyPath:"cornerRadius")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.fromValue = circleView.layer.cornerRadius
animation.toValue = toValue
animation.duration = duration
circleView.layer.cornerRadius = circleView.frame.size.width/2
circleView.layer.add(animation, forKey:"cornerRadius")
}
private func circlePulseAinmation(_ summand: CGFloat, duration: TimeInterval, completionBlock:#escaping ()->()) {
guard let circleView = circleView else { return }
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut, animations: { [weak self] in
self?.resizeCircle(summand: summand)
}) { _ in completionBlock() }
animateChangingCornerRadius(toValue: circleView.frame.size.width/2, duration: duration)
}
func resizeCircleWithPulseAinmation(_ summand: CGFloat, duration: TimeInterval) {
if (!isAnimating) {
isAnimating = true
circlePulseAinmation(summand, duration:duration) { [weak self] in
guard let self = self else { return }
self.circlePulseAinmation((-1)*summand, duration:duration) {self.isAnimating = false}
}
}
}
}
ViewController
import UIKit
class ViewController: UIViewController {
weak var circleView: CircleView?
weak var button: UIButton?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let circleView = CircleView(frame: CGRect(x: 40, y: 50, width: 40, height: 60))
circleView.backgroundColor = UIColor.clear
view.addSubview(circleView)
self.circleView = circleView
let button = UIButton(frame: CGRect(x: 20, y: 150, width: 80, height: 40))
button.setTitle("Animate", for: UIControl.State())
button.setTitleColor(UIColor.blue, for: UIControl.State())
button.setTitleColor(UIColor.blue.withAlphaComponent(0.3), for: .highlighted)
button.addTarget(self, action: #selector(ViewController.animateCircle), for: .touchUpInside)
view.addSubview(button)
self.button = button
}
#objc func animateCircle() {
circleView?.resizeCircleWithPulseAinmation(30, duration: 1.5)
}
}
Result
You should be able to do it a lot simpler using the transform property.
func pulse() {
UIView.animate(withDuration: 0.5, animations:{
self.circleView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}, completion: { _ in
UIView.animate(withDuration: 0.5, animations: {
self.circleView.transform = .identity
})
})
}

Resources