你同樣已經(jīng)學(xué)了不少關(guān)于這個 Cell 如何工作的知識漱凝;亦即,那個UITableViewCellScrollView
碍现,它包含 contentView 和 Disclosure Indicator (以及 Delete 按鈕庄撮,如果它被添加的話)徐钠,明顯是要做某些事 篱竭。你可能已經(jīng)從它的名字以及它是UIScrollView
的子類而猜到了力图。
你可以通過在tableView:cellForRowAtIndexPath:
下面添加一個簡單的for
循環(huán)來測試這個假設(shè),就在recursiveDescription
那一行下面:
for (UIView *view in cell.subviews) { if ([view isKindOfClass:[UIScrollView class]]) { view.backgroundColor = [UIColor greenColor]; }}
再次編譯并允許應(yīng)用掺逼;綠色高亮確認(rèn)了這個私有類確實(shí)是UIScrollView
的子類吃媒,因?yàn)樗采w了 Cell 里所有的紫色。
回想剛才recursiveDescription
輸出的 log吕喘,UITableViewCellScrollView
的 Frame 和 Cell 本身的 Size 是一致的赘那。
但是,這個視圖到底有什么用兽泄?繼續(xù)拖動 Cell 到左邊漓概,你就會看到 Scroll View 在你拖動 Cell 并 釋放時提供了 “彈性(springy)”行為,如下所示:
在你創(chuàng)建你自己的自定義UITableViewCell
子類之前病梢,還有一件事要注意胃珍,它出至 UITableViewCell Class Reference:
如果你想超越預(yù)定義樣式,你可以添加子視圖到 Cell 的contentView
上蜓陌。在添加子視圖時觅彰,你自己要負(fù)責(zé)這些視圖的位置以及設(shè)置它們的內(nèi)容。
直白的說钮热,就是填抬,任何對UITableViewCell
的自定義操作只能在contentView
中進(jìn)行。你不能將自己的視圖加在 Cell 下面——而必須將它們加在 Cell 的contentView
上隧期。
這就意味著你將找出你自己的解決方案以便添加自定義按鈕飒责。但不要害怕,你可以很容易地復(fù)制出 Apple 所使用的方案仆潮。
可滑動 Table View Cell 的組成列表
這對你來說是什么意思宏蛉?到了這里,你就有了一個組成列表來制造出一個UITableViewCell
子類性置,以便放上你自定義的按鈕拾并。
我們從 View Stack 的最底部開始列出條目,你的列表如下:
contentView
是你的基礎(chǔ)視圖鹏浅,因?yàn)槟阒荒軐⒆右晥D添加到它上面嗅义。
在用戶滑動后,任何你想顯示的UIButon
隐砸。
一個位于按鈕之上的容器視圖來裝載你所有的內(nèi)容之碗。
你可以使用一個UIScrollView
來作為你的容器視圖,就像 Apple 使用的凰萨,或者使用一個UIPanGestureRecognizer
继控。這同樣能夠處理滑動去顯示/隱藏按鈕械馆。你將在項(xiàng)目中采用后一種方案。
最后武通,一個裝有實(shí)際內(nèi)容的視圖霹崎。
還有一個可能不那么明顯的成分:你必須確保系統(tǒng)提供的UIPanGestureRecognizer
—— 它能讓你滑動顯示 Delete 按鈕 —— 不可用。否則系統(tǒng)手勢會和自定義手勢沖突冶忱。
好消息是設(shè)置默認(rèn)滑動手勢不可用的操作相當(dāng)簡單尾菇。
打開MasterViewController.m
修改tableView:canEditRowAtIndexPath:
永遠(yuǎn)返回NO
,如下所示:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return NO;}
編譯并運(yùn)行囚枪;試著滑動某個 Cell 派诬,你會發(fā)現(xiàn)你不能再滑動去刪除了。
為了保持簡單链沼,你將使用兩個按鈕來走完這個教程默赂。但同樣的技術(shù)也可以再一個按鈕上工作,或者超過兩個按鈕的情況——作為提醒括勺,你可能需要執(zhí)行一些本文沒有涉及到的調(diào)整缆八,如果你真的添加了多個按鈕,你必須將整個 Cell 滑出才能看到所有的按鈕疾捍。
創(chuàng)建一個自定義 Cell
你可以從基本視圖和手勢識別列表可以看到奈辰,在 Table View Cell 中有許多要做的事。你將創(chuàng)建一個自定義的UITableViewCell
子類乱豆,以將所有的邏輯放在同一個地方奖恰。
去往File\New\ File…
并選擇iOS\Cocoa Touch\Objective-C class
,將新類命名為SwipeableCell
宛裕,將它設(shè)置為UITableViewCell
的子類 瑟啃,如下所示:
在SwipeableCell.m
中設(shè)置下列類擴(kuò)展和IBOutlet
,就在#import
語句后揩尸,@implementation
語句前:
@interface SwipeableCell()
@property (nonatomic, weak) IBOutlet UIButton *button1;
@property (nonatomic, weak) IBOutlet UIButton *button2;
@property (nonatomic, weak) IBOutlet UIView*myContentView;
@property (nonatomic, weak) IBOutlet UILabel *myTextLabel;@end
下一步翰守,進(jìn)入 Storyboard 選中UITableViewCell
原型,如下所示:
打開 Identity Inspector 疲酌,然后修改 Custom Class 為SwipeableCell
,如下所示:
現(xiàn)在UITableViewCell
原型的名字在左邊的 Document Outline 上會顯示為 “Swipeable Cell”了袁。右鍵單擊Swipeable Cell – Cell
朗恳,你會看到一個你之前設(shè)置的IBOutlet
列表:
首先,你要在 Attributes Inspector 里修改兩個地方以便自定義視圖载绿。設(shè)置 Style 為Custom
粥诫, Selection 為None
, Accessory 也為None
崭庸,截圖如下:
然后怀浆,拖兩個按鈕到 Cell 的 Content View 里谊囚。在視圖的 Attributes Inspector 區(qū)設(shè)置每個按鈕的背景色為比較鮮艷的顏色,并設(shè)置每個按鈕的文字顏色為比較易讀的顏色执赡,這樣你就可以清楚地看到按鈕镰踏。
將第一個按鈕放在右邊,和contentView
的上下邊緣接觸沙合。將第二個按鈕放在第一個按鈕的左邊緣處奠伪,也和contentView
的上下邊緣接觸。當(dāng)你做好后首懈,Cell 看起來如下绊率,可能顏色少有差異:
接下來,將每個按鈕和對應(yīng)的 Outlet 關(guān)聯(lián)起來究履。右鍵單擊到可滑動Cell上打開它的 Outlets滤否,然后將 button1 拖動到到右邊的按鈕, button2 拖動到左邊的按鈕最仑,如下:
你需要創(chuàng)建一個方法來處理對每個按鈕的點(diǎn)擊藐俺。
打開SwipeableCell.m
添加如下方法:
- (IBAction)buttonClicked:(id)sender
{
if (sender == self.button1)
{
NSLog(@"Clicked button 1!");
} else if (sender == self.button2)
{
NSLog(@"Clicked button 2!");
} else { NSLog(@"Clicked unknown button!");
}
}
這個方法處理對兩個按鈕的點(diǎn)擊,通過在控制臺打印記錄盯仪,你就能確定按鈕被點(diǎn)擊了紊搪。
再次打開 Storyboard ,將兩個按鈕都連接上 Action 全景。右鍵單擊Swipeable Cell – Cell
出現(xiàn) Outlet 和 Action 的列表耀石。從buttonClicked:
Action 拖動到你的按鈕,如下:
從事件列表中選擇Touch Up Inside
爸黄,如下所示:
重復(fù)上述步驟滞伟,用于第二個按鈕。現(xiàn)在隨便按照任何一個按鈕上炕贵,都會調(diào)用buttonClicked:
梆奈。
打開SwipeableCell.m
添加如下屬性:
@property (nonatomic, strong) NSString *itemText;
稍后你將更多的和itemText
打交道,但目前称开,這就是所有你要做的亩钟。
打開MasterViewController.m
并在頂部添加如下一行:
import "SwipeableCell.h"
這將保證這個類知道你自定義的 Cell 子類。
替換tableView:cellForRowAtIndexPath:
的內(nèi)容為:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
SwipeableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
NSString *item = _objects[indexPath.row]; cell.itemText = item; return cell;
}
現(xiàn)在該使用你的新 Cell 而不是標(biāo)準(zhǔn)的UITableViewCell
鳖轰。
編譯并運(yùn)行清酥;你會看到如下界面:
添加一個 Delegate
歐耶~ 你的按鈕已經(jīng)出現(xiàn)了!如果你點(diǎn)擊任何一個按鈕蕴侣,你都會在控制臺看到合適的信息輸出焰轻。然而,你不能指望 Cell 本身去處理任何直接的 Action 昆雀。
比如說辱志,一個 Cell 不能 Present 其他的 View Controller 或直接將其 push 到 Navigation Stack 里蝠筑。你必須要設(shè)置一個 Delegate 來傳遞按鈕的點(diǎn)擊事件回到 View Controller 中去處理那個事件。
打開SwipeableCell.h
并在@interface
之上添加如下 Delegate 協(xié)議:
@protocol SwipeableCellDelegate <NSObject>
- (void)buttonOneActionForItemText:(NSString *)itemText;
- (void)buttonTwoActionForItemText:(NSString *)itemText;
@end
添加如下 Delegate 屬性到SwipeableCell.h
揩懒,就在itemText
屬性下面:
@property (nonatomic, weak) id <SwipeableCellDelegate> delegate;
更新SwipeableCell.m
中的buttonClicked:
為如下所示:
- (IBAction)buttonClicked:(id)sender
{
if (sender == self.button1)
{
[self.delegate buttonOneActionForItemText:self.itemText];
} else if (sender == self.button2)
{
self.delegate buttonTwoActionForItemText:self.itemText];
} else {
(@"Clicked unknown button!");
}
}
這個更新使得這個方法去調(diào)用合適的 Delegate 方法什乙,而不僅僅是打印一句 log。
現(xiàn)在打開MasterViewController.m
并添加如下 delegate 方法:
#pragma mark - SwipeableCellDelegate
- (void)buttonOneActionForItemText:(NSString *)itemText {
NSLog(@"In the delegate, Clicked button one for %@", itemText);
}
- (void)buttonTwoActionForItemText:(NSString *)itemText {
NSLog(@"In the delegate, Clicked button two for %@", itemText);
}
這個方法目前還是簡單的打印到控制臺旭从,以確保一切傳遞都工作正常稳强。
接下來,添加如下協(xié)議到MasterViewController.m
頂部的類擴(kuò)展上以符合協(xié)議申明:
@interface MasterViewController () <SwipeableCellDelegate>
{
NSMutableArray *_objects;
}
@end
這只是簡單地確認(rèn)這個類會實(shí)現(xiàn)SwipeableCellDelegate
協(xié)議和悦。
最后退疫,你要設(shè)置這個 View Controller 為 Cell 的 delegate。
添加如下語句到tableView:cellForRowAtIndexPath:
鸽素,就在最后的 return 語句之前:
cell.delegate = self;
編譯并運(yùn)行褒繁;當(dāng)你點(diǎn)擊按鈕時,你就會看到合適的“In the delegate”消息馍忽。
為按鈕添加 Action
如果你看到log消息很很高興了棒坏,也可以跳過下一節(jié)。然而遭笋,如果你喜歡更加實(shí)在的東西坝冕,你可以添加一些處理,這樣當(dāng) delegate 方法被調(diào)用時瓦呼,你就可以顯示已經(jīng)引入的DetailViewController
喂窟。
添加如下兩個方法到MasterViewController.m
:
- (void)showDetailWithText:(NSString *)detailText
{
//1 UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
DetailViewController *detail = [storyboard instantiateViewControllerWithIdentifier:@"DetailViewController"];
detail.title = @"In the delegate!";
detail.detailItem = detailText;
//2 UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:detail];
//3 UIBarButtonItem *done = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(closeModal)];
[detail.navigationItem setRightBarButtonItem:done];
[self presentViewController:navController animated:YES completion:nil];
}
//4
- (void)closeModal
{
[self dismissViewControllerAnimated:YES completion:nil];
}
在上面的代碼里,你執(zhí)行了四個操作:
從 Storyboard 里取出 Detail View Controller 并設(shè)置其 title 和 detailItem 央串。
設(shè)置一個UINavigationController
作為包含 Detail View Controller 的容器磨澡,并給你放置 close 按鈕的地方。
添加 close 按鈕质和,關(guān)聯(lián)MasterViewController
里的一個 Action稳摄。
設(shè)置這個 Action 的響應(yīng)方法,它將 dismiss 任何以 Modal 方式顯示 View Controller
接下來饲宿,用下列版本替換你之前添加的兩個方法:
- (void)buttonOneActionForItemText:(NSString *)itemText
{
[self showDetailWithText:[NSString stringWithFormat:@"Clicked button one for %@", itemText]];
}
- (void)buttonTwoActionForItemText:(NSString *)itemText
{
[self showDetailWithText:[NSString stringWithFormat:@"Clicked button two for %@", itemText]];
}
最后厦酬,打開Main.storyboard
并選中Detail View Controller
。找到 Identity Inspector 并設(shè)置Storyboard ID
為DetailViewController
以匹配類名瘫想,如下所示:
如果你忘了這一步弃锐,instantiateViewControllerWithIdentifier
將會因?yàn)椴缓戏ǖ膮?shù)而 Crash,其異常表示具有這個標(biāo)識符的 View Controller 并不存在殿托。
編譯并運(yùn)行;點(diǎn)擊某個 Cell 中的按鈕剧蚣,然后看著 Modal View Controller 出現(xiàn)支竹,如下面的截圖所示:
添加頂層視圖并添加滑動 Action
現(xiàn)在你到了視圖工作的后段部分旋廷,是時候讓頂層部分啟動并運(yùn)行起來了。
打開Main.storyboard
并拖一個UIView
到SwipeableTableCell
上礼搁,這個視圖將占據(jù)整個 Cell 的高和寬饶碘,并覆蓋按鈕,所以在Swipe手勢能工作之前馒吴,你不會再看到它們了扎运。
如果你要精確地控制,打開 Size Inspector 并設(shè)置這個視圖地寬和高饮戳,分別為 320 和 43:
你同樣需要一個約束來將視圖釘在 contentView 的邊緣豪治。選中視圖并點(diǎn)擊Pin
按鈕,選擇所有四個間隔約束并設(shè)置它們的值為 0 扯罐,如下所示:
連接好這個視圖的 Outlet负拟,按照之前介紹的步驟:在左邊的導(dǎo)航器里右鍵單擊這個可滑動 Cell 并拖動myContentView
到這個新的視圖上。
下一步歹河,拖動一個UILabel
到視圖里掩浙;設(shè)置其距離左邊 20 點(diǎn),并設(shè)置其垂直劇中秸歧。再將其連接到myTextLabel
Outlet 上厨姚。
編譯并運(yùn)行;你的 Cell 看起來有正常了:
添加數(shù)據(jù)
但為何實(shí)際的文本數(shù)據(jù)沒有顯示出來键菱?那是因?yàn)槟阒皇窃O(shè)置了itemText
屬性谬墙,而沒有做會影響myTextLabel
的事情。
打開SwipeableCell.m
并添加如下方法:
- (void)setItemText:(NSString *)itemText
{
//Update the instance variable _itemText = itemText;
//Set the text to the custom label.
self.myTextLabel.text = _itemText;}
這個方法覆寫了itemText
屬性的 setter 方法纱耻。除了更新后面的實(shí)例變量芭梯,它還會更新可見的 Label。
最后弄喘,為了讓接下來的幾步的結(jié)果更易看到玖喘,你將把 item 的 title 變長一點(diǎn),以便在 Cell 滑動后依然有一些文本可見蘑志。
轉(zhuǎn)到MasterViewController.m
并更新viewDidLoad
中的這一行累奈,這是 item title 生成的地方:
NSString *item = [NSString stringWithFormat:@"Longer Title Item #%d", i];
編譯并運(yùn)行;你就會看到合適的 item title 顯示如下:
手勢識別——GO急但!
終于到了“有趣的”部分——將數(shù)學(xué)澎媒、約束以及手勢識別攪和在一起,以方便地處理滑動操作波桩。
首先戒努,在SwipeableCell
的類擴(kuò)展里添加如下這些屬性:
@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer;
@property (nonatomic, assign) CGPoint panStartPoint;
@property (nonatomic, assign) CGFloat startingRightLayoutConstraintConstant;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint *contentViewRightConstraint;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint *contentViewLeftConstraint;
關(guān)于你所要做的事情,簡短版本是這樣的:記錄一個 Pan 手勢并調(diào)整你的View的左右約束镐躲,根據(jù) a) 用戶將 Cell Pan 了多遠(yuǎn) b) Cell 在何處以及合適開始移動储玫。
為了做到這一點(diǎn)侍筛,你首先要將這個 IBOutlet 連接到myContentView
的左右約束上。這兩個約束將視圖 釘在 Cell 的contentView
中撒穷。
通過打開約束列表匣椰,你可以找出這兩個約束。通過檢查每個約束在 Cell 上的高亮你就能找到那合適的兩個端礼。在這個例子中禽笑,是contentView
右邊和contentView
之間的約束,如下所示:
一旦你定位到合適的約束蛤奥,就將其連接到合適的 Outlet 上——在本例中佳镜,是contentViewRightConstraint
,如下圖所示:
遵循同樣的步驟喻括,連接好contentViewLeftConstraint
邀杏,它代表contentView
左邊和contentView
之間的約束。
下一步唬血,打開SwipeableCell.m
并修改@interface
語句的類擴(kuò)展望蜡,添加UIGestureRecognizerDelegate
協(xié)議:
@interface SwipeableCell() <UIGestureRecognizerDelegate>
然后,依然在SwipeableCell.m
里拷恨,添加如下方法:
- (void)awakeFromNib
{
[super awakeFromNib];
self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panThisCell:)];
self.panRecognizer.delegate = self;
[self.myContentView addGestureRecognizer:self.panRecognizer];
}
這里設(shè)置了 Pan 手勢并將其添加到 Cell 上:
再添加如下方法:
- (void)panThisCell:(UIPanGestureRecognizer *)recognizer
{
switch (recognizer.state)
{
case UIGestureRecognizerStateBegan: self.panStartPoint = [recognizer translationInView:self.myContentView];
NSLog(@"Pan Began at %@", NSStringFromCGPoint(self.panStartPoint));
break;
case UIGestureRecognizerStateChanged: { CGPoint currentPoint = [recognizer translationInView:self.myContentView];
CGFloat deltaX = currentPoint.x - self.panStartPoint.x;
NSLog(@"Pan Moved %f", deltaX); }
break;
case UIGestureRecognizerStateEnded: NSLog(@"Pan Ended");
break;
case UIGestureRecognizerStateCancelled: NSLog(@"Pan Cancelled");
break;
default: break;
}
}
這個方法會在 Pan 手勢識別器發(fā)動時執(zhí)行脖律,暫時,它只簡單地打印 Pan 手勢的細(xì)節(jié)腕侄。
編譯并運(yùn)行小泉;用手指拖動 Cell ,你就會看到如下log記錄了移動信息:
如果你往初始點(diǎn)的右邊滑動冕杠,你會看到正數(shù)微姊,往初始點(diǎn)的左邊滑動就會看到負(fù)數(shù)。這些數(shù)字將用于調(diào)整myContentView
的約束分预。
移動這些約束
從本質(zhì)上將兢交,你需要通過調(diào)整將 Cell 的contentView
釘住的左、右約束來推動myContentView
到左邊笼痹。右約束將會接受一個正值配喳,而左約束將接受一個絕對值相等的負(fù)值。
舉例來說凳干,如果myContentView
需要往左移動 5 點(diǎn)晴裹,那么 右約束將會接受的值是 5,而左約束將接受的值是 -5 救赐。這將會將整個視圖往左邊滑動 5 點(diǎn)涧团,而不會改變他的寬度。
聽起來蠻容易的——但還有許多移動相關(guān)的事情要注意。根據(jù) Cell 是否已經(jīng)打開和用戶 Pan 的方向少欺,你要處理不同的一大把事情喳瓣。
你同樣需要知道 Cell 最遠(yuǎn)可以滑動多遠(yuǎn)。你將通過計(jì)算被按鈕覆蓋的區(qū)域的寬度來確定這一點(diǎn)赞别。最簡單的方法是用視圖的整個寬度減去最左邊的按鈕的最小 X 位置。
為了闡明配乓,下面來個 sneak peek 仿滔,以明確的圖示表明你所要關(guān)注的方面:
幸好,感謝 CGRect
CGGeometry 函數(shù) 犹芹,這些很容易被轉(zhuǎn)換為代碼:
添加如下方法到SwipeableCell.m
:
- (CGFloat)buttonTotalWidth { return CGRectGetWidth(self.frame) - CGRectGetMinX(self.button2.frame);}
添加如下兩個骨架方法到SwipeableCell.m
:
- (void)resetConstraintContstantsToZero:(BOOL)animated notifyDelegateDidClose:(BOOL)endEditing
{
//TODO: Build.
}
- (void)setConstraintsToShowAllButtons:(BOOL)animated notifyDelegateDidOpen:(BOOL)notifyDelegate
{
//TODO: Build
}
這兩個骨架方法——一旦你填上血肉——將 snap 打開 Cell 并 snap 關(guān)閉 Cell崎页。在你對 pan 手勢識別起添加更多處理后,你會回到這兩個方法腰埂。
替換panThisCell:
中的UIGestureRecognizerStateBegan
case 為下列代碼:
case UIGestureRecognizerStateBegan: self.panStartPoint = [recognizer translationInView:self.myContentView]; self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
break;
你需要存儲 Cell 的初始位置(例如飒焦,約束值)以確定 Cell 是要打開還是關(guān)閉。
下一步你需要添加更多處理以應(yīng)對 pan 手勢識別器的改變屿笼。還是在panThisCell:
里牺荠,修改UIGestureRecognizerStateChanged
case ,如下所示:
case UIGestureRecognizerStateChanged: { CGPoint currentPoint = [recognizer translationInView:self.myContentView];
CGFloat deltaX = currentPoint.x - self.panStartPoint.x;
BOOL panningLeft = NO;
if (currentPoint.x < self.panStartPoint.x)
{
//1 panningLeft = YES;
} if (self.startingRightLayoutConstraintConstant == 0)
{
//2 //The cell was closed and is now opening
if (!panningLeft)
{
CGFloat constant = MAX(-deltaX, 0);
//3
if (constant == 0)
{
//4 [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:NO];
} else {
//5 self.contentViewRightConstraint.constant = constant;
}
} else {
CGFloat constant = MIN(-deltaX, [self buttonTotalWidth]);
//6 if (constant == [self buttonTotalWidth]) {
//7 [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:NO];
} else {
//8 self.contentViewRightConstraint.constant = constant;
}
}
}
上面大部分代碼都在 Cell 默認(rèn)的“關(guān)閉”狀態(tài)下 處理pan手勢識別器驴一,下面是細(xì)節(jié)說明:
判斷 pan 手勢是往左還是往右休雌。
如果右約束常量為 0 ,意味著myContentView
完全擋住contentView
肝断。因此 Cell 在這里一定已經(jīng)關(guān)閉杈曲,而用戶準(zhǔn)備打開它。
這是處理用戶從做到右滑動以關(guān)閉 Cell 的 情況胸懈。除了說“你不能做那個”之外担扑,你還要處理的情況是,當(dāng)用戶滑動 Cell 只打開一點(diǎn)點(diǎn)趣钱,然后他們希望不必抬起他們的手指來結(jié)束此手勢就可以滑動它關(guān)閉涌献。譯者注:就是說,打開一點(diǎn)點(diǎn)不會完全顯示出后面的按鈕羔挡,Cell 會自動關(guān)閉洁奈。
因?yàn)橐粋€從左到右的滑動會導(dǎo)致deltaX
為正值,而從右到左的滑動回到導(dǎo)致deltaX
為負(fù)值绞灼,你必須根據(jù)負(fù)的deltaX
計(jì)算出常量以設(shè)置到右約束上利术。因?yàn)槭菑乃c0中找出最大值,所以視圖不可能往右邊走多遠(yuǎn)低矮。
如果常量為 0印叁,Cell 就是完全關(guān)閉的。調(diào)用處理關(guān)閉的方法——它(如你回憶起的)在目前還什么也不會做。
如果常量為不為 0轮蜕,那么你就將其設(shè)置到右手邊的約束上昨悼。
否者,如果是從右往做滑動跃洛,那么用戶試圖打開 Cell 率触。這在個情況里,常量將會小于負(fù)deltaX
或兩個按鈕的寬度之和汇竭。
如果目標(biāo)常量是兩個按鈕的寬度之和葱蝗,那么 Cell 就被打開至捕捉點(diǎn)(catch point),你應(yīng)該調(diào)用方法來處理這個打開狀態(tài)细燎。
如果常量不是兩個按鈕的寬度之和两曼,那就將其設(shè)置到右約束上。
喲玻驻!處理得真不少… 而這個只是處理了 Cell 已經(jīng)關(guān)閉得情況悼凑。你現(xiàn)在還要編寫代碼處理當(dāng)手勢開始時 Cell 就已經(jīng)部分開啟的情況。
就在剛在添加的代碼之下添加如下代碼:
else {
//The cell was at least partially open. CGFloat adjustment = self.startingRightLayoutConstraintConstant - deltaX;
//1 if (!panningLeft) { CGFloat constant = MAX(adjustment, 0);
//2 if (constant == 0) {
//3 [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:NO];
} else {
//4 self.contentViewRightConstraint.constant = constant;
}
} else {
CGFloat constant = MIN(adjustment, [self buttonTotalWidth]);
//5 if (constant == [self buttonTotalWidth]) {
//6 [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:NO];
} else {
//7 self.contentViewRightConstraint.constant = constant;
}
}
} self.contentViewLeftConstraint.constant = -self.contentViewRightConstraint.constant; //8
} break;
這是 if 語句的后半段璧瞬。因此它用于處理 Cell 原本就打開的情況户辫。
再一次,下面說明你要處理的幾個情況:
在這個情況下彪蓬,你只是接受deltaX
寸莫,你就用 rightLayoutConstraint 的原始位置減去deltaX
以便得知要做多少調(diào)整。
如果用戶從做往右滑動档冬,你必須接受 adjustment 與 0 中的較大值膘茎。如果 adjustment 已變成負(fù)值,那就說明用戶已經(jīng)把 Cell 滑到邊界之外了酷誓,Cell 就關(guān)閉了披坏,這就讓你進(jìn)入下一個情況。
如果常量為 0盐数,那么 Cell 已經(jīng)關(guān)閉棒拂,你就調(diào)用處理其關(guān)閉的方法。
否則玫氢,將常量設(shè)置到右約束上帚屉。
對于從右到左的滑動,你將接受 adjustment 與 兩個按鈕寬度之和 中的較小值漾峡。如果 adjustment 更大攻旦,那就表示用戶已經(jīng)滑出超過捕捉點(diǎn)了。
如果常量剛好等于兩個按鈕寬度之和生逸,那么 Cell 就打開了牢屋,你必須調(diào)用處理 Cell 打開的方法且预。
否則,將常量設(shè)置到右約束上烙无。
現(xiàn)在锋谐,你已經(jīng)處理完“Cell關(guān)閉”和“Cell部分開啟”的情況,在這兩個情況里截酷,你都可對左約束做同樣的事情:將其設(shè)置為右約束常量的負(fù)值涮拗。這就保證了myContentView
的寬度一直保持不變。
編譯并運(yùn)行迂苛;現(xiàn)在你可以來回滑動 Cell 多搀!它不是非常流暢,而且它在你希望的地方之前的一點(diǎn)就停下了灾部。這是因?yàn)槟氵€沒有真正實(shí)現(xiàn)那兩個用于處理打開和關(guān)閉 Cell 的方法。
Note:你可以也注意到惯退,Table View 本身已經(jīng)不會 scroll 了赌髓。不要擔(dān)心,一旦你正確處理好 Cell 的滑動催跪,你就能修復(fù)它锁蠕。
Snap!
接下來,你要讓 Cell Snao 進(jìn)入合適的位置懊蒸。你會注意到荣倾,如果你放手 Cell 會停到合適的位置。
在你進(jìn)入方法開始處理之前骑丸,你需要一個單獨(dú)的生成動畫的方法舌仍。
打開SwipeableCell.m
并添加如下方法:
```objc
- (void)updateConstraintsIfNeeded:(BOOL)animated completion:(void (^)(BOOL finished))completion {
float duration = 0;
if (animated)
{
duration = 0.1;
}
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
[self layoutIfNeeded];
} completion:completion];
}
Note:0.1 秒的間隔和 ease-out curve 動畫都是我從實(shí)踐和錯誤中總結(jié)出來的。如果你找到其他更讓你看著愉悅的速度或動畫類型通危,可以自由修改它們铸豁。
接下來,你將填充那兩個處理打開和關(guān)閉的骨架方法菊碟。記得在 Apple 的原始實(shí)現(xiàn)里节芥,因?yàn)槭褂昧薝IScrollView
子類作為最底層的試圖,所以會有一點(diǎn)彈性逆害。
要讓事情看起來正確头镊,你將在 Cell 撞到邊界時給它一點(diǎn)彈性空另。你同樣要確保contentView
和myContentView
有同樣的backgroundColor
以造成彈性非常順滑的錯覺孽水。
添加如下常量到SwipeableCell.m
頂部,就在 import 語句之下:
static CGFloat const kBounceValue = 20.0f;
這個常量存儲了彈性值蜒茄,將用于你的彈性動畫中梅垄。
如下更新setConstraintsToShowAllButtons:notifyDelegateDidOpen:
:
- (void)setConstraintsToShowAllButtons:(BOOL)animated notifyDelegateDidOpen:(BOOL)notifyDelegate
{
//TODO: Notify delegate.
//1 if (self.startingRightLayoutConstraintConstant == [self buttonTotalWidth] && self.contentViewRightConstraint.constant == [self buttonTotalWidth])
{
return;
}
//2 self.contentViewLeftConstraint.constant = -[self buttonTotalWidth] - kBounceValue;
self.contentViewRightConstraint.constant = [self buttonTotalWidth] + kBounceValue;
[self updateConstraintsIfNeeded:animated completion:^(BOOL finished)
{
//3 self.contentViewLeftConstraint.constant = -[self buttonTotalWidth];
self.contentViewRightConstraint.constant = [self buttonTotalWidth];
[self updateConstraintsIfNeeded:animated completion:^(BOOL finished)
{
//4 self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
}];
}];
}
這個方法在 Cell 完全打開時執(zhí)行厂捞。下面解釋發(fā)生了什么:
如果 Cell 已經(jīng)開啟输玷,約束已經(jīng)到達(dá)完全開啟值,那就返回——否則彈性操作將會一次又一次的發(fā)生靡馁,就像你繼續(xù)滑動超過總按鈕寬度那樣欲鹏。
你初始設(shè)置約束值為按鈕總寬度和彈性值的結(jié)合值,它將 Cell 拉到左邊一點(diǎn)點(diǎn)臭墨,這樣才好 snap 回來赔嚎。然后你就調(diào)用動畫來實(shí)現(xiàn)這個設(shè)置。
當(dāng)?shù)谝粋€動畫完成胧弛,發(fā)動第二個動畫尤误,它將 Cell 正好打開在從按鈕寬度的位置。
當(dāng)?shù)诙€動畫完成结缚,重設(shè)起始約束否則你會看到多次彈跳损晤。
如下更新resetConstraintContstantsToZero:notifyDelegateDidClose:
:
- (void)resetConstraintContstantsToZero:(BOOL)animated notifyDelegateDidClose:(BOOL)notifyDelegate {
//TODO: Notify delegate. if (self.startingRightLayoutConstraintConstant == 0 && self.contentViewRightConstraint.constant == 0) {
//Already all the way closed, no bounce necessary return;
}
self.contentViewRightConstraint.constant = -kBounceValue; self.contentViewLeftConstraint.constant = kBounceValue;
[self updateConstraintsIfNeeded:animated completion:^(BOOL finished) { self.contentViewRightConstraint.constant = 0; self.contentViewLeftConstraint.constant = 0;
[self updateConstraintsIfNeeded:animated completion:^(BOOL finished) { self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
}];
}];
}
如你所見,這類似于setConstraintsToShowAllButtons:notifyDelegateDidOpen:
红竭,但它的邏輯是關(guān)閉 Cell 而不是打開尤勋。
編譯并運(yùn)行;隨意滑動 Cell 到它的捕捉點(diǎn)茵宪,你就會在放手時看到彈性行為最冰。
然而,如果你在 Cell 完全開啟或完全關(guān)閉之前將釋放手指稀火,它將會卡在中間暖哨。Whoops! 你還沒有處理觸摸結(jié)束或被取消的情況。
找到panThisCell:
用下列代碼替換UIGestureRecognizerStateEnded
case :
case UIGestureRecognizerStateEnded:
if (self.startingRightLayoutConstraintConstant == 0) {
//1 //Cell was opening
CGFloat halfOfButtonOne = CGRectGetWidth(self.button1.frame) / 2;
//2
if (self.contentViewRightConstraint.constant >= halfOfButtonOne) {
//3
//Open all the way
[self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
} else {
//Re-close [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES];
}
} else {
//Cell was closing CGFloat buttonOnePlusHalfOfButton2 = CGRectGetWidth(self.button1.frame) + (CGRectGetWidth(self.button2.frame) / 2);
//4
if (self.contentViewRightConstraint.constant >= buttonOnePlusHalfOfButton2) {
//5 //Re-open all the way
[self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
} else {
//Close
[self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES];
}
}
break;
在這里凰狞,你根據(jù) Cell 是否已經(jīng)打開或關(guān)閉以及手勢結(jié)束時 Cell 的位置在執(zhí)行不同的處理篇裁。具體來講:
通過檢查開始右約束值,得知手勢開始時 Cell 是否已經(jīng)打開或關(guān)閉服球。
如果 Cell 是關(guān)閉的茴恰,那你就正在打開它,你要讓 Cell 自動滑動到打開斩熊,至少需要先滑動右邊按鈕(self.button1)一半的寬度往枣。因?yàn)槟阍跍y量約束的常量,你只需要計(jì)算實(shí)際的按鈕寬度粉渠,而不是它在視圖中的 X 位置分冈。
接下來,測試約束是否已被打開至超過你希望讓 Cell 自動打開的點(diǎn)霸株。如果已經(jīng)超過雕沉,那就自動打開 Cell。如果沒有去件,那就自動關(guān)閉 Cell坡椒。
此處表示 Cell 從打開的狀態(tài)開始扰路,你需要那個能讓 Cell 自動 snap 關(guān)閉的點(diǎn),至少需要超過最左邊按鈕的一半倔叼。 將不是最左邊的按鈕的那些按鈕的寬度加起來汗唱,在這個情況里,只有 self.button1 而已丈攒,再加上最左邊按鈕的一半——也就是 self.button2 —— 以便找到需要的檢查點(diǎn)哩罪。
測試約束是否以及超過這個點(diǎn),即你希望 Cell 自動關(guān)閉的那個點(diǎn)巡验。如果超過了际插,關(guān)閉 Cell。如果沒有显设,那就重新打開 Cell框弛。
最后,你還要處理一下手勢被取消的情況捕捂。用如下代碼替換UIGestureRecognizerStateCancelled
case :
case UIGestureRecognizerStateCancelled:
if (self.startingRightLayoutConstraintConstant == 0) {
//Cell was closed - reset everything to 0
[self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES];
} else {
//Cell was open - reset to the open state
[self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
}
break;
這個處理相當(dāng)直白功咒;由于用戶取消了觸摸,表示他們不想改變 Cell 當(dāng)前的狀態(tài)绞蹦,所以你只需要將一切都設(shè)置為它們原本的樣子即可。
編譯并運(yùn)行榜旦;滑動 Cell 幽七,你會看到 Cell Snap 到打開或關(guān)閉,而不論你的手指再哪里溅呢,如下所示:
更好地處理 Table View
在最終完成前澡屡,只有少數(shù)幾步了!
首先咐旧,你的UIPanGestureRecognizer
有時候會影響UITableView
的 Scroll 操作驶鹉。由于你已經(jīng)設(shè)置了 Cell 的 Pan 手勢識別器 的UIGestureRecognizerDelegate
,你只需要實(shí)現(xiàn)一個(有些滑稽且冗長命名的) delegate 方法即可將一切恢復(fù)正常铣墨。
添加如下方法到SwipeableCell.m
:
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
這個方法告知各手勢識別器室埋,它們可以同時工作。
編譯并運(yùn)行伊约;打開第一個 Cell 然后你依然可以 Scroll tableView 姚淆。
還有一個 Cell 重用引起的小問題:各個行不記得它們的狀態(tài),看起來是因?yàn)?Cell 重用了它們的視圖的 開啟/關(guān)閉 狀態(tài)屡律,然后它們的視圖就不能正確反應(yīng)用戶的操作了腌逢。要查看這一情況,打開一個 Cell 超埋,然后將 Table Scroll 一點(diǎn)點(diǎn)搏讶。你就會注意每次都有一個 Cell 始終保持打開狀態(tài)佳鳖,但每次都不同。
要修復(fù)這個問題頭一半媒惕,添加如下方法到SwipeableCell.m
:
- (void)prepareForReuse {
[super prepareForReuse];
[self resetConstraintContstantsToZero:NO notifyDelegateDidClose:NO];
}
這個方法確保 Cell 在其回收重利用時再次關(guān)閉系吩。
要解決這個問題的后一半,你將添加一個公共方法給 Cell 以促使其打開吓笙。然后你會添加一些 delegate 方法以允許MasterViewController
去管理那個 Cell 是打開的淑玫。
打開SwipeableCell.h
。在SwipeableCellDelegate
協(xié)議的申明里面睛,添加如下兩個新的方法絮蒿,就在已存在的那兩個下面:
- (void)cellDidOpen:(UITableViewCell *)cell;
- (void)cellDidClose:(UITableViewCell *)cell;
這些方法將會通知 delegate —— 在你的情況里,就是 Master View Controller —— 某個 Cell 被打開或關(guān)閉了叁鉴。
添加如下公共方法申明到SwipeableCell
的@interface
里:
- (void)openCell;
接下來土涝,打開SwipeableCell.m
并添加openCell
的實(shí)現(xiàn):
- (void)openCell {
[self setConstraintsToShowAllButtons:NO notifyDelegateDidOpen:NO];
}
這個方法允許 delegate 修改 Cell 的狀態(tài)。
依然在用一個文件里幌墓,找到resetConstraintsToZero:notifyDelegateDidOpen:
并替換其中TODO
為如下代碼:
if (notifyDelegate) {
[self.delegate cellDidClose:self];
}
接下來但壮,找到setConstraintsToShowAllButtons:notifyDelegateDidClose:
并替換其中TODO
為如下代碼:
if (notifyDelegate) { [self.delegate cellDidOpen:self];}
這兩個修改會在一個 swipe 手勢完成時通知 delegate ,無論 Cell 是否以及打開或關(guān)閉常侣。
添加如下屬性申明到MasterViewController.m
頂部的類擴(kuò)展里:
@property (nonatomic, strong) NSMutableSet *cellsCurrentlyEditing;
它將存儲當(dāng)前已被打開的 Cell 的列表蜡饵。
添加如下代碼到viewDidLoad
的最后:
self.cellsCurrentlyEditing = [NSMutableSet new];
這個初始化保證了之后你可以正常使用數(shù)組。
現(xiàn)在在同一個文件里添加如下方法實(shí)現(xiàn):
- (void)cellDidOpen:(UITableViewCell *)cell {
NSIndexPath *currentEditingIndexPath = [self.tableView indexPathForCell:cell];
[self.cellsCurrentlyEditing addObject:currentEditingIndexPath];
}
- (void)cellDidClose:(UITableViewCell *)cell {
[self.cellsCurrentlyEditing removeObject:[self.tableView indexPathForCell:cell]];
}
注意到你添加的時 Index Path 而不是 Cell 本身到列表里胳施。如果你直接添加 Cell 對象溯祸,那么之后你就會看到同樣的問題,在 Cell 被回收后再次被打開舞肆。用了這個方法焦辅,你就可以使用合適 的 Index Path 來打開 Cell 了。
最后椿胯,添加下面幾行到tableView:cellForRowAtIndexPath:
筷登,就在 return 語句之前:
if ([self.cellsCurrentlyEditing containsObject:indexPath]) { [cell openCell];}
如果當(dāng)前的 Cell 的 Index Path 在列表里,它就會將其設(shè)置為打開哩盲。
編譯并運(yùn)行前方;全都搞定了!你現(xiàn)在有了一個能夠 Scroll 的 Table View廉油,還能處理 Cell 的打開和關(guān)閉狀態(tài)镣丑,并在 Cell 的任意被點(diǎn)擊時,使用 delegate 方法來加載任何任務(wù)娱两。
下一步怎么走莺匠?
譯者注:吐血,終于翻譯到這一句了十兢!
最終的項(xiàng)目可以在此處下載趣竣。我還會繼續(xù)我在此所開發(fā)的東西摇庙,并組成一個開源項(xiàng)目,以便讓事情更有靈活性——在準(zhǔn)備好推出時遥缕,我會在論壇里貼個鏈接卫袒。
任何時候,如你在不知道他們?nèi)绾巫龅降那闆r下復(fù)制出 Apple 所做的某些效果单匣,你都會發(fā)現(xiàn)有許多許多的方式去做到這樣的效果夕凝。所以這里的方案只是這個效果的實(shí)現(xiàn)辦法之一;然而户秤,它是我所發(fā)現(xiàn)的唯一一個不需要處理嵌套 Scroll View 的辦法码秉,產(chǎn)生的手勢識別沖突也可以非常簡單地解決! :]
寫這篇文章時有一些很有用的資源鸡号,但文章里最終使用了非常不同的辦法转砖。這些資源是 Ash Furrow 的文章 能讓一切都工作起來,以及 Massimiliano Bigatti’s BMXSwipeableCell 項(xiàng)目鲸伴,它現(xiàn)實(shí)通過UIScrollView
這條路可以挖到多深府蔗。
如果你有任何建議、問題或相關(guān)的代碼汞窗,請?jiān)谠u論區(qū)講出來吧姓赤!
譯者:@nixzhu
轉(zhuǎn)載自:
https://github.com/nixzhu/dev-blog