Martin

思想空间

我是Martin,iOS / Android 开发者。


欢迎来到我的思想空间

iOS开发tips

打log

在 Xcode 的 Build Setting 中,在 Other Swift flags 的 Debug 栏中加入 -D DEBUG 即可加入一个编译标识。

Other Swift flags

添加方法:

/// 打log方法,release不会输出
 ///
/// - Parameters:
///   - message: log 文本
///   - fileName: 文件名,有默认值
///   - methodName: 方法名,有默认值
///   - lineNumber: 行数,有默认值
public func DebugLog<T>(item: T, fileName: String = #file, methodName: String =  #function, lineNumber: Int = #line)
{
    #if DEBUG
    let str : String = ((fileName as NSString).pathComponents.last! as NSString).replacingOccurrences(of: "swift", with: "")
    print("\(str)\(methodName)[\(lineNumber)]:\(item)")
    #endif
}

测试,在didFinishLaunchingWithOptions打印各种情况:

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

    DebugLog(item: "输出字符串")
    DebugLog(item: ["arr1", "arr2"])
    DebugLog(item: ["key" : "value"])
    DebugLog(item: testLog())
    return true
}

func testLog() -> String {
    DebugLog(message: "test")
    return "testResult"
}       

输出结果:

AppDelegate.application(_:didFinishLaunchingWithOptions:)[20]:输出字符串
AppDelegate.application(_:didFinishLaunchingWithOptions:)[21]:["arr1", "arr2"]
AppDelegate.application(_:didFinishLaunchingWithOptions:)[22]:["key": "value"]
AppDelegate.testLog()[28]:test
AppDelegate.application(_:didFinishLaunchingWithOptions:)[23]:testResult   

注意这里如果你传入的 items 是一个表达式而不是直接的变量的话,这个表达式还是会被先执行求值的。如果这对性能也产生了可测的影响的话,我们最好用 @autoclosure 修饰参数来重新包装 print。这可以将求值运行推迟到方法内部,这样在 Release 时这个求值也会被一并去掉:

func dPrint(@autoclosure item: () -> Any) {
    #if DEBUG
    print(item())
    #endif
}

dPrint(resultFromHeavyWork())
// Release 版本中 resultFromHeavyWork() 不会被执行     

xcode8添加注释快捷键

zhushi

闭包循环引用的解决办法和区别

参考内存管理,WEAK 和 UNOWNED

方法1: OC 的方法

注意weak 的变量在运行时有可能被设置为 nil,weak不能使用 let

//weakSelf -> ViewController? 
//self - ViewController
//'weak' must be a mutable variable, because it may change at runtime
//weak 的变量在运行时有可能被设置为 nil,weak不能使用 let

weak var weakSelf = self
loadData { (result) -> () in
    print(result)
    print(weakSelf)
}       

方法2: Swift 的方法[weak self] - 首选

// [weak self] 表示 闭包中的 self 都是弱引用的,不需要再使用其他的变量
// weak 的变量在运行时有可能被设置为 nil,所以闭包中的 self,都是可选的
loadData { [weak self] (result) -> () in
     // unexpectedly found nil while unwrapping an Optional value
     // ! 强行解包,值不存在抛出异常
     // ? 可选解包,值不存在,给nil发送消息
     print(self?.view)
 }

方法3: [unowned self]

// [unowned self] 和 OC 中的 assign 是一样的,对象释放之后,指针保持不变
// 1> 闭包中不需要考虑解包的问题
// 2> 但是如果 self 释放,会出现野指针访
loadData { [unowned self] (result) -> () in
    // EXC_BREAKPOINT 野指针访问
    print(self.view)
 }
weak 和 unowned 关于两者使用的选择,Apple 给我们的建议是如果能够确定在访问时不会已被释放的话,尽量使用 unowned,如果存在被释放的可能,那就选择用 weak

Swift 关于计算属性的一种用法

今天看了喵神的Kingfisher项目代码,发现一段有趣的代码,给我造成了很大困扰:

public final class Kingfisher<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

/**
 A type that has Kingfisher extensions.
 */
public protocol KingfisherCompatible {
    associatedtype CompatibleType
    var kf: CompatibleType { get }
}

public extension KingfisherCompatible {
    public var kf: Kingfisher<Self> {
        get { return Kingfisher(self) }
    }
}

extension ImageView: KingfisherCompatible { }

没看过其他源码可能不明白是什么意思,我们简化一下:

class A {
    let base: B
    init(_ base: B) {
        self.base = base
    }
    
    deinit {
        print("A dinit")
    } 
}

class B {
    var kf: A {
        get { return A(self) }
    }
    
    deinit {
        print("B dinit")
    }
}    

看到这段代码,我的第一反应是调用b.kf这不会造成循环引用吗? 通过测试发现并不会造成循环引用,这是怎么回事? 通过测试终于发现了其中的道理:

原来每次在调用b.kf时都会生成一个新的A对象,而且是临时变量,在出了大括号后就会被立即释放。B并没有直接持有A的对象。

所以在遇到计算属性时一定要理解好它其中的代码的真正意义。同时这种写法也值得学习。

参考:内存管理,WEAK 和 UNOWNED

关于didset不能再初始化方法调用的问题

Apple’s docs specify that:

willSet and didSet observers are not called when a property is first initialized. They are only called when the property’s value is set outside of an initialization context.

意思是:

class SomeClass {
    var someProperty: AnyObject {
    	didSet {
        	doStuff()
   	 }
   		 }

	init(someProperty: AnyObject) {
    	self.someProperty = someProperty
	}

	func doStuff() {
   	 // do stuff now that someProperty is set
   	 	}
}

doStuff是不会调用的,解决方法也有很多,如:

class SomeClass {
   		 var someProperty: AnyObject! {
   	 didSet {
       	 //do some Stuff
   	 }
  		  }

	init(someProperty: AnyObject) {
    	setSomeProperty(someProperty)
	}

	func setSomeProperty(newValue:AnyObject) {
    	self.someProperty = newValue
	}
}

或者:

public class MyNewType : NSObject {
	public var myRequiredField:Int
	public var myOptionalField:Float? {
    	willSet {
        	if let newValue = newValue {
            	print("I'm going to change to \(newValue)")
        	}
    	}
    	didSet {
        	if let myOptionalField = self.myOptionalField {
            	print("Now I'm \(myOptionalField)")
       	 }
    	}
	}

	override public init() {
    	self.myRequiredField = 1

    	super.init()

    	// Non-defered
    	self.myOptionalField = 6.28

    	// Defered
    	defer {
        	self.myOptionalField = 3.14
   	 }
	}
}

可以参考:Is it possible to allow didSet to be called during initialization in Swift?

xcode 8 Visual Debugging with Xcode

具体可以查看Visual Debugging with Xcode xcode 提供了新的方式可以更快的发现内存问题等。

这里记录下导出Memory Graph的方法:可以在启动Memory Graph后,选择 file->Export Memory Graph导出文件。

Swift 选项集(option)写法

在oc中写法如下:

typedef enum {
	NSCaseInsensitiveSearch = 1,
	NSLiteralSearch = 2,
	NSBackwardsSearch = 4,
	NSAnchoredSearch = 8,
	NSNumericSearch = 64,
	NSDiacriticInsensitiveSearch = 128,
	NSWidthInsensitiveSearch = 256,
	NSForcedOrderingSearch = 512,
	NSRegularExpressionSearch = 1024
} NSStringCompareOptions;

使用:

NSStringCompareOptions options = NSCaseInsensitiveSearch | NSBackwardsSearch;
// → 5 (= 1 + 4)	

在Swift中写法如下:

struct Sports: OptionSet {
    let rawValue: Int

    static let running = Sports(rawValue: 1)
    static let cycling = Sports(rawValue: 2)
    static let swimming = Sports(rawValue: 4)
    static let fencing = Sports(rawValue: 8)
    static let shooting = Sports(rawValue: 32)
    static let horseJumping = Sports(rawValue: 512)
}	

使用:

let triathlon: Sports = [.swimming, .cycling, .running]
triathlon.contains(.swimming)  // → true
triathlon.contains(.fencing)   // → false	 注意赋值需要 2 的整数次幂按照升序赋给你的选项,也可以使用左移 1 << 0 这种写法	 #### 讨论:当您使用中括号将选项集 (option set) 括起来之后,您将得到的是一个新的选项集。这意味着在 Swift 当中,[.foo] 是等同于 .foo 的。

当代码当中需要传递一个选项集的时候,那么就应该让该参数看起来像一个选项集,所以建议在写选项集参数时,即使只有一个值也加上[]。

Swift 关联对象

Swift 扩展只能添加计算属性。然而如果你愿意用 Objective-C 的关联对象,你可以在 Swift 中做一些同样有趣的事。

推荐一种写法如下:

import Foundation
func associatedObject<ValueType: AnyObject>(
        base: AnyObject, 
        key: UnsafePointer<UInt8>, 
        initialiser: () -> ValueType) 
        -> ValueType {
    if let associated = objc_getAssociatedObject(base, key) 
        as? ValueType { return associated }
    let associated = initialiser()
    objc_setAssociatedObject(base, key, associated, 
                             .OBJC_ASSOCIATION_RETAIN)
    return associated
}
func associateObject<ValueType: AnyObject>(
        base: AnyObject, 
        key: UnsafePointer<UInt8>, 
        value: ValueType) {
    objc_setAssociatedObject(base, key, value, 
                             .OBJC_ASSOCIATION_RETAIN)
}

函数就那么长。你可以这样使用它们:

class Miller {} // 这是我们要扩展的类
class Cat { // 每个磨坊主都有一只猫
    var name = “Puss”
}
private var catKey: UInt8 = 0 // 我们还是需要这样的模板
extension Miller {
    var cat: Cat { // cat「实际上」是一个存储属性
        get { 
            return associatedObject(self, key: &catKey) 
                { return Cat() } // 设置变量的初始值
        }
        set { associateObject(self, key: &catKey, value: newValue) }
    }
}	

NSStringFromClass 替代方案

在自定义 TableViewCell 时,我们一般用类名作为 cell 的 identifier,在oc时我们用NSStringFromClass来获取类名字符串,但是swift没有相应的方法,但swift提供了更优雅的方式:

swift2 时:

// This now works!!!
String(MyTableViewCell)	

swift3以后:

// This now works!!!
String(describing: MyTableViewCell.self)	

使用闭包初始化

声明常量后,在一个紧接着的闭包中进行初始化,而不是之后在 viewDidLoad 或其他类似的方法中进行设置,这在 Swift 中是很常见的写法(也确实是一种不错的写法!)。

let purpleView: UIView = {
    // 在此初始化 view
    // 直接叫 "view" 真的好吗?
    let view = UIView()
    view.backgroundColor = .purpleColor()
    return view
}()	

但上面的代码并不好看,闭包中的view该如何命名?

下面推荐另一种写法

let yellowView: UIView = {
    $0.backgroundColor = .yellowColor()
    return $0
}(UIView())	// 确保这一行的括号内要传入 UIView()

这相当于一个方法传入一个 UIView() 参数,之后在对这个UIview对象进行设置,我们不需要显示地再命名一个变量

闭包捕获

先写个延迟方法

func delay(seconds: Int, closure: ()->()) {
  let time = DispatchTime.now() + .seconds(seconds)
  DispatchQueue.main.after(when: time) {
    print("🕑")
    closure()
  }
}

写一个正常的闭包:

func demo4() {
  var value = 42
  print("before closure: \(value)")
  delay(1) {
    print("inside closure 1, before change: \(value)")
    value = 1337
    print("inside closure 1, after change: \(value)")
  }
  delay(2) {
    print("inside closure 2: \(value)")
  }
}

输出结果如下:

before closure: 42
🕑
inside closure 1, before change: 42
inside closure 1, after change: 1337
🕑
inside closure 2: 1337	

上面的代码说明:

  • 在 Swift 闭包中使用的所有外部变量,闭包会自动捕获这些变量的引用
  • 在闭包执行时,会根据这些变量引用得到所对应的具体值
  • 因为我们捕获的是变量的引用(而不是变量自身的值),所以你可以在闭包内部修改变量的值(当然变量要声明为 var,而不能是 let)

如果想要在闭包创建时捕获变量的值,而不是在闭包执行时才去获取变量的值,你可以使用 捕获列表

func demo5() {
  var value = 42
  print("before closure: \(value)")
  delay(1) { [constValue = value] in
    print("inside closure: \(constValue)")
  }
  value = 1337
  print("after closure: \(value)")
}

输出结果:

before closure: 42
after closure: 1337
🕑
inside closure: 42	

与上面的 demo4() 比较,这次闭包打印的是变量创建时的值,而不是后来赋的新值 1337,即使整个闭包的执行是在对变量重新赋值之后。

这就是 [constValue = value] 在闭包中所做的事情:在闭包创建时捕获变量 value 的内容 — 而不是变量的引用。

上面说明:

  • 你可以在闭包创建时获取变量中的值,然后把它存储到本地常量中,而不是捕获变量的引用。我们可以使用带中括号的捕获列表来实现。

阅读swift源码

参考:如何阅读 Swift 标准库中的源码

总结:根据官方readme从源码编译swift,当 build 构建完成后,你可以在 ./build/Xcode-ReleaseAssert/swift-macosx-x86_64/ 的子文件夹中找到结果。其中会有一个 Swift.xcodeproj Xcode 项目。

设置status bar style

在info.plist文件中添加一个属性,选中表空白地方,右键选中Add Row, 然后在列表中选中最后一个选项,也就是View controller-based status bar appearance,设置为No(不支持在文件中设置状态栏样式),然后在AppDelegate中写上[UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent,这样的话,全局的状态栏样式就OK了。其他的界面如果想要改变样式的话,直接用 [UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleDefault;修改就可以了。

最近的文章

关于屏幕旋转的测试

swift 3 关于屏幕旋转的测试开发中有这么个需求:主页面是竖屏显示,进入一个类似于播放视频的子页面需要横屏显示,退出到主页面,恢复竖屏参考:小胖说swift11——– ios 进入某个VC强转为横屏,出VC后复原下面记录实现过程:首先创建项目,配置没做任何修改,基本结构如下:在Appdelegate中添加代码:var orientationPortrait = true //记录是否为竖屏func application(_ application: UIApplication, s...…

总结知识管理继续阅读
更早的文章

swift 类型转换笔记

swift 类型转换笔记AnyObject为了便于理解,你可以认为AnyObject是一种“type”(实际上并不是type),就像Double,Array等,AnyObject主要用于兼容现有的Objective-C API和iOS代码,swift不能用AnyObject构建自己的数据结构,swift是强类型语言,并有类型推断的特性。什么是AnyObject?在哪会看到AnyObject?AnyObject的意思是指向一个对象的指针,也就是一个对象的实例,但你不知道它是什么class,所...…

总结知识管理继续阅读