Swift Tips

Khi bắt đầu với lập trình iOS, tôi luôn tò mò về các cách hoạt động tốt nhất ở các công ty lớn, họ cấu trúc project trông như thế nào? Kiến trúc họ đang sử dụng là gì? Thường sử dụng các thư viện nào? Đây là điều mình luôn canh cánh trong lòng để xây dựng dựa trên kinh nghiệm của người khác và không lãng phí thời gian cho những vấn đề đã được giải quyết. Hãy bắt tay vào thôi!

Tránh lạm dụng Reference types

  • Nên sử dụng reference type cho các object còn sống. Sống là như thế nào, hãy cùng xem ví dụ dưới đây
struct Car { 
  let model: String
}
class CarManager { 
  private(set) var cars: [Car]
  func fetchCars()
  func registerCar(_ car: Car)
}

🚗 chỉ là một giá trị, nó đại diện cho dữ liệu. Giống như 0, nó không quản lý bất cứ điều gì. Vì vậy, nó không phải sống. Không có điểm nào xác định nó là một reference type.

Mặt khác, CarManager cần là 1 object sống, bởi vì object đó gọi request tới network và chờ response và lưu trữ các cars đã lấy được từ request. Ta không thể thực hiện bất kỳ hành động bất đồng bộ nào lên một reference type vì có thể chúng sẽ chết. CarManager sẽ sống trong phạm vi khi fetch cars từ server tới register new car.

Không bao giờ sử dụng implicity unwrapped properties

Không nên sử dụng các thuộc tính ngầm mặc định. Vì bạn có thể quên nó ở hầu hết các trường hợp. Nhưng có thể có một số trường hợp đặc biệt khi đó bạn cần khái niệm này để làm hài lòng trình biên dịch. Và nó rất quan trọng để hiểu logic đằng sau nó.

Về cơ bản, nếu một thuộc tính phải nil trong quá trình khởi tạo, nhưng sẽ được gán sau đó cho giá trị không phải là nil, bạn có thể khai báo thuộc tính ngầm. Bởi vì bạn sẽ không bao giờ truy cập nó trước khi nó được set, do đó bạn sẽ không muốn trình biên dịch cảnh báo về việc nó bị nil

Nếu bạn nghĩ về mối quan hệ view-xib, bạn có thể hiểu hơn. Ví dụ về nameLabel outlet ở ví dụ sau

class SomeView: UIView {
  @IBOutlet let nameLabel: UILabel
}

Nếu khai báo như trên, trình biên dịch sẽ cảnh báo bạn khai báo 1 giá trị khởi gạo và gán cho nameLabel 1 giá trị khác nil. Điều này hoàn toàn bình thươngf vì bạn khai báo rằng SomeView sẽ luôn có một nameLabel. Nhưng bạn không thể làm điều này vì quá trình binding sẽ thực hiện phía sau scenes trong initWithCoder.

Trong trường hợp này, bạn định nghĩa nó là một thuộc tính ngầm định. Nó giống như ký hợp đồng với trình biên dịch:

You: “This will never be nil, so stop warning me about it.”

Compiler: “OK.”

class SomeView: UIView {
  @IBOutlet var nameLabel: UILabel!
}

Câu hỏi thường gặp: Có nên sử dụng thuộc tính ngầm định trong khi dequeing một cell từ tableview?

guard let cell = tableView.dequeueCell(...) else {
  fatalError("Cannot dequeue cell with identifier \(cellID)")
}

Tránh lạm dụng AppDelegate

AppDelegate không phải là nơi giữ PersistentStoreCoordinator, global objects, helper functions, manager, ...vv. Nó giống như bất kỳ class nào implement 1 protocol. Hãy tách riêng nó ra và không để các phần khác ảnh hưởng đến nó.

Vẫn biết rằng có nhiều thứ cần phải ném trong applicationDidFinishLaunching nhưng nó quá dễ để vượt khỏi tầm kiểm soát khi dự án ngày một lớn. Hãy luôn cố gắng tạo các class(files) riêng biệt để quản lý các chức năng riêng biệt.

Không nên

let persistentStoreCoordinator: NSPersistentStoreCoordinator
func rgb(r: CGFloat, g: CGFloat, b: CGFloat) -> UIColor { ... }
func appDidFinishLaunching... {
  Firebase.setup("3KDSF-234JDF-234D")
  Firebase.logLevel = .verbose
  AnotherSDK.start()
  AnotherSDK.enableSomething()
  AnotherSDK.disableSomething()
  AnotherSDK.anotherConfiguration()
  persistentStoreCoordinator = ...
  return true
}

Nên

func appDidFinishLaunching... {
  DependencyManager.configure()
  CoreDataStack.setup()
  return true
}

Tránh lạm dụng các tham số mặc định

Bạn có thể set các giá trị mặc định cho tham số trong function, nó rất tiện lợi vì nếu không bạn sẽ phải tạo nhiều function chỉ để thêm cú pháp. Ví dụ

func print(_ string: String, options: String?) { ... }
func print(_ string: String) {
  print(string, options: nil)
}

Với parameter mặc định thì

func print(_ string: String, options: String? = nil) { ... }!

Khá đơn giản, nó đơn giản khi đặt màu mặc định cho custom UI component, để tuỳ chọn các option mặc định cho các chức năng parse function hoặc để chỉ định thời gian chờ mặc định cho thành phần mạng của bạn, nhưng nên cẩn thận khi quá phụ thuộc vào nó.

Hãy xem ví dụ dưới đây

class TicketsViewModel {
  let service: TicketService
  let database: TicketDatabase
  init(service: TicketService,
       database: TicketDatabase) { ... }
}

Sử dụng trong App target

let model = TicketsViewModel(
  service: LiveTicketService()
  database: LiveTicketDatabase()
)

Sử dụng trong Test target

let model = TicketsViewModel(
  service: MockTicketService()
  database: MockTicketDatabase()
)

Lý do bạn có các protocol cho service(TicketService) và database(TicketDatabase) là để trừu tượng hoá. Điều này cho phép bạn thực hiện bất kỳ implementation nào mà bạn muốn trong TicketsViewModel. Vì vậy nếu đưa tham số mặc định vào TicketsViewModel điều này sẽ khiến ViewModel phụ phuộc vào LiveTicketService, đây là một loại cụ thể. Nó mâu thuẫn với những gì bên trên ta mới đề cập

Liệu đã đủ thuyết phục chưa?

Hãy tưởng tượng bạn có các target là AppTest. TicketsViewModel thông thường sẽ được thêm vào cả 2 target, sau đó triển khai viewmodel với từng target khác nhau. Nếu bạn tạo ra một sự phụ thuộc giữa TicketsViewModelLiveTicketService Target Test của bạn sẽ không được biên dịch vì nó ko biết gì về LiveTicketService

Sử dụng vardiric parameters

Nó khá là hay, dễ thực hiện

func sum(_ numbers: Int...) -> Int {
  return numbers.reduce(0, +)
}
sum(1, 2)       // Returns 3
sum(1, 2, 3)    // Returns 6
sum(1, 2, 3, 4) // Returns 10

Sử dụng nested type

Swift hỗ trợ kiểu inner types để bạn có thể các kiểu lồng bất cứ chỗ nào có ý nghĩa

Không nên:

enum PhotoCollectionViewCellStyle {
  case default
  case photoOnly
  case photoAndDescription
}

Bạn sẽ không bao giờ sử dụng enum này bên ngoài PhotoCollectionViewCell vì vậy không có điểm nào đặt nó trong global.

Nên:

class PhotoCollectionViewCell {
  enum Style {
    case default
    case photoOnly
    case photoAndDescription
  }
  let style: Style = .default
  // Implementation...
}

Điều này có ý nghĩa hơn vì Style là một phần của PhotoCollectionViewCell và ngắn hơn 23 ký tự so với PhotoCollectionViewCellStyle.

Final là mặc định

Các class nên được final theo mặc định bởi vì bạn thường không thiết kế chúng để có thể mở rộng. Vì vậy, nó thực sự là một lỗi không làm cho họ cuối cùng. Ví dụ: bạn đã phân lớp PhotoCollectionViewCell bao nhiêu lần?

Bonus: Bạn nhận được thời gian biên dịch tốt hơn một chút.

Namespace cho constants

Bạn có biết rằng bạn có thể đặt tên cho các constants global của mình đúng cách thay vì sử dụng các tiền tố xấu như PFX hoặc k?

Không nên

static let kAnimationDuration: TimeInterval = 0.3
static let kLowAlpha = 0.2
static let kAPIKey = "13511-5234-5234-59234"

Nên

enum Constant {
  enum UI {
    static let animationDuration: TimeInterval = 0.3
    static let lowAlpha: CGFloat = 0.2  
  }
  enum Analytics {
    static let apiKey = "13511-5234-5234-59234"
  }
}

Sở thích cá nhân của tôi là chỉ sử dụng C thay vì Constant vì nó đủ rõ ràng. Bạn có thể chọn bất cứ điều gì bạn thích.

Before: kAnimationDuration or kAnalyticsAPIKey After: C.UI.animationDuration or C.Analytics.apiKey

Look good đấy nhỉ 😄

Tránh lạm dụng _

_ là một biến lưu chỗ chứa các giá trị không sử dụng. Đó là một cách để nói với tôi, tôi không quan tâm đến giá trị này với trình biên dịch để nó không bị warning.

Không nên

if let _ = name {
  print("Name is not nil.")
}

Nên:

Nil-check:
if name != nil {
  print("Name is not nil.")
}

Thường không sử dụng

_ = manager.removeCar(car) // Returns true if successful.
  • Completion block
service.fetchItems { data, error, _ in 
  // Hey, I don't care about the 3rd parameter to this block.
}

Tránh đặt tên các phương thức mơ hồ

Điều này thực sự áp dụng cho bất kỳ ngôn ngữ lập trình nào cần được hiểu bởi con người. Mọi người không nên nỗ lực nhiều hơn để hiểu ý của bạn, thật khó để hiểu ngôn ngữ máy tính!

Ví dụ, kiểm tra func sau

driver.driving()

Thực sự nó làm gì? Nó kiểm tra nếu lái xe đang lái xe và trả lại đúng nếu như vậy.

Refs: https://medium.com/nsistanbul/swifty-tips-️-8564553ba3ec