SwiftUI/Swift/Objective-C修饰符总结
SwiftUI
@State
修饰的变量是值传递的,所以作用范围在当前视图,变量值变化后会触发相关的View刷新。
@Binding
修饰的变量会变成引用的形式传递,保证持有的值是一致的。
@ObservedObject
和State作用类似,对于变量只是引用,且对象所属的类要遵从ObservableObject协议,要监视的属性用@Published修饰。
其生命周期不由创建它的View所持有,需要开发者自行管理。
@StateObject
与ObservedObject的区别是生命周期由创建它的View所持有,无需开发者手动管理。
@EnvironmentObject
对所有View可见,因此变量可以在任意View之间传递,调用根视图的environmentObject()方法传递变量,以便所有的子视图可以访问它。其所属的类同样要遵从ObservableObject协议。
@Environment
读写环境变量,内置了系统的一些公共变量,也可以自定义。
@AppStorage
@AppStorage 是 SwiftUI 框架提供的一个属性包装器,设计初衷是创建一种在视图中保存和读取 UserDefaults 变量的快捷方法。@AppStorage 在视图中的行为同@State 很类似,其值变化时将导致与其依赖的视图无效并进行重新绘制。
@AppStorage 声明时需要指定在 UserDefaults 中保存的键名称(Key)以及默认值。
@AppStorage("username") var name = "lijianfei.com"
userName为键名称,fatbobman是为username设定的默认值,如果 UserDefaults 中的username已经有值,则使用保存值。
如果不设置默认值,则变量的为可选值类型。
@AppStorage("username") var name:String?
默认情况下使用的是 UserDefaults.standard,也可以指定其他的 UserDefaults。
public extension UserDefaults {
static let shared = UserDefaults(suiteName: "group.com.lijianfei.examples")!
}
@AppStorage("userName",store:UserDefaults.shared) var name = "lijianfei"
对 UserDefaults 操作将直接影响对应的@AppStorage
UserDefaults.standard.set("bob",forKey:"username")
上述代码将更新所有依赖@AppStorage("username")的视图。
更多参考:@AppStorage 研究 | 肘子的Swift记事本
Objc
Swift
@dynamicMemberLookup
这个特性中文可以叫动态查找成员。在使用@dynamicMemberLookup标记了对象后(对象、结构体、枚举、protocol),实现了subscript(dynamicMember member: String)方法后我们就可以访问到对象不存在的属性。如果访问到的属性不存在,就会调用到实现的 subscript(dynamicMember member: String)方法,key 作为 member 传入这个方法。
比如我们声明了一个结构体,没有声明属性。
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> String {
let properties = ["nickname": "Jeffery", "city": "Singapore"]
return properties[member, default: "undefined"]
}
}
//执行以下代码
let p = Person()
print(p.city)
print(p.nickname)
print(p.name)
如果没有声明@dynamicMemberLookup的话,执行的代码肯定会编译失败。很显然作为一门类型安全语言,编译器会告诉你不存在这些属性。但是在声明了@dynamicMemberLookup后,虽然没有定义 city等属性,但是程序会在运行时动态的查找属性的值,调用subscript(dynamicMember member: String)方法来获取值。
而且骚的很,这东西还可以继承。
@available
@available: 可用来标识计算属性、函数、类、协议、结构体、枚举等类型的生命周期。(依赖于特定的平台版本 或 Swift 版本)。它的后面一般跟至少两个参数,参数之间以逗号隔开。其中第一个参数是固定的,代表着平台和语言,可选值有以下这几个:
iOS
iOSApplicationExtension
macOS
macOSApplicationExtension
watchOS
watchOSApplicationExtension
tvOS
tvOSApplicationExtension
swift
可以使用*指代支持所有这些平台。
有一个我们常用的例子,当需要关闭ScrollView的自动调整inset功能时:
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
automaticallyAdjustsScrollViewInsets = false
}
还有一种用法是放在函数、结构体、枚举、类或者协议的前面,表示当前类型仅适用于某一平台:
@available(iOS 12.0, *)
func adjustDarkMode() {
/* code */
}
@available(iOS 12.0, *)
struct DarkModeConfig {
/* code */
}
@available(iOS 12.0, *)
protocol DarkModeTheme {
/* code */
}
版本和平台的限定可以写多个:
@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
public func applying(_ difference: CollectionDifference<Element>) -> ArraySlice<Element>?
注意:作为条件语句的available前面是#,作为标记位时是@
刚才说了,available后面参数至少要有两个,后面的可选参数这些:
deprecated:从指定平台标记为过期,可以指定版本号
obsoleted=版本号:从指定平台某个版本开始废弃(注意弃用的区别,deprecated是还可以继续使用,只不过是不推荐了,obsoleted是调用就会编译错误)该声明
message=信息内容:给出一些附加信息
unavailable:指定平台上是无效的
renamed=新名字:重命名声明
它的含义是针对swift语言,该方式在swift4.1版本之后标记为过期,对应该函数的新名字为compactMap(😃,如果我们在4.1之上的版本使用该函数会收到编译器的警告,即⚠️Please use compactMap(😃 for the case where closure returns an optional value。
在Realm库里,有一个销毁NotificationToken的方法,被标记为unavailable:
extension RLMNotificationToken {
@available(*, unavailable, renamed: "invalidate()")
@nonobjc public func stop() { fatalError() }
}
标记为unavailable就不会被编译器联想到。这个主要是为升级用户的迁移做准备,从可用stop()的版本升上了,会红色报错,提示该方法不可用。因为有renamed,编译器会推荐你用invalidate(),点击fix就直接切换了。所以这两个标记参数常一起出现。
@discardableResult
带返回的函数如果没有处理返回值会被编译器警告⚠️。但有时我们就是不需要返回值的,这个时候我们可以让编译器忽略警告,就是在方法名前用@discardableResult声明一下。可以参考Alamofire中request的写法:
@discardableResult
public func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest
{
return SessionManager.default.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers
)
}
@inlinable
这个关键词是可内联的声明,它来源于C语言中的inline。C中一般用于函数前,做内联函数,它的目的是防止当某一函数多次调用造成函数栈溢出的情况。因为声明为内联函数,会在编译时将该段函数调用用具体实现代替,这么做可以省去函数调用的时间。
内联函数常出现在系统库中,OC中的runtim中就有大量的inline使用:
static inline id autorelease(id obj)
{
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
Swift中的@inlinable和C中的inline基本相同,它在标准库的定义中也广泛出现,可用于方法,计算属性,下标,便利构造方法或者deinit方法中。
例如Swift对Array中map函数的定义:
@inlinable public func map
复制代码其实Array中声明的大部分函数前面都加了@inlinable,当应用某一处调用该方法时,编译器会将调用处用具体实现代码替换。
需要注意内联声明不能用于标记为private或者fileprivate的地方。
这很好理解,对私有方法的内联是没有意义的。内联的好处是运行时更快,因为它省略了从标准库调用map实现的步骤。但这个快也是有代价的,因为是编译时做替换,这增加了编译的开销,会相应的延长编译时间。
内联更多的是用于系统库的特性,目前我了解的Swift三方库中仅有CocoaLumberjack使用了@inlinable这个特性
@warn_unqualified_access
通过命名我们可以推断出其大概含义:对“不合规”的访问进行警告。这是为了解决对于相同名称的函数,不同访问对象可能产生歧义的问题。
比如说,Swift 标准库中Array和Sequence均实现了min()方法,而系统库中也定义了min(:😃,对于可能存在的二义性问题,我们可以借助于@warn_unqualified_access。
这个特性声明会由编译器在可能存在二义性的场景中对我们发出警告。这里有一个场景可以便于理解它的含义,我们自定义一个求Array中最小值的函数:
extension Array where Element: Comparable {
func minValue() -> Element? {
return min()
}
}
我们会收到编译器的警告:Use of 'min' treated as a reference to instance method in protocol 'Sequence', Use 'self.' to silence this warning。它告诉我们编译器推断我们当前使用的是Sequence中的min(),这与我们的想法是违背的。因为有这个@warn_unqualified_access限定,我们能及时的发现问题,并解决问题:self.min()。
@objc
把这个特性用到任何可以在 Objective-C 中表示的声明上——例如,非内嵌类,协议,非泛型枚举(原始值类型只能是整数),类和协议的属性、方法(包括 setter 和 getter ),初始化器,反初始化器,下标。 objc 特性告诉编译器,这个声明在 Objective-C 代码中是可用的。
用 objc 特性标记的类必须继承自一个 Objective-C 中定义的类。如果你把 objc 用到类或协议中,它会隐式地应用于该类或协议中 Objective-C 兼容的成员上。如果一个类继承自另一个带 objc 特性标记或 Objective-C 中定义的类,编译器也会隐式地给这个类添加 objc 特性。标记为 objc 特性的协议不能继承自非 objc 特性的协议。
@objc还有一个用处是当你想在OC的代码中暴露一个不同的名字时,可以用这个特性,它可以用于类,函数,枚举,枚举成员,协议,getter,setter等。
// 当在OC代码中访问enabled的getter方法时,是通过isEnabledclass ExampleClass: NSObject {
@objc var enabled: Bool {
@objc(isEnabled) get {
// Return the appropriate value
}
}
}
这一特性还可以用于解决潜在的命名冲突问题,因为Swift有命名空间,常常不带前缀声明,而OC没有命名空间是需要带的,当在OC代码中引用Swift库,为了防止潜在的命名冲突,可以选择一个带前缀的名字供OC代码使用。
Charts作为一个在OC和Swift中都很常用的图标库,是需要较好的同时兼容两种语言的使用的,所以也可以看到里面有大量通过@objc标记对OC调用时的重命名代码:
@objc(ChartAnimator)
open class Animator: NSObject { }
@objc(ChartComponentBase)
open class ComponentBase: NSObject { }
@ObjcMembers
因为Swift中定义的方法默认是不能被OC调用的,除非我们手动添加@objc标识。但如果一个类的方法属性较多,这样会很麻烦,于是有了这样一个标识符@objcMembers,它可以让整个类的属性方法都隐式添加@objc,不光如此对于类的子类、扩展、子类的扩展都也隐式的添加@objc,当然对于OC不支持的类型,仍然无法被OC调用:
@objcMembersclass MyClass : NSObject {
func foo() { } // implicitly @objc
func bar() -> (Int, Int) // not @objc, because tuple returns
// aren't representable in Objective-C
}
extension MyClass {
func baz() { } // implicitly @objc
}
class MySubClass : MyClass {
func wibble() { } // implicitly @objc
}
extension MySubClass {
func wobble() { } // implicitly @objc
}
testable
@testable是用于测试模块访问主target的一个关键词。
因为测试模块和主工程是两个不同的target,在swift中,每个target代表着不同的module,不同module之间访问代码需要public和open级别的关键词支撑。但是主工程并不是对外模块,为了测试修改访问权限是不应该的,所以有了@testable关键词。使用如下:
import XCTest@testable import Project
class ProjectTests: XCTestCase {
/* code */
}
这时测试模块就可以访问那些标记为internal或者public级别的类和成员了。
@frozen 和@unknown default
frozen意为冻结,是为Swift5的ABI稳定准备的一个字段,意味向编译器保证之后不会做出改变。为什么需要这么做以及这么做有什么好处,他们和ABI稳定是息息相关的,内容有点多就不放这里了,之后会单独出一篇文章介绍,这里只介绍这两个字段的含义。
@frozen public enum ComparisonResult : Int {
case orderedAscending = -1
case orderedSame = 0
case orderedDescending = 1
}
@frozen public struct String {}
extension AVPlayerItem {
public enum Status : Int {
case unknown = 0
case readyToPlay = 1
case failed = 2
}
}
ComparisonResult这个枚举值被标记为@frozen即使保证之后该枚举值不会再变。注意到String作为结构体也被标记为@frozen,意为String结构体的属性及属性顺序将不再变化。其实我们常用的类型像Int、Float、Array、Dictionary、Set等都已被“冻结”。需要说明的是冻结仅针对struct和enum这种值类型,因为他们在编译器就确定好了内存布局。对于class类型,不存在是否冻结的概念,可以想下为什么。
对于没有标记为frozen的枚举AVPlayerItem.Status,则认为该枚举值在之后的系统版本中可能变化。
对于可能变化的枚举,我们在列出所有case的时候还需要加上对@unknown default的判断,这一步会有编译器检查:
switch currentItem.status {
case .readyToPlay:
/* code */
case .failed:
/* code */
case .unknown:
/* code */
@unknown default:
fatalError("not supported")
}
Swift 几个重要关键词
lazy
lazy是懒加载的关键词,当我们仅需要在使用时进行初始化操作就可以选用该关键词。举个例子:
class Avatar {
lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize)
var largeImage: UIImage
init(largeImage: UIImage) {
self.largeImage = largeImage
}
}
对于smallImage,我们声明了lazy,如果我们不去调用它是不会走后面的图片缩放计算的。但是如果没有lazy,因为是初始化方法,它会直接计算出smallImage的值。所以lazy很好的避免的不必要的计算。
另一个常用lazy的地方是对于UI属性的定义:
lazy var dayLabel: UILabel = {
let label = UILabel()
label.text = self.todayText()
return label
}()
这里使用的是一个闭包,当调用该属性时,执行闭包里面的内容,返回具体的label,完成初始化。
使用lazy你可能会发现它只能通过var初始而不能通过let,这是由lazy 的具体实现细节决定的:它在没有值的情况下以某种方式被初始化,然后在被访问时改变自己的值,这就要求该属性是可变的。
unowned weak
Swift开发过程中我们会经常跟闭包打交道,而用到闭包就不可避免的遇到循环引用问题。在Swift处理循环引用可以使用unowned和weak这两个关键词。看下面两个例子:
class Dog {
var name: String
init (name: String ) {
self.name = name
}
deinit {
print("\(name) is deinitialized")
}
}
class Bone {
// weak 修饰词
weak var owner: Dog?
init(owner: Dog?) {
self.owner = owner
}
deinit {
print("bone is deinitialized" )
}
}
var lucky: Dog? = Dog(name: "Lucky")var bone: Bone? = Bone(owner: lucky!)
lucky = nil// Lucky is deinitialized
这里Dog和Bone是相互引用的关系,如果没有weak var owner: Dog?这里的weak声明,将不会打印Lucky is deinitialized。还有一种解决循环应用的方式是把weak替换为unowned关键词。
weak相当于oc里面的weak,弱引用,不会增加循环计数。主体对象释放时被weak修饰的属性也会被释放,所以weak修饰对象就是optional。
·unowned相当于oc里面的unsafe_unretained,它不会增加引用计数,即使它的引用对象释放了,它仍然会保持对被已经释放了的对象的一个 "无效的" 引用,它不能是 Optional 值,也不会被指向nil。如果此时为无效引用,再去尝试访问它就会crash。
这两者还有一个更常用的地方是在闭包里面:
lazy var someClosure: () -> Void = { [weak self] in
// 被weak修饰后self为optional,这里是判断self非空的操作
guard let self = self else { retrun }
self.doSomethings()
}
这里如果是unowned修饰self的话,就不需要用guard做解包操作了。但是我们不能为了省略解包的操作就用unowned,也不能为了安全起见全部weak,弄清楚两者的适用场景非常重要。
KeyPath
KeyPath是键值路径,最开始是用于处理KVC和KVO问题,后来又做了更广泛的扩展。
// KVC问题,支持struct、classstruct
User {
let name: String
var age: Int
}
var user1 = User()
user1.name = "ferry"
user1.age = 18
//使用KVC取值let path: KeyPath = \User.name
user1[keyPath: path] = "zhang"let name = user1[keyPath: path]print(name) //zhang
// KVO的实现还是仅限于继承自NSObject的类型// playItem为AVPlayerItem对象
playItem.observe(\.status, changeHandler: { (_, change) in
/* code */
})
这个KeyPath的定义是这样的:
public class AnyKeyPath : Hashable, _AppendKeyPath {}
/// A partially type-erased key path, from a concrete root type to any/// resulting value type.public class PartialKeyPath<Root> : AnyKeyPath {}
/// A key path from a specific root type to a specific resulting value type.public class KeyPath<Root, Value> : PartialKeyPath<Root> {}
定义一个KeyPath需要指定两个类型,根类型和对应的结果类型。对应上面示例中的path:
let path: KeyPath<User, String> = \User.name
根类型就是User,结果类型就是String。也可以不指定,因为编译器可以从\User.name推断出来。那为什么叫根类型的?可以注意到KeyPath遵循一个协议_AppendKeyPath,它里面定义了很多append的方法,KeyPath是多层可以追加的,就是如果属性是自定义的Address类型,形如:
struct Address {
var country: String = ""
}let path: KeyPath<User, String> = \User.address.country
这里根类型为User,次级类型是Address,结果类型是String。所以path的类型依然是KeyPath<User, String>。
明白了这些我们可以用KeyPath做一些扩展:
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
return a[keyPath: keyPath] < b[keyPath: keyPath]
}
}
}// users is Array<User>let newUsers = users.sorted(by: \.age)
这个自定义sorted函数实现了通过传入keyPath进行升序排列功能。
some
some是Swift5.1新增的特性。它的用法就是修饰在一个 protocol 前面,默认场景下 protocol 是没有具体类型信息的,但是用some 修饰后,编译器会让 protocol 的实例类型对外透明。
可以通过一个例子理解这段话的含义,当我们尝试定义一个遵循Equatable协议的value时:
// Protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
var value: Equatable {
return 1
}
var value: Int {
return 1
}
编译器提示我们Equatable只能被用来做泛型的约束,它不是一个具体的类型,这里我们需要使用一个遵循Equatable的具体类型(Int)进行定义。但有时我们并不想指定具体的类型,这时就可以在协议名前加上some,让编译器自己去推断value的类型:
var value: some Equatable {
return 1
}
在SwiftUI里some随处可见:
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
这里使用some就是因为View是一个协议,而不是具体类型。