Double Tap to Zoom In/Out - ios

I have an app that displays a bunch of pictures when the user presses the button from a text list view screen. I have a specific subclass that allows the user to pinch and zoom in, but I am curious as to what code I need to enter in my particular swift file to allow the user to double tap to zoom in and out. I get three errors using this code. Two say "Value of type 'ZoomingScrollView' has no member 'scrollview' or 'imageview' " and also " 'CGRectZero' is unavaiable in Swift" My code for the subclass that allows the user to zoom in is below along with the code I am using to double tap to zoom.
import UIKit
class ZoomingScrollView: UIScrollView, UIScrollViewDelegate {
#IBOutlet weak var viewForZooming: UIView? = nil {
didSet {
self.delegate = self
} }
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return viewForZooming }
#IBAction func userDoubleTappedScrollview(recognizer: UITapGestureRecognizer) {
if let scrollV = self.scrollview {
if (scrollV.zoomScale > scrollV.minimumZoomScale) {
scrollV.setZoomScale(scrollV.minimumZoomScale, animated: true)
}
else {
//(I divide by 3.0 since I don't wan't to zoom to the max upon the double tap)
let zoomRect = self.zoomRectForScale(scrollV.maximumZoomScale / 3.0, center: recognizer.locationInView(recognizer.view))
self.scrollview?.zoomToRect(zoomRect, animated: true)
}
} }
func zoomRectForScale(scale : CGFloat, center : CGPoint) -> CGRect {
var zoomRect = CGRectZero
if let imageV = self.imageView {
zoomRect.size.height = imageV.frame.size.height / scale;
zoomRect.size.width = imageV.frame.size.width / scale;
let newCenter = imageV.convertPoint(center, fromView: self.scrollview)
zoomRect.origin.x = newCenter.x - ((zoomRect.size.width / 2.0));
zoomRect.origin.y = newCenter.y - ((zoomRect.size.height / 2.0));
}
return zoomRect; }}

The issue seems to be that you copied some code from a view controller that had a scrollView and imageView outlet. I'm guessing your viewForZooming: UIView is your image view that you are zooming on. You also don't have to reference self.scrollView anymore because self is the scroll view since you're subclassing scroll view :) I think the code below should fix your problem (note: it's in the latest swift syntax. what you posted was not, so you may have to switch the code back to the old style if necessary. You should try to use the latest Swift though). Good luck and let me know if you have questions.
import UIKit
class ZoomingScrollView: UIScrollView, UIScrollViewDelegate {
#IBOutlet weak var viewForZooming: UIView? = nil {
didSet {
self.delegate = self
} }
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return viewForZooming }
#IBAction func userDoubleTappedScrollview(recognizer: UITapGestureRecognizer) {
if (zoomScale > minimumZoomScale) {
setZoomScale(minimumZoomScale, animated: true)
}
else {
//(I divide by 3.0 since I don't wan't to zoom to the max upon the double tap)
let zoomRect = zoomRectForScale(scale: maximumZoomScale / 3.0, center: recognizer.location(in: recognizer.view))
zoom(to: zoomRect, animated: true)
}
}
func zoomRectForScale(scale : CGFloat, center : CGPoint) -> CGRect {
var zoomRect = CGRect.zero
if let imageV = self.viewForZooming {
zoomRect.size.height = imageV.frame.size.height / scale;
zoomRect.size.width = imageV.frame.size.width / scale;
let newCenter = imageV.convert(center, from: self)
zoomRect.origin.x = newCenter.x - ((zoomRect.size.width / 2.0));
zoomRect.origin.y = newCenter.y - ((zoomRect.size.height / 2.0));
}
return zoomRect;
}
}

Related

Interactive ViewController transition triggered by pinch and pan gesture recognisers simultaneously

I have two viewControllers:
ViewController1
A complex stack of sub viewcontrollers with somewhere in the middle an imageView
ViewController2
A scrollView with an imageView embedded in it
What I'm trying to achieve is a transition between the two viewControllers which gets triggered by pinching the imageView from viewController 1 causing it to zoom in and switch over to viewController 2. When the transition has ended, the imageView should be zoomed in as far as it's been zoomed during the pinch gesture triggered transition.
At the same time I want to support panning the image while performing the zoom transition so that just like with the zoom, the image in the end state will be transformed to the place it's been panned to.
So far I've tried the Hero transitions pod and a custom viewController transitions I wrote myself. The problem with the hero transitions is that the image doesn't properly get snapped to the end state in the second viewController. The problem I had with the custom viewController transition is that I couldn't get both zooming and panning to work at the same time.
Does anyone have an idea of how to implement this in Swift? Help is much appreciated.
The question can be divided in to two:
How to implement pinch zoom and dragging using pan gesture on an imageView
How to present a view controller with one of its subviews (imageView in vc2) positioned same as a subview (imageView in vc1) in the presenting view controller
Pinch gesture zoom: Pinch zooming is easier to implement using UIScrollView as it supports it out of the box with out a need to add the gesture recogniser. Create a scrollView and add the view you'd like to zoom with pinch as its subview (scrollView.addSubview(imageView)). Don't forget to add the scrollView itself as well (view.addSubview(scrollView)).
Configure the scrollView's min and max zoom scales: scrollView.minimumZoomScale, scrollView.maximumZoomScale. Set a delegate for scrollView.delegate and implement UIScrollViewDelegate:
func viewForZooming(in scrollView: UIScrollView) -> UIView?
Which should return your imageView in this case and,
Also conform to UIGestureRecognizerDelegate and implement:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
Which should return true. This is the key that allows us have pan gesture recogniser work with the internal pinch gesture recogniser.
Pan gesture dragging: Simply create a pan gesture recogniser with a target and add it to your scroll view scrollView.addGestureRecognizer(pan).
Handling gestures: Pinch zoom is working nicely by this stage except you'd like to present the second view controller when pinching ends. Implement one more UIScrollViewDelegate method to be notified when zooming ends:
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat)
And call your method that presents the detail view controller presentDetail(), we'll implement it in a bit.
Next step is to handle the pan gesture, I'll let the code explain itself:
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
#objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
The implementation moves imageView around following the pan location and calls presentDetail() when gesture ends.
Before we implement presentDetail(), head to the detail view controller and add properties to hold imageViewFrame and the image itself. Now in vc1, we implement presentDetail() as such:
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
// Note that we do not need the animation.
present(detail, animated: false, completion: nil)
}
In your DetailViewController, make sure to set the imageViewFrame and the image in e.g. viewDidLoad and you'll be set.
Complete working example:
class ViewController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate {
let imageView: UIImageView = UIImageView()
let scrollView: UIScrollView = UIScrollView()
lazy var pan: UIPanGestureRecognizer = {
return UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
}()
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = // set your image
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 10.0
scrollView.addSubview(imageView)
view.addSubview(scrollView)
scrollView.frame = view.frame
let w = view.bounds.width - 30 // padding of 15 on each side
imageView.frame = CGRect(x: 0, y: 0, width: w, height: w)
imageView.center = scrollView.center
scrollView.addGestureRecognizer(pan)
}
// NOTE: Do NOT set from anywhere else than pan handler.
private var initialTouchPositionY: CGFloat = 0
private var initialTouchPositionX: CGFloat = 0
#objc func panned(_ pan: UIPanGestureRecognizer) {
let y = pan.location(in: scrollView).y
let x = pan.location(in: scrollView).x
switch pan.state {
case .began:
initialTouchPositionY = pan.location(in: imageView).y
initialTouchPositionX = pan.location(in: imageView).x
case .changed:
let offsetY = y - initialTouchPositionY
let offsetX = x - initialTouchPositionX
imageView.frame.origin = CGPoint(x: offsetX, y: offsetY)
case .ended:
presentDetail()
default: break
}
}
// MARK: UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
presentDetail()
}
// MARK: UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// MARK: Private
private func presentDetail() {
let frame = view.convert(imageView.frame, from: scrollView)
let detail = DetailViewController()
detail.imageViewFrame = frame
detail.image = imageView.image
present(detail, animated: false, completion: nil)
}
}
class DetailViewController: UIViewController {
let imageView: UIImageView = UIImageView()
var imageViewFrame: CGRect!
var image: UIImage?
override func viewDidLoad() {
super.viewDidLoad()
imageView.frame = imageViewFrame
imageView.image = image
view.addSubview(imageView)
view.addSubview(backButton)
}
lazy var backButton: UIButton = {
let button: UIButton = UIButton(frame: CGRect(x: 10, y: 30, width: 60, height: 30))
button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
button.setTitle("back", for: .normal)
return button
}()
#objc func back(_ sender: UIButton) {
dismiss(animated: false, completion: nil)
}
}
seems like UIView.animate(withDuration: animations: completion:) should help you; for example, in animations block you can set new image frame, and in completion: - present second view controller (without animation);

Changing UIImageView transform scale broke movement system

I am trying to create an Image view which I can move and scale on screen. the problem is that when I change the scale of the Image, the movement system seams to be broken.
I wrote some code to drag the object from an anchor point which could be different from the center of the UIImage, but the scale ruined the process.
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
Main view controller for the AR experience.
*/
import ARKit
import SceneKit
import UIKit
import ModelIO
class ViewController: UIViewController, ARSessionDelegate , UIGestureRecognizerDelegate{
// MARK: Outlets
#IBOutlet var sceneView: ARSCNView!
#IBOutlet weak var blurView: UIVisualEffectView!
#IBOutlet weak var dropdown: UIPickerView!
#IBOutlet weak var AddStickerButton: UIButton!
#IBOutlet weak var deleteStickerButton: UIImageView!
var offset : CGPoint = CGPoint.zero
var isDeleteVisible : Bool = false
let array:[String] = ["HappyHeart_Lisa", "Logo_bucato", "Sweety_2_Lisa", "Sweety_Lisa", "Tonglue_Lisa"]
lazy var statusViewController: StatusViewController = {
return childViewControllers.lazy.flatMap({ $0 as? StatusViewController }).first!
}()
var stickers = [Sticker]()
// MARK: Properties
var myScene : SCNScene!
/// Convenience accessor for the session owned by ARSCNView.
var session: ARSession {
sceneView.session.configuration
//sceneView.scene.background.contents = UIColor.black
return sceneView.session
}
var nodeForContentType = [VirtualContentType: VirtualFaceNode]() //Tiene sotto controllo la selezione(Tipo maschera)
let contentUpdater = VirtualContentUpdater() //Chiama la VirtualContentUpdater.swift
var selectedVirtualContent: VirtualContentType = .faceGeometry {
didSet {
// Set the selected content based on the content type.
contentUpdater.virtualFaceNode = nodeForContentType[selectedVirtualContent]
}
}
// MARK: - View Controller Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = contentUpdater
sceneView.session.delegate = self
sceneView.automaticallyUpdatesLighting = true
createFaceGeometry()
// Set the initial face content, if any.
contentUpdater.virtualFaceNode = nodeForContentType[selectedVirtualContent]
// Hook up status view controller callback(s).
statusViewController.restartExperienceHandler = { [unowned self] in
self.restartExperience()
}
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(scale))
let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotate))
pinchGesture.delegate = self
rotationGesture.delegate = self
view.addGestureRecognizer(pinchGesture)
view.addGestureRecognizer(rotationGesture)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
/*
AR experiences typically involve moving the device without
touch input for some time, so prevent auto screen dimming.
*/
UIApplication.shared.isIdleTimerDisabled = true
resetTracking()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
session.pause()
}
// MARK: - Setup
/// - Tag: CreateARSCNFaceGeometry
func createFaceGeometry() {
// This relies on the earlier check of `ARFaceTrackingConfiguration.isSupported`.
let device = sceneView.device!
let maskGeometry = ARSCNFaceGeometry(device: device)!
let glassesGeometry = ARSCNFaceGeometry(device: device)!
nodeForContentType = [
.faceGeometry: Mask(geometry: maskGeometry),
.overlayModel: GlassesOverlay(geometry: glassesGeometry),
.blendShapeModel: RobotHead(),
.sfere: RobotHead()
]
}
// MARK: - ARSessionDelegate
func session(_ session: ARSession, didFailWithError error: Error) {
guard error is ARError else { return }
let errorWithInfo = error as NSError
let messages = [
errorWithInfo.localizedDescription,
errorWithInfo.localizedFailureReason,
errorWithInfo.localizedRecoverySuggestion
]
let errorMessage = messages.flatMap({ $0 }).joined(separator: "\n")
DispatchQueue.main.async {
self.displayErrorMessage(title: "The AR session failed.", message: errorMessage)
}
}
func sessionWasInterrupted(_ session: ARSession) {
blurView.isHidden = false
statusViewController.showMessage("""
SESSION INTERRUPTED
The session will be reset after the interruption has ended.
""", autoHide: false)
}
func sessionInterruptionEnded(_ session: ARSession) {
blurView.isHidden = true
DispatchQueue.main.async {
self.resetTracking()
}
}
/// - Tag: ARFaceTrackingSetup
func resetTracking() {
statusViewController.showMessage("STARTING A NEW SESSION")
guard ARFaceTrackingConfiguration.isSupported else { return }
let configuration = ARFaceTrackingConfiguration()
configuration.isLightEstimationEnabled = true
session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
}
// MARK: - Interface Actions
/// - Tag: restartExperience
func restartExperience() {
// Disable Restart button for a while in order to give the session enough time to restart.
statusViewController.isRestartExperienceButtonEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.statusViewController.isRestartExperienceButtonEnabled = true
}
resetTracking()
}
// MARK: - Error handling
func displayErrorMessage(title: String, message: String) {
// Blur the background.
blurView.isHidden = false
// Present an alert informing about the error that has occurred.
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let restartAction = UIAlertAction(title: "Restart Session", style: .default) { _ in
alertController.dismiss(animated: true, completion: nil)
self.blurView.isHidden = true
self.resetTracking()
}
alertController.addAction(restartAction)
present(alertController, animated: true, completion: nil)
}
//Create a new Sticker
func createNewSticker(){
stickers.append(Sticker(view : self.view, viewCtrl : self))
}
#IBAction func addNewSticker(_ sender: Any) {
createNewSticker()
}
//Function To Move the Stickers, all the Touch Events Listener
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in (touches as! Set<UITouch>) {
var location = touch.location(in: self.view)
for sticker in stickers {
if(sticker.imageView.frame.contains(location) && !isSomeOneMoving()){
//sticker.imageView.center = location
offset = touch.location(in: sticker.imageView)
let offsetPercentage = CGPoint(x: offset.x / sticker.imageView.bounds.width, y: offset.y / sticker.imageView.bounds.height)
let offsetScaled = CGPoint(x: sticker.imageView.frame.width * offsetPercentage.x, y: sticker.imageView.frame.height * offsetPercentage.y)
offset.x = (sticker.imageView.frame.width / 2) - offsetScaled.x
offset.y = (sticker.imageView.frame.height / 2) - offsetScaled.y
location = touch.location(in: self.view)
location.x = (location.x + offset.x)
location.y = (location.y + offset.y)
sticker.imageView.center = location
disableAllStickersMovements()
isDeleteVisible = true
sticker.isStickerMoving = true;
deleteStickerButton.isHidden = false
}
}
}
}
func disableAllStickersMovements(){
for sticker in stickers {
sticker.isStickerMoving = false;
}
}
func isSomeOneMoving() -> Bool{
for sticker in stickers {
if(sticker.isStickerMoving){
return true
}
}
return false
}
var lastLocationTouched : CGPoint = CGPoint.zero
var lastStickerTouched : Sticker = Sticker()
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in (touches as! Set<UITouch>) {
var location = touch.location(in: self.view)
for sticker in stickers {
if(sticker.imageView.frame.contains(location) && sticker.isStickerMoving){
lastLocationTouched = location
location = touch.location(in: self.view)
location.x = (location.x + offset.x)
location.y = (location.y + offset.y)
sticker.imageView.center = location
//sticker.imageView.center = location
}
if(deleteStickerButton.frame.contains(lastLocationTouched) && isDeleteVisible && sticker.isStickerMoving){
sticker.imageView.alpha = CGFloat(0.5)
}else{
sticker.imageView.alpha = CGFloat(1)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for sticker in stickers {
if(deleteStickerButton.frame.contains(lastLocationTouched) && isDeleteVisible && sticker.isStickerMoving){
removeASticker(sticker : sticker)
disableAllStickersMovements()
}
}
disableAllStickersMovements()
isDeleteVisible = false
deleteStickerButton.isHidden = true
}
func removeASticker(sticker : Sticker ){
sticker.imageView.removeFromSuperview()
let stickerPosition = stickers.index(of: sticker)!
stickers.remove(at: stickerPosition)
for sticker in stickers {
sticker.isStickerMoving = false;
}
}
var identity = CGAffineTransform.identity
#objc func scale(_ gesture: UIPinchGestureRecognizer) {
for sticker in stickers {
if(sticker.isStickerMoving){
switch gesture.state {
case .began:
identity = sticker.imageView.transform
case .changed,.ended:
sticker.imageView.transform = identity.scaledBy(x: gesture.scale, y: gesture.scale)
case .cancelled:
break
default:
break
}
}
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
#objc func rotate(_ gesture: UIRotationGestureRecognizer) {
for sticker in stickers {
if(sticker.isStickerMoving){
sticker.imageView.transform = sticker.imageView.transform.rotated(by: gesture.rotation)
}
}
}
}
and then the sticker class
import UIKit
import Foundation
class Sticker : NSObject, UIGestureRecognizerDelegate{
var location = CGPoint(x: 0 , y: 0);
var sticker_isMoving = false;
let imageView = UIImageView()
var isStickerMoving : Bool = false;
init(view : UIView, viewCtrl : ViewController ) {
super.init()
imageView.image = UIImage(named: "BroccolFace_Lisa.png")
imageView.isUserInteractionEnabled = true
imageView.contentMode = UIViewContentMode.scaleAspectFit
imageView.frame = CGRect(x: view.center.x, y: view.center.y, width: 200, height: 200)
view.addSubview(imageView)
}
override init(){
}
}
This is because the imageView.bounds and the touch.location(in: imageView) are in unscaled values. This will overcome the problem:
offset = touch.location(in: imageView)
let offsetPercentage = CGPoint(x: offset.x / imageView.bounds.width, y: offset.y / imageView.bounds.height)
let offsetScaled = CGPoint(x: imageView.frame.width * offsetPercentage.x, y: imageView.frame.height * offsetPercentage.y)
offset.x = (imageView.frame.width / 2) - offsetScaled.x
offset.y = (imageView.frame.height / 2) - offsetScaled.y
Basically it converts the offset into a percentage based on the unscaled values and then converts that into scaled values based on the imageView frame (which is modified by the scale). It then uses that to calculate the offset.
EDIT (NUMBER TWO)
This is more complete way to do it and it should solve any issues that may arise due to scaling or rotation.
Add this structure to hold the details of the dragging for images:
struct DragInfo {
let imageView: UIImageView
let startPoint: CGPoint
}
Add these instance variables (you can also remove offset if you want):
var dragStartPoint: CGPoint = CGPoint.zero
var currentDragItems: [DragInfo] = []
var dragTouch: UITouch?
Change touchesBegan to this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard self.dragTouch == nil, let touch = touches.first else { return }
self.dragTouch = touch
let location = touch.location(in: self.view)
self.dragStartPoint = location
for imageView in self.imageList {
if imageView.frame.contains(location) {
self.currentDragItems.append(DragInfo(imageView: imageView, startPoint: imageView.center))
}
}
}
Change touchesMoved to this:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let dragTouch = self.dragTouch else { return }
for touch in touches {
if touch == dragTouch {
let location = touch.location(in: self.view)
let offset = CGPoint(x: location.x - self.dragStartPoint.x, y: location.y - self.dragStartPoint.y)
for dragInfo in self.currentDragItems {
let imageOffSet = CGPoint(x: dragInfo.startPoint.x + offset.x, y: dragInfo.startPoint.y + offset.y)
dragInfo.imageView.center = imageOffSet
}
}
}
}
Change touchesEnded to this:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let dragTouch = self.dragTouch, touches.contains(dragTouch) else { return }
self.currentDragItems.removeAll()
self.dragTouch = nil
}
Set the following properties on the gesture recognisers used:
scaleGesture.delaysTouchesEnded = false
scaleGesture.cancelsTouchesInView = false
rotationGesture.delaysTouchesEnded = false
rotationGesture.cancelsTouchesInView = false
Some explanation about how it works.
With all the touch events it only considers the first touch because dragging from multiple touches doesn't make much sense (what if two touches were over the same image view and move differently). It records this touch and then only considers that touch for dragging things around.
When touchesBegan is called it checks no touch for dragging exists (indicating a drag in progress) and it finds all image views that are under the touch and for each one it records the details of itself and it start centre position in a DragInfo structure and stores it in the currentDragItems array. It also records the position the touch started in the main view and the touch that initiated it.
When touchesMoved is called it only considers the touch that started the dragging and it calculates the offset from the original position the touch started in the main view and then goes down the list of images involved in the dragging and calculates their new centre based on their original starting position and the offset calculated and sets that as the new centre.
When touchesEnded is called assuming it is the dragging touch that is ended it clears the array of DragInfo objects to ready for the next drag.
You need to set the delaysTouchesEnded and cancelsTouchesInView properties on all gesture recognisers so that all touches are passed through to the view otherwise the touchesEnded methods in particular are not called.
Doing the calculations like this removes the problems of scale and rotation as you are just concerned with offsets from initial positions. It also works if multiple image views are dragged at the same time as their details are kept separately.
Now there are some things to be aware of:
You will need to put in all the other code you app required as this is just a basic example to show the idea.
This assumes that you only want to drag image views that you pick up at the start. If you want to collect image views as you drag around you would need to develop a much more complicated system.
As I stated only one drag operation can be in progress at a time and it takes the first touch registered as this source touch. This source touch is then used to filter out any other touches that may happen. This is done to keep things simple and otherwise you would have to account for all kinds of strange situations like if multiple touches were on the same image view.
I hope this all makes sense and you can adapt it to solve your problem.
Here is an extension that I use to pan, pinch and rotate an image with UIPanGestureRecognizer, UIPinchGestureRecognizer and UIRotationGestureRecognizer
extension ViewController : UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func panGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .ended: fallthrough
case .changed:
let translation = gesture.translation(in: gesture.view)
if let view = gesture.view {
var finalPoint = CGPoint(x:view.center.x + translation.x, y:view.center.y + translation.y)
finalPoint.x = min(max(finalPoint.x, 0), self.myImageView.bounds.size.width)
finalPoint.y = min(max(finalPoint.y, 0), self.myImageView.bounds.size.height)
view.center = finalPoint
gesture.setTranslation(CGPoint.zero, in: gesture.view)
}
default : break
}
}
func pinchGesture(gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .changed:
let scale = gesture.scale
gesture.view?.transform = gesture.view!.transform.scaledBy(x: scale, y: scale)
gesture.scale = 1
default : break
}
}
func rotateGesture(gesture: UIRotationGestureRecognizer) {
switch gesture.state {
case .changed:
let rotation = gesture.rotation
gesture.view?.transform = gesture.view!.transform.rotated(by: rotation)
gesture.rotation = 0
default : break
}
}
}
setting the UIGestureRecognizerDelegate will help you do the three of gestures at the same time.

When I Scroll the tableView, how can I know which cell is touch the Green Area

The Screen In My Prototype
My question is based onThe image in the link . Because my reputation is not enough, I can't post any image here
We assume that Green Area in the image is fixed.
And, my requirement is that When a cell contains the GA, that cell'saudioPlayer will speak the word in the cell, like AirPod
OR, you can regard my requirement as When a cell contains the GA, the text of that cell's label changes to "Touch the Green"
My question is that when I Scroll the tableView, how can I get which one(Cell) is containing the GA?
But I can’t find a way to get that(some position/index information about That Cell)
could anyone help me ? ObjectiveC solution is OK, Swift solution is better for me, Thank you so much
In this code, I am using GreenArea as in Center of UIView. Some modification from Ruslan's Answer.
#IBOutlet weak var greenAreaVw: UIView!
var contHeight : CGFloat = 0.0
var eachRowHeight : CGFloat = 45
var topSpaceTableView : CGFloat = 62
var GreenAreaOriginY : CGFloat = 0.0
// Give UITableView Edge Insets in ViewDidLoad
contHeight = ((self.view.frame.size.height / 2) - eachRowHeight / 2 - topSpaceTableView)
userTblVw.contentInset = UIEdgeInsets(top: contHeight, left: 0, bottom: contHeight, right: 0)
userTblVw.contentOffset.y = -contHeight
GreenAreaOriginY = greenAreaVw.frame.origin.y
/*------------------- -----------------------*/
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
checkCells()
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
checkCells()
}
func checkCells() {
userTblVw.visibleCells.forEach { cell in
if let indexPath = userTblVw.indexPathForCell(cell) {
let rect = userTblVw.rectForRowAtIndexPath(indexPath)
let convertedRect = self.userTblVw.convertRect(rect, toView: self.view)
if convertedRect.origin.y >= GreenAreaOriginY && convertedRect.origin.y < (GreenAreaOriginY + eachRowHeight)
{
let contFloat : CGFloat = (eachRowHeight * CGFloat(indexPath.row)) - contHeight
userTblVw.setContentOffset(CGPoint(x: 0, y: contFloat), animated: true)
}
}
}
}
Find below Screenshots:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// We check cells here to set the state of whether it contains the green or not before the scrolling
checkCells()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// And we are continuously checking cells while scrolling
checkCells()
}
func checkCells() {
tableView.visibleCells.forEach { cell in
if let indexPath = tableView.indexPath(for: cell) {
let rect = tableView.rectForRow(at: indexPath)
// This is the rect in your VC's coordinate system (and not the table view's one)
let convertedRect = self.view.convert(rect, from: tableView)
if convertedRect.contains(greenArea.frame) {
cell.textLabel?.text = "Touch the Green"
} else {
cell.textLabel?.text = "Does not touch the Green"
}
}
}
}
How about something like:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
[0, 1, 2].forEach {
let rect = tableView.rectForRow(at: IndexPath(row: $0, section: 0))
if rect.contain(GAView.frame) {
// play sound here
}
}
}

Zoom UIImageView in UIScrollView – not centered

I have an UIScrollView with an UIImageView in it. Just a simple test case: The UIImageView is full width of the scroll view and centered to Y with auto layout. However, in the final result on my device it is not really centered by Y.
But i have the problem that when zooming the UIImageView with the scroll view it "drifts" to the bottom area – not just scaled in the center as it should be. It also "drifts" to the left when zoom out (at the very end of the video) and bounces back to the center.
I've made a small preview video of this behavior: https://www.youtube.com/watch?v=ivRNkzNrcEA
Here is my simple test code:
class TestController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var SCROLL: UIScrollView!
#IBOutlet weak var IMAGE: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
SCROLL.minimumZoomScale = 1;
SCROLL.maximumZoomScale = 6.0;
SCROLL.zoomScale = 1.0;
SCROLL.contentSize = IMAGE.frame.size;
SCROLL.delegate = self;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return IMAGE
}
func scrollViewDidZoom(scrollView: UIScrollView) {
}
func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat) {
}
}
And here is my test auto layout:
I am not sure if it is possible to keep centering with just auto-layout. One approach that works for me is to to sub-class UIScrollView and modify layoutSubviews as illustrated in Apple WWDC example. Code below
-(void) layoutSubviews
{
[super layoutSubviews];
UIImageView *v = (UIImageView *)[self.delegate viewForZoomingInScrollView:self];
//Centering code
CGFloat svw = self.bounds.size.width;
CGFloat svh = self.bounds.size.height;
CGFloat vw = v.frame.size.width;
CGFloat vh = v.frame.size.height;
CGRect f = v.frame;
if (svw > vw)
{
f.origin.x = (svw - vw)/2.0;
}
else
{
f.origin.x = 0;
}
if (svh > vh)
{
f.origin.y = (svh - vh)/2.0;
}
else
{
f.origin.y = 0;
}
v.frame = f;
}

How to programmatically scroll through a collection view?

I have a collection view in my view. In the cells are image views and it has one section.
I now want to scroll programmatically through the images. I want the animation to happen endless so it starts with item one after reaching item 10.
It also would be helpful if you provide some method where to put animations like the cells getting bigger after getting programmatically swiped in from right.
There are two methods that could help you achieve this:
func scrollToItemAtIndexPath(indexPath: NSIndexPath,
atScrollPosition scrollPosition: UICollectionViewScrollPosition,
animated animated: Bool)
or
func setContentOffset(contentOffset: CGPoint,
animated animated: Bool)
Both are on UICollectionView so you can use whatever seems more convenient. To create a custom animation for this however is more difficult. A quick solution depending on what you need could be this answer.
Swift 4, iOS 11:
// UICollectionView method
func scrollToItem(at indexPath: IndexPath,
at scrollPosition: UICollectionViewScrollPosition,
animated: Bool)
// UIScrollView method
func setContentOffset(_ contentOffset: CGPoint, animated: Bool)
Swift 5
Based on Mr.Bean's answer, here is an elegant way using UICollectionView extension:
extension UICollectionView {
func scrollToNextItem() {
let contentOffset = CGFloat(floor(self.contentOffset.x + self.bounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func scrollToPreviousItem() {
let contentOffset = CGFloat(floor(self.contentOffset.x - self.bounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func moveToFrame(contentOffset : CGFloat) {
self.setContentOffset(CGPoint(x: contentOffset, y: self.contentOffset.y), animated: true)
}
}
Now you can use it wherever you want:
collectionView.scrollToNextItem()
collectionView.scrollToPreviousItem()
By far this is the best approach i have encountered, the trick is to scroll to the frame which is containing next objects. Please refer the code
/* -------------- display previous friends action ----------------*/
#IBAction func actionPreviousFriends(_ sender: Any) {
let collectionBounds = self.collectionView.bounds
let contentOffset = CGFloat(floor(self.collectionView.contentOffset.x - collectionBounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
/* -------------- display next friends action ----------------*/
#IBAction func actionNextFriends(_ sender: Any) {
let collectionBounds = self.collectionView.bounds
let contentOffset = CGFloat(floor(self.collectionView.contentOffset.x + collectionBounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func moveToFrame(contentOffset : CGFloat) {
let frame: CGRect = CGRect(x : contentOffset ,y : self.collectionView.contentOffset.y ,width : self.collectionView.frame.width,height : self.collectionView.frame.height)
self.collectionView.scrollRectToVisible(frame, animated: true)
}
You can use this UICollectionView extension (Swift 4.2)
extension UICollectionView {
func scrollToNextItem() {
let scrollOffset = CGFloat(floor(self.contentOffset.x + self.bounds.size.width))
self.scrollToFrame(scrollOffset: scrollOffset)
}
func scrollToPreviousItem() {
let scrollOffset = CGFloat(floor(self.contentOffset.x - self.bounds.size.width))
self.scrollToFrame(scrollOffset: scrollOffset)
}
func scrollToFrame(scrollOffset : CGFloat) {
guard scrollOffset <= self.contentSize.width - self.bounds.size.width else { return }
guard scrollOffset >= 0 else { return }
self.setContentOffset(CGPoint(x: scrollOffset, y: self.contentOffset.y), animated: true)
}
}
And the usage will be like
yourCollectionView.scrollToNextItem()
yourCollectionView.scrollToPreviousItem()

Resources