I've created a subclassed textfield view inside my view controller, with the font size to 15, the AdjustFontSizeToWidth enabled and the minimum font size to 10.
My textfield has a placeholder text too big to fit the view. The adjust font size to width is activated and the system is reducing the size of the font to 14. I don't exactly know why 14, because the placeholder still doesn't fit (see screenshot)
Any idea why is this happening ? Did i missed something ?
I subclassed UITextfield and override the following methods (Not sure it is relevant, but this seem to be methods who may cause this bugs) :
- (CGRect)leftViewRectForBounds:(CGRect)bounds {
CGRect leftViewRect = [super leftViewRectForBounds:bounds];
if ([self shouldShowLeftView] == YES) {
CGFloat newHeight = self.frame.size.height * 0.5f;
if (self.leftImage) {
leftViewRect.size.width = newHeight;
}
leftViewRect.origin.x = kMargins;
leftViewRect.origin.y = (self.frame.size.height - leftViewRect.size.height) / 2.0f;
}
return leftViewRect;
}
- (CGRect)textRectForBounds:(CGRect)bounds {
CGRect rectForBounds = [super textRectForBounds:bounds];
if ([self shouldShowLeftView] == YES) {
rectForBounds.origin.x += kMargins;
rectForBounds.size.width -= 2.0 * kMargins;
} else {
rectForBounds.origin.x += kMargins / 2.0;
rectForBounds.size.width -= 2.0 * (kMargins / 2.0);
}
return rectForBounds;
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
CGRect rectForBounds = [super editingRectForBounds:bounds];
if ([self shouldShowLeftView] == YES) {
rectForBounds.origin.x += kMargins;
rectForBounds.size.width -= 2.0 * kMargins;
} else {
rectForBounds.origin.x += kMargins / 2.0;
rectForBounds.size.width -= 2.0 * (kMargins / 2.0);
}
return rectForBounds;
}
EDIT: Update, I tried to use this solution
and adapted the code a bit to make it work for the placeholder also. The font resizing is fine, but the UI is messed up when the text is reduced, the text offset itself to the left, behind the left image view. On the screenshot, we can see that the right margin is too big and the text too much on the left.
You aren't missing anything - this is a quirk of iOS. minimumFontSize values less than 14 seem to be completely ignored as per my answer to a similar question.
Set your original font size to 300 and the UITextField will still be 14 pt, not 10 pt.
14 is an odd magic number; undocumented, but probably used to maintain readability. If only it could be lower.
Related
I have several subviews which are laid out based on the size of their super view. And I use auto layout here but the size of the super view is always 0, here is the code:
- (instancetype)initWithFrame:(CGRect)frame Count:(NSUInteger)count
{
self = [super initWithFrame:frame];
if (self)
{
self.count = count;
const float circleHeight = self.bounds.size.height * (float)4 / (5 * self.count - 1);
NSLog(#"selfHeight %f",self.bounds.size.height);
for (UIImageView * circle in self.circleViewArray)
{
[self addSubview:circle];
}
for (int i = 0;i < self.count;i++)
{
[self.circleViewArray[i] mas_makeConstraints:^(MASConstraintMaker * make){
make.width.equalTo(self);
make.height.equalTo(self).multipliedBy((float)4 / (5 * self.count - 1));
make.centerX.equalTo(self);
make.bottom.equalTo(self).with.offset(-i * (circleHeight + stickHeight));
}];
}
}
return self;
}
Note that here I use the third-party Masonry to simplify my code.
When I print the "selfHeight" in the console the output is always 0. How should I handle it?
So your offsets are never going to work because they're calculated against zero and never updated. You need to make the constraints relative somehow, or you need to remove and update the constraints each time the frame changes (the layout needs to be updated).
Making the constraints relative is better, which in your case you should look at linking the views together so the spacing between yhen is set and the heights resize to fit the full available height.
Pseudocode:
UIView *previousView = nil;
for (view in views) {
view.leading = super.leading;
view.trailing = super.trailing;
if (previousView) {
view.top = previousView.bottom;
view.height = previousView.height; // equal relation, not static height
} else {
view.top = super.top;
}
previousView = view;
}
previousView.bottom = super.bottom;
I'm new to iOS development and for my assignment, I'm tasked changing updating the ViewController programmatically when the device's orientation changes. I've found a snippet of an answer here, but it doesn't get the job done.
I tried adding this to the viewWillLayoutSubviews of my View Controller, but all I get is an unused variable warning.
CGRect rotatedFrame = [self.view convertRect:self.view.frame fromView:self.view.superview];
viewWillLayoutSubviews and rotation
As a "hint", I've been told it's simple to implement in viewWillLayoutSubviews. Going through and changing all the CGRects in my VC doesn't sound like a couple of lines of code. There's got to be a simpler, more efficient way to do this, but I've only found snippets of solutions digging around on this site. Thanks for reading.
The line of code you are using is assigning a CGRect to the rotatedFrame variable, it's not updating anything on your view controller.
There's many ways to approach this but it depends on what is contained in your view and how it's been configured. Things like Auto Layout for example could let you configure almost everything in Interface Builder and let you avoid doing most things in code.
You've been tasked to do this programatically and since we know that viewWillLayoutSubviews is called every time the device is rotated that's a good place to start. Here's a lazy way I've gone about rotating a video to fit a new orientation using a transformation:
//Vertical
CGSize size = self.view.frame.size;
someView.transform = CGAffineTransformMakeRotation((M_PI * (0) / 180.0))
someView.frame = CGRectMake(0, 0, MIN(size.width, size.height), MAX(size.width, size.height));
//Horizontal
CGSize size = [UIScreen mainScreen].bounds.size;
int directionModifier = ([UIDevice currentDevice].orientation == UIInterfaceOrientationLandscapeLeft) ? -1 : 1;
someView.bounds = CGRectMake(0, 0, MAX(size.width, size.height), MIN(size.width, size.height));
someView.transform = CGAffineTransformMakeRotation((M_PI * (90) / 180.0) *directionModifier);
someView.transform = CGAffineTransformTranslate(someView.transform,0,0);
How many subviews are in your view? Are they grouped? If you're using auto-resizing masks you might get away with only adjusting the frames of one or two views. If your root view has a number of subviews you can loop through views that need similar adjustments to avoid having to write excess code. It really depends on how everything has been set up.
I figured out how to determine the viewWidth and viewHeight and set those as CGFloats. I then added an if-else statement which figures out if the display is in portrait or landscape and sets the problematic calculateButton accordingly.
Apologies for lengthy code, but I've found in searching this site I find "snippets" of answers, but being new to iOS it's difficult to figure out what goes where. Hopefully, this helps someone later. (and hopefully it's correct)
- (void) viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
CGFloat viewWidth = self.view.bounds.size.width;
CGFloat viewHeight = self.view.bounds.size.height;
CGFloat padding = 20;
CGFloat itemWidth = viewWidth - padding - padding;
CGFloat itemHeight = 44;
// Bunch of setup code for layout items
// HOMEWORK: created if-else statement to deal w/ portrait vs. landscape placement of calculateButton.
if (viewWidth > viewHeight) {
// portrait
CGFloat bottomOfLabel = viewHeight;
self.calculateButton.frame = CGRectMake(padding, bottomOfLabel - itemHeight, itemWidth, itemHeight);
} else {
// landscape
CGFloat bottomOfLabel = CGRectGetMaxY(self.resultLabel.frame);
self.calculateButton.frame = CGRectMake(padding, bottomOfLabel + padding, itemWidth, itemHeight);
}
}
I can configure my UIViewController's edgesForExtendedLayout so that it will extend underneath content such as the navigation bar or tab bar. If I do this, is there some way to determine the frame that is not obscured?
As a possible alternative, is there a way for a UIViewController to determine the default contentInset to apply to a UIScrollView it contains?
Use case
I have zoomable UIScrollView containing an image.
When it is fully zoomed out I want to adjust the content inset too allow the content to stay centred (details here). However, my modified insets don't take in to account the insets that the UIViewController applies automatically so that its content isn't obscured by navigation bars, etc.
I also need to compute the minimum zoom for the content – that at which the whole image will be visible and not obscured. To compute this, I need to know the size of the unobscured part of the content view.
You need this
-(CGRect) unobscuredBounds
{
CGRect bounds = [self.view bounds];
return UIEdgeInsetsInsetRect(bounds, [self defaultContentInsets]);
}
-(UIEdgeInsets) defaultContentInsets
{
const CGFloat topOverlay = self.topLayoutGuide.length;
const CGFloat bottomOverlay = self.bottomLayoutGuide.length;
return UIEdgeInsetsMake(topOverlay, 0, bottomOverlay, 0);
}
You could put this in a category for easy reusability.
These methods correctly handle the changes that occur when the view resizes after a rotation – the change to the UINavigationBar size is correctly handled.
Centring Content
To use this to centre content by adjusting insets, you'd do something like this:
-(void) scrollViewDidZoom:(UIScrollView *)scrollView
{
[self centerContent];
}
- (void)centerContent
{
const CGSize contentSize = self.scrollView.contentSize;
const CGSize unobscuredBounds = [self unobscuredBounds].size;
const CGFloat left = MAX(0, (unobscuredBounds.width - contentSize.width)) * 0.5f;
const CGFloat top = MAX(0, (unobscuredBounds.height - contentSize.height)) * 0.5f;
self.scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
}
Your content insets will now reflect the default insets that they need (to avoid being covered up) and will also have the insets they need to be nicely centred.
Handling Rotation & Zoom
You probably also want to perform centring when animating between landscape and portrait. At the same time, you might want to adjust your minimum zoom scale so that your content will always fit. Try out something like this:
-(void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self centerContent];
const bool zoomIsAtMinimum = self.scrollView.zoomScale == self.scrollView.minimumZoomScale;
self.scrollView.minimumZoomScale = [self currentMinimumScale];
if(zoomIsAtMinimum)
{
self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
}
}
-(CGFloat) currentMinimumScale
{
const CGFloat currentScale = self.scrollView.zoomScale;
const CGSize scaledContentSize = self.scrollView.contentSize;
const CGSize scrollViewSize = [self unobscuredBounds].size;
CGFloat scaleToFitWidth = currentScale * scrollViewSize.width / scaledContentSize.width;
CGFloat scaleToFitHeight = currentScale * scrollViewSize.height / scaledContentSize.height;
return MIN(scaleToFitWidth, scaleToFitHeight);
}
The willAnimateRotationToInterfaceOrientation:… method is called within the view animation block, so the changes that it applies will lead to nice smooth animated changes as you switch from landscape to portrait.
I'm having a lot of trouble figuring out how best to reposition my UIScrollView's image view (I have a gallery kind of app going right now, similar to Photos.app, specifically when you're viewing a single image) when the orientation switches from portrait to landscape or vice-versa.
I know my best bet is to manipulate the contentOffset property, but I'm not sure what it should be changed to.
I've played around a lot, and it seems like for whatever reason 128 works really well. In my viewWillLayoutSubviews method for my view controller I have:
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.x >= 128) {
newContentOffset.x -= 128.0;
}
else {
newContentOffset.x = 0.0;
}
newContentOffset.y += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
else {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.y >= 128) {
newContentOffset.y -= 128.0;
}
else {
newContentOffset.y = 0.0;
}
newContentOffset.x += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
And it works pretty well. I hate how it's using a magic number though, and I have no idea where this would come from.
Also, whenever I zoom the image I have it set to stay centred (just like Photos.app does):
- (void)centerScrollViewContent {
// Keep image view centered as user zooms
CGRect newImageViewFrame = self.imageView.frame;
// Center horizontally
if (newImageViewFrame.size.width < CGRectGetWidth(self.scrollView.bounds)) {
newImageViewFrame.origin.x = (CGRectGetWidth(self.scrollView.bounds) - CGRectGetWidth(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.x = 0;
}
// Center vertically
if (newImageViewFrame.size.height < CGRectGetHeight(self.scrollView.bounds)) {
newImageViewFrame.origin.y = (CGRectGetHeight(self.scrollView.bounds) - CGRectGetHeight(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.y = 0;
}
self.imageView.frame = newImageViewFrame;
}
So I need it to keep it positioned properly so it doesn't show black borders around the image when repositioned. (That's what the checks in the first block of code are for.)
Basically, I'm curious how to implement functionality like in Photos.app, where on rotate the scrollview intelligently repositions the content so that the middle of the visible content before the rotation is the same post-rotation, so it feels continuous.
You should change the UIScrollView's contentOffset property whenever the scrollView is layouting its subviews after its bounds value has been changed. Then when the interface orientation will be changed, UIScrollView's bounds will be changed accordingly updating the contentOffset.
To make things "right" you should subclass UIScrollView and make all the adjustments there. This will also allow you to easily reuse your "special" scrollView.
The contentOffset calculation function should be placed inside UIScrollView's layoutSubviews method. The problem is that this method is called not only when the bounds value is changed but also when srollView is zoomed or scrolled. So the bounds value should be tracked to hint if the layoutSubviews method is called due to a change in bounds as a consequence of the orientation change, or due to a pan or pinch gesture.
So the first part of the UIScrollView subclass should look like this:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the prevBoundsSize to the initial bounds, so the first time
// layoutSubviews is called we won't do any contentOffset adjustments
self.prevBoundsSize = self.bounds.size;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGSizeEqualToSize(self.prevBoundsSize, self.bounds.size)) {
[self _adjustContentOffset];
self.prevBoundsSize = self.bounds.size;
}
[self _centerScrollViewContent];
}
Here, the layoutSubviews method is called every time the UIScrollView is panned, zoomed or its bounds are changed. The _centerScrollViewContent method is responsible for centering the zoomed view when its size becomes smaller than the size of the scrollView's bounds. And, it is called every time user pans or zooms the scrollView, or rotates the device. Its implementation is very similar to the implementation you provided in your question. The difference is that this method is written in the context of UIScrollView class and therefore instead of using self.imageView property to reference the zoomed view, which may not be available in the context of UIScrollView class, the viewForZoomingInScrollView: delegate method is used.
- (void)_centerScrollViewContent {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
CGRect frame = zoomView.frame;
if (self.contentSize.width < self.bounds.size.width) {
frame.origin.x = roundf((self.bounds.size.width - self.contentSize.width) / 2);
} else {
frame.origin.x = 0;
}
if (self.contentSize.height < self.bounds.size.height) {
frame.origin.y = roundf((self.bounds.size.height - self.contentSize.height) / 2);
} else {
frame.origin.y = 0;
}
zoomView.frame = frame;
}
}
But the more important thing here is the _adjustContentOffset method. This method is responsible for adjusting the contentOffset. Such that when UIScrollView's bounds value is changed the center point before the change will remain in center. And because of the condition statement, it is called only when UIScrollView's bounds is changed (e.g.: orientation change).
- (void)_adjustContentOffset {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
// Using contentOffset and bounds values before the bounds were changed (e.g.: interface orientation change),
// find the visible center point in the unscaled coordinate space of the zooming view.
CGPoint prevCenterPoint = (CGPoint){
.x = (self.prevContentOffset.x + roundf(self.prevBoundsSize.width / 2) - zoomView.frame.origin.x) / self.zoomScale,
.y = (self.prevContentOffset.y + roundf(self.prevBoundsSize.height / 2) - zoomView.frame.origin.y) / self.zoomScale,
};
// Here you can change zoomScale if required
// [self _changeZoomScaleIfNeeded];
// Calculate new contentOffset using the previously calculated center point and the new contentOffset and bounds values.
CGPoint contentOffset = CGPointMake(0.0, 0.0);
CGRect frame = zoomView.frame;
if (self.contentSize.width > self.bounds.size.width) {
frame.origin.x = 0;
contentOffset.x = prevCenterPoint.x * self.zoomScale - roundf(self.bounds.size.width / 2);
if (contentOffset.x < 0) {
contentOffset.x = 0;
} else if (contentOffset.x > self.contentSize.width - self.bounds.size.width) {
contentOffset.x = self.contentSize.width - self.bounds.size.width;
}
}
if (self.contentSize.height > self.bounds.size.height) {
frame.origin.y = 0;
contentOffset.y = prevCenterPoint.y * self.zoomScale - roundf(self.bounds.size.height / 2);
if (contentOffset.y < 0) {
contentOffset.y = 0;
} else if (contentOffset.y > self.contentSize.height - self.bounds.size.height) {
contentOffset.y = self.contentSize.height - self.bounds.size.height;
}
}
zoomView.frame = frame;
self.contentOffset = contentOffset;
}
}
Bonus
I've created a working SMScrollView class (here is link to GitHub) implementing the above behavior and additional bonuses:
You can notice that in Photos app, zooming a photo, then scrolling it to one of its boundaries and then rotating the device does not keep the center point in its place. Instead it sticks the scrollView to that boundary. And if you scroll to one of the corners and then rotate, the scrollView will be stick to that corner as well.
In addition to adjusting contentOffset you may find that you also want to adjust the scrollView's zoomScale. For example, assume you are viewing a photo in portrait mode that is scaled to fit the screen size. Then when you rotate the device to the landscape mode you may want to upscale the photo to take advantage of the available space.
I'm using iCarousel to have a scroll of images. THe code is:
- (CATransform3D)carousel:(iCarousel *)_carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform
{
CGFloat count = 5;
CGFloat spacing = 0.9f;
CGFloat arc = M_PI * 0.3f;
CGFloat radius = fmaxf(140.0 * spacing / 2.0f, 140.0 * spacing / 2.0f / tanf(arc/2.0f/count));
CGFloat angle = offset / count * arc;
radius = -radius;
angle = -angle;
transform = CATransform3DTranslate(transform, radius * sin(angle),radius * cos(angle) - radius, 0.0f);
return transform;
}
But when I scroll the images there is an ugly effect, the transition is not smooth and the images comes out in a jerky way, but I'd like to come out smootly. Can you help me? Thanks.
Edit: The problem is that when I scroll the images the transition is not smooth and the images comes out in front of the images on the back with a detachment from the other images. Pratically, the images comes in front of the others only when the scrolling did end and this causes a bad effect.
We had a similar issue in our last implementation of iCarousel. Here's how we fixed it:
- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value {
if (option == iCarouselOptionSpacing) {
return value;// * 1.05f;
} else if(option == iCarouselOptionWrap) {
return NO;
} else if(option == iCarouselOptionVisibleItems) {
return 3;
}
return value;
}
Specifically, what you're probably needing is that last else if statement where we specify the number of visible items. It defaults to 1 which means that as you swipe through the new images come out in a jerky way. By specifying 3, you guarantee that the most previous item, the current item, and the next item are always loaded in memory so scrolling between them is always smooth. If this doesn't solve your problem, increase the number 3 to whatever works.
Also, don't forget to set the delegate of the iCarousel to self.
Best of luck.