I have a SKSpriteNode that is too big for the view and I am looking for a simple way in Sprite Kit to scroll horizontally to be able to see it completely.
Thanks!
You could try using an SKCameraNode and respond to a pan gesture like so:
// GameScene.swift
override func didMove(to view: SKView) {
self.camera = SKCameraNode()
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanFrom(recognizer:)))
panGestureRecognizer.maximumNumberOfTouches = 1
view.addGestureRecognizer(panGestureRecognizer)
}
func handlePanFrom(recognizer: UIPanGestureRecognizer) {
if recognizer.state != .changed {
return
}
// Get touch delta
let translation = recognizer.translation(in: recognizer.view!)
// Move camera
self.camera?.position.x -= translation.x
self.camera?.position.y += translation.y
// Reset
recognizer.setTranslation(CGPoint.zero, in: recognizer.view)
}
The easiest way to get neat scroll-view-like UX is ... to use a Scroll View. Nest one in your SKSceneView:
Scene View
|-Scroll View
Then you need to set up a contentSize of the Scroll View according to the scene width and viewport width. Here you should make some corresponding calculations depending on the scaleMode of the scene and other specific features of your layout.
There are very good lessons from Raywenderlich on this topic:
Scroll View School Part 13: Sprite Kit Integration
Scroll View School Part 14: Sprite Kit Level Selector
The lessons are good because they are very detailed. They will tell you how to convert coordinates correctly. However, some things are not ideal there: for example you don't need to add a content view to the scroll view, setting the contentSize will be enough. Also, I recommend to use SKCameraNode instead of putting everything in a root "world" node.
After this you must listen to the Scroll View's delegate:
// Example from one of my games
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollWidth = scrollView.contentSize.width - scrollView.bounds.width
guard scrollWidth > 0.01 else { return }
let relativePosX = scrollView.contentOffset.x / scrollWidth
// Feed relative position to the scene.
// `setCameraRelativePosition` will convert it into the absolute camera
// coordinates in the scene coordinate space
gameScene.setCameraRelativePosition(relativePosX)
}
... and update your tilemap or camera position as appropriate.
UIPanGestureRecognizer will never give your deceleration and bouncing like a native Scroll View.
Related
I am making an app that returns venues (like restaurants) in a fashion similar to apple maps, i.e. a table view that is initially hidden mostly off screen and then moves up above other content when the table view's contents are to be displayed.
This is what I've gotten so far:
You'll notice that when pulling the table view back down to reveal the map behind it, the table view does not move down in one fluid motion. In fact, when the table view reaches its "top," you'll notice that the scroll bar on the right side is visible but the view isn't moving. This is because, while recording, I'm dragging downward but the view takes a few moments to actually begin to translate downward. I'll explain why it has this sort of delay, but first compare this behavior to that of the Apple maps table view:
Notice the fluidity in the Apple maps table view's transition from translating the view itself to scrolling the scroll view. A translation upward of the view converts into a scrolling downward of the scroll view (and vice versa) when the view's minY hits a certain point. This is all done throughout one upward swipe by the user.
Now I'll explain what I did to create my translating scroll view. The scroll view itself is a UITableViewController embedded in a container view on the map's view controller.
The view that holds the container view has a pan gesture recognizer. When the pan gesture recognizer's state is UIGestureRecognizerState.ended, i.e. the drag has ended, it checks the container view's minY in relation to a predetermined Y coordinate. If the minY is less than the predetermined Y coordinate, it will animate the container view to snap its minY to that Y coordinate. The case is the same if the minY is slightly greater than the predetermined Y coordinate, in this instance the container view will animate upward so its minY snaps to this predetermined Y coordinate.
Once the view has snapped into this position, the embedded UITableViewController's ".isScrollEnabled" property, which is initially set to false, is now set to true. When the embedded table view is now scroll enabled, a drag executed by the user will scroll the table view as opposed to translating the container view. To undo this, I've made it so that when the table view has reached the top of its scroll, its ".isScrollEnabled" property is set back to false. This means that any dragging motion by the user will translate the view as opposed to scrolling the table view. This does not create the fluidity in transition between scrolling and translating the way Apple maps does. The user would actually have to execute multiple drags on the area in order to transition from scrolling to translating; Apple maps is able to do this in one drag.
Here is the code I used within the container view's parent View controller to:
Snap container view into place
Change .isScrollEnabled property of embedded table view controller to true
#objc func handlePan(sender: UIPanGestureRecognizer) {
let resultView = self.resultView
let translation = sender.translation(in: view)
switch sender.state {
case .began, .changed:
resultView?.center = CGPoint(x: (resultView?.center.x)!, y: (resultView?.center.y)! + translation.y)
sender.setTranslation(CGPoint.zero, in: view)
if (resultView?.frame.minY)! <= self.view.frame.height - self.searchView.frame.height - self.view.safeAreaInsets.bottom && self.resultView.frame.height >= (self.searchView.frame.height + self.view.safeAreaInsets.bottom) {
resultView?.frame.size.height -= translation.y
}
case .ended:
if (resultView?.frame.minY)! > self.view.frame.height * 0.40 {
self.resultTableViewController?.tableView.isScrollEnabled = false
} else {
self.resultTableViewController?.tableView.isScrollEnabled = true
}
if (resultView?.frame.minY)! <= view.frame.height * 0.40 {
let difference = (resultView?.frame.minY)! - (view.frame.height * 0.05)
UIView.animate(withDuration: 0.25) {
resultView?.center = CGPoint(x: (resultView?.center.x)!, y: (resultView?.center.y)! - difference)
resultView?.frame.size.height += difference
self.view.layoutIfNeeded()
}
dismissed = false
} else if (resultView?.frame.minY)! >= view.frame.height * 0.75 {
let difference = (view.frame.height - dismissResultButton.frame.height - view.safeAreaInsets.bottom) - (resultView?.frame.minY)!
UIView.animate(withDuration: 0.25, animations: {
resultView?.center = CGPoint(x: (resultView?.center.x)!, y: (resultView?.center.y)! + difference)
self.resultView.frame.size.height = self.searchView.frame.size.height + self.view.safeAreaInsets.bottom
self.view.layoutIfNeeded()
})
dismissed = true
} else if (resultView?.frame.minY)! >= view.frame.height * 0.40 {
let difference = mapView.frame.height - (resultView?.frame.minY)!
UIView.animate(withDuration: 0.25) {
resultView?.center = CGPoint(x: (resultView?.center.x)!, y: (resultView?.center.y)! + difference)
self.resultView.frame.size.height = self.searchView.frame.size.height + self.view.safeAreaInsets.bottom
self.view.layoutIfNeeded()
}
dismissed = false
}
sender.setTranslation(CGPoint.zero, in: view)
default:
break
}
}
And now, within the UITableViewController:
Ensures that the scroll view only bounces when at the bottom of the scroll (it should lock and then convert to a container translation when reaching the top)
Change .isScrollEnabled back to false and allow container view translation on drag
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 0)
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview!)
if translation.y > 0 {
if scrollView.contentOffset.y == 0 {
tableView.isScrollEnabled = false
}
}
}
This is the best I could come up with to try and replicate this behavior by the Apple maps table view. Any help/suggestions would be appreciated.
P.S. heres the github to the project if you'd like to break it down more: https://github.com/ChopinDavid/What2Do
I want to restrict the area a UITextView can be moved to only the frame of another view. I tried to accomplish this with the following code, however, it will not properly restrict where the user can drag the UITextView:
#IBOutlet weak var canvas: UIView!
#IBAction func handleDragOfCaption(_ sender: UIPanGestureRecognizer) {
if sender.state == .began || sender.state == .changed {
guard let senderView = sender.view else {return}
let translation = sender.translation(in: sender.view)
let x = senderView.center.x + translation.x
let y = senderView.center.y + translation.y
let point = CGPoint(x: x, y: y)
if canvas.frame.contains(point) {
senderView.center = point
sender.setTranslation(CGPoint.zero, in: senderView)
}
}
}
See the following image for a visual representation of what I am trying to accomplish. I only want the user to be able to drag the UITextView within the boundaries of the UIView (which is the white square underneath the UITextView). Any help would be appreciated.
Here is another approach, using constraints...
Give your "draggable view" (your text view, in this case) a Width constraint and disable scrolling (that will allow it to auto-size its height).
Constrain the "draggable view" to Zero on all four sides - then edit those constraints to be >= 0. This will prevent the view from being dragged outside its parent view.
Also constrain the "draggable view" to Center Horizontally and Vertically - then edit those constraints to have Priority = 750. By setting the center constraints to a lower priority than the edge constraints, the centers will only take effect if the edges are fully inside the parent view.
Connect the center constraints to IBOutlets in your view controller, and add vars for "current" constant values for the center constraints.
Now add a Pan Gesture Recognizer to the "draggable view".
When you start the pan, save the center constraint constants to the "current" vars.
When you pan, get the translation X and Y (the distance moved from the start of the pan), and update the center constants based on the difference between the start and new values. This will move the "draggable view" to its new position.
When you finish the pan, update the center constants based on the final position of the "draggable view".
#IBAction func handleDragOfCaption(_ sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else { return }
guard let parentView = senderView.superview else { return }
// get the current pan movement - relative to the view (the text view, in this case)
let translation = sender.translation(in: canvas)
switch sender.state {
case .began:
// save the current Center X and Y constants
currentCenterXConstant = textViewCenterXConstraint.constant
currentCenterYConstant = textViewCenterYConstraint.constant
break
case .changed:
// update the Center X and Y constants
textViewCenterXConstraint.constant = currentCenterXConstant + translation.x
textViewCenterYConstraint.constant = currentCenterYConstant + translation.y
break
case .ended, .cancelled:
// update the Center X and Y constants based on the final position of the dragged view
textViewCenterXConstraint.constant = senderView.center.x - parentView.frame.size.width / 2.0
textViewCenterYConstraint.constant = senderView.center.y - parentView.frame.size.height / 2.0
break
default:
break
}
}
To help with setting up the constraints correctly, I've posted an example project here: https://github.com/DonMag/LimitDrag
I want to be able to move an instance of UIImageView from a UIScrollView to a UIView that is outside the containing UIScrollView.
I've got the panGesture working but a UIImageView shows only inside the containing UIScrollView when being dragged and gets hidden if going outside the containing UIScrollView as shown in the screenshot image.
I've tried something like someScrollView.sendSubview(toBack: self.view) to set the layer order and also imageView.layer.zIndex = .. but it doesn't seem to work in my case.
How do I achieve something as shown in the screenshot image so it can be dragged to a target UIView outside its containing view?
And also if possible, how can I create a new instance of UIImageView as the panGesture begins so the original images stay.
#IBOutlet weak var someScrollView: UIScrollView!
var letters: [String] = ["g","n","d"]
override func viewDidLoad() {
super.viewDidLoad()
someScrollView.addSubview(createLetters(letters))
someScrollView.sendSubview(toBack: self.view)
}
func createLetters(_ named: String]) -> [UIImageView] {
return named.map { name in
let letterImage = UIImageView()
letterImage.image = UIImage(named: "\(name)")
addPanGesture(image: letterImage)
return letterImage
}
}
func addPanGesture(image: UIImageView) {
let pan = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(sender:)))
image.addGestureRecognizer(pan)
}
#objc func handlePan(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
if let imageView = sender.view {
imageView.center = CGPoint(x:imageView.center.x + translation.x,
y:imageView.center.y + translation.y)
}
sender.setTranslation(CGPoint.zero, in: self.view)
switch sender.state {
case .began:
...
}
}
First off, it's good practice to prioritize gesture recognizers - tell the scroll view's pan gesture that it won't receive touches on account of your pan gestures (Yours comes first).
someScrollView.panGestureRecognizer.require(toFail: yourGestureRecognizer)
Unclip scroll-view's subviews to it's bounds - It's subviews will be visible when dragged outside the scrollView's bounds.
scrollView.clipsToBounds = false
You can convert the frame of your dragged view to the scrollView and newParentView ancestor's coord' system, like this (assuming self.view is scrollView & newParentView's ancestor).
let pannedViewFrame = someScrollView.convert(pannedView, to: self.view)
Then, in your gesture recognizer's selector, you can test frame intersection of pannedViewFrame and newParentView.frame, like this:
// You have an frame intersection
if pannedViewFrame.intersects(newParentView.frame) {
}
Now, if you test intersection of frames when your gesture recognizer's state is .cancelled, .ended or .failed, then:
The panning has ended AND pannedView is within newParentView's bounds
Last step, just convert pannedViewFrame to newParentView.frame's coord' using the same trick and added pannedView as subview to newParentView.
Another solution is to remove the pannedView from scrollView when GR state is .began and add it to scrollView and newParentView's common ancestor. The rest is the same as i previously mentioned.
The problem in short, related to working with pan gesture inside a scrollView.
I have a canvas(which is an UIView itself but bigger in size) where i am drawing some UIView objects with pan gesture enabled over each of them(Each little UIView Objects I am talking about, are making using another UIView class).
Now the canvas can be bigger in height and width...which can be changed as per the user input.
So to achieve that I have placed the canvas inside a UIScrollView. Now the canvas is increasing or decreasing smoothly.
Those tiny UIView objects on the canvas can be rotated also.
Now the problem.
If I am not changing the canvas size(static) i.e. if its not inside the scrollview then each UIView objects inside the canvas are moving superbly and everything is working just fine with the following code.
If the canvas is inside the UIScrollView then the canvas can be scrollable right? Now inside the scrollview if I am panning the UIView objects on the canvas then those little UIView objects are not following the touch of the finger rather than its moving on another point when touch is moving on the canvas.
N.B. - Somehow I figured out that I need to disable the scrolling of the scrollview when any of the subviews are getting touch. For that thing I have implemented NSNotificationCenter to pass the signal to the parent viewController.
Here is the code.
This functions are defined inside the parent viewController class
func canvusScrollDisable(){
print("Scrolling Off")
self.scrollViewForCanvus.scrollEnabled = false
}
func canvusScrollEnable(){
print("Scrolling On")
self.scrollViewForCanvus.scrollEnabled = true
}
override func viewDidLoad() {
super.viewDidLoad()
notificationUpdate.addObserver(self, selector: "canvusScrollEnable", name: "EnableScroll", object: nil)
notificationUpdate.addObserver(self, selector: "canvusScrollDisable", name: "DisableScroll", object: nil)
}
This is the Subview class of the canvas
import UIKit
class ViewClassForUIView: UIView {
let notification: NSNotificationCenter = NSNotificationCenter.defaultCenter()
var lastLocation: CGPoint = CGPointMake(0, 0)
var lastOrigin = CGPoint()
var myFrame = CGRect()
var location = CGPoint(x: 0, y: 0)
var degreeOfThisView = CGFloat()
override init(frame: CGRect){
super.init(frame: frame)
let panRecognizer = UIPanGestureRecognizer(target: self, action: "detectPan:")
self.backgroundColor = addTableUpperViewBtnColor
self.multipleTouchEnabled = false
self.exclusiveTouch = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func detectPan(recognizer: UIPanGestureRecognizer){
let translation = recognizer.translationInView(self.superview!)
self.center = CGPointMake(lastLocation.x + translation.x, lastLocation.y + translation.y)
switch(recognizer.state){
case .Began:
break
case .Changed:
break
case .Ended:
notification.postNotificationName("EnableScroll", object: nil)
default: break
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
notification.postNotificationName("DisableScroll", object: nil)
self.superview?.bringSubviewToFront(self)
lastLocation = self.center
lastOrigin = self.frame.origin
let radians:Double = atan2( Double(self.transform.b), Double(self.transform.a))
self.degreeOfThisView = CGFloat(radians) * (CGFloat(180) / CGFloat(M_PI) )
if self.degreeOfThisView != 0.0{
self.transform = CGAffineTransformIdentity
self.lastOrigin = self.frame.origin
self.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_4))
}
myFrame = self.frame
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
notification.postNotificationName("EnableScroll", object: nil)
}
}
Now the scrollView is disabling its scroll perfectly whenever one of the UIView object is receiving touch over the canvas which is inside the scrollview but sometimes those UIView objects are not properly following the touch location over the canvas/screen.
I am using Swift 2.1 with Xcode 7 but anyone can tell me the missing things of mine or the solution using Objective-c/Swift?
Where do you set the lastLocation? I think it would be better for you to use locationInView and compute the translation by yourself. Then save the lastLocation on every event that triggers the method.
Also you might want to handle the Cancel state as well to turn the scrolling back on.
All of this does seem a bit messy though. The notifications are maybe not the best idea in your case nor is putting the gesture recognizers on the subviews. I think you should have a view which handles all those small views; it should also have a gesture recognizer that can simultaneously interact with other recognizers. When the gesture is recognized it should check if any of the subviews are hit and decide if any of them should be moved. If it should be moved then use the delegate to report that the scrolling must be disabled. If not then cancel the recognizer (disable+enable does that). Also in most cases where you put something movable on the scrollview you usually want a long press gesture recognizer and not a pan gesture recognizer. Simply use that one and set some very small minimum press duration. Note that this gesture works exactly the same as the pan gesture but can have a small delay to be detected. It is very useful in these kind of situations.
Update (The architecture):
The hierarchy should be:
View controller -> Scrollview -> Canvas view -> Small views
The canvas view should contain the gesture recognizer that controls the small views. When the gesture begins you should check if any of the views are hit by its location by simply iterating through the subviews and check if their frame contains a point. If so it should start moving the hit small view and it should notify its delegate that it has began moving it. If not it should cancel the gesture recognizer.
As the canvas view has a custom delegate it is the view controller that should implement its protocol and assign itself to the canvas view as a delegate. When the canvas view reports that the view dragging has begin it should disable the scrollview scrolling. When the canvas view reports it has stopped moving the views it should reenable the scrolling of the scroll view.
Create this type of view hierarchy
Create a custom protocol of the canvas view which includes "did begin dragging" and "did end dragging"
When the view controller becomes active assign self as a delegate to the canvas view. Implement the 2 methods to enable or disable the scrolling of the scroll view.
The canvas view should add a gesture recognizer to itself and should contain an array of all the small movable subviews. The recognizer should be able to interact with other recognizers simultaneously which is done through its delegate.
The Canvas gesture recognizer target should on begin check if any of the small views are hit and save it as a property, it should also save the current position of the gesture. When the gesture changes it should move the grabbed view depending on the last and current gesture location and re-save the current location to the property. When the gesture ends it should clear the currently dragged view. On begin and end it should call the delegate to notify the change of the state.
Disable or enable the scrolling in the view controller depending on the canvas view reporting to delegate.
I think this should be all.
I am attempting to use UIPanGestureRecognizer to translate and dismiss a UITableViewController. I want the gesture to trigger translation of the TableView only when it has reached the bottom of its scroll view and dismiss the TableView when it has been translated 1/3 the height of the screen. I've tried to add the GestureRecognizer when the TableView has reached the bottom of its scroll view, but the application ends up adding the gesture recognizer and disabling the freedom to scroll back up in the TableView.
I can supply code upon request, but I should be able to follow general solutions you may have.
Thanks in advance.
Few extra things to note:
I've added the gesture recognizer to the table view
I want to create a similar effect to a particular view in Facebook's iPhone application. In their app, whenever you select a photo from a photo album, it presents a TableView that allows you to translate and dismiss it whenever you reach the any of the edges in its scrollview.
Here's the code I currently have:
override func scrollViewDidScroll(scrollView: UIScrollView) {
// MARK: - Scrollview dismiss from bottom
let scrollViewHeight = scrollView.frame.height
let scrollContentSizeHeight = scrollView.contentSize.height
let scrollOffset = scrollView.contentOffset.y
// Detects if scroll view is at bottom of table view
if scrollOffset + scrollViewHeight >= scrollContentSizeHeight {
println("Reached bottom of table view")
self.panGesture = UIPanGestureRecognizer(target: self, action: "slideViewFromBottom:")
self.imageTableView.addGestureRecognizer(panGesture)
}
}
func slideViewFromBottom(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translationInView(imageTableView).y
let velocity = recognizer.velocityInView(self.imageTableView)
var centerOfImageTableView = self.imageTableView.center.y
switch recognizer.state {
case .Began:
// Touches begin
println("Touches began")
case .Changed:
// Limits view translation to only pan up
if velocity.y < 0.0 {
centerOfImageTableView = self.screenbounds.height/2 + translation
}
case .Ended:
// Determines length of translation to be animated
let moveToTop = screenbounds.height + translation
// Animates view depending on view location at end of gesture
if translation <= -UIScreen.mainScreen().bounds.height/2 {
UIView.animateWithDuration(0.2) {
self.imageTableView.transform = CGAffineTransformMakeTranslation(0.0, -moveToTop)
return
}
delay(0.3) {
self.presentingViewController?.dismissViewControllerAnimated(false, completion: nil)
}
}
else {
UIView.animateWithDuration(0.3) {
self.imageTableView.transform = CGAffineTransformMakeTranslation(0.0, -translation)
return
}
recognizer.enabled = false
}
default:
println("Default executed")
}
}