First of all I have checked almost every places over the internet but I didn't get any solution about this topic.
In my cases I have multiple UIView objects inside a superview or you can say a canvas where I am drawing this views.
All this views are attached with pan gesture so they can be moved inside anywhere of their superview.
Some of this views can be rotated using either rotation gesture or CGAffineTransformRotate.
Whenever any of the view will be outside of the main view then it will be deleted.
Now following are my code.
#IBOutlet weak var mainView: UIView!
var newViewToAdd = UIView()
newViewToAdd.layer.masksToBounds = true
var transForm = CGAffineTransformIdentity
transForm = CGAffineTransformScale(transForm, 0.8, 1)
transForm = CGAffineTransformRotate(transForm, CGFloat(M_PI_4)) //Making the transformation
newViewToAdd.layer.shouldRasterize = true //Making the view edges smooth after applying transforamtion
newViewToAdd.transform = transForm
self.mainView.addSubview(newViewToAdd) //Adding the view to the main view.
Now in case the gesture recognizer its inside the custom UIView Class -
var lastLocation: CGPoint = CGPointMake(0, 0)
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.superview?.bringSubviewToFront(self)
lastLocation = self.center //Getting the last center point of the view on first touch.
}
func detectPan(recognizer: UIPanGestureRecognizer){
let translation = recognizer.translationInView(self.superview!) //Making the translation
self.center = CGPointMake(lastLocation.x + translation.x, lastLocation.y + translation.y) //Updating the center point.
switch(recognizer.state){
case .Began:
break
case .Changed:
//MARK: - Checking The view is outside of the Superview or not
if (!CGRectEqualToRect(CGRectIntersection(self.superview!.bounds, self.frame), self.frame)) //if its true then view background color will be changed else background will be replaced.
{
self.backgroundColor = outsideTheViewColor
var imageViewBin : UIImageView
imageViewBin = UIImageView(frame:CGRectMake(0, 0, 20, 25));
imageViewBin.image = UIImage(named:"GarbageBin")
imageViewBin.center = CGPointMake(self.frame.width/2, self.frame.height/2)
addSubview(imageViewBin)
}else{
for subViews in self.subviews{
if subViews.isKindOfClass(UIImageView){
subViews.removeFromSuperview()
}
self.backgroundColor = deSelectedColorForTable
}
}
case .Ended:
if (!CGRectEqualToRect(CGRectIntersection(self.superview!.bounds, self.frame), self.frame)) //If its true then the view will be deleted.
{
self.removeFromSuperview()
}
default: break
}
}
The main problem is if the view is not rotated or transformed then all the "CGRectIntersection" inside the .Changed/.Ended case is working fine as expected but if the view is rotated or transformed then "CGRectIntersection" always becoming true even the view is inside the "mainView" and its removing from the mainview/superview.
Please help about my mistake.
Thanks in advance.
Frame of the view gets updated after applying transform. Following code ensures that it is inside the its superviews bounds.
if (CGRectContainsRect(self.superview!.bounds, self.frame))
{
//view is inside of the Superview
}
else
{
//view is outside of the Superview
}
Related
I'm using this code to make a UIViewController pannable take from this so post.
class ViewControllerPannable: UIViewController {
var panGestureRecognizer: UIPanGestureRecognizer?
var originalPosition: CGPoint?
var currentPositionTouched: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
view.addGestureRecognizer(panGestureRecognizer!)
}
func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)
if panGesture.state == .began {
originalPosition = view.center
currentPositionTouched = panGesture.location(in: view)
} else if panGesture.state == .changed {
view.frame.origin = CGPoint(
x: translation.x,
y: translation.y
)
} else if panGesture.state == .ended {
let velocity = panGesture.velocity(in: view)
if velocity.y >= 1500 {
UIView.animate(withDuration: 0.2
, animations: {
self.view.frame.origin = CGPoint(
x: self.view.frame.origin.x,
y: self.view.frame.size.height
)
}, completion: { (isCompleted) in
if isCompleted {
self.dismiss(animated: false, completion: nil)
}
})
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.center = self.originalPosition!
})
}
}
}
}
This works great on older phones that don't have a notch. But if the phone has a notch, once you start panning, any views pinned to the safe area jump to the superview. I think the issue is in the
view.frame.origin = CGPoint(
x: translation.x,
y: translation.y
)
But I'm not sure how to make anything that was pinned to the safe area stay that way when panning.
You want to avoid layoutSubviews after you drag the view outside the superview's frame. As long as you are not calling setNeedsLayout manually, you can achieve this by ensuring that last frame.size change happens before crossing outside. Just make enough variables to precisely control for this situation.
I encountered this issue with a custom action-sheet-like presentation. When I pulled the view down to dismiss, there was no problem. But when I pulled the view up to stretch and then down, the safeAreaInsets jumped from 0 to non-zero suddenly.
The reason was because my view stretched upwards but not downwards. Pull up was frame.origin.y change and a frame.size.height change. Downwards was only a frame.origin.y change. However, UIPanGestureRecognizer does not have infinite granularity, and so the jump from negative to positive translation caused origin.y to change at the same time as frame.size.height in the downward direction. This triggered layoutSubviews while the view was partially outside the superview, which meant it ran layout code that suddenly acknowledged a safeAreaInsets change.
My solution is essentially:
if translation < 0 && oldTranslation > 0 || translation > 0 && oldTranslation < 0 {
view.frame = originalFrame
} else {
view.frame = calculate(translation)
}
This forces the view to be a static size before it crosses the superview's boundary, ensuring that layoutSubviews is called with the correct safeAreaInsets, but as the position changes, the safeArea does not.
Your situation may be a little different if you have something else triggering layoutSubviews. You might have to hunt that down.
Another thing I notice is that you're moving the UIViewController's view when it might have a parent, which could have other implications or edge cases because safeArea is inherited from the UIViewController hierarchy rather than solely superview.
The question is how should I define and set my shape layer's position and how should it be updated so that the layer appears where I'm expecting it to during the animation? Namely, the shape should be stuck on the end of the stick.
I have a CALayer instance called containerLayer, and it has a sublayer which is a CAShapeLayer instance called shape. containerLayer is supposed to place shape at a specific position unitLoc like this:
class ContainerLayer: CALayer, CALayerDelegate {
// ...
override func layoutSublayers() {
super.layoutSublayers()
if !self.didSetup {
self.setup()
self.didSetup = true
}
updateFigure()
setNeedsDisplay()
}
func updateFigure() {
figureCenter = self.bounds.center
figureDiameter = min(self.bounds.width, self.bounds.height)
figureRadius = figureDiameter/2
shapeDiameter = round(figureDiameter / 5)
shapeRadius = shapeDiameter/2
locRadius = figureRadius - shapeRadius
angle = -halfPi
unitLoc = CGPoint(x: self.figureCenter.x + cos(angle) * locRadius, y: self.figureCenter.y + sin(angle) * locRadius)
shape.bounds = CGRect(x: 0, y: 0, width: shapeDiameter, height: shapeDiameter)
shape.position = unitLoc
shape.updatePath()
}
// ...
}
I'm having trouble finding the right way to specify what this position should be before, and during a resize animation which changes containerLayer.bounds. I do understand that the problem I'm having is that I'm not setting the position in such a way that the animation will display it the way that I'm expecting it would.
I have tried using a CABasicAnimation(keyPath: "position") to animate the position, and it improved the result over what I had tried previously, but it's still off.
#objc func resize(sender: Any) {
// MARK:- animate containerLayer bounds & shape position
// capture bounds value before changing
let oldBounds = self.containerLayer.bounds
// capture shape position value before changing
let oldPos = self.containerLayer.shape.position
// update the constraints to change the bounds
isLarge.toggle()
updateConstraints()
self.view.layoutIfNeeded()
let newBounds = self.containerLayer.bounds
let newPos = self.containerLayer.unitLoc
// set up the bounds animation and add it to containerLayer
let baContainerBounds = CABasicAnimation(keyPath: "bounds")
baContainerBounds.fromValue = oldBounds
baContainerBounds.toValue = newBounds
containerLayer.add(baContainerBounds, forKey: "bounds")
// set up the position animation and add it to shape layer
let baShapePosition = CABasicAnimation(keyPath: "position")
baShapePosition.fromValue = oldPos
baShapePosition.toValue = newPos
containerLayer.shape.add(baShapePosition, forKey: "position")
containerLayer.setNeedsLayout()
self.view.layoutIfNeeded()
}
I also tried using the presentation layer like this to set the position, and it also seems to get it close, but it's still off.
class ViewController: UIViewController {
//...
override func loadView() {
super.loadView()
displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.default)
//...
}
#objc func animationDidUpdate(displayLink: CADisplayLink) {
let newCenter = self.containerLayer.presentation()!.bounds.center
let new = CGPoint(x: newCenter.x + cos(containerLayer.angle) * containerLayer.locRadius, y: newCenter.y + sin(containerLayer.angle) * containerLayer.locRadius)
containerLayer.shape.position = new
}
//...
}
class ContainerLayer: CALayer, CALayerDelegate {
// ...
func updateFigure() {
//...
//shape.position = unitLoc
//...
}
// ...
}
With some slight exaggeration, I was able to make it clearer what's happening in your code. In my example, the circle layer is supposed to remain 1/3 the height of the background view:
At the time the animation starts, the background view has already been set to its ultimate size at the end of the animation. You don't see that, because animation relies on portraying the layer's presentation layer, which is unchanged; but the view itself has changed. Therefore, when you position the shape of the shape layer, and you do it in terms of the view, you are sizing and positioning it at the place it will need to be when the animation ends. Thus it jumps to its final size and position, which makes sense only when we reach the end of the animation.
Okay, but now consider this:
Isn't that nicer? How is it done? Well, using the principles I have already described elsewhere, I've got a layer with a custom animatable property. The result is that on every frame of the animation, I get an event (the draw(in:) method for that layer). I respond to this by recalculating the path of the shape layer. Thus I am giving the shape layer a new path on every frame of the animation, and so it behaves smoothly. It stays in the right place, it resizes in smooth proportion to the size of the background view, and its stroke thickness remains constant throughout.
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.
Sorry for the header, I just did not know how to call it.
I want to make my scrollView able to scroll from top or bottom until the circle in the center, like LinkedIn does it:
So there you can scroll your image until the circle in the center. How can I improve my code, to achieve this?
My code is:
scrollView!.delegate = self
imageView.frame = CGRectMake(0, 0, (scrollView?.frame.width)!, (scrollView?.frame.height)!)
if let validImage = self.avatarImage {
self.imageView.image = validImage
imageView.contentMode = .ScaleAspectFill
imageView.frame = avatarImageFrame!
}
imageView.userInteractionEnabled = true
scrollView?.addSubview(imageView)
scrollView?.contentSize = (avatarImage?.size)!
let scrollViewFrame = scrollView?.frame
let scaleWidth = (scrollViewFrame?.size.width)! / (scrollView?.contentSize.width)!
let scaleHeight = (scrollViewFrame?.size.height)! / (scrollView?.contentSize.height)!
let minScale = min(scaleHeight, scaleWidth)
scrollView?.minimumZoomScale = minScale
scrollView?.maximumZoomScale = 1
scrollView?.zoomScale = minScale
centerScrollViewContents()
}
func centerScrollViewContents() {
let boundsSize = scrollView?.bounds.size
var contentsFrame = imageView.frame
if contentsFrame.size.width < boundsSize?.width {
contentsFrame.origin.x = ((boundsSize?.width)! - contentsFrame.size.width) / 2
} else {
contentsFrame.origin.x = 0
}
if contentsFrame.size.height < boundsSize?.height {
contentsFrame.origin.y = ((boundsSize?.height)! - contentsFrame.size.height) / 2
} else {
contentsFrame.origin.y = 0
}
contentsFrame.size.height = UIScreen.mainScreen().bounds.height
imageView.frame = contentsFrame
}
func scrollViewDidZoom(scrollView: UIScrollView) {
centerScrollViewContents()
}
There are many ways to achieve this. Probably what would work best with the scroll view is to set the frame of the scroll view the same as is the frame of the circle. The content size should still be the same as the background (or rather the image in your case) and when you first enter the screen the content offset should be set so that the image is in center.
So till now you can imagine a small scroll view just behind the circle but the rest of the background is missing. To fix this all you need to do is disable bound clipping on the scroll view which is set to true by default. So set "clip subviews" to false.
Now you can already see the whole image and bouncing is correct to always keep the image inside the circle but one more thing is missing. Since the scroll view is actually quite small you can see you may not interact with it outside its frame. This is a natural behavior which can be changed by overriding a method called hitTest. This method will return a view which should collect the interaction. So we need to subclass the superview of the scroll view:
Regardless of where you do this in the interface builder or in the code I expect you have some view (will call it background) which contains a scroll view (the small one) and an overlay (the one with the circle). The background must be subclassed and have a reference to the scroll view. Then simply override the hit test method like so:
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
var myFrame = frame
myFrame.origin = CGPointZero // maybe this should be commented out
if CGRectContainsPoint(myFrame, point) {
return scrollView
} else {
return super.hitTest(point, withEvent: event)
}
}
By doing this the scroll view is interactable from anywhere within the background, whole image is visible on the background, the image will always be within the circle.
Good luck.
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")
}
}