These are some common code quality metrics. Our goal is how to better use Swift to write code that meets the requirements of code quality indicators.

Tip: This article is not about design patterns/architectures, but more about refactoring parts of code through the proper use of Swift features.

Some good practice

1. Use compile checks

To use lessAny/AnyObject

Because Any/AnyObject lacks explicit type information, the compiler cannot do type checking, causing some problems:

  • The compiler cannot check that the type is correct for type safety
  • A lot of codeas?conversion
  • The absence of a type prevents the compiler from doing some potentialCompiler optimization
useas?Problems brought about

When using Any/AnyObject, as? Is frequently used. Type conversion. This doesn’t seem to be a problem because you use as? Does not cause the program to Crash. However, code errors should be divided into at least two categories. One is the error of the program itself, which usually causes Crash, and the other is the error of business logic. Use the as? It only avoids program error Crash, but it cannot prevent business logic error.

func do(data: Any?) {
    guard let string = data as? String else {
        return
    }
    // 
}

do(1)
do("")
Copy the code

As the example above, we conducted as? Conversion, which is processed when data is a String. However, when the String function changes in the DO method, the user does not know that the change has been made and does not adapt accordingly, which can cause business logic errors.

Tip: These types of errors are often harder to find, which is what we encountered in a real bug scenario.

useCustom typeInstead ofDictionary

A large number of Dictionary data structures in your code reduces code maintainability and introduces potential bugs:

  • keyStrings are hard coded and cannot be checked at compile time
  • valueThere are no type restrictions.Modify theWhen the type cannot be restricted, the type conversion and unpacking operation need to be repeated when reading
  • Can’t useAir safetyProperty that specifies that an attribute must have a value

Tip: Custom types have another benefit, such as type /nil/ property name check when converting JSON to custom types, to avoid throwing bad data to the next layer.

Is not recommended
let dic: [String: Any]
let num = dic["value"] as? Int
dic["name"] = "name"
Copy the code
recommended
struct Data {
  let num: Int
  var name: String?
}
let num = data.num
data.name = "name"
Copy the code
Suitable for useDictionaryThe scene of
  • Data not used– The data is notreadIt’s just for passing.
  • The decoupling 
    • 1.Intercomponent communicationDecoupling usingHashMapCommunicate by passing parameters.
    • 2. Scenarios across technology stack boundaries,Hybrid stack communication/back and forth communicationuseHashMap/JSONTo communicate.

useEnumeration associated valueInstead ofAny

For example, using enumerations to modify NSAttributedStringAPI, the original APIvalue of Any cannot restrict a particular type.

Before optimization
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor, value: UIColor.red, range: range)
Copy the code
After transforming
enum NSAttributedStringKey { case foregroundColor(UIColor) } let string = NSMutableAttributedString() String.addattribute (.foregroundcolor (uicolor.red), range: range) // An error occurs if you do not pass ColorCopy the code

useThe generic/Protocol association typeInstead ofAny

Use generic or protocol-associated types instead of Any to enable the compiler to do more type checking through generic type constraints.

_

useThe enumeration/constantInstead ofHard coded

There are duplicate hard-coded strings/numbers in the code, which can cause bugs when modified out of sync. Minimize hard-coded strings/numbers and use enumerations or constants instead.

useKeyPathInstead ofstringHard coded

KeyPath contains the attribute name and type information to avoid hard-coding strings, and the compiler checks when the attribute name or type changes.

Is not recommended
class SomeClass: NSObject {
    @objc dynamic var someProperty: Int
    init(someProperty: Int) {
        self.someProperty = someProperty
    }
}
let object = SomeClass(someProperty: 10)
object.observeValue(forKeyPath: "", of: nil, change: nil, context: nil)
Copy the code
recommended
let object = SomeClass(someProperty: 10)
object.observe(.someProperty) { object, change in
}
Copy the code

2. Memory security

! Property will be implicitly strong unpack when read, when the value does not exist, a runtime exception will occur, resulting in Crash.

class ViewController: UIViewController { @IBOutlet private var label: UILabel! // @ibOutlet needs to be used! }Copy the code

To use less!Perform strong unpack

Use! Strong unpacking causes a runtime exception when the value is not present, leading to a Crash.

var num: Int? let num2 = num! / / errorCopy the code

Tip: It is recommended to use only in a small range of local code segments! The package.

Avoid the use oftry!Error handling

Use the try! A runtime exception is generated when the method throws an exception, causing a Crash.

try! method()
Copy the code

useweak/unownedAvoid circular references to √

resource.request().onComplete { [weak self] response in guard let self = self else { return } let model = self.updateModel(response) self.updateUI(model) } resource.request().onComplete { [unowned self] response in let model =  self.updateModel(response) self.updateUI(model) }Copy the code

To use lessunowned

unowned causes a runtime exception to Crash if the value does not exist, and unowned is used only if self is certain to exist.

class Class {
    @objc unowned var object: Object
    @objc weak var object: Object?
} 
Copy the code

Unowned/weak difference:

  • weak– Must be set to optional value, weak reference processing will be worse performance. Automatically set tonil
  • unowned– You can leave the value as optional, and the weak reference processing performance is better. But it is not automatically set tonilIf theselfReleased triggers an error.

Error handling

  • An optional value– The caller does not care about internal errors and returns when an error occursnil
  • try/catch– The caller is explicitly prompted to handle an exception that needs to be implementedErrorThe protocol defines explicit error types
  • assert– assertions. Only in theDebugValid in mode
  • precondition– andassertSimilarly, you can do it againDebug/ReleaseValid in mode
  • fatalError– Generating run-time crashes can cause crashes and should be avoided
  • Result– Usually used ofclosureAsynchronous callback return value

Reduce the use of optional values

The value of optional values is that the compiler enforces nil determination of values by explicitly identifying them as potentially nil. However, optional values should not be arbitrarily defined. Optional values cannot be defined by LET, and it is relatively tedious to unpack them when using them. Code should be designed with the possibility of this value being nil in mind, and use optional values only in appropriate scenarios.

useinitInjection instead ofAn optional valueattribute

Is not recommended
class Object {
  var num: Int?
}
let object = Object()
object.num = 1
Copy the code
recommended
class Object {
  let num: Int

  init(num: Int) {
    self.num = num
  }
}
let object = Object(num: 1)
Copy the code

Avoid arbitrarily giving optional values default values

When using optional values, we usually need to do exception handling if the optional value is nil. Sometimes we do this by giving optional values default values. But consider the circumstances in which default values can be given. In scenarios where default values cannot be given, returns or exceptions should be used promptly to prevent the wrong values from being passed to more business processes.

Is not recommended
Func confirmOrder(id: String) {} // Giving the wrong value will cause the wrong value to be passed to more business processes "")Copy the code
recommended
Func confirmOrder(id: String) {} Guard let orderId = orderId else {// return} confirmOrder(id: orderId)Copy the code

Tip: In general, strongly business relevant values cannot be given default values: such as item/order ID or price. Use default values, such as default text/text color, in scenarios where bottom-of-the-pocket logic can be used.

Use enumeration to optimize optional values

Only one value can be present at a time:

Before optimization
class Object {
    var name: Int?
    var num: Int?
}
Copy the code
The optimized
  • Reduce memory footprintEnumerated association typesThe size of depends on the maximum association type size
  • Clearer logic– useenumAs opposed to heavy useif/elseClearer logic
enum CustomType {
    case name(String)
    case num(Int)
}
Copy the code

To reducevarattribute

Using computed properties

Using computed attributes reduces potential bugs associated with synchronization of multiple variables.

Is not recommended
class model { var data: Object? Var loaded: Bool} model.data = Object() loaded = falseCopy the code
recommended
class model { var data: Object? var loaded: Bool { return data ! = nil } } model.data = Object()Copy the code

Tip: Since the calculation of attributes is repeated each time, the calculation process needs to be lightweight to avoid performance problems.

usefilter/reduce/mapInstead offorcycle

There are many benefits to using filter/ Reduce/Map, including fewer local variables, less template code, cleaner code, and better readability.

Is not recommended
let nums = [1, 2, 3]
var result = []
for num in nums {
    if num < 3 {
        result.append(String(num))
    }
}
// result = ["1", "2"]
Copy the code
recommended
let nums = [1, 2, 3]
let result = nums.filter { $0 < 3 }.map { String($0) }
// result = ["1", "2"]
Copy the code

useguardMake an early return

recommended
guard ! a else { return } guard ! b else { return } // doCopy the code
Is not recommended
if a {
    if b {
        // do
    }
}
Copy the code

Use ternary operators? :

recommended
let b = true let a = b ? 1 : 2 let c: Int? let b = c ?? 1 Copy codeCopy the code
Is not recommended
var a: Int?
if b {
    a = 1
} else {
    a = 2
}
Copy the code

usefor whereTo optimize the cycle

The for loop adds a WHERE statement and only enters the loop if the WHERE condition is satisfied

Is not recommended
for item in collection { if item.hasProperty { // ... }}Copy the code
recommended
For item in collection where item.hasProperty {// item.hasProperty == true}Copy the code

usedefer

Defer guarantees that it will be executed before the function exits. You can avoid omissions by using the actions in defer that must be performed upon exit, such as resource release.

Func method() {lock.lock() defer {lock.unlock() // will be called at the end of the method scope} // do}Copy the code

string

use"" "

When defining complex strings, using multi-line string literals preserves special characters such as newlines/quotes of the original string without escaping them using ‘ ‘.

let quotation = """ The White Rabbit put on his spectacles. "Where shall I begin, please your Majesty?" he asked. "Begin at the beginning," the King said gravely, "and go on till you come to the end; Then stop." """ copies the codeCopy the code

Tip: the “” and newline in the string above can be retained automatically.

Using string interpolation

Using string interpolation improves code readability.

Is not recommended
Multiplier = 7 let message = String(multiplier) + "times 2.5 is" + String((Double(multiplier) * 2.5))Copy the code
recommended
Multiplier = 2 let message = "(multiplier) times 2.5 is (Double(multiplier) * 2.5)"Copy the code

A collection of

Use higher-order functions provided by the standard library

Is not recommended
Var nums = [] nums.count == 0 nums[0] Copy codeCopy the code
recommended
var nums = []
nums.isEmpty
nums.first
Copy the code

Access control

The default access control level in Swift is internal. The code should minimize the level of access control for attributes/methods/types and hide internal implementations.

Tip: It’s also good for compiler optimization.

useprivate/fileprivateModify the privateattributeandmethods

private let num = 1
class MyClass {
    private var num: Int
}
Copy the code

useprivate(set)Decorates external read-only/internal read-write properties

Class MyClass {private(set) var num = 1} let num = MyClass().num MyClass().num = 2Copy the code

function

Use the parameter defaults

Using parameter defaults allows the caller to pass fewer arguments.

Is not recommended
func test(a: Int, b: String? , c: Int?) { } test(1, nil, nil)Copy the code
recommended
func test(a: Int, b: String? = nil, c: Int? = nil) {
}
test(1)
Copy the code

Tip: Parameter defaults also allow us to define fewer methods than ObjC.

Limit the number of parameters

Consider using custom types instead when methods have too many parameters.

Is not recommended
Func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {} Copy codeCopy the code
recommended
struct Params {
    let a, b, c, d, e, f: Int
}
func f(params: Params) {
}
Copy the code

use@discardableResult

Some method users do not necessarily process return values. Consider adding @discardableresult to indicate that Xcode allows you to process return values without warning.

Func report(id: String) -> Bool {} @discardAbleresult func report2(id: String) -> Bool {} report("1") // Compiler will warn report2("1") // Compiler will not warn if return value is not processedCopy the code

tuples

Avoid long tuples

Although the tuple has type information, it does not contain variable name information, so the user does not know the meaning of the variable clearly. Therefore, when there are too many tuples, consider using custom types instead.

Func test() -> (Int, Int, Int) {} let (a, b, c) = test() print("a \(a), b: \(b), c: \(c) ")Copy the code

System libraries

KVO/Notificationuseblock API

Block API benefits:

  • KVOCan supportKeyPath
  • Instead of actively removing the wiretap,observerAutomatically removes listening on release
Is not recommended
class Object: NSObject {
  init() {
    super.init()
    addObserver(self, forKeyPath: "value", options: .new, context: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(test), name: NSNotification.Name(rawValue: ""), object: nil)
  }

  override class func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  }

  @objc private func test() {
  }

  deinit {
    removeObserver(self, forKeyPath: "value")
    NotificationCenter.default.removeObserver(self)
  }
}
Copy the code
recommended
class Object: NSObject {

  private var observer: AnyObserver?
  private var kvoObserver: NSKeyValueObservation?

  init() {
    super.init()
    observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: ""), object: nil, queue: nil) { (_) in 
    }
    kvoObserver = foo.observe(.value, options: [.new]) { (foo, change) in
    }
  }
}
Copy the code

Protocol

useprotocolInstead of inheritance

Swift provides many new features for protocol, such as default implementations, association types, and support for value types. Protocol can be prioritized in code design to avoid bloated parent classes and make more use of value types.

Tip: Some scenarios where you can’t replace inheritance with protocol:

  • 1. Need to subclass NSObject.
  • 2. Call is requiredsuperMethods.
  • 3. The implementationAn abstract classAbility.

Extension

useextensionOrganization code

Use Extension to separate the code of different functions such as private methods, parent methods, and protocol methods for clearer/easier maintenance.

class MyViewController: UIViewController {
  // class stuff here
}
// MARK: - Private
extension: MyViewController {
    private func method() {}
}
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
  // table view data source methods
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
  // scroll view delegate methods
}
Copy the code

Code style.

A good code style can improve the readability of the code, and a unified code style can reduce the cost of mutual understanding within the team. For Swift code formatting, it is recommended to implement automatic formatting tools, add automatic formatting to the code submission process, and unify the code style within the team by defining Lint rules. Consider using SwiftFormat and SwiftLint.

Tip: SwiftFormat focuses on formatting code styles, and SwiftLint can use Autocorrect to automatically fix some bad code.

Common automatic formatting fixes
  • Remove excess;
  • Save at most one newline
  • Automatic alignmentThe blank space
  • Limit the width of each lineWord wrap

Author: He Lele Link: juejin.cn/post/698476… Source: Rare earth mining copyright belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.

Swift programming official website specification

— — — — — — — — — — — — — — — — — — — — — —