iOS 7 - 如何在表格视图中显示一个日期选择器?

121
在WWDC 2013视频中,苹果建议在iOS 7的表视图中直接显示选取器。如何在表格视图单元格之间插入并动画显示一个视图?
就像这样,来自苹果日历应用程序的图片: In-place date picker

这是哪个WWDC视频?我想了解为什么这是iOS7特定的,以及早期版本为什么不能做到这一点的原因。 - Clafou
4
找到了,它是“iOS用户界面设计有什么新变化”(感谢ASCIIwwdc)。其原因是旧的样式在内联显示时看起来不太对劲,但新的扁平化外观则不会。 - Clafou
13个回答

102

iOS7发布后,苹果公司提供了样例代码DateCell

enter image description here

演示了日期对象在表格单元中的格式化显示以及使用UIDatePicker编辑这些值的方法。 该示例作为此表格的代理,使用方法“didSelectRowAtIndexPath”打开UIDatePicker控件。

对于iOS 6.x及更早版本,UIViewAnimation用于将UIDatePicker向上滑动到屏幕上并向下移出屏幕。对于iOS 7.x,则在表视图中内联添加UIDatePicker。

UIDatePicker的操作方法将直接设置自定义表格单元的NSDate属性。此外,此示例展示了如何使用NSDateFormatter类实现自定义单元格的日期格式化外观。

enter image description here

您可以从此处下载样例代码:DateCell


苹果的代码存在问题。如果您想保留静态tableView,请参阅下面的答案。 - Aaron Bratcher
1
@AaronBratcher,我在我的回答中介绍了Datecell,但你不能期望得到完全符合你要求的代码。你需要根据自己的需求进行修改和更改。你的解决方案也不错,但请记住,你永远无法从其他人的代码中得到完全符合你要求的代码。 - Nitin Gohel
3
最可怕的示例代码。它们需要一些关于何时从《代码大全》中创建方法的建议。 - Anirudha Agashe
2
哇,这个苹果公司的代码……嗯,我们可以说重点是在表格视图效果上。我希望没有人会受到他们在这个示例项目中的代码启发。:s - Daniel

84

您可以使用我之前下面给出的答案,或者使用我写的这个Swift新类,使这个任务变得更加简单和清晰:https://github.com/AaronBratcher/TableViewHelper


我发现苹果提供的代码有几个问题:

  • 由于它们正在使用tableView:cellForRowAtIndexPath方法,因此您不能拥有静态的tableView。
  • 如果您在最后一个日期选择器单元格下面没有其他行,则代码会崩溃。

对于静态单元格表格,我将我的日期选择器单元格定义在日期显示单元格下面,并设置一个标志来标识我是否正在编辑它。如果我正在编辑,我将返回适当的单元格高度,否则我将返回零高度的单元格。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0 && indexPath.row == 2) { // this is my picker cell
        if (editingStartTime) {
            return 219;
        } else {
            return 0;
        }
    } else {
        return self.tableView.rowHeight;
    }
}

当单击显示日期的行时,我会更改标记并执行更新动画以显示选择器。

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0 && indexPath.row == 1) { // this is my date cell above the picker cell
        editingStartTime = !editingStartTime;
        [UIView animateWithDuration:.4 animations:^{
            [self.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:2 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView reloadData];
        }];
    }
}

如果在同一个表中有多个日期/时间选择器,我会根据点击设置标志并重新加载适当的行。我发现可以保持静态表格,使用更少的代码,并且更容易理解正在发生的事情。


1
你的意思是要显示一行,以便用户可以输入文本而不是选择日期吗?如果是这样,代码是相同的。只是显示的行内容不同。 - Aaron Bratcher
22
iOS7.1存在一个问题,即显示超出单元格边界的对象,比如高度为0且内部有Picker的对象。解决方法是获取单元格的输出口(outlet),并设置cell.clipsToBounds = YES;。 - EmilDo
我刚刚注意到了那个 bug,正准备寻找一个解决方法。谢谢! - Aaron Bratcher
使用以下代码替换UIView动画块:[tableView beginUpdates]; [tableView endUpdates]; // 更简单的解决方案 - budiDino
1
@Dunken - 上面点赞的评论中描述了一种解决方案:https://dev59.com/RWMk5IYBdhLWcg3wywwU#3qifEYcBWogLw_1bjU8y 即使行高为零,内容默认情况下也不会被裁剪,因此您可以在下一行下面看到它们。 - Chris Nolet
显示剩余11条评论

18

使用storyboard和静态表格,我能够使用以下代码实现相同的结果。这是一个很好的解决方案,因为如果你有许多奇怪形状的单元格或者想要动态显示/隐藏多个单元格,这个代码仍然可以工作。

@interface StaticTableViewController: UITableViewController

@property (weak, nonatomic) IBOutlet UITableViewCell *dateTitleCell; // cell that will open the date picker. This is linked from the story board
@property (nonatomic, assign, getter = isDateOpen) BOOL dateOpen;

@end


@implementation StaticTableViewController

-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{

    // This is the index path of the date picker cell in the static table
    if (indexPath.section == 1 && indexPath.row == 1 && !self.isDateOpen){
        return 0;
    }
    return [super tableView:tableView heightForRowAtIndexPath:indexPath];
}

-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
    [tableView beginUpdates];
    if (cell == self.dateTitleCell){
        self.dateOpen = !self.isDateOpen;
    }
    [tableView reloadData];
    [self.tableView endUpdates];
}

这里还有什么其他的东西缺失吗?单元格的高度没有改变 :) - DevC
2
自从iOS 7.1版本以来,您还必须在属性检查器中选择“剪切到子视图” - 这就是我之前遗漏的 :) - DevC
另外,"self.dateOpen =!self.isDateOpen;" 这一行应该改为 "self.isDateOpen =" 开头,而不是 "self.dateOpen ="。 - Peter Johnson
2
[tableView reloadData]; 调用并非必需。目前它会禁用行选择,但最好像这样取消选择行: [self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES]; - Barry Haanstra
我遇到了奇怪的动画效果,比如日期单元格在“打开”后会变成空白,但使用begin和endupdates解决了这个问题。现在非常流畅。谢谢datinc! - nh32rg

5

这方面最好的教程之一是iOS 7 in-line UIDatePicker – Part 2。基本上我使用静态表格视图单元并实现了一些额外的方法。我使用Xamarin和C#进行了编写:

您需要激活Clip Subviews

设置高度:

public override float GetHeightForRow (UITableView tableView, NSIndexPath indexPath)
{
    if (indexPath.Row == 4) {
        return (datePickerIsShowing) ? 206f : 0.0f;
    }

    return base.GetHeightForRow(tableView,indexPath);
}

比类变量更好的是:private bool datePickerIsShowing = false;

显示日期选择器:

private void showDatePickerCell(){
    datePickerIsShowing = true;
    this.TableView.BeginUpdates ();
    this.TableView.EndUpdates ();
    this.datePicker.Hidden = false;
    this.datePicker.Alpha = 0.0f;

    UIView.Animate (0.25, animation:
        () => {
            this.datePicker.Alpha = 1.0f;
        }
    );
} 

隐藏日期选择器:

private void hideDatePickerCell(){
    datePickerIsShowing = false;
    this.TableView.BeginUpdates ();
    this.TableView.EndUpdates ();

    UIView.Animate (0.25,
        animation: () => {
            this.datePicker.Alpha = 0.0f;
        },
        completion: () => {
            this.datePicker.Hidden = true;
        }
    );
} 

同时调用这些函数:

public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
    if (indexPath.Row == 3) {
        if (datePickerIsShowing) {
            hideDatePickerCell ();
        } else {
            showDatePickerCell ();
        }
    }

    this.TableView.DeselectRow (indexPath, true);
}

2
如果你要点踩,或许可以解释一下为什么要点踩。不要只是贴链接到网站,我也包含了一些代码。在我的情况下,这是C#代码。不知道哪里出了问题。 - testing

5

谢谢@Ajay,我使用了您修改过的代码,并想用UIPickerView替换UIDatePicker。我尝试了一些方法,并能够在每个单元格上显示pickerView,但它不会随着每个单元格的更新而更新。我已经在这里发布了我的问题和代码。请看一下,如果您能为我提供解决方案,那将非常友善。 - Mughees Musaddiq
我看到了你的问题,它包含了太多信息,并没有真正解释清楚。也许你可以把你的代码/压缩文件上传到某个地方,让我来看看。如果我能运行这段代码,我可以进行调试。 - Ajay Gautam
谢谢,我设法用UIPickerView替换了UIDatePicker,但出现了一些问题。1.当您打开UIPickerView并滚动表格时,它会崩溃。2.当值被分配给顶部行的详细标签时,它会自动将该值分配给表格底部行的UILabel详细信息。这是我的代码 - Mughees Musaddiq
你能找到testProject吗?有什么运气吗? - Mughees Musaddiq
谢谢。在iOS 8中也可以工作。 - Isuru
显示剩余2条评论

3

在之前的回答中,我尝试了@datinc和@Aaron Bratcher的解决方案,两者都非常好用,但在分组静态tableView中动画效果不太流畅。

经过一些尝试后,我得到了以下代码,对我来说运行得很流畅:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0 && indexPath.row == 1)
    {
        if (self.isPickerOpened)
        {
            return 162;
        }
        else
        {
            return 0;
        }
    }
    else
    {
        return [super tableView:tableView heightForRowAtIndexPath:indexPath];
    }
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.section == 0 && indexPath.row == 0) {
        [tableView beginUpdates];
        self.isPickerOpened = ! self.isPickerOpened;
        [super tableView:tableView heightForRowAtIndexPath:indexPath];
        [self.tableView endUpdates];
    }
}

主要的变化是使用 -

        [super tableView:tableView heightForRowAtIndexPath:indexPath];

为了更新行,这样表格的其余部分和单元格都不会动画。

希望能对某人有所帮助。

Shani


这帮了我很大的忙,谢谢!具体来说,[super tableView]这个方面以及Aaron Bratcher的文章为我在iOS 9.2上得到了需要的结果。再次感谢! - Terrance Shaw

3

Aaron Bratcher 的答案在使用多个部分时存在问题。动画有点卡顿,而且它不能很好地下滑到下一部分。为了解决这个问题,我遍历了下一个部分的行,并将它们向下平移与日期选择器的高度相同的量。

我编辑了 didSelectRowAtIndexPath 方法:

// Return Data to delegate: either way is fine, although passing back the object may be more efficient
// [_delegate imageSelected:currentRecord.image withTitle:currentRecord.title withCreator:currentRecord.creator];
// [_delegate animalSelected:currentRecord];
if (indexPath.section == 1 && indexPath.row == 0) { // this is my date cell above the picker cell
    editingStartTime = !editingStartTime;
    [UIView animateWithDuration:.4 animations:^{
        int height = 0;
        if (editingStartTime) {
            height = 162;
        }
        UITableViewCell* temp = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:1]];
        [temp setFrame:CGRectMake(temp.frame.origin.x, temp.frame.origin.y, temp.frame.size.width, height)];
        for (int x = 2; x < [tableView numberOfSections]; x++) {
            for (int y = 0; y < [tableView numberOfRowsInSection:x]; y++) {
                UITableViewCell* temp = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:y inSection:x]];
                int y_coord = temp.frame.origin.y-162;
                if (editingStartTime) {
                    y_coord = temp.frame.origin.y+162;
                }
                [temp setFrame:CGRectMake(temp.frame.origin.x, y_coord, temp.frame.size.width, temp.frame.size.height)];
            }
        }
    }completion:^(BOOL finished){
        [self.tableView reloadData];
    }];
}

谢谢@Timmahh,我也发现了动画部分有些卡顿。我成功地在我的第一个Swift应用程序中使用了你的解决方案!不过我发现当单元格不在屏幕上时,cellForRowAtIndexPath会返回nil。为了避免这种情况,我添加了一个IB outlet-collection,其中包含所有我的单元格(位于选择器单元格之后),并使用你的新y_coord迭代它们。但是这似乎不太灵活,因为我必须在添加新单元格时保持集合最新。 - Josh
你说得对。事实上,我已经解决了这个问题,但是堆栈溢出不允许我编辑我放在那里的代码。 - Timothy Logan

3
我找到了一个关于苹果日期单元格示例中缺陷的解决方案,即必须在最后一个日期单元格下面有一行,否则会出错。在CellForRowAtIndexPath方法中,将ItemData行替换为:
NSArray *itemsArray = [self.dataArray objectAtIndex:indexPath.section];
NSDictionary *itemData = nil;

if(![indexPath isEqual:self.datePickerIndexPath])
    itemData = [itemsArray objectAtIndex:modelRow];

在替换示例代码之后,我现在可以显示一个没有下面单元格的datePicker单元格。

我刚加入stackoverflow,如果这个问题不应该在这里或者在其他地方,请见谅。


3
我已经制作了自己的自定义视图控制器,以简化在表格视图中添加内联选择器的过程。您只需对其进行子类化并遵循一些简单的规则,它就会处理日期选择器的呈现。
您可以在此处找到它以及演示如何使用它的示例项目: https://github.com/ale84/ALEInlineDatePickerViewController

2

在之前的答案和@Aaron Bratcher的解决方案基础上...

iOS 9以来,我的动画变得卡顿,表格加载时间过长,让人感到烦恼。我发现是由于从storyboard中加载日期选择器太慢导致的。通过以编程方式添加选择器而不是在storyboard中添加选择器,可以提高加载性能,同时也使动画更加流畅。

从storyboard中移除日期选择器并保留一个空单元格,在其中设置与之前答案相同的高度,然后在viewDidLoad中调用初始化函数:

- (void)initialiseDatePickers
{
    self.isEditingStartTime = NO;
    self.startTimePickerCell.clipsToBounds = YES;

    UIDatePicker *startTimePicker = [[UIDatePicker alloc] init];
    [startTimePicker addTarget:self action:@selector(startTimePickerChanged:) forControlEvents:UIControlEventValueChanged];
    [self.startTimePickerCell addSubview:startTimePicker];
}

然后实现动作,例如:

- (IBAction)startTimePickerChanged:(id)sender
{
    NSLog(@"start time picker changed");
}

这比之前更快地加载了表格。您还可以从didSelectRowAtIndexPath中删除动画行,因为即使没有该行,它也可以平稳地进行动画(可能因人而异)。

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0 && indexPath.row == 1) { // this is my date cell above the picker cell
        editingStartTime = !editingStartTime;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接