SwiftiOS开发iOS Developer

学习Swift的一些Tips

2017-07-05  本文已影响192人  Wang66

前言:

最近几天在看《100个Swift必备tips》这本书,本篇为读书笔记,以作总结记录用。


protocol的方法声明为mutating:

swift里,protocol中定义的方法既可以被class实现,也可以被structenum来实现。但若是被后两者实现,默认情况下,实现的方法内部是无法更改structenum变量。若你将要定义的协议可能需要被structenum实现,请在定义协议方法时,加上关键字mutating

为方便起见,若定义的协议需要被structenum实现,建议在协议中定义方法时均添加mutating关键字。无论你定义的方法需不需要更改变量。


Sequence 协议:

Swift 中的 Sequence(一)
Swift 中的 Sequence(二)


元组Tuple:

让我们想想,在C或OC中我们想让函数返回多个值时,应该怎么实现?你可以将多个返回值拼装成一个DictionaryModel再返回。你还可以使用“指针类型参数”来传递值。

    CGRect small;
    CGRect large;
    CGRectDivide(self.view.bounds, &small, &large, 20, CGRectMinXEdge);

Swift中,就变得很简单了,只需返回一个Tuple就可以了。对于元组里的元素,既可以像数组那样通过下标来访问,也可以像字典那样通过key来访问。


可选链:

像下面的代码,即便Toy类中的name属性不是可选型的,但是最终获取到的结果toyName仍然是可选的,因为在整个调用链条中pet,toy都是可选的,都有可能为nil,而提前中止调用链条,返回nil
既然返回了nil,就说明结果是可选的,是String?型的。因此,我们要用if let语句来解包。

if let toyName = xiaoming.pet?.toy?.name {
}

使参数可变 inout:

Swift中,方法参数默认是不可变的,不能在方法内部修改参数的值。
若你真的想直接修改参数的值,则需用inout关键字来修饰参数。 如此,则在方法内部就可以直接修改参数了,且inout让参数具有了“传址调用”的能力,因此在调用时要在参数名前加&

下面代码定义了一个将变量重置为0的方法reset

    func reset(x: inout Int) {
        x = 0;
    }
        var x = 5
        self.reset(x: &x)
        print("result:x=\(x)")
        // log:   result:x=0

自定义下标 subscript:

在编程语言中,都可以以下标来访问ArrayDictionary等集合对象。
Swift中可就更厉害了,它支持给任何类、结构体和枚举自定义下标,使它们都可以以下标的形式被访问。
无论是项目中你自己定义的类,还是原先就有下标的集合类,你都可以自定义下标访问它们!

自定义下标,用subscript关键字,非常类似用关键字init自定义构造器。

比如下面的代码,我们拓展了Array的下标,使其下标可以为数组,读写任意位置的元素。

extension Array {
    
    subscript(indexs: [Int]) -> [Element] {
        
        get {
            var resultArr = [Element]()
            for index in indexs {
                assert(index<self.count, "index out of range")
                resultArr.append(self[index])
            }
            return resultArr
        }
        
        set {
            
            for (index, i) in indexs.enumerated() {
                assert(i<self.count, "index out of range")
                self[i] = newValue[index]
            }
        }
    }
}

        var array = [1, 2, 3, 4, 5, 6]
        let resultArr = array[[0,1,2]] // 获取下标[1,2,3]的值
        print(resultArr)
        // log ————> [1,2,3]
        
        array[[0,1,2]] = [0,0,0] // 修改下标[1,2,3]的值
        print(array)
        // log ————> [0,0,0,4,5,6]

Any 和 AnyObject:

简单来说,AnyObject表示任何class类型;
Any表示任何类型,包括任何classstructenum类型。

我们知道,在OCid代表任何类型,并也有经常见于Cocoa框架中,而Swift用的框架仍为Cocoa。为了能完美对接,Swift不得不发明个与OCid相对应的类型,这便是AnyObject的来历。(AnyObject其实是个协议,Swift中的所有class均实现了该协议。)

但是很遗憾的是,Swift中的所有的基本类型,包括ArrayDictionary这些在OC中为class的东西,统统都是struct

所以,苹果就又新增了更特殊,更强大,能代表任何类型的Any


多类型和容器:

按理说,数组中存入的元素都应该是类型相同的。但是在Swift中有AnyAnyObject这两个很特殊的,代表任何类型的类型。试想:若我们在定义数组时,将数组元素申明为AnyAnyObject,是不是就可以存入不同类型的元素了。不管是Int还是String,它们也是AnyAnyObject类型啊。

let array: [Any] = [1, "string"]

这样确实是可以的,但这样是极不安全的,我们将其以Any的方式存入数组,是一种类型提升,这样会丢失很多原本具有的数据。若你从该数组中取出string,它便不具备原本String具有的能力了,包括属性和方法等。此时,若你调用这些属性或方法的话,程序就会出问题。

其实,数组中存入的元素,并不一定必须得是相同类型的,也可以是实现同一协议的不同类型。

像下面这样,因为IntString本身都是实现了CustomStringConvertible协议的。我们可以将数组申明为『实现了CustomStringConvertible协议』的类型。

        let array: [CustomStringConvertible] = [1, "string"]
        
        for item in array {
            print(item.description);
        }

public protocol CustomStringConvertible {

    public var description: String { get }
}

还有另一种做法是使用enum可以带有值的特点,将类型信息封装到特定的enum中。下面定义了一个枚举AnyType

enum AnyType {
        case IntType(Int)
        case StringType(String)
    }
let mixedArr = [AnyType.IntType(1), AnyType.StringType("string")]
        
        for item in mixedArr {
            switch item {
            case let .IntType(i):
                print(i)
            case let .StringType(str):
                print(str)
            }
        }
        

属性观察:

Swift让属性观察这件事变得异常简单。下面代码观察了People类的count属性,若其值小于0,则打印提示信息。

class People: NSObject {

    var count: Int {
        willSet {
            print("count----willSet")
        }
        didSet {
            print("count----didSet")
            if(count<0){
                print("count不能小于0")
            }
        }
    }
    
    override init() {
        count = 0
    }
}

let people = People()
people.count = -10

需要注意的是,Swift中的属性有『存储属性』和『计算属性』之分。前者会在内存中实际分配地址来存储属性,而后者并不是实际存于内存的,它只是通过提供getset两个方法来访问,读写该属性(它和普通的方法很类似)。

Swift中的『计算型属性』是无法使用willSet/didSet属性观察的,也就是说在定义属性时,不能同时出现setwillSet/didSet。因为要监听计算型属性,我们完全可以在set里面添加监听属性的处理代码。

有时,我们往往需要观察一个属性,但是,这个属性是别人定义的,我们不便直接在源代码里写didSet用以监听。此时,子类化该类,重写其属性,在重写中对该属性添加didSet监听是个很好的解决方案。
更值得说的是: 子类化该类,重写其属性,然后添加didSet监听,竟然可以用在父类是计算型属性的情况下。也就是说,这也是解决Swift中计算型属性无法写didSet监听的一种方案了,即把didSet写到子类重写的属性中去。


单例的最正确写法:

用两个关键字static let修饰,可以限制sharedInstance是全局且不可变的,这就有了『单例』的意思了。但是还不够,还要堵住其他构造途径,防止调用其他构造器创建实例,所以在原本的init方法前加上了private关键字。

class UserInfoManager: NSObject {
    static let sharedInstance = UserInfoManager()
    private override init() {}
}
let userInfo = UserInfoManager.sharedInstance

@UIApplicationMain:

对于一个Objective-C的iOS项目,Xcode会自动帮我们生成一个main.m文件,其中有个main函数。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

main函数里调用了UIKitUIApplicationMain方法,这个方法根据第三个参数初始化一个UIApplication或者子类的对象并开始接收事件(默认传入的是nil,意味使用默认的UIApplication)。最后一个参数指定了AppDelegate类作为委托,用来接收didFinishLaunchingWithOptions:applicationDidEnterBackground:等有关程序生命周期的方法。

后两个参数都可以传入自定义的子类来完成更加个性化的需求。

另外,main函数虽然标明返回一个int,但是它不会真正返回。它会一直存在于内存中,直到用户或系统将其强制终止。

..... 现在,我们来看看在Swift中情况是怎样的。

可以看到,在Swift的iOS项目中找不到main.m文件,但是却在AppDelegate类开头多了个@UIApplicationMain标签。其实,这个标签的作用就是上面所说的main函数的作用:
初始化一个默认为UIApplication类的实例来接收事件,指定AppDelegate为委托来接收程序生命周期方法。

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }
...
...
}

所以说,Swift的iOS项目中,并不是不需要main函数,而是通过添加@UIApplicationMain标签,Xcode帮我们自动处理了main函数的逻辑。

当然,这个标签并不是必须的,若你需要指定子类化的UIApplicationAppDelegate,则可以自己给项目中添加个main.m文件,实现main函数,并指定这两个子类化的实例为参数。


如何动态地获取一个实例的类型:

方法一,若类是继承于NSObject类的,则我们可以利用OC的运行时:

public func object_getClass(_ obj: Any!) -> Swift.AnyClass!
        let people = People()
        let peoType: AnyClass! = object_getClass(people)
        print(peoType)
        // log ----> People

方法二:用一个全局的函数type(of:),如下:

        let people = People()
        let peoType = type(of: people)
        print(peoType)
        // log ----> People

// type(of:)方法是Swift3.0新增的,代替了前期版本中的dynamicType属性(Swift3.0之前,使用该属性可返回该对象的类型)。


自省:

自省:向一个对象发出询问,以确定它是不是属于某个类,这种操作就称为“自省”。

OC中我们是这样判断一个对象是不是属于某个类的:

if([_type isKindOfClass:[NSString class]]){
            
}

Swift中,若该类是继承自NSObject的,则也有上面相应的两个方法:

class People: NSObject {
    
}
        let peo = People()
        if peo.isKind(of: People.self) {
            print("isKind")
        }
        if peo.isMember(of: People.self) {
           print("isMember")
        }

除此外,Swift提供了更强大简洁的关键字is,同样能达判断类型的效果。
而且is的强大在于,它不仅适用于class,在structenum中,它同样是可用的。

        if peo is People {
            print("isKind")
        }

KVO:

Swift中也可以使用KVO,但是仅限于在NSObject子类中。因为KVO是基于KVC和动态派发技术的,而这些都是NSObject运行时的概念。另外,由于Swift为了提高效率,默认禁止了动态派发,因此想用Swift使用KVO的话,还要将被观测的对象标为dynamic

简单来说,Swift要使用KVO有两个限制:
1.被观测的对象属于NSObject类;2.被观测的对象属性被标为dynamic

下面的代码,我们观察People类里的money属性,它即为被观测的对象,所以要将其标为dynamic

class People: NSObject {
    dynamic var money = 0 // 标为dynamic
    
}

peoplemoney属性添加观察,并重写回调方法,在其中拿到改变后的新值。并且,一定要记得在deinit方法中移除观察。

private var myContext = 0

class ViewController: UIViewController {
    
    var people = People()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 添加观察
        people.addObserver(self, forKeyPath: "money", options: .new, context: &myContext)
        people.money = 3
    }
    
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let change = change, context == &myContext{
            let newValue = change[NSKeyValueChangeKey.newKey]
            if let newValue = newValue {
                print(newValue)
            }
        }
    }
    
    deinit {
        people.removeObserver(self, forKeyPath: "money")
    }
  
}

但是,在实际开发过程中,我们往往会遇到不符合上述两点的情况,那在这种情况下,我们还能不能使用KVO呢?
若被观测的对象不继承自NSObject的话,那真的无法使用KVO,只能自己利用“属性观察”自己实现一套观察机制(在didSet里发送通知,通知外界属性值已改变)。
若被观测的对象我们无法直接标为dynamic,比如系统类的对象。那我们可以子类化该类,重写该属性,重写时将其标为dynamic

如下面的例子,假如我们要对Peoplemoney属性进行观测,但是又不方便或不允许将money属性标为dynamic,所以我们新建继承自PeopleChildPeople类,在其中重写money时将其标为dynamic

class People: NSObject {
    var money = 0
    
}

class ChildPeople: People {
    
    dynamic override var money: Int {
        get {
            return super.money
        }
        set {
            super.money = newValue
        }
    }
}

判等:

我们可以实现Equatable中的上面方法,来完成自定义对象判等逻辑。NSObject实现了Equatable协议,若类是继承自NSObject的,则不需我们自己实现该协议。

像下面代码,我们自定义了People对象的判等逻辑:只要俩人的钱一样多,就相等。

class People {
    var money = 0
    
    init(mon: Int){
        money = mon
    }
}

extension People: Equatable {
    
    public static func ==(lhs: People, rhs: People) -> Bool {
        return lhs.money == lhs.money
    }
}

若类仅仅是继承自NSObject,而不自己实现该协议方法,则==操作符默认是NSObject里的实现,即对指针引用对象内存地址的比较。除非俩引用同一块内存地址,否则不相等。

Swift里是用===来比较引用的内存地址是否相等。

下面创建的people1people2内存地址是不同的,所以不会打印东西。

        let people1 = People(mon: 10)
        let people2 = People(mon: 10)
        if people1===people2 {
            print("===")
        }
屏幕快照 2017-07-03 下午3.08.01.png
类簇:

字符串格式化:

OC的字符串可以通过占位符%@/%f/%d等,将多个元素拼接成新的字符串。

    NSString *name = @"wang66";
    NSString *str = [NSString stringWithFormat:@"%@%@", @"name =", name];
    NSLog(@"%@",str);

Swift的字符串中则不需要占位符了,直接可以在字符串中插值。

        let name = "wang66"
        let str = "name = \(name)"
        print(str)
        // log ----> name = wang66

这样确实非常简洁了,但是若是出现将小数保留两位小数的需求时该怎么办呢?,OC中是这样的:

    CGFloat distance = 12.21424;
    NSString *str = [NSString stringWithFormat:@"%0.2f%@", distance,@"km"];
    NSLog(@"%@",str);

但是在Swift中就不能继续简洁了,而是要用``String(format:)

        let distance = 12.26578
        let distanceStr = String(format: "%0.2f", distance)
        let str = "\(distanceStr)km"
        print(str)
//         log ----> 12.27km


Options:

我们从UIView的一个动画方法说起,下面这个执行动画方法有个options参数,意为“选项”,用以配置动画。在OC中是这样的:

    [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionCurveEaseOut animations:^{
        NSLog(@"动画进行中...");
    } completion:^(BOOL finished) {
        NSLog(@"动画完成...");
    }];

options参数是枚举类型的,可以以|将多个枚举值连接,组合使用。UIViewAnimationOptions的定义如下,因为该参数允许组合枚举各值,所以被定义成了支持掩码位移的NS_OPTIONS:(它和NS_ENUM的区别主要是它自动支持掩码位移)

typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews            = 1 <<  0,
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, // turn on user interaction while animating
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, // start all views from current value, not initial value
    UIViewAnimationOptionRepeat                    = 1 <<  3, // repeat animation indefinitely
    UIViewAnimationOptionAutoreverse               = 1 <<  4, // if repeat, run animation back and forth
    UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5, // ignore nested duration
    UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6, // ignore nested curve
    UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7, // animate contents (applies to transitions only)
    UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8, // flip to/from hidden state instead of adding/removing
    UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9, // do not inherit any options or animation type

而在Swift中,options参数的情况却大有不同。与上面OC对应的方法,在Swift中是这样的:

        UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseIn, .curveEaseOut], animations: {
            print("动画进行中...")
        }) { isFinshed in
            print("动画结束...")
        }

Swift的枚举无法支持位移赋值。所以options并不是枚举,而是一个实现OptionSet协议的struct,各个选项值为staticget属性。
要达到像OC中那样多个选项值options|符号组合,在Swift中,我们以集合包装每个选项值。 就像这样:[.curveEaseIn, .curveEaseOut]

public struct UIViewAnimationOptions : OptionSet {

    public init(rawValue: UInt)

    public static var layoutSubviews: UIViewAnimationOptions { get }

    public static var allowUserInteraction: UIViewAnimationOptions { get } // turn on user interaction while animating

    public static var beginFromCurrentState: UIViewAnimationOptions { get } // start all views from current value, not initial value

    public static var `repeat`: UIViewAnimationOptions { get } // repeat animation indefinitely

    public static var autoreverse: UIViewAnimationOptions { get } // if repeat, run animation back and forth


weak delegate:

"委托代理"模式在iOS开发中可谓是最常用的模式,我们都知道,为了避免委托和代理对象互相引用,无法释放的问题,我们一般将委托的delegate属性标为weak弱引用,主动示弱,以打破互相引用的僵局。

但是我们在Swift中,我们不能直接在任何一个delegate属性前面加weak关键字修饰。因为Swift中的协议是可以被除了class外的structenum来实现的,而后两者它们是值类型,它们不是通过引用计数规则来管理内存的,所以当然不能以weak,这个ARC里的东西修饰。

所以,在Swift中使用“委托代理模式”,首先要在定义协议时将其申明为只能被class实现。

protocol DemoDelegate: class {
    func demoFunction()
}

或者,还有方案二:将定义的协议用@objc申明为Objective-C的。因为在Objective-C中,协议只能被class实现。

@objc protocol DemoDelegate {
    func demoFunction()
}

Swift 命令行工具:

启动REPL(Read-Eval-Print Loop)环境: 在终端输入:xcrun swift来启动。

屏幕快照 2017-07-05 下午2.00.31.png

然后就可以“交互式编程”了,在终端每输入代码,然后回车,就会实时编译执行。

屏幕快照 2017-07-05 下午2.05.45.png

试试定义一个方法,并调用。可以看到它甚至还可以反馈给我们错误提示,提示我“调用myFunction(5)方法时缺少参数标签num。”

屏幕快照 2017-07-05 下午2.07.14.png
打印对象,自定义description:

我们打印一下People的对象people

        let people = People(name: "wang66", mobile: "18693133051", address: "白石洲")
        print(people)
        // log ----> <LearnAlamofireDemo.People: 0x6080000d6960>

可以看到打印信息只有对象的类型People和内存地址。但是这样的信息几乎没什么用。

打印对象后展示什么信息,我们是可以定制的。

只要这个类实现CustomStringConvertible协议,然后重写description属性,需要展示什么就return什么。

public protocol CustomStringConvertible {

    public var description: String { get }
}

比如,我们在拓展中实现了自定制的description属性:

class People {
    var name: String?
    var mobile: String?
    var address: String?
    
    init(name: String?, mobile: String?, address: String?) {
        self.name = name
        self.mobile = mobile
        self.address = address
    }
}

extension People: CustomStringConvertible {
    var description: String {
        get {
            return "[\(type(of:self)): name=\(self.name ?? "") | mobile=\(self.mobile ?? "") | address=\(self.address ?? "") ]"
        }
    }
    
}

打印结果如下,非常完美。

        let people = People(name: "wang66", mobile: "18693133051", address: "白石洲")
        print(people)
        // log ----> [People: name=wang66 | mobile=18693133051 | address=白石洲 ]

断言 assert:

有时我们写的方法是有数据传入限制的,但是当我写的代码别人调用,或者以后我自己调用时,可能都不熟悉这个方法具体需要传入什么条件的数据,这有可能让大家浪费不必要的时间。最好,在调用传入数据时有反馈提示。

断言,可以很好地解决这个问题。当调用者传入的数据不符合条件时,编译不会通过,且会打印出提示信息。

printYourAge(age: -10)
    func printYourAge(age: Int) {
        assert(age >= 0, "年龄都是正数")
        print(age)
    }
屏幕快照 2017-07-05 下午3.18.13.png
上一篇下一篇

猜你喜欢

热点阅读