iOS 练习项目 Landmarks (三):详情页设计
iOS 练习项目 Landmarks (三):详情页设计
为starButton添加目标-动作对直接修改places数组
在获取cell的方法里,给cell.starButton.tag赋值为对应的行号,再添加一个目标-动作对,当点击starButton时,触发btnClicked:方法。
在btnClicked:中,先修改按钮的状态,来切换显示的图片;再根据sender.tag获取要修改的places数组元素的下标,取得对应行号的Place实例,设置其favorite属性与sender.selected一致。
// ViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
// 设置 PlaceCell 对象的 starButton 的 tag 为对应的行号
cell.starButton.tag = indexPath.row;
// 添加目标-动作对
[cell.starButton addTarget:self action:@selector(btnClicked:) forControlEvents:UIControlEventTouchUpInside];
...
return cell;
}
#pragma mark - starButton click method
- (void)btnClicked:(UIButton *)sender
{
// NSLog(@"before starButton in row %ld clicked: %d", sender.tag, [self.places[sender.tag] favorite]);
// 修改按钮的状态,切换图片
sender.selected = !sender.selected;
// 修改数据源(根据当前按钮状态设置对应行号的 Place 对象的 favorite 属性)
[self.places[sender.tag] setFavorite:sender.selected];
// NSLog(@"after starButton in row %ld clicked: %d", sender.tag, [self.places[sender.tag] favorite]);
}
PlaceCell通过通知其代理的方式更新数据源
上面的方法有两个缺点:
- 定义tag存在风险,可能会和系统的tag相冲突,太多的tag会使得项目变得过于复杂。
- 在PlaceCell内部直接修改数据源这种方法不好,界面和数据直接交互不符合MVC原则。
我们可以通过协议让数据和界面解耦。
先定义一个PlaceCellDelegate协议:
// PlaceCellDelegate.h
#import <Foundation/Foundation.h>
@class PlaceCell;
NS_ASSUME_NONNULL_BEGIN
@protocol PlaceCellDelegate <NSObject>
- (void)updateFavorite:(BOOL)newFavorite atPlaceCell:(PlaceCell *)cell;
@end
NS_ASSUME_NONNULL_END
没有PlaceCellDelegate.m,该声明也可以放在PlaceCell里。
从协议的名字就可以看出,它是PlaceCell的协议,需要在PlaceCell.h中引入和声明:
#import <UIKit/UIKit.h>
#import "PlaceCellDelegate.h"
NS_ASSUME_NONNULL_BEGIN
@interface PlaceCell : UITableViewCell
@property (nonatomic) UIButton *starButton;
@property (nonatomic, weak) id<PlaceCellDelegate> placeCellDelegate;
@property (nonatomic) BOOL favorite;
- (void)configureWithFavorite:(BOOL)favor;
@end
NS_ASSUME_NONNULL_END
这里还设置了一个favorite属性,和设置该属性和按钮状态的方法,该属性和对应数据源Place对象的favorite属性保持同步。
还是在获取cell的方法里:
// ViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
// 设置 PlaceCellDelegate
cell.placeCellDelegate = self;
[cell configureWithFavorite:[item favorite]];
...
return cell;
}
configureWithFavorite: 方法除了设置PlaceCell的favorite属性,还要设置starButton的状态,进而设置好按钮的图片:
- (void)configureWithFavorite:(BOOL)favor
{
self.favorite = favor;
self.starButton.selected = self.favorite;
}
该方法比存方法多做了一件事,就不取名叫setFavorite了。
在ViewController.h中新增PlaceCellDelegate协议:
@interface ViewController : UIViewController
<UITableViewDelegate, UITableViewDataSource, PlaceCellDelegate>
既然要使用PlaceCellDelegate协议,那就要在ViewController.m里实现PlaceCellDelegate协议的方法:
#pragma mark - PlaceCellDelegate Method
- (void)updateFavorite:(BOOL)newFavorite atPlaceCell:(PlaceCell *)cell
{
// 去 TableView 查该 cell 对应的 indexPath
NSIndexPath *indexPath = [placeTable indexPathForCell:cell];
NSLog(@"before starButton in row %ld clicked: %d", indexPath.row, [self.places[indexPath.row] favorite]);
// 修改数据源对应的对象
[self.places[indexPath.row] setFavorite:newFavorite];
NSLog(@"after starButton in row %ld clicked: %d", indexPath.row, [self.places[indexPath.row] favorite]);
// TableView 重新加载被修改了的那一行
[placeTable reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
PlaceCellDelegate协议的部分就完成了。接下来在PlaceCell中实现收藏按钮点击事件的处理,为starButton添加目标-动作对:
// PlaceCell.m
- (void)initSubView
{
CGRect buttonFrame = CGRectMake(320, 12, 25, 25);
// 创建并设置 UIButton 对象
self.starButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.starButton setFrame:buttonFrame];
...
// 添加目标-动作对
[self.starButton addTarget:self action:@selector(starButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:self.starButton];
}
- (void)starButtonClicked:(UIButton *)sender
{
sender.selected = !sender.selected;
self.favorite = sender.selected;
// 通知 placeCellDelegate
[self.placeCellDelegate updateFavorite:self.favorite atPlaceCell:self];
// NSLog(@"tag: %ld", sender.tag);
// PlaceCell *curCell = (PlaceCell *)[sender superview];
// NSIndexPath *indexPath = [IndexPath indexPathForRow:sender.tag inSection:[tableView reloadRows];
}
当按钮被点击时,首先将sender.selected取反,做到收藏/不收藏的切换,再修改cell的favorite属性,确保和sender.selected一致,再通知其placeCellDelegate(其实就是ViewController),placeCellDelegate就可以根据self.favorite和self(就是PlaceCell对象)去更新数据源。
景点详情页界面设计
参考界面:
可以看出一共有6个部分:
- 地图
- 圆形的位于中央的风景照片
- 景点名标签
- 收藏按钮
- 景区名标签
- 地区名标签
新建一个DetailViewController,声明这6个部分的组件对象,这些组件对象由一个Place对象来设置,所以还要有一个Place指针。
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
@class Place;
NS_ASSUME_NONNULL_BEGIN
@interface DetailViewController : UIViewController
@property (nonatomic) MKMapView *mapView;
@property (nonatomic) UIImageView *pictureView;
@property (nonatomic) UILabel *sightLabel;
@property (nonatomic) UIButton *starButton;
@property (nonatomic) UILabel *scenicAreaLabel;
@property (nonatomic) UILabel *stateLabel;
@property (nonatomic) Place *place;
@end
NS_ASSUME_NONNULL_END
在其实现文件中,对于每个组件,先创建其frame,再设置好样式,再根据Place对象的内容进行设置展现的内容,最后把组件添加到view:
// DetailViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
/* width: 393, height: 852 */
CGFloat viewFrameWidth = self.view.frame.size.width;
CGFloat viewFrameHeight = self.view.frame.size.height;
NSLog(@"%lf, %lf", viewFrameWidth, viewFrameHeight);
CGRect mapFrame = CGRectMake(0, 0, viewFrameWidth, 300);
CGRect pictureFrame = CGRectMake(viewFrameWidth / 2 - 100, 200, 200, 200);
CGRect sightFrame = CGRectMake(20, 420, 150, 25);
CGRect starButtonFrame = CGRectMake(180, 420, 25, 25);
CGRect scenicAreaFrame = CGRectMake(20, 455, 140, 20);
CGRect stateFrame = CGRectMake(viewFrameWidth - 120, 455, 100, 20);
// 创建并设置 mapView
self.mapView = [[MKMapView alloc] initWithFrame:mapFrame];
[self.mapView setMapType:MKMapTypeStandard];
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance([[self place] location].coordinate, 10000, 10000);
[self.mapView setRegion:viewRegion];
// 创建并设置 pictureView
self.pictureView = [[UIImageView alloc] initWithFrame:pictureFrame];
[self.pictureView setImage:[self.place picture]];
// 创建并设置 sightLabel
self.sightLabel = [[UILabel alloc] initWithFrame:sightFrame];
[self.sightLabel setFont:[UIFont systemFontOfSize:24]];
[self.sightLabel setText:[self.place sight]];
// 创建并设置 starButton
self.starButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.starButton setFrame:starButtonFrame];
[self.starButton setTitle:@"" forState:UIControlStateNormal];
[self.starButton setImage:[UIImage imageNamed:@"Image_star"] forState:UIControlStateNormal];
[self.starButton setImage:[UIImage imageNamed:@"Image_starred"] forState:UIControlStateSelected];
// 创建并设置 scenicAreaLabel
self.scenicAreaLabel = [[UILabel alloc] initWithFrame:scenicAreaFrame];
[self.scenicAreaLabel setFont:[UIFont systemFontOfSize:16]];
[self.scenicAreaLabel setText:[self.place scenicArea]];
// 创建并设置 stateLabel
self.stateLabel = [[UILabel alloc] initWithFrame:stateFrame];
[self.stateLabel setFont:[UIFont systemFontOfSize:16]];
[self.stateLabel setText:[self.place state]];
[self.view setBackgroundColor:[UIColor whiteColor]];
[self.view addSubview:self.mapView];
[self.view addSubview:self.pictureView];
[self.view addSubview:self.sightLabel];
[self.view addSubview:self.starButton];
[self.view addSubview:self.scenicAreaLabel];
[self.view addSubview:self.stateLabel];
}
这样一个基本的详情页就创建好了。
点击UITableViewCell跳转到对应详情页
首先要给ViewController提供一个NavigationController,可以直接在Main故事板中添加一个:
UITableViewDelegate协议里的tableView:didSelectRowAtIndexPath:方法会在选中一个UITableViewCell后触发,我们在这个方法里实现页面跳转:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// NSLog(@"row %ld clicked", indexPath.row);
DetailViewController *detailViewController = [DetailViewController new];
[detailViewController setPlace:[self.places objectAtIndex:indexPath.row]];
[self.navigationController pushViewController:detailViewController animated:NO];
}
步骤分为3步:
- 初始化一个DetailViewController对象;
- 从数据源取出被点击cell对应行的数据,来设置DetailViewController对象的place属性;
- 使用NavigationController的pushViewController:animated:方法进行跳转。
运行效果
详情页如下所示: