知识点(技巧)
一.通过appearance统一设置所有UITabBarItem的文字属性
局限性
方法后面必须带有UI_APPEARANCE_SELECTOR, 才可以通过appearance对象来统一设置
比如
- (void)setTitleTextAttributes:(nullable NSDictionary<NSString *,id> *)attributes forState:(UIControlState)state NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
后面有“UI_APPEARANCE_SELECTOR”
案例代码
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
attrs[NSFontAttributeName] = [UIFont systemFontOfSize:12];
attrs[NSForegroundColorAttributeName] = [UIColor grayColor];
NSMutableDictionary *selectedAttrs = [NSMutableDictionary dictionary];
selectedAttrs[NSFontAttributeName] = attrs[NSFontAttributeName];
selectedAttrs[NSForegroundColorAttributeName] = [UIColor darkGrayColor];
UITabBarItem *item = [UITabBarItem appearance];
[item setTitleTextAttributes:attrs forState:UIControlStateNormal];
[item setTitleTextAttributes:selectedAttrs forState:UIControlStateSelected];
扩展方法
- //获取哪个类下面的导航条
UINavigationBar *na = [UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[self]]
最佳建议:最好写在此方法中
/**
* 当第一次使用这个类的时候会调用一次
*/
+ (void)initialize
{
}
二、当需要UITabBar控制器时,初始化控制器的方法封装
只需要传入该控制器和一些图片、名称就可以添加到UITabBar控制器内
封装代码
/**
* 初始化子控制器
*/
- (void)setupChildVc:(UIViewController *)vc title:(NSString *)title image:(NSString *)image selectedImage:(NSString *)selectedImage
{
// 设置文字和图片
vc.tabBarItem.title = title;
vc.tabBarItem.image = [UIImage imageNamed:image];
vc.tabBarItem.selectedImage = [UIImage imageNamed:selectedImage];
vc.view.backgroundColor = [UIColor colorWithRed:arc4random_uniform(100)/100.0 green:arc4random_uniform(100)/100.0 blue:arc4random_uniform(100)/100.0 alpha:1.0];
// 添加为子控制器
[self addChildViewController:vc];
}
问题深入:上面代码直接用self.view设置背景颜色,会导致该控制器的view提前创建,应该是在控制器的view需要的时候创建,在viewDidLoad里设置背景颜色
三、自定义tabBar
问题:tabBar属性是只读的。
@property(nonatomic,readonly) UITabBar *tabBar NS_AVAILABLE_IOS(3_0);
解决方案:一:直接利用KVC获取该属性修改
代码
[self setValue:[[XMGTabBar alloc] init] forKeyPath:@"tabBar"];
弊端:tabBar属性需要事先知道,容易写错
之后:事后在initWithFrame方法内创建按钮,在layoutSubviews方法内设置frame
解决方案:二:将原来的tabBarButton删除,全部用UIView,按钮自定义放在上面
主要代码
- (void)viewDidLoad{
不能全部删除,由于稍后hidesBottomBarWhenPushed属性需要用到,
所以只是将自己自定义的视图放在系统自带的上面,上面的按钮可以删除
// [self.tabBar removeFromSuperview];
LZGTabBar *tab = [[LZGTabBar alloc]init];
tab.items = self.array;
tab.backgroundColor = [UIColor redColor];
tab.delegate = self;
tab.frame = self.tabBar.bounds;
[self.tabBar addSubview:tab];
}
删除原来的tabBarButton
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
for (UIView *kkk in self.tabBar.subviews) {
if (![kkk isKindOfClass:[LZGTabBar class]]) {
[kkk removeFromSuperview];
}
}
}
四、 UITabBarButton属性是系统私有的,当需要用它判断时,无法用代码敲出
错误源头
![button isKindOfClass:[UITabBarBu... class]]
- 会发现UITabBarButton无法敲出来判断
解决方法
- 1.利用[ ],逆向判断
- 2.利用这个方法
![button isKindOfClass:NSClassFromString(@"UITabBarButton")]
- 3.取巧
![button isKindOfClass:[UIControl class]] || button == self.publishButton
------------------------------------------
注意:self.title = @"我"
这句话等效于:
self.navigationItem.title = @"我"
self.tabBarItem.title = @"我"
所以当两个控制器都包含的时候,最好不要用self.title.
五、设置navigationBar和tabBar的背景图片
navigationBar
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
[nav.navigationBar setBackgroundImage:[UIImage imageNamed:@"navigationbarBackgroundWhite"] forBarMetrics:UIBarMetricsDefault];
tabBar
[self.tabBar setBackgroundImage:[UIImage imageNamed:@"tabbar-light"]];
也可以通过appearance统一设置所有图片
/**
* 当第一次使用这个类的时候会调用一次
*/
+ (void)initialize
{
// 当导航栏用在XMGNavigationController中, appearance设置才会生效
// UINavigationBar *bar = [UINavigationBar appearanceWhenContainedIn:[self class], nil];
UINavigationBar *bar = [UINavigationBar appearance];
[bar setBackgroundImage:[UIImage imageNamed:@"navigationbarBackgroundWhite"] forBarMetrics:UIBarMetricsDefault];
}
常用设置
设置导航条返回按钮文字的颜色
一.
self.navigationBar.tintColor = [UIColor whiteColor];
- 二.
+ (void)initialize{
UINavigationBar *na = [UINavigationBar appearanceWhenContainedInInstancesOfClasses:@[self]];
[na setBackgroundImage:[UIImage imageNamed:@"NavBar64"] forBarMetrics:UIBarMetricsDefault];
// 设置导航条标题颜色
NSMutableDictionary *titleAttr = [NSMutableDictionary dictionary];
titleAttr[NSForegroundColorAttributeName] = [UIColor whiteColor];
titleAttr[NSFontAttributeName] = [UIFont boldSystemFontOfSize:20];
[na setTitleTextAttributes:titleAttr];
[na setTintColor:[UIColor whiteColor]];
}
自定义导航条返回按钮
- 先自定义导航控制器,在导航控制器内重写push方法
代码
注意点: [super push...]要放在后面, 让viewController可以覆盖这次设置的leftBarButtonItem
/**
* 可以在这个方法中拦截所有push进来的控制器,让返回按钮都一样
*/
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (self.childViewControllers.count > 0) { // 如果push进来的不是第一个控制器
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setTitle:@"返回" forState:UIControlStateNormal];
[button setImage:[UIImage imageNamed:@"navigationButtonReturn"] forState:UIControlStateNormal];
[button setImage:[UIImage imageNamed:@"navigationButtonReturnClick"] forState:UIControlStateHighlighted];
button.size = CGSizeMake(70, 30);
// 让按钮内部的所有内容左对齐
button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
// [button sizeToFit];
// 让按钮的内容往左边偏移10
button.contentEdgeInsets = UIEdgeInsetsMake(0, -10, 0, 0);
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[button setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
[button addTarget:self action:@selector(back) forControlEvents:UIControlEventTouchUpInside];
viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button];
// 隐藏tabbar
viewController.hidesBottomBarWhenPushed = YES;
}
// 这句super的push要放在后面, 让viewController可以覆盖上面设置的leftBarButtonItem
[super pushViewController:viewController animated:animated];
}
- (void)back
{
[self popViewControllerAnimated:YES];
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
if (self.viewControllers.count != 0) {
viewController.hidesBottomBarWhenPushed = YES;
// 设置导航条左边按钮的内容,把系统的返回按钮给覆盖,导航控制器的滑动返回功能就木有啦
viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"NavBack"] style:UIBarButtonItemStylePlain target:self action:@selector(back)];
// 就有滑动返回功能
// self.interactivePopGestureRecognizer.delegate = nil;
}
[super pushViewController:viewController animated:animated];
}
- (void)back{
[self popViewControllerAnimated:YES];
}
UILable内容换行
XIB按键:option + 回车
代码添加文子:\n
注意:
- 当cell的selection为None时, cell被选中时, 内部的子控件就不会进入高亮状态
- 可以在这个方法中监听cell的选中和取消选中
- (void)setSelected:(BOOL)selected animated:(BOOL)animated;
六、数据加载细节问题
本质原因:右边的控制器是公有
1.重复发送请求
描述:
已经发生过的数据,再次发送,浪费用户流量
解决方法:
在模型中添加一个额外数组属性,用于储存上次的数据
关键代码
- 模型属性及实现
/** 这个类别对应的用户数据 */
@property (nonatomic, strong) NSMutableArray *users;
懒加载,在模型内部实现
- (NSMutableArray *)users
{
if (!_users) {
_users = [NSMutableArray array];
}
return _users;
}
- 利用该储存数据,并判断是否有数据
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
XMGRecommendCategory *c = self.categories[indexPath.row];
if (c.users.count) {
// 显示曾经的数据
[self.userTableView reloadData];
} else {
// 发送请求给服务器, 加载右侧的数据
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"a"] = @"list";
params[@"c"] = @"subscribe";
params[@"categoryzz_id"] = @(c.id);
[[AFHTTPSessionManager manager] GET:@"http://api.budejie.com/api/api_open.php" parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
// 字典数组 -> 模型数组
NSArray *users = [XMGRecommendUser objectArrayWithKeyValuesArray:responseObject[@"list"]];
// 添加到当前类别对应的用户数组中
[c.users addObjectsFromArray:users];
// 刷新右边的表格
[self.userTableView reloadData];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
XMGLog(@"%@", error);
}];
}
}
- 通过该属性告示tableView返回多少行
XMGRecommendCategory *c = self.categories[self.categoryTableView.indexPathForSelectedRow.row];
return c.users.count;
2.只能显示1页数据
描述:
在API接口设置请求体的参数已经写死,该怎么灵活变动
解决方案:
- 在模型中添加一个属性用来记录当前的页码,在第一次加载数据时设置为1,之后每次加载数据时该属性要加一
parame[@"page"] = @(++c.category);
;
3.网络慢带来的细节问题
描述1:
1.点击左边第二个分类,由于网速,数据加载慢,右边还是显示第一个页面的数据
核心代码
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
XMGRecommendCategory *c = self.categories[indexPath.row];
if (c.users.count) {
// 显示曾经的数据
[self.userTableView reloadData];
} else {
// 赶紧刷新表格,目的是: 马上显示当前category的用户数据, 不让用户看见上一个category的残留数据
[self.userTableView reloadData];
描述2:
2.其中一个页面上拉几次后会出现“全部加载完毕”字样,换其他页面会直接出现“全部加载完毕”字样
- 在模型内添加一个属性用于保存总用户数目
核心代码
- 写在上拉刷新加载数据,数据返回后的block内
//
是否加载完毕
if (c.users.count == c.total) {//显示全部加载完毕
[self.tableViewRight.mj_footer endRefreshingWithNoMoreData];
}else{
// 让底部控件结束刷新
[self.tableViewRight.mj_footer endRefreshing];
}
- 在最初加载数据的时候就判断数据是否加载完毕
//
是否加载完毕
if (c.users.count == c.total) {//显示全部加载完毕
[self.tableViewRight.mj_footer endRefreshingWithNoMoreData];
}else{
// 让底部控件结束刷新
[self.tableViewRight.mj_footer endRefreshing];
}
- 在每次刷新数据的时候就应该判断数据是否加载完毕
- 作用:左边的的按钮每一次点击的时候都应该判断它自己的数据是否加载完毕
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
if (tableView == self.tableViewLeft) {
return self.categories.count;
}else{
LZGRecommendCategory *c = LZGSelectedCategory;
self.tableViewRight.mj_footer.hidden = (c.users.count == 0);
// 是否加载完毕
if (c.users.count == c.total) {//显示全部加载完毕
[self.tableViewRight.mj_footer endRefreshingWithNoMoreData];
}else{
// 让底部控件结束刷新
[self.tableViewRight.mj_footer endRefreshing];
}
return c.users.count;
}
}
描述3:
3.当加载更多的时候,由于网速过慢,用户切换另一个页面,此时刚才的页面应该取消请求
// 让底部控件结束刷新
[self.tableViewRight.mj_footer endRefreshing];
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
// 结束刷新
[self.tableViewRight.mj_header endRefreshing];
[self.tableViewRight.mj_footer endRefreshing];
LZGRecommendCategory *c = self.categories[indexPath.row];
if (c.users.count) {
[self.tableViewRight reloadData];
}{
[self.tableViewRight reloadData];
// 进入下拉刷新状态
[self.tableViewRight.mj_header beginRefreshing];
}
}
但没有解决根本问题
描述3.5:
3.5.但用户连续点击多次的时候,应该取消上几次的请求,只请求最后一次,前几次可能也有数据加载失败,弹出错误提示,用户体验不好
一
- 首先,应该定义一个属性用于保存发送的请求。
@property (nonatomic , strong)NSMutableDictionary *parms;
核心代码
// 下拉
self.tableViewRight.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
LZGRecommendCategory *c = LZGSelectedCategory;
c.category = 1;
NSMutableDictionary *parame = [NSMutableDictionary dictionary];
parame[@"a"] = @"list";
parame[@"c"] = @"subscribe";
parame[@"category_id"] = @(c.id);
parame[@"page"] = @(c.category);
self.parms = parame;
// 发送请求给服务器, 加载右侧的数据
[self.manager GET:@"http://api.budejie.com/api/api_open.php" parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
// 字典数组 -> 模型数组
NSArray *users = [XMGRecommendUser objectArrayWithKeyValuesArray:responseObject[@"list"]];
// 清除所有旧数据
[rc.users removeAllObjects];
// 添加到当前类别对应的用户数组中
[rc.users addObjectsFromArray:users];
// 保存总数
rc.total = [responseObject[@"total"] integerValue];
// 不是最后一次请求
if (self.params != params) return;
// 刷新右边的表格
[self.userTableView reloadData];
// 结束刷新
[self.userTableView.header endRefreshing];
// 让底部控件结束刷新
// 每次刷新右边数据时, 都控制footer显示或者隐藏
self.userTableView.footer.hidden = (rc.users.count == 0);
// 让底部控件结束刷新
if (rc.users.count == rc.total) { // 全部数据已经加载完毕
[self.userTableView.footer noticeNoMoreData];
} else { // 还没有加载完毕
[self.userTableView.footer endRefreshing];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if (self.params != params) return;
// 提醒
[SVProgressHUD showErrorWithStatus:@"加载用户数据失败"];
// 结束刷新
[self.userTableView.header endRefreshing];
}];
- 上拉部分也要添加
二
新方案
- 同样定义一个属性用于保存请求
/** 管理者 */
@property (nonatomic, strong) AFHTTPSessionManager *manager;
- 取消上次请求,在下拉上拉的时候都要监测
- (void)loadMoreComments{
// 结束之前的所有请求
[self.manager.tasks makeObjectsPerformSelector:@selector(cancel)];
// 页码
NSInteger page = self.page + 1;
// 参数
NSMutableDictionary *params = [NSMutableDictionary dictionary];
params[@"a"] = @"dataList";
params[@"c"] = @"comment";
params[@"data_id"] = self.topic.ID;
params[@"page"] = @(page);
XMGComment *cmt = [self.latestComment lastObject];
......
- 在dealloc方法中的操作
// 取消所有任务
// [self.manager.tasks makeObjectsPerformSelector:@selector(cancel)];
// 取消所有请求
[self.manager invalidateSessionCancelingTasks:YES];
新问题:
描述
用户请求的数据还没有返回,但用户直接点击返回按钮,销毁了此控制器,那返回的数据怎么处理
- 定义一个AF全局变量,
@property(nonatomic,strong)AFHTTPSessionManager *manger;
- 并懒加载
- (AFHTTPSessionManager *)manger{
if (_manger == nil) {
_manger = [AFHTTPSessionManager manager];
}
return _manger;
}
核心代码
/**
* 控制器销毁时
*/
- (void)dealloc{
[self.manger.operationQueue cancelAllOperations];
}
- 或者,调用该请求的cancel方法
NSURLSessionDataTask *ii = [self.manger GET:@"http://api.budejie.com/api/api_open.php" parameters:parame progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if (self.parms != parame) return ;
[c.users addObjectsFromArray:[LZGRecommendUser mj_objectArrayWithKeyValuesArray:responseObject[@"list"]]];
[self.tableViewRight reloadData];
[self checkFooterState];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
if (self.parms != parame) return ;
// 让底部控件结束刷新
[self.tableViewRight.mj_footer endRefreshing];
[SVProgressHUD showErrorWithStatus:@"数据加载失败!"];
}];
[ii cancel];
描述4:
4.第三方框架的底部刷新控件--“点击或上拉加载更多”会在tableView没有数据的时候也显示出来,而我们想要的效果是等数据已经加载第一页的时候或者tableView有数据的时候才出现此提示控件
核心代码
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
if (tableView == self.tableViewLeft) {
return self.categories.count;
}else{
LZGRecommendCategory *c = LZGSelectedCategory;
self.tableViewRight.mj_footer.hidden = (c.users.count == 0);
return c.users.count;
}
}
七.cell的左右两边都有空隙,中间留有空隙
方案一:
在cell上添加一个view,与 cell保持边距,cell为clearColor,具体的控件内容添加到view上
方案二:
设置cell内部的contentView的frame
方案三:
直接让cell的x值往右边偏移,宽度、高度减少
方案三核心代码
- 在cell内部重写frame的set方法拦截frame
- (void)setFrame:(CGRect)frame{
frame.origin.x = 10;
frame.size.width -= 2 * frame.origin.x;
frame.size.height -=10;
[super setFrame:frame];
}
扩展
- (void)setBounds:(CGRect)bounds{
[super setBounds:bounds];
}
frame.size.height -=10;
- (void)setFrame:(CGRect)frame{
frame.origin.x = 10;
frame.size.width -= 2 * frame.origin.x;
frame.size.height = self.topic.cellHeight - XMGTopicCellMargin;
[super setFrame:frame];
}
扩展
- (void)setBounds:(CGRect)bounds{
[super setBounds:bounds];
}