Can anyone point me how could I achieve such design in the UITabbar. I have tried adding the back-ground Image, but that does not look like the design. Here the curve is extended beyond the frame of UITabbar, not sure how to add this views on top of active tabbar.
Creating a custom TabBar from UITabBarController can be solved the problem. Instead of adding a direct image to the Tabbar, use an on the fly image using UIGraphicsBeginImageContext for selectedTabBackgroundImage.
Create the image.
Clip the top part round in the image
Here is the example of the code.
import UIKit
class CustomTabBarViewController: UITabBarController {
var topClipSize: CGFloat = 24.5 //Adjust based on the number of tabbar
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let singleTabWidth: CGFloat = self.tabBar.frame.size.width / CGFloat((self.tabBar.items?.count)!)
let singleTabSize = CGSize(width:singleTabWidth , height: self.tabBar.frame.size.height)
// Create the backgound image
let selectedTabBackgroundImage: UIImage = self.imageWithColor(color: .blue, size: singleTabSize)
// Clip the top
self.tabBar.selectionIndicatorImage = selectedTabBackgroundImage.roundTopImage(topClipSize: topClipSize)
}
func imageWithColor(color: UIColor, size: CGSize) -> UIImage {
if size.height > 55 {
topClipSize = 30.0 // iPhone 8 tabbar height is 53 and iPnone X is 83 - We need more space on top.
}
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height + topClipSize)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor)
context!.fill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
extension UIImage {
func roundTopImage(topClipSize: CGFloat) -> UIImage {
let rect = CGRect(origin:CGPoint(x: 0, y: 0), size: self.size)
let rectBounds: CGRect = CGRect(x: rect.origin.x, y: rect.origin.y + (topClipSize * 2), width: rect.size.width, height: rect.size.height - (topClipSize * 2))
let ovalBounds: CGRect = CGRect(x: rect.origin.x - topClipSize, y: rect.origin.y, width: rect.size.width + (topClipSize * 2), height: rect.size.height)
UIGraphicsBeginImageContextWithOptions(self.size, false, 1)
let rectPath = UIBezierPath(rect: rectBounds)
let ovalPath = UIBezierPath(ovalIn: ovalBounds)
rectPath.append(ovalPath)
rectPath.addClip()
self.draw(in: rect)
return UIGraphicsGetImageFromCurrentImageContext()!
}
}
Here is the output:
It's not really possible to elegantly change the native UITabBar's appearance to that extent. Your options are to create a custom container view controller that acts like a UITabBarController, or just hide the default tab bar and implement your own view in that space.
Even though it's less elegant because you'd be just throwing a view on top of the default tab bar, I actually like that method because you retain the benefits of the native UITabBarController (calling self.tabBarController? from its view controllers, it already adjusts layout margins, etc).
To do this, in your subclass of UITabBarController hide the tabBar:
self.tabBar.isHidden = true
self.tabBar.alpha = 0
Then after implementing your custom view however you want, just set the frame of your custom view to self.tabBar.frame in viewDidLayoutSubviews.
For changing viewControllers, call this when the user taps one of your custom tabs:
self.selectedIndex = newIndex
I would like to inset the text of a UITextField.
Is this possible?
Overriding -textRectForBounds: will only change the inset of the placeholder text. To change the inset of the editable text, you need to also override -editingRectForBounds:
// placeholder position
- (CGRect)textRectForBounds:(CGRect)bounds {
return CGRectInset(bounds, 10, 10);
}
// text position
- (CGRect)editingRectForBounds:(CGRect)bounds {
return CGRectInset(bounds, 10, 10);
}
I was able to do it through:
myTextField.layer.sublayerTransform = CATransform3DMakeTranslation(5, 0, 0);
Of course remember to import QuartzCore and also add the Framework to your project.
If you need just a left margin, you can try this:
UItextField *textField = [[UITextField alloc] initWithFrame:...];
UIView *leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, textField.frame.size.height)];
leftView.backgroundColor = textField.backgroundColor;
textField.leftView = leftView;
textField.leftViewMode = UITextFieldViewModeAlways;
It works for me. I hope this may help.
In a class derived from UITextField, override at least this two methods:
- (CGRect)textRectForBounds:(CGRect)bounds;
- (CGRect)editingRectForBounds:(CGRect)bounds;
It might be as simple as this if you have no additional content:
return CGRectInset(bounds , 10, 10);
UITextField provides several positioning methods you can override.
How about an #IBInspectable, #IBDesignable swift class.
#IBDesignable
class TextField: UITextField {
#IBInspectable var insetX: CGFloat = 6 {
didSet {
layoutIfNeeded()
}
}
#IBInspectable var insetY: CGFloat = 6 {
didSet {
layoutIfNeeded()
}
}
// placeholder position
override func textRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds , insetX , insetY)
}
// text position
override func editingRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds , insetX , insetY)
}
}
You'll see this in your storyboard.
Update - Swift 3
#IBDesignable
class TextField: UITextField {
#IBInspectable var insetX: CGFloat = 0
#IBInspectable var insetY: CGFloat = 0
// placeholder position
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: insetX, dy: insetY)
}
// text position
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: insetX, dy: insetY)
}
}
If you have a clear button, the accepted answer won't work for you. We should also guard against Apple changing things in the future by calling super.
So, to make sure the text doesn't overlap the clear button, let's get the 'default' value from super first, then adjust as necessary.
This code will add a 10px insets on the top, left and bottom of the text field:
#interface InsetTextField : UITextField
#end
#implementation InsetTextField
// Placeholder position
- (CGRect)textRectForBounds:(CGRect)bounds {
CGRect rect = [super textRectForBounds:bounds];
UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 10, 0);
return UIEdgeInsetsInsetRect(rect, insets);
}
// Text position
- (CGRect)editingRectForBounds:(CGRect)bounds {
CGRect rect = [super editingRectForBounds:bounds];
UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 10, 0);
return UIEdgeInsetsInsetRect(rect, insets);
}
// Clear button position
- (CGRect)clearButtonRectForBounds:(CGRect)bounds {
CGRect rect = [super clearButtonRectForBounds:bounds];
return CGRectOffset(rect, -5, 0);
}
#end
Note: UIEdgeInsetsMake takes parameters in the order: top, left, bottom, right.
Thought I would supply a Swift Solution
import UIKit
class TextField: UITextField {
let inset: CGFloat = 10
// placeholder position
override func textRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds , inset , inset)
}
// text position
override func editingRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds , inset , inset)
}
override func placeholderRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds, inset, inset)
}
}
Swift 3+
import UIKit
class TextField: UITextField {
let inset: CGFloat = 10
// placeholder position
override func textRect(forBounds: CGRect) -> CGRect {
return forBounds.insetBy(dx: self.inset , dy: self.inset)
}
// text position
override func editingRect(forBounds: CGRect) -> CGRect {
return forBounds.insetBy(dx: self.inset , dy: self.inset)
}
override func placeholderRect(forBounds: CGRect) -> CGRect {
return forBounds.insetBy(dx: self.inset, dy: self.inset)
}
}
Swift
class TextField: UITextField {
let inset: CGFloat = 8
// placeholder position
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: inset)
}
// text position
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: inset)
}
}
Using textRectForBounds: is the correct approach. I have wrapped this up in my subclass so you can simply use textEdgeInsets. See SSTextField.
You can set text inset for UITextField by setting the leftView.
Like this:
UITextField *yourTextField = [[UITextField alloc] init];
UIView *leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 5, 5)];
leftView.backgroundColor = [UIColor clearColor];
yourTextField.leftViewMode = UITextFieldViewModeAlways;
yourTextField.leftView = leftView;
Swift
// adjust place holder text
let paddingView = UIView(frame: CGRectMake(0, 0, 10, usernameOrEmailField.frame.height))
usernameOrEmailField.leftView = paddingView
usernameOrEmailField.leftViewMode = UITextFieldViewMode.Always
For people, who are searching for a easier solution.
Add the UITextField inside a UIView. To simulate an inset around text field I keep 10 px left and width is 20px less than the view. For a rounded corner border around the textfield, use the view's border
viewBG.layer.cornerRadius = 8.0;
viewBG.layer.borderColor = [UIColor darkGrayColor].CGColor;
viewBG.layer.borderWidth = 1.0;
A good approach to add padding to UITextField is to subclass UITextField and add an edgeInsets property. You then set the edgeInsets and the UITextField will be drawn accordingly. This will also function correctly with a custom leftView or rightView set.
OSTextField.h
#import <UIKit/UIKit.h>
#interface OSTextField : UITextField
#property (nonatomic, assign) UIEdgeInsets edgeInsets;
#end
OSTextField.m
#import "OSTextField.h"
#implementation OSTextField
- (id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.edgeInsets = UIEdgeInsetsMake(0, 0, 0, 0);
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if(self){
self.edgeInsets = UIEdgeInsetsMake(0, 0, 0, 0);
}
return self;
}
- (CGRect)textRectForBounds:(CGRect)bounds {
return [super textRectForBounds:UIEdgeInsetsInsetRect(bounds, self.edgeInsets)];
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
return [super editingRectForBounds:UIEdgeInsetsInsetRect(bounds, self.edgeInsets)];
}
#end
Swift 5 version of Christopher's answer with extra usage sample
import UIKit
private class InsetTextField: UITextField {
var insets: UIEdgeInsets
init(insets: UIEdgeInsets) {
self.insets = insets
super.init(frame: .zero)
}
required init(coder aDecoder: NSCoder) {
fatalError("not intended for use from a NIB")
}
// placeholder position
override func textRect(forBounds bounds: CGRect) -> CGRect {
return super.textRect(forBounds: bounds.inset(by: insets))
}
// text position
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return super.editingRect(forBounds: bounds.inset(by: insets))
}
}
extension UITextField {
class func textFieldWithInsets(insets: UIEdgeInsets) -> UITextField {
return InsetTextField(insets: insets)
}
}
Usage: -
class ViewController: UIViewController {
private let passwordTextField: UITextField = {
let textField = UITextField.textFieldWithInsets(insets: UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15))
// ---
return textField
}()
}
I did this in IB where I created a UIView Behind the textView that was a little bit longer. With the textField background color set to clear.
Swift 3 / Designable in Interface builder / Separate horizontal & vertical insects / usable out of the box
#IBDesignable
class TextFieldWithPadding: UITextField {
#IBInspectable var horizontalInset: CGFloat = 0
#IBInspectable var verticalInset: CGFloat = 0
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: horizontalInset, dy: verticalInset)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: horizontalInset , dy: verticalInset)
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: horizontalInset, dy: verticalInset)
}
}
usage:
&
Swift 4.2 version:
import UIKit
class InsetTextField: UITextField {
let inset: CGFloat = 10
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: inset)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: inset)
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: inset, dy: inset)
}
}
It's the quickest way I've found without doing any subclasses:
UIView *spacerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10., 10.)];
[textField setLeftViewMode:UITextFieldViewModeAlways];
[textField setLeftView:spacerView];
In Swift:
let spacerView = UIView(frame:CGRect(x:0, y:0, width:10, height:10))
textField.leftViewMode = UITextFieldViewMode.Always
textField.leftView = spacerView
It's absurd you have to subclass, since UITextField already implements the methods, as #Adam Waite points out. Here's a swift extension that exposes a factory method, also available in our categories repo:
private class InsetTextField: UITextField {
var insets: UIEdgeInsets
init(insets: UIEdgeInsets) {
self.insets = insets
super.init(frame: CGRectZero)
}
required init(coder aDecoder: NSCoder) {
fatalError("not intended for use from a NIB")
}
// placeholder position
override func textRectForBounds(bounds: CGRect) -> CGRect {
return super.textRectForBounds(UIEdgeInsetsInsetRect(bounds, insets))
}
// text position
override func editingRectForBounds(bounds: CGRect) -> CGRect {
return super.editingRectForBounds(UIEdgeInsetsInsetRect(bounds, insets))
}
}
extension UITextField {
class func textFieldWithInsets(insets: UIEdgeInsets) -> UITextField {
return InsetTextField(insets: insets)
}
}
Here is the same subclassed UITextField written in Swift 3. It is quite different from prior versions of Swift, as you'll see:
import UIKit
class MyTextField: UITextField
{
let inset: CGFloat = 10
// placeholder position
override func textRect(forBounds bounds: CGRect) -> CGRect
{
return bounds.insetBy(dx: inset, dy: inset)
}
// text position
override func editingRect(forBounds bounds: CGRect) -> CGRect
{
return bounds.insetBy(dx: inset, dy: inset)
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect
{
return bounds.insetBy(dx: inset, dy: inset)
}
}
Incidentally, you can also do something like the following, if you want to control the inset of just one side. This particular example of adjusting only the left inset comes in handy if you place an image on top of the UITextField but you want it to appear to the user to be within the text field:
override func editingRect(forBounds bounds: CGRect) -> CGRect
{
return CGRect.init(x: bounds.origin.x + inset, y: bounds.origin.y, width: bounds.width - inset, height: bounds.height)
}
You can adjust the positioning of the text within a text field by making it a subclass of UITextField and overriding the -textRectForBounds: method.
I subclased UITextField to handle this that supports left, top, right and bottom inset, and clear button positioning as well.
MRDInsetTextField.h
#import <UIKit/UIKit.h>
#interface MRDInsetTextField : UITextField
#property (nonatomic, assign) CGRect inset;
#end
MRDInsetTextField.m
#import "MRDInsetTextField.h"
#implementation MRDInsetTextField
- (id)init
{
self = [super init];
if (self) {
_inset = CGRectZero;
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
_inset = CGRectZero;
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_inset = CGRectZero;
}
return self;
}
- (void)setInset:(CGRect)inset {
_inset = inset;
[self setNeedsLayout];
}
- (CGRect)getRectForBounds:(CGRect)bounds withInset:(CGRect)inset {
CGRect newRect = CGRectMake(
bounds.origin.x + inset.origin.x,
bounds.origin.y + inset.origin.y,
bounds.origin.x + bounds.size.width - inset.origin.x - inset.size.width,
bounds.origin.y + bounds.size.height - inset.origin.y - inset.size.height
);
return newRect;
}
- (CGRect)textRectForBounds:(CGRect)bounds {
return [self getRectForBounds:[super textRectForBounds:bounds] withInset:_inset];
}
- (CGRect)placeholderRectForBounds:(CGRect)bounds {
return [self getRectForBounds:bounds withInset:_inset];
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
return [self getRectForBounds:[super editingRectForBounds:bounds] withInset:_inset];
}
- (CGRect)clearButtonRectForBounds:(CGRect)bounds {
return CGRectOffset([super clearButtonRectForBounds:bounds], -_inset.size.width, _inset.origin.y/2 - _inset.size.height/2);
}
#end
Example of usage where *_someTextField* comes from nib/storyboard view with MRDInsetTextField custom class
[(MRDInsetTextField*)_someTextField setInset:CGRectMake(5, 0, 5, 0)]; // left, top, right, bottom inset
This is not as short as the other examples, but takes an entirely different approach to solving this problem. Note, the caret will still begin flush to the left edge but the text will be properly indented when typed/displayed. This works without subclassing if your looking for just a left margin and you are already using UITextFieldDelegate for your text fields. You need to set both the default text attributes and the typing attributes. You set the default text attributes when you create the text field. The typing attributes you need to set in the delegate. If you are also using a placeholder you will want to set that to the same margin as well. Putting it altogether you get something like this.
First create a category on the UITextField class.
// UITextField+TextAttributes.h
#import <UIKit/UIKit.h>
#interface UITextField (TextAttributes)
- (void)setIndent:(CGFloat)indent;
#end
// UITextField+TextAttributes.m
#import "UITextField+TextAttributes.h"
#implementation UITextField (TextAttributes)
- (void)setTextAttributes:(NSDictionary*)textAttributes indent:(CGFloat)indent
{
if (!textAttributes) return;
NSMutableParagraphStyle *paragraphStyle = [textAttributes objectForKey:NSParagraphStyleAttributeName];
paragraphStyle.firstLineHeadIndent = indent;
paragraphStyle.headIndent = indent;
}
- (void)setIndent:(CGFloat)indent
{
[self setTextAttributes:self.defaultTextAttributes indent:indent];
[self setTextAttributes:self.typingAttributes indent:indent];
}
#end
Then, if you are using placed holders make sure to use an attributed placeholder setting the same indent. Create a default attributed dictionary with the proper attributes, something like this:
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.firstLineHeadIndent = 7;
paragraphStyle.headIndent = 7;
NSDictionary *placeholderAttributes = [NSDictionary dictionaryWithObjectsAndKeys: paragraphStyle, NSParagraphStyleAttributeName, nil];
Then, import the above category and whenever you create a text field set the default indent, the delegate and use the default placeholder attributes defined above. For example:
UITextField *textField = [[UITextField alloc] init];
textField.indent = 7;
textField.delegate = self;
textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:#"Placeholder Text" attributes:placeholderAttributes];
Lastly, in the delegate, implement the textFieldDidBeginEditing method, something like this:
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
textField.indent = 7;
}
A solution that actually works and covers all cases:
Should use offsetBy not insetBy.
Should also call the super function to get the original Rect.
Bounds is faulty. you need to offset the original X, Y. Bounds have X, Y as zeros.
Original x, y can be non-zero for instance when setting the leftView of the UITextField.
Sample:
override func textRect(forBounds bounds: CGRect) -> CGRect {
return super.textRect(forBounds: bounds).offsetBy(dx: 0.0, dy: 4)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return super.editingRect(forBounds: bounds).offsetBy(dx: 0.0, dy: 4)
}
I found the option posted by roberto.buratti the fastest solution, here it is in Swift:
let leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: textField.frame.size.height))
leftView.backgroundColor = textField.backgroundColor
textField.leftView = leftView
textField.leftViewMode = UITextField.ViewMode.always
I normally try avoid subclassing but this works if you have already:
// add a property
#property (nonatomic) UIEdgeInsets edgeInsets;
// and override:
- (CGRect)textRectForBounds:(CGRect)bounds
{
return [super textRectForBounds:UIEdgeInsetsInsetRect(bounds, self.edgeInsets)];
}
- (CGRect)editingRectForBounds:(CGRect)bounds
{
return [super editingRectForBounds:UIEdgeInsetsInsetRect(bounds, self.edgeInsets)];
}
To throw in another solution that has no need for subclassing:
UITextField *txtField = [UITextField new];
txtField.borderStyle = UITextBorderStyleRoundedRect;
// grab BG layer
CALayer *bgLayer = txtField.layer.sublayers.lastObject;
bgLayer.opacity = 0.f;
// add new bg view
UIView *bgView = [UIView new];
bgView.backgroundColor = [UIColor whiteColor];
bgView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
bgView.userInteractionEnabled = NO;
[txtField addSubview: bgView];
[txtField sendSubviewToBack: bgView];
Tested with iOS 7 and iOS 8. Both working. Still there might be the chance of Apple modifying the UITextField's layer hierarchy screwing up things badly.
Here's a comprehensive Swift answer that includes a leftView (custom icon) and a custom clear button, both set in Interface Builder with customizable insets.
import UIKit
#IBDesignable
class InsetTextField: UITextField {
#IBInspectable var leftInset:CGFloat = 0
#IBInspectable var rightInset:CGFloat = 0
#IBInspectable var icon:UIImage? { didSet {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 16, height: 16))
imageView.image = icon
self.leftView = imageView
self.leftViewMode = .Always
} }
#IBInspectable var clearButton:UIImage? { didSet {
let button = UIButton(type: .Custom)
button.setImage(clearButton, forState: .Normal)
button.addTarget(self, action: "clear", forControlEvents: UIControlEvents.TouchUpInside)
button.frame = CGRect(x: 0, y: 0, width: 18, height: 18)
self.rightView = button
self.rightViewMode = .WhileEditing
} }
func clear() {
self.text = ""
}
override func leftViewRectForBounds(bounds: CGRect) -> CGRect {
var height:CGFloat = 0
var width:CGFloat = 0
if let leftView = self.leftView {
height = leftView.bounds.height
width = leftView.bounds.width
}
return CGRect(x: leftInset, y: bounds.height/2 - height/2, width: width, height: height)
}
override func rightViewRectForBounds(bounds: CGRect) -> CGRect {
var height:CGFloat = 0
var width:CGFloat = 0
if let rightView = self.rightView {
height = rightView.bounds.height
width = rightView.bounds.width
}
return CGRect(x: bounds.width - width - rightInset, y: bounds.height/2 - height/2, width: width, height: height)
}
}
You might need this solution which also supports leftView and rightView. 😃
class InsettedTextField: UITextField {
private let textInset: UIEdgeInsets
var rightViewInset: CGRect {
rightView.flatMap { $0.frame } ?? .zero
}
var leftViewInset: CGRect {
leftView.flatMap { $0.frame } ?? .zero
}
/// Init the text field with insets.
init(textInset: UIEdgeInsets) {
self.textInset = textInset
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func textRect(forBounds bounds: CGRect) -> CGRect {
bounds
.inset(by: textInset)
.inset(by: UIEdgeInsets(top: 0, left: leftViewInset.width, bottom: 0, right: rightViewInset.width))
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
bounds
.inset(by: textInset)
.inset(by: UIEdgeInsets(top: 0, left: leftViewInset.width, bottom: 0, right: rightViewInset.width))
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
bounds
.inset(by: textInset)
.inset(by: UIEdgeInsets(top: 0, left: leftViewInset.width, bottom: 0, right: rightViewInset.width))
}
}
If you want to change TOP and LEFT indent only then
// placeholder position
- (CGRect)textRectForBounds:(CGRect)bounds {
CGRect frame = bounds;
frame.origin.y = 3;
frame.origin.x = 5;
bounds = frame;
return CGRectInset( bounds , 0 , 0 );
}
// text position
- (CGRect)editingRectForBounds:(CGRect)bounds {
CGRect frame = bounds;
frame.origin.y = 3;
frame.origin.x = 5;
bounds = frame;
return CGRectInset( bounds , 0 , 0 );
}
Is there a way to make a UIScrollView auto-adjust to the height (or width) of the content it's scrolling?
Something like:
[scrollView setContentSize:(CGSizeMake(320, content.height))];
The best method I've ever come across to update the content size of a UIScrollView based on its contained subviews:
Objective-C
CGRect contentRect = CGRectZero;
for (UIView *view in self.scrollView.subviews) {
contentRect = CGRectUnion(contentRect, view.frame);
}
self.scrollView.contentSize = contentRect.size;
Swift
let contentRect: CGRect = scrollView.subviews.reduce(into: .zero) { rect, view in
rect = rect.union(view.frame)
}
scrollView.contentSize = contentRect.size
UIScrollView doesn't know the height of its content automatically. You must calculate the height and width for yourself
Do it with something like
CGFloat scrollViewHeight = 0.0f;
for (UIView* view in scrollView.subviews)
{
scrollViewHeight += view.frame.size.height;
}
[scrollView setContentSize:(CGSizeMake(320, scrollViewHeight))];
But this only work if the views are one below the other. If you have a view next to each other you only have to add the height of one if you don't want to set the content of the scroller larger than it really is.
Solution if you're using auto layout:
Set translatesAutoresizingMaskIntoConstraints to NO on all views involved.
Position and size your scroll view with constraints external to the scroll view.
Use constraints to lay out the subviews within the scroll view, being sure that the constraints tie to all four edges of the scroll view and do not rely on the scroll view to get their size.
Source:
https://developer.apple.com/library/ios/technotes/tn2154/_index.html
I added this to Espuz and JCC's answer. It uses the y position of the subviews and doesn't include the scroll bars. Edit Uses the bottom of the lowest sub view that is visible.
+ (CGFloat) bottomOfLowestContent:(UIView*) view
{
CGFloat lowestPoint = 0.0;
BOOL restoreHorizontal = NO;
BOOL restoreVertical = NO;
if ([view respondsToSelector:#selector(setShowsHorizontalScrollIndicator:)] && [view respondsToSelector:#selector(setShowsVerticalScrollIndicator:)])
{
if ([(UIScrollView*)view showsHorizontalScrollIndicator])
{
restoreHorizontal = YES;
[(UIScrollView*)view setShowsHorizontalScrollIndicator:NO];
}
if ([(UIScrollView*)view showsVerticalScrollIndicator])
{
restoreVertical = YES;
[(UIScrollView*)view setShowsVerticalScrollIndicator:NO];
}
}
for (UIView *subView in view.subviews)
{
if (!subView.hidden)
{
CGFloat maxY = CGRectGetMaxY(subView.frame);
if (maxY > lowestPoint)
{
lowestPoint = maxY;
}
}
}
if ([view respondsToSelector:#selector(setShowsHorizontalScrollIndicator:)] && [view respondsToSelector:#selector(setShowsVerticalScrollIndicator:)])
{
if (restoreHorizontal)
{
[(UIScrollView*)view setShowsHorizontalScrollIndicator:YES];
}
if (restoreVertical)
{
[(UIScrollView*)view setShowsVerticalScrollIndicator:YES];
}
}
return lowestPoint;
}
Here is the accepted answer in swift for anyone who is too lazy to convert it :)
var contentRect = CGRectZero
for view in self.scrollView.subviews {
contentRect = CGRectUnion(contentRect, view.frame)
}
self.scrollView.contentSize = contentRect.size
Here's a Swift 3 adaptation of #leviatan's answer :
EXTENSION
import UIKit
extension UIScrollView {
func resizeScrollViewContentSize() {
var contentRect = CGRect.zero
for view in self.subviews {
contentRect = contentRect.union(view.frame)
}
self.contentSize = contentRect.size
}
}
USAGE
scrollView.resizeScrollViewContentSize()
Very easy to use !
Following extension would be helpful in Swift.
extension UIScrollView{
func setContentViewSize(offset:CGFloat = 0.0) {
// dont show scroll indicators
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
var maxHeight : CGFloat = 0
for view in subviews {
if view.isHidden {
continue
}
let newHeight = view.frame.origin.y + view.frame.height
if newHeight > maxHeight {
maxHeight = newHeight
}
}
// set content size
contentSize = CGSize(width: contentSize.width, height: maxHeight + offset)
// show scroll indicators
showsHorizontalScrollIndicator = true
showsVerticalScrollIndicator = true
}
}
Logic is the same with the given answers. However, It omits hidden views within UIScrollView and calculation is performed after scroll indicators set hidden.
Also, there is an optional function parameter and you're able to add an offset value by passing parameter to function.
Great & best solution from #leviathan. Just translating to swift using FP (functional programming) approach.
self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect(), {
CGRectUnion($0, $1.frame)
}.size
You can get height of the content inside UIScrollView by calculate which child "reaches furthers". To calculate this you have to take in consideration origin Y (start) and item height.
float maxHeight = 0;
for (UIView *child in scrollView.subviews) {
float childHeight = child.frame.origin.y + child.frame.size.height;
//if child spans more than current maxHeight then make it a new maxHeight
if (childHeight > maxHeight)
maxHeight = childHeight;
}
//set content size
[scrollView setContentSize:(CGSizeMake(320, maxHeight))];
By doing things this way items (subviews) don't have to be stacked directly one under another.
I came up with another solution based on #emenegro's solution
NSInteger maxY = 0;
for (UIView* subview in scrollView.subviews)
{
if (CGRectGetMaxY(subview.frame) > maxY)
{
maxY = CGRectGetMaxY(subview.frame);
}
}
maxY += 10;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, maxY)];
Basically, we figure out which element is furthest down in the view and adds a 10px padding to the bottom
Or just do:
int y = CGRectGetMaxY(((UIView*)[_scrollView.subviews lastObject]).frame); [_scrollView setContentSize:(CGSizeMake(CGRectGetWidth(_scrollView.frame), y))];
(This solution was added by me as a comment in this page. After getting 19 up-votes for this comment, I've decided to add this solution as a formal answer for the benefit of the community!)
Because a scrollView can have other scrollViews or different inDepth subViews tree, run in depth recursively is preferable.
Swift 2
extension UIScrollView {
//it will block the mainThread
func recalculateVerticalContentSize_synchronous () {
let unionCalculatedTotalRect = recursiveUnionInDepthFor(self)
self.contentSize = CGRectMake(0, 0, self.frame.width, unionCalculatedTotalRect.height).size;
}
private func recursiveUnionInDepthFor (view: UIView) -> CGRect {
var totalRect = CGRectZero
//calculate recursevly for every subView
for subView in view.subviews {
totalRect = CGRectUnion(totalRect, recursiveUnionInDepthFor(subView))
}
//return the totalCalculated for all in depth subViews.
return CGRectUnion(totalRect, view.frame)
}
}
Usage
scrollView.recalculateVerticalContentSize_synchronous()
For swift4 using reduce:
self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect.zero, {
return $0.union($1.frame)
}).size
The size depends on the content loaded inside of it, and the clipping options. If its a textview, then it also depends on the wrapping, how many lines of text, the font size, and so on and on. Nearly impossible for you to compute yourself. The good news is, it is computed after the view is loaded and in the viewWillAppear. Before that, it's all unknown and and content size will be the same as frame size. But, in the viewWillAppear method and after (such as the viewDidAppear) the content size will be the actual.
Wrapping Richy's code I created a custom UIScrollView class that automates
content resizing completely!
SBScrollView.h
#interface SBScrollView : UIScrollView
#end
SBScrollView.m:
#implementation SBScrollView
- (void) layoutSubviews
{
CGFloat scrollViewHeight = 0.0f;
self.showsHorizontalScrollIndicator = NO;
self.showsVerticalScrollIndicator = NO;
for (UIView* view in self.subviews)
{
if (!view.hidden)
{
CGFloat y = view.frame.origin.y;
CGFloat h = view.frame.size.height;
if (y + h > scrollViewHeight)
{
scrollViewHeight = h + y;
}
}
}
self.showsHorizontalScrollIndicator = YES;
self.showsVerticalScrollIndicator = YES;
[self setContentSize:(CGSizeMake(self.frame.size.width, scrollViewHeight))];
}
#end
How to use:
Simply import the .h file to your view controller and
declare a SBScrollView instance instead of the normal UIScrollView one.
why not single line of code??
_yourScrollView.contentSize = CGSizeMake(0, _lastView.frame.origin.y + _lastView.frame.size.height);
I created a subclass of ScrollView to handle the intrinsicContentSize and it worked perfectly for me
public final class ContentSizedScrollView: UIScrollView {
override public var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override public var intrinsicContentSize: CGSize {
layoutIfNeeded()
return self.contentSize
}
}
Now you can create a scrollview with this class and set constraints on all sides
Make sure that the subviews are tied up to all fours edges of the scrollView
it depends on the content really : content.frame.height might give you what you want ? Depends if content is a single thing, or a collection of things.
I also found leviathan's answer to work the best. However, it was calculating a strange height. When looping through the subviews, if the scrollview is set to show scroll indicators, those will be in the array of subviews. In this case, the solution is to temporarily disable the scroll indicators before looping, then re-establish their previous visibility setting.
-(void)adjustContentSizeToFit is a public method on a custom subclass of UIScrollView.
-(void)awakeFromNib {
dispatch_async(dispatch_get_main_queue(), ^{
[self adjustContentSizeToFit];
});
}
-(void)adjustContentSizeToFit {
BOOL showsVerticalScrollIndicator = self.showsVerticalScrollIndicator;
BOOL showsHorizontalScrollIndicator = self.showsHorizontalScrollIndicator;
self.showsVerticalScrollIndicator = NO;
self.showsHorizontalScrollIndicator = NO;
CGRect contentRect = CGRectZero;
for (UIView *view in self.subviews) {
contentRect = CGRectUnion(contentRect, view.frame);
}
self.contentSize = contentRect.size;
self.showsVerticalScrollIndicator = showsVerticalScrollIndicator;
self.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;
}
I think this can be a neat way of updating UIScrollView's content view size.
extension UIScrollView {
func updateContentViewSize() {
var newHeight: CGFloat = 0
for view in subviews {
let ref = view.frame.origin.y + view.frame.height
if ref > newHeight {
newHeight = ref
}
}
let oldSize = contentSize
let newSize = CGSize(width: oldSize.width, height: newHeight + 20)
contentSize = newSize
}
}
Set dynamic content size like this.
self.scroll_view.contentSize = CGSizeMake(screen_width,CGRectGetMaxY(self.controlname.frame)+20);
import UIKit
class DynamicSizeScrollView: UIScrollView {
var maxHeight: CGFloat = UIScreen.main.bounds.size.height
var maxWidth: CGFloat = UIScreen.main.bounds.size.width
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size,self.intrinsicContentSize){
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
let height = min(contentSize.height, maxHeight)
let width = min(contentSize.height, maxWidth)
return CGSize(width: width, height: height)
}
}
If you using Auto layout, just set the border element's edge equal to your scroll view.
For example, I wanna my horizontal scroll view auto fit my horizontal contents:
Swift
let bottomConstrint = NSLayoutConstraint.init(item: (bottommost UI element),
attribute: .bottom,
relatedBy: .equal,
toItem: (your UIScrollView),
attribute: .bottom,
multiplier: 1.0,
constant: 0)
bottomConstrint.isActive = true
If you using Snapkit like me, just:
scrollView.addSubview( (bottommost element) )
(bottommost element).snp.makeConstraints { make in
/*other constraints*/
make.bottom.equalToSuperview()
}
I would create a subclass of UIScrollView with the following:
class ContentSizedScrollView: UIScrollView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
This will resize automatically based on the height of the content.
class ContentSizedScrollView: UIScrollView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
// In UIViewController
import SnapKit
...
var scrollView: ContentSizedScrollView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = .init(width: view.bounds.width, height: stackView.bounds.height)
}
// Here some example of content composing inside of UIStackView
func setupContent() {
scrollView = ContentSizedScrollView()
blockView.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.top.equalTo(19)
make.left.equalToSuperview()
make.right.equalToSuperview()
make.bottom.equalTo(-20)
}
scrollView.contentInset = .init(top: 0, left: 0, bottom: 26, right: 0)
scrollView.showsVerticalScrollIndicator = false
scrollView.clipsToBounds = true
scrollView.layer.cornerRadius = blockView.layer.cornerRadius / 2
stackView = UIStackView()
scrollView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.centerX.equalToSuperview()
make.width.equalToSuperview().offset(-10)
}
stackView.axis = .vertical
stackView.alignment = .center
textTitleLabel = Label()
stackView.addArrangedSubview(textTitleLabel)
textTitleLabel.snp.makeConstraints { make in
make.width.equalToSuperview().offset(-30)
}
textTitleLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold)
textTitleLabel.textColor = Color.Blue.oxfordBlue
textTitleLabel.textAlignment = .center
textTitleLabel.numberOfLines = 0
stackView.setCustomSpacing(10, after: textTitleLabel)
}