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
}
}
Related
I've been trying to do something in Swift that is very similar to the below code sample (Objective C). Is anyone able to provide a Swift implementation of the function shown below or something similar?
- (UIImageView *) getSectorByValue:(int)value {
UIImageView *res;
NSArray *views = [container subviews];
for (UIImageView *im in views) {
if (im.tag == value)
res = im;
}
return res;
}
This is a similar implementation in Swift:
func getSectorByValue(value: Int) -> UIImageView? {
for subView in container.subviews {
if subView.tag == value {
return subView as? UIImageView
}
}
return nil
}
Something like this should work.
func getSectorByValue(value: Int) -> UIImageView? {
let views = container.subviews()
if let i = views.indexOf({$0.tag == value}) {
return views[i] as? UIImageView
}
return nil
}
Looping though the subviews manually is needlessly complex.
There is a UIView method viewWithTag that fetches the desired view with 1 line of code:
func getSectorByValue(value: Int) -> UIImageView?
{
return container.viewWithTag(value) as? UIImageView
}
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.
I have UITableView with hided separator line, and when I dragging cell, shadows appears some kind as borders comes out on up and down. How to hide this? Please see example:
Great thanks!
So, I have answer, just subclass of UITableView with method:
- (void) didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
if([subview.class.description isEqualToString:#"UIShadowView"]) {
subview.hidden = YES;
}
}
NoShadowTableView.m
#import "NoShadowTableView.h"
#interface NoShadowTableView ()
{
// iOS7
__weak UIView* wrapperView;
}
#end
#implementation NoShadowTableView
- (void) didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
// iOS7
if(wrapperView == nil && [[[subview class] description] isEqualToString:#"UITableViewWrapperView"])
wrapperView = subview;
// iOS6
if([[[subview class] description] isEqualToString:#"UIShadowView"])
[subview setHidden:YES];
}
- (void) layoutSubviews
{
[super layoutSubviews];
// iOS7
for(UIView* subview in wrapperView.subviews)
{
if([[[subview class] description] isEqualToString:#"UIShadowView"])
[subview setHidden:YES];
}
}
#end
Quick hack is subclassing UITableViewCell and add method:
override func layoutSubviews() {
super.layoutSubviews()
superview?.subviews.filter({ "\(type(of: $0))" == "UIShadowView" }).forEach { (sv: UIView) in
sv.removeFromSuperview()
}
}
This code works for me!
import UIKit
class NoShadowTableView: UITableView {
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if "\(type(of: subview))" == "UIShadowView" {
subview.removeFromSuperview()
}
}
}
I was facing a similar problem by using default UITableView reordering controls. So I used this external third-party library which solved my problem.
https://github.com/shusta/ReorderingTableViewController
Hope this helps
Swift 3 implementation (removed iOS6 support)
import UIKit
class NoShadowTableView: UITableView {
weak var wrapperView: UIView?
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if wrapperView == nil && "\(type(of: subview))" == "UITableViewWrapperView" {
wrapperView = subview
}
}
override func layoutSubviews() {
super.layoutSubviews()
wrapperView?.subviews.forEach({ view in
if "\(type(of: view))" == "UIShadowView" {
view.isHidden = true
}
})
}
}
For me hacks with the UIShadowView didn't work. I checked the solution on iOS 10. But one line in a cell class has done the trick:
override func layoutSubviews() {
super.layoutSubviews()
self.subviews.filter{ $0 is UIImageView }.forEach { $0.isHidden = true }
}
Swift 4 solution; use extended uitableviewcontroller because now shadow is in the table and only added when cell is moved.
class UITableViewControllerEx: UITableViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
view.subviews.filter({ String(describing: type(of: $0)) == "UIShadowView" }).forEach { (sv: UIView) in
sv.isHidden = true
}
}
}
All of the other answers appear to be using private APIs which is obviously not a good thing to do. Additionally, they don't appear to work anymore (at least not on iOS 16).
I found that using UIDragPreviewParameters allowed me to get rid of the horrible background colour and reshape the shadow to fit one of the subviews in my table view cell.
-(UIDragPreviewParameters *)tableView:(UITableView *)tableView dragPreviewParametersForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
return [self createParametersForTableView:tableView atIndexPath:indexPath];
}
-(UIDragPreviewParameters *)tableView:(UITableView *)tableView dropPreviewParametersForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self createParametersForTableView:tableView atIndexPath:indexPath];
}
-(UIDragPreviewParameters *)createParametersForTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {
// Get the selected table view cell.
CustomCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// Create the dragged cell preview parameters.
UIDragPreviewParameters *parameters = [[UIDragPreviewParameters alloc] init];
[parameters setBackgroundColor:[UIColor clearColor]];
[parameters setVisiblePath:[UIBezierPath bezierPathWithRoundedRect:cell.customSubView.frame cornerRadius:22.2]];
[parameters setShadowPath:[UIBezierPath bezierPathWithRoundedRect:cell.customSubView.frame cornerRadius:22.2]];
return parameters;
}
If you use this method, you will need to implement two delegates: UITableViewDragDelegate and UITableViewDropDelegate.
[yourTableView setDragDelegate:self];
[yourTableView setDropDelegate:self];
The drag delegate method will allow you to control the preview background when dragging the cell. When dropping the cell, you need to use the drop delegate method to avoid the preview background going back to the default white background.
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
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