iOS TableView 编程指导(三)-创建和配置TableView

在展示TableView前要配置好, 设置TableView的delegate和DataSource对象.
本章用到的代码来之demo:UITableView Fundamentals for iOSTheElements

创建TableView的基本知识


为创建TableView, 需要创建几个相互交互的实体对象:视图控制器, TableView自己和tableView的DataSource和delegate对象, 视图控制器, DataSource和delegate通常是同一个对象. 视图控制器的启动了调用序列, 图3-1使用展示了这一过程.

  1. 视图控制器使用frame和style来创建tableView实例. frame通常为screen的frame,减去各种bar(导航/状态栏等)的高度即可. 在此刻也可设置一个全局变量表保存tableView, 或者用一个属性保存tableView的全局属性, 比如, 行高, autoresizing行为.
  2. 视图控制器给tableView设置DataSource和delegate, 然后给TableView发送reloadData消息.
  3. TableView会给DataSource发送numberOfSectionsInTableView:消息, DataSource会使用该方法返回TableView的section数, 这个方法时可选的, 但如果你的tableView中有多个section的话, 必须实现该方法来告知TableView具有多个section.
  4. 对于每一个section, DataSource都会收到TableView发送来的TableView:numberOfRowsInSection:消息, 该方法中返回每个section中的行数
  5. 之后DataSource会收到对应的可见cell的tableView:cellForRowAtIndexPath:消息, 然后返回对应cell的UITableViewCell对象, tableView使用该对象去绘制表视图中的行.
图3-1 创建和配置TableView调用时序图

图4-1展示了DataSource中的required方法加上numberOfSectionsInTableView:. DataSource和delegate除了实现required方法外, 可以实现其他optional方法来提供一些额外的特性, 比如实现tableView:titleForHeaderInSection:方法来给section提供title. 你可以创建plain或者grouped样式的tableView, 尽管以两种样式创建表视图的过程是相同的, 但是您可能希望执行不同类型的配置. 例如, 因为分组表视图通常呈现项目细节, 所以您可能还希望添加自定义附件视图(例如, 开关和滑块)或自定义内容(例如, 文本字段). 具体见详细地看看tableView中的cell

创建和配置TableView的一些建议


在APP中, tableView的用法很多, 想怎么用就怎么用, 你可以自定义一个对象去创建, 管理, 配置tableView, 但是你应该使用UIKit提供的设计好的一些关于tableView的类和技术, 也应该遵循UIKit对于tableView使用的一些规范, 比如数据/表现分离, 下面是UIKit关于创建和使用tableView的一些建议:

  • 尽量使用UITableViewController来创建和管理TableView
  • 如果你的APP中大量使用TableView的话, 那么你在创建xcode工程时, 应该使用Master-Detail应用程序模板
  • 对于展示连续的TableView, 你应该使用自定义的UITableViewController, 这样你既可以从storyboard中加载TableView, 也可以通过代码创建一个关联TableView.

如果你的界面是多个视图组合而成, 而TableView只是组成界面的一部分, 那么你应该使用UIViewController来管理tableView, 不能用UITableViewController, 因为后者会使得tableView的size固定填充屏幕(减去各种bar的空间).

使用storyboard来创建TableView


使用xcode创建包含tableView的APP时, 选则模板应该包含方便创建TableView的代码存根和storyboard, 该模板提供一个骨架, 你只需添加而外的代码和设置即可, 这样很方便

创建基于TableView的APP:

  1. 选择Xcode, 选择文件>新建>项目
  2. 在弹出的对话框的左侧,选择iOS, 选择应用程序
  3. 在对话框的中间主要区域中, 选择'Mater-Detail 应用程序', 点击'下一步'
  4. 选择项目选项(确保使用storyboard), 然后点击'下一步'
  5. 选择项目的保存位置, 然后点击'创建'

在第四步选择设备时, 会决定项目中的storyboard数量. 点击项目导航栏中的storyboard文件, 会显示storyboard编辑视图. 如果你选择的是iPhone, 那么storyboard中包含一个TableViewController, 如图3-2所示.


图3-2 Master-Detail应用中的master view controller对应的storyboard

确保画布上的场景表示代码中的主视图控制器

  • 在画布上, 单击场景标题来选择表视图控制器
  • 单击工具栏区域顶部的Identity button来打开Identity inspector
  • 检查工程中的类是否包含UITableViewController的子类

选择TableView的Display Style

TableView风格包括两种:plain和grouped
在storyboard中选择TableView的style

  1. 点击scene的中间来选中tableView.
  2. 在工具栏中选择Attributes inspector
  3. 在tableView的Attributes inspector中选择plain或者grouped style

选择TableView的Content Type

在设计tableView的内容是, storyboard中有两种便捷的方式.

  • Dynamic prototypes 设计的cell的prototype的目的是为了复用. 如果你的cell复用时, 使用的是相同的layout, 那么使用Dynamic, Dynamic的内容由表视图数据源(表视图控制器)在运行时以任意数量的单元格进行管理。图3-3显示了带有一个Dynamic的plain表视图。
    图3-3 Dynamic tableView

注意:如果storyboard中的tableView是dynamic, 那么tableView一定需要一个DataSource对象, 也就是说UITableViewController的子类需要实现data source协议

  • Static cells 使用是static cell的话, cell的内容的layout, cell的个数已经确定. TableView的cell和内容在运行前已经确定好, 是固定的. 你还是设置section header等静态信息. 当TableView的cell的layout不会改变时, 使用static cell. 如图3-4, static cell的tableView.
图3-4 static cell

注意:如果storyboard中的TableView使用static cell, 那么就不需要DataSource对象了, 因为TableView的cell时确定的.

cell的重用标识符(reuse identifier)是用来标识重用的cell的, 标识符的字符串最好是能够描述cell中包含的内容, 比如cell是用来展示鸟瞰图的, 那么他的重用标识符为@"BirdSightingCell"

设计TableView的cell

前面文章有讲过, UIKit为你提供了四种类型的cell. 如果系统提供的这四种类型不能满足需求, 你可以继承UITableViewCell来自定义cell. 具体如何自定义cell你将在接下来的文章学到, 请关注本系列文章.

TableViewCell可以拥有附加视图(Accessory view). 附加视图是UIKit提供显示在cell的右边的一个视图, 比如disclosure indicator, 使用>图标来表示, 告诉用户点击该图标可以获取更多信息.

创建多个TableView

如果你的APP中存在和管理着多个TableView, 那么往storyboard中多拖几个TableView, 可以通过将你创建的UITableViewController子类和TableView绑定.
往工程中添加自定义类文件

  1. 打开Xcode, 选择File > New > File
  2. 在弹出的对话框中选择iOS中的cocoa touch
  3. 在对话框中选择Object-C类, 点击下一步
  4. 输入文件名称, 选择UITableViewController, 点击下一步
  5. 选择保存的路径, 点击创建

往storyboard中添加表视图控制器

  1. 打开要添加TableView的storyboard
  2. 在对象库中拖一个TableViewController到storyboard中
  3. 选中添加的scene, 点击utility area中的Identity button
  4. 在custom class组中, 选择刚创建的类
  5. 选择TableView的cell的style(dynamic/static)
  6. 往新的scene上添加一个segue

通过创建一个demo APP可以学到更多

通过Apple实战指导Your Second iOS App: Storyboards, 你可以学习如何通过storyboard技术来创建APP, 在该指导文档中, 你将会学习如何创建一个基于导航的APP, 以及如何创建使用TableView来构建界面.

使用代码创建tableView


使用UITableViewController的好处, 能够省写很多代码, 而且可以避免一些错误.

实现DataSource和Delegate协议

使一个类成为TableView的DataSource和delegate需要遵守UITableViewDatasSourceUITableViewDelegate协议. 代码清单3-1展示TableView的DataSource和Delegate.

代码清单3-1 遵守DataSource和delegate协议

@interface RootViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
 
@property (nonatomic, strong) NSArray *timeZoneNames;
@end

创建和配置tableView

第二步是alloc, init出TableView实例. 代码清单3-2展示了创建一个plain表视图, 然后设置各种属性,frame, DataSource, delegate等信息, 在使用UITableViewController时, 是不用设置这些属性的, 因为它自动帮你设置好了
代码清单3-2 创建一个tableView

- (void)loadView
{
    UITableView *tableView = [[UITableView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame] style:UITableViewStylePlain];
    tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
    tableView.delegate = self;
    tableView.dataSource = self;
    [tableView reloadData];
 
    self.view = tableView;
}

使用数据动态填充tableView


当TableView被创建后, TableView会收到reloadData消息, 让TableView去访问DataSourcedelegate对象, 来获取section, row等信息, 这时DataSource会告诉TableView该显示多少section和row. 之后DataSource重复调用tableView:cellForRowAtIndexPath:方法,返回一个UITableViewCell对象, 来告诉tableView该如何绘制row.(当你滚动tableView时, 也会导致tableView:cellForRowAtIndexPath:被调用, 这是因为新cell会显示).

前面提过, 如果tableView是动态的(dynamic), 那么就必须实现DataSource协议, 代码清单3-3展示一个实现了DataSource和delegate协议的类.

代码清单3-3 给动态tableView填充数据

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [regions count];
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    // Number of rows is the number of time zones in the region for the specified section.
    Region *region = [regions objectAtIndex:section];
    return [region.timeZoneWrappers count];
}
 
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    // The header for the section is the region name -- get this from the region at the section index.
    Region *region = [regions objectAtIndex:section];
    return [region name];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *MyIdentifier = @"MyReuseIdentifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault  reuseIdentifier:MyIdentifier];
    }
    Region *region = [regions objectAtIndex:indexPath.section];
    TimeZoneWrapper *timeZoneWrapper = [region.timeZoneWrappers objectAtIndex:indexPath.row];
    cell.textLabel.text = timeZoneWrapper.localeName;
    return cell;
}

tableView:cellForRowAtIndexPath:方法中, 出于性能考虑, DataSource会尽可能的复用cell, 首先会发送一个dequeueReusableCellWithIdentifier:消息给到tableView, 要求TableView提供一个特定重用标识符的复用cell, 如果该cell不存在, DataSource要负责创建该cell, 并设置特定的reuse identifier. DataSource还要负责设置cell的content(上例中, 设置text)然后返回cell给TableView. 具体如何设置cell的content请看后续文章详细地看看tableView中的cell

如果你的cell是在storyboard定义的, 使用dequeueReusableCellWithIdentifier:方法进行获取cell时总是有效的, 如果那里不需要使用复用的cell, 那么该方法会返回一个使用storyboard信息创建的新cell. 这样就不需要检查返回值是否为nil了.

使用数据动态填充tableView


如果TableView是静态的, 那么就不需要DataSource对象了, TableView在编译期配置了好, 不需要在运行期在去调用DataSource的方法了. 但是你需要使用data model中的数据来填充TableView的内容, 下面代码清单3-4就是一个这样的示例.(改代码示例来源Your Second iOS App: Storyboards)
代码清单3-4 使用数据填充静态tableView

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    BirdSighting *theSighting = self.sighting;
    static NSDateFormatter *formatter = nil;
    if (formatter == nil) {
        formatter = [[NSDateFormatter alloc] init];
        [formatter setDateStyle:NSDateFormatterMediumStyle];
    }
    if (theSighting) {
        self.birdNameLabel.text = theSighting.name;
        self.locationLabel.text = theSighting.location;
        self.dateLabel.text = [formatter stringFromDate:(NSDate*)theSighting.date];
    }
}

上面的代码实例中, 在viewDidLoad(该方法会等view加载到内存后调用)对tableView进行填充数据, 其中一些属性(birdNameLabel,locationLabel)是storyboard中的outlet.

填充索引列表


当需要大量按字母分类排序的数据时, 可以使用索引列表(如第一章图1-2所示). 索引列表是一种plain风格的TableView, 而且还需要对DataSource对象进行一些特殊的设置:

  • sectionIndexTitlesForTableView:
    返回一组字符串作为index使用

  • tableView:titleForHeaderInSection:
    为每一个section指定一个index, 作为section的title

  • tableView:sectionForSectionIndexTitle:atIndex:
    指定用户点击进入相应的section的index.

在构建索引列表时, 用来填充tableView的数据结构与索引结构相符. 一般需要创建一个二维数组来保存数据. section的个数通过外层数组来反映, 内层数组代表一个section中的内容, 而且还要对这些数组按照现行索引顺序进行排序(一般按照字面表排序, 如A-Z). 可以通过类UILocalizedIndexedCollation(该类可以根据特定localization)来对二维数组排序, 这样可以简化构建顺序的二维数据这种数据结构.

模型对象需要提供一个string属性和返回string的方法来供类UILocalizedIndexedCollation进行检验. 如果模型对象提供的是一个方法, 那么这个方法应该不需要参数. 如果一个model对象可以代表tableView中的行, 这样非常方便, 所以在定义model时, 你可以定义一个属性来保存section的index, 或者row的index. 代码清单4-5展示了这样的model的定义
代码清单3-5 设计model类

@interface State : NSObject
@property(nonatomic,copy) NSString *name;
@property(nonatomic,copy) NSString *capitol;
@property(nonatomic,copy) NSString *population;
@property NSInteger sectionNumber;
@end

在填充tableView数据之前, 你应该将数据创建加载好. 下面代码展示了数据的加载和数据的排序处理

代码清单3-6 loading数据

- (void)viewDidLoad {
    [super viewDidLoad];
    UILocalizedIndexedCollation *theCollation = [UILocalizedIndexedCollation currentCollation];
    self.states = [NSMutableArray arrayWithCapacity:1];
 
    NSString *thePath = [[NSBundle mainBundle] pathForResource:@"States" ofType:@"plist"];
    NSArray *tempArray;
    NSMutableArray *statesTemp;
    if (thePath && (tempArray = [NSArray arrayWithContentsOfFile:thePath]) ) {
        statesTemp = [NSMutableArray arrayWithCapacity:1];
        for (NSDictionary *stateDict in tempArray) {
            State *aState = [[State alloc] init];
            aState.name = [stateDict objectForKey:@"Name"];
            aState.population = [stateDict objectForKey:@"Population"];
            aState.capitol = [stateDict objectForKey:@"Capitol"];
            [statesTemp addObject:aState];
        }
    } else  {
        return;
    }

当数据加载完后, 可以使用UILocalizedIndexedCollation类对数据进行处理

代码清单3-7 对数据进行索引化

// viewDidLoad continued...
    // (1)
    for (State *theState in statesTemp) {
        NSInteger sect = [theCollation sectionForObject:theState collationStringSelector:@selector(name)];
        theState.sectionNumber = sect;
    }
    // (2)
    NSInteger highSection = [[theCollation sectionTitles] count];
    NSMutableArray *sectionArrays = [NSMutableArray arrayWithCapacity:highSection];
    for (int i = 0; i < highSection; i++) {
        NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:1];
        [sectionArrays addObject:sectionArray];
    }
    // (3)
    for (State *theState in statesTemp) {
        [(NSMutableArray *)[sectionArrays objectAtIndex:theState.sectionNumber] addObject:theState];
    }
    // (4)
    for (NSMutableArray *sectionArray in sectionArrays) {
        NSArray *sortedSection = [theCollation sortedArrayFromArray:sectionArray
            collationStringSelector:@selector(name)];
        [self.states addObject:sortedSection];
    }
} // end of viewDidLoad

对上面代码做一些说明:

  1. 遍历所有model数据, 调用collation对象的sectionForObject:collationStringSelector:方法, 该方法的参数是一个model对象, 和一个model对象属性或者定义的方法(用来进行检验的), 然后该方法会返回一个处于哪个section的index. 将前面的index设置为model的sectionNumber
  2. 创建一个section数组(外层), 保存每一个section中的数据.
  3. 循环遍历一个模型数组中的每个数据, 将其加入相应的section数组(内存)中.
  4. 然后调用collation manager的sortedArrayFromArray:collationStringSelector:方法来对section数组(外层)中的内层数组排序

经过上面的步骤后, DataSource已经将数据准备好, 下面是具体设置列表的index, 代码清单3-8展示了这一过程, 该过程调用了UILocalizedIndexedCollation类的两个方法sectionIndexTitlessectionForSectionIndexTitleAtIndex:来完成任务, 注意在方法tableView:titleForHeaderInSection:方法中, 会将空的section从列表中移除.

代码清单3-8 为tableView提供section-index数据

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles];
}
 
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    if ([[self.states objectAtIndex:section] count] > 0) {
        return [[[UILocalizedIndexedCollation currentCollation] sectionTitles] objectAtIndex:section];
    }
    return nil;
}
 
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
    return [[UILocalizedIndexedCollation currentCollation] sectionForSectionIndexTitleAtIndex:index];
}

最后实现DataSource协议方法, 如代码清单3-9所示
代码清单3-9 填充索引列表中的行

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [self.states count];
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [[self.states objectAtIndex:section] count];
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"StateCell";
    UITableViewCell *cell;
    cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
    State *stateObj = [[self.states objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
    cell.textLabel.text = stateObj.name;
    return cell;
}

如果你的TableView是索引列表, 那么请注意, 你应该将cell的accessoryType设置为UITableViewCellAccessoryNone.

经过上面的一个步骤轮廓, 你可以调用reloadSectionIndextitles方法来reload列表的index

tableView的可选性配置


对于tableView, UIKit提供了很多Api来对tableView进行配置, 可以在外观和行为上对tableView进行各种控制, 比如控制section, 控制row等等. 下面就这些配置提供几个方向给你选择.

添加一个自定义标题

在对tableView进行配置时, 使用UITableView类的方法对tableView配置是全局性的. 下面代码展示了如何为tableView添加一个自定义的title(使用UILabel)

代码清单3-10 为tableView添加一个标题

- (void)loadView {
    CGRect titleRect = CGRectMake(0, 0, 300, 40);
    UILabel *tableTitle = [[UILabel alloc] initWithFrame:titleRect];
    tableTitle.textColor = [UIColor blueColor];
    tableTitle.backgroundColor = [self.tableView backgroundColor];
    tableTitle.opaque = YES;
    tableTitle.font = [UIFont boldSystemFontOfSize:18];
    tableTitle.text = [curTrail objectForKey:@"Name"];
    self.tableView.tableHeaderView = tableTitle;
    [self.tableView reloadData];
}

提供一个section表示

代码清单3-11 返回section的title

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    // Returns section title based on physical state: [solid, liquid, gas, artificial]
    return [[[PeriodicElements sharedPeriodicElements] elementPhysicalStatesArray] objectAtIndex:section];
}

缩进一行

代码清单3-12 设置行的缩进

- (NSInteger)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath {
    if ( indexPath.section==TRAIL_MAP_SECTION && indexPath.row==0 ) {
        return 2;
    }
    return 1;
}

改变一行的高度

代码清单3-13 控制不同行的行高

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat result;
    switch ([indexPath row])
    {
        case 0:
        {
            result = kUIRowHeight;
            break;
        }
        case 1:
        {
            result = kUIRowLabelHeight;
            break;
        }
    }
    return result;
}

自定义Cell

关于要自定义cell, 那么主要的操作集中在DataSource的tableView:cellForRowAtIndexPath:方法中, 返回一个自定义的UITableViewCell子类的cell, 具体内容请看下篇文章详细地看看tableView中的cell

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容