Swift - Add UIImageView as subview of UIWebView scrollView and scaling - ios

I have a UIWebView and I have successfully added a UIImage view to the UIWebView’s scrollView like so:
let localUrl = String(format:"%#/%#", PDFFilePath, fileNameGroup)
let url = NSURL.fileURLWithPath(localUrl)
panRecognizer = UITapGestureRecognizer(target: self, action: #selector(panDetected))
pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchDetected))
panRecognizer.delegate = self
pinchRecognizer.delegate = self
webview = UIWebView()
webview.frame = self.view.bounds
webview.scrollView.frame = webview.frame
webview.userInteractionEnabled = true
webview.scalesPageToFit = true
webview.becomeFirstResponder()
webview.delegate = self
webview.scrollView.delegate = self
self.view.addSubview(webview)
webview.loadRequest(NSURLRequest(URL:url))
webview.gestureRecognizers = [pinchRecognizer, panRecognizer]
let stampView:StampAnnotation = StampAnnotation(imageIcon: UIImage(named: "approved.png"), location: CGPointMake(currentPoint.x, currentPoint.y))
self.webview.scrollView.addSubview(stampView)
My UIWebView scrollView is scalable. Now I am looking for away to have my UIImageView (StampAnnotation is a class and UIImageView is its subclass) scale when the scrollView scales. So if the user zooms in on the scrollView, the UIImageView will get bigger and stay in a fixed position and if the user zooms out, the UIImageView will get smaller while the scrollView gets smaller while staying in a fixed position.
I really hope that makes sense. I have tried the following:
func pinchDetected(recognizer:UIPinchGestureRecognizer)
{
for views in webview.scrollView.subviews
{
if(views.isKindOfClass(UIImageView))
{
views.transform = CGAffineTransformScale(views.transform, recognizer.scale, recognizer.scale)
recognizer.scale = 1
}
}
if(appDelegate.annotationSelected == 0)
{
webview.scalesPageToFit = true
}
else
{
webview.scalesPageToFit = false
}
}
but this does nothing, if I remove this line:
recognizer.scale = 1
it scales way too big too fast. My question is, how do I get my UIImageView to scale when the UIWebview’s scrollView scrolls?
Any help would be appreciated.
This solved my problem.
func scrollViewDidZoom(scrollView: UIScrollView) {
for views in webview.scrollView.subviews
{
if(views.isKindOfClass(UIImageView))
{
views.transform = CGAffineTransformMakeScale(scrollView.zoomScale, scrollView.zoomScale)
}
}
}
No it does not stay in a fixed position on the page, but I think that is a constraints issue?

You were close...
1) Add a property to hold onto an external reference for your stampViewFrame:
var stampViewFrame = CGRect(x: 100, y: 100, width: 100, height: 100)
2) Replace your scrollViewDidZoom() with this:
func scrollViewDidZoom(scrollView: UIScrollView) {
for views in webView.scrollView.subviews
{
if(views.isKindOfClass(UIImageView))
{
views.frame = CGRect(x: stampViewFrame.origin.x * scrollView.zoomScale, y: stampViewFrame.origin.y * scrollView.zoomScale, width: stampViewFrame.width * scrollView.zoomScale, height: stampViewFrame.height * scrollView.zoomScale)
}
}
}
3) Finally, because the zoom scale resets to 1 at the begining of each new zooming action, you need to adjust the value of your stampViewFrame property:
func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat) {
stampViewFrame = CGRect(x: stampViewFrame.origin.x * scale, y: stampViewFrame.origin.y * scale, width: stampViewFrame.width * scale, height: stampViewFrame.height * scale)
}
I also tried to answer your other question about layout during orientation change, but I now have a much better understanding of what you are trying to do. If you want your stampView to always be on in the same place relative to the web content, you have to get into HTML/JS because the webpage lays itself out dynamically. A much much more simple (and hopefully close enough) solution would be to add the following:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
webView.frame = view.bounds
stampView.frame = stampViewFrame
}

Use the scroll delegate method of scrollViewDidZoom :
func scrollViewDidZoom(scrollView: UIScrollView){
//Change the subview of scroll frame as per the scroll frame scale
//rect = initial position & size of the image.<class instance>
stampView.frame = CGRectMake((CGRectGetMaxX(rect)-rect.size.width)*webView.scrollView.zoomScale, (CGRectGetMaxY(rect)-rect.size.height)*webView.scrollView.zoomScale, rect.width*webView.scrollView.zoomScale,rect.height*webView.scrollView.zoomScale)
}

Related

Zoom and paging in one ScrollView

i am trying to create something like Photos gallery.
But now i have problem, with zooming in UIScrollView.
for i in 0..<imageArray.count{
let imageView = UIImageView()
imageView.isUserInteractionEnabled = true
imageView.image = imageArray[i]
imageView.contentMode = .scaleAspectFit
let xPosition = self.view.frame.width * CGFloat(i)
imageView.frame = CGRect(x: 0, y: 0, width: self.scrollView.frame.width, height: self.scrollView.frame.height)
scrollView.addSubview(imageView)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i+1)
}
scrollView.isScrollEnabled = true
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 6.0
scrollView.isUserInteractionEnabled = true
scrollView.delegate = self
Paging works fine, but i dont know now how to enable zooming each imageView.
You will need to implement the following method from the UIScrollViewDelegate protocol:
func viewForZooming(in: UIScrollView) -> UIView?
In that you return the view that you want to be zooming.
Now in this case you have multiple image views (depending on imageArray.count) but I assume you want to scroll into them all (i.e. not an individual image) so the best thing would be to add a 'content view' (UIView) to the scroll view and then add the individual image views to that instead of the scroll view directly and size it appropriately.
Then you can return that 'content view' in the viewForZooming method.
So assuming you have created this 'content view' then the method would look like this:
func viewForZooming(in: UIScrollView) -> UIView? {
return self.contentView
}

my initial problems with UIScrollView now appear to be related to autolayout

For my first challenge using UIScrollView I modified this example to make UIScrollView display not just another background colour but another UIView and UILabel on each page. But I could have just as easily chosen to display objects like UITableView, UIButton or UIImage.
Potentially, UIScrollView could be much more than a giant content view where users scroll from one part to the next, e.g., some pages might have a UIButton that takes a user to a specific page, the same way we use books.
Code Improvements
My question has evolved since I first posted it. Initially the labels piled up on page 1 (as shown below) but this has now been corrected. I also included this extension to make the font larger.
Further improvement ?
As the code evolved I became more aware of other issues e.g. iPhone 5 images (below) appear differently on iPhone 7 where the UILabel is centred but not the UIView. So my next challenge is possibly to learn how to combine UIScrollView with Autolayout. I invite anyone to spot other things that might be wrong.
ViewController.swift (corrected)
import UIKit
class ViewController: UIViewController,UIScrollViewDelegate {
let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
var views = [UIView]()
var lables = [UILabel]()
var colors:[UIColor] = [UIColor.red, UIColor.magenta, UIColor.blue, UIColor.cyan, UIColor.green, UIColor.yellow]
var frame: CGRect = CGRect.zero
var pageControl: UIPageControl = UIPageControl(frame: CGRect(x: 50, y: 500, width: 200, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
initialiseViewsAndLables()
configurePageControl()
scrollView.delegate = self
self.view.addSubview(scrollView)
for index in 0..<colors.count {
frame.origin.x = self.scrollView.frame.size.width * CGFloat(index)
frame.size = self.scrollView.frame.size
self.scrollView.isPagingEnabled = true
views[index].frame = frame
views[index].backgroundColor = colors[Int(index)]
views[index].layer.cornerRadius = 20
views[index].layer.masksToBounds = true
lables[index].frame = frame
lables[index].center = CGPoint(x: (view.frame.midX + frame.origin.x), y: view.frame.midY)
lables[index].text = String(index + 1)
lables[index].defaultFont = UIFont(name: "HelveticaNeue", size: CGFloat(200))
lables[index].textAlignment = .center
lables[index].textColor = .black
let subView1 = views[index]
let subView2 = lables[index]
self.scrollView .addSubview(subView1)
self.scrollView .addSubview(subView2)
}
print(views, lables)
self.scrollView.contentSize = CGSize(width: self.scrollView.frame.size.width * CGFloat(colors.count), height: self.scrollView.frame.size.height)
pageControl.addTarget(self, action: Selector(("changePage:")), for: UIControlEvents.valueChanged)
}
func initialiseViewsAndLables() {
// Size of views[] and lables[] is linked to available colors
for index in 0..<colors.count {
views.insert(UIView(), at:index)
lables.insert(UILabel(), at: index)
}
}
func configurePageControl() {
// Total number of available pages is based on available colors
self.pageControl.numberOfPages = colors.count
self.pageControl.currentPage = 0
self.pageControl.backgroundColor = getColour()
self.pageControl.pageIndicatorTintColor = UIColor.black
self.pageControl.currentPageIndicatorTintColor = UIColor.green
self.view.addSubview(pageControl)
}
func getColour() -> UIColor {
let index = colors[pageControl.currentPage]
return (index)
}
func changePage(sender: AnyObject) -> () {
scrollView.setContentOffset(CGPoint(x: CGFloat(pageControl.currentPage) * scrollView.frame.size.width, y: 0), animated: true)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
pageControl.backgroundColor = getColour()
}
}
Extension
extension UILabel{
var defaultFont: UIFont? {
get { return self.font }
set { self.font = newValue }
}
}
The centre point of the lable on each frame must be offset by the origin of the content view (as Baglan pointed out). I've modified the following line of code accordingly.
lables[Int(index)].center = CGPoint(x: (view.frame.midX + frame.origin.x), y: view.frame.midY)

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.

When to use UITouch vs UIScroll

I would like to achieve the design you see in dating apps. Where you can vertically scroll images of a profile and also horizontally scroll to view the next or previous person in the list.
Currently I have my views laid out as such.
Previous-UIView - current UIView - next UIView
UIScrollView. UIScrollView. UIScrollView
Images. Images. Images
UIView. UIView. UIView
Profile info. Profile info. Profile info
UIPageControl. UIPageControl UIPageControl.
Only one of the Views occupies the main view with next and previous off screen. Ideally when the user moves the view left I would programmatically remove the previous view, make current the previous, the next current and add a new view for next. Visa versa for moving right.
What is the best way to scroll the views horizontally?
Should I wrap them all in a UIScrollView? And would that interfere with the UIScrollView sub Views?
Or should I program touch controls to move the views?
Or is there a better way?
I'm still a newbie at iOS development so any help would be greatly appreciated.
So I've tried some experimenting with a test app and I'm pleased to say you can have UIScrollviews inside UIScrollviews.
I was able to get it running perfectly. Here is my code below.
override func viewDidLoad() {
super.viewDidLoad()
self.superView.delegate = self
// Do any additional setup after loading the view, typically from a nib.
var subImages1 = ["IMG_0004.JPG","IMG_0005.JPG","IMG_0008.JPG"]
var subImages2 = ["IMG_0009.JPG","IMG_0010.JPG","IMG_0011.JPG"]
var subImages3 = ["IMG_0013.JPG","IMG_0017.JPG","IMG_0018.JPG"]
self.images.append(subImages1)
self.images.append(subImages2)
self.images.append(subImages3)
self.superView.frame = self.view.frame
self.superView.contentSize = CGSizeMake(self.view.frame.width*3, self.view.frame.height)
self.superView.contentOffset = CGPoint(x:self.view.frame.width,y:0)
self.superView.pagingEnabled = true
self.view.addSubview(self.superView)
//layout the UIVeiws into the master ScrollView
for i in 0...2{
var offset = self.view.frame.width * CGFloat(i)
var pView = UIView()
pView.frame = CGRectMake(offset, 0, self.view.frame.width, self.view.frame.height)
pView.backgroundColor = colours[i]
self.superView.addSubview(pView)
self.profileViews.append(pView)
}
// Add sub Scroll views and images to the Views.
for (index, view) in enumerate(self.profileViews){
var scrollView = UIScrollView()
scrollView.delegate = self
scrollView.frame = CGRectMake(10, 10, self.view.frame.width-20, self.view.frame.height-20)
scrollView.pagingEnabled = true
scrollView.contentSize = CGSizeMake(scrollView.frame.width, scrollView.frame.height * CGFloat(images[index].count))
for (index2, image) in enumerate(images[index]){
var subImage = UIImageView()
subImage.frame = CGRectMake(0, scrollView.frame.height * CGFloat(index2), scrollView.frame.width, scrollView.frame.height)
subImage.contentMode = UIViewContentMode.ScaleAspectFit
subImage.image = UIImage(named: image as! String)
scrollView.addSubview(subImage)
}
view.addSubview(scrollView)
self.scrollViews.append(scrollView)
}
}
//Use the did end decelerating as it executes the code once the scoll has finished moving.
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
if(scrollView == self.superView){
var contentOffset = scrollView.contentOffset
var pageWidth = self.superView.frame.width
var fractionalPage:Double = Double(self.superView.contentOffset.x / pageWidth)
var page = lround(fractionalPage)
// In this example I take the last UIView from the stack and move it to the first.
// I would do the same in the real app but update the contents of the view after
if(page == 0){
var tempView = self.profileViews[2]
self.profileViews[2].removeFromSuperview()
self.profileViews.removeAtIndex(2)
for view in self.profileViews{
view.frame = CGRectMake(view.frame.minX + self.view.frame.width, 0, view.frame.width, view.frame.height)
println(view.frame)
}
tempView.frame = CGRectMake(0, 0, tempView.frame.width, tempView.frame.height)
self.profileViews.insert(tempView, atIndex: 0)
self.superView.addSubview(tempView)
var newOffset = contentOffset.x + pageWidth
self.superView.contentOffset = CGPoint(x: newOffset, y: 0)
}
// Take the first view and move it to the last.
if(page == 2){
var tempView = self.profileViews[0]
self.profileViews[0].removeFromSuperview()
self.profileViews.removeAtIndex(0)
for view in self.profileViews{
view.frame = CGRectMake(view.frame.minX - self.view.frame.width, 0, view.frame.width, view.frame.height)
println(view.frame)
}
tempView.frame = CGRectMake(tempView.frame.width*2, 0, tempView.frame.width, tempView.frame.height)
self.profileViews.append(tempView)
self.superView.addSubview(tempView)
var newOffset = contentOffset.x - pageWidth
self.superView.contentOffset = CGPoint(x: newOffset, y: 0)
}
}
}

Image overlaps previous view when tapping/swiping back

I have a UIScrollView which covers one of my views entirely. I have added a background image to this same view which scrolls at a slightly different rate to the actual UIScrollView. This works absolutely fine unless I use the back swipe gesture or tap the 'Back' button. What happens is the image covers the view for about 0.5 seconds before disappearing, and it looks pretty bad.
This is what I mean:
As you can see, that is mid way through the gesture, and rather than being able to see the previous view, you just see the part of the image that is off to the left. It doesn't happen on the first page of the UIScrollView so I guess it's because the image is overlapping the previous view.
Here is my code:
override func viewDidLoad() {
let pagesScrollViewSize = scrollView.frame.size
scrollView.contentSize = CGSize(width: pagesScrollViewSize.width * CGFloat(images.count), height: pagesScrollViewSize.height)
backgroundImageView.frame = CGRect(x: 0, y: 0, width: 2484, height: 736)
backgroundImageView.image = UIImage(named: "SF.png")
var visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .Light)) as UIVisualEffectView
visualEffectView.frame = backgroundImageView.bounds
backgroundImageView.addSubview(visualEffectView)
view.addSubview(backgroundImageView)
view.sendSubviewToBack(backgroundImageView)
scrollView.backgroundColor = UIColor.clearColor()
}
func scrollViewDidScroll(scrollView: UIScrollView) {
loadVisiblePages()
var factor = scrollView.contentOffset.x / (scrollView.contentSize.width - 414);
if factor < 0 {
factor = 0
}
if factor > 1 {
factor = 1
}
var frame: CGRect = backgroundImageView.frame
frame.origin.x = factor * (414 - backgroundImageView.frame.size.width)
backgroundImageView.frame = frame
}
Anyone have any suggestions?
You have to add the following in your viewDidLoad function:
self.view.clipsToBounds = true or scrollView.clipsToBounds = true if you just want to clip the subviews in your UIScrollView.
Setting this value to true causes subviews to be clipped to the bounds of the receiver. If set to false, subviews whose frames extend beyond the visible bounds of the receiver are not clipped. The default value is false.
From Apple' doc : https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/index.html#//apple_ref/occ/instp/UIView/clipsToBounds

Resources