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.
Related
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)
}
}
}
}
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.
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
I have a UIView, inside it I have a UIImageView. I have a UIPinchGestureRecognizer added to the UIVIew to handle the pinch and zoom and make the UIView grow with the UIImageView altogether.
My UIView has a border. I added the border this way:
self.layer.borderColor = [UIColor blackColor].CGColor;
self.layer.borderWidth = 1.0f;
self.layer.cornerRadius = 8.0f;
And the problem I'm having is I can't find a way of making my UIView bigger while keeping the same width of the border. When pinching and zooming the border gets thicker.
This is my UIPinchGestureRecognizer handler:
- (void)scale:(UIPanGestureRecognizer *)sender{
if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
_lastScale = 1.0;
}
CGFloat scale = 1.0 - (_lastScale - [(UIPinchGestureRecognizer*)sender scale]);
CGAffineTransform currentTransform = self.transform;
CGAffineTransform newTransform = CGAffineTransformScale(currentTransform, scale, scale);
_lastScale = [(UIPinchGestureRecognizer*)sender scale];
[self setTransform:newTransform];
}
Thanks a lot!!
I've been googling around A LOT and found this:
self.layer.borderWidth = 2.0f / scaleFactor;
Sadly is not working for me... It makes sense but not working.
Also I read the solution about adding an other view in the back and making the front view to have an offset in the position so the back view is shown and looks like a border. That's not an option because I need my image to view transparent.
I want to recreate what Aviary does in their app. You can scale up and down an "sticker" and the border always stays the same size.
I was trying to do this effect in this way as well in Swift 4.2. Ultimately I could not do it successfully using CGAffine Effects. I ultimately had to update the constraints -- as I was using anchors.
Here is the code I ultimately started using. SelectedView is the view that should be scaled.
Keep in mind that this does not break other CGAffine effects. I'm still using those for rotation.
#objc private func scaleSelectedView(_ sender: UIPinchGestureRecognizer) {
guard let selectedView = selectedView else {
return
}
var heightDifference: CGFloat?
var widthDifference: CGFloat?
// increase width and height according to scale
selectedView.constraints.forEach { constraint in
guard
let view = constraint.firstItem as? UIView,
view == selectedView
else {
return
}
switch constraint.firstAttribute {
case .width:
let previousWidth = constraint.constant
constraint.constant = constraint.constant * sender.scale
widthDifference = constraint.constant - previousWidth
case .height:
let previousHeight = constraint.constant
constraint.constant = constraint.constant * sender.scale
heightDifference = constraint.constant - previousHeight
default:
return
}
}
// adjust leading and top anchors to keep view centered
selectedView.superview?.constraints.forEach { constraint in
guard
let view = constraint.firstItem as? UIView,
view == selectedView
else {
return
}
switch constraint.firstAttribute {
case .leading:
guard let widthDifference = widthDifference else {
return
}
constraint.constant = constraint.constant - widthDifference / 2
case .top:
guard let heightDifference = heightDifference else {
return
}
constraint.constant = constraint.constant - heightDifference / 2
default:
return
}
}
// reset scale after applying in order to keep scaling linear rather than exponential
sender.scale = 1.0
}
Notes: width and height anchors are on the view itself. Top and Leading anchors are on the superview -- hence the two forEach blocks.
I had to figure out a way of keeping my borders the same width during transforms (also creating stickers), and needed the solution to work with CGAffineTransform because my stickers rotate (and you cannot easily use frame-based solutions if you're doing rotations).
My approach was to change the layer.borderWidth in response to the transform changing, like this. Works for me.
class MyView : UIView {
let borderWidth = CGFloat(2)
override var transform: CGAffineTransform {
didSet {
self.setBorder()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.setBorder()
}
fileprivate func setBorder() {
self.layer.borderWidth = self.borderWidth / transform.scale
}
}
extension CGAffineTransform {
var scale: CGFloat {
return sqrt((a * a + c * c))
}
}
while setting CGAffineTransformScale, you wont be able to control the border-width separately.
The solution would be to use another view say borderView as subview in above view hierarchy than the view to be scaled (with backgroundColor as clearcolor)whose SIZE should be changed according to scale factor of other view.
Apply your border-width to the borderView keeping the desired borderwidth.
Best of luck!
Switch from CGAffineTransform to view's frame.
Sample code for pinching a view with constant border width:
#interface ViewController ()
#property (nonatomic, weak) IBOutlet UIView *testView;
#end
#implementation ViewController {
CGFloat _lastScale;
CGRect _sourceRect;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.testView.backgroundColor = [UIColor yellowColor];
self.testView.layer.borderColor = [UIColor blackColor].CGColor;
self.testView.layer.borderWidth = 1.0f;
self.testView.layer.cornerRadius = 8.0f;
_sourceRect = self.testView.frame;
_lastScale = 1.0;
}
- (IBAction)scale:(UIPanGestureRecognizer *)sender{
if([(UIPinchGestureRecognizer*)sender state] == UIGestureRecognizerStateBegan) {
_lastScale = 1.0;
}
CGFloat scale = 1.0 - (_lastScale - [(UIPinchGestureRecognizer*)sender scale]);
CGPoint center = self.testView.center;
CGRect newBounds = CGRectMake(0, 0, _sourceRect.size.width * scale, _sourceRect.size.height * scale);
self.testView.bounds = newBounds;
self.testView.layer.transform = CATransform3DMakeRotation(M_PI * (scale - 1), 0, 0, 1);
}
#end
UPD:
I have checked Aviary app: scale by frame, rotate by CGAffineTransform. You may need to implement some additional logic to make it work.
UPD:
Use bounds and play with rotation
Without setting the transform, scale the size of view by changing it's bounds. This solution works for me.
You can add a method in the view class you want to scale, sample code:
- (void)scale:(CGFloat)scale {
self.bounds = CGRectMake(0, 0, CGRectGetWidth(self.bounds) * scale, CGRectGetHeight(self.bounds) * scale);
}
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.