How to attach multiple UIDynamicItems to each other - ios

I am trying to implement circles attached to each other like in Apple's Music App via UIDynamicAnimator. I need to attach circles to each other and to view center. I was trying to implement this via UIAttachmentBehavior, but seems to it's not supporting multiple attachments. In result, circles overlaps on each other :)
let attachment = UIAttachmentBehavior(item: circle, attachedToAnchor: CGPoint(x: view.center.x, y: view.center.y))
attachment.length = 10
animator?.addBehavior(attachment)
let push = UIPushBehavior(items: [circle], mode: .continuous)
collision.addItem(circle)
animator?.addBehavior(push)
What I am doing wrong?

I don't think the apple music genre picker thing uses UIAttachmentBehavior which is closer to attaching two views with a pole or a rope. But, it seems like the problem you're experiencing might be that all of the views are added at the same location which has the effect of placing them on top of each other and with the collision behavior causes them to be essentially be stuck together. One thing to do is to turn on UIDynamicAnimator debugging by calling animator.setValue(true, forKey: "debugEnabled").
For recreating the above circle picker design, I would look into using UIFieldBehavior.springField().
For example:
class ViewController: UIViewController {
lazy var animator: UIDynamicAnimator = {
let animator = UIDynamicAnimator(referenceView: view)
return animator
}()
lazy var collision: UICollisionBehavior = {
let collision = UICollisionBehavior()
collision.collisionMode = .items
return collision
}()
lazy var behavior: UIDynamicItemBehavior = {
let behavior = UIDynamicItemBehavior()
behavior.allowsRotation = false
behavior.elasticity = 0.5
behavior.resistance = 5.0
behavior.density = 0.01
return behavior
}()
lazy var gravity: UIFieldBehavior = {
let gravity = UIFieldBehavior.springField()
gravity.strength = 0.008
return gravity
}()
lazy var panGesture: UIPanGestureRecognizer = {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.didPan(_:)))
return panGesture
}()
var snaps = [UISnapBehavior]()
var circles = [CircleView]()
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(panGesture)
animator.setValue(true, forKey: "debugEnabled")
addCircles()
addBehaviors()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
gravity.position = view.center
snaps.forEach {
$0.snapPoint = view.center
}
}
func addCircles() {
(1...30).forEach { index in
let xIndex = index % 2
let yIndex: Int = index / 3
let circle = CircleView(frame: CGRect(origin: CGPoint(x: xIndex == 0 ? CGFloat.random(in: (-300.0 ... -100)) : CGFloat.random(in: (500 ... 800)), y: CGFloat(yIndex) * 200.0), size: CGSize(width: 100, height: 100)))
circle.backgroundColor = .red
circle.text = "\(index)"
circle.textAlignment = .center
view.addSubview(circle)
gravity.addItem(circle)
collision.addItem(circle)
behavior.addItem(circle)
circles.append(circle)
}
}
func addBehaviors() {
animator.addBehavior(collision)
animator.addBehavior(behavior)
animator.addBehavior(gravity)
}
#objc
private func didPan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: sender.view)
switch sender.state {
case .began:
animator.removeAllBehaviors()
fallthrough
case .changed:
circles.forEach { $0.center = CGPoint(x: $0.center.x + translation.x, y: $0.center.y + translation.y)}
case .possible, .cancelled, .failed:
break
case .ended:
circles.forEach { $0.center = CGPoint(x: $0.center.x + translation.x, y: $0.center.y + translation.y)}
addBehaviors()
#unknown default:
break
}
sender.setTranslation(.zero, in: sender.view)
}
}
final class CircleView: UILabel {
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .ellipse
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height * 0.5
layer.masksToBounds = true
}
}
For more information I would watch What's New in UIKit Dynamics and Visual Effects from WWDC 2015

Related

Swift PanGesture : Problems Managing Constraints: Flash on New Pan

This is the code used to control a view with a pan-capable View.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var redRect: PaletteView!
#IBOutlet var paletteTop : NSLayoutConstraint?
#IBOutlet var paletteLeading : NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
addRedViewUI(tgt: redRect!)
redRect.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(palettePan(_:))))
redRect?.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(paletteConstraints())
}
func addRedViewUI(tgt : PaletteView) {
var pos = CGPoint(x: 10, y: 10)
var size = CGSize(width: 200, height: 240)
var v = UIView(frame: CGRect(origin: pos, size: size))
v.backgroundColor = .black
tgt.addSubview(v)
}
func paletteConstraints() -> Array<NSLayoutConstraint> {
var constraints : Array<NSLayoutConstraint> = []
constraints.append((redRect?.leadingAnchor.constraint( equalTo: view.leadingAnchor, constant: 100))!)
paletteLeading = constraints.last
constraints.append((redRect?.topAnchor.constraint( equalTo: view.topAnchor, constant: 100))!)
paletteTop = constraints.last
constraints.append((redRect?.widthAnchor.constraint(equalToConstant: 250))! )
constraints.append((redRect?.heightAnchor.constraint(equalToConstant: 350))! )
return constraints
}
func paletteConstraints(_ tgtView : PaletteView ) -> Array<NSLayoutConstraint> {
var constraints : Array<NSLayoutConstraint> = []
constraints.append((tgtView.leadingAnchor.constraint( equalTo: redRect!.leadingAnchor)))
constraints.append((tgtView.topAnchor.constraint( equalTo: redRect!.topAnchor)))
return constraints
}
#objc func palettePan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
switch sender.state {
case .began:
print("start \(redRect!.frame)")
paletteLeading?.constant = redRect!.frame.origin.x
paletteTop?.constant = redRect!.frame.origin.y
case .changed:
redRect!.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
case .ended:
print("done \(redRect!.frame)")
default:
_ = true
}
}
}
class PaletteView : UIView {
var lastPos: CGPoint = CGPoint(x: 20, y: 20)
convenience init() {
self.init(frame: CGRect(x: 10, y: 10, width: 200, height: 200))
translatesAutoresizingMaskIntoConstraints = false
}
}
It works fine, except that when beginning a new pan, it jumps from its position from the end of the last pan, before instantaneously jumping back and the pan can be done. This jump is in the same direction as the last pan, so the second pan the view won't jump, and subsequent pans it jumps in the direction that the last pan went, in roughly the same amount.
This makes me think that it's something associated with the sender state, but I can't figure out what. If the amount of the last pan is small, or the pan is begun slowly, there's no flash, but I haven't been able to understand where that comes from.
The view being panned has no constraints in the storyBoard, but it does get defined and connected to its outlet in the storyboard.
The solution is to change the function so both start and changed perform the CGAffineTransform, after the constraints are updated in the case of changed:
#objc func palettePan(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
switch sender.state {
case .began:
print("start \(redRect!.frame)")
paletteLeading?.constant = redRect!.frame.origin.x
paletteTop?.constant = redRect!.frame.origin.y
redRect!.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
case .changed:
redRect!.transform = CGAffineTransform(translationX: translation.x, y: translation.y)
case .ended:
print("done \(redRect!.frame)")
default:
_ = true
}
}

Press a button and thereafter be able to touch on screen and drag a hollow rectangle from (startpoint) until you let go (endpoint)

When using a drawing program or when using photoshop there is usually the ability to select a rectangle from the buttons panel. There you can press the button and afterwards be able to drag a rectangle on the screen depending on your chosen startpoint/endpoint.
Class
#IBDesignable class RectView: UIView {
#IBInspectable var startPoint:CGPoint = CGPoint.zero {
didSet{
self.setNeedsDisplay()
}
}
#IBInspectable var endPoint:CGPoint = CGPoint.zero {
didSet{
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
if (startPoint != nil && endPoint != nil){
let path:UIBezierPath = UIBezierPath(rect: CGRect(x: min(startPoint.x, endPoint.x),
y: min(startPoint.y, endPoint.y),
width: abs(startPoint.x - endPoint.x),
height: abs(startPoint.y - endPoint.y)))
UIColor.black.setStroke()
path.lineCapStyle = .round
path.lineWidth = 10
path.stroke()
}
}
}
Top ViewController
+Added class RectView to View(Custom Class)
class ViewController: UIViewController, UIGestureRecognizerDelegate {
let rectView = RectView()
#IBOutlet var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(panGestureMoveAround(sender:)))
tap.delegate = self
myView.addGestureRecognizer(tap)
ViewController
#objc func panGestureMoveAround(sender: UITapGestureRecognizer) {
var locationOfBeganTap: CGPoint
var locationOfEndTap: CGPoint
if sender.state == UIGestureRecognizer.State.began {
locationOfBeganTap = sender.location(in: rectView)
rectView.startPoint = locationOfBeganTap
rectView.endPoint = locationOfBeganTap
} else if sender.state == UIGestureRecognizer.State.ended {
locationOfEndTap = sender.location(in: rectView)
rectView.endPoint = sender.location(in: rectView)
} else{
rectView.endPoint = sender.location(in: rectView)
}
}
Code gives no particular errors however nothing is happening on screen. Any advice would be helpful.
You should try to focus on one issue at a time, however...
1) #IBDesignable and #IBInspectable are for use during design-time - that is, when laying out views in Storyboard / Interface Builder. That's not what you're trying to do here, so remove those.
2) CGrect() needs x, t, width and height:
let path:UIBezierPath = UIBezierPath(rect: CGRect(x: min(startPoint!.x, endPoint!.x),
y: min(startPoint!.y, endPoint!.y),
width: fabs(startPoint!.x - endPoint!.x),
height: fabs(startPoint!.y - endPoint!.y)))
3) Wrong way to instantiate your view:
// wrong
let rectView = RectView.self
// right
let rectView = RectView()
Correct those issue and see where you get. If you're still running into problems, first search for the specific issue. If you can't find the answer from searching, then come back and post a specific question.
Probably worth reviewing How to Ask

Move spritekit node only within a specific bounds

I'm new to spritekit so this looks like a silly question but I can't figure out. The player (shown in blue circle) can only go above lines and inside the square. I added a joystick, user can go up or down above left line. I want player to be limited to only line so when It comes the left edge, user should move joystick to right. How can I achieve it?
I tried to update player position in override func update(_ currentTime: TimeInterval) function like below to update enum position and check it everytime in move logic;
override func update(_ currentTime: TimeInterval) {
if((player?.position.x)!.rounded() <= self.barra.frame.minX.rounded()){
player?.playerPosition == .left
}
print(player?.position)
}
How I declare square;
let barra = SKShapeNode(rectOf: CGSize(width: 600, height: 300)) //Line
override func sceneDidLoad() {
player = self.childNode(withName: "player") as? Player
player?.physicsBody?.categoryBitMask = playerCategory
player?.physicsBody?.collisionBitMask = noCategory
player?.physicsBody?.contactTestBitMask = enemyCategory | itemCategory
player?.playerPosition = .left
barra.name = "bar"
barra.fillColor = SKColor.clear
barra.lineWidth = 3.0
barra.position = CGPoint(x: 0, y: 0)
self.addChild(barra)
player?.position = CGPoint(x: barra.frame.minX , y: barra.frame.minY)
}
How I move the player;
override func didMove(to view: SKView) {
/* Setup your scene here */
backgroundColor = UIColor.black
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
moveAnalogStick.position = CGPoint(x: moveAnalogStick.radius + 15, y: moveAnalogStick.radius + 15)
addChild(moveAnalogStick)
moveAnalogStick.stick.color = UIColor.white
//MARK: Handlers begin
moveAnalogStick.beginHandler = { [unowned self] in
guard let aN = self.player else {
return
}
//aN.run(SKAction.sequence([SKAction.scale(to: 0.5, duration: 0.5), SKAction.scale(to: 1, duration: 0.5)]))
}
moveAnalogStick.trackingHandler = { [unowned self] data in
guard let aN = self.player else {
return
}
if(self.player?.playerPosition == .left){
aN.position = CGPoint(x: aN.position.x, y: aN.position.y + (data.velocity.y * 0.12))
}
}
moveAnalogStick.stopHandler = { [unowned self] in
guard let aN = self.player else {
return
}
// aN.run(SKAction.sequence([SKAction.scale(to: 1.5, duration: 0.5), SKAction.scale(to: 1, duration: 0.5)]))
}
//MARK: Handlers end
let selfHeight = frame.height
let btnsOffset: CGFloat = 10
let btnsOffsetHalf = btnsOffset / 2
view.isMultipleTouchEnabled = true
}
Player class:
enum Position{
case left
case right
case up
case down
case inside
}
enum CanMove{
case upDown
case leftRight
case all
}
class Player: SKSpriteNode {
var playerSpeed: CGFloat = 0.0
var playerPosition: Position = .left //Default one
var canMove: CanMove = .upDown
func move(){
}
}

SpriteKit - safe area layout Iphone X

So, Currently I have a game that i’ve made in sprite kit and have used this way of fitting everything to the screen size:
buyButton = SKSpriteNode(texture: SKTexture(imageNamed: "BuyButton"), color: .clear, size: CGSize(width: frame.maxX / 2.9, height: frame.maxY / 10))
buyButton.position = CGPoint(x:-frame.maxX + frame.midX*2, y: -frame.maxY + frame.midY*1.655)
addChild(buyButton)
as you can see it uses the frame to calculate the width and height along with the position of the node on the scene, this works with all screen sizes that I have been using from the 6s to the 8 Plus.
but when it comes to the iPhone X there is a problem with it. it seems to stretch everything because of the screen size being bigger and oddly shaped as you can see below compared to the iPhone 8 Plus
I’ve been looking for solutions to this problem but none have really helped or just even make it worse and I couldn’t understand how to programmatically use the safe area layout in my sprite kit project like in this solution here
My Question is
How do I go about getting everything to fit in the iPhone X’s screen so that it fits and isn’t cutting off the score labels and stuff I have in the top right corners?
EDIT 2:
itemDescriptionLabel = UILabel(frame: CGRect(x: frame.maxX - 150, y: frame.maxY - 130 , width: frame.maxX / 0.7, height: frame.maxY / 3.5))
itemDescriptionLabel.font = UIFont(name: "SFCompactRounded-Light", size: 17)
itemDescriptionLabel.text = "This purchase stops all the popup ads that happen after a certain amount of time playing the game from showing up."
itemDescriptionLabel.numberOfLines = 4
itemDescriptionLabel.adjustsFontSizeToFitWidth = true
self.scene?.view?.addSubview(itemDescriptionLabel)
EDIT 3
EDIT 4
let safeAreaInsets = yourSpriteKitView.safeAreaInsets;
buyButton.position = CGPoint(x:safeAreaInsets.left - frame.maxX + frame.midX*2, y: safeAreaInsets.top - frame.maxY + frame.midY*1.655)
EDIT 5
screenWidth = self.view!.bounds.width
screenHeight = self.view!.bounds.height
coinScoreLabel = Score(num: 0,color: UIColor(red:1.0, green: 1.0, blue: 0.0, alpha: 1), size: 50, useFont: "SFCompactRounded-Heavy" ) // UIColor(red:1.00, green:0.81, blue:0.07, alpha:1.0)
coinScoreLabel.name = "coinScoreLabel"
coinScoreLabel.horizontalAlignmentMode = .right
//coinScoreLabel.verticalAlignmentMode = .top
coinScoreLabel.position = CGPoint(x: screenWidth / 2.02, y: inset.top - screenHeight / 2.02)
coinScoreLabel.zPosition = 750
addChild(coinScoreLabel)
I tried it on another SpriteNode that I have such as this one below, which it worked for I have no idea why the yellow label is doing this.
playButton = SKSpriteNode(texture: SKTexture(imageNamed: "GameOverMenuPlayButton"), color: .clear, size: CGSize(width: 120, height: 65))
playButton.position = CGPoint(x: inset.right - frame.midX, y: inset.bottom + frame.minY + playButton.size.height / 1.7)
playButton.zPosition = 1000
deathMenuNode.addChild(playButton)
It came out like this which is perfect:
Interesting question. The problem borns when we try to "transform" our view to SKView to create our game scene. Essentially, we could intercept that moment (using viewWillLayoutSubviews instead of viewDidLoad) and transform the view frame accordling with the safeAreaLayoutGuide layout frame property.
Some code as example:
GameViewController:
class GameViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if #available(iOS 11.0, *), let view = self.view {
view.frame = self.view.safeAreaLayoutGuide.layoutFrame
}
guard let view = self.view as! SKView? else { return }
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
view.showsPhysics = true
view.showsDrawCount = true
let scene = GameScene(size:view.bounds.size)
scene.scaleMode = .aspectFill
view.presentScene(scene)
}
override func viewDidLoad() {
super.viewDidLoad()
print("---")
print("∙ \(type(of: self))")
print("---")
}
}
GameScene:
class GameScene: SKScene {
override func didMove(to view: SKView) {
print("---")
print("∙ \(type(of: self))")
print("---")
let labLeftTop = SKLabelNode.init(text: "LEFTTOP")
labLeftTop.horizontalAlignmentMode = .left
labLeftTop.verticalAlignmentMode = .top
let labRightTop = SKLabelNode.init(text: "RIGHTTOP")
labRightTop.horizontalAlignmentMode = .right
labRightTop.verticalAlignmentMode = .top
let labLeftBottom = SKLabelNode.init(text: "LEFTBTM")
labLeftBottom.horizontalAlignmentMode = .left
labLeftBottom.verticalAlignmentMode = .bottom
let labRightBottom = SKLabelNode.init(text: "RIGHTBTM")
labRightBottom.horizontalAlignmentMode = .right
labRightBottom.verticalAlignmentMode = .bottom
self.addChild(labLeftTop)
self.addChild(labRightTop)
self.addChild(labLeftBottom)
self.addChild(labRightBottom)
labLeftTop.position = CGPoint(x:0,y:self.frame.height)
labRightTop.position = CGPoint(x:self.frame.width,y:self.frame.height)
labLeftBottom.position = CGPoint(x:0,y:0)
labRightBottom.position = CGPoint(x:self.frame.width,y:0)
}
}
Other way to do:
Another way to obtain just only the differences between the safe area layout frame and the current view frame without passing through viewWillLayoutSubviews for launching our scene, could be using protocols.
P.S.: Down below, I've report a reference image where you can see the different sizes between:
the main screen height (our view height)
safe area
status bar height (on the top, 44 px for the iPhone X)
GameViewController:
protocol LayoutSubviewDelegate: class {
func safeAreaUpdated()
}
class GameViewController: UIViewController {
weak var layoutSubviewDelegate:LayoutSubviewDelegate?
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let _ = self.view {
layoutSubviewDelegate?.safeAreaUpdated()
}
}
override func viewDidLoad() {
super.viewDidLoad()
print("---")
print("∙ \(type(of: self))")
print("---")
guard let view = self.view as! SKView? else { return }
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
view.showsPhysics = true
view.showsDrawCount = true
let scene = GameScene(size:view.bounds.size)
scene.scaleMode = .aspectFill
view.presentScene(scene)
}
}
GameScene:
class GameScene: SKScene,LayoutSubviewDelegate {
var labLeftTop:SKLabelNode!
var labRightTop:SKLabelNode!
var labLeftBottom:SKLabelNode!
var labRightBottom:SKLabelNode!
func safeAreaUpdated() {
if let view = self.view {
let top = view.bounds.height-(view.bounds.height-view.safeAreaLayoutGuide.layoutFrame.height)+UIApplication.shared.statusBarFrame.height
let bottom = view.bounds.height - view.safeAreaLayoutGuide.layoutFrame.height-UIApplication.shared.statusBarFrame.height
refreshPositions(top: top, bottom: bottom)
}
}
override func didMove(to view: SKView) {
print("---")
print("∙ \(type(of: self))")
print("---")
if let view = self.view, let controller = view.next, controller is GameViewController {
(controller as! GameViewController).layoutSubviewDelegate = self
}
labLeftTop = SKLabelNode.init(text: "LEFTTOP")
labLeftTop.horizontalAlignmentMode = .left
labLeftTop.verticalAlignmentMode = .top
labRightTop = SKLabelNode.init(text: "RIGHTTOP")
labRightTop.horizontalAlignmentMode = .right
labRightTop.verticalAlignmentMode = .top
labLeftBottom = SKLabelNode.init(text: "LEFTBTM")
labLeftBottom.horizontalAlignmentMode = .left
labLeftBottom.verticalAlignmentMode = .bottom
labRightBottom = SKLabelNode.init(text: "RIGHTBTM")
labRightBottom.horizontalAlignmentMode = .right
labRightBottom.verticalAlignmentMode = .bottom
self.addChild(labLeftTop)
self.addChild(labRightTop)
self.addChild(labLeftBottom)
self.addChild(labRightBottom)
labLeftTop.position = CGPoint(x:0,y:self.frame.height)
labRightTop.position = CGPoint(x:self.frame.width,y:self.frame.height)
labLeftBottom.position = CGPoint(x:0,y:0)
labRightBottom.position = CGPoint(x:self.frame.width,y:0)
}
func refreshPositions(top:CGFloat,bottom:CGFloat){
labLeftTop.position = CGPoint(x:0,y:top)
labRightTop.position = CGPoint(x:self.frame.width,y:top)
labLeftBottom.position = CGPoint(x:0,y:bottom)
labRightBottom.position = CGPoint(x:self.frame.width,y:bottom)
}
}
Output:
Subclassing methods:
Another way to get the correct safe area dimensions without touch your current classes code, could be made using some subclass, so for our GameViewController we set it as GameController and for GameScene we can set it as GenericScene like this example:
GameViewController:
class GameViewController: GameController {
override func viewDidLoad() {
super.viewDidLoad()
guard let view = self.view as! SKView? else { return }
view.ignoresSiblingOrder = true
let scene = GameScene(size:view.bounds.size)
scene.scaleMode = .aspectFill
view.presentScene(scene)
}
}
protocol LayoutSubviewDelegate: class {
func safeAreaUpdated()
}
class GameController:UIViewController {
weak var layoutSubviewDelegate:LayoutSubviewDelegate?
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let _ = self.view {
layoutSubviewDelegate?.safeAreaUpdated()
}
}
}
GameScene:
class GameScene: GenericScene {
override func safeAreaUpdated() {
super.safeAreaUpdated()
if let view = self.view {
let insets = view.safeAreaInsets
// launch your code to update positions here
}
}
override func didMove(to view: SKView) {
super.didMove(to: view)
// your stuff
}
}
class GenericScene: SKScene, LayoutSubviewDelegate {
func safeAreaUpdated(){}
override func didMove(to view: SKView) {
if let view = self.view, let controller = view.next, controller is GameViewController {
(controller as! GameViewController).layoutSubviewDelegate = self
}
}
}
References:
You can get margins through your view's safeAreaInsets. The insets will be zero on most devices, but non-zero on the iPhone X.
For example replace your line with this:
let safeAreaInsets = yourSpriteKitView.safeAreaInsets;
buyButton.position = CGPoint(x:safeAreaInsets.left - frame.maxX + frame.midX*2, y: safeAreaInsets.top - frame.maxY + frame.midY*1.655)

Draggable UIView Swift 3

I want to be able to drag the objects on the screen, but they wont. I tried everything but still cant.
Here are the code.
func panGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
print("Began.")
for i in 0..<forms.count {
if forms[i].frame.contains(gesture.location(in: view)) {
gravity.removeItem(forms[i])
}
}
case .changed:
let translation = gesture.translation(in: forms[1])
gesture.view!.center = CGPoint(x: gesture.view!.center.x + translation.x, y: gesture.view!.center.y + translation.y)
gesture.setTranslation(CGPoint.zero, in: self.view)
print("\(gesture.view!.center.x)=\(gesture.view!.center.y)")
print("t;: \(translation)")
case .ended:
for i in 0..<forms.count {
if forms[i].frame.contains(gesture.location(in: view)) {
gravity.addItem(forms[i])
}
}
print("Ended.")
case .cancelled:
print("Cancelled")
default:
print("Default")
}
}
Also they have gravity. The forms are squares and circles.
Explanation:
in .began - i disable the gravity for selected form.
in .changed - i try to change the coordinates.
in .end - i enable again gravity.
ScreenShot.
Step 1 : Take one View which you want to drag in storyBoard.
#IBOutlet weak var viewDrag: UIView!
Step 2 : Add PanGesture.
var panGesture = UIPanGestureRecognizer()
Step 3 : In ViewDidLoad adding the below code.
override func viewDidLoad() {
super.viewDidLoad()
panGesture = UIPanGestureRecognizer(target: self, action: #selector(ViewController.draggedView(_:)))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
}
Step 4 : Code for draggedView.
func draggedView(_ sender:UIPanGestureRecognizer){
self.view.bringSubview(toFront: viewDrag)
let translation = sender.translation(in: self.view)
viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x, y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
Step 5 : Output.
It's very easy if you subclass a view:
DraggableView...
class DraggableView: UIIView {
var fromleft: NSLayoutConstraint!
var fromtop: NSLayoutConstraint!
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
fromleft = constraint(id: "fromleft")!
fromtop = constraint(id: "fromtop")!
}
}
override func common() {
super.common()
let p = UIPanGestureRecognizer(
target: self, action: #selector(drag))
addGestureRecognizer(p)
}
#objc func drag(_ s:UIPanGestureRecognizer) {
let t = s.translation(in: self.superview)
fromleft.constant = fromleft.constant + t.x
fromtop.constant = fromtop.constant + t.y
s.setTranslation(CGPoint.zero, in: self.superview)
}
}
Drop a UIView in your scene.
As normal, add a constraint from the left (that's the x position) and add a constraint from the top (that's the y position).
In storyboard simply simply name the constraints "fromleft" and "fromtop"
You're done.
It now works perfectly - that's it.
What is that handy constraint( call ?
Notice the view simply finds its own constraints by name.
In Xcode there is STILL no way to use constraints like IBOutlets. Fortunately it is very easy to find them by "identifier". (Indeed, this is the very purpose of the .identifier feature on constraints.)
extension UIView {
func constraint(id: String) -> NSLayoutConstraint? {
let cc = self.allConstraints()
for c in cc { if c.identifier == id { return c } }
//print("someone forgot to label constraint \(id)") //heh!
return nil
}
func allConstraints() -> [NSLayoutConstraint] {
var views = [self]
var view = self
while let superview = view.superview {
views.append(superview)
view = superview
}
return views.flatMap({ $0.constraints }).filter { c in
return c.firstItem as? UIView == self ||
c.secondItem as? UIView == self
}
}
Tip...edge versus center!
Don't forget when you make the constraints on a view (as in the image above):
you can set the left one to be either:
to the left edge of the white box, or,
to the center of the white box.
Choose the correct one for your situation. It will make it much easier to do calculations, set sliders, etc.
Footnote - an "initializing" UIView, UI "I" View,
// UI "I" View ... "I" for initializing
// Simply saves you typing inits everywhere
import UIKit
class UIIView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
common()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
common()
}
func common() { }
}
Use below code for Swift 5.0
Step 1 : Take one UIView from Storyboard, drag it into your ViewController file and Create IBOutlet of UIView.
#IBOutlet weak var viewDrag: UIView!
var panGesture = UIPanGestureRecognizer()
Step 2 : In viewDidLoad() adding the below code.
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action:(Selector(("draggedView:"))))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
}
Step 3 : Create func and add code to move the UIView as like below.
func draggedView(sender:UIPanGestureRecognizer){
self.view.bringSubviewToFront(viewDrag)
let translation = sender.translation(in: self.view)
viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x, y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
Hope this will help someone.
This UIView extension makes a UIView object draggable and limits the movement to stay within the bounds of the screen.
extension UIView {
func makeDraggable() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
self.addGestureRecognizer(panGesture)
}
#objc func handlePan(_ gesture: UIPanGestureRecognizer) {
guard gesture.view != nil else { return }
let translation = gesture.translation(in: gesture.view?.superview)
var newX = gesture.view!.center.x + translation.x
var newY = gesture.view!.center.y + translation.y
let halfWidth = gesture.view!.bounds.width / 2.0
let halfHeight = gesture.view!.bounds.height / 2.0
// Limit the movement to stay within the bounds of the screen
newX = max(halfWidth, newX)
newX = min(UIScreen.main.bounds.width - halfWidth, newX)
newY = max(halfHeight, newY)
newY = min(UIScreen.main.bounds.height - halfHeight, newY)
gesture.view?.center = CGPoint(x: newX, y: newY)
gesture.setTranslation(CGPoint.zero, in: gesture.view?.superview)
}
}

Resources