Swift: How to zoom an image as I scroll up in scrollview - ios

I'm trying to zoom my image view as I scroll the scrollView past the top of the screen. Here's my code:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
if (offset <= 0) {
let ratio: CGFloat = -offset*1.0 / UIScreen.main.bounds.height
self.coverImageView.transform = CGAffineTransform(scaleX: 1.0 + ratio, y: 1.0 + ratio)
}
}
This zooms the image as I scroll up, but because I am also scrolling up, my view goes down, and reveals the white background behind the image as it expands. How do I prevent that from happening?

It sounds like a scroll view is not really suited to what you are trying to do. How about using a gesture recognizer instead? Something along these lines:
override func viewDidLoad() {
super.viewDidLoad()
coverImageView.isUserInteractionEnabled = true
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPan))
coverImageView.addGestureRecognizer(panGestureRecognizer)
}
func didPan(panGestureRecognizer: UIPanGestureRecognizer) {
let translation = panGestureRecognizer.translation(in: coverImageView)
if translation.y > 0 {
let zoomRatio = (translation.y * 0.1) + 1.0
coverImageView.transform = CGAffineTransform(scaleX: zoomRatio, y: zoomRatio)
}
}
You'll have to play around to get it to behave exactly how you want, but it should be enough to get you started.

Related

[Swift-iOS]Dragging down view using UIPanGestureRecognizer, not stretching

I'm trying to drag down a specific view to make the height go longer than before using UIPanGestureRecognizer. However, it keeps on going back to the normal size that i had initially programmed. The view that i'm trying to stretch keeps on shaking and goes back to the initial height. Any guesses why?
let CalendarDrag = UIPanGestureRecognizer(target: self, action: #selector(didDragCalendar))
lineView.isUserInteractionEnabled = true
lineView.addGestureRecognizer(CalendarDrag)
}
#objc func didDragCalendar(sender: UIPanGestureRecognizer) {
let velocity = sender.velocity(in: self.view) //속도
let translation = sender.translation(in: self.view) //μœ„μΉ˜
let height = self.topView.frame.maxY
if sender.state == .ended{
if velocity.y>0{
calendar.scope = .month
print("down")
}else{
calendar.scope = .week
print("up")
}
}else{
if height <= height+translation.y && height+translation.y <= height+230{
self.topView.frame = CGRect(x: 0, y: 0, width: self.topView.frame.width, height: height+translation.y)
UIView.animate(withDuration: 0, animations: {
self.view.layoutIfNeeded()
sender.setTranslation(CGPoint.zero, in: self.view)
})
}
}
}
For more information, I'm right now trying to make FSCalendar to show in "month" and "week" view according to the UIPanGesture that user make.
You are changing the frame height of your view. Don't do that.
Have an instance var to a height constraint, and change the constant value for that constraint. Then when you call layoutIfNeeded() from your animation, the constraint will change your height for you.

Change a label position while scrollview is scrolling

I have a label outside of a scrollview. I want to move the label from left to right of screen and vice versa when scrollview scrolling up and down.I wrote this code and it works when scrollview is scrolling in normal speed and when it scrolls very fast the label x position changes very slowly. How I can do that for all scrollview scrolling speeds?
func scrollViewDidScroll(_ scrollView: UIScrollView!) {
let offset = scrollView.contentOffset.y
let imahey = view.convert(placeImgview.frame, from:scrollview).origin.y + placeImgview.frame.width
print(offset)
let ratio: CGFloat = (-offset*1.0 / placeImgview.frame.height)
topBarView.alpha = -ratio
if placeName.frame.origin.x < -20 {
placeName.center = CGPoint(x: placeName.center.x - 3*ratio, y: placeName.center.y + ratio/1.5)
}
if (self.lastContentOffset > scrollView.contentOffset.y) {
placeName.center = CGPoint(x: placeName.center.x + 6*ratio, y: placeName.center.y - 2*ratio)
if offset == -20 {
placeName.frame.origin.x = -view.frame.width/2
placeName.frame.origin.y = 60
}
}
self.lastContentOffset = scrollView.contentOffset.y
}
So, the problem is that the faster you scroll, the larger the "gap" gets inbetween scrollViewDidScroll events.
You probably should consider to move around your label using UIView.animate... that would create a more consistent experience because the animation always has the same speed.
This way you could apply the animation using CGAffineTransform(translationX: , y: )
depending if your contentOffset.y passes a given threshold, whenever that label should appear or disappear.

How can you make a custom scroll indicator using Swift?

I have a UITableView that has the potential to have a lot of rows (importing contacts) and I'm looking to implement a custom scroll indicator similar to how Snapchat has. The two basic goals are as follows:
When you scroll the tableview, the scroll indicator moves down/up the correct amount (and does not exceed the top or bottom of the tableview).
When you drag the scroll indicator, it not only follows your finger, but scrolls the tableview to correct amount.
I've been trying to use different calculations for the scroll amount, but I can't seem to get it to be correct. I've been doing work on a sample app (just containing names in a tableview) and I can get it to work okay on there but when I transfer the code to a larger tableview, it doesn't work properly.
I've created a custom view for my scroll indicator which is basically just a view with a label on it that changes based on the letter of the name in the cell:
class IndicatorView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var letterLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("IndicatorView", owner: self, options: nil)
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView)
contentView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
contentView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
contentView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
contentView.layer.cornerRadius = 5
}
}
Here are the methods I'm using...
scrollViewWillEndDragging:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
//Get velocity and store in global variable
self.velocity = velocity.y
}
scrollViewDidScroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollBar.becomeFirstResponder()
let offset: CGPoint = scrollView.contentOffset
var frame = self.scrollBar.frame
var numberOfRows = 0
for i in 0...friendsTableView.numberOfSections - 1 {
let rows = friendsTableView.numberOfRows(inSection: i)
numberOfRows += rows
}
//Calculate the amount of scroll
let percentage = (44*CGFloat(numberOfRows) + 40)/friendsTableView.frame.height
UIView.animate(withDuration: 0.0) {
//Set the y value to new percentage
frame.origin.y = 30 + offset.y + (offset.y/percentage)
//Set label to correct letter
let currentPoint = CGPoint(x: 30, y: frame.origin.y)
if let indexPath = self.friendsTableView.indexPathForRow(at: currentPoint) {
let letter = self.sectionHeaders[indexPath.section]
self.scrollBar.letterLabel.text = letter
}
//Only expand the scroll indicator if the user scrolls fast enough
if abs(self.velocity) > 1.5 {
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 50
frame.origin.x = self.screenWidth - 50
}
//Set the new y value of the scroll indicator
self.scrollBar.frame = frame
}
}
Custom Pan Gesture Recognizer:
#objc func customDrag(_ sender: UIPanGestureRecognizer) {
self.friendsTableView.bringSubview(toFront: scrollBar)
//Only expand fully if the state is not ended
if sender.state != .ended {
var frame = self.scrollBar.frame
//Expand the view fully
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 100
frame.origin.x = self.screenWidth - 100
self.scrollBar.frame = frame
//Get translation and track the content offset
let translation = sender.translation(in: friendsTableView)
let originalContentOffset = friendsTableView.contentOffset
var numberOfRows = 0
for i in 0...friendsTableView.numberOfSections - 1 {
let rows = friendsTableView.numberOfRows(inSection: i)
numberOfRows += rows
}
//Calculate the percentage of scroll and add the translation to the original content offset
let percentage = (44*CGFloat(numberOfRows) + 50)/friendsTableView.frame.height
let vertTranslation = (percentage*translation.y) + originalContentOffset.y
//Set the content offset and the center of the scroll indicator
friendsTableView.contentOffset = CGPoint(x: 0, y: (vertTranslation))
self.scrollBar.center = CGPoint(x: self.screenWidth-50, y: self.scrollBar.center.y + (translation.y/percentage))
sender.setTranslation(CGPoint.zero, in: friendsTableView)
} else {
//once the state is ended, collapse back to the smaller size
var frame = self.scrollBar.frame
self.scrollBar.letterLabel.isHidden = false
frame.size.width = 50
frame.origin.x = self.screenWidth - 50
self.scrollBar.frame = frame
}
}
I found the basics for the code that does the percentage calculation on another StackOverflow post but admittedly, I'm not 100% sure what it is doing/why it works in my demo app and not the real one. I've applied different number and calculations but still can't seem to get it right.
Any help or insight would be much appreciated.

Stop UIDynamicAnimator Whit view Limits

I have been trying to create a custom UIClass that implements an horizontal scrolling for its inner content by using UIPanGestureRecognizer.
My first success approach was:
#IBAction func dragAction(_ sender: UIPanGestureRecognizer) {
let velocity = sender.velocity(in: sender.view)
if velocity.x > 0{
//print("\(logClassName) Dragging Right ...")
if innerView!.frame.origin.x < CGFloat(0){
offSetTotal += 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
else{
//print("\(logClassName) Dragging Left ...")
if totalWidth - innerView!.bounds.maxX < innerView!.frame.origin.x{
offSetTotal -= 1
innerView?.transform = CGAffineTransform(translationX: CGFloat(offSetTotal), y: 0)
}
}
}
That way, althought is a little bit clunky, I am able to scroll from left to right (and reverse) and the innerview is always covering in the holderView.
Then, and because i wanted to get the scrolling effect that you would get with a UIScrolling view i tried the following approach
#objc func handleTap(_ pan: UIPanGestureRecognizer){
let translation = pan.translation(in: innerView)
let recogView = pan.view
let curX = recogView?.frame.origin.x
if pan.state == UIGestureRecognizerState.began {
self.animator.removeAllBehaviors()
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.changed {
recogView?.frame.origin.x = curX! + translation.x
pan.setTranslation(CGPoint.init(x: 0, y: 0), in: recogView)
}else if pan.state == UIGestureRecognizerState.ended {
let itemBehavior = UIDynamicItemBehavior(items: [innerView!]);
var velocity = pan.velocity(in: self)
print(velocity.y)
velocity.y = 0
itemBehavior.addLinearVelocity(velocity, for: innerView!);
itemBehavior.friction = 1
itemBehavior.resistance = 1;
//itemBehavior.elasticity = 0.8;
self.collision = UICollisionBehavior(items: [innerView!]);
self.collision!.collisionMode = .boundaries
self.animator.addBehavior(collision!)
self.animator.addBehavior(itemBehavior);
}
Now the problem i face is that it moves horizontally but it goes away and gets lose.
Is that the right approach? Is there a delegate?
The question was posted due to a lack of knowledge of UIScrollViews. I Knew how to do with Storyboard and only scroll vertically.
I ended up creating a programatically UIScrollView and setting up its content size with the inner view.
scrollView = UIScrollView(frame: bounds)
scrollView?.backgroundColor = UIColor.yellow
scrollView?.contentSize = innerView!.bounds.size
scrollView?.addSubview(innerView!)
addSubview(scrollView!)
But first I have defined the innerView.

Move UIImageView Independently from its Mask in Swift

I'm trying to mask a UIImageView in such a way that it would allow the user to drag the image around without moving its mask. The effect would be similar to how one can position an image within the Instagram app essentially allowing the user to define the crop region of the image.
Here's an animated gif to demonstrate what I'm after.
Here's how I'm currently masking the image and repositioning it on drag/pan events.
import UIKit
class ViewController: UIViewController {
var dragDelta = CGPoint()
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
attachMask()
// listen for pan/drag events //
let pan = UIPanGestureRecognizer(target:self, action:#selector(onPanGesture))
pan.maximumNumberOfTouches = 1
pan.minimumNumberOfTouches = 1
self.view.addGestureRecognizer(pan)
}
func onPanGesture(gesture:UIPanGestureRecognizer)
{
let point:CGPoint = gesture.locationInView(self.view)
if (gesture.state == .Began){
print("begin", point)
// capture our drag start position
dragDelta = CGPoint(x:point.x-imageView.frame.origin.x, y:point.y-imageView.frame.origin.y)
} else if (gesture.state == .Changed){
// update image position based on how far we've dragged from drag start
imageView.frame.origin.y = point.y - dragDelta.y
} else if (gesture.state == .Ended){
print("ended", point)
}
}
func attachMask()
{
let mask = CAShapeLayer()
mask.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 100, width: imageView.frame.size.width, height: 400), cornerRadius: 5).CGPath
mask.anchorPoint = CGPoint(x: 0, y: 0)
mask.fillColor = UIColor.redColor().CGColor
view.layer.addSublayer(mask)
imageView.layer.mask = mask;
}
}
This results in both the image and mask moving together as you see below.
Any suggestions on how to "lock" the mask so the image can be moved independently underneath it would be very much appreciated.
Moving a mask and frame separately from each other to reach this effect isn't the best way to go about doing this. Most apps that do this sort of effect do the following:
Add a UIScrollView to the root view (with panning/zooming enabled)
Add a UIImageView to the UIScrollView
Size the UIImageView such that it has a 1:1 ratio with the image
Set the contentSize of the UIScrollView to match that of the UIImageView
The user can now pan around and zoom into the UIImageView as needed.
Next, if you're, say, cropping the image:
Get the visible rectangle (taken from Getting the visible rect of an UIScrollView's content)
CGRect visibleRect = [scrollView convertRect:scrollView.bounds toView:zoomedSubview];
Use whatever cropping method you'd like on the UIImage to get the necessary content.
This is the smoothest way to handle this kind of interaction and the code stays pretty simple!
Just figured it out. Setting the CAShapeLayer's position property to the inverse of the UIImageView's position as it's dragged will lock the CAShapeLayer in its original position however CoreAnimation by default will attempt to animate it whenever its position is reassigned.
This can be disabled by wrapping both position settings within a CATransaction as shown below.
func onPanGesture(gesture:UIPanGestureRecognizer)
{
let point:CGPoint = gesture.locationInView(self.view)
if (gesture.state == .Began){
print("begin", point)
// capture our drag start position
dragDelta = CGPoint(x:point.x-imageView.frame.origin.x, y:point.y-imageView.frame.origin.y)
} else if (gesture.state == .Changed){
// update image & mask positions based on the distance dragged
// and wrap both assignments in a CATransaction transaction to disable animations
CATransaction.begin()
CATransaction.setDisableActions(true)
mask.position.y = dragDelta.y - point.y
imageView.frame.origin.y = point.y - dragDelta.y
CATransaction.commit()
} else if (gesture.state == .Ended){
print("ended", point)
}
}
UPDATE
Here's an implementation of what I believe AlexKoren is suggesting. This approach nests a UIImageView within a UIScrollView and uses the UIScrollView to mask the image.
class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
var imageView:UIImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
let image = UIImage(named: "point-bonitas")
imageView.image = image
imageView.frame = CGRectMake(0, 0, image!.size.width, image!.size.height);
scrollView.delegate = self
scrollView.contentMode = UIViewContentMode.Center
scrollView.addSubview(imageView)
scrollView.contentSize = imageView.frame.size
let scale = scrollView.frame.size.width / scrollView.contentSize.width
scrollView.minimumZoomScale = scale
scrollView.maximumZoomScale = scale // set to 1 to allow zoom out to 100% of image size //
scrollView.zoomScale = scale
// center image vertically in scrollview //
let offsetY:CGFloat = (scrollView.contentSize.height - scrollView.frame.size.height) / 2;
scrollView.contentOffset = CGPointMake(0, offsetY);
}
func scrollViewDidZoom(scrollView: UIScrollView) {
print("zoomed")
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
}
The other, perhaps simpler way would be to put the image view in a scroll view and let the scroll view manage it for you. It handles everything.

Resources