UIImageView Scaling


Rick Aurbach
 

I have a UIImageView, which I load with the following function:

func loadData() {

guard let recipe = recipe,

let fileN = recipe.photo,

!fileN.isEmpty else {

imageView.image = nil

xableTrashButton()

return

}

let filePath = ImageUtils.fileURL(fileName: fileN, directory: .applicationSupportDirectory)!.path

imageView.image = UIImage(contentsOfFile: filePath)

imageView.contentMode = .scaleAspectFit

imageView.sizeToFit()

xableTrashButton()

}


I create a new image using a standard UIImagePickerController. In its imagePickerController(_ picker:didFinishPickingMediaWithInfo info:) delegate method, I save the image to a file (in the .applicationSupportDirectory) and save the image name in my database. Then I dismiss the pickerController, and call the above loadData() method in it's completion handler.

I observe two cases. In all cases except the above "exit from picker" case, the image is shown scaled to fit in the UIImageView. But in the case of "exit from picker", the upper left corner of the image is shown in the UIImageView at its full size.

Does anyone have any idea what might be happening here? And what I can do to get the (desired) effect of displaying the image scaled to its imageView in all cases?

Thanks,

Rick Aurbach


Steve Christensen
 

Questions:

1. Do you get different sizing behavior for the same image depending on which case/path gets taken?

2. How does the UIImageView size compare with image.size for the misbehaving case?


On Jul 20, 2018, at 1:35 PM, Rick Aurbach via Groups.Io <rlaurb@...> wrote:

I have a UIImageView, which I load with the following function:

func loadData() {

guard let recipe = recipe,

let fileN = recipe.photo,

!fileN.isEmpty else {

imageView.image = nil

xableTrashButton()

return

}

let filePath = ImageUtils.fileURL(fileName: fileN, directory: .applicationSupportDirectory)!.path

imageView.image = UIImage(contentsOfFile: filePath)

imageView.contentMode = .scaleAspectFit

imageView.sizeToFit()

xableTrashButton()

}


I create a new image using a standard UIImagePickerController. In its imagePickerController(_ picker:didFinishPickingMediaWithInfo info:) delegate method, I save the image to a file (in the .applicationSupportDirectory) and save the image name in my database. Then I dismiss the pickerController, and call the above loadData() method in it's completion handler.

I observe two cases. In all cases except the above "exit from picker" case, the image is shown scaled to fit in the UIImageView. But in the case of "exit from picker", the upper left corner of the image is shown in the UIImageView at its full size.

Does anyone have any idea what might be happening here? And what I can do to get the (desired) effect of displaying the image scaled to its imageView in all cases?

Thanks,

Rick Aurbach


Rick Aurbach
 

1. For any image which has an intrinsic size larger than the destination UIImageView object, the sizing behavior is the same. Namely, when the picker is dismissed (calling loadData(), the upper left corner of the image appears, fully expanded. In all other cases where the image is displayed (such as after switching between scenes, etc.), the image has been scaled to fit the UIImageView's physical size.

2. This is two questions. The images that cause problems have a size (i.e. UIImage::size) larger than the physical dimensions of the device. And in both the "correct" and "incorrect" cases, the UIImageView:bounds rectangle has a size equal to the image's size.

Rick


Steve Christensen
 

I think I ran into similar issues in the dim past. If I recall correctly, the image view would resize to accommodate the image size if the image was set before a layout pass, or maintain some fixed size (initial frame?) if the image was set later. And I believe this was happening when there were no explicit constraints. 

I didn’t ask before, but do you have constraints, either fixed or proportional to something else, set on the image view? If so, and the image view is set to aspect fit, then you should get consistent behavior no matter the image size. 

Steve


On Jul 20, 2018, at 10:07 PM, Rick Aurbach via Groups.Io <rlaurb@...> wrote:

1. For any image which has an intrinsic size larger than the destination UIImageView object, the sizing behavior is the same. Namely, when the picker is dismissed (calling loadData(), the upper left corner of the image appears, fully expanded. In all other cases where the image is displayed (such as after switching between scenes, etc.), the image has been scaled to fit the UIImageView's physical size.

2. This is two questions. The images that cause problems have a size (i.e. UIImage::size) larger than the physical dimensions of the device. And in both the "correct" and "incorrect" cases, the UIImageView:bounds rectangle has a size equal to the image's size.

Rick


Rick Aurbach
 

I have explicit constraints. I expected consistent behavior and really don't understand why I don't get it.

I've tried two cases:
(1) UIImageView directly embedded in a (sub)-controller's view. Fully bound.
(2) UIScrollView directly embedded in a (sub)-controller's view. Fully bound. UIImageView inside the UIScrollView with all four sides bound to the scrollView and with "equal heights" and "equal widths" constraints set.

Both cases behave the same (test case is iPad Air 2 running 11.4.1.


Steve Christensen
 

Setting only edge constraints (top/bottom/leaing/trailing) on the image view will not guarantee "consistent" size. Unlike UILabel, for example, an image view doesn't necessarily have an intrinsic size based on current content since it's designed to scale the image in several ways, independent of the size of the view.

If your goal is to resize the image view to fit the image, perhaps with some limits such as image width less than the screen width, then I think your best bet is to create IBOutlets for the image view's width and height constraints and then adjust them based on your requirements whenever you set the image.


On Jul 21, 2018, at 2:37 PM, Rick Aurbach via Groups.Io <rlaurb@...> wrote:

I have explicit constraints. I expected consistent behavior and really don't understand why I don't get it.

I've tried two cases:
(1) UIImageView directly embedded in a (sub)-controller's view. Fully bound.
(2) UIScrollView directly embedded in a (sub)-controller's view. Fully bound. UIImageView inside the UIScrollView with all four sides bound to the scrollView and with "equal heights" and "equal widths" constraints set.

Both cases behave the same (test case is iPad Air 2 running 11.4.1.


On Jul 20, 2018, at 10:39 PM, Steve Christensen via Groups.Io <punster@...> wrote:

I think I ran into similar issues in the dim past. If I recall correctly, the image view would resize to accommodate the image size if the image was set before a layout pass, or maintain some fixed size (initial frame?) if the image was set later. And I believe this was happening when there were no explicit constraints. 

I didn’t ask before, but do you have constraints, either fixed or proportional to something else, set on the image view? If so, and the image view is set to aspect fit, then you should get consistent behavior no matter the image size. 

Steve


On Jul 20, 2018, at 10:07 PM, Rick Aurbach via Groups.Io <rlaurb@...> wrote:

1. For any image which has an intrinsic size larger than the destination UIImageView object, the sizing behavior is the same. Namely, when the picker is dismissed (calling loadData(), the upper left corner of the image appears, fully expanded. In all other cases where the image is displayed (such as after switching between scenes, etc.), the image has been scaled to fit the UIImageView's physical size.

2. This is two questions. The images that cause problems have a size (i.e. UIImage::size) larger than the physical dimensions of the device. And in both the "correct" and "incorrect" cases, the UIImageView:bounds rectangle has a size equal to the image's size.

Rick


Rick Aurbach
 

Steve,

I don’t want to disagree with you, but I’m seeing additional pieces of evidence which suggest (to me, at any rate) that this issue is related to timing, not insufficient constraints. I’ll try to explain. (I know a sample app would be best, but I haven’t had time yet to build it. When I do, I’ll post it here.)

(1) First of all, is the important observation that the ONLY time the image is drawn unscaled is when returning from UIImagePickerController. That is, if I visit the scene with the ImageView at any other time, the image appears scaled to the ImageView. It is only when first picking the image and populating the ImageVIew for the first time with the new image that it appears unscaled.

Let me be a bit more explicit about that. The only time the image is drawn unscaled in the ImageView is when performing the following sequence:
  • User selects to put a new image in the image view. 
  • We display the appropriate UIImagePickerController. 
  • The user makes the selection and clicks Done.
  • The picker delegate’s didFinishPickingMediaWithInfo handler is called.
  • In that handler, we explicitly dismiss the picker controller.
  • In the dismiss function’s completion handler, we load the newly selected image into the ImageView (using exactly the same logic as we do when displaying the scene originally).
  • In this case (and this case only), the image in the ImageView is displayed unscaled. 

(2) Originally, the ImageView sat bare in the (sub)-controller’s view. I also tried embedding the ImageView in a ScrollView. In this case, I bound the scrollView to its enclosures Safe Area, then bound the ImageView to the ScrollView and added the “equal height” and “equal width” constraints between ImageView and its enclosing ScrollView. [The ScrollView was not enabled for zoom.] This made no change in behavior.

(3) My imageView is built with a tap gesture recognizer. Tapping the image causes a new window to be presented (in overFullScreen mode), providing pan/zoom support. So I changed the code so that, when dismissing the UIImagePickerController, I first load data into the ImageView, then IMMEDIATELY present this other (covering) window. And when I close this window (thereby revealing the ImageView once again), the image is properly scaled. [I am, in fact, using this approach as a temporary workaround in my latest field-test release. If you’d like to see things in action, please email me privately and I’ll add you to the field test.]

Sorry if this is a bit muddy. It’s much harder to write about than to understand with even a brief glimpse at the code. That’s why I’ll try to generate a sample app.

Hope this helps.

Cheers,

Rick



Steve Christensen
 

No worries about disagreements: you're looking at the code and I'm getting the condensed version in your description.

Your comment about timing being an issue disturbs me since, ideally, the behavior should be consistent irrespective of "timing" since everything is funneled through a single thread. Assuming that one of your paths isn't accidentally setting imageView.image on a background thread, are you making any layout adjustments in -viewWillLayoutSubviews, for example? I have had problems where, ultimately, it was my fault but it took a long time to figure out that I was effectively trying to position something at the wrong time.


On Jul 23, 2018, at 8:13 AM, Rick Aurbach via Groups.Io <rlaurb@...> wrote:

Steve,

I don’t want to disagree with you, but I’m seeing additional pieces of evidence which suggest (to me, at any rate) that this issue is related to timing, not insufficient constraints. I’ll try to explain. (I know a sample app would be best, but I haven’t had time yet to build it. When I do, I’ll post it here.)

(1) First of all, is the important observation that the ONLY time the image is drawn unscaled is when returning from UIImagePickerController. That is, if I visit the scene with the ImageView at any other time, the image appears scaled to the ImageView. It is only when first picking the image and populating the ImageVIew for the first time with the new image that it appears unscaled.

Let me be a bit more explicit about that. The only time the image is drawn unscaled in the ImageView is when performing the following sequence:
  • User selects to put a new image in the image view. 
  • We display the appropriate UIImagePickerController. 
  • The user makes the selection and clicks Done.
  • The picker delegate’s didFinishPickingMediaWithInfo handler is called.
  • In that handler, we explicitly dismiss the picker controller.
  • In the dismiss function’s completion handler, we load the newly selected image into the ImageView (using exactly the same logic as we do when displaying the scene originally).
  • In this case (and this case only), the image in the ImageView is displayed unscaled. 

(2) Originally, the ImageView sat bare in the (sub)-controller’s view. I also tried embedding the ImageView in a ScrollView. In this case, I bound the scrollView to its enclosures Safe Area, then bound the ImageView to the ScrollView and added the “equal height” and “equal width” constraints between ImageView and its enclosing ScrollView. [The ScrollView was not enabled for zoom.] This made no change in behavior.

(3) My imageView is built with a tap gesture recognizer. Tapping the image causes a new window to be presented (in overFullScreen mode), providing pan/zoom support. So I changed the code so that, when dismissing the UIImagePickerController, I first load data into the ImageView, then IMMEDIATELY present this other (covering) window. And when I close this window (thereby revealing the ImageView once again), the image is properly scaled. [I am, in fact, using this approach as a temporary workaround in my latest field-test release. If you’d like to see things in action, please email me privately and I’ll add you to the field test.]

Sorry if this is a bit muddy. It’s much harder to write about than to understand with even a brief glimpse at the code. That’s why I’ll try to generate a sample app.

Hope this helps.

Cheers,

Rick


Rick Aurbach
 

Your comment about timing being an issue disturbs me since, ideally, the behavior should be consistent irrespective of "timing" since everything is funneled through a single thread. Assuming that one of your paths isn't accidentally setting imageView.image on a background thread, are you making any layout adjustments in -viewWillLayoutSubviews, for example? I have had problems where, ultimately, it was my fault but it took a long time to figure out that I was effectively trying to position something at the wrong time.

Yup. It disturbs me too.

But unless UIImagePickerController is doing something funky behind the scenes, then no, I am not doing anything on a background thread, nor am I explicitly making any changes to layout except for setting imageView.image and calling imageView.sizeToFit().

In fact, I’ve tried calling my loadData() method from
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in 
self?.loadData()
}

and it doesn’t help.

What it “feels like” (whatever that means) is that there is some race condition or timing condition between code being executed in the UIImagePickerControllerDelegate’s done-handler and the layout/display subsystem for the imageView in the presenter’s view scene.

If you’re inclined to play with this, I put a sample app together yesterday afternoon. https://www.dropbox.com/s/avx6y6kkb9mpwhh/ImageViewScaling.zip?dl=0
Please note that Case #1 appears to work in the Sample App, but not in my real app. I’m currently using Case #3 in my real app. Case #2 fails in both places.

Cheers,

Rick


Steve Christensen
 

Hi Rick,

Sorry I didn't reply sooner but the day job got in the way. I just tried out your test app on my iPhone. I ended up adding overrides of viewWillLayoutSubviews() and viewDidLayoutSubviews() and printing the imageView bounds before and after the layout pass. Here's what I saw:

Case 1:
- select a photo
- imagePickerController(_ didFinishPickingMediaWithInfo:) -> loadData(): loads image + sizeToFit()
- viewWillAppear() -> loadData(): loads image + sizeToFit()
- viewWillLayoutSubviews(): imageView size = (3024.0, 4032.0)
- viewDidLayoutSubviews(): imageView size = (296.0, 318.0)

Case 2:
- select a photo
- viewWillAppear() -> loadData(): loads image + sizeToFit()
- viewWillLayoutSubviews(): imageView size = (3024.0, 4032.0)
- viewDidLayoutSubviews(): imageView size = (3024.0, 4032.0)
- imagePickerController(_ didFinishPickingMediaWithInfo:) -> picker.dismiss() -> loadData(): loads image + sizeToFit()

Case 3:
- select a photo
- viewWillAppear() -> loadData(): loads image + sizeToFit()
- viewWillLayoutSubviews(): imageView size = (3024.0, 4032.0)
- viewDidLayoutSubviews(): imageView size = (296.0, 300.5)
- imagePickerController(_ didFinishPickingMediaWithInfo:) -> loadData(): loads image + sizeToFit()
- viewWillLayoutSubviews(): imageView size = (3024.0, 4032.0)
- viewDidLayoutSubviews(): imageView size = (296.0, 320.5)


As you descibed, case 2 didn't resize the imageView. Since we talked about timing, I got to wondering if hopping between the image picker and the case image views was causing a layout pass to *effectively* get lost due to the timing so I added imageView.superview!.setNeedsLayout() right after the imageView.sizeToFit() and it resized correctly. (Actually even removing the sizeToFit() and just doing the setNeedsLayout() worked as well.)

I don't have better insight into the actual details but this might, at least, provide a starting point.

Steve

On Jul 24, 2018, at 1:25 PM, Rick Aurbach via Groups.Io <rlaurb=me.com@groups.io> wrote:

Your comment about timing being an issue disturbs me since, ideally, the behavior should be consistent irrespective of "timing" since everything is funneled through a single thread. Assuming that one of your paths isn't accidentally setting imageView.image on a background thread, are you making any layout adjustments in -viewWillLayoutSubviews, for example? I have had problems where, ultimately, it was my fault but it took a long time to figure out that I was effectively trying to position something at the wrong time.
Yup. It disturbs me too.

But unless UIImagePickerController is doing something funky behind the scenes, then no, I am not doing anything on a background thread, nor am I explicitly making any changes to layout except for setting imageView.image and calling imageView.sizeToFit().

In fact, I’ve tried calling my loadData() method from
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.loadData()
}

and it doesn’t help.

What it “feels like” (whatever that means) is that there is some race condition or timing condition between code being executed in the UIImagePickerControllerDelegate’s done-handler and the layout/display subsystem for the imageView in the presenter’s view scene.

If you’re inclined to play with this, I put a sample app together yesterday afternoon. https://www.dropbox.com/s/avx6y6kkb9mpwhh/ImageViewScaling.zip?dl=0
Please note that Case #1 appears to work in the Sample App, but not in my real app. I’m currently using Case #3 in my real app. Case #2 fails in both places.

Cheers,

Rick


Rick Aurbach
 

Steve,

I know I’ve tried various things to force a re-layout, but obviously not that. 

Your solution worked in my app beautifully. Thank you so much for the suggestion.

I will follow up by filing a radar about the problem. I’ll suggest that they either fix the (presumed timing) bug or address the issue in documentation.

Thanks again.

Cheers,

Rick Aurbach


Rick Aurbach
 



Follow-up. Filed rdar:\\42735829
42735829

Image Scaling When Returning From UIImagePickerController

Created on July 30 2018, 12:01 PM for iOS + SDK
Close Bug

Comments

  • attached

Area:
UIKit

Summary:
I have a UIImageView in a scene and call UIImagePickerController to select an image to display in it. In the UIImagePickerControllerDelegate:didFinishPickingMediaWithInfo() function, I load the new image and call SizeToFit(). Depending on implementation details, the displayed image might be correctly sized to fit or may be displayed at full size (as if contentMode were topLeft).

Steps to Reproduce:
Run the enclosed test application. It includes three cases. In the test app, cases 1 and 3 behave as expected, case 2 shows the incorrect scaling. In may actual app, case 1 also shows incorrect scaling.

Expected Results:
I expect the image to be scaled to fit in all cases.

Actual Results:
In some cases, the image is scaled; in others it is not. Please note that opening the scene (when an image exists) ALWAYS displays it correctly. The loadData() method only fails when being called from UIImagePickerController's didFinish... method.

THIS LOOKS LIKE A TIMING ISSUE TO ME.

WORKAROUND: in LoadImage(), follow imageView.scaleToFit() with imageView.superView!.setNeedsLayout().

IF THIS IS DEEMED CORRECT BEHAVIOR, THEN YOU NEED TO UPDATE DOCUMENTATION.

Version/Build:
Xcode 9.4.1 and 10.0, iOS 10.3, 11.x, 12.0

Configuration:
Tested mainly on a real iPad Air 2 (11.4). Results in Simulator are inconclusive, particularly when using the standard images include in the default Camera Roll (which aren't big enough to show this problem clearly).

Richard Aurbach
July 30 2018, 12:01 PM
  • ImageViewScaling.zipattached
    107.7 KB


Cheers,

Rick Aurbach


Steve Christensen
 

I'm glad it worked for you.

One thing I noticed in your bug report is your mention of the *addition* of setNeedsLayout() to the existing sizeToFit(). I believe in the auto layout world that sizeToFit() isn't actually doing anything useful since it's manipulating the view frame directly, so just calling setNeedsLayout() alone should do the trick.

The layout pass that occurs automatically in two of your cases, or the one triggered explicitly by calling setNeedsLayout(), is causing the layout engine to calculate the new bounds based on the contraints, not on the image's actual size. You can see that in how the imageview size changes in viewDidLayoutSubviews() for cases 1 and 3.

Steve


On Jul 30, 2018, at 9:24 AM, Rick Aurbach via Groups.Io <rlaurb@...> wrote:

Steve,

I know I’ve tried various things to force a re-layout, but obviously not that. 

Your solution worked in my app beautifully. Thank you so much for the suggestion.

I will follow up by filing a radar about the problem. I’ll suggest that they either fix the (presumed timing) bug or address the issue in documentation.

Thanks again.

Cheers,

Rick Aurbach


Rick Aurbach
 

Steve,

I haven’t explored your argument in a test environment, but the reason I said the call to setNeedsLayout() was in ‘addition’ to the call to sizeToFit() is the following excerpt from the documentation of UIImageView (color enhancement is mine):

var image: UIImage?

The image displayed in the image view.

Declaration

var image: UIImage? { get set }

Discussion

This property contains the main image displayed by the image view. This image is displayed when the image view is in its natural state. When highlighted, the image view displays the image in its highlightedImage property instead. If that property is set to nil, the image view applies a default highlight to this image. If the animationImages property contains a valid set of images, those images are used instead.

Changing the image in this property does not automatically change the size of the image view. After setting the image, call the sizeToFit() method to recompute the image view’s size based on the new image and the active constraints. 

This property is set to the image you specified at initialization time. If you did not use the init(image:) or init(image:highlightedImage:) method to initialize your image view, the initial value of this property is nil.







So, at an absolute minimum, this is a documentation issue…

Cheers,

Rick


Steve Christensen
 

The docs for that section do reflect the behavior, which you can see where the imageView size was set to the image size pre-layout by calling sizeToFit(). The problem is that in the auto layout world, constraints have the last word in how views are ultimately sized unless one or both sides of a view's edge constraints is not set so the edge is floating.

In the better-behaved cases 1 and 3, you can see that happen at the pre- and post-layout points. Case 2 shows the same behavior once setNeedsLayout() was added so that a layout pass would be scheduled for a bit in the future outside of that weird timing-issue period.


On Jul 31, 2018, at 3:55 PM, Rick Aurbach via Groups.Io <rlaurb@...> wrote:

Steve,

I haven’t explored your argument in a test environment, but the reason I said the call to setNeedsLayout() was in ‘addition’ to the call to sizeToFit() is the following excerpt from the documentation of UIImageView (color enhancement is mine):

var image: UIImage?

The image displayed in the image view.

Declaration

var image: UIImage? { get set }

Discussion

This property contains the main image displayed by the image view. This image is displayed when the image view is in its natural state. When highlighted, the image view displays the image in its highlightedImage property instead. If that property is set to nil, the image view applies a default highlight to this image. If the animationImages property contains a valid set of images, those images are used instead.

Changing the image in this property does not automatically change the size of the image view. After setting the image, call the sizeToFit() method to recompute the image view’s size based on the new image and the active constraints. 

This property is set to the image you specified at initialization time. If you did not use the init(image:) or init(image:highlightedImage:) method to initialize your image view, the initial value of this property is nil.







So, at an absolute minimum, this is a documentation issue…

Cheers,

Rick