How to change UITableViewRowAction title color? - ios

Am using UITableViewRowAction in "editActionsForRowAtIndexPath" method. I can change the backgroundcolor of UITableViewRowAction, but am not able to change the title color. But I would like to change the color of the UITableViewRowAction. Any inputs on this regard will be appreciable.

There is one way that can achieve what you are looking for. But it is little tricky though.
Example of result:
The idea of this trick is that you can actually modify background color. This means that you can set UIColor's +colorWithPatternImage: and set an bitmap image that match desired styling. This can be achieved either by creating image using graphic editor or by rendering it using for example Core Graphics. The only problem with this solution is, that you have to mimic original title length with you specific text attributes to make it work properly and also you must set title for table view row action as string of white spaces so that table view cell will prepare enough space for you "custom action button". Creating static png assets in photoshop may be inappropriate if you use variable cell rows.
This is category for NSString that creates string of empty spaces that will create space for your custom button and second will generate bitmap image that will be placed as background pattern image. For parameters you must set text attributes for original title, that is basically #{NSFontAttributeName: [UIFont systemFontOfSize:18]}, than your desired text attributes. Maybe there is better way to achieve this :)
#implementation NSString (WhiteSpace)
- (NSString *)whitespaceReplacementWithSystemAttributes:(NSDictionary *)systemAttributes newAttributes:(NSDictionary *)newAttributes
{
NSString *stringTitle = self;
NSMutableString *stringTitleWS = [[NSMutableString alloc] initWithString:#""];
CGFloat diff = 0;
CGSize stringTitleSize = [stringTitle sizeWithAttributes:newAttributes];
CGSize stringTitleWSSize;
NSDictionary *originalAttributes = systemAttributes;
do {
[stringTitleWS appendString:#" "];
stringTitleWSSize = [stringTitleWS sizeWithAttributes:originalAttributes];
diff = (stringTitleSize.width - stringTitleWSSize.width);
if (diff <= 1.5) {
break;
}
}
while (diff > 0);
return [stringTitleWS copy];
}
#end
Second important part is code that renders bitmap that can be used as pattern image for UITableViewRowAction's backgroundColor.
- (UIImage *)imageForTableViewRowActionWithTitle:(NSString *)title textAttributes:(NSDictionary *)attributes backgroundColor:(UIColor *)color cellHeight:(CGFloat)cellHeight
{
NSString *titleString = title;
NSDictionary *originalAttributes = #{NSFontAttributeName: [UIFont systemFontOfSize:18]};
CGSize originalSize = [titleString sizeWithAttributes:originalAttributes];
CGSize newSize = CGSizeMake(originalSize.width + kSystemTextPadding + kSystemTextPadding, originalSize.height);
CGRect drawingRect = CGRectIntegral(CGRectMake(0, 0, newSize.width, cellHeight));
UIGraphicsBeginImageContextWithOptions(drawingRect.size, YES, [UIScreen mainScreen].nativeScale);
CGContextRef contextRef = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(contextRef, color.CGColor);
CGContextFillRect(contextRef, drawingRect);
UILabel *label = [[UILabel alloc] initWithFrame:drawingRect];
label.textAlignment = NSTextAlignmentCenter;
label.attributedText = [[NSAttributedString alloc] initWithString:title attributes:attributes];
[label drawTextInRect:drawingRect];
//This is other way how to render string
// CGSize size = [titleString sizeWithAttributes:attributes];
// CGFloat x = (drawingRect.size.width - size.width)/2;
// CGFloat y = (drawingRect.size.height - size.height)/2;
// drawingRect.origin = CGPointMake(x, y);
// [titleString drawInRect:drawingRect withAttributes:attributes];
UIImage *returningImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return returningImage;
}
And then when you are creating your row action you can do something like this:
NSDictionary *systemAttributes = #{ NSFontAttributeName: [UIFont systemFontOfSize:18] };
NSDictionary *newAttributes = #{NSFontAttributeName: [UIFont fontWithName:#"Any font" size:15]};
NSString *actionTitle = #"Delete";
NSString *titleWhiteSpaced = [actionTitle whitespaceReplacementWithSystemAttributes:systemTextFontAttributes newAttributes:newAttributes];
UITableViewRowAction *rowAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:titleWhiteSpaced handle:NULL];
UIImage *patternImage = [self imageForTableViewRowActionWithTitle:actionTitle textAttributes:newAttributes backgroundColor:[UIColor redColor] cellHeight:50];
rowAction.backgroundColor = [UIColor colorWithPatternImage:patternImage];
This solution is obviously hacky but you can achieve desired results without using private APIs and in future if something breaks, this will not break your app. Only button will look inappropriate.
Example of result:
This code has of course a lot of space for improvements, any suggestions are appreciated.

I'm afraid that there's no way to change the title color of the UITableViewRowAction.
The only things you can change on the action are:
backgroundColor
style (destructive (red backgroundcolor, ...)
title
For more info, please refer to the Apple Doc UITableViewRowAction

Swift
No need to mess with UIButton.appearance...
Put this in your cell's class and change UITableViewCellActionButton according to your needs.
override func layoutSubviews() {
super.layoutSubviews()
for subview in self.subviews {
for subview2 in subview.subviews {
if (String(subview2).rangeOfString("UITableViewCellActionButton") != nil) {
for view in subview2.subviews {
if (String(view).rangeOfString("UIButtonLabel") != nil) {
if let label = view as? UILabel {
label.textColor = YOUR COLOUR
}
}
}
}
}
}
}

So there still is no public api to change textColor or font to the cells action in the iOS 13 days.
Working solution for Swift 5.3, iOS 14
Hacks from answers in the thread have very unreliable results but I have managed to get my version of the hack working.
1. Getting the label
Here is my simplified way of accessing the actions UILabel:
extension UITableViewCell {
var cellActionButtonLabel: UILabel? {
superview?.subviews
.filter { String(describing: $0).range(of: "UISwipeActionPullView") != nil }
.flatMap { $0.subviews }
.filter { String(describing: $0).range(of: "UISwipeActionStandardButton") != nil }
.flatMap { $0.subviews }
.compactMap { $0 as? UILabel }.first
}
}
2. Updating the label on layout changes
Next, overriding layoutSubviews() in my UITableViewCell subclass wasn't enough so additionally I had to override layoutIfNeeded() for the hack to work.
Note that it's important to override both of them!
override func layoutSubviews() {
super.layoutSubviews()
cellActionButtonLabel?.textColor = .black // you color goes here
}
override func layoutIfNeeded() {
super.layoutIfNeeded()
cellActionButtonLabel?.textColor = .black // you color goes here
}
3. Triggering extra layout refresh
The last piece of the puzzle is to schedule an additional refresh of the color, so we cover all of those cases where for some reason above functions would not get called. The best place for doing so is UITableViewDelegate method ..., willBeginEditingRowAt: ...
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
tableView.cellForRow(at: indexPath)?.layoutIfNeeded()
}
}
A delayed layout refresh trigger does the trick of letting the UIKit configure the cell first, so we can jump right after with the customisation. 0.01 was just enough for me but i can probably vary from case to case.
4. Profit
Now you can customise the label anyway you want! Change the text, its color, font or add a subview!
It goes without saying that this can break -anytime- if Apple will decide to change their private implementation.

There is indeed a way to change the title color of the UITableViewRowAction. It's a button. You can use the UIButton appearance proxy:
[[UIButton appearance] setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal];

Lee Andrew's answer in Swift 3 / Swift 4:
class MyCell: UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
for subview in self.subviews {
for sub in subview.subviews {
if String(describing: sub).range(of: "UITableViewCellActionButton") != nil {
for view in sub.subviews {
if String(describing: view).range(of: "UIButtonLabel") != nil {
if let label = view as? UILabel {
label.textColor = UIColor.black
}
}
}
}
}
}
}
}

iOS 11 solution:
Create UITableViewCell extension like this:
extension UITableViewCell {
/// Returns label of cell action button.
///
/// Use this property to set cell action button label color.
var cellActionButtonLabel: UILabel? {
for subview in self.superview?.subviews ?? [] {
if String(describing: subview).range(of: "UISwipeActionPullView") != nil {
for view in subview.subviews {
if String(describing: view).range(of: "UISwipeActionStandardButton") != nil {
for sub in view.subviews {
if let label = sub as? UILabel {
return label
}
}
}
}
}
}
return nil
}
}
And write this in your UITableViewCell
override func layoutSubviews() {
super.layoutSubviews()
cellActionButtonLabel?.textColor = .red
}
But there is a bug - if you pull the cell fast this code sometimes doesn't change the color.

You can add these two functions in your UITableViewCell subclass and call the setActionButtonsTitleColor function to set action buttons' title color.
func setActionButtonsTitleColor(color: UIColor) {
let actionButtons: [UIButton] = self.getActionButtons()
for actionButton in actionButtons {
actionButton.setTitleColor(color, for: .normal)
}
}
func getActionButtons() -> [UIButton] {
let actionButtons: [UIButton] = self.subviews.map {
(view: UIView) -> [UIView] in
return view.subviews
}
.joined()
.filter {
(view: UIView) -> Bool in
return String(describing: view).contains("UITableViewCellActionButton")
}.flatMap {
(view: UIView) -> UIButton? in
return view as? UIButton
}
return actionButtons
}

Thanks #Witek for sharing.
Your solution works but it's not stable. So, I try to update your code, and now it's working very well.
Put this code below in your UITableViewCell
override func layoutSubviews() {
super.layoutSubviews()
if let button = actionButton {
button.setTitleColor(.black, for: .normal)
}
}
override func layoutIfNeeded() {
super.layoutIfNeeded()
if let button = actionButton {
button.setTitleColor(.black, for: .normal)
}
}
var actionButton: UIButton? {
superview?.subviews
.filter({ String(describing: $0).range(of: "UISwipeActionPullView") != nil })
.flatMap({ $0.subviews })
.filter({ String(describing: $0).range(of: "UISwipeActionStandardButton") != nil })
.compactMap { $0 as? UIButton }.first
}

-(NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewRowAction *editAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:#"edit" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath)
{
// Action something here
}];
editAction.backgroundColor = [UIColor whiteColor];
[[UIButton appearance] setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
return #[editAction];

IF someone is still looking for an alternative solution:
You can use the swipecellkit pod
https://github.com/SwipeCellKit/SwipeCellKit
This lets you customize the label color, image color and even the entire background while perserving the actual look and feel of the native implementation.

Related

How to do custom font and color in UITableViewRowAction without Storyboard

I have classic TableView where you can delete item if you swipe and than clicking on the button. I know how to set custom background on the cell, but I can't find how I can set custom font and color for that.
Thank you for help!
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]? {
var deleteAction = UITableViewRowAction(style: UITableViewRowActionStyle.Default,
title: "Delete",
handler: {
(action:UITableViewRowAction!, indexPath:NSIndexPath!) -> Void in
println("Delete button clicked!")
})
deleteAction.backgroundColor = UIColor.redColor()
return [deleteAction]
}
Well, the only way I've found to set a custom font is to use the appearanceWhenContainedIn method of the UIAppearance protocol. This method isn't yet available in Swift, so you have to do it in Objective-C.
I made a class method in a utility Objective-C class to set it up:
+ (void)setUpDeleteRowActionStyleForUserCell {
UIFont *font = [UIFont fontWithName:#"AvenirNext-Regular" size:19];
NSDictionary *attributes = #{NSFontAttributeName: font,
NSForegroundColorAttributeName: [UIColor whiteColor]};
NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString: #"DELETE"
attributes: attributes];
/*
* We include UIView in the containment hierarchy because there is another button in UserCell that is a direct descendant of UserCell that we don't want this to affect.
*/
[[UIButton appearanceWhenContainedIn:[UIView class], [UserCell class], nil] setAttributedTitle: attributedTitle
forState: UIControlStateNormal];
}
This works, but it's definitely not ideal. If you don't include UIView in the containment hierarchy, then it ends up affecting the disclosure indicator as well (I didn't even realize the disclosure indicator was a UIButton subclass). Also, if you have a UIButton in your cell that is inside a subview in the cell, then that button will get affected by this solution as well.
Considering the complications, it might be better to just use one of the more customizable open source libraries out there for table cell swipe options.
I want to share my solution for ObjC, this is just a trick but works as expected for me.
- (NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath
{
// this just convert view to `UIImage`
UIImage *(^imageWithView)(UIView *) = ^(UIView *view) {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
};
// This is where the magic happen,
// The width and height must be dynamic (it's up to you how to implement it)
// to keep the alignment of the label in place
//
UIColor *(^getColorWithLabelText)(NSString*, UIColor*, UIColor*) = ^(NSString *text, UIColor *textColor, UIColor *bgColor) {
UILabel *lbDelete = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 47, 40)];
lbDelete.font = [UIFont boldSystemFontOfSize:11];
lbDelete.text = text;
lbDelete.textAlignment = NSTextAlignmentCenter;
lbDelete.textColor = textColor;
lbDelete.backgroundColor = bgColor;
return [UIColor colorWithPatternImage:imageWithView(lbDelete)];
};
// The `title` which is `#" "` is important it
// gives you the space you needed for the
// custom label `47[estimated width], 40[cell height]` on this example
//
UITableViewRowAction *btDelete;
btDelete = [UITableViewRowAction
rowActionWithStyle:UITableViewRowActionStyleDestructive
title:#" "
handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
NSLog(#"Delete");
[tableView setEditing:NO];
}];
// Implementation
//
btDelete.backgroundColor = getColorWithLabelText(#"Delete", [UIColor whiteColor], [YJColor colorWithHexString:#"fe0a09"]);
UITableViewRowAction *btMore;
btMore = [UITableViewRowAction
rowActionWithStyle:UITableViewRowActionStyleNormal
title:#" "
handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
NSLog(#"More");
[tableView setEditing:NO];
}];
// Implementation
//
btMore.backgroundColor = getColorWithLabelText(#"More", [UIColor darkGrayColor], [YJColor colorWithHexString:#"46aae8"]);
return #[btMore, btDelete];
}
[YJColor colorWithHexString:<NSString>]; is just to convert hex string to UIColor.
Check the example output screenshot.
If you use XCode's Debug View Hierarchy to look what is happening in UITableView when the swipe buttons are active, you'll see that UITableViewRowAction items translates to button called _UITableViewCellActionButton, contained in UITableViewCellDeleteConfirmationView. One way to change button's properties is to intercept it when it's added to UITableViewCell. In your UITableViewCell derived class write something like this:
private let buttonFont = UIFont.boldSystemFontOfSize(13)
private let confirmationClass: AnyClass = NSClassFromString("UITableViewCellDeleteConfirmationView")!
override func addSubview(view: UIView) {
super.addSubview(view)
// replace default font in swipe buttons
let s = subviews.flatMap({$0}).filter { $0.isKindOfClass(confirmationClass) }
for sub in s {
for button in sub.subviews {
if let b = button as? UIButton {
b.titleLabel?.font = buttonFont
}
}
}
}
This seems to work, at least for setting the font color:
- (void)setupRowActionStyleForTableViewSwipes {
UIButton *appearanceButton = [UIButton appearanceWhenContainedInInstancesOfClasses:#[[NSClassFromString(#"UITableViewCellDeleteConfirmationView") class]]];
[appearanceButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateNormal];
}
You could use UIButton.appearance to style the button inside the row action. Like so:
let buttonStyle = UIButton.appearance(whenContainedInInstancesOf: [YourViewController.self])
let font = UIFont(name: "Custom-Font-Name", size: 16.0)!
let string = NSAttributedString(string: "BUTTON TITLE", attributes: [NSAttributedString.Key.font : font, NSAttributedString.Key.foregroundColor : UIColor.green])
buttonStyle.setAttributedTitle(string, for: .normal)
Note: this will affect all of your buttons in this view controller.
Here is some Swift Code that might be helpful:
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) ->[AnyObject]? {
let attributes = [NSFontAttributeName: UIFont.systemFontOfSize(UIFont.systemFontSize())] as Dictionary!
UIButton.appearance().setAttributedTitle(NSAttributedString(string: "Your Button", attributes: attributes), forState: .Normal)
// Things you do...
}
This will manipulate all buttons in your application.
I think you can use this method to change the appearance only in one (or more, you can define it) viewcontrollers:
//create your attributes however you want to
let attributes = [NSFontAttributeName: UIFont.systemFontOfSize(UIFont.systemFontSize())] as Dictionary!
//Add more view controller types in the []
UIButton.appearanceWhenContainedInInstancesOfClasses([ViewController.self])
Hope this helped.
//The following code is in Swift3.1
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?
{
let rejectAction = TableViewRowAction(style: UITableViewRowActionStyle.default, title: "\u{2715}\nReject") { action, indexPath in
print("didtapReject")
}
rejectAction.backgroundColor = UIColor.gray
let approveAction = TableViewRowAction(style: UITableViewRowActionStyle.default, title: "\u{2713}\nApprove") { action, indexPath in
print("didtapApprove")
}
approveAction.backgroundColor = UIColor.orange
return [rejectAction, approveAction]
}
How to use a custom font?
It's pretty easy.
Firstly, you need to include your custom font files to your project.
Next, go to your info.plist file and add a new entry with the key "Fonts provided by application". Note that this entry should be an Array.
Then add the names of these files as elements of this array.
And that's it! All you need then is to use the font by its name like this
cell.textLabel.font = [UIFont fontWithName:#"FontName" size:16];
How to change the font color?
Even easier. All you need is
cell.textlabel.textcolor = UIColor.redColor()
Edit:
In your case you want to change the font of the RowAction. So I think of only 2 solutions. One to use [UIColor colorWithPatterImage:]
Or you can user [[UIButton appearance] setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal]; because the RowAction contains a button.

UIBarButtonItem: How can I find its frame?

I have a button in a toolbar. How can I grab its frame? Do UIBarButtonItems not have a frame property?
Try this one;
UIBarButtonItem *item = ... ;
UIView *view = [item valueForKey:#"view"];
CGFloat width;
if(view){
width=[view frame].size.width;
}
else{
width=(CGFloat)0.0 ;
}
This way works best for me:
UIView *targetView = (UIView *)[yourBarButton performSelector:#selector(view)];
CGRect rect = targetView.frame;
With Swift, if you needs to often work with bar button items, you should implement an extension like this:
extension UIBarButtonItem {
var frame: CGRect? {
guard let view = self.value(forKey: "view") as? UIView else {
return nil
}
return view.frame
}
}
Then in your code you can access easily:
if let frame = self.navigationItem.rightBarButtonItems?.first?.frame {
// do whatever with frame
}
Oof, lots of rough answers in this thread. Here's the right way to do it:
import UIKit
class ViewController: UIViewController {
let customButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
customButton.setImage(UIImage(named: "myImage"), for: .normal)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: customButton)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(self.customButton.convert(self.customButton.frame, to: nil))
}
}
Thanks to Anoop Vaidya for the suggested answer. An alternative could be (providing you know the position of the button in the toolbar)
UIView *view= (UIView *)[self.toolbar.subviews objectAtIndex:0]; // 0 for the first item
CGRect viewframe = view.frame;
Here's what I'm using in iOS 11 & Swift 4. It could be a little cleaner without the optional but I'm playing it safe:
extension UIBarButtonItem {
var view: UIView? {
return perform(#selector(getter: UIViewController.view)).takeRetainedValue() as? UIView
}
}
And usage:
if let barButtonFrame = myBarButtonItem.view?.frame {
// etc...
}
Edit: I don't recommend using this anymore. I ended up changing my implementation to use UIBarButtonItems with custom views, like Dan's answer
-(CGRect) getBarItemRc :(UIBarButtonItem *)item{
UIView *view = [item valueForKey:#"view"];
return [view frame];
}
You can create a UIBarButtonItem with a custom view, which is a UIButton, then you can do whatever you want. :]
in Swift 4.2 and inspired with luca
extension UIBarButtonItem {
var frame:CGRect?{
return (value(forKey: "view") as? UIView)?.frame
}
}
guard let frame = self.navigationItem.rightBarButtonItems?.first?.frame else{ return }
You can roughly calculate it by using properties like layoutMargins and frame on the navigationBar, combined with icon size guides from Human Interface Guidelines and take into count the current device orientation:
- (CGRect)rightBarButtonFrame {
CGFloat imageWidth = 28.0;
CGFloat imageHeight = UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeLeft || UIDevice.currentDevice.orientation == UIDeviceOrientationLandscapeRight ? 18.0 : 28.0;
UIEdgeInsets navigationBarLayoutMargins = self.navigationController.navigationBar.layoutMargins;
CGRect navigationBarFrame = self.navigationController.navigationBar.frame;
return CGRectMake(navigationBarFrame.size.width-(navigationBarLayoutMargins.right + imageWidth), navigationBarFrame.origin.y + navigationBarLayoutMargins.top, imageWidth, imageHeight);
}
Try this implementation:
#implementation UIBarButtonItem(Extras)
- (CGRect)frameInView:(UIView *)v {
UIView *theView = self.customView;
if (!theView.superview && [self respondsToSelector:#selector(view)]) {
theView = [self performSelector:#selector(view)];
}
UIView *parentView = theView.superview;
NSArray *subviews = parentView.subviews;
NSUInteger indexOfView = [subviews indexOfObject:theView];
NSUInteger subviewCount = subviews.count;
if (subviewCount > 0 && indexOfView != NSNotFound) {
UIView *button = [parentView.subviews objectAtIndex:indexOfView];
return [button convertRect:button.bounds toView:v];
} else {
return CGRectZero;
}
}
#end
You should do a loop over the subviews and check their type or their contents for identifying.
It is not safe to access view by kvo and you cannot be sure about the index.
Check out this answer: How to apply borders and corner radius to UIBarButtonItem? which explains how to loop over subviews to find the frame of a button.
I used a view on the bar button item with a tag on the view:
for view in bottomToolbar.subviews {
if let stackView = view.subviews.filter({$0 is UIStackView}).first {
//target view has tag = 88
if let targetView = stackView.subviews.filter({$0.viewWithTag(88) != nil}).first {
//do something with target view
}
}
}
Swift 4 up The current best way to do it is to access its frame from :
self.navigationItem.rightBarButtonItems by
let customView = navigationItem.rightBarButtonItems?.first?.customView // access the first added customView
Accessing this way is safer than accessing private api.
check out the answer in this :
After Add a CustomView to navigationItem, CustomView always return nil

ios Changing UIScrollView scrollbar color to different colors

How can we change color of UIScrollview's scroll indicator to something like blue, green etc.
I know we can change it to white, black. But other then these colors.
Many Thanks
Unfortunately you can't, of course you can always roll your own. These are your options:
UIScrollViewIndicatorStyleDefault:
The default style of scroll indicator, which is black with a white border. This style is good against any content background.
UIScrollViewIndicatorStyleBlack:
A style of indicator which is black and smaller than the default style. This style is good against a white content background.
UIScrollViewIndicatorStyleWhite:
A style of indicator is white and smaller than the default style. This style is good against a black content background.
Here's more safe Swift 3 method:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let verticalIndicator = scrollView.subviews.last as? UIImageView
verticalIndicator?.backgroundColor = UIColor.green
}
Both UIScrollView indicator are sub view of UIScrollView. So, we can
access subview of UIScrollView and change the property of subview.
1 .Add UIScrollViewDelegate
#interface ViewController : UIViewController<UIScrollViewDelegate>
#end
2. Add scrollViewDidScroll in implementation section
-(void)scrollViewDidScroll:(UIScrollView *)scrollView1
{
//get refrence of vertical indicator
UIImageView *verticalIndicator = ((UIImageView *)[scrollView.subviews objectAtIndex:(scrollView.subviews.count-1)]);
//set color to vertical indicator
[verticalIndicator setBackgroundColor:[UIColor redColor]];
//get refrence of horizontal indicator
UIImageView *horizontalIndicator = ((UIImageView *)[scrollView.subviews objectAtIndex:(scrollView.subviews.count-2)]);
//set color to horizontal indicator
[horizontalIndicator setBackgroundColor:[UIColor blueColor]];
}
Note:- Because these indicator update every time when you scroll
(means reset to default). SO, we put this code in scrollViewDidScroll
delegate method.
Demo available on GitHub - https://github.com/developerinsider/UIScrollViewIndicatorColor
Based on the answer of #Alex (https://stackoverflow.com/a/58415249/3876285), I'm posting just a little improvement to change the color of scroll indicators.
extension UIScrollView {
var scrollIndicators: (horizontal: UIView?, vertical: UIView?) {
guard self.subviews.count >= 2 else {
return (horizontal: nil, vertical: nil)
}
func viewCanBeScrollIndicator(view: UIView) -> Bool {
let viewClassName = NSStringFromClass(type(of: view))
if viewClassName == "_UIScrollViewScrollIndicator" || viewClassName == "UIImageView" {
return true
}
return false
}
let horizontalScrollViewIndicatorPosition = self.subviews.count - 2
let verticalScrollViewIndicatorPosition = self.subviews.count - 1
var horizontalScrollIndicator: UIView?
var verticalScrollIndicator: UIView?
let viewForHorizontalScrollViewIndicator = self.subviews[horizontalScrollViewIndicatorPosition]
if viewCanBeScrollIndicator(view: viewForHorizontalScrollViewIndicator) {
horizontalScrollIndicator = viewForHorizontalScrollViewIndicator.subviews[0]
}
let viewForVerticalScrollViewIndicator = self.subviews[verticalScrollViewIndicatorPosition]
if viewCanBeScrollIndicator(view: viewForVerticalScrollViewIndicator) {
verticalScrollIndicator = viewForVerticalScrollViewIndicator.subviews[0]
}
return (horizontal: horizontalScrollIndicator, vertical: verticalScrollIndicator)
}
}
If you don't add .subviews[0], you will get the deeper view and when you try to change the color of the indicator, this will appear with a weird white effect. That's because there is another view in front of it:
By adding .subviews[0] to each indicator view, once you try to change the color by calling:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
DispatchQueue.main.async() {
scrollView.scrollIndicators.vertical?.backgroundColor = UIColor.yourcolor
}
}
You will access to the first view and change the color properly:
Kudos to #Alex who posted a great solution 👍
in IOS 13
Try this one
func scrollViewDidScroll(_ scrollView: UIScrollView){
if #available(iOS 13, *) {
(scrollView.subviews[(scrollView.subviews.count - 1)].subviews[0]).backgroundColor = UIColor.themeColor(1.0) //verticalIndicator
(scrollView.subviews[(scrollView.subviews.count - 2)].subviews[0]).backgroundColor = UIColor.themeColor(1.0) //horizontalIndicator
} else {
if let verticalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 1)] as? UIImageView) {
verticalIndicator.backgroundColor = UIColor.themeColor(1.0)
}
if let horizontalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 2)] as? UIImageView) {
horizontalIndicator.backgroundColor = UIColor.themeColor(1.0)
}
}
}
Swift 2.0 :
Add UIScrollView Delegate.
func scrollViewDidScroll(scrollView: UIScrollView){
let verticalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 1)] as! UIImageView)
verticalIndicator.backgroundColor = UIColor.greenColor()
let horizontalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 2)] as! UIImageView)
horizontalIndicator.backgroundColor = UIColor.blueColor()
}
Try this it would certainly help you
for ( UIView *view in scrollBar.subviews ) {
if (view.tag == 0 && [view isKindOfClass:UIImageView.class])
{
UIImageView *imageView = (UIImageView *)view;
imageView.backgroundColor = [UIColor yellowColor];
}
}
Explanation: UIScrollBar is a collection of subviews. Here scrollBar indicator(vertical/horizontal) is the one of the subviews and it's an UIImageView.So if we set custom color to the UIImageView it effects scrollBar Indicator.
You can change an image of indicator, but you should do this repeadeatly
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.chageScrollIndicator()
}
func chageScrollIndicator (){
if let indicator = self.collection.subviews.last as? UIImageView {
let edge = UIEdgeInsets(top: 1.25,
left: 0,
bottom: 1.25,
right: 0)
indicator.image = UIImage(named: "ScrollIndicator")?.withRenderingMode(.alwaysTemplate).resizableImage(withCapInsets: edge)
indicator.tintColor = UIConfiguration.textColor
}
}
You can use this 2 image as template:
in IOS 13
Since iOS13 scroll indicators have class _UIScrollViewScrollIndicator, not UIImageView.
Many people used code like
let verticalIndicator: UIImageView = (scrollView.subviews[(scrollView.subviews.count - 1)] as! UIImageView)
It's not good idea, because they promised that last subview will be UIImageView :). Now it's not and they can get crash.
You can try following code to get scrollView indicators:
extension UIScrollView {
var scrollIndicators: (horizontal: UIView?, vertical: UIView?) {
guard self.subviews.count >= 2 else {
return (horizontal: nil, vertical: nil)
}
func viewCanBeScrollIndicator(view: UIView) -> Bool {
let viewClassName = NSStringFromClass(type(of: view))
if viewClassName == "_UIScrollViewScrollIndicator" || viewClassName == "UIImageView" {
return true
}
return false
}
let horizontalScrollViewIndicatorPosition = self.subviews.count - 2
let verticalScrollViewIndicatorPosition = self.subviews.count - 1
var horizontalScrollIndicator: UIView?
var verticalScrollIndicator: UIView?
let viewForHorizontalScrollViewIndicator = self.subviews[horizontalScrollViewIndicatorPosition]
if viewCanBeScrollIndicator(view: viewForHorizontalScrollViewIndicator) {
horizontalScrollIndicator = viewForHorizontalScrollViewIndicator
}
let viewForVerticalScrollViewIndicator = self.subviews[verticalScrollViewIndicatorPosition]
if viewCanBeScrollIndicator(view: viewForVerticalScrollViewIndicator) {
verticalScrollIndicator = viewForVerticalScrollViewIndicator
}
return (horizontal: horizontalScrollIndicator, vertical: verticalScrollIndicator)
}
}
If you need only one (h or v indicator) - it's better to cut this func and keep only one you need (to improve perfomance).
Also it would be good to call update func inside of DispatchQueue, to keep smoothness of scrolling.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
DispatchQueue.main.async {
scrollView.updateCustomScrollIndicatorView()
}
}
This is how the color of the scroll bar is changed:
//scroll view
UIScrollView *scView = [[UIScrollView alloc] init];
scView.frame = self.view.bounds; //scroll view occupies full parent views
scView.contentSize = CGSizeMake(400, 800);
scView.backgroundColor = [UIColor lightGrayColor];
scView.indicatorStyle = UIScrollViewIndicatorStyleBlack;
scView.showsHorizontalScrollIndicator = NO;
scView.showsVerticalScrollIndicator = YES;
scView.scrollEnabled = YES;
[self.view addSubview: scView];
If you wish to add image as well, here is the code for Swift 3
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let verticalIndicator = scrollView.subviews.last as? UIImageView
verticalIndicator?.image = UIImage(named: "imageName")
}
This works for UITableView and UICollectionView as well.
I wrote an article about this not so far ago. Unfortunately color of this bars defined by pre-defined images, so if you are going to change the color of bars some extra work will be required. Take a look to following link, you will definitely find an answer here since I tried to solve the same issue.
http://leonov.co/2011/04/uiscrollviews-scrollbars-customization/
I ran into the same problem recently so I decided to write a category for it.
https://github.com/stefanceriu/UIScrollView-ScrollerAdditions
[someScrollView setVerticalScrollerTintColor:someColor];
[someScrollView setHorizontalScrollerTintColor:someColor];`
It blends it with the original image so only the color will change. On the other hand, it can also be modified to provide a custom image for the scrollers to use.
Here is what I did in Swift 4, similar to previous answers. In my case I'm recoloring the image to be invisible, set correct corner radius and only execute this process once.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let color = UIColor.red
guard
let verticalIndicator = scrollView.subviews.last as? UIImageView,
verticalIndicator.backgroundColor != color,
verticalIndicator.image?.renderingMode != .alwaysTemplate
else { return }
verticalIndicator.layer.masksToBounds = true
verticalIndicator.layer.cornerRadius = verticalIndicator.frame.width / 2
verticalIndicator.backgroundColor = color
verticalIndicator.image = verticalIndicator.image?.withRenderingMode(.alwaysTemplate)
verticalIndicator.tintColor = .clear
}
please use below code on iOS Renderer
private bool _layouted;
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (!_layouted)
{
this.Layer.BorderColor = UIColor.Red.CGColor;
var Verticalbar = (UIImageView)this.Subviews[this.Subviews.Length - 1];
Verticalbar.BackgroundColor = Color.FromHex("#0099ff").ToUIColor();
var Horizontlebar = (UIImageView)this.Subviews[this.Subviews.Length - 2];
Horizontlebar.BackgroundColor = Color.FromHex("#0099ff").ToUIColor();
_layouted = true;
}
}
As for iOS 13 subviews changed so adding simple if, solved this issues.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 13.0) {
UIView *verticalIndicator = [scrollView.subviews lastObject];
verticalIndicator.backgroundColor = [UIColor redColor];
} else {
UIImageView *verticalIndicator = [scrollView.subviews lastObject];
verticalIndicator.backgroundColor = [UIColor redColor];
}
}
You can use custom UIScrollView scrollBars to implement color in scrollbars. For more details look here

Change default icon for moving cells in UITableView

I need to change default icon for moving cells in UITableView.
This one:
Is it possible?
This is a really hacky solution, and may not work long term, but may give you a starting point. The re-order control is a UITableViewCellReorderControl, but that's a private class, so you can't access it directly. However, you could just look through the hierarchy of subviews and find its imageView.
You can do this by subclassing UITableViewCell and overriding its setEditing:animated: method as follows:
- (void) setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing: editing animated: YES];
if (editing) {
for (UIView * view in self.subviews) {
if ([NSStringFromClass([view class]) rangeOfString: #"Reorder"].location != NSNotFound) {
for (UIView * subview in view.subviews) {
if ([subview isKindOfClass: [UIImageView class]]) {
((UIImageView *)subview).image = [UIImage imageNamed: #"yourimage.png"];
}
}
}
}
}
}
Or in Swift
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
if editing {
for view in subviews where view.description.contains("Reorder") {
for case let subview as UIImageView in view.subviews {
subview.image = UIImage(named: "yourimage.png")
}
}
}
}
Be warned though... this may not be a long term solution, as Apple could change the view hierarchy at any time.
Ashley Mills' answer was excellent at the time it was offered, but as others have noted in the comments, the view hierarchy has changed from version to version of iOS. In order to properly find the reorder control, I'm using an approach that traverses the entire view hierarchy; hopefully this will give the approach an opportunity to continue working even if Apple changes the view hierarchy.
Here's the code I'm using to find the reorder control:
-(UIView *) findReorderView:(UIView *) view
{
UIView *reorderView = nil;
for (UIView *subview in view.subviews)
{
if ([[[subview class] description] rangeOfString:#"Reorder"].location != NSNotFound)
{
reorderView = subview;
break;
}
else
{
reorderView = [self findReorderView:subview];
if (reorderView != nil)
{
break;
}
}
}
return reorderView;
}
And here's the code I'm using to override the -(void) setEditing:animated: method in my subclass:
-(void) setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (editing)
{
// I'm assuming the findReorderView method noted above is either
// in the code for your subclassed UITableViewCell, or defined
// in a category for UIView somewhere
UIView *reorderView = [self findReorderView:self];
if (reorderView)
{
// I'm setting the background color of the control
// to match my cell's background color
// you might need to do this if you override the
// default background color for the cell
reorderView.backgroundColor = self.contentView.backgroundColor;
for (UIView *sv in reorderView.subviews)
{
// now we find the UIImageView for the reorder control
if ([sv isKindOfClass:[UIImageView class]])
{
// and replace it with the image we want
((UIImageView *)sv).image = [UIImage imageNamed:#"yourImage.png"];
// note: I have had to manually change the image's frame
// size to get it to display correctly
// also, for me the origin of the frame doesn't seem to
// matter, because the reorder control will center it
sv.frame = CGRectMake(0, 0, 48.0, 48.0);
}
}
}
}
}
Swift 4
// Change default icon (hamburger) for moving cells in UITableView
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let imageView = cell.subviews.first(where: { $0.description.contains("Reorder") })?.subviews.first(where: { $0 is UIImageView }) as? UIImageView
imageView?.image = #imageLiteral(resourceName: "new_hamburger_icon") // give here your's new image
imageView?.contentMode = .center
imageView?.frame.size.width = cell.bounds.height
imageView?.frame.size.height = cell.bounds.height
}
Swift version of Rick's answer with few improvements:
override func setEditing(editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
if editing {
if let reorderView = findReorderViewInView(self),
imageView = reorderView.subviews.filter({ $0 is UIImageView }).first as? UIImageView {
imageView.image = UIImage(named: "yourImage")
}
}
}
func findReorderViewInView(view: UIView) -> UIView? {
for subview in view.subviews {
if String(subview).rangeOfString("Reorder") != nil {
return subview
}
else {
findReorderViewInView(subview)
}
}
return nil
}
Updated solution of Ashley Mills (for iOS 7.x)
if (editing) {
UIView *scrollView = self.subviews[0];
for (UIView * view in scrollView.subviews) {
NSLog(#"Class: %#", NSStringFromClass([view class]));
if ([NSStringFromClass([view class]) rangeOfString: #"Reorder"].location != NSNotFound) {
for (UIView * subview in view.subviews) {
if ([subview isKindOfClass: [UIImageView class]]) {
((UIImageView *)subview).image = [UIImage imageNamed: #"moveCellIcon"];
}
}
}
}
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
for (UIControl *control in cell.subviews)
{
if ([control isMemberOfClass:NSClassFromString(#"UITableViewCellReorderControl")] && [control.subviews count] > 0)
{
for (UIControl *someObj in control.subviews)
{
if ([someObj isMemberOfClass:[UIImageView class]])
{
UIImage *img = [UIImage imageNamed:#"reorder_icon.png"];
((UIImageView*)someObj).frame = CGRectMake(0.0, 0.0, 43.0, 43.0);
((UIImageView*)someObj).image = img;
}
}
}
}
}
I use editingAccessoryView to replace reorder icon.
Make a subclass of UITableViewCell.
Override setEditing. Simply hide reorder control and set editingAccessoryView to an uiimageview with your re-order image.
- (void) setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing: editing animated: YES];
self.showsReorderControl = NO;
self.editingAccessoryView = editing ? [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"yourReorderIcon"]] : nil;
}
If you are not using editing accessory view, this may be a good choice.
I could not get any other answer to work for me, but I found a solution.
Grzegorz R. Kulesza's answer almost worked for me but I had to make a couple changes.
This works with Swift 5 and iOS 13:
// Change default reorder icon in UITableViewCell
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let imageView = cell.subviews.first(where: { $0.description.contains("Reorder") })?.subviews.first(where: { $0 is UIImageView }) as? UIImageView
imageView?.image = UIImage(named: "your_custom_reorder_icon.png")
let size = cell.bounds.height * 0.6 // scaled for padding between cells
imageView?.frame.size.width = size
imageView?.frame.size.height = size
}
I did this on iOS 12 with swift 4.2
I hope this helps:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
for view in cell.subviews {
if view.self.description.contains("UITableViewCellReorderControl") {
for sv in view.subviews {
if (sv is UIImageView) {
(sv as? UIImageView)?.image = UIImage(named: "your_image")
(sv as? UIImageView)?.contentMode = .center
sv.frame = CGRect(x: 0, y: 0, width: 25, height: 25)
}
}
}
}
}
After debuging the UITableViewCell, you can use KVC in UITableViewCell subclass to change it.
// key
static NSString * const kReorderControlImageKey = #"reorderControlImage";
// setting when cellForRow calling
UIImage *customImage;
[self setValue:customImage forKeyPath:kReorderControlImageKey];
// to prevent crash
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if ([key isEqualToString:kReorderControlImageKey]) return;
else [super setValue:value forUndefinedKey:key];
}
You can also simply add your own custom reorder view above all others inside your cell.
All you have to do is ensure this custom view is always above others, which can be checked in [UITableViewDelegate tableView: willDisplayCell: forRowAtIndexPath: indexPath:].
In order to allow the standard reorder control interaction, your custom view must have its userInteractionEnabled set to NO.
Depending on how your cell looks like, you might need a more or less complex custom reorder view (to mimic the cell background for exemple).
Swift 5 solution:
Subclass UITableViewCell and override didAddSubview method:
override func didAddSubview(_ subview: UIView) {
if !subview.description.contains("Reorder") { return }
(subview.subviews.first as? UIImageView)?.removeFromSuperview()
let imageView = UIImageView()
imageView.image = UIImage()
subview.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.height.width.equalTo(24)
make.centerX.equalTo(subview.snp.centerX)
make.centerY.equalTo(subview.snp.centerY)
}
}
I've used SnapKit to set constraints, you can do it in your way.
Please note, it could be temporary solution in order of Apple updates.
Working with iOS 16 and Swift 5
I tried the above solution, but sometimes my custom image was not displayed in some cells.
This code works fine for me into the UITableViewCell subclass:
private lazy var customReorderImgVw: UIImageView = {
let img = UIImage(named: "imgCustomReorder")!
let imgVw = UIImageView(image: img)
imgVw.contentMode = .center
imgVw.frame = CGRect(origin: .zero, size: img.size)
imgVw.alpha = 0
return imgVw
}()
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
if editing {
for subVw in subviews {
if "\(subVw.classForCoder)" == "UITableViewCellReorderControl" {
subVw.subviews.forEach { $0.removeFromSuperview() }
customReorderImgVw.center.y = subVw.center.y
subVw.addSubview(customReorderImgVw)
break
}
}
}
showOrHideCustomReorderView(isToShow: editing)
}
private func showOrHideCustomReorderView(isToShow: Bool) {
let newAlpha: CGFloat = (isToShow ? 1 : 0)
UIView.animate(withDuration: 0.25) {
self.customReorderImgVw.alpha = newAlpha
}
}

Which is the best way to change the color/view of disclosure indicator accessory view in a table view cell in iOS?

I need to change the color of the disclosureIndicatorView accessory in a UITableViewCell.
I think there are two ways to get this done, but I'm not able to figure out which one's the optimum. So here is what I think I can do.
There is a property of UITableViewCell - accessoryView. So I can use setAccessoryView:(UIView *)view and pass view as the UIImageView holding the image that I want.
I have written an utility class which creates the content view (stuff like background color, adding other stuff, etc) for my cell and I add this content view to the cell in UITableViewDelegate. The other option is to draw a UIImage overriding the drawRect method of CustomContentView utility class.
Performing option 1 - I can get the things done the apple way. Just give them the view and they do the rest. But I guess adding a new UIView object to every row might turn out to be a heavy object allocation and decreasing the frame rate. As compared to just a UIImage object in my contentView. I believe UIImage is lighter than UIView.
Please throw some light people and help me decide over it.
Great post on Cocoanetics that addresses this. The UIControl class inherits the properties selected, enabled and highlighted Custom-Colored Disclosure Indicators
If you're interested in drawing the indicator, instead of using an image file, here's code I worked out to do so:
// (x,y) is the tip of the arrow
CGFloat x = CGRectGetMaxX(self.bounds) - RIGHT_MARGIN;
CGFloat y = CGRectGetMidY(self.bounds);
const CGFloat R = 4.5;
CGContextRef ctxt = UIGraphicsGetCurrentContext();
CGContextMoveToPoint(ctxt, x-R, y-R);
CGContextAddLineToPoint(ctxt, x, y);
CGContextAddLineToPoint(ctxt, x-R, y+R);
CGContextSetLineCap(ctxt, kCGLineCapSquare);
CGContextSetLineJoin(ctxt, kCGLineJoinMiter);
CGContextSetLineWidth(ctxt, 3);
// If the cell is highlighted (blue background) draw in white; otherwise gray
if (CONTROL_IS_HIGHLIGHTED) {
CGContextSetRGBStrokeColor(ctxt, 1, 1, 1, 1);
} else {
CGContextSetRGBStrokeColor(ctxt, 0.5, 0.5, 0.5, 1);
}
CGContextStrokePath(ctxt);
If you make a custom UIView subclass, do the above in the drawRect: method, and use that as your accessory view, you'll be able to make the color anything you want.
An accessory view (custom or UIImageView won't be a major performance problem as long as you are properly recycling UITableViewCell instances.
Here is an implementation that works in iOS 8+.
It does exactly what's asked for:
change the color of the original Apple disclosure indicator to a custom color.
Use it like this:
#import "UITableViewCell+DisclosureIndicatorColor.h"
// cell is a UITableViewCell
cell.disclosureIndicatorColor = [UIColor redColor]; // custom color
[cell updateDisclosureIndicatorColorToTintColor]; // or use global tint color
UITableViewCell+DisclosureIndicatorColor.h
#interface UITableViewCell (DisclosureIndicatorColor)
#property (nonatomic, strong) UIColor *disclosureIndicatorColor;
- (void)updateDisclosureIndicatorColorToTintColor;
#end
UITableViewCell+DisclosureIndicatorColor.m
#implementation UITableViewCell (DisclosureIndicatorColor)
- (void)updateDisclosureIndicatorColorToTintColor {
[self setDisclosureIndicatorColor:self.window.tintColor];
}
- (void)setDisclosureIndicatorColor:(UIColor *)color {
NSAssert(self.accessoryType == UITableViewCellAccessoryDisclosureIndicator,
#"accessory type needs to be UITableViewCellAccessoryDisclosureIndicator");
UIButton *arrowButton = [self arrowButton];
UIImage *image = [arrowButton backgroundImageForState:UIControlStateNormal];
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
arrowButton.tintColor = color;
[arrowButton setBackgroundImage:image forState:UIControlStateNormal];
}
- (UIColor *)disclosureIndicatorColor {
NSAssert(self.accessoryType == UITableViewCellAccessoryDisclosureIndicator,
#"accessory type needs to be UITableViewCellAccessoryDisclosureIndicator");
UIButton *arrowButton = [self arrowButton];
return arrowButton.tintColor;
}
- (UIButton *)arrowButton {
for (UIView *view in self.subviews)
if ([view isKindOfClass:[UIButton class]])
return (UIButton *)view;
return nil;
}
#end
In swift 3, I have adapted the solution from #galambalazs as a class extention as follows:
import UIKit
extension UITableViewCell {
func setDisclosure(toColour: UIColor) -> () {
for view in self.subviews {
if let disclosure = view as? UIButton {
if let image = disclosure.backgroundImage(for: .normal) {
let colouredImage = image.withRenderingMode(.alwaysTemplate);
disclosure.setImage(colouredImage, for: .normal)
disclosure.tintColor = toColour
}
}
}
}
}
Hope this helps some.
Use an UIImageView. This will also allow you to change the image when the cell is selected:
UIImageView* arrowView = [[UIImageView alloc] initWithImage:normalImage];
arrowView.highlightedImage = selectedImage;
cell.accessoryView = arrowView;
[arrowView release];
But I guess adding a new UIView object to every row might turn out to be a heavy obj allocation and decreasing the frame rate. As compared to just a UIImage object in my contentView. I believe UIImage is lighter than UIView.
Drawing an image directly will almost certainly have better performance than adding a subview. You have to determine if that extra performance is necessary. I've used a few accessory views for custom disclosure indicators on basic cells and performance was fine. However, if you're already doing custom drawing for the content rect, it might not be that difficult to do the accessory view also.
benzado's solution works fine, but it showed a black background. In the UIView class that you setup (the one who's drawRect function you put in his code) needs to have the following initWithFrame implementation in order for the disclosure drawing to have a transparent background:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setBackgroundColor:[UIColor clearColor]];
// Initialization code.
}
return self;
}
Naturally, you can set this to whatever color you want...
While galambalazs' answer works, it should be noted that it is somewhat of a hack as it indirectly accesses and updates Apple's private implementation of the disclosure indicator. At best, it could stop working in future iOS releases, and at worst lead to App Store rejection. Setting the accessoryView is still the approved method until Apple exposes a property for directly setting the color.
Regardless, here is the Swift implementation of his answer for those who may want it:
Note: cell.disclosureIndicatorColor has to be set after cell.accessoryType = .DisclosureIndicator is set so that the disclosureIndicator button is available in the cell's subviews:
extension UITableViewCell {
public var disclosureIndicatorColor: UIColor? {
get {
return arrowButton?.tintColor
}
set {
var image = arrowButton?.backgroundImageForState(.Normal)
image = image?.imageWithRenderingMode(.AlwaysTemplate)
arrowButton?.tintColor = newValue
arrowButton?.setBackgroundImage(image, forState: .Normal)
}
}
public func updateDisclosureIndicatorColorToTintColor() {
self.disclosureIndicatorColor = self.window?.tintColor
}
private var arrowButton: UIButton? {
var buttonView: UIButton?
self.subviews.forEach { (view) in
if view is UIButton {
buttonView = view as? UIButton
return
}
}
return buttonView
}
}
In iOS 13+ the disclosure indicator is set via a non-templeted UIImage, which is determined by the user's dark mode preference. Since the image is non-templated, it will not respect the cell's tintColor property. In other words, the dark mode preference has top priority. If you don't wan't to use the disclosure indicator derived by iOS, you will have to use a custom image.
As a contribution to the solution of #benzado I swiftified his code as followed:
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let context = UIGraphicsGetCurrentContext();
let right_margin : CGFloat = 15.0
let length : CGFloat = 4.5;
// (x,y) is the tip of the arrow
let x = CGRectGetMaxX(self.bounds) - right_margin;
let y = CGRectGetMidY(self.bounds);
CGContextMoveToPoint(context, x - length, y - length);
CGContextAddLineToPoint(context, x, y);
CGContextAddLineToPoint(context, x - length, y + length);
CGContextSetLineCap(context, .Round);
CGContextSetLineJoin(context, .Miter);
CGContextSetLineWidth(context, 2.5);
if (self.highlighted)
{
CGContextSetStrokeColorWithColor(context, UIColor.appColorSelected().CGColor);
}
else
{
CGContextSetStrokeColorWithColor(context, UIColor.appColor().CGColor);
}
CGContextStrokePath(context);
}
With a change of the app color a call to setNeedsDisplay() on the UITableCellView will update the color. I like to avoid the use of UIImage objects in cell views.
A swift 3 version of the CocoaNetics solution
public class DisclosureIndicator: UIControl {
public static func create(color: UIColor?, highlightedColor: UIColor?) -> DisclosureIndicator{
let indicator = DisclosureIndicator(frame: CGRect(x: 0, y: 0, width: 11, height: 15))
if let color = color { indicator.color = color }
if let color = highlightedColor { indicator.highlightedColor = color }
return indicator
}
public var color: UIColor = .black
public var highlightedColor: UIColor = .white
override public init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = .clear
}
override public func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()!;
// (x,y) is the tip of the arrow
let x = self.bounds.maxX - 3.0;
let y = self.bounds.midY;
let length : CGFloat = 4.5;
context.move(to: CGPoint(x: x - length, y: y - length))
context.addLine(to: CGPoint(x: x, y: y))
context.addLine(to: CGPoint(x: x - length, y: y + length))
context.setLineCap(.round)
context.setLineJoin(.miter)
context.setLineWidth(3)
context.setStrokeColor((isHighlighted ? highlightedColor : color).cgColor)
context.strokePath()
}
override public var isHighlighted: Bool {
get {
return super.isHighlighted
}
set {
super.isHighlighted = newValue
setNeedsDisplay()
}
}
}
Change the tint colour of table view cell.
Check the screenshot to do the same via Storyboard.

Resources