【支持iOS11】UITableView左滑刪除自定義 - 實現多選項並使用自定義圖片

寫在開頭:本文所介紹的方法使用的是iOS8-10中的API,不過支持在iOS11上運行。以後會寫一篇介紹如何用iOS11的新API來實現,到時通知你們。(2017-08-16)xcode


本文介紹兩種UITableView左滑菜單的實現方法,1. 默認, 2. 自定義。效果以下:app

1. 系統默認效果
字體

 
swipe-default.PNG

 

2. 自定義圖標效果 (相似「郵件」應用)
ui

 
swipe-customize-1.PNG

 


目錄:

  1. 系統默認圖標實現方法
  2. 自定義圖標實現方法
    a. 自定義多個左滑菜單選項
    b. 自定義左滑選項外觀
      UITableView視圖層級(iOS8-10, 11)
      具體實現方法 (支持iOS8-10, 11)
  3. TableCell上有其餘按鈕的處理方法

1. 系統默認圖標實現方法

若是隻須要使用默認圖標,只須要在對應的TableViewController裏實現數據源方法tableView:commitEditingStyle:forRowAtIndexPath就好了:this

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { [self removeAction:indexPath.section]; // 在此處自定義刪除行爲 } else { DEBUG_OUT(@"Unhandled editing style: %ld", (long) editingStyle); } } 

向左滑動table cell,該cell會自動進入編輯模式(cell.isEditing = 1),並在右邊出現刪除按鈕,紅底白字,按鈕上的文字會根據系統語言自動改變;點擊該按鈕則觸發commitEditingStyle執行相應的動做。atom

若是不進行自定義,默認的左滑菜單隻會有一個按鈕,不過按鈕上的文字能夠用隨意進行更改,按鈕的寬度會根據文字標題長度自動調整,須要本身支持多語言:spa

- (NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath { return @"想寫什麼都行"; } 

 

 

效果以下:
 
change-title.PNG

PS:若是除了文字內容外,還想調整其它,好比文字顏色,背景顏色,選項的寬高等,則能夠拿到對應的UIButton之後直接修改,具體方法參照本文2) b部分。
3d


2. 自定義圖標實現方法

我認爲自定義又分爲兩個層次: a. 自定義多個左滑菜單選項b. 自定義左滑選項外觀 .代理

a. 自定義多個左滑菜單選項

若是須要超過一個左滑選項,須要實現代理方法tableView:editActionsForRowAtIndexPath,在裏面建立多個UITableViewRowAction:code

- (NSArray*)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(nonnull NSIndexPath *)indexPath { // delete action UITableViewRowAction *deleteAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:NSLocalizedString(@"DeleteLabel", @"") handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) { [tableView setEditing:NO animated:YES]; // 這句很重要,退出編輯模式,隱藏左滑菜單 [self removeNotificationAction:index]; }]; // read action // 根據cell當前的狀態改變選項文字 NSInteger index = indexPath.section; BOOL isRead = [[NotificationManager instance] read:index]; NSString *readTitle = isRead ? @"Unread" : @"Read"; // 建立action UITableViewRowAction *readAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:readTitle handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) { [tableView setEditing:NO animated:YES]; // 這句很重要,退出編輯模式,隱藏左滑菜單 [[NotificationManager instance] setRead:!isRead index:index]; }]; return @[deleteAction, readAction]; } 

能夠看到這裏咱們建立了delete和read兩個action。這是由於實現了該方法之後,1) 中用的commitEditingStyle:forRowAtIndexPath就不會被觸發了,因此刪除按鈕也須要本身定義。

[tableView setEditing:NO animated:YES]; 這一行代碼很重要,它的效果是在點擊以後退出編輯模式,關閉左滑菜單。若是忘了加這一句的話,即便點擊了按鈕cell也不會還原。在1) 中使用默認模式的時候,系統會自動幫咱們調用這一句,如今則須要手動調用。

對建立並返回的每一個action,apple library會自動幫咱們生成一個對應按鈕,配置好基本的交互,並添加到左滑菜單中。

 

效果以下:
 
two-actions-2.PNG

上圖咱們對兩個action都指定了UITableViewRowActionStyleNormal(灰底白字),不過其實有幾種不一樣的預設外觀 (不要問我爲啥有兩個都是紅底白字。。。)
  UITableViewRowActionStyleNormal:灰底白字
  UITableViewRowActionStyleDefault:紅底白字
  UITableViewRowActionStyleDestructive:紅底白字

咱們還能夠更改action button的背景色,在建立action的時候添加一行代碼便可:

deleteAction.backgroundColor = [UIColor orangeColor]; readAction.backgroundColor = [UIColor blueColor]; 

 

 

效果以下:
 
change-background-2.PNG

 


2b. 自定義左滑選項外觀

自定義左滑選項外觀的資料不多,我作的時候找得至關辛苦。不事後來理解了UITableView的視圖層級,一切就變得很簡單了。

UITableView視圖層級(iOS8-10, 11)

和iOS8-10相比,iOS11的左滑選項的視圖層級有了較大改變。最顯著的改變是從是UITableViewCell的子視圖變成了UITableView的子視圖。總結一下就是:

iOS 8-10: UITableView -> UITableViewCell -> UITableViewCellDeleteConfirmationView -> _UITableViewCellActionButton
iOS 11 (Xcode 8編譯): UITableView -> UITableViewWrapperView -> UISwipeActionPullView -> UISwipeActionStandardButton
iOS 11 (Xcode 9編譯): UITableView -> UISwipeActionPullView -> UISwipeActionStandardButton

不想看原理的看到這裏就能夠直接跳到下面去看具體的實現方法了, 有興趣看每一層具體有些什麼的同窗能夠繼續。


iOS8-10下的層級:

在tableView代理方法裏設置斷點打印發現,正常狀態下cell上只有兩個subview:

(lldb) po [tableCell subviews]
<__NSArrayM 0x14de75670>( <UITableViewCellContentView: 0x14dd56940; frame = (0 0; 440 105); opaque = NO; gestureRecognizers = <NSArray: 0x14dd65ff0>; layer = <CALayer: 0x14dd56ac0>>, <_UITableViewCellSeparatorView: 0x14dd73810; frame = (15 154.5; 347 0.5); layer = <CALayer: 0x14dd736f0>> ) 

而在左滑進入editing mode以後,就變成了3個,多出來一個 叫作UITableViewCellDeleteConfirmationView的子視圖:

(lldb) po [tableCell subviews]
<__NSArrayM 0x14de75670>( <UITableViewCellDeleteConfirmationView: 0x14de737d0; frame = (375 0; 0 105); clipsToBounds = YES; autoresize = H; animations = { bounds.origin=<CASpringAnimation: 0x14db56aa0>; bounds.size=<CASpringAnimation: 0x14db5b2e0>; position=<CASpringAnimation: 0x14db498e0>; bounds.origin-2=<CASpringAnimation: 0x14db8a3d0>; bounds.size-2=<CASpringAnimation: 0x14db26c10>; }; layer = <CALayer: 0x14de72fc0>>, <UITableViewCellContentView: 0x14dd56940; frame = (0 0; 375 105); opaque = NO; gestureRecognizers = <NSArray: 0x14dd65ff0>; layer = <CALayer: 0x14dd56ac0>>, <_UITableViewCellSeparatorView: 0x14dd73810; frame = (15 154.5; 347 0.5); layer = <CALayer: 0x14dd736f0>> ) 

再進一步查看這個多出來的UITableViewCellDeleteConfirmationView的子視圖,發現兩個UIButton:

(lldb) po tableCell.subviews[0].subviews <__NSArrayM 0x14dea14c0>( <_UITableViewCellActionButton: 0x14de93ea0; frame = (71.5 0; 80.5 105); opaque = NO; autoresize = H; layer = <CALayer: 0x14de93150>>, <_UITableViewCellActionButton: 0x14de9d900; frame = (0 0; 71.5 105); opaque = NO; autoresize = H; layer = <CALayer: 0x14de9dbb0>> ) 

最後再打印一下這兩個UIButton的title,發現分別是「Read」和「Delete」。

也就是說,這兩個UIButton,分別對應咱們在a部分建立的兩個UITableViewRowAction。因此咱們只要遍歷UITableViewCell的子視圖,拿到對應UIButton的reference,什麼修改高度,添加圖片,修改字體,都是手到擒來。


iOS11下的層級 (用Xcode 8編譯)

依然在tableView代理方法裏設置斷點打印發現,UITableViewCell下面沒有UITableViewCellDeleteConfirmationView子視圖了,不過在UITableViewWrapperView下面,多了一個UISwipeActionPullView。

(lldb) po self.tableView.subviews <__NSArrayM 0x1c0652cf0>( <UITableViewWrapperView: 0x105094200; frame = (0 0; 1000 1000); gestureRecognizers = <NSArray: 0x1c4457e50>; layer = <CALayer: 0x1c4224de0>; contentOffset: {0, 0}; contentSize: {1000, 1000}; adjustedContentInset: {0, 0, 0, 0}>, <UIImageView: 0x10466bb20; frame = (994.5 733; 2.5 220); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x1c0426900>> ) (lldb) po [self.tableView.subviews[0] subviews] <__NSArrayM 0x1c04457f0>( <UITableViewCell: 0x10e17d800; baseClass = UITableViewCell; frame = (0 277.5; 375 212.5); autoresize = W; layer = <CALayer: 0x1c0427c80>>, <: UITableViewCell: 0x10e165200; baseClass = UITableViewCell; frame = (0 8; 375 261.5); autoresize = W; userInteractionEnabled = NO; layer = <CALayer: 0x1c0423300>>, <UISwipeActionPullView: 0x10de126a0; cellEdge = UIRectEdgeRight, actions = <NSArray: 0x1c44430c0>> ) 

再看一下這個UISwipeActionPullView子視圖,發現了咱們要找的選項按鈕:

(lldb) po subview2.subviews
<__NSArrayM 0x1c0452ba0>( <UISwipeActionStandardButton: 0x14deae190; frame = (0 0; 591 104.5); opaque = NO; autoresize = W+H; tintColor = UIExtendedGrayColorSpace 1 1; layer = <CALayer: 0x1c0435860>>, <UISwipeActionStandardButton: 0x14debd7c0; frame = (0 0; 591 104.5); opaque = NO; autoresize = W+H; tintColor = UIExtendedGrayColorSpace 1 1; layer = <CALayer: 0x1c0621200>> ) 

這兩個button的title和action都和咱們以前所建立的左滑選項相對應,因此咱們能夠用相似的方法遍歷UITableView的子視圖,拿到對應UIButton的reference進行修改。


iOS11下的層級 (用Xcode 9編譯)

Xcode 9 默認使用iOS11 SDK來編譯,添加打印後發現Xcode 9 編譯出來的沒有UITableViewWrapperView這一層,UISwipeActionPullView的子視圖直接附屬於UITableViewCell。

除了少了一層UITableViewWrapperView之外,其餘和Xcode 8編譯出來的同樣。


放一下Xcode 八、9編譯的對比圖:

 
xcode8.png
 
xcode9.png

 


懶人請直接看這裏:p

具體實現方法 (支持iOS8-10, 11)

爲了同時支持iOS8-10和iOS11, 我把操做選項外觀的代碼統一放在UITableView的ViewController的- (void)viewDidLayoutSubviews實現。

這樣作的緣由有兩個:

  1. 本來由於iOS8-10中,左滑選項是UITableViewCell的子視圖,而在iOS11中,左滑選項變成了UITableView的子視圖。雖然能夠用tabelCell.superview來獲取tableView,不過我認爲最好從高層級去操做低層級。因此統一在UITableView層處理。
  2. iOS8-10的UITableViewCellDeleteConfirmationView子視圖出現得較晚。在代理方法willBeginEditingRowAtIndexPath中尚未出現,而在viewDidLayoutSubviews則能夠保證子視圖出現。

首先咱們遍歷UITableView的子視圖拿到選項按鈕(UIButton)的reference,對iOS8-10和iOS11作不一樣處理:

#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) #define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending) @property (weak, nonatomic) IBOutlet UITableView *tableView; @property (strong, nonatomic) NSIndexPath* editingIndexPath; //當前左滑cell的index,在代理方法中設置 - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; if (self.editingIndexPath) { [self configSwipeButtons]; } } 

Xcode 8 編譯版本:(若是你使用的是Xcode 9,參見下面)

- (void)configSwipeButtons { // 獲取選項按鈕的reference if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0")) { // iOS 11層級 (Xcode 8編譯): UITableView -> UITableViewWrapperView -> UISwipeActionPullView for (UIView *subview in self.tableView.subviews) { if ([subview isKindOfClass:NSClassFromString(@"UITableViewWrapperView")]) { for (UIView *subsubview in subview.subviews) { if ([subsubview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")] && [subsubview.subviews count] >= 2) { // 和iOS 10的按鈕順序相反 UIButton *deleteButton = subsubview.subviews[1]; UIButton *readButton = subsubview.subviews[0]; [self configDeleteButton:deleteButton]; [self configReadButton:readButton]; } } } } } else { // iOS 8-10層級: UITableView -> UITableViewCell -> UITableViewCellDeleteConfirmationView NotificationCell *tableCell = [self.tableView cellForRowAtIndexPath:self.editingIndexPath]; for (UIView *subview in tableCell.subviews) { if ([subview isKindOfClass:NSClassFromString(@"UITableViewCellDeleteConfirmationView")] && [subview.subviews count] >= 2) { UIButton *deleteButton = subview.subviews[0]; UIButton *readButton = subview.subviews[1]; [self configDeleteButton:deleteButton]; [self configReadButton:readButton]; [subview setBackgroundColor:[[ColorUtil instance] colorWithHexString:@"E5E8E8"]]; } } } [self configDeleteButton:deleteButton]; [self configReadButton:readButton]; } 

Xcode 9 編譯版本:(比Xcode 8編譯出來少一層)

- (void)configSwipeButtons { // 獲取選項按鈕的reference if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0")) { // iOS 11層級 (Xcode 9編譯): UITableView -> UISwipeActionPullView for (UIView *subview in self.tableView.subviews) { if ([subview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")] && [subview.subviews count] >= 2) { // 和iOS 10的按鈕順序相反 UIButton *deleteButton = subsubview.subviews[1]; UIButton *readButton = subsubview.subviews[0]; [self configDeleteButton:deleteButton]; [self configReadButton:readButton]; } } } else { // iOS 8-10層級: UITableView -> UITableViewCell -> UITableViewCellDeleteConfirmationView NotificationCell *tableCell = [self.tableView cellForRowAtIndexPath:self.editingIndexPath]; for (UIView *subview in tableCell.subviews) { if ([subview isKindOfClass:NSClassFromString(@"UITableViewCellDeleteConfirmationView")] && [subview.subviews count] >= 2) { UIButton *deleteButton = subview.subviews[0]; UIButton *readButton = subview.subviews[1]; [self configDeleteButton:deleteButton]; [self configReadButton:readButton]; [subview setBackgroundColor:[[ColorUtil instance] colorWithHexString:@"E5E8E8"]]; } } } [self configDeleteButton:deleteButton]; [self configReadButton:readButton]; } 

注意一下這裏咱們用到了一個變量self.editingIndexPath,這表明着當前左滑的cell的index,方便咱們獲取iOS8-10上面的tableCell的reference。分別在控制進入和退出編輯模式的代理方法中設置的:

- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath { self.editingIndexPath = indexPath; [self.view setNeedsLayout]; // 觸發-(void)viewDidLayoutSubviews } - (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath { self.editingIndexPath = nil; } 

[self.view setNeedsLayout]; 這一句很是重要,它的做用強制UITableView從新繪圖。只有添加了這一句,- (void)viewDidLayoutSubviews纔會被調用,才能使咱們的自定義外觀生效。


好了,咱們已經拿到了按鈕(UIButton)的reference,而後就能夠給按鈕添加了圖片,而且設置文本的字體和顏色了。

- (void)configDeleteButton:(UIButton*)deleteButton { if (deleteButton) { [deleteButton.titleLabel setFont:[UIFont fontWithName:@"SFUIText-Regular" size:12.0]]; [deleteButton setTitleColor:[[ColorUtil instance] colorWithHexString:@"D0021B"] forState:UIControlStateNormal]; [deleteButton setImage:[UIImage imageNamed:@"Delete_icon_.png"] forState:UIControlStateNormal]; [deleteButton setBackgroundColor:[[ColorUtil instance] colorWithHexString:@"E5E8E8"]]; // 調整按鈕上圖片和文字的相對位置(該方法的實如今下面) [self centerImageAndTextOnButton:deleteButton]; } } - (void)configReadButton:(UIButton*)readButton { if (readButton) { [readButton.titleLabel setFont:[UIFont fontWithName:@"SFUIText-Regular" size:12.0]]; [readButton setTitleColor:[[ColorUtil instance] colorWithHexString:@"4A90E2"] forState:UIControlStateNormal]; // 根據當前狀態選擇不一樣圖片 BOOL isRead = [[NotificationManager instance] read:self.editingIndexPath.row]; UIImage *readButtonImage = [UIImage imageNamed: isRead ? @"Mark_as_unread_icon_.png" : @"Mark_as_read_icon_.png"]; [readButton setImage:readButtonImage forState:UIControlStateNormal]; [readButton setBackgroundColor:[[ColorUtil instance] colorWithHexString:@"E5E8E8"]]; // 調整按鈕上圖片和文字的相對位置(該方法的實如今下面) [self centerImageAndTextOnButton:readButton]; } } 

 

 

若是沒有[self centerImageAndTextOnButton:readButton],則效果以下:
 
after-layoutsubviews-1.PNG

能夠看到圖標在左,文字在右,還互相重合。這就是咱們熟悉的UIButton的外觀處理,須要分別修改UILabel和UIImageView的frame:

- (void)centerImageAndTextOnButton:(UIButton*)button { // this is to center the image and text on button. // the space between the image and text CGFloat spacing = 35.0; // lower the text and push it left so it appears centered below the image CGSize imageSize = button.imageView.image.size; button.titleEdgeInsets = UIEdgeInsetsMake(0.0, - imageSize.width, - (imageSize.height + spacing), 0.0); // raise the image and push it right so it appears centered above the text CGSize titleSize = [button.titleLabel.text sizeWithAttributes:@{NSFontAttributeName: button.titleLabel.font}]; button.imageEdgeInsets = UIEdgeInsetsMake(-(titleSize.height + spacing), 0.0, 0.0, - titleSize.width); // increase the content height to avoid clipping CGFloat edgeOffset = (titleSize.height - imageSize.height) / 2.0; button.contentEdgeInsets = UIEdgeInsetsMake(edgeOffset, 0.0, edgeOffset, 0.0); // move whole button down, apple placed the button too high in iOS 10 if (SYSTEM_VERSION_LESS_THAN(@"11.0")) { CGRect btnFrame = button.frame; btnFrame.origin.y = 18; button.frame = btnFrame; } } 

 

 

調整事後就能夠作到文章開頭展現的效果了:
 
swipe-customize-2.PNG

PS:假如僅支持iOS8-10的話,我我的是傾向於建立一個custom class,繼承UITableViewCell,而後在該custom class中-(void)layoutSubviews來實現的。那樣代碼更乾淨,不須要特地去調用[self.view setNeedsLayout]; 不過爲了支持新版本老是要有所犧牲的。


3. TableCell上有其它按鈕的處理方法

 

 

我本身作的時候遇到了一種特殊狀況,UITableViewCell上面帶有比較顯著的button,相似下圖的這種狀況:
 
swipe-button-1.PNG

這種狀況比較尷尬的就是當你左滑的時候若是恰好碰到了YES或者NO button, 在進入左滑選項的同時會觸發按鈕選項,至關容易引起誤操做。爲了解決這個問題,我在TableViewController中實現了下面兩個代理方法:

- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *tableCell = [tableView cellForRowAtIndexPath:indexPath]; // disable button touch event during swipe for (UIView *view in [tableCell.contentView subviews]) { if ([view isKindOfClass:[UIButton class]]) { [view setUserInteractionEnabled:NO]; } } } - (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *tableCell = [tableView cellForRowAtIndexPath:indexPath]; for (UIView *view in [tableCell.contentView subviews]) { if ([view isKindOfClass:[UIButton class]]) { [view setUserInteractionEnabled:YES]; } } } 

tableView:willBeginEditingRowAtIndexPath是在cell進入editing mode以前調用的,在這裏將contentView下面的全部按鈕的交互設置爲disabled。
tableView:didEndEditingRowAtIndexPath是在cell即將退出editing mode時調用的,在這裏將以前被disable的全部button的交互從新設置爲enabled。

這樣就能夠保證在左滑菜單出現的時候,本來cell上的那些按鈕都處於不能點按的狀態,也就不會觸發誤操做了。

PS: 試過直接disable 整個contentView不起做用,必須直接disable對應的UIButton才行,推測跟apple本身處理event的有限次序有關。

做者:pika11 連接:https://www.jianshu.com/p/779f36c21632 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索