Swift Advanced Golden Road (I)

One question left over from last time is why rethrows are generally used in higher-order functions that contain methods that can be thrown as parameters.

We can review rethrows again with the official documents of Swift:

A function or method can be declared with the rethrows keyword to indicate that it throws an error only if one of its function parameters throws an error. These functions and methods are known as rethrowing functions and rethrowing methods. Rethrowing functions and methods must have at least one throwing function parameter.

A function that returns REthrows requires at least one functional argument that can throw an exception. A function that takes a function as an argument is called a higher-order function.

This installment covers Swift in two parts: feature modifiers and some important Swift concepts.

Characteristic modifier

There are a lot of @ symbols in Swift syntax. Most of them were OC compatible in versions prior to Swift4, and more and more new features with the @ symbol appear in Swift4 and later. Attributes that start with @ are called Attributes in the official website and features in SwiftGG translation. I haven’t found a general term for these symbols modified by @, so I’ll just call them Attributes modifiers. If you have any friends who know better, please let me know.

Judging from the Swift5 release (@dynamicCallable,@State), there will be more feature modifiers to come. Before they come out, it’s worth taking a look at some of the existing feature modifiers and what they do.

Reference: Swift Attributes

@available

@available: Used to identify the life cycle of computed properties, functions, classes, protocols, structures, enumerations, and so on. (Depending on the particular platform version or Swift version). It is usually followed by at least two arguments separated by commas. The first parameter is fixed and represents the platform and language. The options are as follows:

  • iOS
  • iOSApplicationExtension
  • macOS
  • macOSApplicationExtension
  • watchOS
  • watchOSApplicationExtension
  • tvOS
  • tvOSApplicationExtension
  • swift

You can use * to indicate support for all of these platforms.

Here’s an example we use when we need to turn off the auto-adjust inset feature of a ScrollView:

// specify that this method is only available in iOS11 and later
if #available(iOS 11.0, *) {
  scrollView.contentInsetAdjustmentBehavior = .never
} else {
  automaticallyAdjustsScrollViewInsets = false
}
Copy the code

Another use is to precede a function, struct, enumeration, class, or protocol to indicate that the current type is only suitable for a particular platform:

@available(iOS 12.0*),func adjustDarkMode(a) {
  /* code */
}
@available(iOS 12.0*),struct DarkModeConfig {
  /* code */
}
@available(iOS 12.0*),protocol DarkModeTheme {
  /* code */
}
Copy the code

Version and platform restrictions can be written in multiple ways:

@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6*),public func applying(_ difference: CollectionDifference<Element>) -> ArraySlice<Element>?
Copy the code

Note: as a conditional statementavailableThe front is#Is used as a marker bit@

For example, available must have at least two parameters.

  • deprecated: marks expired from the specified platform and can specify a version number
  • Obsoleted = version numberDeprecation starts with a version of the specified platform (note the difference between deprecation,deprecatedYou can still use it, but it’s not recommended.obsoletedThis declaration will compile errors if it is called
  • Message = Message content: Give some additional information
  • unavailable: is invalid on the specified platform
  • Renamed = new name: renaming statement

Let’s look at a few examples. This is a function for Array flatMap:

@available(swift, deprecated: 4.1, renamed: "compactMap(_:)", message: "Please use compactMap(_:) for the case where closure returns an optional value")
public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?).rethrows- > [ElementOfResult]
Copy the code

The new name for this function is compactMap(_:). If we use this function in versions above 4.1, we will receive a warning from the compiler. ⚠️Please use compactMap(_:) for the case where closure returns an optional value.

In the Realm library, there is a method for destroying a NotificationToken, flagged as Unavailable:

extension RLMNotificationToken {
    @available(*, unavailable, renamed: "invalidate()")
    @nonobjc public func stop(a) { fatalError()}}Copy the code

Mark it as unavailable and the compiler will not associate it with it. This is intended to be used in preparation for the migration of upgrade users. If you upgrade from the stop() version, you will get a red error indicating that the method is not available. The compiler recommends invalidate() for the system, and click fix to switch. So these two tag parameters often appear together.

@discardableResult

Functions that return are warned by the compiler if they do not process the return value ⚠️. But sometimes we just don’t need a return value, and in that case we can tell the compiler to ignore the warning by saying @discardableresult before the method name. Request in Alamofire can be written as follows:

@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
    )
}
Copy the code

@inlinable

The keyword is inline declaration, which comes from the C language inline. In C, it is generally used to do inline functions before functions. Its purpose is to prevent overflow of function stack when a function is called many times. Because declared as an inline function, the segment of function call is replaced by a concrete implementation at compile time, which saves time on the function call.

Inline functions often appear in system libraries. Runtim in OC has a lot of inline uses:

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;
}
Copy the code

@inlinable in Swift is basically the same as inline in C, and it is widely used in standard library definitions for methods, computed properties, subscripts, convenience constructors, and deinit methods.

For example, Swift defines the map function in Array:

@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows- > [T]
Copy the code

In fact, most of the functions declared in Array are preceded by @inlinable, and when this method is called with a specific application, the compiler replaces the call with implementation code.

Note that inline declarations cannot be used to mark asprivateorfileprivatePlace.

This makes sense; inlining private methods makes no sense. The benefit of inlining is faster runtime because it omits the step of calling the Map implementation from the standard library. This speed comes at a cost, however, because substitution is done at compile time, which increases compilation overhead and increases compilation time accordingly.

Inlining is more of a system library feature, so far only CocoaLumberjack uses @inlinable in Swift third-party libraries that I know of.

@warn_unqualified_access

From the naming we can deduce the general meaning: to warn against “non-compliant” access. This is to solve the problem of possible ambiguity between different access objects for functions with the same name.

For example, the min() method is implemented in the Swift standard library for Array and Sequence, and min(::) is defined in the system library. For possible ambiguity problems, we can use @warn_unqualified_access.

extension Array where Self.Element : Comparable {
  @warn_unqualified_access
  @inlinable public func min(a) -> Element?
}
extension Sequence where Self.Element : Comparable {
  @warn_unqualified_access
  @inlinable public func min(a) -> Self.Element?
}
Copy the code

This feature declaration will be warned by the compiler in cases of possible ambiguity. Here’s a scenario to help you understand what it means. Let’s customize a function that minimizes Array:

extension Array where Element: Comparable {
    func minValue(a) -> Element? {
        return min()}}Copy the code

We receive a warning from the compiler: Use of ‘min’ treated as a reference to instance method in protocol ‘Sequence’, Use ‘self.’ to silence this warning. It tells us that the compiler has inferred that we are currently using min() from Sequence, which goes against our wishes. Because of the @warn_unqualified_access qualifier, we were able to spot problems in time and fix them: self.min().

@objc

Apply this feature to any declaration that can be represented in Objective-C — for example, non-embedded classes, protocols, non-generic enumerations (primitive value types can only be integers), properties of classes and protocols, methods (including setters and getters), initializers, de-initializers, subscripts. The objC feature tells the compiler that this declaration is available in Objective-C code.

Classes marked with objC features must inherit from a class defined in Objective-C. If you apply objC to a class or protocol, it implicitly applies to objective-C-compliant members of that class or protocol. If a class inherits from another class with objC feature tags or defined in Objective-C, the compiler implicitly adds objC features to that class as well. Protocols marked as objC features cannot inherit from protocols that are not objC features.

@objc is also useful when you want to expose a different name in OC code. It can be used for classes, functions, enumerations, enumerated members, protocols, getters, setters, etc.

// When accessing the getter for enabled in OC code, it is through isEnabled
class ExampleClass: NSObject {
    @objc var enabled: Bool {
        @objc(isEnabled) get {
            // Return the appropriate value}}}Copy the code

This feature can also be used to resolve potential naming conflicts, since Swift has namespaces and is often declared without a prefix, whereas OC does not need namespaces. When referencing the Swift library in OC code, a prefixed name can be selected to be used by OC code to prevent potential naming conflicts.

Charts, as an icon library commonly used in OC and Swift, needs to be compatible with both languages, so it can also be seen that there are a lot of rename codes for calling OC through @objc flag:

@objc(ChartAnimator)
open class Animator: NSObject {}@objc(ChartComponentBase)
open class ComponentBase: NSObject {}Copy the code

@objcMembers

Because methods defined in Swift cannot be called by OC by default, unless we manually add the @objc flag. The @objcmembers identifier allows you to implicitly add @objc to all property methods of a class. It also implicitly adds @objc to subclasses, extensions, and extensions of subclasses. Of course, for types that OC does not support, Still cannot be called by OC:

@objcMembers
class MyClass : NSObject {
  func foo(a){}// implicitly @objc

  func bar(a)- > (Int.Int)   // not @objc, because tuple returns
      // aren't representable in Objective-C
}

extension MyClass {
  func baz(a){}// implicitly @objc
}

class MySubClass : MyClass {
  func wibble(a){}// implicitly @objc
}

extension MySubClass {
  func wobble(a){}// implicitly @objc
}
Copy the code

See @objc, @objcmembers, and Dynamic in Swift3, 4

@testable

Testable is a keyword used to test modules to access the main target.

Because the test module and the main project are two different targets, in Swift, each target represents a different module, and accessing code between modules requires public and open level keyword support. However, the main project is not testable by testable testable modules, so modifications to the testable version should not be testable, so there is @testable keywords. Use as follows:

import XCTest
@testable import Project

class ProjectTests: XCTestCase {
  /* code */
}
Copy the code

The test module can then access classes and members marked internal or public.

@ frozen and @ unknown default

Frozen, which means frozen, is a field prepared for Swift5 ABI stability, which guarantees the compiler that no changes will be made later. Why you need to do this and what the benefits of doing this are are closely related to ABI stability, which will be covered in a separate article later, but only what these two fields mean.

@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}}Copy the code

The ComparisonResult enumeration value is marked as @frozen even though the enumeration value is guaranteed to remain unchanged. Notice that String as a structure is also marked @frozen, meaning that the properties of the String structure and their order will not change. In fact, our common types like Int, Float, Array, Dictionary, Set, and so on are “frozen”. It is important to note that freezing only applies to value types such as struct and enum, because they are laid out in memory at the compiler. For class types, there is no such thing as a freeze or not. You can think about why.

For enumeration avplayerItem. Status that is not marked frozen, the enumeration value is considered likely to change in later system versions.

For enumerations that may change, we also need to add @unknown default judgment when listing all cases. This step will be checked by the compiler:

switch currentItem.status {
    case .readyToPlay:
        /* code */
    case .failed:
        /* code */
    case .unknown:
        /* code */
    @unknown default:
        fatalError("not supported")}Copy the code

@state, @Binding, @observedobject, @environmentobject

These are the feature modifiers in SwiftUI, because I don’t know much about SwiftUI, I won’t explain it here. An article is attached for your understanding.

Understand the property decorators @state, @Binding, @observedobject, @environmentobject in SwiftUI

A few key words

lazy

Lazy is a lazy load keyword that can be used when we only need to initialize when using it. Here’s an example:

class Avatar {
  lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize)
  var largeImage: UIImage

  init(largeImage: UIImage) {
    self.largeImage = largeImage
  }
}
Copy the code

For smallImage, we declare lazy. If we don’t call it, it won’t do the image scaling. But without lazy, because it’s an initialization method, it calculates the smallImage value directly. So lazy is a good way to avoid unnecessary calculations.

Another common use of lazy is the definition of UI attributes:

lazy var dayLabel: UILabel = {
    let label = UILabel()
  	label.text = self.todayText()
    return label
}()
Copy the code

We use a closure here, and when we call this property, we execute the contents of the closure, return the specific label, and complete the initialization.

With lazy you might find that it can only be initialized with var and not with let, depending on the specifics of the implementation of lazy: it is initialized in some way without a value, and then changes its value when it is accessed, which requires that the property be mutable.

Alternatively, we can use lazy in Sequences, so let’s take a look at an example before explaining it:

func increment(x: Int) -> Int {
  print("Computing next value of \(x)")
  return x+1
}

let array = Array(0..<1000)
let incArray = array.map(increment)
print("Result:")
print(incArray[0], incArray[4])
Copy the code

Computing next value of print(“Result:”) It’s going to be executed 1000 times, but really we only need the values of 0 and 4.

You can also use lazy sequences as follows:

let array = Array(0..<1000)
let incArray = array.lazy.map(increment)
print("Result:")
print(incArray[0], incArray[4])

// Result:
/ / 1 5
Copy the code

Before print(“Result:”), nothing is printed, just the ones and fives we used. This means that lazy can be delayed until we evaluate the result in the map.

Let’s look at the definition of lazy:

@inlinable public var lazy: LazySequence<Array<Element> > {get }
Copy the code

It returns a LazySequence structure containing Array

, and the map calculation is redefined in the LazySequence:

/// Returns a `LazyMapSequence` over this `Sequence`. The elements of
/// the result are computed lazily, each time they are read, by
/// calling `transform` function on a base element.
@inlinable public func map<U>(_ transform: @escaping (Base.Element) -> U) - >LazyMapSequence<Base.U>
Copy the code

This completes the implementation of the lazy sequence. Lazy of the LazySequence type can only be used in higher-order functions such as map, flatMap, and compactMap.

Reference: “Lazy” is good

Correct: Reference passage: “These types (LazySequence) can only be used in higher-order functions such as map, flatMap and filter.” In fact, there is no filter, because filter is a filtering function, which requires a complete sequence traversal to complete the filtering operation, so it cannot be loaded lazily. And I looked up the definition of LazySequence, there is no filter function indeed.

unowned weak

In Swift development, we will deal with closures a lot, and when using closures, we will inevitably encounter circular reference problems. The keywords unowned and weak can be used in Swift processing loop references. Consider the following two examples:

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
Copy the code

Dog and Bone refer to each other. If there is no weak var owner: Dog? The weak declaration will not print Lucky is deinitialized. Another way of dealing with circular applications is to replace weak with the unowned keyword.

  • Weak is equivalent to oc. Weak is a weak reference and does not increase the loop count. The weak modifier is also released when the main object is released, so the weak modifier is optional.
  • unowned is equivalent to ocunsafe_unretainedIt does not increase the reference count, and even if its reference object is freed, it will retain an “invalid” reference to the freed object, which cannot be Optional, and will not be pointed tonil. If the reference is invalid, attempts to access it will crash.

A more common place for both is inside closures:

lazy var someClosure: () -> Void= {[weak self] in
    // self is optional if it is weak
    guard let self = self else { retrun }
    self.doSomethings()
}
Copy the code

If unowned decorates self, guard does not need to be unpacked. However, we cannot use unowned to omit unpacking operations, nor can we use all weak for security purposes. It is very important to know the applicable scenarios of both.

According to Apple:

Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.

When the closure and the instances it captures always refer to each other and are always released at the same time, i.e. the same lifecycle, we should use unowned, and otherwise use weak.

Reference: Memory management, WEAK and UNOWNED

Unowned or Weak? Life cycle and performance comparison

KeyPath

KeyPath is a key-value path, first used to handle KVC and KVO problems, and later extended more broadly.

// KVC problem, support struct, class
struct User {
    let name: String
    var age: Int
}

var user1 = User()
user1.name = "ferry"
user1.age = 18
 
// Use KVC
let path: KeyPath = \User.name
user1[keyPath: path] = "zhang"
let name = user1[keyPath: path]
print(name) //zhang

// The implementation of KVO is still limited to types inherited from NSObject
PlayItem is an AVPlayerItem object
playItem.observe(\.status, changeHandler: { (_, change) in
    /* code */    
})
Copy the code

The KeyPath definition looks like this:

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> {}
Copy the code

Defining a KeyPath requires specifying two types, the root type and the corresponding result type. Corresponds to path in the example above:

let path: KeyPath<User.String> = \User.name
Copy the code

The root type is User, and the result type is String. You can also omit it, as the compiler can infer from \ user.name. Then why is it called root? Note that KeyPath follows a protocol _AppendKeyPath that defines a number of append methods. KeyPath is layered and can be appended if the attribute is of a custom Address type, such as:

struct Address {
    var country: String = ""
}
let path: KeyPath<User.String> = \User.address.country
Copy the code

Here the root type is User, the secondary type is Address, and the result type is String. So the path type is KeyPath

.
,>

With that in mind we can make some extensions with 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)
Copy the code

This custom sorted function implements ascending sorting by passing in keyPath.

The power of key paths in Swift

some

Some is a new feature in Swift5.1. It is used to refer to a protocol. By default, protocol has no specific type, but with some the compiler makes the instance type of protocol transparent.

An example of what this means is when we try to define a value that follows the Equatable protocol:

// 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
}
Copy the code

The compiler tells us that Equatable can only be used as a constraint on generics. It is not a concrete type, so we need to define it using a concrete type (Int) that follows Equatable. Sometimes we don’t want to specify a specific type, so we can add some to the protocol name and let the compiler infer the type of value:

var value: some Equatable {
    return 1
}
Copy the code

In SwiftUI some is everywhere:

struct ContentView: View {
    var body: some View {
        Text("Hello World")}}Copy the code

Some is used here because View is a protocol, not a concrete type.

When we try to trick the compiler by randomly returning a different Equatable type each time:

var value: some Equatable {
    if Bool.random() {
        return 1
    } else {
        return "1"}}Copy the code

A smart compiler will find out, Function declares an opaque return type, But the return statements in its body do not have matching precise types.

Some preliminary explorations of SwiftUI