Best way to implement view like map - ios

I'm making view that represent something like map, with pan and pinch gestures.
At current moment it's implemented via overriden drawRect that called in handlePan with setNeedsDisplay.
So, the resulting performance a little bit awkward. Is there any more appropriate approaches?

So, more appropriate solution here is UIScrollView.
To get it work:
Set up scrollView delegate, like
scrollView.delegate = self
Implement
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView?
{
return fieldView
}
Optionally, you can set
scrollView.minimumZoomScale = scrollView.frame.width /
fieldView.frame.width
scrollView.maximumZoomScale = 2
Where fieldView is target custom view:
fieldView = FieldView(frame: frameRect)
scrollView.addSubview(fieldView)
scrollView.contentSize = frameRect.size

Related

Zoom and Scroll support UICollectionView

I have a requirement to support zoom in UICollectionView.
Requirements:
After zoom in, it has to support to view the UICollectionViewCell’s hidden area ( area out of viewport) by horizontal and vertical scroll.
After Zoom out/in, it has to support the selection of UICollectionViewCell and able to scroll the UICollectionView ( Basically the default UICollectionView behavior on going back to no zoom state. ).
The list of approaches tried:
Added GestureRecognizer
a. Added UIPinchGestureRecognizer to transform the UICollectionView by scale.
b. After Zoom in, it was not possible to move the UICollectionViewcell to view the hidden area.
c. Added UIPanGestureRecognizer to move the center of UICollectionView
d. It was working fine to move the UICollectionView.
e. Now we can’t able to select the UICollectionViewCell and can’t able to scroll UICollectionView.
Added UICollectionView inside UIScrollView
a. Added UIScrollView with delegates.
b. Added UICollectionView as sub view of UIScrollView
c. Zoom out is not happening because UICollectionView (inherited by UIScrollView) consumes the zoom gesture
Added UIColectionView and UIScrollView both as siblings
a. Added UIScrollView and UICollectionView to parent.
b. Bring UIScrollView to front.
c. Zoom is working but not able to pan to see the hidden area.
Please suggest if there any way to fix above approaches or a better strategy to achieve zoom in a collectionView.
I have solved this using a UIScrollView and a UICollectionViewLayout subclass.
1) place a UIScrollView on top of the UICollectionView with the same frame.
self.view.addSubview(scrollView)
scrollView.addSubview(dummyViewForZooming)
scrollView.frame = collectionView.frame
scrollView.bouncesZoom = false
scrollView.minimumZoomScale = 0.5
scrollView.maximumZoomScale = 3.0
2) Set the contentSize of the UIScrollView and zoomingView to be the same as the UICollectionView
override func viewDidLayoutSubviews() {
super.viewWillLayoutSubviews()
scrollView.contentSize = layout.collectionViewContentSize
dummyViewForZooming.frame = CGRect(origin: .zero, size: layout.collectionViewContentSize)
scrollView.frame = collectionView.frame
}
3) Remove all gesture recognizers from the UICollectionView and add a delegate for the UIScrollView. Add a tap gesture recognizer to the UIScrollview
collectionView.gestureRecognizers?.forEach {
collectionView.removeGestureRecognizer($0)
}
let tap = UITapGestureRecognizer.init(target: self, action: #selector(scrollViewWasTapped(sender:)))
tap.numberOfTapsRequired = 1
scrollView.addGestureRecognizer(tap)
scrollView.delegate = self
4) When the ScrollView scrolls or zooms, set the contentOffset of the UICollectionView to be the same as the ScrollView contentOffset, set the layoutScale of your UICollectionViewLayout as the zoomscale and invalidate the layout.
func scrollViewDidZoom(_ scrollView: UIScrollView) {
if let layout = self.layout, layout.getScale() != scrollView.zoomScale {
layout.layoutScale = scrollView.zoomScale
self.layout.invalidateLayout()
collectionView.contentOffset = scrollView.contentOffset
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return dummyViewForZooming
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.contentOffset = scrollView.contentOffset
}
5) override the prepare method in the UICollectionViewLayout, scan through all your layoutAttributes and set a transform:
attribute.transformedFrame = attribute.originalFrame.scale(layoutScale)
let ts = CGAffineTransform(scaleX: layoutScale, y: layoutScale)
attribute.transform = ts
let xDifference = attribute.frame.origin.x - attribute.transformedFrame.origin.x
let yDifference = attribute.frame.origin.y - attribute.transformedFrame.origin.y
let t1 = CGAffineTransform(translationX: -xDifference, y: -yDifference)
let t = ts.concatenating(t1)
attribute.transform = t
6) ensure you scale the collectionView content size:
override var collectionViewContentSize: CGSize {
return CGSize(width: width * layoutScale, height: height * layoutScale)
}
7) Intercept taps from the tap gesture recognizer and convert the location in view to a point in the collection view, you can then get the indexPath of that cell using indexPathForItem(point:) and select the cell or pass on events to the underlying views of the cell etc..
hope this helps

Connect Two ScrollView’s Together with different Content Offset Values

To Scroll 2 different scrollView together, Many questions have been already answered regarding using the scrollViewDidScroll Method and passing the content offset of one scrollview to other.
But My Question here is a bit different, Let’s say I have 2 ScrollView A and B both with horizontal scrolling only.
When the view loads ScrollView A has contentOffSet say (x,y) and B scrollview’s content offset : (m,n).
As per my understanding content Offset is the new (x,y) value while scrolling.
Now I can’t pass the content Offset value of A to B here to scroll them together as they loads at different points due to content requirement.I need the exact x points displaced while scrolling in A, then may be pass it to B.
I have also tried getting the velocity from pangesture of A and passing it to B, which doesn’t work smoothly.
How can I achieve a smooth scrolling for both views ?
It's not entirely clear what you're trying to do. But if you want to be updated about when scroll view A changes it's contentOffset you can subclass UIScrollView and pass the data through a delegate or a closure.
class ScrollView: UIScrollView {
var contentOffsetChanged: ((CGPoint)->())?
override var contentOffset: CGPoint {
didSet {
if let contentOffsetChanged = contentOffsetChanged {
contentOffsetChanged(contentOffset)
}
}
}
}
Update
After reading the comment you left me and the one you wrote to iOS Geek, it seems contradictory. There are a few possibilities for how your math would work out so don't take my exact solution as the answer, but more of the design. I think this is the design you're interested in.
class Controller: UIViewController, UIScrollViewDelegate {
let scrollView = UIScrollView()
var scrollViewB: UIScrollView?
var initialOffset: CGPoint = .zero
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
}
func loadScrollViewB() {
initialOffset = scrollView.contentOffset
scrollViewB = UIScrollView()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.scrollView {
var contentOffset: CGPoint = .zero
contentOffset.x = initialOffset.x - scrollView.contentOffset.x
scrollViewB?.contentOffset = contentOffset
}
}
}

Smooth custom paging for UIScrollView

I have two (possibly more) views in a UIScrollView and want to use paging with it. The problem arises when I try to use the default Paging option for UIScrollView, since the views have different widths it can not page properly.
So I have implemented a custom paging code which works. However, when the scrolls are slow, it does not function as expected. (It goes back to the original position without animation.)
Here is how I currently do the custom paging through the UIScrollViewDelegate
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if direction == 1{
targetContentOffset.pointee.x = 0
}else{
targetContentOffset.pointee.x = 100
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView.superview).x > 0 {
direction = 1
}
else {
direction = 0
}
}
What I want:
What I have:
try to below example for Custom UIScrollView Class
import UIKit
public class BaseScrollViewController: UIViewController, UIScrollViewDelegate {
public var leftVc: UIViewController!
public var middleVc: UIViewController!
public var rightVc: UIViewController!
public var initialContentOffset = CGPoint() // scrollView initial offset
public var maximumWidthFirstView : CGFloat = 0
public var scrollView: UIScrollView!
public class func containerViewWith(_ leftVC: UIViewController,
middleVC: UIViewController,
rightVC: UIViewController) -> BaseScrollViewViewController {
let container = BaseScrollViewViewController()
container.leftVc = leftVC
container.middleVc = middleVC
container.rightVc = rightVC
return container
}
override public func viewDidLoad() {
super.viewDidLoad()
setupHorizontalScrollView()
}
func setupHorizontalScrollView() {
scrollView = UIScrollView()
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.bounces = false
let view = (
x: self.view.bounds.origin.x,
y: self.view.bounds.origin.y,
width: self.view.bounds.width,
height: self.view.bounds.height
)
scrollView.frame = CGRect(x: view.x,
y: view.y,
width: view.width,
height: view.height
)
self.view.addSubview(scrollView)
let scrollWidth = 3 * view.width
let scrollHeight = view.height
scrollView.contentSize = CGSize(width: scrollWidth, height: scrollHeight)
leftVc.view.frame = CGRect(x: 0,
y: 0,
width: view.width,
height: view.height
)
middleVc.view.frame = CGRect(x: view.width,
y: 0,
width: view.width,
height: view.height
)
rightVc.view.frame = CGRect(x: 2 * view.width,
y: 0,
width: view.width,
height: view.height
)
addChildViewController(leftVc)
addChildViewController(middleVc)
addChildViewController(rightVc)
scrollView.addSubview(leftVc.view)
scrollView.addSubview(middleVc.view)
scrollView.addSubview(rightVc.view)
leftVc.didMove(toParentViewController: self)
middleVc.didMove(toParentViewController: self)
rightVc.didMove(toParentViewController: self)
scrollView.contentOffset.x = middleVc.view.frame.origin.x
scrollView.delegate = self
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.initialContentOffset = scrollView.contentOffset
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if maximumWidthFirstView != 0
{
if scrollView.contentOffset.x < maximumWidthFirstView
{
scrollView.isScrollEnabled = false
let newOffset = CGPoint(x: maximumWidthFirstView, y: self.initialContentOffset.y)
self.scrollView!.setContentOffset(newOffset, animated: false)
scrollView.isScrollEnabled = true
}
}
}
}
Use of BaseScrollViewController
let left = FirstController.init()
let middle = MiddleController()
let right = RightController.init()
let container = BaseScrollViewController.containerViewWith(left,middleVC: middle,rightVC: right)
container.maximumWidthFirstView = 150
Output:
GitHub gist Example code: https://gist.github.com/mspvirajpatel/58dac2fae0d3b4077a0cb6122def6570
I have previously written a short memo about this problem, and I'll copy/paste it since it is no longer accessible from anywhere. This may not be a specific answer and the codes are pretty old, but I hope this would help you in some degree.
If you have used a paging feature included in UIScrollView, you might also have tempted to customize the width of each page instead of a default, boring, frame width paging. It would be great if you can make the scroll stop at shorter or longer intervals than just multiples of its frame width. Surprisingly, there's no built-in way to configure the width of pages even in our latest iOS7 SDK. There are some ways to achieve custom paging, but none of them I would say are complete. As for now, you'll have to choose either of the following solutions.
1. Change the frame size of your UIScrollView
Alexander Repty has introduced a nice and easy solution to this problem and also included a sample code through his blog: http://blog.proculo.de/archives/180-Paging-enabled-UIScrollView-With-Previews.html
Basically, the instruction can be watered down to the following steps:
Create UIView subclass and override hitTest: withEvent:.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
if ([self.subviews count] == 0) return nil;
else return [self.subviews lastObject];
}
return nil;
}
Include UIScrollView as a subview of the above UIView subclass.
Adjust the frame size of your UIScrollView.
Set clipsToBound property of your scroll view to NO.
Set pagingEnabled property of your scroll view to YES.
As you can see, I've just assumed that there is only one subview (the scrollView!) to your UIView subclass. Since you are passing all the touch events occurred in the UIView subclass to your UIScrollView, you'll be able to scroll the content by panning on the UIView subclass, but the paging width will be decided by the width of UIScrollView's frame.
The best part of this approach is that you'll get the genuine feeling and responsiveness, as it is somewhat hard to mimic the paging by using UIScrollView delegate methods.
The only problem I found using this solution is that the width of all pages will have to be identical. You can't set different widths to different pages. If you tries to change your scrollView's frame size dynamically, you'll find there're a number of new emerging problems to deal with. Before trying to fix these glitches, you may want to check out other two solutions using UIScrollView delegates.
2. scrollViewWillEndDragging: withVelocity: targetContentOffset
scrollViewWillEndDragging: withVelocity: targetContentOffset is one of the latest UIScrollView delegate methods(iOS 5.0 or up) that gives you more information than the other old ones.
Since you get the velocity of the scrollView right after you lift the finger up from the screen, we can figure out the direction of the scrolled contents. The last argument, targetContentOffset, not only gives you the expected offset when the scrolling stops eventually, you can also assign CGPoint value in order to let the scrollView scrolls to the desired point.
targetContentOffset = CGPointMake(500, 0);
or
targetContentOffset->x = 500;
However, this will not work as you would think it should because you cannot set the speed of scrolling animation. It feels more like the scrollView happens to stop at the right point rather than it snaps to the spot. I also have to warn you that manually scrolling the contents with setContentOffset: animated: or just by using UIView animation inside the method will not work as expected.
If the velocity is 0, however, you may(and you have to) use manual scrolling to make it snap to the nearest paging point.
It could be the simplest and the most clean approach among all, but the major downside is that it does not provide the same experience that you always had with the real paging feature. To be more honest, it's not even similar to what we call paging. For the better result, we need to combine more delegate methods.
3. Use multiple UIScrollView delegate methods
From my shallow experience, an attempt to scroll your scrollView manually inside any UIScrollView delegate methods will only work when your scrollView has started to decelerate, or when it's not scrolling at all. Therefore, the best place I've found to perform the manual scrolling is scrollViewWillBeginDecelerating:.
Before looking inside the sample code, remember scrollViewEndDragging: withVelocity: targetContentOffset: method will always called prior to scrollViewWillBeginDecelerating:.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
_scrollVelocity = velocity.x;
if (_scrollVelocity == 0) {
// Find the nearest paging point and scroll.
}
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
if (_scrollVelocity < 0) {
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
scrollView.contentOffset = // Previous page offset
} completion:^(BOOL finished){}];
} else if (_scrollVelocity > 0) {
// Animate to the next page offset
}
}
_scrollVelocity is meant to be a global variable or a property, and I've assumed that you have your own ways to decide paging offsets for each page. Note that you'll have to handle the case of zero velocity inside the upper method because the latter method will not be called.
UIView animation with the duration 0.3 and the EaseOut curve option gave me the best result, but of course you should try other combinations to find what's the best for you.
This not the exact solution you might be looking for.
1) Check the offset of the scrollView when it reaches 0, You could show the VIEW you have above , You could animate while checking the scrollview movement so that it looks nice .But not completely
2) Now the VIEW is partially above your camera(you can decrease it alpha so that scrollview is still visible).
3) user can tap the view and you can show it completely.
You may want to consider calculating the most visible cell in your collection view after dragging ends and then programmatically scroll to – and center – that cell.
So something like:
First, implement the scrollViewDidEndDragging(_:willDecelerate:) method of your collection view's delegate. Then, in that method's body, determine which cell in collectionView.visibleCells is most visible by comparing each of their centers against your collection view's center. Once you find your collection view's most visible cell, scroll to it by calling scrollToItem(at:at:animated:).

Animating a zoomScale on UIScrollView with Swift

Relatively new to iOS apps, and swift, so I'm a little lost on how to proceed.
Basically, I have a UIImageView, and i'm able to pinch to zoom/scroll, as my UIImageView is in a UIScrollView, but I'd like to add a double tap on the UIImage to zoom in as well.
I'm able to zoom in (just set the zoomScale to 3.0 for now), but I'd like to have a smooth transition. I did see the zoom(to:, animated:) method, but I wasn't quite sure on how to zoom into the center of the view.
Anyways, here's what I have thus far.
I've created the tapGestureRecognizer, and assigned it to the UIImageView
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapImageViewGesture))
self.Image1.isUserInteractionEnabled = true
self.Image1.addGestureRecognizer(tapGesture)
and below is the function to perform the zoom
func tapImageViewGesture(sender: UITapGestureRecognizer) {
print("Touched")
self.pinchZoomScroll1.zoomScale = 3.0
}
Is there a simple way I can have some sort of a smooth animation of zooming in?, or should I go with using the zoom function instead, as it already supports animation?
Thank you!!
Either you can set set animation true by using this :
self.pinchZoomScroll1.setZoomScale(3.0, animated: true)
or you can also use scrollView delegate func by setting scrollViewminimumScale and max scale:
pinchZoomScroll1.minimumZoomScale = 1.0
pinchZoomScroll1.maximumZoomScale = 6.0
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return Image1
}

Trying to zoom a multilayer set of UIViews

Please point me in the right direction - I can find neither a solution nor a tutorial that solves my problem..
I have the following hierarchy of views in a ViewController:
UIView
ScrollView
Container
Drawing
Image
I can zoom in and out using a tap gesture, however, I don't want the pens in the drawing view to resize. I need to be able to draw lines on the image at a greater detail.
If I zoom the views separately, then the pens are out of sync with the image.
The views are setup in IB.
The code in ViewDidLoad to initialize things is as follows:
scrollView.backgroundColor = UIColor.black
scrollView.contentSize = imageView.frame.size
scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let tap = UITapGestureRecognizer(target: self, action: #selector(doubleTapped))
tap.numberOfTapsRequired = 2
imageView.addGestureRecognizer(tap)
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 4.0
scrollView.delegate = self
And the zoom method is:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
I've tried starting the following alternatives:
scrollView.contentSize = drawingView.frame.size and
..
return drawingView
I've also set imageView as a subView of drawingView.
And finally, I've set both as subViews of the Container View (a UIView, not a UIContainerView.
I am conforming to the UIScrollViewDelegate.
Any help would be appreciated.

Resources