Swift Runtime ?

From: https://cloud.tencent.com/developer/article/1037800

你肯定也想过

 在OC中相信每一个iOS开发都知道Runtime, 现在Swift也更新到4.0版本了,要是你也学习过Swift的话你可能也会想过这样一个问题,OC大家都知道是有动态性的,你能通过Runtime 的API获取你想要的属性方法等等,那Swift呢?是不是也和OC一样呢? 

 这个问题在我看Swift的时候也有想过,带着这个问题就总结出了今天这篇文章。 

 先说说这个Runtime,在自己之前的文章中有总结过关于OC的Runtime以及它API的一些基本的方法和在项目中具体的使用,在这里再大概的提一下Runtime的基本的概念: 

 RunTime简称运行时。OC就是`运行时机制`,也就是在程序运行时候的一些机制,其中最主要的是消息机制。对于我们熟悉的C语言,`函数的调用在编译的时候会决定调用哪个函数`。但对于OC的函数,属于`动态调用过程`,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。 

也就有了下面这两点结论: 

1、在编译阶段,OC可以`调用任何函数`,即使这个函数并未实现,只要声明过就不会报错。 

2、在编译阶段,C语言调用`未实现的函数`就会报错。 

看看Swift Runtime

  先不直接丢出结论,从下面的简单的代码入手,一步步的找出我们想要的答案: 

  我们定义一个纯Swift的类  TestASwiftClass ,代码如下: 

class TestASwiftClass{

        var aBoll :Bool = true
        var aInt : Int = 0
        func testReturnVoidWithaId(aId : UIView) {

            print("TestASwiftClass.testReturnVoidWithaId")
        }
}


   代码也是很简单,我们定义了两个变量一个方法,下面我们再写一个继承自 UIViewController 的 ViewController ,代码如下: 

class ViewController: UIViewController{

    let testStringOne  = "testStringOne"
    let testStringTwo  = "testStringTwo"
    let testStringThr  = "testStringThr"
    var count:UInt32 = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        let  SwiftClass = TestASwiftClass()
        let  proList = class_copyPropertyList(object_getClass(SwiftClass),&count)
        for  i in 0..<numericCast(count) {

                let property = property_getName(proList?[i]);
                print("属性成员属性:%@",String.init(utf8String: property!) ?? "没有找到你要的属性");
        }
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}


  上面的代码也很简单,我们在ViewController中添加了一些变量,然后通过Runtime的方法尝试着先来获取一下我们最上面定义的纯Swift类TestASwiftClass的属性,你运行上面代码你就会发现: 

  什么都没有!!!为什么?? 

  下面我们先给出答案,用它来解释一下为什么我们通过上面Runtime的API没有获取到任何东西,然后再接着用OC来证明一下我们说的结论: 

  C 语言是在函数编译的时候决定调用那个函数,在编译阶段,C要是调用了没有实现的函数就会报错。 

  OC 的函数是属于动态调用,在编译的时候是不能决定真正去调用那个函数的,只有在运行的时候才能决定去调用哪一个函数 ,在编译阶段,OC可以调用任何的函数,即使这个函数没有实现,只要声明过也就不会报错。 

Swift 纯Swift类的函数的调用已经不是OC的运行时发送消息,和C类似,在编译阶段就确定了调用哪一个函数,所以纯Swift的类我们是没办法通过运行时去获取到它的属性和方法的。

Swift 对于继承自OC的类,为了兼容OC,凡是继承与OC的都是保留了它的特性的,所以可以使用Runtime获取到它的属性和方法等等其他我们在OC中获得的东西。

  针对上面给出的结论,我们看看Swift对于继承自OC的类是不是保留了OC所有的特性呢?再看下面代码,只是做一个简单的修改,把通过object_getClass方法获取的对象写成self: 

let  proList = class_copyPropertyList(object_getClass(self),&count)
for  i in 0..<numericCast(count) {

       let property = property_getName(proList?[i]);
       print("属性成员属性:%@",String.init(utf8String: property!) ?? "没有找到你要的属性");
}

通过上面的方法我们获取到的日志如下:

可以看到我们获取到了我们在ViewController中定义的变量。这样也就证明了的确是上面答案说的那样。

那这样就又衍生出一个问题

  那Swiftw就没办法利用Runtime了吗? 

  想一想,要是真的Swift没办法利用Runtime,那是一件得多让人失望的事!答案也肯定是否定的,我们还是能让Swift用Runtime的。看下面的代码: 

class TestASwiftClass{

        dynamic  var aBoll :Bool = true
        var aInt : Int = 0
        dynamic func testReturnVoidWithaId(aId : UIView) {

            print("TestASwiftClass.testReturnVoidWithaId")
        }
}


  上面还是我们定义的  TestASwiftClass 类,不同的地方不知道大家注意到没? 

  嗯,我们利用了dynamic(英文单词动态的意思)关键字,在第一个变量和方法的定义前面我们添加了这个关键字,那添加了这个关键字之后又什么变化呢?我们再通过最开始我们获取纯Swift类的代码获取一下试试,看结果! 

 结果: 

          可以看到这里是获取到了变量了的。(这里是获取属性没有写获取方法代码所以是值拿到变量没有拿到方法) 

aBoll 这个变量前面是添加了dynamic关键字的,我们获取到了。在aInt这个变量前面我们是没有添加的,所以可以看到我们是没有获取到这个变量的,那关键的就是我们要理解:dynamic 关键字的含义:

首先有 @objc 这个关键字,它是用来将Swift的API导出来给 Object-C 和 Runtime 使用的,如果你类继承自OC的类,这个标识符就会被自动加进去,加了这标识符的属性、方法无法保证都会被运行时调用,因为Swift会做静态优化,想要完全被声明成动态调用,必须使用 dynamic 标识符修饰,当然添加了 dynamic 的时候,它会自己在加上@objc这个标识符。

这样我们就理解了dynamic这个关键字,知道了它的作用,那我们接下来就是尝试着多使用一下 **Swift Runtime。**

Swift Runtime

  上面解释了这个关键字之后关于Swift的Runtime方面的只是就有了一个基本的了解了,下面的这些代码就像我们整理OC Runtime 那样也整理出来: 

  1、获取方法: 

let  mthList = class_copyMethodList(object_getClass(SwiftClass),&count)
for  index in 0..<numericCast(count) {

     let method = method_getName(mthList?[index])
     print("属性成员方法:%@",String.init(NSStringFromSelector(method!)) ?? "没有找到你要的方法")
}


  2、属性成员变量 

 let IvarList = class_copyIvarList(object_getClass(SwiftClass),&count)
 for index in 0..<numericCast(count) {

     let Ivar = ivar_getName(IvarList?[index])
     print("属性成员变量:%@",String.init(utf8String: Ivar!) ?? "没有找到你想要的成员变量")
}


  3、协议列表  

let protocalList = class_copyProtocolList(object_getClass(self),&count)
for index in 0..<numericCast(count) {

     let protocal = protocol_getName(protocalList?[index])
     print("协议:%@",String.init(utf8String: protocal!) ?? "没有找到你想要的协议")
}


  4、方法交换  

       这个就是Runtime的一个重点了,仔细说一说。 

       OC的动态性最常用的其实就是方法的替换,将某个类的方法替换成自己定义的类,从而达到Hook的作用。(以前面试有人问过OC怎样Hook一个消息,那时候太懵懂,不知道怎么说!不知道大家有没有遇到过?) 

       对于纯粹的Swift类,由于前面的测试你知道无法拿到类的属性饭方法等,也就没办法进行方法的替换,但是对于继承自NSObject的类,由于集成了OC的所有特性,所以是可以利用Runtime的属性来进行方法替换,记得我们前面说的dynamic关键字。     

func ChangeMethod() -> Void {

        // 获取交换之前的方法
        let originaMethodC = class_getInstanceMethod(object_getClass(self), #selector(self.originaMethod))
        // 获取交换之后的方法
        let swizzeMethodC  = class_getInstanceMethod(object_getClass(self), #selector(self.swizzeMethod))

         //替换类中已有方法的实现,如果该方法不存在添加该方法
         //获取方法的Type字符串(包含参数类型和返回值类型)
         //class_replaceMethod(object_getClass(self), #selector(self.swizzeMethod), method_getImplementation(originaMethodC), method_getTypeEncoding(originaMethodC))

         print("你交换两个方法的实现")
         method_exchangeImplementations(originaMethodC, swizzeMethodC)
    }

    dynamic func originaMethod() -> Void {

         print("我是交换之前的方法")
    }

    dynamic func swizzeMethod() -> Void {

         print("我是交换之后的方法")
    }


  5、关联属性 

       说上面的方法Hook比较重要的话,这个关联属性也是比较重要的,在前面我总结OC的Runtime的时候在方法的添加这里专门有提过一个Demo,我们把这个Demo重新整理一下,导航的渐变就是利用Runtime给导航添加属性来实现的。 

extension UINavigationBar {

    var navigationGradualChangeBackgroundView:UIView?{

        get{
            return objc_getAssociatedObject(self, &self.navigationGradualChangeBackgroundView) as? UIView;
        }
        set{

            objc_setAssociatedObject(self, &self.navigationGradualChangeBackgroundView, navigationGradualChangeBackgroundView, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
    }

    func setNavigationBackgroundColor (backgroundColor: UIColor) -> Void {

        if (self.navigationGradualChangeBackgroundView == nil) {

            self.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
            self.navigationGradualChangeBackgroundView = UIView.init(frame: CGRect.init(x: 0, y: -20, width: SCREENWIDTH, height: self.bounds.size.height + 20))
            self.navigationGradualChangeBackgroundView!.isUserInteractionEnabled = false
            self.insertSubview(self.navigationGradualChangeBackgroundView!, at: 0)
        }

       self.navigationGradualChangeBackgroundView!.backgroundColor = backgroundColor
    }

    func removeNavigationBackgroundColor() -> Void {

       self.setBackgroundImage(nil, for: UIBarMetrics.default)
       self.navigationGradualChangeBackgroundView!.removeFromSuperview()
       self.navigationGradualChangeBackgroundView = nil
    }
}

1、上面是给UINavigationBar添加扩展来写的,注意Swift的写法和OC的区别。

2、在应用这点知识的时候,可以直接在ScrollView滚动的代理方法里面通过滚动距离的改变透明度生成你需要的Color,然后直接就在它的代理方法中调用setNavigationBackgroundColor方法即可。

看个其他的例子


在整理资料的时候,发现了一篇文章: [iOS---防止UIButton重复点击的三种实现方式][1]
在最后面说道的利用Runtime的方法解决的时候,最后是这样一段代码: 

说明:

可以看到最后是直接把自己定义的方法和系统的方法交换了,重点就是自己方法里面的实现! 

可以看到在自己定义的方法前面加了时间判断,最后还是调用了方法本身!这样就有了一个问题。你用自己的方法代替了系统的方法,加入了自己的一些东西,最有没有再去调用系统的方法?你不知道系统方法实现的具体内容却直接用自己的方法规代替了,那系统按钮的功能肯定是受到影响的!大家应该能理解我说的意思。那我们就得记得一点: 

切记: 我们使用 Method Swizzling(方法交换) 的目的通常都是为了给程序增加功能,而不是完全地替换某个功能,所以我们一般都需要在自定义的实现中调用原始的实现。

针对这一点特别说明一下,怎么修改的其实原文下面的同学也有给出了答案的,具体的内容建议大家看看这篇文章,应该会有收获! 

Objective-C Method Swizzling 的最佳实践

appledoc导出iOS代码文档的使用和问题详解

From: https://www.cnblogs.com/xiaoyouPrince/p/6591734.html

1. 简单说一下背景和自己感受

背景:
项目好像突然黄了,公司让详细写项目代码的注释并且导出文档,弄完之后就要封版。

说实话:听到这个消息之后心里还是很担心的,因为我知道公司不可能养闲人,我手上的项目本来年后就没有什么起色,加上突然来了这样的一个‘噩耗’,顿时就知道后面肯定没好事

我知道公司不会养闲人,所以在这几天项目闲下来的日子里,忐忑过,也想到了项目可能面临的种种,当然也包括自己所可能受到的种种影响。但是毕竟我们只是听上面安排的一线开发人员,做不了项目大方向的主,只能服从安排,所以不管心情如何还是要把工作完成,只是想记录一下心情和工作中遇到的问题。

2. Xcode代码导出注释实践

Xcode导出代码文档的方式一共有三种,Doxygen, headdoc 和 appledoc 。以下是三者官网链接:

3. 介绍appledoc

由于我查到的资料显示appledoc最受欢迎,并且生成的文档风格和apple一致,非常满足我的需求,故我使用的也是appledoc,有兴趣的同学可以自行进入官网或网页自行查询。

appledoc的几点优点:

  • 它默认生成的文档风格和苹果的官方文档是一致的,无需额外配置。
  • appledoc 就是用 objective-c 生成的,必要的时候调试和改动也比较方便。
  • 可以生成 docset,并且集成到 Xcode 中。这一点是很赞的,相当于在源码中按住 option 再单击就可以调出相应方法的帮助。

4.安装appledoc

安装appledoc步骤非常简单,只需两步:

  1. 终端中clone项目到本地
  2. 运行安装脚本

    git clone git://github.com/tomaz/appledoc.git
    cd appledoc
    sudo sh install-appledoc.sh

验证是否安装成功

appledoc --version

我这边版本如下:

5.使用

appledoc的使用非常简单,2步即可:

  1. 在终端中进入要导出文档的目录下
  2. 输入如下命令

    appledoc –project-name “XYBannerView” –project-company “xiaoyouPrince” ./

注意:

  1. XYBannerView:是你自己的项目名(随便写也可以)
  2. xiaoyouPrince: 是项目对应的公司名(随便写也可以)
  3. ./ 导出到当前路径的一个参数,前面要有空格!

appledoc 会扫描当前路径下的所有文件,然后生成好文档放到 doc 目录下。你也可以用 appledoc –help 查看所有可用的参数。

基本上使用起来还是比较方便的,详细的信息可以查看官方的文档:http://gentlebytes.com/appledoc/

6.我遇到的问题:Command /bin/sh failed with exit code 250

报错信息:

Command /bin/sh failed with exit code 250

如图:(遇到的同学肯定印象深刻,并且还很难找到答案,这也是我为什么想写这个文章的原因)

我从网上找到答案的主要意思(有很多是相关的,具体的答案真没找到):

  1. 和 enum 和 NS_ENUM 类型的支持有关(这个在作者的更新中已经修改好像)
  2. 和 Pods 中的三方库等资源有关。由于项目大很多东西是不支持的
  3. 警告一般是项目中的注释,缺少参数或格式问题(三方库中尤其明显)

7. 解决方法

说了这么多,下面说一下解决方法:

由于三方库和一些资源有问题,那就跳过三方(Pods和一些手动导入的),进入下一层目录执行命令

appledoc --project-name "XYBannerView" --project-company "xiaoyouPrince" ./

这就对于项目中文件结构的分层很重要,我们自己的代码和项目中引用的三方代码需要分开

Statusbar状态栏样式

From: http://www.jianshu.com/p/ae47fdbf28fd

很多时候我们需要修改页面的Statusbar的样式,这里的样式是固定的,系统仅提供了两种:

typedef NS_ENUM(NSInteger, UIStatusBarStyle) {
    UIStatusBarStyleDefault                                     = 0, // Dark content, for use on light backgrounds
    UIStatusBarStyleLightContent     NS_ENUM_AVAILABLE_IOS(7_0) = 1, // Light content, for use on dark backgrounds

    UIStatusBarStyleBlackTranslucent NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 1,
    UIStatusBarStyleBlackOpaque      NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 2,
} __TVOS_PROHIBITED;

这里是对UIStatusBarStyle的枚举,虽然有四个,但是后两个是在iOS7.0之后废弃的,使用的时候会有警告,所以,实际上只有两种

UIStatusBarStyleLightContent

UIStatusBarStyleDefault

下面,我们就来看一看,怎么在项目中根据我们的需要进行选择;
与状态栏相关的方法主要有以下几个:

- (UIStatusBarStyle)preferredStatusBarStyle NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // Defaults to UIStatusBarStyleDefault
- (BOOL)prefersStatusBarHidden NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // Defaults to NO
// Override to return the type of animation that should be used for status bar changes for this view controller. This currently only affects changes to prefersStatusBarHidden.
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // Defaults to UIStatusBarAnimationFade

第一个是状态栏的样式,第二个是否隐藏状态栏,第三个是动画方式;

1. 不含有导航

如果工程中没有使用导航,我们直接在ViewController中重写上面的方法就可以修改状态栏的样式了:

- (UIStatusBarStyle)preferredStatusBarStyle {

    return UIStatusBarStyleLightContent;
}

这样可以根据我们的需求在不同的页面进行修改,隐藏,显示状态栏;
但是,当我们使用了导航后,好像没效果了?

2. 含有导航

如果使用了导航,我们再去重写上面的方法,就失去作用了,查了写资料,发现:

UINavigationController不会将 preferredStatusBarStyle方法调用转给它的子视图,而是由它自己管理状态,而且它也应该那样做.因为UINavigationController 包含了它自己的状态栏;
因此就算 UINavigationController中的viewController 实现了 preferredStatusBarStyle方法 也不会调用

那么,我们怎么在使用导航的时候修改status bar的状态呢?

它是基于它的 UINavigationBar.barStyle属性.默认(UIBarStyleDefault)的是黑色文本的状态栏 而 UIBarStyleBlack是设置为白色文本的状态栏;

也就是说,如果viewController是在导航中的,想要改变状态栏文本颜色,那么需要通过代码:

self.navigationController.navigationBar.barStyle = UIBarStyleBlack;//设置为白色
self.navigationController.navigationBar.barStyle = UIBarStyleDefault;//设置为黑色

很多时候,我们虽然使用了导航,但是导航条都是我们自定义的,这个时候,我们隐藏了系统的导航:

self.navigationController.navigationBarHidden = YES;//隐藏系统导航

这时,我们就可以在ViewController里重写preferredStatusBarStyle方法,来设置状态栏的样式了,方式同1;

PS:如果我们设置了不同页面的状态栏样式,但是在进入到相应页面时,状态栏并没有按我们的预期发生变化,可尝试调用下面的方式试试:

- (void)setNeedsStatusBarAppearanceUpdate

3. 设置全局的状态栏

如果我们想更改所有的状态栏为同样状态,可以在设置中进行修改:
在info.plist中添加如下字段:

<key>View controller-based status bar appearance<key>
<value>NO<value>

类型为Boolean,设置为NO;

设置为NO后,就不能使用代码控制状态栏的样式了;如果想用代码控制,就在这里设置为YES,当然,这样在下面的设置就无效了 (注:UIApplication.shared.statusBarStyle = UIStatusBarStyle.default 还是有效的)

然后在项目的额General–>Deployment Info–>Status Bar Style中选择需要的样式:

这样,项目中所有的状态栏就都变为白色的了;

4. 总结

上面的第三种方法虽然使用简单,但是不够灵活,不能使用代码在程序中动态调整状态栏的设置,如果,APP中状态栏统一,可以使用此方法,简单,高效;如果状态栏的设置需要动态调整,就只能在方法1,2中选择了!!!

个人总结:
全局
info 中加入

View controller-based status bar appearance

NO

然后在项目的额General–>Deployment Info–>Status Bar Style中选择需要的样式:

分文件 View controller-based status bar appearance

YES
General中配置无效
重写

1
2
3
4
5
UIViewController
/// 根据info 是否起效
override var preferredStatusBarStyle: UIStatusBarStyle{
return UIStatusBarStyle.lightContent
}

http://www.jianshu.com/p/ee1c9c91a477

1
2
3
如果将View controller-based status bar appearance设置为NO,不在UIApplication管理的情况下,所有控制器view上状态栏的只受启动图导航栏颜色设置的影响(保持一样),其它任何设置都不起作用。
如果将View controller-based status bar appearance设置为YES。所有控制器View上状态栏首先受navigationBar的barStyle影响,为UIBarStyleBlack时状态栏字体颜色为白色,UIBarStyleDefault时状态栏前景部分颜色为黑色。
只有将View controller-based status bar appearance设置为YES的情况下,才能单独修改某个控制器View的状态栏前景部分颜色,更改方法见34部分。

—- kvc 来使用 ——

1
2
3
4
5
6
7
- (UIView *) statusBar{
if (_statusBar == nil){
_statusBar = [[[UIApplication sharedApplication] valueForKey:@"statusBarWindow"] valueForKey:@"statusBar"];
}
return _statusBar;
}
// 设置他的隐藏和显示

iOS 如何优雅地 hook 系统的 delegate 方法

From: http://www.daizi.me/2017/11/01/iOS%20%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E5%9C%B0%20hook%20%E7%B3%BB%E7%BB%9F%E7%9A%84%20delegate%20%E6%96%B9%E6%B3%95%EF%BC%9F/

在 iOS 开发中我们经常需要去 hook 系统方法,来满足一些特定的应用场景。

比如使用 Swizzling 来实现一些 AOP 的日志功能,比较常见的例子是 hook UIViewControllerviewDidLoad ,动态为其插入日志。

这当然是一个很经典的例子,能让开发者迅速了解这个知识点。不过正如现在的娱乐圈,diss 天 diss 地,如果我们也想 hook 天,hook 地,顺便 hook 一下系统的 delegate 方法,该怎么做呢?

所以就进入今天的主题:如何优雅地 hook 系统的 delegate 方法?

hook 系统类的实例方法

首先,我们回想一下 hook UIViewControllerviewDidLoad 方法,我们需要使用 category,为什么需要 category 呢?因为在 category 里面才能在不入侵源码的情况下,拿到实例方法 viewDidLoad ,并实现替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
@implementation UIViewController (swizzling)
+ (void)load {
Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
method_exchangeImplementations(fromMethod, toMethod);
}
- (void)swizzlingViewDidLoad {
NSString *str = [NSString stringWithFormat:@"%@", self.class];
NSLog(@"日志打点 : %@", self.class);
[self swizzlingViewDidLoad];
}
@end

这个例子里面,有一个注意点,通常我们创建 ViewController 都是继承于 UIViewController,因此如果想要使用这个日志打点功能,在自定义 ViewController 里面需要调用 [super viewDidLoad]。所以一定需要明白,这个例子是替换 UIViewControllerviewDidLoad,而不是全部子类的 viewDidLoad

1
2
3
4
5
6
7
8
9
10
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
@end

hook webView 的 delegate 方法

这个需求最初是项目中需要统计所有 webView 相关的数据,因此需要 hook webView 的 delegate 方法,今天也是以此为例,主要是 hook UIWebViewWKWebView类似)。

首先,我们需要明白,调用 delegate 的对象,是继承了 UIWebViewDelegate 协议的对象,因此如果要 hook delegate 方法,我们先要找到这个对象。

因此我们需要 hook [UIWebView setDelegate:delegate] 方法,拿到 delegate 对象,才能动态地替换该方法。这里 swizzling 上场:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@implementation UIWebView(delegate)
+(void)load{
Method originalMethod = class_getInstanceMethod([UIWebView class], @selector(setDelegate:));
Method swizzledMethod = class_getInstanceMethod([UIWebView class], @selector(hook_setDelegate:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)dz_setDelegate:(id<UIWebViewDelegate>)delegate{
[self dz_setDelegate:delegate];
}
@end

这里有个局限性,源码中需要调用 setDelegate: 方法,这样才会调用 dz_setDelegate:

接下来就是重点了,我们需要根据两种情况去动态地 hook delegate 方法,以 hook webViewDidFinishLoad: 为例:

  • delegate 对象实现了 webViewDidFinishLoad: 方法。则交换实现。
  • delegate 对象未实现了 webViewDidFinishLoad: 方法。则动态添加该 delegate 方法。

下面是 category 实现的完整代码,实现了以上两种情况下都能正确统计页面加载完成的数据:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

static void dz_exchangeMethod(Class originalClass, SEL originalSel, Class replacedClass, SEL replacedSel, SEL orginReplaceSel){

Method originalMethod = class_getInstanceMethod(originalClass, originalSel);

Method replacedMethod = class_getInstanceMethod(replacedClass, replacedSel);

if (!originalMethod) {

Method orginReplaceMethod = class_getInstanceMethod(replacedClass, orginReplaceSel);

BOOL didAddOriginMethod = class_addMethod(originalClass, originalSel, method_getImplementation(orginReplaceMethod), method_getTypeEncoding(orginReplaceMethod));

if (didAddOriginMethod) {

NSLog(@”did Add Origin Replace Method”);

}

return;

}

BOOL didAddMethod = class_addMethod(originalClass, replacedSel, method_getImplementation(replacedMethod), method_getTypeEncoding(replacedMethod));

if (didAddMethod) {

NSLog(@”class_addMethod_success –> (%@)”, NSStringFromSelector(replacedSel));

Method newMethod = class_getInstanceMethod(originalClass, replacedSel);

method_exchangeImplementations(originalMethod, newMethod);

}else{

NSLog(@”Already hook class –> (%@)”,NSStringFromClass(originalClass));

}

}

@implementation UIWebView(delegate)

+(void)load{

Method originalMethod = class_getInstanceMethod([UIWebView class], @selector(setDelegate:));

Method swizzledMethod = class_getInstanceMethod([UIWebView class], @selector(dz_setDelegate:));

method_exchangeImplementations(originalMethod, swizzledMethod);

}

  • (void)dz_setDelegate:(id)delegate{

[self dz_setDelegate:delegate];

[self exchangeUIWebViewDelegateMethod:delegate];

}

#pragma mark - hook webView delegate 方法

  • (void)exchangeUIWebViewDelegateMethod:(id)delegate{

dz_exchangeMethod([delegate class], @selector(webViewDidFinishLoad:), [self class], @selector(replace_webViewDidFinishLoad:),@selector(oriReplace_webViewDidFinishLoad:));

}

  • (void)oriReplace_webViewDidFinishLoad:(UIWebView *)webView{

NSLog(@”统计加载完成数据”);

}

  • (void)replace_webViewDidFinishLoad:(UIWebView *)webView

{

NSLog(@”统计加载完成数据”);

[self replace_webViewDidFinishLoad:webView];

}

@end

与 hook 实例方法不相同的地方是,交换的两个类以及方法都不是 [self class],在实现过程中:

  1. 判断 delegate 对象的 delegate 方法(originalMethod)是否为空,为空则用 class_addMethod 为 delegate 对象添加方法名为 (webViewDidFinishLoad:) ,方法实现为(oriReplace_webViewDidFinishLoad:)的动态方法。

  2. 若已实现,则说明该 delegate 对象实现了 webViewDidFinishLoad: 方法,此时不能简单地交换 originalMethodreplacedMethod,因为 replaceMethod 是属于 UIWebView 的实例方法,没有实现 delegate 协议,无法在 hook 之后调用原来的 delegate 方法:[self replace_webViewDidFinishLoad:webView];

因此,我们也需要将 replace_webViewDidFinishLoad: 方法动态添加到 delegate 对象中,并使用添加后的方法和源方法交换。

结语

以上,通过动态添加方法并替换的方式,可以在不入侵源码的情况下,优雅地 hook 系统的 delegate 方法。通过合理使用 runtime 期间几个方法的特性,使得 hook 系统未实现的 delegate 方法成为可能。

最后献上:github 源码地址

Extensions存储属性

From: https://grayluo.github.io/WeiFocusIo/cocoa-swift/2015/12/21/swiftpracticeextensionmd

在OC中我们使用Category提高的代码的规范,减少了冗余,在swift中虽然不再有Category了,但是有了类似功能的Extension,相比于Category而言,Extension没有名称,而且不能定义存储属性(当然我们可以使用runtime解决,稍后会讲到)。

Extensions可以扩展的内容包括:

  • 添加计算型属性和计算静态属性
  • 定义实例方法和类型方法
  • 提供新的构造器
  • 定义下标
  • 定义和使用新的嵌套类型
  • 使一个已有类型符合某个接口
  • 属性与方法:
extension SomeType {
    // 加到SomeType的新功能写到这里
}
  • 协议:
extension SomeType: SomeProtocol, AnotherProctocol {
    // 协议实现写到这里
}


//CustomCell   
extension CustomCell{
    var defaulHeight:Float{
        return 50;
    }
}
//test
let cell = CustomCell(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
let defaultHeight = cell.defaulHeight
print("default height:\(defaultHeight)")

虽然我们前面已经讲到了,默认的extension不支持存储属性,我们来试一下:

//CustomCell   
extension CustomCell{
    var company:String?
}

编译器直接就提示: Extensions may not contain stored properties

但是有时候我们也会有这种需要在Extension中添加存储属性的时候,比如我们使用别人的第三方库时,那我们有没有其它办法可以实现在Extension中添加存储属性呢,当默认的方法都实现不了的时候,在iOS开发中不论是OC还是Swift,我们都可以考虑一下Runtime。

//CustomCell 
extension CustomCell{
    private struct AssociatedKeys{
        static var name = "CustomCell_name"
        static var location = "CustomCell_location"
    }
    var name:String?{
        get{
            return objc_getAssociatedObject(self, &AssociatedKeys.name) as? String
        }
        set{
            if let newValue = newValue{
                objc_setAssociatedObject(self, &AssociatedKeys.name, newValue as String, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    var location:String?{
        get{
            return objc_getAssociatedObject(self, &AssociatedKeys.location) as? String
        }
        set{
            if let newValue = newValue{
                objc_setAssociatedObject(self, &AssociatedKeys.location, newValue as String, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
}

//test
let cell = CustomCell(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
cell.name = "Grey.Luo"
cell.location = "成都市高新区"
print("cell.name:\(cell.name),cell.location:\(cell.location)")

以上就是一个最基本的使用Runtime给Extension添加存储属性的示例,其中objc_getAssociatedObject与objc_setAssociatedObject中都会用到一个关联key,可以把其想作是KVC的那种思想,为了不对整个类和命名空间(即模块)导致污染,使用这种私有嵌套结构体的方式是业内大牛们推荐的方式,OK,我们再看一下runtime的文档:

 /** 
 * Sets an associated value for a given object using a given key and association policy.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * @param value The value to associate with the key key for object. Pass nil to clear an existing association.
 * @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
 * 
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
@available(iOS 3.1, *)
public func objc_setAssociatedObject(object: AnyObject!, _ key: UnsafePointer<Void>, _ value: AnyObject!, _ policy: objc_AssociationPolicy)

/** 
 * Returns the value associated with a given object for a given key.
 * 
 * @param object The source object for the association.
 * @param key The key for the association.
 * 
 * @return The value associated with the key \e key for \e object.
 * 
 * @see objc_setAssociatedObject
 */
@available(iOS 3.1, *)
public func objc_getAssociatedObject(object: AnyObject!, _ key: UnsafePointer<Void>) -> AnyObject!


//CustomCell
extension CustomCell{
    convenience init(defaultHeight:Float){
        self.init()
        print("convenience init...\(defaultHeight)")
    }

//test
let cell = CustomCell(defaultHeight: 50)


extension CustomCell{    
    func normalFun(param1:String,param2:Int){
        print("normalFun,\(param1),\(param2)")
    }
}

PS: 一旦某个扩展方法中要修改实例本身,则需要在方法前添加mutating,而且仅支持结构体与枚举的扩展,不支持类和协议,其实就是数据类型与类在系统中的存储机制不一样。 如:

extension Int{
    mutating func doubleIt(){
        self = self * 2
    }
}

以下方法则不行:

class CustomOb: NSObject {
    var customName:String?
    var customLocation:String?
    init(name:String, location:String) {
        customName = name
        customLocation = location
     }
}
extension CustomOb{
    mutating func modifyMyself(defaultHeight:Float){
        self = CustomOb(name: "Grey.Luo", location: "成都高新")
    }
} 

以上这样对CustomOb进行扩展,无法使用mutating进行实例修改,直接报错:

‘mutating’ isn’t valid on methods in classes or class-bound protocols

如果对Subscripts不太熟悉的同学,可以查看[Cocoa-Swift之Subscripts][1]

   [1]: 


//CustomOb
class CustomOb: NSObject {
    var customName:String?
    var customLocation:String?
    init(name:String, location:String) {
        customName = name
        customLocation = location
     }
}
extension CustomOb{
    subscript(index:Int) -> String{
        var rs:String? = nil
        if(index == 0){
            rs = customName
        }else if(index == 1){
            rs = customLocation
        }
        return rs!
    }
}
//test
let customOb = CustomOb(name: "Grey.Luo", location:"成都高新")
print("\(customOb[0]),\(customOb[1])")

我们前面在讲存储属性的时候其实已经涉及到了,前面嵌套了一个结构体用于Runtime的关联key。我们再来一个枚举示例。

class CustomOb: NSObject {
    var customName:String?
    var customLocation:String?
    var customGroup:String?

    init(name:String , location:String , group:String?) {
        customName = name
        customLocation = location
        customGroup = group
    }

    enum Sex{
        case Secret,Man,Woman
    }
    var sex:Sex{
        switch(customGroup?.lowercaseString){
            case "ManGroup"?:
                return .Man
            case "WomanGroup"?:
                return .Woman
            default:
                return .Secret
        }
    }
}

//test
let customOb = CustomOb(name: "Grey.Luo", location:"成都高新" ,group: nil)
print("customOb.Sex:\(customOb.sex)")


protocol CustomObProtocol{
    func protocolFun(age:Int,company:String)->Bool
}
extension CustomOb:CustomObProtocol{
    func protocolFun(age: Int, company: String) -> Bool {
        print("age:\(age),company:\(company)")
        return true
    }
}

参考:
本文主要用于一个知识的归纳总结,过程中可能会引用到其它地方的文字或代码,如有侵权请及时联系我,在此对写作过程中参考了的文章作者表示感谢!

CocoaPods 升级

//最新源 https://gems.ruby-china.org
From: http://www.jianshu.com/p/935f156b7499

简述

当我们自己电脑上的CocoaPods版本过低,但是别的项目使用的CocoaPods版本过高的时候,你如果通过pod update获取Pods中的内容,就会提示下面的问题:

[!] The 项目 repo requires CocoaPods 1.0.0 -(currently using 0.39.0)

这就是提醒你项目用的CocoaPods的版本是1.0.0,你现在自己的版本是0.39.0。需要升级CocoaPods,可以通过pod –version查看你当前的CocoaPods版本号。

升级

依次使用下面的命令执行更新:(因为一些资源被墙了,步骤2,3是为了国内访问,4是让你确认2,3的操作是否正常执行。当然如果你已经安装了vpn可以访问国外的网站,2,3,4步骤省略)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.$ sudo gem update --system// 先更新gem,国内需要切换源
2.$ gem sources --removehttps://rubygems.org/
3.$ gem sources -ahttps://ruby.taobao.org
4./$ gem sources -l
\*\*\* CURRENT SOURCES \*\*\*https://ruby.taobao.org/
5.$ sudo gem install cocoapods// 安装cocoapods
6.$ pod setup
```
注: 转

更新
如果你不加版本号应该会给你安装最新版

$ sudo gem install cocoapods
我想要更新到1.1.1,所以我执行以下命令

$ sudo gem install cocoapods -v 1.1.1
如果你想尝试预览版,那就用以下命令

$ sudo gem install cocoapods –pre

问题
现实总是不让你开心,各种各样的问题都会出现,今天我在更新pod的时候就遇到了下面的问题。

$ sudo gem install cocoapods -v 1.1.1
Password:
Fetching: cocoapods-core-1.1.1.gem (100%)
Successfully installed cocoapods-core-1.1.1
Fetching: cocoapods-1.1.1.gem (100%)
ERROR: While executing gem … (Errno::EPERM)
Operation not permitted - /usr/bin/pod
从提示可以看出是因为没有权限在/usr/bin/这个目录写入,经过搜索发现原因是OS X 10.11的新安全机制:System Integrity Protection也叫作:rootless,这个默认的安全机制用于保护下面三个目录,所以无法在/usr/bin/中安装pod

/System
/sbin
/usr (with the exception of /usr/local subdirectory)
这个问题网上给出了两种解决方案,一个是关闭这个安全机制,另一个是绕过这个机制安装在别的位置。

关闭该机制

$ sudo nvram boot-args=”rootless=0”; sudo reboot
执行该命令后会重启电脑,保证设置生效

然后再进行正常的安装

$ sudo gem install cocoapods
绕开该机制

$ sudo gem install -n /usr/local/bin cocoapods
这样呢就把pod安装在了/usr/local/bin目录下

对于这两种方法,第二种会好一些,因为自己的可执行程序确实应该放在/usr/local/bin目录下,而且苹果既然有这项设置那肯定有他的道理,没有必要非要关闭这个设置,多个防护多点安全。

```

如果一切顺利,执行完上面的操作,你的CocoaPods就更新完了。但是有时候会碰到一下问题:

ERROR:While executing gem … (Errno::EPERM)

Operation not permitted - /usr/bin/xcodeproj

如果遇到此问题可以将上面步骤5中的命令改为下面的命令

sudo gem install -n /usr/local/bin cocoapods。

参考:http://www.cnblogs.com/brycezhang/p/3675670.html

http://stackoverflow.com/questions/30812777/cannot-install-cocoa-pods-after-uninstalling-results-in-error/30851030#30851030  

GPUImage 手动导入

From: http://www.jianshu.com/p/f7076dfd8b62

GPUImage是Brad Larson在github托管的开源项目。
GPUImage是一个基于GPU图像和视频处理的开源iOS框架,提供各种各样的图像处理滤镜,并且支持照相机和摄像机的实时滤镜; 基于GPU的图像加速,因此可以加速对实时摄像头视频、电影以及image的滤镜和其它效果处理,并且能够自定义图像滤镜。另外, GPUImage支持ARC。
使用GPUImage处理图片比Core Image更简单,只需要将过滤器赋给图片对象即可,不用考虑context或者设备等其他问题。GPUImage提供了除高斯模糊外的其他几种不同效果的模糊,虽然Core Image也提供了几种模糊效果,但目前在iOS上能用的就只有高斯模糊,而GPUImage可用的有FastBlur, GaussianBlur, GaussianSelectiveBlur 和 BoxBlur。此外,作为开源框架的GPUImage还支持自定义的过滤器。
github链接

😄刚开始准备研究这个开源的框架时就遇到了一个问题,如何将该框架导入到项目中使用……..折腾了一上午,先是看作者对框架的描述(全英文的…),按照里边的步骤一步步做,最终也还是没搞出来….后来goolge了半天,终于按照一篇文章的步骤将该框架顺利导入到工程了~
链接

下载GPUImage

下载下来之后注意下整个文件的内容

屏幕快照 2016-04-12 下午3.32.27.png

整个framework都是我们需要用的东西!

将下载好的文件拷贝到自己的工程里边

在自己工程目录下(最好是在根目录下)新建一个文件夹,我的文件夹名字叫GPUImage(后面将会用到这个路径),然后将整个 framework 文件夹复制粘贴到该文件夹下,这一步做好之后应该是这个个样子的

屏幕快照 2016-04-12 下午3.34.11.png

将GPUImage.xcodeproj拖到工程里边

拖拽的是你刚刚拷贝过来的那个GPUImage.xcodeproj

屏幕快照 2016-04-12 下午3.45.38.png

在自己项目的target依赖设置里面添加GPUImage.a作为Target Dependency

屏幕快照 2016-04-12 下午3.48.50.png

屏幕快照 2016-04-12 下午3.49.51.png

添加下面这些系统framework

CoreMedia
CoreVideo
OpenGLES
AVFoundation
QuartzCore

添加头文件路径

Build Settings -> Header Search Paths 添加GPUImage的路径
因为我之前是在项目的根目录下创建的GPUImage这个文件夹,framework 在GPUImage这个文件夹下,所以添加的路径为 GPUImage/framework 。!!注意,路径需要选择recursive!!

屏幕快照 2016-04-12 下午3.57.50.png

target-build setting里面,other linker flags 里面添加 -fobjc-arc -ObjC 这两项

ok!在 ViewController里边导入 GPUImage.h

屏幕快照 2016-04-12 下午3.58.52.png

编译通过了没????

ok 接下来要做的就是如何使用 GPUImage 这个框架了!

上边的步骤如果有不对或者不妥的地方,还请大神赐教,我只是一个小菜鸟~

iOS开发之源码解析 - MBProgressHUD

From: http://dev.dafan.info/detail/365641?p=60

iOS开发之源码解析 - MBProgressHUD

MBProgressHUD 是一个为 APP 添加 HUD 窗口的第三方框架,使用起来极其简单方便,关于 MBProgressHUD 的使用方法,GitHub 上有详细的说明,这里就不多加介绍了,本文主要是从源码的角度分析 MBProgressHUD的具体实现。

  • 先来对 MBProgressHUD 有个大体的认识,这是刚从 GitHub 上拉下来的代码,如下图,MBProgressHUD 的主要文件只有两个:

MBProgressHUD 源码解析


下面我们就开始分析 MBProgressHUD 的具体实现。在此之前先了解下文章的目录,本文主要有三个部分:

  1. MBProgressHUD 核心 API
    • 这一部分讲的主要是 MBProgressHUD 的一些属性方法等,主要是为了对 MBProgressHUD 先有个大概的认识
  2. show 系列方法
    • 这一部分主要是展示 HUD 窗口时调用的方法及代码分析
  3. hide 系列方法
    • 这一部分主要是隐藏 HUD 窗口时调用的方法及代码分析

begin~~~

一、MBProgressHUD 核心 API

这一部分讲的主要是 MBProgressHUD 的一些属性方法等,主要是为了对 MBProgressHUD 先有个大概的认识

1.1 模式

首先来看看 [MBProgressHUD][7] 中定义的枚举,mode 一共有六种显示样式:

[7]: https://github.com/jdg/MBProgressHUD

/// 显示样式
typedef NS_ENUM(NSInteger, MBProgressHUDMode) {
/// 默认模式, 系统自带的指示器
MBProgressHUDModeIndeterminate,
/// 圆形饼图
MBProgressHUDModeDeterminate,
/// 水平进度条
MBProgressHUDModeDeterminateHorizontalBar,
/// 圆环
MBProgressHUDModeAnnularDeterminate,
/// 自定义视图
MBProgressHUDModeCustomView,
/// 只显示文字
MBProgressHUDModeText
};

简单的效果图如下(颜色尺寸等都可以优化,我这里只是简单地示例):

默认模式:hud.mode = MBProgressHUDModeIndeterminate

圆形饼图:hud.mode = MBProgressHUDModeDeterminate

水平进度条:hud.mode = MBProgressHUDModeDeterminateHorizontalBar

圆环:hud.mode = MBProgressHUDModeAnnularDeterminate

自定义视图:hud.mode = MBProgressHUDModeCustomView

只显示文字:hud.mode = MBProgressHUDModeText

1.2 动画效果

MBProgressHUD 在显示 HUD 窗口的时候,一般都伴随着动画效果,MBProgressHUD 中的动画效果也是一个枚举,如下:

typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) {
    ///  默认效果,只有透明度变化
    MBProgressHUDAnimationFade,
    /// 透明度变化 + 形变 (放大时出现缩小消失)
    MBProgressHUDAnimationZoom,
    /// 透明度变化 + 形变 (缩小)
    MBProgressHUDAnimationZoomOut,
    /// 透明度变化 + 形变 (放大)
    MBProgressHUDAnimationZoomIn
};

这里先简单的罗列出来,下文中还会多次用到。

1.3 MBProgressHUD 组成

MBProgressHUD 主要由四部分组成:loading 动画视图标题文本框详情文本框HUD 背景框,如下图。

MBProgressHUD 组成

之前用 MBProgressHUD 设置标题文本详情文本是通过几个属性来实现的,功能少也较为繁琐,因此被遗弃了;现在设置标题文本详情文本等十分简便,直接通过 label 等控件就可以实现,而且在功能上也有很大的扩展,详情请看下面这个代码块:

/// bezelView 是指包括文本和指示器的视图,和自定义的 customView 类似
@property (strong, nonatomic, readonly) MBBackgroundView *bezelView;
/// backgroundView 背景视图
@property (strong, nonatomic, readonly) MBBackgroundView *backgroundView;
/// customView 自定义视图
@property (strong, nonatomic, nullable) UIView *customView;
/// label 指的是标题文本
@property (strong, nonatomic, readonly) UILabel *label;
/// detailsLabel指的是详情文本
@property (strong, nonatomic, readonly) UILabel *detailsLabel;
/// hud 窗口还可以加入button,添加事件
@property (strong, nonatomic, readonly) UIButton *button;

另外这里再附加两张 MBProgressHUD 的整体布局图,以便更好地认识 MBProgressHUD

MBProgressHUD 的整体布局图 1

MBProgressHUD 的整体布局图 2

简单介绍

  • backgroundView:整个背景图层,可以通过 MBBackgroundView 的 style 属性设置
  • bezelView:提供元素 (indicator、label、detailLabel、button)的背景
  • indicator:指示器显示进度情况 这个视图由我们设定的mode属性决定
  • label:显示标题文本
  • detailLabel:显示详情文本
  • button:添加点击事件

1.4 MBProgressHUD 中的属性

MBProgressHUD 文件中主要包括四个类,它们分别是 MBProgressHUDMBRoundProgressViewMBBarProgressViewMBBackgroundView。这四个类各有各的用法,比如如果是进度条模式(MBProgressHUDModeDeterminateHorizontalBar),则使用的是 MBBarProgressView 类;如果是饼图模式(MBProgressHUDModeDeterminate)或环形模式(MBProgressHUDModeAnnularDeterminate),则使用的是 MBRoundProgressView类。下面是这四个类的相关属性。

1.41 MBProgressHUD 相关属性
/// show 方法触发到显示 HUD 窗口的间隔时间,默认是 0
@property (assign, nonatomic) NSTimeInterval graceTime;

/// HUD 窗口显示的最短时间,默认是 0
@property (assign, nonatomic) NSTimeInterval minShowTime;

/// HUD 窗口显示模式, 默认是系统自带的指示器
@property (assign, nonatomic) MBProgressHUDMode mode;

/// 进度条指示器以及文本的颜色
@property (strong, nonatomic, nullable) UIColor *contentColor UI_APPEARANCE_SELECTOR;

/// HUD 窗口显示和隐藏的动画类型MBProgressHUD
@property (assign, nonatomic) MBProgressHUDAnimation animationType UI_APPEARANCE_SELECTOR;

/// HUD 窗口位置设置,比如 hud.offset = CGPointMake(0.f, MBProgressMaxOffset),可以移到底部中心位置
@property (assign, nonatomic) CGPoint offset UI_APPEARANCE_SELECTOR;

/// HUD 元素到 HUD 边缘的距离,默认是 20.f
@property (assign, nonatomic) CGFloat margin UI_APPEARANCE_SELECTOR;

/// HUD 窗口背景框的最小尺寸
@property (assign, nonatomic) CGSize minSize UI_APPEARANCE_SELECTOR;

/// 是否强制 HUD 背景框宽高相等
@property (assign, nonatomic, getter = isSquare) BOOL square UI_APPEARANCE_SELECTOR;

/// 进度条 (0.0 到 1.0)
@property (nonatomic, assign) float progress;

/// bezelView 是指包括文本和指示器的视图,和自定义的 customView 类似
@property (strong, nonatomic, readonly) MBBackgroundView *bezelView;

/// backgroundView 背景视图
@property (strong, nonatomic, readonly) MBBackgroundView *backgroundView;

/// customView 自定义视图
@property (strong, nonatomic, nullable) UIView *customView;

/// label 指的是标题文本
@property (strong, nonatomic, readonly) UILabel *label;

/// detailsLabel指的是详情文本
@property (strong, nonatomic, readonly) UILabel *detailsLabel;

/// hud 窗口还可以加入button,添加事件
@property (strong, nonatomic, readonly) UIButton *button;
1.42 MBRoundProgressView 相关属性
/// 进度条 (0.0 到 1.0)
@property (nonatomic, assign) float progress;

/// 进度条颜色
@property (nonatomic, strong) UIColor *progressColor;

/// 进度条指示器的颜色
@property (nonatomic, strong) UIColor *progressTintColor;

/// 进度条指示器的背景颜色,只适用在 iOS7 以上,默认为半透明的白色 (透明度 0.1)
@property (nonatomic, strong) UIColor *backgroundTintColor;

/// 显示模式,NO = 圆形;YES = 环形。默认是 NO
@property (nonatomic, assign, getter = isAnnular) BOOL annular;
1.43 MBBarProgressView 相关属性
/// 进度条 (0.0 到 1.0)
@property (nonatomic, assign) float progress;

/// 进度条边界线的颜色,默认是白色
@property (nonatomic, strong) UIColor *lineColor;

/// 进度条背景色,默认是透明
@property (nonatomic, strong) UIColor *progressRemainingColor;

/// 进度条颜色
@property (nonatomic, strong) UIColor *progressColor;
1.44 MBBackgroundView 相关属性
/// 背景图层样式,有两种,iOS7 或者以上版本默认风格是MBProgressHUDBackgroundStyleBlur,其他为MBProgressHUDBackgroundStyleSolidColor,由于 iOS7 不支持 UIVisualEffectView,所以在 iOS7 和更高版本中会有所不同
@property (nonatomic) MBProgressHUDBackgroundStyle style;

/// 背景颜色,由于 iOS7 不支持 UIVisualEffectView,所以在 iOS7 和更高版本中会有所不同
@property (nonatomic, strong) UIColor *color;

1.5 MBProgressHUD 中的一些方法

1.51 类方法
/// 创建一个 HUD 窗口,并把它显示在 view 上,还可以设置是否有动画
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;

/// 找到最上层的 HUD subview 并把它隐藏,成功为YES、其他情况为 NO
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;

/// 返回最上层的 HUD subview
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;

这三个类方法中,常用的是第一个函数+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;直接创建 HUD,并把它显示在 view 上,用起来极其方便

1.52 对象方法
/// 以view为基准创建初始化一个HUD对象,为HUD的初始化构造函数
- (instancetype)initWithView:(UIView *)view;

/// 显示HUD控件,此函数应该在主线程中调用
- (void)showAnimated:(BOOL)animated;

/// 隐藏HUD控件,animated控制是否显示动画。对应于- (void)showAnimated:(BOOL)animated;
- (void)hideAnimated:(BOOL)animated;

/// 在delay时间之后隐藏HUD,animated控制显示动画与否,delay控制延迟时间
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;

这几个对象方法中,常用的也有两个- (void)hideAnimated:(BOOL)animated;- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;

二、show 系列方法

这一部分主要是展示 HUD 窗口时调用的方法及代码分析

下面这个方法在我们创建 MBProgressHUD 对象时首先调用

+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {

    /// 创建并初始化 MBProgressHUD 对象,根据传进来的 view 来设定
    MBProgressHUD *hud = [[self alloc] initWithView:view];

    /// 移除 HUD 窗口
    hud.removeFromSuperViewOnHide = YES;

    /// 添加到 View 上,并显示
    [view addSubview:hud];
    [hud showAnimated:animated];
    return hud;
}

这个方法会调用两个主要方法:- (id)initWithView:(UIView *)view- (void)showAnimated:(BOOL)animated,具体的调用流程如下图:

show 相关的方法调用

当然在 MBProgressHUD 中,+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated 调用的方法远不止上图列的这些,图上列的只是几个主要方法。接下来我们就根据程序的执行过程来一步一步分析一下代码。

在方法 - (id)initWithView:(UIView *)view中,调用 - (instancetype)initWithFrame:(CGRect)frame,接着会调用- (void)commonInit

- (id)initWithView:(UIView *)view {
    NSAssert(view, @"View must not be nil.");
    return [self initWithFrame:view.bounds];
}

- (instancetype)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        [self commonInit];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit {

    /// 默认效果, 透明度变化
    _animationType = MBProgressHUDAnimationFade;

    /// 默认模式, 系统自带的指示器
    _mode = MBProgressHUDModeIndeterminate;

    /// HUD 元素到 HUD 边缘的距离,默认是 20.f
    _margin = 20.0f;
    _opacity = 1.f;
    _defaultMotionEffectsEnabled = YES;

    // 默认颜色,根据当前的 iOS 版本
    BOOL isLegacy = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0;

    /// 进度条指示器以及文本的颜色
    _contentColor = isLegacy ? [UIColor whiteColor] : [UIColor colorWithWhite:0.f alpha:0.7f];

    /// opaque 类似 Alpha,表示当前 UIView 的不透明度,设置是否之后对于 UIView 的显示并没有什么影响,官方文档的意思是 opaque 默认为 YES,如果 alpha 小于 1,那么应该设置 opaque 设置为 NO,当 alpha 为 1,opaque设置为 NO
    self.opaque = NO;

    /// 背景色
    self.backgroundColor = [UIColor clearColor];

    // 透明度为 0 
    self.alpha = 0.0f;
    /// 自动调整子控件与父控件之间的宽高
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    self.layer.allowsGroupOpacity = NO;

    /// 设置所需的子view
    [self setupViews];
    /// 设置指示器样式
    [self updateIndicators];
    /// 注册通知
    [self registerForNotifications];
}

上面代码块中的代码已经加过注释,因此在这里不再累述某句代码有什么作用,这里直接说程序的执行流程。当程序执行到 - (void)commonInit 这个方法时,会相继执行- (void)setupViews- (void)updateIndicators- (void)registerForNotifications 这三个方法,当然在执行这三个方法期间,也会执行其它的方法,比如会执行- (void)updateForBackgroundStyle- (void)updateBezelMotionEffects等等,这和你设置的 mode 的模式,以及和 label,detailsLabel ,button 这一系列元素,以及和相应的属性都有一定的关系。

接着我们来分析一下 - (void)setupViews- (void)updateIndicators- (void)registerForNotifications 这三个方法。

- (void)setupViews {

    /// 进度条指示器以及文本的颜色
    UIColor *defaultColor = self.contentColor;


    /// 创建背景视图
    MBBackgroundView *backgroundView = [[MBBackgroundView alloc] initWithFrame:self.bounds];

    /// 背景图层样式
    backgroundView.style = MBProgressHUDBackgroundStyleSolidColor;
    backgroundView.backgroundColor = [UIColor clearColor];

    /// 自动调整 view 的宽度和高度
    backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    backgroundView.alpha = 0.f;
    [self addSubview:backgroundView];
    _backgroundView = backgroundView;

    /// 创建背景视图(和上面那个大小不同)
    MBBackgroundView *bezelView = [MBBackgroundView new];

    /// 代码层面使用 Autolayout,需要对使用的 View 的translatesAutoresizingMaskIntoConstraints 属性设置为NO
    bezelView.translatesAutoresizingMaskIntoConstraints = NO;
    bezelView.layer.cornerRadius = 5.f;
    bezelView.alpha = 0.f;
    [self addSubview:bezelView];
    _bezelView = bezelView;

    /// 调用 updateBezelMotionEffects 方法,设置视差效果
    [self updateBezelMotionEffects];


    /// 创建 label 标签,显示主要文本
    UILabel *label = [UILabel new];

    /// 取消文字大小自适应
    label.adjustsFontSizeToFitWidth = NO;
    label.textAlignment = NSTextAlignmentCenter;
    label.textColor = defaultColor;
    label.font = [UIFont boldSystemFontOfSize:MBDefaultLabelFontSize];

    /// opaque 类似 Alpha,表示当前 UIView 的不透明度,设置是否之后对于 UIView 的显示并没有什么影响,官方文档的意思是 opaque 默认为 YES,如果 alpha 小于 1,那么应该设置 opaque 设置为 NO,当 alpha 为 1,opaque设置为 NO
    label.opaque = NO;
    label.backgroundColor = [UIColor clearColor];
    _label = label;


    /// 创建 detailsLabel 标签,显示详细信息

    UILabel *detailsLabel = [UILabel new];
    /// 取消文字大小自适应
    detailsLabel.adjustsFontSizeToFitWidth = NO;
    detailsLabel.textAlignment = NSTextAlignmentCenter;
    detailsLabel.textColor = defaultColor;
    detailsLabel.numberOfLines = 0;
    detailsLabel.font = [UIFont boldSystemFontOfSize:MBDefaultDetailsLabelFontSize];

    /// opaque 类似 Alpha,表示当前 UIView 的不透明度,设置是否之后对于 UIView 的显示并没有什么影响,官方文档的意思是 opaque 默认为 YES,如果 alpha 小于 1,那么应该设置 opaque 设置为 NO,当 alpha 为 1,opaque设置为 NO
    detailsLabel.opaque = NO;
    detailsLabel.backgroundColor = [UIColor clearColor];
    _detailsLabel = detailsLabel;


    /// 创建 button 按钮,并添加响应按钮
    UIButton *button = [MBProgressHUDRoundedButton buttonWithType:UIButtonTypeCustom];
    button.titleLabel.textAlignment = NSTextAlignmentCenter;
    button.titleLabel.font = [UIFont boldSystemFontOfSize:MBDefaultDetailsLabelFontSize];
    [button setTitleColor:defaultColor forState:UIControlStateNormal];
    _button = button;


    /// 将 label,detailLabel,button 添加到蒙版视图
    for (UIView *view in @[label, detailsLabel, button]) {

        /// 代码层面使用 Autolayout,需要对使用的 View 的translatesAutoresizingMaskIntoConstraints 属性设置为NO
        view.translatesAutoresizingMaskIntoConstraints = NO;

        /// 为视图设置水平方向上优先级为 998 的压缩阻力
        [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];

        /// 为视图设置垂直方向上优先级为 998 的压缩阻力
        [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];
        [bezelView addSubview:view];
    }


    /// 创建顶部视图
    UIView *topSpacer = [UIView new];

    /// 代码层面使用 Autolayout,需要对使用的 View 的translatesAutoresizingMaskIntoConstraints 属性设置为NO
    topSpacer.translatesAutoresizingMaskIntoConstraints = NO;
    topSpacer.hidden = YES;
    [bezelView addSubview:topSpacer];
    _topSpacer = topSpacer;

    /// 创建底部视图
    UIView *bottomSpacer = [UIView new];

        /// 代码层面使用 Autolayout,需要对使用的 View 的translatesAutoresizingMaskIntoConstraints 属性设置为NO
    bottomSpacer.translatesAutoresizingMaskIntoConstraints = NO;
    bottomSpacer.hidden = YES;
    [bezelView addSubview:bottomSpacer];
    _bottomSpacer = bottomSpacer;
}

- (void)updateIndicators {
    UIView *indicator = self.indicator;

    /// 判断当前指示器是否是 UIActivityIndicatorView
    BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]];

    /// 判断当前指示器是否是 MBRoundProgressView
    BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]];

    MBProgressHUDMode mode = self.mode;
    /// MBProgressHUDModeIndeterminate:系统自带的指示器
    if (mode == MBProgressHUDModeIndeterminate) {
        if (!isActivityIndicator) {
             // 如果当前指示器不属于 UIActivityIndicatorView 类型,则移除之前的indicator,重新创建
            [indicator removeFromSuperview];
            indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
            [(UIActivityIndicatorView *)indicator startAnimating];
            [self.bezelView addSubview:indicator];
        }
    }
    else if (mode == MBProgressHUDModeDeterminateHorizontalBar) {
        /// 如果当前指示器不属于 MBBarProgressView 类型,则移除之前的indicator,重新创建
        [indicator removeFromSuperview];
        indicator = [[MBBarProgressView alloc] init];
        [self.bezelView addSubview:indicator];
    }
    else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) {
        if (!isRoundIndicator) {
            /// 如果当前指示器不属于 MBRoundProgressView 类型,则移除之前的indicator,重新创建
            [indicator removeFromSuperview];
            indicator = [[MBRoundProgressView alloc] init];
            [self.bezelView addSubview:indicator];
        }
        if (mode == MBProgressHUDModeAnnularDeterminate) { /// 圆环指示器
            [(MBRoundProgressView *)indicator setAnnular:YES];
        }
    } 
    else if (mode == MBProgressHUDModeCustomView && self.customView != indicator) { /// 自定义视图指示器
        [indicator removeFromSuperview];
        indicator = self.customView;
        [self.bezelView addSubview:indicator];
    }
    else if (mode == MBProgressHUDModeText) { /// 文本形式,去除指示器视图
        [indicator removeFromSuperview];
        indicator = nil;
    }
        /// 代码层面使用 Autolayout,需要对使用的 View 的translatesAutoresizingMaskIntoConstraints 属性设置为NO
    indicator.translatesAutoresizingMaskIntoConstraints = NO;
    self.indicator = indicator;

    if ([indicator respondsToSelector:@selector(setProgress:)]) {
        /// 设置进度条的数值
        [(id)indicator setValue:@(self.progress) forKey:@"progress"];
    }


     /// 为视图设置水平方向上优先级为 998 的压缩阻力
    [indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];

    /// 为视图设置垂直方向上优先级为 998 的压缩阻力
    [indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];

    /// 设置控件颜色
    [self updateViewsForColor:self.contentColor];
    /// 更新布局
    [self setNeedsUpdateConstraints];
}

- (void)registerForNotifications {
#if !TARGET_OS_TV
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    /// 通过通知 UIApplicationDidChangeStatusBarOrientationNotification 来处理屏幕转屏事件
    [nc addObserver:self selector:@selector(statusBarOrientationDidChange:)
               name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
#endif
}
  • 由上面代码我们可以看出,在方法- (void)setupViews中,创建了 backgroundView、bezelView、label、detailsLabel、button 这几个控件,并使用 for 循环把 label、detailsLabel、button 添加到bezelView 视图中,最后还创建了顶部视图和底部视图,不过默认是隐藏的。有一点值得说明,在创建 button 时并没有设置 button 的 size 等属性,那么这个按钮是不会显示的。在这里 MBProgressHUD 重写了一个 Unbutton 的子类 MBProgressHUDRoundedButton。这个子类里面有一个方法,- (CGSize)intrinsicContentSize,通过这个方法来设置 Unbutton 的 size。

    • (CGSize)intrinsicContentSize {
      /// 只有当有事件才显示(这里也告诉我们,如果这个 button 没有任何事件的话,它的大小就是 CGSizeZero,即不会显示)
      if (self.allControlEvents == 0) return CGSizeZero;
      CGSize size = [super intrinsicContentSize];
      // Add some side padding
      size.width += 20.f;
      return size;
      }
  • - (void)updateIndicators这个方法主要是用来设置 indicator 指示器的,根据 mode 的属性显示不同的形式,具体可以参看代码注释。这个方法最后调用的是setNeedsUpdateConstraints函数,这个函数是系统自带的方法,它会自动调用- (void)updateConstraints 方法,- (void)updateConstraints 主要作用是更新各个控件的布局,我们稍后再对这个方法进行详细分析。

  • - (void)registerForNotifications这个方法中的代码量很少,它的作用是通过通知 UIApplicationDidChangeStatusBarOrientationNotification 来处理屏幕转屏事件

- (void)registerForNotifications这一系列方法执行完毕之后,程序会重新返回到+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated这个方法中,接着调用另一个主要函数- (void)showAnimated:(BOOL)animated

- (void)showAnimated:(BOOL)animated {

    /// 显示放在主线程中
    MBMainThreadAssert();

    /// 取消定时器
    [self.minShowTimer invalidate];
    self.useAnimation = animated;
    self.finished = NO;

    /// 如果设置了宽限时间graceTime,则延迟显示(避免 HUD 一闪而过的差体验)
    if (self.graceTime > 0.0) {

        /// 创建定时器,把它加入 NSRunLoop 中 
        NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        self.graceTimer = timer;
    } 

    /// 没有设置 graceTime,则直接显示
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}

/// 设置宽限时间 graceTime时调用的方法
- (void)handleGraceTimer:(NSTimer *)theTimer {
    // Show the HUD only if the task is still running
    if (!self.hasFinished) {
        [self showUsingAnimation:self.useAnimation];
    }
}

- (void)showUsingAnimation:(BOOL)animated {
    /// 移除所有动画
    [self.bezelView.layer removeAllAnimations];
    [self.backgroundView.layer removeAllAnimations];

    /// 取消 hideDelayTimer
    [self.hideDelayTimer invalidate];

    /// 开始时间
    self.showStarted = [NSDate date];
    self.alpha = 1.f;

    /// 以防我们隐藏 NSProgress 对象
    [self setNSProgressDisplayLinkEnabled:YES];

    if (animated) {
        [self animateIn:YES withType:self.animationType completion:NULL];
    } else {
        /// 方法弃用告警
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        self.bezelView.alpha = self.opacity;
#pragma clang diagnostic pop
        self.backgroundView.alpha = 1.f;
    }
}

由上面这段代码我们可以看出,在方法- (void)showAnimated:(BOOL)animated中,无论我们有没有设置graceTime这个属性,最后都会去执行一个方法 - (void)showUsingAnimation:(BOOL)animated- (void)showUsingAnimation:(BOOL)animated 这个方法在上面已经做过注释,不再细说,不过有两小点值得我们注意,第一点是 - (void)showUsingAnimation:(BOOL)animated 在执行过程中调用了一个方法 - (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled,先来看下这个方法

- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {

    /// 这里使用 CADisplayLink,是因为如果使用 KVO 机制会非常消耗主线程(因为 NSProgress 频率非常快)
    if (enabled && self.progressObject) {
        /// 创建 CADisplayLink 对象
        if (!self.progressObjectDisplayLink) {
            self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
        }
    } else {
        self.progressObjectDisplayLink = nil;
    }
}

这个方法是关于 CADisplayLink 的,CADisplayLink 是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个runloop 中,并给它提供一个 targetselector 在屏幕刷新的时候调用。一旦 CADisplayLink 以特定的模式注册到 runloop 之后,每当屏幕需要刷新,runloop 就会向 CADisplayLink 指定的target 发送一次指定的 selector 消息, CADisplayLink 类对应的 selector 就会被调用一次。

- (void)showUsingAnimation:(BOOL)animated 这个方法中还有一点值得注意,就是只有具有动画效果的前提下,即 animated 为真时才会调用 - (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 这个方法,下面我们再一起来看下这个方法。

/// animated 为真时调用,消失或出现时的伸缩效果,以及透明度
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
    /// 自动确定正确的缩放动画类型,关于 MBProgressHUDAnimation 的几种类型,上文已全部列出,这里不再详细介绍
    if (type == MBProgressHUDAnimationZoom) {
        type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
    }

    /// CGAffineTransformMakeScale 中的两个参数,分别代表x和y方向缩放倍数
    CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
    CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);

    /// 设定初始状态
    UIView *bezelView = self.bezelView;
    if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
        bezelView.transform = small; /// 缩放
    } else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
        bezelView.transform = large; /// 扩大
    }

    /// 创建动画任务
    dispatch_block_t animations = ^{
        if (animatingIn) {
            bezelView.transform = CGAffineTransformIdentity;
        } else if (!animatingIn && type == MBProgressHUDAnimationZoomIn) {
            bezelView.transform = large;
        } else if (!animatingIn && type == MBProgressHUDAnimationZoomOut) {
            bezelView.transform = small;
        }

    /// 方法弃用告警
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        bezelView.alpha = animatingIn ? self.opacity : 0.f;
#pragma clang diagnostic pop
        self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
    };

    /// 动画的两种形式,>= iOS7 的是一种形式,iOS7以下是另一种
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
    if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
        /// 只支持 >= iOS7
        [UIView animateWithDuration:0.3 delay:0. usingSpringWithDamping:1.f initialSpringVelocity:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
        return;
    }
#endif
    [UIView animateWithDuration:0.3 delay:0. options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
}

- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 这个方法,无论是处于 show 状态还是处于 hide 状态,都会调用,下边我们再一起看下 hide 系列的一些方法。

三、hide 系列方法

这一部分主要是隐藏 HUD 窗口时调用的方法及代码分析

关于隐藏 HUD 窗口,MBProgressHUD 给我们提供的方法有以下几个:

/// 找到最上层的 HUD subview 并把它隐藏,成功为YES、其他情况为 NO
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;

//隐藏HUD控件,animated控制是否显示动画。对应于- (void)showAnimated:(BOOL)animated;
- (void)hideAnimated:(BOOL)animated;

//在delay时间之后隐藏HUD,animated控制显示动画与否,delay控制延迟时间
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;

最常用的是后面两个: - (void)hideAnimated:(BOOL)animated- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay,这两个方法的本质是相同的,不同的只是形式,也就是说这两个方法的实现流程基本上是一致的,只不过 - (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay 多执行一两个方法而已。下面我们就来具体分析下 hide 系列的方法。

  • 首先还是来说说+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated这个函数,如果调用这个方法来隐藏 HUD 窗口,那么会先调用两个方法:

    • (BOOL)hideHUDForView:(UIView )view animated:(BOOL)animated {
      /// 获取当前 view 的最上面的 HUD
      MBProgressHUD
      hud = [self HUDForView:view];
      if (hud != nil) {

      /// 移除 HUD 窗口
      hud.removeFromSuperViewOnHide = YES;
      [hud hideAnimated:animated];
      return YES;
      

      }
      return NO;
      }

    • (MBProgressHUD )HUDForView:(UIView )view {
      /// NSEnumerator 是一个枚举器,依附于集合类(NSArray,NSSet,NSDictionary等),reverseObjectEnumerator 倒序遍历
      NSEnumerator subviewsEnum = [view.subviews reverseObjectEnumerator];
      for (UIView
      subview in subviewsEnum) {

      if ([subview isKindOfClass:self]) {
          return (MBProgressHUD *)subview;
      }
      

      }
      return nil;
      }

当执行完上面这两个方法之后,接下来执行的方法和调用- (void)hideAnimated:(BOOL)animated隐藏 HUD 窗口时执行的方法相同,所以下边会详细分析。

  • 接下来说说调用- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay 隐藏 HUD 窗口时的情况,上文已经说过,调用这个方法会比调用- (void)hideAnimated:(BOOL)animated 多执行一两个方法:

    • (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay {
      /// 创建定时器,并把它加入到 NSRunLoop 中
      NSTimer *timer = [NSTimer timerWithTimeInterval:delay target:self selector:@selector(handleHideTimer:) userInfo:@(animated) repeats:NO];
      [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
      self.hideDelayTimer = timer;
      }

    • (void)handleHideTimer:(NSTimer *)timer {
      [self hideAnimated:[timer.userInfo boolValue]];
      }

由上面代码可以清晰的看出,- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay 这个方法中加了一个定时器,当执行完这个定时器的selector时,就会执行- (void)hideAnimated:(BOOL)animated方法。

  • 由此可见无论使用哪种方法隐藏 HUD 窗口,最终都会来到这个方法,- (void)hideAnimated:(BOOL)animated,接下来我们就来分析下这个方法的具体调用流程,先看个图:

hide 相关的方法调用

上图显示的是 hide 相关的方法调用,只罗列了几个主要方法。接下来我们就来分析下这几个主要方法。先来到- (void)hideAnimated:(BOOL)animated方法中:

- (void)hideAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.graceTimer invalidate];
    self.useAnimation = animated;
    self.finished = YES;

    /// 如果设置了最小显示时间,则执行此步骤,否则直接隐藏
    if (self.minShowTime > 0.0 && self.showStarted) {
        NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];

        /// 如果 minShowTime 比较大,则暂时不触发 HUD 的隐藏,而是启动一个 NSTimer
        if (interv < self.minShowTime) {
            /// 创建定时器,并把它加入到 NSRunLoop 中
            NSTimer *timer = [NSTimer timerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
            self.minShowTimer = timer;
            return;
        } 
    }
    /// 直接隐藏 HUD
    [self hideUsingAnimation:self.useAnimation];
}

- (void)handleMinShowTimer:(NSTimer *)theTimer {
    [self hideUsingAnimation:self.useAnimation];
}

从上面代码块中可以看出,无论我们有没有设置最小显示时间 self.minShowTime,都会触发 - (void)hideUsingAnimation:(BOOL)animated 这个方法,因此程序最后都会来到 - (void)hideUsingAnimation:(BOOL)animated 这个方法中:

- (void)hideUsingAnimation:(BOOL)animated {
    if (animated && self.showStarted) {
        /// 将 showStarted 设为 nil
        self.showStarted = nil;
        [self animateIn:NO withType:self.animationType completion:^(BOOL finished) {
            [self done];
        }];
    } else {
        self.showStarted = nil;
        self.bezelView.alpha = 0.f;
        self.backgroundView.alpha = 1.f;
        [self done];
    }
}

这个方法和 show 系列的 - (void)showUsingAnimation:(BOOL)animated 方法一样,只要设定 animated 的属性为 YES,最终都会走到 - (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 这个方法中,同时会执行一个方法:- (void)done,接下来我们来看一下这两个方法:

/// animated 为真时调用,消失或出现时的伸缩效果,以及透明度
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
    /// 自动确定正确的缩放动画类型,关于 MBProgressHUDAnimation 的几种类型,上文已全部列出,这里不再详细介绍
    if (type == MBProgressHUDAnimationZoom) {
        type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
    }

    /// CGAffineTransformMakeScale 中的两个参数,分别代表x和y方向缩放倍数
    CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
    CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);

    /// 设定初始状态
    UIView *bezelView = self.bezelView;
    if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
        bezelView.transform = small; /// 缩放
    } else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
        bezelView.transform = large; /// 扩大
    }

    /// 创建动画任务
    dispatch_block_t animations = ^{
        if (animatingIn) {
            bezelView.transform = CGAffineTransformIdentity;
        } else if (!animatingIn && type == MBProgressHUDAnimationZoomIn) {
            bezelView.transform = large;
        } else if (!animatingIn && type == MBProgressHUDAnimationZoomOut) {
            bezelView.transform = small;
        }

    /// 方法弃用告警
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        bezelView.alpha = animatingIn ? self.opacity : 0.f;
#pragma clang diagnostic pop
        self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
    };

    /// 动画的两种形式,>= iOS7 的是一种形式,iOS7以下是另一种
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
    if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
        /// 只支持 >= iOS7
        [UIView animateWithDuration:0.3 delay:0. usingSpringWithDamping:1.f initialSpringVelocity:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
        return;
    }
#endif
    [UIView animateWithDuration:0.3 delay:0. options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
}

- (void)done {
    /// 取消 hideDelayTimer
    [self.hideDelayTimer invalidate];
    /// 隐藏 NSProgress 对象
    [self setNSProgressDisplayLinkEnabled:NO];

    if (self.hasFinished) {
        self.alpha = 0.0f;
        if (self.removeFromSuperViewOnHide) {
            /// 从父视图中移除
            [self removeFromSuperview];
        }
    }
    MBProgressHUDCompletionBlock completionBlock = self.completionBlock;
    if (completionBlock) {
        completionBlock();
    }
    id<MBProgressHUDDelegate> delegate = self.delegate;
    if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
        [delegate performSelector:@selector(hudWasHidden:) withObject:self];
    }
}

关于 - (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 这个方法,只要 animated 的属性为 YES,都会调用;而在- (void)done 这个方法中,如果 removeFromSuperViewOnHide 属性为 YES,则将自己从父视图上移除;如果有 completionBlock 回调函数,则执行回调;如果实现了代理并实现了代理方法,则执行代理方法。而且我们还观察到在 hide 时,也会调用 - (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled 方法,只是在 hide 时 enabled 为 NO。

- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {

    /// 这里使用 CADisplayLink,是因为如果使用 KVO 机制会非常消耗主线程(因为 NSProgress 频率非常快)
    if (enabled && self.progressObject) {
        /// 创建 CADisplayLink 对象
        if (!self.progressObjectDisplayLink) {
            self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
        }
    } else {
        self.progressObjectDisplayLink = nil;
    }
}

over


以上便是对 MBProgressHUD 源码的一些总结和认识,如果有不足之处,还希望各位道友能多指点哈!


原文链接:http://www.jianshu.com/p/59121cac78bd

CocoaPods 升级指定版本or降级

From: http://blog.csdn.net/hsf_study/article/details/69945473

=====================升级版本===================

CocoaPods 1.1.0+ is required to build SnapKit 3.0.0+.

在swift3以后很多github框架需要在cocoapods1.1.0以后版本环境下才能正常使用,比如SnapKit .
我的cocoapods 依然是原始的 0.39.0 版本,俨然跟不上时代的进步.
$ pod –version
0.39.0

故记录升级cocoapods1.1.0 的历程.望对你有用.

第一步 升级 ruby - 升级至2.2.2以上

执行 ruby -v 命令查看当前ruby环境,如果在2.2.2以上自行忽略第一步.

$ ruby -v

1、 RVM安装

$ curl -L get.rvm.io | bash -s stable

###2、 之后就是等待一段时间之后,就可以安装成功了,使用以下命令来验证

$ source ~/.bashrc

$ source ~/.bash_profile

3、列出已知ruby的版本

rvm list known

这里写图片描述

我们可以选择安装2.2.4版本 执行命令->

4.安装ruby 2.2.4()

rvm install 2.2.4

等待安装…
这里写图片描述

如果安装失败(

解决方案:
1.brew update
2.brew install gmp

查看 http://www.jianshu.com/p/95c1ebbc403b)

5、安装完之后,可以ruby -v 测试一下 在这一步,需要按回车键,需要输入电脑密码,当然如果你没装xcode,需要先去装xcode.

ruby -v

测试安装完成
这里写图片描述

第二步:升级cocoapods1.2.0

1.先切换gem源 中间可能会要求输入电脑密码

1.1移除旧源:
如果之前是 https://rubygems.org/
gem sources --remove https://rubygems.org/

如果之前是 https://ruby.taobao.org/
gem sources --remove https://ruby.taobao.org/

1.2可以先执行一次系统更新操作
$ sudo gem update --system

1.3切换gem源 (2019-3-18更新)
gem source -a https://gems.ruby-china.com

切换 gem源 成功
这里写图片描述

2.升级cocoapods

下面命令,只选择一个使用。


sudo gem install -n /usr/local/bin cocoapods --pre //最新版本

或着 sudo gem install -n /usr/local/bin cocoapods -v (版本号) //指定安装 cocoapods的版本

这里写图片描述

安装成功! 测试一下

pod --version

会显示已安装的最新版本
这里写图片描述

其实这已经完成标题的需求了…
不过最后还是需要设置pod仓库

3.设置pod仓库

pod setup

根据网速等待不等时间..等待 ” Setup completed “

这里写图片描述

这就可以使用swift3.0以后的框架了.

pod install –no-repo-update

IOS可变参数

From: http://swifter.tips/variadic/

可变参数函数指的是可以接受任意多个参数的函数,我们最熟悉的可能就是 NSString-stringWithFormat: 方法了。在 Objective-C 中,我们使用这个方法生成字符串的写法是这样的:

NSString *name = @"Tom";
NSDate *date = [NSDate date];
NSString *string = [NSString stringWithFormat:
                @"Hello %@. Date: %@", name, date];

这个方法中的参数是可以任意变化的,参数的第一项是需要格式化的字符串,后面的参数都是向第一个参数中填空。在这里我们不再详细描述 Objective-C 中可变参数函数的写法 (毕竟这是一本 Swift 的书),但是我相信绝大多数即使有着几年 Objective-C 经验的读者,也很难在不查阅资料的前提下正确写出一个接受可变参数的函数。

但是这一切在 Swift 中得到了前所未有的简化。现在,写一个可变参数的函数只需要在声明参数时在类型后面加上 ... 就可以了。比如下面就声明了一个接受可变参数的 Int 累加函数:

func sum(input: Int...) -> Int {
    //...
}

输入的 input 在函数体内部将被作为数组 [Int] 来使用,让我们来完成上面的方法吧。当然你可以用传统的 for...in 做累加,但是这里我们选择了一种看起来更 Swift 的方式:

func sum(input: Int...) -> Int {
    return input.reduce(0, combine: +)
}

print(sum(1,2,3,4,5))
// 输出:15

Swift 的可变参数十分灵活,在其他很多语言中,因为编译器和语言自身语法特性的限制,在使用可变参数时往往可变参数只能作为方法中的最后一个参数来使用,而不能先声明一个可变参数,然后再声明其他参数。这是很容易理解的,因为编译器将不知道输入的参数应该从哪里截断。这个限制在 Swift 中是不存在的,因为我们会对方法的参数进行命名,所以我们可以随意地放置可变参数的位置,而不必拘泥于最后一个参数:

func myFunc(numbers: Int..., string: String) {
    numbers.forEach {
        for i in 0..<$0 {
            print("\(i + 1): \(string)")
        }
    }
}

myFunc(1, 2, 3, string: "hello")
// 输出:
// 1: hello
// 1: hello
// 2: hello
// 1: hello
// 2: hello
// 3: hello

限制自然是有的,比如在同一个方法中只能有一个参数是可变的,比如可变参数都必须是同一种类型的等。对于后一个限制,当我们想要同时传入多个类型的参数时就需要做一些变通。比如最开始提到的 -stringWithFormat: 方法。可变参数列表的第一个元素是等待格式化的字符串,在 Swift 中这会对应一个 String 类型,而剩下的参数应该可以是对应格式化标准的任意类型。一种解决方法是使用 Any 作为参数类型,然后对接收到的数组的首个元素进行特殊处理。不过因为 Swift 提供了使用下划线 _ 来作为参数的外部标签,来使调用时不再需要加上参数名字。我们可以利用这个特性,在声明方法时就指定第一个参数为一个字符串,然后跟一个匿名的参数列表,这样在写起来的时候就 “好像” 是所有参数都是在同一个参数列表中进行的处理,会好看很多。比如 Swift 的 NSString 格式化的声明就是这样处理的:

extension NSString {
    convenience init(format: NSString, _ args: CVarArgType...)
    //...
}

调用的时候就和在 Objective-C 时几乎一样了,非常方便:

let name = "Tom"
let date = NSDate()
let string = NSString(format: "Hello %@. Date: %@", name, date)