How to mask UITableViewCells underneath a UITableView Transparent Header - iphone

I want the header to mask the cells, but not the background.
I have a UITableView with transparent headers and cells similar to Apple's Notification Center (when you swipe down on the status bar on your iPhone). I can't figure out how to mask the cells so they don't show up underneath the header when it scrolls.
I've tried changing the contentInsets of the tableview, and I've tried changing the frame of the header View to a negative origin.

Try to make a subclass of UITableviewCell and add these methods
- (void)maskCellFromTop:(CGFloat)margin {
self.layer.mask = [self visibilityMaskWithLocation:margin/self.frame.size.height];
self.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskWithLocation:(CGFloat)location {
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = self.bounds;
mask.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:1 alpha:0] CGColor], (id)[[UIColor colorWithWhite:1 alpha:1] CGColor], nil];
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location], [NSNumber numberWithFloat:location], nil];
return mask;
}
and add this delegate method in UITableView
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (iNotifyTableViewCell *cell in self.visibleCells) {
CGFloat hiddenFrameHeight = scrollView.contentOffset.y + [iNotifyHeaderView height] - cell.frame.origin.y;
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
[cell maskCellFromTop:hiddenFrameHeight];
}
}
}
*Note that [iNotifyHeaderView height] is the height of the HeaderView. and use #import <QuartzCore/QuartzCore.h> for the custom cell.

A little edit on Alex Markman's answer, where you could skip creating a subclass for an UITableViewCell. Benefit of this approach is that you can use it for multiple different UITableViewCells.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (UITableViewCell *cell in self.tableView.visibleCells) {
CGFloat hiddenFrameHeight = scrollView.contentOffset.y + self.navigationController.navigationBar.frame.size.height - cell.frame.origin.y;
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
[self maskCell:cell fromTopWithMargin:hiddenFrameHeight];
}
}
}
- (void)maskCell:(UITableViewCell *)cell fromTopWithMargin:(CGFloat)margin
{
cell.layer.mask = [self visibilityMaskForCell:cell withLocation:margin/cell.frame.size.height];
cell.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskForCell:(UITableViewCell *)cell withLocation:(CGFloat)location
{
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = cell.bounds;
mask.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:1 alpha:0] CGColor], (id)[[UIColor colorWithWhite:1 alpha:1] CGColor], nil];
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location], [NSNumber numberWithFloat:location], nil];
return mask;
}

#Alex Markman - your answer is great and was very usefull for me, but I found that when you're scrolling on retina devices, the cells do not scroll smoothly. I found that it is caused by layer's locations parameter during the rendering process:
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location];
I slightly modified your code. Maybe someone will find it useful:
- (void)maskCellFromTop:(CGFloat)margin
{
self.layer.mask = [self visibilityMaskFromLocation:margin];
self.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskFromLocation:(CGFloat)location
{
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = CGRectMake(
self.bounds.origin.x,
location+self.bounds.origin.y,
self.bounds.size.width,
self.bounds.size.height-location);
mask.colors = #[
(id)[[UIColor colorWithWhite:1 alpha:1] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:1] CGColor]
];
return mask;
}

Swift version
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let hiddenFrameHeight = scrollView.contentOffset.y + navigationController!.navigationBar.frame.size.height - cell.frame.origin.y
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
maskCell(cell: cell, margin: Float(hiddenFrameHeight))
}
}
}
func maskCell(cell: UITableViewCell, margin: Float) {
cell.layer.mask = visibilityMaskForCell(cell: cell, location: (margin / Float(cell.frame.size.height) ))
cell.layer.masksToBounds = true
}
func visibilityMaskForCell(cell: UITableViewCell, location: Float) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = cell.bounds
mask.colors = [UIColor(white: 1, alpha: 0).cgColor, UIColor(white: 1, alpha: 1).cgColor]
mask.locations = [NSNumber(value: location), NSNumber(value: location)]
return mask;
}

Clean Swift 3 Version:
extension YourViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let hiddenFrameHeight = scrollView.contentOffset.y + navigationController!.navigationBar.frame.size.height - cell.frame.origin.y
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
if let customCell = cell as? CustomCell {
customCell.maskCell(fromTop: hiddenFrameHeight)
}
}
}
}
}
class CustomCell: UITableViewCell {
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
}
}
I just used this and it works like a charm. I want to thank everyone in the post! Up votes all around!

I have two possible solutions, no code - just the idea:
not generic, should work with the settup/design apple uses at the Notification Center.
Make the Section-Header opaque, 'clone' the background-pattern of the table as background of the section-Header. Position the background-pattern depending on the section-header offset.
genereic, but probably more performance problems. Should work fine with few cells.
Add a Alpha Mask to all cell-layers. Move the Alpha Mask depending on the cell-position.
(use scrollViewDidScroll delegate method to maintain the background-pattern / Alpha-Mask offset).

I ended up setting the height of the section header to its minimum, and overriding UITableView's layoutSubviews to place the header on the tableView's superview, adjusting the frame's origin upward by its height.

The Swift 3 version didn't work for me because I added the UITableViewController as a subview. So I had to make some changes in the extension of the scrollview.
This should also work with UITableViewController that have been pushed from another ViewController (Note: not tested)
extension NavNotitionTableViewController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let calculatedY = cell.frame.origin.y - scrollView.contentOffset.y;
if let customCell = cell as? NavNotitionTableViewCell {
if(calculatedY < 44 && calculatedY > 0){
let hideAmount = 44 - calculatedY;
if let customCell = cell as? NavNotitionTableViewCell {
customCell.maskCell(fromTop: hideAmount)
}
}else if (calculatedY > 0){
//All other cells
customCell.maskCell(fromTop: 0)
}else if (calculatedY < 0){
customCell.maskCell(fromTop: cell.frame.height);
}
}
}
}
}
In this example, I first get the frame Y origin of the cell and distract the scollViews contentOffsetY.
The height of my custom section is 44. So I define the hideAmount value for the mask.
The Cell functions are untouched:
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
}

This wouldn't work if you wanted to show content behind your table view, but, since I'm only trying to create rounded headers with a plain solid colour background behind them, what solved it for me was mimicking transparency by setting the background colour of the header's background view to the background colour of the table view (or the first parent view with an opaque background).

class YourViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
// if tableView is under navigationbar, set `systemTopInset`
cell.sectionHeaderMask(delegate: self, systemTopInset: self.navigationController?.navigationBar.frame.height ?? 0)
}
}
}
public extension UITableViewCell {
func sectionHeaderMask<T: UITableViewDelegate>(delegate: T, systemTopInset: CGFloat = 0) {
guard let tableView = self.superview as? UITableView else { return }
guard let indexPath = tableView.indexPath(for: self) else { return }
guard let heightForHeader = delegate.tableView?(tableView, heightForHeaderInSection: indexPath.section) else { return }
let hiddenFrameHeight = tableView.contentOffset.y - self.frame.origin.y + heightForHeader + tableView.contentInset.top + systemTopInset
if hiddenFrameHeight >= 0 || hiddenFrameHeight <= self.frame.size.height {
mask(margin: Float(hiddenFrameHeight))
}
}
private func mask(margin: Float) {
layer.mask = visibilityMask(location: (margin / Float(frame.size.height) ))
layer.masksToBounds = true
}
private func visibilityMask(location: Float) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = self.bounds
mask.colors = [UIColor(white: 1, alpha: 0).cgColor, UIColor(white: 1, alpha: 1).cgColor]
mask.locations = [NSNumber(value: location), NSNumber(value: location)]
return mask
}
}

Or, if all you need is for your UI to look nice,
you could
change your table view to not have floating section headers
In two quick and easy steps (iOS 6):
Change your UITableView style to UITableViewStyleGrouped. (You can do this from Storyboard/NIB, or via code.)
Next, set your tableview's background view to a empty view like so [in either a method such as viewDidAppear or even in the cellForRow method (though I would prefer the former)].
yourTableView.backgroundView = [[UIView alloc] initWithFrame:listTableView.bounds];
Voila, now you have your table view - but without the floating section headers. Your section headers now scroll along with the cells and your messy UI problems are solved!
Do try this out and let me know how it goes.
Happy coding :)
EDIT: for iOS 7, simply change the table view style to 'UITableViewStyleGrouped' and change the view's tint color to 'clear color'.

Related

Customize dot with image of UIPageControl at index 0 of UIPageControl

I'm a new learner of ios programming. I have tried to search with another example and more questions at stackoverflow but it's not my goal. I want to set an image of dot at index 0 of UIPageControl as similar as iPhone search homescreen. Have any way to do it ? Please explain me with some code or another useful link.
Thanks in advance
I have found a solution for this problem. I know it is not the way but it will work till iOS 8 will be launched in the market.
Reason for Crash:
in iOS 7 [self.subViews objectAtIndex: i] returns UIView Instead of UIImageView and setImage is not the property of UIView and the app crashes. I solve my problem using this following code.
Check Whether the subview is UIView(for iOS7) or UIImageView(for iOS6 or earlier). And If it is UIView I am going to add UIImageView as subview on that view and voila its working and not crash..!!
-(void) updateDots
{
for (int i = 0; i < [self.subviews count]; i++)
{
UIImageView * dot = [self imageViewForSubview: [self.subviews objectAtIndex: i]];
if (i == self.currentPage) dot.image = activeImage;
else dot.image = inactiveImage;
}
}
- (UIImageView *) imageViewForSubview: (UIView *) view
{
UIImageView * dot = nil;
if ([view isKindOfClass: [UIView class]])
{
for (UIView* subview in view.subviews)
{
if ([subview isKindOfClass:[UIImageView class]])
{
dot = (UIImageView *)subview;
break;
}
}
if (dot == nil)
{
dot = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, view.frame.size.width, view.frame.size.height)];
[view addSubview:dot];
}
}
else
{
dot = (UIImageView *) view;
}
return dot;
}
Also, to clear the images that are already there, set the tint colors for the existing indicators to transparent:
- (void)awakeFromNib
{
self.pageIndicatorTintColor = [UIColor clearColor];
self.currentPageIndicatorTintColor = [UIColor clearColor];
}
Hope this will solve ur issue for iOS 7.
Happy coding
Try this link:-
Answer with GrayPageControl:-
Is there a way to change page indicator dots color
It is really good and reliable.I also have used this code.
You might have to do some more customization as
-(void) updateDots
{
for (int i = 0; i < [self.subviews count]; i++)
{
UIImageView* dot = [self.subviews objectAtIndex:i];
if (i == self.currentPage) {
if(i==0) {
dot.image = [UIImage imageNamed:#"activesearch.png"];
} else {
dot.image = activeImage;
}
} else {
if(i==0) {
dot.image = [UIImage imageNamed:#"inactivesearch.png"];
} else {
dot.image = inactiveImage;
}
}
}
}
Simply change the UIPageControl page indicator Color with pattern Image self.pageControl.currentPageIndicatorTintColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"circle"]];
The best compilation of the code for Swift 3 to replace the first icon of UIPageControl by a location marker:
import UIKit
class LocationPageControl: UIPageControl {
let locationArrow: UIImage = UIImage(named: "locationArrow")!
let pageCircle: UIImage = UIImage(named: "pageCircle")!
override var numberOfPages: Int {
didSet {
updateDots()
}
}
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = UIColor.clear
self.currentPageIndicatorTintColor = UIColor.clear
self.clipsToBounds = false
}
func updateDots() {
var i = 0
for view in self.subviews {
var imageView = self.imageView(forSubview: view)
if imageView == nil {
if i == 0 {
imageView = UIImageView(image: locationArrow)
} else {
imageView = UIImageView(image: pageCircle)
}
imageView!.center = view.center
view.addSubview(imageView!)
view.clipsToBounds = false
}
if i == self.currentPage {
imageView!.alpha = 1.0
} else {
imageView!.alpha = 0.5
}
i += 1
}
}
fileprivate func imageView(forSubview view: UIView) -> UIImageView? {
var dot: UIImageView?
if let dotImageView = view as? UIImageView {
dot = dotImageView
} else {
for foundView in view.subviews {
if let imageView = foundView as? UIImageView {
dot = imageView
break
}
}
}
return dot
}
}
Attached images:
I've created a custom page controller that should function in mostly the same way without hacking into the internals of a UIPageControl or having a whole library for one small widget.
Just place an empty UIStackView in your storyboard and make its custom class this class below, and use numberOfPages and currentPage just like a normal UIPageControl. Set the spacing on the UIStackView to change how much space there is between the views.
Swift 4.2
/**
If adding via storyboard, you should not need to set a width and height constraint for this view,
just set a placeholder for each so autolayout doesnt complain and this view will size itself once its populated with pages at runtime
*/
class PageControl: UIStackView {
#IBInspectable var currentPageImage: UIImage = UIImage(named: "whiteCircleFilled")!
#IBInspectable var pageImage: UIImage = UIImage(named: "whiteCircleOutlined")!
/**
Sets how many page indicators will show
*/
var numberOfPages = 3 {
didSet {
layoutIndicators()
}
}
/**
Sets which page indicator will be highlighted with the **currentPageImage**
*/
var currentPage = 0 {
didSet {
setCurrentPageIndicator()
}
}
override func awakeFromNib() {
super.awakeFromNib()
axis = .horizontal
distribution = .equalSpacing
alignment = .center
layoutIndicators()
}
private func layoutIndicators() {
for i in 0..<numberOfPages {
let imageView: UIImageView
if i < arrangedSubviews.count {
imageView = arrangedSubviews[i] as! UIImageView // reuse subview if possible
} else {
imageView = UIImageView()
addArrangedSubview(imageView)
}
if i == currentPage {
imageView.image = currentPageImage
} else {
imageView.image = pageImage
}
}
// remove excess subviews if any
let subviewCount = arrangedSubviews.count
if numberOfPages < subviewCount {
for _ in numberOfPages..<subviewCount {
arrangedSubviews.last?.removeFromSuperview()
}
}
}
private func setCurrentPageIndicator() {
for i in 0..<arrangedSubviews.count {
let imageView = arrangedSubviews[i] as! UIImageView
if i == currentPage {
imageView.image = currentPageImage
} else {
imageView.image = pageImage
}
}
}
}
Works for my purposes but I make no guarantees
Update for Swift 3.0 ... you know if you are OK with accepting stated risk: "Modifying the subviews of an existing control is fragile".
import UIKit
class CustomImagePageControl: UIPageControl {
let activeImage:UIImage = UIImage(named: "SelectedPage")!
let inactiveImage:UIImage = UIImage(named: "UnselectedPage")!
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = UIColor.clear
self.currentPageIndicatorTintColor = UIColor.clear
self.clipsToBounds = false
}
func updateDots() {
var i = 0
for view in self.subviews {
if let imageView = self.imageForSubview(view) {
if i == self.currentPage {
imageView.image = self.activeImage
} else {
imageView.image = self.inactiveImage
}
i = i + 1
} else {
var dotImage = self.inactiveImage
if i == self.currentPage {
dotImage = self.activeImage
}
view.clipsToBounds = false
view.addSubview(UIImageView(image:dotImage))
i = i + 1
}
}
}
fileprivate func imageForSubview(_ view:UIView) -> UIImageView? {
var dot:UIImageView?
if let dotImageView = view as? UIImageView {
dot = dotImageView
} else {
for foundView in view.subviews {
if let imageView = foundView as? UIImageView {
dot = imageView
break
}
}
}
return dot
}
}
You just need do it like this:
((UIImageView *)[[yourPageControl subviews] objectAtIndex:0]).image=[UIImage imageNamed:#"search.png"];
I think for this you need to customize whole UIPageControl. Please find more out at below links
How can i change the color of pagination dots of UIPageControl?
http://iphoneappcode.blogspot.in/2012/03/custom-uipagecontrol.html
From iOS 14 you can get and set the indicator image with these methods:
#available(iOS 14.0, *)
open func indicatorImage(forPage page: Int) -> UIImage?
#available(iOS 14.0, *)
open func setIndicatorImage(_ image: UIImage?, forPage page: Int)
From iProgrammer's answer
In case you want to hide the original dot
- (UIImageView *)imageViewForSubview:(UIView *)view {
UIImageView * dot = nil;
[view setBackgroundColor:[UIColor clearColor]]; << add this line
Following the previous answers I came up with the solution below.
Keep in mind that I had to add valueChanged listener that calls updateDots() as well in controller to handle taps made on UIPageControl
import UIKit
class PageControl: UIPageControl {
private struct Constants {
static let activeColor: UIColor = .white
static let inactiveColor: UIColor = .black
static let locationImage: UIImage = UIImage(named: "Location")!
}
// Update dots when currentPage changes
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = .clear
self.currentPageIndicatorTintColor = .clear
}
func updateDots() {
for (index, view) in self.subviews.enumerated() {
// Layers will be redrawn, remove old.
view.layer.sublayers?.removeAll()
if index == 0 {
drawImage(view: view)
} else {
drawDot(index: index, view: view)
}
}
}
private func drawDot(index: Int, view: UIView) {
let dotLayer = CAShapeLayer()
dotLayer.path = UIBezierPath(ovalIn: view.bounds).cgPath
dotLayer.fillColor = getColor(index: index)
view.layer.addSublayer(dotLayer)
}
private func drawImage(view: UIView) {
let height = view.bounds.height * 2
let width = view.bounds.width * 2
let topMargin: CGFloat = -3.5
let maskLayer = CALayer()
maskLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
maskLayer.contents = Constants.locationImage.cgImage
maskLayer.contentsGravity = .resizeAspect
let imageLayer = CALayer()
imageLayer.frame = CGRect(x:0, y: topMargin, width: width, height: height)
imageLayer.mask = maskLayer
imageLayer.backgroundColor = getColor()
view.backgroundColor = .clear // Otherwise black background
view.layer.addSublayer(imageLayer)
}
private func getColor(index: Int? = 0) -> CGColor {
return currentPage == index ? Constants.activeColor.cgColor : Constants.inactiveColor.cgColor
}
}

How to draw vertical text in UILabel

I'm currently working on drawing vertical Chinese text in a label. Here's what I am trying to achieve, albeit with Chinese Characters:
I've been planning to draw each character, rotate each character 90 degrees to the left, then rotating the entire label via affine transformations to get the final result. However, it feels awfully complicated. Is there an easier way to draw the text without complicated CoreGraphics magic that I'm missing?
Well, You can do like below:
labelObject.numberOfLines = 0;
labelObject.lineBreakMode = NSLineBreakByCharWrapping;
and setFrame with -- height:100, width:20 It will work fine..
It works
UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 30, 100)];
lbl.transform = CGAffineTransformMakeRotation((M_PI)/2);
Tried the method offered by Simha.IC but it didn't work well for me. Some characters are thinner than others and get placed two on a line. E.g.
W
ai
ti
n
g
The solution for me was to create a method that transforms the string itself into a multiline text by adding \n after each character. Here's the method:
- (NSString *)transformStringToVertical:(NSString *)originalString
{
NSMutableString *mutableString = [NSMutableString stringWithString:originalString];
NSRange stringRange = [mutableString rangeOfString:mutableString];
for (int i = 1; i < stringRange.length*2 - 2; i+=2)
{
[mutableString insertString:#"\n" atIndex:i];
}
return mutableString;
}
Then you just setup the label like this:
label.text = [self transformStringToVertical:myString];
CGRect labelFrame = label.frame;
labelFrame.size.width = label.font.pointSize;
labelFrame.size.height = label.font.lineHeight * myString.length;
label.frame = labelFrame;
Enjoy!
If you would like to rotate the whole label (including characters), you can do so as follows:
First add the QuartzCore library to your project.
Create a label:
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 30.0)];
[label setText:#"Label Text"];
Rotate the label:
[label setTransform:CGAffineTransformMakeRotation(-M_PI / 2)];
Depending on how you'd like to position the label you may need to set the anchor point. This sets the point around which a rotation occurs. Eg:
[label.layer setAnchorPoint:CGPointMake(0.0, 1.0)];
This is another way to draw vertical text, by subclassing UILabel. But it is some kind different of what the question want.
Objective-C
#implementation MyVerticalLabel
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform transform = CGAffineTransformMakeRotation(-M_PI_2);
CGContextConcatCTM(context, transform);
CGContextTranslateCTM(context, -rect.size.height, 0);
CGRect newRect = CGRectApplyAffineTransform(rect, transform);
newRect.origin = CGPointZero;
NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
textStyle.lineBreakMode = self.lineBreakMode;
textStyle.alignment = self.textAlignment;
NSDictionary *attributeDict =
#{
NSFontAttributeName : self.font,
NSForegroundColorAttributeName : self.textColor,
NSParagraphStyleAttributeName : textStyle,
};
[self.text drawInRect:newRect withAttributes:attributeDict];
}
#end
A sample image is following:
Swift
It can put on the storyboard, and watch the result directly. Like the image, it's frame will contain the vertical text. And text attributes, like textAlignment, font, work well too.
#IBDesignable
class MyVerticalLabel: UILabel {
override func drawRect(rect: CGRect) {
guard let text = self.text else {
return
}
// Drawing code
let context = UIGraphicsGetCurrentContext()
let transform = CGAffineTransformMakeRotation( CGFloat(-M_PI_2))
CGContextConcatCTM(context, transform)
CGContextTranslateCTM(context, -rect.size.height, 0)
var newRect = CGRectApplyAffineTransform(rect, transform)
newRect.origin = CGPointZero
let textStyle = NSMutableParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
textStyle.lineBreakMode = self.lineBreakMode
textStyle.alignment = self.textAlignment
let attributeDict: [String:AnyObject] = [
NSFontAttributeName: self.font,
NSForegroundColorAttributeName: self.textColor,
NSParagraphStyleAttributeName: textStyle,
]
let nsStr = text as NSString
nsStr.drawInRect(newRect, withAttributes: attributeDict)
}
}
Swift 4
override func draw(_ rect: CGRect) {
guard let text = self.text else {
return
}
// Drawing code
if let context = UIGraphicsGetCurrentContext() {
let transform = CGAffineTransform( rotationAngle: CGFloat(-Double.pi/2))
context.concatenate(transform)
context.translateBy(x: -rect.size.height, y: 0)
var newRect = rect.applying(transform)
newRect.origin = CGPoint.zero
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
textStyle.lineBreakMode = self.lineBreakMode
textStyle.alignment = self.textAlignment
let attributeDict: [NSAttributedStringKey: AnyObject] = [NSAttributedStringKey.font: self.font, NSAttributedStringKey.foregroundColor: self.textColor, NSAttributedStringKey.paragraphStyle: textStyle]
let nsStr = text as NSString
nsStr.draw(in: newRect, withAttributes: attributeDict)
}
}
Swift 5
More easy way with CGAffineTransform
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var verticalText: UILabel
override func viewDidLoad() {
verticalText.transform = CGAffineTransform(rotationAngle:CGFloat.pi/2)
}
}
import UIKit
class VerticalLabel : UILabel {
private var _text : String? = nil
override var text : String? {
get {
return _text
}
set {
self.numberOfLines = 0
self.textAlignment = .center
self.lineBreakMode = .byWordWrapping
_text = newValue
if let t = _text {
var s = ""
for c in t {
s += "\(c)\n"
}
super.text = s
}
}
}
}

How do I auto size a UIScrollView to fit its content

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)
}

How do I make UITableViewCell's ImageView a fixed size even when the image is smaller

I have a bunch of images I am using for cell's image views, they are all no bigger than 50x50. e.g. 40x50, 50x32, 20x37 .....
When I load the table view, the text doesn't line up because the width of the images varies. Also I would like small images to appear in the centre as opposed to on the left.
Here is the code I am trying inside my 'cellForRowAtIndexPath' method
cell.imageView.autoresizingMask = ( UIViewAutoresizingNone );
cell.imageView.autoresizesSubviews = NO;
cell.imageView.contentMode = UIViewContentModeCenter;
cell.imageView.bounds = CGRectMake(0, 0, 50, 50);
cell.imageView.frame = CGRectMake(0, 0, 50, 50);
cell.imageView.image = [UIImage imageWithData: imageData];
As you can see I have tried a few things, but none of them work.
It's not necessary to rewrite everything. I recommend doing this instead:
Post this inside your .m file of your custom cell.
- (void)layoutSubviews {
[super layoutSubviews];
self.imageView.frame = CGRectMake(0,0,32,32);
}
This should do the trick nicely. :]
For those of you who don't have a subclass of UITableViewCell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
[...]
CGSize itemSize = CGSizeMake(40, 40);
UIGraphicsBeginImageContextWithOptions(itemSize, NO, UIScreen.mainScreen.scale);
CGRect imageRect = CGRectMake(0.0, 0.0, itemSize.width, itemSize.height);
[cell.imageView.image drawInRect:imageRect];
cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[...]
return cell;
}
The code above sets the size to be 40x40.
Swift 2
let itemSize = CGSizeMake(25, 25);
UIGraphicsBeginImageContextWithOptions(itemSize, false, UIScreen.mainScreen().scale);
let imageRect = CGRectMake(0.0, 0.0, itemSize.width, itemSize.height);
cell.imageView?.image!.drawInRect(imageRect)
cell.imageView?.image! = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Or you can use another(not tested) approach suggested by #Tommy:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
[...]
CGSize itemSize = CGSizeMake(40, 40);
UIGraphicsBeginImageContextWithOptions(itemSize, NO, 0.0)
[...]
return cell;
}
Swift 3+
let itemSize = CGSize.init(width: 25, height: 25)
UIGraphicsBeginImageContextWithOptions(itemSize, false, UIScreen.main.scale);
let imageRect = CGRect.init(origin: CGPoint.zero, size: itemSize)
cell?.imageView?.image!.draw(in: imageRect)
cell?.imageView?.image! = UIGraphicsGetImageFromCurrentImageContext()!;
UIGraphicsEndImageContext();
The code above is the Swift 3+ version of the above.
Here's how i did it. This technique takes care of moving the text and detail text labels appropriately to the left:
#interface SizableImageCell : UITableViewCell {}
#end
#implementation SizableImageCell
- (void)layoutSubviews {
[super layoutSubviews];
float desiredWidth = 80;
float w=self.imageView.frame.size.width;
if (w>desiredWidth) {
float widthSub = w - desiredWidth;
self.imageView.frame = CGRectMake(self.imageView.frame.origin.x,self.imageView.frame.origin.y,desiredWidth,self.imageView.frame.size.height);
self.textLabel.frame = CGRectMake(self.textLabel.frame.origin.x-widthSub,self.textLabel.frame.origin.y,self.textLabel.frame.size.width+widthSub,self.textLabel.frame.size.height);
self.detailTextLabel.frame = CGRectMake(self.detailTextLabel.frame.origin.x-widthSub,self.detailTextLabel.frame.origin.y,self.detailTextLabel.frame.size.width+widthSub,self.detailTextLabel.frame.size.height);
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
}
}
#end
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[SizableImageCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
cell.textLabel.text = ...
cell.detailTextLabel.text = ...
cell.imageView.image = ...
return cell;
}
image view add as a sub view to the tableview cell
UIImageView *imgView=[[UIImageView alloc] initWithFrame:CGRectMake(20, 5, 90, 70)];
imgView.backgroundColor=[UIColor clearColor];
[imgView.layer setCornerRadius:8.0f];
[imgView.layer setMasksToBounds:YES];
[imgView setImage:[UIImage imageWithData: imageData]];
[cell.contentView addSubview:imgView];
The whole cell doesn't need to be remade. You could use the indentationLevel and indentationWidth property of tableViewCells to shift the content of your cell. Then you add your custom imageView to the left side of the cell.
Better create an image view and add it as a sub view to the cell.Then you can get the desired frame size.
A Simply Swift,
Step 1: Create One SubClass of UITableViewCell
Step 2: Add this method to SubClass of UITableViewCell
override func layoutSubviews() {
super.layoutSubviews()
self.imageView?.frame = CGRectMake(0, 0, 10, 10)
}
Step 3: Create cell object using that SubClass in cellForRowAtIndexPath,
Ex: let customCell:CustomCell = CustomCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
Step 4: Enjoy
UIImage *image = cell.imageView.image;
UIGraphicsBeginImageContext(CGSizeMake(35,35));
// draw scaled image into thumbnail context
[image drawInRect:CGRectMake(5, 5, 35, 35)]; //
UIImage *newThumbnail = UIGraphicsGetImageFromCurrentImageContext();
// pop the context
UIGraphicsEndImageContext();
if(newThumbnail == nil)
{
NSLog(#"could not scale image");
cell.imageView.image = image;
}
else
{
cell.imageView.image = newThumbnail;
}
I've created an extension using #GermanAttanasio 's answer. It provides a method to resize an image to a desired size, and another method to do the same while adding a transparent margin to the image (this can be useful for table views where you want the image to have a margin as well).
import UIKit
extension UIImage {
/// Resizes an image to the specified size.
///
/// - Parameters:
/// - size: the size we desire to resize the image to.
///
/// - Returns: the resized image.
///
func imageWithSize(size: CGSize) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.mainScreen().scale);
let rect = CGRectMake(0.0, 0.0, size.width, size.height);
drawInRect(rect)
let resultingImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resultingImage
}
/// Resizes an image to the specified size and adds an extra transparent margin at all sides of
/// the image.
///
/// - Parameters:
/// - size: the size we desire to resize the image to.
/// - extraMargin: the extra transparent margin to add to all sides of the image.
///
/// - Returns: the resized image. The extra margin is added to the input image size. So that
/// the final image's size will be equal to:
/// `CGSize(width: size.width + extraMargin * 2, height: size.height + extraMargin * 2)`
///
func imageWithSize(size: CGSize, extraMargin: CGFloat) -> UIImage {
let imageSize = CGSize(width: size.width + extraMargin * 2, height: size.height + extraMargin * 2)
UIGraphicsBeginImageContextWithOptions(imageSize, false, UIScreen.mainScreen().scale);
let drawingRect = CGRect(x: extraMargin, y: extraMargin, width: size.width, height: size.height)
drawInRect(drawingRect)
let resultingImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resultingImage
}
}
This worked for me in swift:
Create a subclass of UITableViewCell (make sure you link up your cell in the storyboard)
class MyTableCell:UITableViewCell{
override func layoutSubviews() {
super.layoutSubviews()
if(self.imageView?.image != nil){
let cellFrame = self.frame
let textLabelFrame = self.textLabel?.frame
let detailTextLabelFrame = self.detailTextLabel?.frame
let imageViewFrame = self.imageView?.frame
self.imageView?.contentMode = .ScaleAspectFill
self.imageView?.clipsToBounds = true
self.imageView?.frame = CGRectMake((imageViewFrame?.origin.x)!,(imageViewFrame?.origin.y)! + 1,40,40)
self.textLabel!.frame = CGRectMake(50 + (imageViewFrame?.origin.x)! , (textLabelFrame?.origin.y)!, cellFrame.width-(70 + (imageViewFrame?.origin.x)!), textLabelFrame!.height)
self.detailTextLabel!.frame = CGRectMake(50 + (imageViewFrame?.origin.x)!, (detailTextLabelFrame?.origin.y)!, cellFrame.width-(70 + (imageViewFrame?.origin.x)!), detailTextLabelFrame!.height)
}
}
}
In cellForRowAtIndexPath , dequeue the cell as your new cell type:
let cell = tableView.dequeueReusableCellWithIdentifier("MyCell", forIndexPath: indexPath) as! MyTableCell
Obviously change number values to suit your layout
Here is #germanattanasio 's working method, written for Swift 3
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.imageView?.image = myImage
let itemSize = CGSize(width:42.0, height:42.0)
UIGraphicsBeginImageContextWithOptions(itemSize, false, 0.0)
let imageRect = CGRect(x:0.0, y:0.0, width:itemSize.width, height:itemSize.height)
cell.imageView?.image!.draw(in:imageRect)
cell.imageView?.image! = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
}
If you use cell.imageView?.translatesAutoresizingMaskIntoConstraints = false you can set constraints on the imageView. Here's a working example I used in a project. I avoided subclassing and didn't need to create storyboard with prototype cells but did take me quite a while to get running, so probably best to only use if there isn't a simpler or more concise way available to you.
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: String(describing: ChangesRequiringApprovalTableViewController.self))
let record = records[indexPath.row]
cell.textLabel?.text = "Title text"
if let thumb = record["thumbnail"] as? CKAsset, let image = UIImage(contentsOfFile: thumb.fileURL.path) {
cell.imageView?.contentMode = .scaleAspectFill
cell.imageView?.image = image
cell.imageView?.translatesAutoresizingMaskIntoConstraints = false
cell.imageView?.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor).isActive = true
cell.imageView?.widthAnchor.constraint(equalToConstant: 80).rowHeight).isActive = true
cell.imageView?.heightAnchor.constraint(equalToConstant: 80).isActive = true
if let textLabel = cell.textLabel {
let margins = cell.contentView.layoutMarginsGuide
textLabel.translatesAutoresizingMaskIntoConstraints = false
cell.imageView?.trailingAnchor.constraint(equalTo: textLabel.leadingAnchor, constant: -8).isActive = true
textLabel.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
textLabel.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
let bottomConstraint = textLabel.bottomAnchor.constraint(equalTo: margins.bottomAnchor)
bottomConstraint.priority = UILayoutPriorityDefaultHigh
bottomConstraint.isActive = true
if let description = cell.detailTextLabel {
description.translatesAutoresizingMaskIntoConstraints = false
description.bottomAnchor.constraint(equalTo: margins.bottomAnchor).isActive = true
description.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
cell.imageView?.trailingAnchor.constraint(equalTo: description.leadingAnchor, constant: -8).isActive = true
textLabel.bottomAnchor.constraint(equalTo: description.topAnchor).isActive = true
}
}
cell.imageView?.clipsToBounds = true
}
cell.detailTextLabel?.text = "Detail Text"
return cell
}
I had the same problem. Thank you to everyone else who answered - I was able to get a solution together using parts of several of these answers.
My solution is using swift 5
The problem that we are trying to solve is that we may have images with different aspect ratios in our TableViewCells but we want them to render with consistent widths. The images should, of course, render with no distortion and fill the entire space. In my case, I was fine with some "cropping" of tall, skinny images, so I used the content mode .scaleAspectFill
To do this, I created a custom subclass of UITableViewCell. In my case, I named it StoryTableViewCell. The entire class is pasted below, with comments inline.
This approach worked for me when also using a custom Accessory View and long text labels. Here's an image of the final result:
Rendered Table View with consistent image width
class StoryTableViewCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
// ==== Step 1 ====
// ensure we have an image
guard let imageView = self.imageView else {return}
// create a variable for the desired image width
let desiredWidth:CGFloat = 70;
// get the width of the image currently rendered in the cell
let currentImageWidth = imageView.frame.size.width;
// grab the width of the entire cell's contents, to be used later
let contentWidth = self.contentView.bounds.width
// ==== Step 2 ====
// only update the image's width if the current image width isn't what we want it to be
if (currentImageWidth != desiredWidth) {
//calculate the difference in width
let widthDifference = currentImageWidth - desiredWidth;
// ==== Step 3 ====
// Update the image's frame,
// maintaining it's original x and y values, but with a new width
self.imageView?.frame = CGRect(imageView.frame.origin.x,
imageView.frame.origin.y,
desiredWidth,
imageView.frame.size.height);
// ==== Step 4 ====
// If there is a texst label, we want to move it's x position to
// ensure it isn't overlapping with the image, and that it has proper spacing with the image
if let textLabel = self.textLabel
{
let originalFrame = self.textLabel?.frame
// the new X position for the label is just the original position,
// minus the difference in the image's width
let newX = textLabel.frame.origin.x - widthDifference
self.textLabel?.frame = CGRect(newX,
textLabel.frame.origin.y,
contentWidth - newX,
textLabel.frame.size.height);
print("textLabel info: Original =\(originalFrame!)", "updated=\(self.textLabel!.frame)")
}
// ==== Step 4 ====
// If there is a detail text label, do the same as step 3
if let detailTextLabel = self.detailTextLabel {
let originalFrame = self.detailTextLabel?.frame
let newX = detailTextLabel.frame.origin.x-widthDifference
self.detailTextLabel?.frame = CGRect(x: newX,
y: detailTextLabel.frame.origin.y,
width: contentWidth - newX,
height: detailTextLabel.frame.size.height);
print("detailLabel info: Original =\(originalFrame!)", "updated=\(self.detailTextLabel!.frame)")
}
// ==== Step 5 ====
// Set the image's content modoe to scaleAspectFill so it takes up the entire view, but doesn't get distorted
self.imageView?.contentMode = .scaleAspectFill;
}
}
}
The regular UITableViewCell works well to position things but the cell.imageView doesn't seem to behave like you want it to. I found that it's simple enough to get the UITableViewCell to lay out properly by first giving the cell.imageView a properly sized image like
// Putting in a blank image to make sure text always pushed to the side.
UIGraphicsBeginImageContextWithOptions(CGSizeMake(kGroupImageDimension, kGroupImageDimension), NO, 0.0);
UIImage *blank = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
cell.imageView.image = blank;
Then you can just connect up your own properly working UIImageView with
// The cell.imageView increases in size to accomodate the image given it.
// We don't want this behaviour so we just attached a view on top of cell.imageView.
// This gives us the positioning of the cell.imageView without the sizing
// behaviour.
UIImageView *anImageView = nil;
NSArray *subviews = [cell.imageView subviews];
if ([subviews count] == 0)
{
anImageView = [[UIImageView alloc] init];
anImageView.translatesAutoresizingMaskIntoConstraints = NO;
[cell.imageView addSubview:anImageView];
NSLayoutConstraint *aConstraint = [NSLayoutConstraint constraintWithItem:anImageView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:cell.imageView attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0];
[cell.imageView addConstraint:aConstraint];
aConstraint = [NSLayoutConstraint constraintWithItem:anImageView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:cell.imageView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0];
[cell.imageView addConstraint:aConstraint];
aConstraint = [NSLayoutConstraint constraintWithItem:anImageView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:kGroupImageDimension];
[cell.imageView addConstraint:aConstraint];
aConstraint = [NSLayoutConstraint constraintWithItem:anImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:kGroupImageDimension];
[cell.imageView addConstraint:aConstraint];
}
else
{
anImageView = [subviews firstObject];
}
Set the image on anImageView and it will do what you expect a UIImageView to do. Be the size you want it regardless of the image you give it. This should go in tableView:cellForRowAtIndexPath:
This solution essentially draws the image as 'aspect fit' within the given rect.
CGSize itemSize = CGSizeMake(80, 80);
UIGraphicsBeginImageContextWithOptions(itemSize, NO, UIScreen.mainScreen.scale);
UIImage *image = cell.imageView.image;
CGRect imageRect;
if(image.size.height > image.size.width) {
CGFloat width = itemSize.height * image.size.width / image.size.height;
imageRect = CGRectMake((itemSize.width - width) / 2, 0, width, itemSize.height);
} else {
CGFloat height = itemSize.width * image.size.height / image.size.width;
imageRect = CGRectMake(0, (itemSize.height - height) / 2, itemSize.width, height);
}
[cell.imageView.image drawInRect:imageRect];
cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
The solution we ended up with is similar to many of the others. But to get the correct position of the separator we had to set it before calling super.layoutSubviews(). Simplified example:
class ImageTableViewCell: UITableViewCell {
override func layoutSubviews() {
separatorInset.left = 70
super.layoutSubviews()
imageView?.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
textLabel?.frame = CGRect(x: 70, y: 0, width: 200, height: 50)
}
}

Is there a way to remove the separator line from a single cell in UITableView?

I know I can change the UITableView property separatorStyle to UITableViewCellSeparatorStyleNone or UITableViewCellSeparatorStyleSingleLine to change all the cells in the TableView one way or the other.
I'm interested having some cells with a SingleLine Separator and some cells without. Is this possible?
Your best bet is probably to set the table's separatorStyle to UITableViewCellSeparatorStyleNone and manually adding/drawing a line (perhaps in tableView:cellForRowAtIndexPath:) when you want it.
Following Mike's advice, here is what I did.
In tableView:cellForRowAtIndexPath:
...
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
// Drawing our own separatorLine here because I need to turn it off for the
// last row. I can only do that on the tableView and on on specific cells.
// The y position below has to be 1 less than the cell height to keep it from
// disappearing when the tableView is scrolled.
UIImageView *separatorLine = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, cell.frame.size.height - 1.0f, cell.frame.size.width, 1.0f)];
separatorLine.image = [[UIImage imageNamed:#"grayDot"] stretchableImageWithLeftCapWidth:1 topCapHeight:0];
separatorLine.tag = 4;
[cell.contentView addSubview:separatorLine];
[separatorLine release];
}
// Setup default cell setttings.
...
UIImageView *separatorLine = (UIImageView *)[cell viewWithTag:4];
separatorLine.hidden = NO;
...
// In the cell I want to hide the line, I just hide it.
seperatorLine.hidden = YES;
...
In viewDidLoad:
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
This works for me nicely.
cell.separatorInset = UIEdgeInsetsMake(0, 160, 0, 160);
All is does it pushes the insets for the left and right of the line to the dead center, which is 160, which makes it invisible.
Then you can control the which cells to apply it by the indexPath.row;
self.separatorInset = UIEdgeInsetsMake(0, CGRectGetWidth(self.frame)/2, 0, CGRectGetWidth(self.frame)/2);
I was able to hide the separator line for multiple different cells by making the left inset the size of the entire cell bounds width. Replace the "indexPath.row == 0" with the row number that you need to remove the separator line.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
if (indexPath.row == 0 || indexPath.row == 2 || indexPath.row == 3 || indexPath.row == 8 || indexPath.row == 9) {
cell.separatorInset = UIEdgeInsetsMake(0, cell.bounds.size.width, 0, 0);
}
else {
cell.separatorInset = UIEdgeInsetsMake(0, 24, 0, 0);
}
return cell;
}
For people with the same question who use swift and want to hide the separation line for only a specific type of cell, a way to do it is the following one:
override func layoutSubviews() {
super.layoutSubviews()
subviews.forEach { (view) in
if view.dynamicType.description() == "_UITableViewCellSeparatorView" {
view.hidden = true
}
}
}
The best way to achieve this is to turn off default line separators, subclass UITableViewCell and add a custom line separator as a subview of the contentView - see below a custom cell that is used to present an object of type SNStock that has two string properties, ticker and name:
import UIKit
private let kSNStockCellCellHeight: CGFloat = 65.0
private let kSNStockCellCellLineSeparatorHorizontalPaddingRatio: CGFloat = 0.03
private let kSNStockCellCellLineSeparatorBackgroundColorAlpha: CGFloat = 0.3
private let kSNStockCellCellLineSeparatorHeight: CGFloat = 1
class SNStockCell: UITableViewCell {
private let primaryTextColor: UIColor
private let secondaryTextColor: UIColor
private let customLineSeparatorView: UIView
var showsCustomLineSeparator: Bool {
get {
return !customLineSeparatorView.hidden
}
set(showsCustomLineSeparator) {
customLineSeparatorView.hidden = !showsCustomLineSeparator
}
}
var customLineSeparatorColor: UIColor? {
get {
return customLineSeparatorView.backgroundColor
}
set(customLineSeparatorColor) {
customLineSeparatorView.backgroundColor = customLineSeparatorColor?.colorWithAlphaComponent(kSNStockCellCellLineSeparatorBackgroundColorAlpha)
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(reuseIdentifier: String, primaryTextColor: UIColor, secondaryTextColor: UIColor) {
self.primaryTextColor = primaryTextColor
self.secondaryTextColor = secondaryTextColor
self.customLineSeparatorView = UIView(frame:CGRectZero)
super.init(style: UITableViewCellStyle.Subtitle, reuseIdentifier:reuseIdentifier)
selectionStyle = UITableViewCellSelectionStyle.None
backgroundColor = UIColor.clearColor()
contentView.addSubview(customLineSeparatorView)
customLineSeparatorView.hidden = true
}
override func prepareForReuse() {
super.prepareForReuse()
self.showsCustomLineSeparator = false
}
// MARK: Layout
override func layoutSubviews() {
super.layoutSubviews()
layoutCustomLineSeparator()
}
private func layoutCustomLineSeparator() {
let horizontalPadding: CGFloat = bounds.width * kSNStockCellCellLineSeparatorHorizontalPaddingRatio
let lineSeparatorWidth: CGFloat = bounds.width - horizontalPadding * 2;
customLineSeparatorView.frame = CGRectMake(horizontalPadding,
kSNStockCellCellHeight - kSNStockCellCellLineSeparatorHeight,
lineSeparatorWidth,
kSNStockCellCellLineSeparatorHeight)
}
// MARK: Public Class API
class func cellHeight() -> CGFloat {
return kSNStockCellCellHeight
}
// MARK: Public API
func configureWithStock(stock: SNStock) {
textLabel!.text = stock.ticker as String
textLabel!.textColor = primaryTextColor
detailTextLabel!.text = stock.name as String
detailTextLabel!.textColor = secondaryTextColor
setNeedsLayout()
}
}
To disable the default line separator use, tableView.separatorStyle = UITableViewCellSeparatorStyle.None;. The consumer side is relatively simple, see example below:
private func stockCell(tableView: UITableView, indexPath:NSIndexPath) -> UITableViewCell {
var cell : SNStockCell? = tableView.dequeueReusableCellWithIdentifier(stockCellReuseIdentifier) as? SNStockCell
if (cell == nil) {
cell = SNStockCell(reuseIdentifier:stockCellReuseIdentifier, primaryTextColor:primaryTextColor, secondaryTextColor:secondaryTextColor)
}
cell!.configureWithStock(stockAtIndexPath(indexPath))
cell!.showsCustomLineSeparator = true
cell!.customLineSeparatorColor = tintColor
return cell!
}