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.
Related
I have 2 UIScrollView. They are not subviews of each other. One is just sitting above the other as an overlay, which was used to get this effect:
Now, I have a UIButton on the 2nd ScrollView (the one sitting below).
I cannot tap on this button because the top scroll view is capturing all the touches.
So here is what I am trying to accomplish. But I'm stuck.
I put a UITapGestureRecognizer recognized on the UIScrollView
I want to convert the CGPoint of the tap and translate it over to a position on the 2nd scrollView
If the point intersects with the position of the UIButton on the 2nd UIScrollView, then I'll just print a message.
Here is my code. Please help me what's wrong with it:
#IBAction func bufferviewCliked (sender: UITapGestureRecognizer) {
// First I try to get the point of the object clicked inside as a point on
// the main self.view
let locationInMainView = sender.location(in: self.view)
// Next I try to convert that point, into a point on my backgroundScrollView
let touchPointInSecondView = self.view.convert(locationInMainView, from: backGround)
// If the point intersects with the point on the UIButton in the backgroundScrollView
// Then print hi
if followButton.frame.contains(touchPointInSecondView) {
print("HI")
}
}
The problem is my point coordinates are wrong and I'm not sure if I used the right thing.
You can get the locationInView from button
#IBAction func bufferviewCliked (sender: UITapGestureRecognizer) {
let location = sender.location(in: followButton)
// If the point intersects with the point on the UIButton in the backgroundScrollView
// Then print hi
if followButton.bounds.contains(location) {
print("HI")
}
}
On Facebook Messenger for iOS, it has it so if the keyboard is hidden, if you swipe up, the keyboard will show. It does this exactly in reverse to the interactive keyboard dismissal mode: the keyboard reveals itself as you swipe up at the speed in which you swipe up.
Does anybody have any pointers on how to do this?
Edit: thanks for the answers! I was mostly looking into whether there was a built-in way to do this, since I saw it being done in Facebook Messenger. However, I just read a blog post where they said they had to screenshot the system keyboard to get the effect—so I’ll assume there’s no built-in way to do this! As mentioned in the comments, I’m using a custom keyboard, so this should be a lot easier, since I have control over the frame!
Basically you'll need UIPanGestureRecognizer.
Set UIScreenEdgePanGestureRecognizer for bottom edge, UIPanGestureRecognizer for hiding the keyboard in Storyboard and drag #IBAction outlets to the code.
Set you keyboard view container with your keyboard in the bottom of the controller in Storyboard, so that user doesn't see it. Drag an #IBOutlet to your code so that you'll be able to modify it's frame.
In gesture actions when dragging your animate the view movement.
When stopped dragging you need to check the view's position and animate it to the destination if it's not there yet.
Also you'll need to add a check for the dragging area so that user cannot drag it further.
It's simple, you'll just need to check all cases and test it properly.
This is a basic setup you can build from this:
class ViewController: UIViewController {
#IBOutlet weak var keyboardContainerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func onEdgePanGestureDrag(_ sender: UIScreenEdgePanGestureRecognizer) {
let point = sender.location(in: view)
view.layoutIfNeeded()
UIView.animate(withDuration: 0.33) {
// Animate your custom keyboard view's position
self.keyboardContainerView.frame = CGRect(x: self.keyboardContainerView.bounds.origin.x,
y: point.y,
width: self.keyboardContainerView.bounds.width,
height: self.keyboardContainerView.bounds.height)
}
view.layoutIfNeeded()
}
#IBAction func onPanGestureDrag(_ sender: UIPanGestureRecognizer) {
let point = sender.location(in: view)
view.layoutIfNeeded()
UIView.animate(withDuration: 0.33) {
// Animate your custom keyboard view's position
self.keyboardContainerView.frame = CGRect(x: self.keyboardContainerView.bounds.origin.x,
y: point.y,
width: self.keyboardContainerView.bounds.width,
height: self.keyboardContainerView.bounds.height)
}
view.layoutIfNeeded()
}
}
Here is an implementation that worked for me. It's not perfect but it works fairly well and is simple to implement.You will need a collection view or a table view.
For the sake of simplicity I will only show code that is necessary for this feature. so please handle everything else that is necessary for the views' initialization.
class ViewController: UIViewController {
var collectionView: UICollectionView!
var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.panGestureRecognizer.addTarget(self, action: #selector(handlePan(_:)))
collectionView.keyboardDismissMode = .interactive
// Only necessary for empty collectionView
collectionView.alwaysBounceVertical = true
}
func handlePan(_ sender: UIPanGestureRecognizer) {
if sender.state == .changed {
let translation = sender.translation(in: view)
if translation.y < 0 && collectionView.isAtBottom && !self.textView.isFirstResponder() {
self.textView.becomeFirstResponder()
}
}
}
}
extension UIScrollView {
var isAtBottom: Bool {
return contentOffset.y >= verticalOffsetForBottom
}
var verticalOffsetForBottom: CGFloat {
let scrollViewHeight = bounds.height
let scrollContentSizeHeight = contentSize.height
let bottomInset = contentInset.bottom
let scrollViewBottomOffset = scrollContentSizeHeight + bottomInset - scrollViewHeight
return scrollViewBottomOffset
}
}
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.
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")
}
}