This question already has answers here:
How to zoom a UIScrollView inside of a UICollectionViewCell?
(7 answers)
Closed 5 years ago.
My goal is to create app like Apple Photos app - show photos, allow to zoom, scroll from horizontally, etc...
I am currently stuck at allowing to zoom photo in collectionViewCell.
What I've done: I have collectionView with scrollView inside collectionViewCell. This is how I create UIImageView inside In CollectionViewController in cellForItemAtIndexPath:
let imageView: UIImageView!
let someImage = UIImage(named: "someImage")
imageView = UIImageView(image: someImage)
imageView.frame = CGRect(origin: CGPointMake(0.0, 0.0), size:someImage.size)
cell.scrollView.addSubview(imageView)
cell.scrollView.contentSize = someImage.size
let scrollViewFrame = cell.scrollView.frame
let scaleWidth = scrollViewFrame.size.width / cell.scrollView.contentSize.width
let scaleHeight = scrollViewFrame.size.height / cell.scrollView.contentSize.height
let minScale = min(scaleWidth, scaleHeight);
cell.scrollView.minimumZoomScale = minScale;
cell.scrollView.maximumZoomScale = 1.0
cell.scrollView.zoomScale = minScale;
centerScrollViewContents(cell.scrollView, imageView: imageView)
(I found this code on Ray Wenderlich)
Next thing I need to add this code, so that zooming would work properly:
func viewForZoomingInScrollView(scrollView: UIScrollView!) -> UIView! {
return imageView
}
func scrollViewDidZoom(scrollView: UIScrollView!) {
let boundsSize = scrollView.bounds.size
var contentsFrame = imageView.frame
if contentsFrame.size.width < boundsSize.width {
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0
} else {
contentsFrame.origin.x = 0.0
}
if contentsFrame.size.height < boundsSize.height {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0
} else {
contentsFrame.origin.y = 0.0
}
imageView.frame = contentsFrame
}
The thing is, my IBAOutlet for scrollView is inside CollectionViewCell, not CollectionViewController, so those two functions have to be called from CollectionViewCell, but I don't know how to access imageView for viewForZoomingInScrollView. Maybe my whole approach is wrong?
Create UICollectionViewCell subclass with xib
Subclassing UICollectionViewCell and initialising from xib
or without xib
http://randexdev.com/2014/08/uicollectionviewcell/
then set delegate for scrollView is collectionViewCell subclass
Add UIScrollViewDelegate code to cell
You can access imageView from cell property
Related
I'm trying to zoom UIImageView and UIView, which are inside the UIScrollView, but it doesn't work.
I have an UIImage and CALayer, both of them have an equal size (2048×2155). These components wrapped into UIImageView (image view) and UIView (canvas) and displayed inside UIScrollView.
The image view and canvas have the same content scale factor, which equals 2.0 (UIScreen.main.scale), but the canvas has the wrong size (see picture below).
CALayer sublayers were built by using CAShapeLayer and UIBezierPath.
The UIScrollViewDelegate looks next:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
canvas
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
self.scrollView.centerContentView()
}
The center content view looks next (method inside custom UIScrollView):
func centerContentView() {
let boundsSize = bounds.size
var centerFrame = canvas.frame
if centerFrame.size.width < boundsSize.width {
centerFrame.origin.x = (boundsSize.width - centerFrame.size.width) / 2.0
} else {
centerFrame.origin.x = 0
}
if centerFrame.size.height < boundsSize.height {
centerFrame.origin.y = (boundsSize.height - centerFrame.size.height) / 2.0
} else {
centerFrame.origin.y = 0
}
imageView.frame = centerFrame
canvas.frame = centerFrame
}
All my efforts have failed. I tried to transform the CALayer with canvas but to no avail. I will be grateful for any suggestions or help. Thank you.
The problem was fixed by using the next lines:
let scale = UIScreen.main.bounds.width / imageSize.height
let scaleTransformation = CATransform3DMakeScale(scale, scale, 1)
for layer in layers {
layer.transform = scaleTransformation
}
where the imageSize equals to the 2048×2155 and everything works fine.
Thank you.
I have a tableview in my iOS project that uses an image as background. The image does not scroll, it is static. Because of that I also have transparent cells and section headers. Now my question is how can I make the (transparent) cells to "hide" or "disappear" behind the (also transparent) section header?
Is it possible?
On your custom cell
public func maskCell(fromTop margin: CGFloat) {
layer.mask = visibilityMask(withLocation: margin / frame.size.height)
layer.masksToBounds = true
}
private func visibilityMask(withLocation location: CGFloat) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = bounds
mask.colors = [UIColor.white.withAlphaComponent(0).cgColor, UIColor.white.cgColor]
let num = location as NSNumber
mask.locations = [num, num]
return mask
}
and on you ViewController UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in self.lessonsTableView.visibleCells {
let paddingToDisapear = CGFloat(25)
let hiddenFrameHeight = scrollView.contentOffset.y + paddingToDisapear - cell.frame.origin.y
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
if let customCell = cell as? LessonTableViewCell {
customCell.maskCell(fromTop: hiddenFrameHeight)
}
}
}
}
My view hierarchy is this
PhotoDetailViewController.swift
View
UIScrollView
UIImageView
I set this up using storyboard, and add four constraints(top=0, bottom=0, leading=0, tailing=0) to UIScrollView, four constraints(top=0, bottom=0, leading=0, tailing=0) to UIImageView, but there are two error says
"ScrollView has ambiguous scrollable content width"
"ScrollView has ambiguous scrollable content height"
I understand that this is because I haven't set UIScrollView contentSize, but What I trying to do is load photo from PHAsset asynchronously, so I can only get the photo size at run time. So the question is:
1:Given that photo size can only be get at run time, how to solve the "ambiguous scrollable content" error?
2:In which View's life cycle method should I call PHImageManager.requestImageForAsset? because I think I should set UIScrollView contentSize programmatically, but when?
update with PhotoDetailViewController.swift
import UIKit
import Photos
class PhotoDetailViewController : UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!
#IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!
#IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!
#IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!
var devicePhotosAsset : PHFetchResult!
var index = 0
var photo : UIImage!
var imgManager:PHImageManager!
#IBOutlet weak var imageView : UIImageView!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
override func awakeFromNib() {
self.imgManager = PHImageManager()
}
override func viewDidLoad() {
super.viewDidLoad()
self.displayPhoto()
}
override func viewWillLayoutSubviews() {
super.viewDidLayoutSubviews()
updateMinZoomScaleForSize()
updateConstraintsForSize()
}
func displayPhoto () {
_ = self.imgManager.requestImageForAsset(self.devicePhotosAsset[self.index] as! PHAsset, targetSize: PHImageManagerMaximumSize, contentMode: .AspectFit, options: nil, resultHandler: {(result, info) -> Void in
NSOperationQueue.mainQueue().addOperationWithBlock(){
self.imageView.image = result
}
})
}
private func targetSize() -> CGSize {
let scale = UIScreen.mainScreen().scale
let targetSize = CGSizeMake(CGRectGetWidth(self.imageView.bounds)*scale, CGRectGetHeight(self.imageView.bounds)*scale)
return targetSize
}
private func updateMinZoomScaleForSize() {
let size = scrollView.bounds.size
let widthScale = size.width / imageView.bounds.width
let heightScale = size.height / imageView.bounds.height
let minScale = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
}
func recenterImage(){
let scrollViewSize = scrollView.bounds.size
let imageSize = imageView.frame.size
let horizontalSpace = imageSize.width < scrollViewSize.width ? (scrollViewSize.width - imageSize.width)/2 : 0
let verticalSpace = imageSize.height < scrollViewSize.height ? (scrollViewSize.height - imageSize.height)/2 : 0
scrollView.contentInset = UIEdgeInsets(top: verticalSpace, left: horizontalSpace, bottom: verticalSpace, right: horizontalSpace)
}
private func updateConstraintsForSize() {
let size = scrollView.bounds.size
let yOffset = max(0, (size.height - imageView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset
imageViewBottomConstraint.constant = yOffset
let xOffset = max(0, (size.width - imageView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset
imageViewTrailingConstraint.constant = xOffset
view.layoutIfNeeded()
}
}
extension PhotoDetailViewController: UIScrollViewDelegate {
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
func scrollViewDidZoom(scrollView: UIScrollView) {
updateConstraintsForSize()
}
}
Your existing constraints are enough to set the content size, it's just that it's based on the image view intrinsic content size and that doesn't really exist until the image view has an image.
You can add a width and height constraint to the image view with default values and deactivate those constraints when the image is set to the view. Or you could use a placeholder image and avoid those extra constraints because you'd always have an intrinsic content size for the image view.
You should set two more constraint to your imageView.
Horizontally in Container (or you can say it center X)
Fixed Height
Second thing you can put UIView on Scrollview with Constraints like,
Top,leading,trailing,bottom,Horizontally in container(center x),fixed height).
Then add your imageview to that view. And can change it's constraint after getting image to resize it's height and width.
You can connect outlet of any constraint and can change it's constant programmatically.
Xcode UI builder has special type of constraint for such cases (when you can setup constraint only in runtime). It's so called "placeholder constraint" which will be removed at build time but helps to remove constraints errors for developing.
So solution is
Add some sample constraints IB and mark them as placeholders
Add needed constraints in runtime
When you get the data, just add these lines
float sizeOfContent = 0;
UIView *lLast = [yourscrollview.subviews lastObject];
NSInteger wd = lLast.frame.origin.y;
NSInteger ht = lLast.frame.size.height;
sizeOfContent = wd+ht;
yourscrollview.contentSize = CGSizeMake(yourscrollview.frame.size.width, sizeOfContent);
Hope this helps
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.
It looks like aspect fit aligns the image to the bottom of the frame by default. Is there a way to override the alignment while keeping aspect fit intact?
** EDIT **
This question predates auto layout. In fact, auto layout was being revealed in WWDC 2012 the same week this question was asked
In short, you cannot do this with a UIImageView.
One solution is to subclass a UIView containing an UIImageView and change its frame according to image size. For example, you can find one version here.
Set the UIImageView's bottom layout constraint priority to lowest (i.e. 250) and it will handle it for you.
The way to do this is to modify the contentsRect of the UIImageView layer. The following code from my project (sub class of UIImageView) assumes scaleToFill and offsets the image such that it aligns top, bottom, left or right instead of the default center alignment. For aspectFit is would be a similar solution.
typedef NS_OPTIONS(NSUInteger, AHTImageAlignmentMode) {
AHTImageAlignmentModeCenter = 0,
AHTImageAlignmentModeLeft = 1 << 0,
AHTImageAlignmentModeRight = 1 << 1,
AHTImageAlignmentModeTop = 1 << 2,
AHTImageAlignmentModeBottom = 1 << 3,
AHTImageAlignmentModeDefault = AHTImageAlignmentModeCenter,
};
- (void)updateImageViewContentsRect {
CGRect imageViewContentsRect = CGRectMake(0, 0, 1, 1);
if (self.image.size.height > 0 && self.bounds.size.height > 0) {
CGRect imageViewBounds = self.bounds;
CGSize imageSize = self.image.size;
CGFloat imageViewFactor = imageViewBounds.size.width / imageViewBounds.size.height;
CGFloat imageFactor = imageSize.width / imageSize.height;
if (imageFactor > imageViewFactor) {
//Image is wider than the view, so height will match
CGFloat scaledImageWidth = imageViewBounds.size.height * imageFactor;
CGFloat xOffset = 0.0;
if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeLeft)) {
xOffset = -(scaledImageWidth - imageViewBounds.size.width) / 2;
} else if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeRight)) {
xOffset = (scaledImageWidth - imageViewBounds.size.width) / 2;
}
imageViewContentsRect.origin.x = (xOffset / scaledImageWidth);
} else if (imageFactor < imageViewFactor) {
CGFloat scaledImageHeight = imageViewBounds.size.width / imageFactor;
CGFloat yOffset = 0.0;
if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeTop)) {
yOffset = -(scaledImageHeight - imageViewBounds.size.height) / 2;
} else if (BM_CONTAINS_BIT(self.alignmentMode, AHTImageAlignmentModeBottom)) {
yOffset = (scaledImageHeight - imageViewBounds.size.height) / 2;
}
imageViewContentsRect.origin.y = (yOffset / scaledImageHeight);
}
}
self.layer.contentsRect = imageViewContentsRect;
}
Swift version
class AlignmentImageView: UIImageView {
enum HorizontalAlignment {
case left, center, right
}
enum VerticalAlignment {
case top, center, bottom
}
// MARK: Properties
var horizontalAlignment: HorizontalAlignment = .center
var verticalAlignment: VerticalAlignment = .center
// MARK: Overrides
override var image: UIImage? {
didSet {
updateContentsRect()
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateContentsRect()
}
// MARK: Content layout
private func updateContentsRect() {
var contentsRect = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
guard let imageSize = image?.size else {
layer.contentsRect = contentsRect
return
}
let viewBounds = bounds
let imageViewFactor = viewBounds.size.width / viewBounds.size.height
let imageFactor = imageSize.width / imageSize.height
if imageFactor > imageViewFactor {
// Image is wider than the view, so height will match
let scaledImageWidth = viewBounds.size.height * imageFactor
var xOffset: CGFloat = 0.0
if case .left = horizontalAlignment {
xOffset = -(scaledImageWidth - viewBounds.size.width) / 2
}
else if case .right = horizontalAlignment {
xOffset = (scaledImageWidth - viewBounds.size.width) / 2
}
contentsRect.origin.x = xOffset / scaledImageWidth
}
else {
let scaledImageHeight = viewBounds.size.width / imageFactor
var yOffset: CGFloat = 0.0
if case .top = verticalAlignment {
yOffset = -(scaledImageHeight - viewBounds.size.height) / 2
}
else if case .bottom = verticalAlignment {
yOffset = (scaledImageHeight - viewBounds.size.height) / 2
}
contentsRect.origin.y = yOffset / scaledImageHeight
}
layer.contentsRect = contentsRect
}
}
this will make the image fill the width and occupy only the height it needs to fit the image (widthly talking)
swift 4.2:
let image = UIImage(named: "my_image")!
let ratio = image.size.width / image.size.height
cardImageView.widthAnchor
.constraint(equalTo: cardImageView.heightAnchor, multiplier: ratio).isActive = true
I had similar problem.
Simplest way was to create own subclass of UIImageView. I add for subclass 3 properties so now it can be use easly without knowing internal implementation:
#property (nonatomic) LDImageVerticalAlignment imageVerticalAlignment;
#property (nonatomic) LDImageHorizontalAlignment imageHorizontalAlignment;
#property (nonatomic) LDImageContentMode imageContentMode;
You can check it here:
https://github.com/LucasssD/LDAlignmentImageView
Add the Aspect Ratio constraint with your image proportions.
Do not pin UIImageView to bottom.
If you want to change the UIImage dynamically remember to update aspect ratio constraint.
I solved this natively in Interface Builder by setting a constraint on the height of the UIImageView, since the image would always be 'pushed' up when the image was larger than the screen size.
More specifically, I set the UIImageView to be the same height as the View it is in (via height constraint), then positioned the UIImageView with spacing constraints in IB. This results in the UIImageView having an 'Aspect Fit' which still respects the top spacing constraint I set in IB.
If you are able to subclass UIImageView, then you can just override the image var.
override var image: UIImage? {
didSet {
self.sizeToFit()
}
}
In Objective-C you can do the same thing by overriding the setter.