The power of extensions in Swift.
Bài đăng này đã không được cập nhật trong 4 năm
- 
Extensioncho phép chúng ta có thể add thêmfunctionalitycho cáctypehayprotocolcó sẵn hay một số phần của thư việnApple SDKcó sẵn hoặc thậm chí là thành phần trong cácthird party packagemà chúng ta sử dụng trongproject.
- 
Tuy nhiên extensiontrongSwiftcó thể được sử dụng với nhiều cách linh hoạt và nâng cao hơn nhiều so với việc chỉ sử dụng để add thêm cácpropertyhaymethodcho cácexternal object. Ở bài viết này chúng ta cùng nghiên cứu các cách sử dụng đó để có thể sử dụng trong cácprojectsắp tới.
1/ Thêm các tính năng vào Type tồn tại sẵn:
- Bắt đầu từ đơn giản ta có cách sử dụng extensionđể thêm mới, tùy chỉnh cácAPIcho các dạngtypethành phần trongApple systemví dụ như cácApple standard library. Ví dụ cụ thể như chúng ta cần làm việc trên mộtappmàlogicxử lý yêu cầu chúng ta cần phảiaccessvào cácelementtrong các mảng khác nhau, để tránh việc luôn phải kiểm traindexcủa cácelementmà chúng taaccessthì chúng ta có thể làm như sau:
extension Array {
    func element(at index: Int) -> Element? {
        guard index >= 0, index < count else {
            return nil
        }
        return self[index]
    }
}
- 
Rất đơn giản nhưng tiện lợi đúng không? Bây giờ chúng ta đã có thể sử dụng methodtrên ở bất kỳArraynào trongproject. Chưa dừng ở đó chúng ta còn có thể thực hiện những tùy chỉnh tốt hơn với việc sử dụngextensionchoprotocolRandomAccessCollection:
- 
RandomAccessCollectionđịnh nghĩa các yêu cầu màcollectioncung cấprandom accesscho cácelement. Mở rộngprotocolnày chúng ta sẽ sử dụngmethodmới cho bất kìcollectionnào bao gồm cảArray:
extension RandomAccessCollection {
    func element(at index: Index) -> Element? {
        guard indices.contains(index) else {
            return nil
        }
        return self[index]
    }
}
- Chỉ với thay đổi trên, chúng ta bây giờ có thể sử dụng methodmới cho cáctypenhưArray,ArraySlice,Range:
// Extracting an optional element from an Array
guard let fifthElement = array.element(at: 4) else {
    return
}
// Doing the same thing, but using an ArraySlice instead:
let slice = array[0..<3]
guard let secondElement = slice.element(at: 1) else {
    return
}
// We could also use our new method with types like Range:
guard let thirdValue = range.element(at: 2) else {
    return
}
- 
Việc sử dụng extensionchoprotocolmang lại cho chúng ta sự linh hoạt hơn trong việc sử dụng cácmethodvàpropertymà chúng ta thêm vào.
- 
Tuy nhiên không phải tất cả extensionchúng ta thêm vào đều hướng tới mục đích chung trên. Trong một số trường hợp chúng ta cần thêm cácconstraintsđể cácextensionthêm cụ thể.
- 
Chúng ta cùng xem một ví dụ mà chúng thêm thêm extensionđể addmethodgiúp chúng ta tính toán tổng giá tiền của cácproductsvới cách sử dụngsame type constraintsđể đảm bảomethodsẽ được chỉ được gọi khiSequenceconformtype củaProductvalue:
extension Sequence where Element == Product {
    func totalPrice() -> Int {
        reduce(0) { price, product in
            price + product.price
        }
    }
}
- Điều hữu dụng của constraintsnày là nó không chỉ tham chiếu và đảm bảo type cho protocol mà còn có thể sử dụng trongclosurenhư sau:
extension Sequence where Element == () -> Void {
    func callAll() {
        forEach { closure in
            closure()
        }
    }
}
- Ở Swift 5.3, tính năng này còn được nâng cấp hơn cho phép chúng ta có thể sử dụng constraintsnày để cá nhân hóa cách khai báomethodcho các type được sử dụng để bao đóng nó.
extension Sequence {
    func callAll<T>(with input: T) where Element == (T) -> Void {
        forEach { closure in
            closure(input)
        }
    }
}
- Methodtrên có thể trở nên hữu dụng khi chúng ta muốn truyền các- same valuecho các- closurekhác nhau như trong ví dụ các- ordersẽ- notifycho tất cả- observerthuộc- Observabletype có- valuethay đổi:
class Observable<Value> {
    var value: Value {
        didSet { observations.callAll(with: value) }
    }
    private var observations = [(Value) -> Void]()
    ...
}
2/ Tổ chức lại các API và cách tuân thủ Protocol:
- 
Extensionthường được dùng trong việc tổ chức code trongproject, đây là một tính năng mà chúng ta đã thực hiện nhiều trênObjective-C. Chúng ta sử dụng để nhóm cácAPIcó cùng chức năng mà nó cung cấp để dễ dàng cho việc tìm kiếm cũng như xếp tính năng.
- 
Trong Swiftchúng ta sử dụng cùng một cách tiếp cận để xếp cácAPItheoaccess level. Lấy ví dụ nhưPublish, chúng ta có một trình khởi tạo đểbuildmỗiwebsitemà trong đóSectiondùng như type để các nhóm được gom vào phải tuân thủ theo nhưpublic,internal,private:
public struct Section<Site: Website>: Location {
    public let id: Site.SectionID
    public private(set) var items = [Item<Site>]()
    ...
}
public extension Section {
    func item(at path: Path) -> Item<Site>? {
        ...
    }
    
    func items(taggedWith tag: Tag) -> [Item<Site>] {
        ...
    }
    
    ...
}
internal extension Section {
    mutating func addItem(_ item: Item<Site>) {
        ...
    }
}
private extension Section {
    ...
    
    mutating func rebuildIndexes() {
        ...
    }
}
- 
Ngoài lợi ích trong việc tổ chức code, cách làm trên chúng ta còn không phải cấp chomethodcácaccess levelmà mỗiAPIsẽ tự động được cung cấp các truy cập cần thiết.
- 
Cách triển khai trên hoàn toàn có thể áp dụng cho protocol, chúng ta có thể kèm theo điều kiện phù hợp cho từngextensionchúng ta thêm, ví dụ như chúng ta tạoListViewControllerconformUITableViewDelegatequaextension:
extension ListViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView,
                   didSelectRowAt indexPath: IndexPath) {
        let item = items[indexPath.item]
        showDetailViewController(for: item)
    }
    
    ...
}
- Ở đây chúng ta đơn giản tạo ra các điều kiện wrapper typeđể thỏa mãnconformchoprotocolnhưEquatablevàHashablechỉ khiWrappedcũng thỏa mãn type của cácprotocol:
// The compiler can still automatically generate the code required
// to conform to protocols like Equatable and Hashable even when
// adding those conformances through extensions:
extension NetworkResponse: Equatable where Wrapped: Equatable {}
extension NetworkResponse: Hashable where Wrapped: Hashable {}
// Most protocols will probably require us to write some form of
// bridging code ourselves, though. For example, here we make our
// network response use its wrapped type's description when it's
// being converted into a string, rather than defining its own:
extension NetworkResponse: CustomStringConvertible
    where Wrapped: CustomStringConvertible {
    var description: String {
        result.description
    }
}
3/ Chuyên môn hóa việc sử dụng generic:
- 
Ở điểm cuối cùng, chúng ta cùng xem cách extensioncó thể được sử dụng để chuyên môn hóa cáctypecũng nhưprotocolchung cho từng trường hợp sử dụng thực thế:
- 
Như SequencevàRandomAccessCollectionprotocolmà chúng ta đã mở rộng để thuận tiện cho việc sử dụng như cách vàiApple Frameworkthường sử dụnggenericđể làm cho cácAPItrờ nên an toàn và dễ mở rộng hơn. TrongCombinecácpublisherđượcimplementđể sử dụngPublisherprotocolbao gồm các cácgenerictype được định nghĩa đểOutputhayFailuređược tạo ra khi cácPublisheremit.
- 
Các generictype đó cho phép chúng ta triển khai hoàn chỉnh cácCombineoperatornhư việc sử dụngResultchovalueđược trả về từpublisher emit:
extension Publisher {
    func asResult() -> AnyPublisher<Result<Output, Failure>, Never> {
        self.map(Result.success)
            .catch { error in
                Just(.failure(error))
            }
            .eraseToAnyPublisher()
    }
}
- Extensiontrên cho phéo chúng ta triển khai- Combinegiống với- AsyncValuevới việc- Outputđược- assigntrực tiếp cho- Result:
class AsyncValue<Value: Decodable>: ObservableObject {
    @Published private(set) var result: Result<Value, Error>?
    private var cancellable: AnyCancellable?
    func load(from url: URL,
              using session: URLSession = .shared,
              decoder: JSONDecoder = .init()) {
        cancellable = session.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: Value.self, decoder: decoder)
            .asResult()
            .sink { [weak self] result in
                self?.result = result
            }
    }
}
- 
Cách làm trên với các constraintscho chúng ta tận dụng khả năng suy luận type mạng mẽ củaSwiftcũng như cách màSwiftUIsử dụng các API để built các view hiển thị.
- 
Lấy ví dụ như việc chúng ta làm việc trên IconViewđược render vớiiconđã được xác định trước. Để tiện cho việc tạoButtonbao gồmiconchúng ta có thể thêmextensionmà sử dụngtype constraintslàLabelđể định nghĩacontentmàButtonđó đượcrender:
extension Button where Label == IconView {
    init(icon: Icon, action: @escaping () -> Void) {
        self.init(action: action, label: {
            IconView(icon: icon)
        })
    }
}
- Và giờ khi chúng ta sử cụng APItrên để tạoinstanceButtonthìcompliersẽ tự động thêm thông báo cho chúng ta muốn sử dụngIconViewnhư sau:
struct ProductView: View {
    @ObservedObject var viewModel: ProductViewModel
    var body: some View {
        VStack {
            ...
            Button(icon: .shoppingCart) {
                viewModel.performPurchase()
            }
        }
    }
}
All rights reserved
 
  
 