美文网首页swiftswift
Swift高阶-从Swift3.3到Swift4.1

Swift高阶-从Swift3.3到Swift4.1

作者: 6ed2651eb5b0 | 来源:发表于2018-07-02 20:16 被阅读403次

    最近把公司的项目从Swift3.3升级到了4.1,相对于Swift2.x到Swift3而言,这次升级所做工作量并不是特别大,但仍旧是碰到了些问题。当我们在XCode里把Swift语言版本选择到4.1然后编译时,错误数总共是999+,那一刻,我和同事的心情是崩溃的,接下来我们就开始了填坑工作。所以,在此记录下我们项目中为了适配Swift4.1改动过的地方。

    一.去OC化:减少隐式@objc自动推断

    问题1.1: dynamic var xxx must also be @objc,insert @objc

    • 问题描述:项目中凡是有dynamic的地方,编译器都给出了可fixed的错误:dynamic var xxx must also be @objc,insert @objc
    • 解决办法
      • 1.点击编译器的红点,让其自动更正。编译器会自动在dynamic前加@objc
        1. 删掉dynamic
    • 原因分析
      之前为了在Swift项目中可以使用热修复JSPatch,所以在每个属性前面都加了dynamic,后来JSPatch被苹果封杀,项目中的dynamic一直保留着,直到Swift4.0开始出现了问题。那么为什么dynamic在Swift4.0之前没事,在4.0之后编译器就报错了呢?

    知识点去OC化是Swift4.0以来最大的变化,这也标志着Swift开始摆脱OC,朝着自己的方向去发展。 在Swift4.0之前,凡是继承了NSObject的类,编译器在编译时会自动在该类所有的属性,方法前加上@objc,供OC代码进行调用。但是在Swift4.0及之后,即使继承于NSObject的类编译器也不会自动的给该类所有的属性,方法前添加@objc。所以,对于我们想让OC代码访问的属性、方法,我们需要手动的在其前面加@objc。显然,dynamic是桥接OC的东西,编译器不自动添加@objc,只有我们手动的去添加了。如果项目中不再使用dynamic,可以直接将其删除。那么,问题又来了:苹果为什么这样做?为什么不让编译器自动的加@objc呢?

    在Swfit4.0之前Swift中凡是继承了NSObject的类,会自动为该类的属性、方法前加上@objc,private修饰的除外,而这样也就可能导致大量不需要暴露给OC代码的属性、方法前加上@objc,大量的@objc也会导致最终二进制文件包增大。在这个OC向Swift过渡已差不多的时刻,哪里需要在哪里加,也显得更为合理。

    问题1.2: Argument of '#selector' refers to instance method 'xxx()' that is not exposed to Objective-C Add '@objc' to expose this instance method to Objective-C

    • 问题描述:使用#selector调用方法的地方,基本都报上述可fixed问题
    • 解决办法: 点击红点,让其自动修复即可。会发现,编译器自动在selector方法前加了@objc
    • 原因分析:
      彻底明白问题1的原因的同学,应该已经知道了这个问题的原因。selector仍旧是属于OC的东西,我们要调用OC的方法,必须要在方法前加@objc。

    问题1.3:Cannot override a non-dynamic class declaration from an extension

    • 问题描述:在Swif4中,如果子类要在extension中重写父类的方法,那么在子类这个方法那里就会报错。在Swift4之前只是警告。
    • 解决办法: 将要被重写的父类的方法前加@objc dynamic
    • 原因分析: to be honest, 对于这个问题的原因,我并没有彻底理解透。先给出我google出的解释(该解释针对Swift4之前),再给出我自己的理解,大家也可以针对此问题留言,一起讨论。

    问题1.4: 方法的调用使用NSSelecrotFromString("")导致项目崩溃

    • 问题描述:在我们项目可以成功在swift4.1下跑起来之后,我和同事以为适配工作告一段落,哪知,我们仅仅完成了第一步。在登录界面 ,点击UITextField右边的x号,程序崩了。后来尝试点击登录按钮,也崩了。顿时,刀郎的一句歌词在我心里飘过“它来的那么快来的那么直接。。。”,查原因吧!
    • 解决办法: 又在一些方法前面加上了@objc
    • 原因分析: 项目中某些方法的调用是通过NSSeletorFromString(""),虽然问题的本质跟问题1是一样的,但是这种方式编译器是不会给提示错误的,只在程序运行起来之后,调用不到相关方法,所以导致崩溃。

    问题1.5: 在Swift4下,MJExtension这个字典转模型框架不好使了,转出的模型的属性值都是空的。

    • 问题描述:程序跑起来,也解决了问题4后,顿时觉得有点扬眉吐气了,当我们点击登录按钮时,发现“哎呀,出错了,等会再试吧”。心里又开始凉凉了,接口怎么搞的,账号问题?换账号,不好使。好吧。心里好多个"草泥马"飘过。连上Xcode,查原因吧。什么?控制台打印“用户名,密码不能为空”,明明有值啊。“威总威总(我们测试),来个线上包”,用线上包一登录,没事。问题很明朗了,Swift4的事,查原因吧。
    • 解决办法:
      • 如果仍旧使用MJExtension的话,需要在对应的模型前加@objcMemebers(推荐)
      • 如果不打算使用MJExtension的话,可以使用苹果的Codable协议进行转模型(不建议,坑比较多)或者使用一些纯Swfit的框架,比如HandyJson(阿里的),SwfityJson。(补充:Codable协议和HandyJson,SwfityJson我们都进行了调研,都不太推荐。Codable协议存在一些问题,比如模型和接口返回的字段类型不匹配会导致转换无值,模型中使用与接口返回字段不一致的命名比较麻烦等;HandyJSON确实牛逼,直接通过操作内存地址来获取属性(自我理解),但是看网上说容易导致内存泄露。而且我也联系了MJ本人,他说在等Swfit5,所以我们也就采用了妥协的办法,使用了方案1)。。。。。哦,打字好累
    • 原因分析: 此问题发生的根本原因还是在Swift4.0之后,即使继承于NSObject的类,编译器也不会自动的在该类的属性、方法前加@objc。而MJExtension的底层是通过运行时方法来获得模型的属性和属性类型,如果该模型不暴露给OC,MJExtension是无法成功获取模型的属性的,导致转出的模型里的属性值都没有值。在我们项目中,传递给接口的参数用户名和密码是在一个loginModel里取的,而loginModel是通过一个字典利用MJExtension转来的,由于loginModel并没有添加@objcMembers或者@objc,所以导致转出的模型里的值为空,也就意味着传递给接口的参数用户名和密码确实空,问题彻底排查。MJExtension在swift4.0下字典转模型失败的问题,还会引发很多问题,因为我们iOS开发基本采用MVC原则,把AFN解析出的字典或者数组进一步转成模型,一旦转出的模型里没有值,会导致界面上无数据等一些列问题。

    二.新增Substring类型

    • 问题描述:'substring(to:)' is deprecated: Please use String slicing subscript with a 'partial range upto' operator.(这是在Swfit4.0下新增的一个警告,并非报错。什么?这么常用的substring方法都被废弃了,苹果爸爸,你要搞什么呀?)

    • 解决办法: 我在项目中使用现在苹果提倡的方式对substring 进行了进一步封装,消除了警告。并且提供了几个更加方便的方法。代码如下:

      // compatibility for Swift4.0
      // tk_substring 这三个方法是为了消除3.2->4的警告。开发中不建议使用这三个,建议使用下面的sbustring方法
      func tk_substring(from index:String.Index) -> String{
            return String(self[index...])
       }
      
        func tk_substring(to index:String.Index) -> String {
        return String(self[..<index])
        }
      
        func tk_substring(with range: Range<String.Index>) -> String {
        return String(self[range.lowerBound..<range.upperBound])
        }
      
        // 开发建议使用已下方法,无需获取String.Index.简单快捷且容错性强
        func substring(from: Int?, to: Int?) -> String {
            if let start = from {
            guard start < self.count else {
                return ""
            }
        }
        
        if let end = to {
            guard end >= 0 else {
                return ""
            }
        }
        
        if let start = from, let end = to {
            guard end - start >= 0 else {
                return ""
            }
        }
        
        let startIndex: String.Index
        if let start = from, start >= 0 {
            startIndex = self.index(self.startIndex, offsetBy: start)
        } else {
            startIndex = self.startIndex
        }
        
        let endIndex: String.Index
        if let end = to, end >= 0, end < self.count {
            endIndex = self.index(self.startIndex, offsetBy: end + 1)
        } else {
            endIndex = self.endIndex
        }
        return String(self[startIndex ..< endIndex])
      }
      
      func substring(from: Int) -> String {
        return self.substring(from: from, to: nil)
      }
      
        func substring(to: Int) -> String {
        return self.substring(from: nil, to: to)
      }
      
      func substring(from: Int?, length: Int) -> String {
        guard length > 0 else {
            return ""
        }
        
        let end: Int
        if let start = from, start > 0 {
            end = start + length - 1
        } else {
            end = length - 1
        }
        return self.substring(from: from, to: end)
      }
      
      func substring(length: Int, to: Int?) -> String {
        guard let end = to, end > 0, length > 0 else {
            return ""
        }
        
        let start: Int
        if let end = to, end - length > 0 {
            start = end - length + 1
        } else {
            start = 0
        }
        return self.substring(from: start, to: to)
      }
      
    • 原因分析:
      正如警告信息那样,苹果建议使用字符串切片下标的方式。
      具体使用形式:https://majing.io/posts/10000001371152
      关于Substring苹果官方解释:https://developer.apple.com/documentation/swift/substring

    三.实现协议中的属性类型必须完全匹配

    • 问题描述: 在点击界面时,发现某些接口调用失败,但是在线上的包却无此问题,说明不是接口的问题。进一步排查发下,是某些传递给接口的参数少了。Swift4还能导致这种问题?肯定是哪里出了问题,查原因吧。

    • 解决办法: 实现协议中的属性类型改成与协议中的属性类型完全一致(这句话确实有些别扭,看原因分析中的Demo演示)

    • 原因分析:先看代码

        @objc protocol myProtocol {
                @objc optional var params:[String : Any]? {get set}
        }
         class Model: NSObject,myProtocol {
                var params: [String : String]?
                var name: String?
        }
      

    是否有看出这段代码的问题。Model遵守了myProtocol协议,并且实现了协议中的属性params。但是Model里params的类型是[String : String]?,而协议里myProtocol的类型却是[String : Any]? ,就这么一个类型之差,直接导致我们项目中请求接口不能成功。那么为什么呢?在swift4.0之前,在model里的params处会给出警告,警告信息是说你与协议中属性类型不一致,你到底是要新定义个属性还是实现协议里的属性啊,在swift4之前,编译器会默认认为是实现协议里的属性,虽然类型不一样,仅是给你个警告。但是在swift4之后,一向擅长变脸的苹果爸爸变了,在swift4中如果你这样写,编译器会默认为你定义了个新的属性,与协议中的属性无关。注意:协议中的属性是可选的,遵守了协议可以选择不实现这个属性。那么,这种不一致会导致什么问题呢?看Demo

     override func viewDidLoad() {
          super.viewDidLoad()
           let m = Model()
          m.params = ["t1":"123"]
          test(m)
      }
    
    func test(_ model:myProtocol) {
        if let _ = model.params {
            print("++")
            return
        }
         print("--")
    }
    

    下面这段代码为对模型Model的使用。运行结果,在swift4之前,打印++,在swift之后,打印--。即在swift4之前,model.params不为nil,在swift4之后,model.params为nil。至于原因就是,在swift4之前,model.params取的是model里的值,而在swift4之后,model.params取的是myProtocol里的值,因为编译器不认为model实现了协议里的属性。(可能有些绕,我是彻底理解了,大家可以多读几遍)

    四.在Swift4下,带一个Void参数的闭包类型,调用时可能报错Missing argument for parameter #1 in call

    • 问题描述: 这个问题是比较明显的,在swfit4下,编译直接给出错误

    • 解决办法: 调用时传入元组() 。 (又比较抽象,看原因分析中的Demo)

    • 原因分析: 看Demo

      var success1:((Void)->Void)?
      var success2:(()->Void)?
      
      override func viewDidLoad() {
        super.viewDidLoad()
        success1!()
      }
      

    上述代码在swift4下编译,在success1!()处会报错,提示让传入一个参数。解决办法就是这样调用 success1!(()),或者按照success2的方式来定义success1。而success1!()这样调用在swift4之前是没事的。原因就是在swift4之后,苹果比之前更加严格的对待元组。(Void)->Void 代表一种闭包类型,这个闭包有一个Void类型的参数,并且返回值是Void,代表没有返回值。这也就是报Missing argument for parameter #1 in call错的原因,我需要一个参数,但是你没给传参数啊,所以编译器就报错了。好吧,我给你传个Void类型的参数,所以传(),因为空的元组()是Void的唯一实例,所以解决办法就是success1!(())。注意:(Void)->Void和()->Void是两种不同的类型,如果想定义一个不带参数的闭包类型,更推荐后一种方式。参考地址:https://stackoverflow.com/questions/45183961/compile-error-in-swift-4-on-parameter-passing

    五.Swift4其他部分特性

    5.1 NSAttributedStringKey

    • NSForegroundColorAttributeName使用NSAttributedStringKey.foregroundColor替代
    • NSParagraphStyleAttributeName使用NSAttributedStringKey.paragraphStyle替代
    • NSFontAttributeName使用NSAttributedStringKey.font替代
    • ...

    5.2 extension中可以访问类里的private属性

    5.3 initialize方法在Swift4.0中报错

    override class func initialize() {
        // some code
    }
    

    以上代码在Swift3中报警告,在swift4中直接报错:Method 'initialize()' defines Objective-C class method 'initialize', which is not permitted by Swift

    总结

    上述所有问题除了5.2,5.3在我们项目中没有碰到外,其余的问题均在我们项目中遇到。我们在解决问题的同时,也在思考苹果为什么要这么变。有时解决问题就是点击下鼠标的事,但是思考原因就没那么easy了。而且,自我觉得只有针对问题去解决问题,才是学习的最好的方式。如果仅仅是干巴巴的学习Swift4新特性,效果不如碰到问题解决问题更好。希望这篇文章能够帮助到做Swift4适配的小伙伴。

    相关文章

      网友评论

        本文标题:Swift高阶-从Swift3.3到Swift4.1

        本文链接:https://www.haomeiwen.com/subject/zdqluftx.html