六天完成一个简单iOS App - 第一天

项目介绍

仿照百思不得姐,通过看李明杰老师视频学习自己实践并简单总结项目开发过程中普遍遇到的问题,并且将可以用到其他项目中的分类方法进行简单总结,便于以后在别的项目中使用。
每天任务 1. 实现相应功能 2. 代码重构,简单优化

第一天任务:

  1. 配置项目基本环境
  2. 搭建框架
  3. 代码重构

配置项目基本环境

一. 接口获取

我们可以通过Charles等工具抓包来获取我们想做的App的接口,然后通过解析将每个接口的数据解析出来。也可以去知乎中有趣的 API 接口推荐找找看。

二. 项目图片获取方式

图片的获取非常简单,我们只要将iTunes中的项目拖到桌面,然后改后缀名为zip,然后在解压就可以了,更简单暴力的可以使用iOS-Images-Extractor运行后直接将项目拖进去,就会自动解压图片。

三. 配置基本环境

创建好项目之后,之后要做的就是配置项目基本信息,首先在info.plist中设置一些基本信息,这里挑选几个比较重要的

《六天完成一个简单iOS App - 第一天》 info信息

其中Bundle name是应用的名称,默认与项目名称相同,可以更改。
项目使用代码,storyboard,和xib结合完成,但是框架的搭建不建议使用storyboard,因为框架的搭建往往页面比较多,多个页面挤在storyboard中实在难受,并且难找。所以框架的搭建就使用代码了。
启动图片的设置在LaunchScreen.storyboard中,当然也可以在Assets.xcassets中直接拖入启动图片,但是需要在General中设置

《六天完成一个简单iOS App - 第一天》 General
《六天完成一个简单iOS App - 第一天》 Migrate

然后我们就会发现在Assets.xcassets中除了AppIcon文件夹还多了Brand Assets文件夹,将启动图片直接拖到Brand Assets中就可以了。AppIcon中放应用图标。
关于图片素材,个人习惯在项目开始前就将图片全部放到Assets.xcassets中,这样使用的时候方便去找。也可以再用到的时候在将使用到的图片素材拖入到Assets.xcassets中,防止一下拖入过多图片素材,不好找。

应用名称,应用图片,应用启动图片设置好之后,需要根据项目分出模块,观察项目发现由5个模块组成,精华,新帖,发布,关注,和我,那么我们将每个模块的代码放在一起,并在根据MVC原则将每个模块的代码细分为3部分。如图

《六天完成一个简单iOS App - 第一天》 模块划分

注意要在文件show in finder 中创建文件,在项目中直接新建的文件夹并不是真实存在的,模块的区分有利于我们对项目模块的理解,更加快捷方便的找到要找的模块,开发也更简单明了

搭建框架

一. 框架结构

框架的搭建使用经典的UITabBarController -> UINavigationController -> UIViewController结构。如图

《六天完成一个简单iOS App - 第一天》 框架基本结构

UITabBarController 中添加五个UINavigationController,UINavigationController的子控制器来显示内容,管理自己的NavigationBar。

二. 解决实际问题

1. UITabBarItem自动将图片文字渲染成蓝色

《六天完成一个简单iOS App - 第一天》 图片文字被自动渲染成蓝色

解决方法:解决图片渲染成蓝色
方法一:

// 产生一张不会进行自动渲染的图片
UIImage *selectedImage = [tempImage imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];

方法二:
直接在Assets.xcassets中选中图片设置即可

《六天完成一个简单iOS App - 第一天》 Paste_Image.png

文字被渲染成蓝色,我们可以通过富文本来解决。

   /* 文字属性 */
   //普通状态下的文字属性
    NSMutableDictionary *normalAttrs = [NSMutableDictionary dictionary];
    normalAttrs[NSFontAttributeName] = [UIFont systemFontOfSize:14];
    normalAttrs[NSForegroundColorAttributeName] = [UIColor grayColor];
    // 选中状态下的文字属性
    NSMutableDictionary *selectedAttrs = [NSMutableDictionary dictionary];
    selectedAttrs[NSForegroundColorAttributeName] = [UIColor darkGrayColor];

    // 设置tabBarItem字体
    [vc0.tabBarItem setTitleTextAttributes:normalAttrs forState:UIControlStateNormal];
    [vc0.tabBarItem setTitleTextAttributes:selectedAttrs forState:UIControlStateSelected];

多个tabBarItem每个都需要设置一遍同样的内容,tabBarItem提供了统一设置的方法,我们可以用appearance属性来对所有的tabBarItem进行统一设置

/**** 设置所有UITabBarItem的文字属性 ****/
// 这里对item进行设置,即相当于对所有item进行统一设置
UITabBarItem *item = [UITabBarItem appearance];

appearance的使用注意:方法或者属性后面必须有UI_APPEARANCE_SELECTOR才可以获得appearance属性进行统一设置,否则则不可以使用appearance属性。例如

《六天完成一个简单iOS App - 第一天》 UI_APPEARANCE_SELECTOR

2. UITabBar 中间添加按钮的实现

我们知道中间加号按钮是没有标题的,即使我们将标题设置为空,还有有标题的label站位,所以UITabBarItem是不能实现了,那么我们只能将一个button覆盖在中间这块区域上。
方法一:添加站位控制器,我们可以在中间的位置上添加一个空的站位控制器,然后将button覆盖到UITabBar中间,这样做简单方便,但是创建了一个Controller和一个UITabBarItem没有别的用处只是用来站位,虽然并不会消耗很多空间,但是总是觉得十分别扭。

《六天完成一个简单iOS App - 第一天》 中间button覆盖在原有UITabBarItem上

方法二:自定义tabbar重写layoutsubViews方法
为了避免第一种方法产生站位Controller和UITabBarItem,我们自定义一个UItabbar,重写layoutsubViews尝试我们自己控制TabBarItem的位置,实现方法很简单,将UITabBar平均分为5段,将中间空出,其他四个TabBarItem设置完frame之后,懒加载button添加到中间位置。并实现其点击方法
layoutSubviews方法。

- (void)layoutSubviews
{
    [super layoutSubviews];
    /**** 设置所有UITabBarButton的frame ****/
    // 按钮的尺寸
    CGFloat buttonW = self.frame.size.width / 5;
    CGFloat buttonH = self.frame.size.height;
    CGFloat buttonY = 0;
    // 按钮索引
    int buttonIndex = 0;
    for (UIView *subview in self.subviews) {
        // 过滤掉非UITabBarButton
        // if (![@"UITabBarButton" isEqualToString:NSStringFromClass(subview.class)]) continue;
        if (subview.class != NSClassFromString(@"UITabBarButton")) continue;
        // 设置frame
        CGFloat buttonX = buttonIndex * buttonW;
        if (buttonIndex >= 2) { // 右边的2个UITabBarButton
            buttonX += buttonW;
        }
        subview.frame = CGRectMake(buttonX, buttonY, buttonW, buttonH);
        // 增加索引
        buttonIndex++;
    }
    /**** 设置中间的发布按钮的frame ****/
    self.publishButton.frame = CGRectMake(0, 0, buttonW, buttonH);
    self.publishButton.center = CGPointMake(self.frame.size.width * 0.5, self.frame.size.height * 0.5);
}
3. 实现UINavigationController 返回按钮统一设置

方法一:创建基类,其他继承基类,自动有这个按钮类型
创建一个UINavigationController基类,设置好统一的返回按钮,然后让其他导航栏控制器继承于他,这样可以达到返回按钮统一,但是这样做有一个局限性,UINavigationController的子控制器是固定的,例如UIViewController,如果我们需要使用UITableViewControlller则需要自己创建tableView。比较麻烦

方法二:自定义UINavigationController 重写pushViewController方法
重写pushViewController方法,判断NavigationController子控制器的个数,如果不是第一个push进来的控制器,则添加左边返回按钮。
注意:NavigationController的根控制器也是push进来的,所以需要判断是否是根控制器

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (self.childViewControllers.count > 0) { // 不是第一个push进来的 左上角加上返回键
      /*
      // 返回button初始化以及设置  
      */
     // 将button放置在leftBarButtonItem
    viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithCustomView:button];
    }
    [super pushViewController:viewController animated:animated];
}
4. pop右划手势失效的问题

当我们重写posh方法后,发现pop右划返回的手势失效,我们猜想是系统的返回按钮做了一些事情,而我们自己的button没有实现,解决办法,遵循代理,并实现代理方法

self.interactivePopGestureRecognizer.delegate = self; 
// 实现代理方法 
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
   // 判断如果不是根控制器 才需要pop返回手势
    return self.childViewControllers.count > 1;
}

三. 代码重构与优化

1. UINavigationControlller 设置左右UIbarbuttonitem代码的抽取

我们发现每一个UINavigationControlller根控制器中都需要写一大段相同的代码来设置UIbarbuttonite,那么我们写一个UIbarbuttonitem的分类抽取一个方法来简化代码。

@implementation UIBarButtonItem (CLExtension)
+(instancetype)itemWithImage:(NSString *)image HeightImage:(NSString *)heightImage Target:(id)target action:(SEL)action
{
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setImage:[UIImage imageNamed:image] forState:UIControlStateNormal];
    [button setImage:[UIImage imageNamed:heightImage] forState:UIControlStateHighlighted];
    [button sizeToFit];
    [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    return [[UIBarButtonItem alloc]initWithCustomView:button];
}

这样我们在根控制器中设置UIbarbuttonitem一句话就搞定了

// 设置左边按钮button
self.navigationItem.leftBarButtonItem = [UIBarButtonItem itemWithImage:@"MainTagSubIcon" HeightImage:@"MainTagSubIconClick" Target:self action:@selector(leftBtnClick)];
2. uiview关于frame的分类

当我们在设置控件的宽高以及位置的时候需要设置self.frame.size.height;代码很长,那么我们可以写一个UIView的分类,直接就可以通过self.height来设置其高度。
UIView+CLExtension.h

@interface UIView (CLExtension)

@property(nonatomic,assign)CGFloat cl_width;
@property(nonatomic,assign)CGFloat cl_height;
@property(nonatomic,assign)CGFloat cl_x;
@property(nonatomic,assign)CGFloat cl_y;
@property(nonatomic,assign)CGFloat cl_centerX;
@property(nonatomic,assign)CGFloat cl_centerY;

@end

UIView+CLExtension.m

@implementation UIView (CLExtension)  
-(void)setCl_width:(CGFloat)cl_width
{
    CGRect frame = self.frame;
    frame.size.width = cl_width;
    self.frame = frame;
}
-(CGFloat)cl_width
{
    return self.frame.size.width;
}
-(void)setCl_height:(CGFloat)cl_height
{
    CGRect frame = self.frame;
    frame.size.height = cl_height;
    self.frame = frame;
}
-(CGFloat)cl_height
{
    return self.frame.size.height;
}
-(void)setCl_x:(CGFloat)cl_x
{
    CGRect frame = self.frame;
    frame.origin.x = cl_x;
    self.frame = frame;
}
-(CGFloat)cl_x
{
    return self.frame.origin.x;
}
-(void)setCl_y:(CGFloat)cl_y
{
    CGRect frame = self.frame;
    frame.origin.y = cl_y;
    self.frame = frame;
}
-(CGFloat)cl_y
{
    return self.frame.origin.y;
}
-(void)setCl_centerX:(CGFloat)cl_centerX
{
    CGPoint center = self.center;
    center.x = cl_centerX;
    self.center = center;
}
-(CGFloat)cl_centerX
{
    return self.center.x;
}
-(void)setCl_centerY:(CGFloat)cl_centerY
{
    CGPoint center = self.center;
    center.y = cl_centerY;
    self.center = center;
}
-(CGFloat)cl_centerY
{
    return self.center.y;
}
@end

这样我们在设置宽高,x,y的时候就可以直接通过height,width,x,y来设置了,建议在这些属性前面加上前缀,防止和其他文件属性冲突

3. PCH文件

所有文件都用的到的东西,例如颜色设置的宏,分类,修改的输出日志等等,我们可以写到PCH文件中,保证所有的文件都可以用,而不用频繁的每个类中都引入

#ifdef __OBJC__
/** 在这之间的 在OC文件中会引用 防止OC与C混编的时候引起错误 **/
#import "UIView+CLExtension.h"
#import "UIBarButtonItem+CLExtension.h"
#define CLLogfunc CLLog(@"%s",__func__);
/******** 输出日志 ********/
#ifdef DEBUG
#define CLLog(...) NSLog(__VA_ARGS__)
#else
#define CLLog(...)
#endif
/******** 日志输出 ********/
/******** 关于颜色的宏********/
// 带透明度的颜色
#define CLColorA(r,g,b,a) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:(a)]
// 不带透明度的颜色
#define CLColor(r,g,b) CLColorA(r,g,b,1);
// 随机颜色
#define CLRandomColor CLColor(arc4random_uniform(255),arc4random_uniform(255),arc4random_uniform(255))
// 灰色
#define CLCommonColor(v) CLColor(v,v,v)

/******** 关于颜色的宏********/
#endif

这是目前的pch文件内容,如果项目报错找不到pch文件,那是因为pch文件路径可能换了,在BuildSettings 搜索 prefix header ,直接将pch拖入其中自动生成路径即可。

四.疑惑

分类中能不能添加属性呢?之前uiview关于frame的分类不就是给分类添加了许多属性吗?
注意:
1. 分类原则是不可以添加属性,只能添加方法,我们之前给 UIView增加了一些属性,而且为其实现了相应的 getter和 setter方法。而这些方法实际上访问的是本类的frame属性,其实frame,bounds也是定义在分类里边的

《六天完成一个简单iOS App - 第一天》
frame,bounds也是定义在分类里

可以看到,这种定义在分类里的属性,实际上是实现了相应的方法,并在方法里边通过访问其它属性来达到目的。这通常用来简化某些操作。

2. 在分类中可以写@property添加属性,但是不会自动生成私有属性,也不会生成set,get方法的实现,只会生成set,get的声明,需要我们自己去实现。

3. 为什么不直接设置frame而需要一个中间量来设置呢?
因为在分类的方法实现中不可以直接访问本类的私有属性,但是可以调用本类的set,get方法。

4. 当分类中有和本类中同名的方法的时候,优先调用分类的方法,如果多个分类中有相同的方法,优先调用最后编译的分类。

5. 分类可以通过Runtime运行时给分类添加属性,对象的属性其实是让属性与对象产生关联,如果想动态添加属性,其实是动态产生一种关系,让对象的某个属性可以关联到另外一块内存地址。

五. 总结

今天的任务已经完成,我们完成了环境的配置,主框架的搭建,以及对一些繁琐重复的代码做了简单整理。第一天效果如下

《六天完成一个简单iOS App - 第一天》 第一天效果

文中如果有不对的地方欢迎指出。我是xx_cc,一只长大很久但还没有二够的家伙。

    原文作者:xx_cc
    原文地址: https://www.jianshu.com/p/006579dba0b2
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞