Всем привет!
На связи iOS Broadcast и сегодня хочется пойти немного перпендикулярно общим тенденциям и рассмотреть не самые новые фишки языка, а то что уже есть, но редко используется.
3 моих любимых Proposal:
Когда они только начали обсуждаться, было не понятно, нужны ли они. Но раз за разом после их выхода появлялись рабочие задачи, в которых без них никуда.
Если вам интересно следить за самыми последними новостями iOS разработки и получать подборку интересных статей по этой тематике, тогда вам стоит подписаться на Телеграм-канал iOS Broadcast
Начнем с со старичков@dynamicCallable
и @dynamicMemberLookup
. Исходя из мотивационной секции proposal, они были добавлены для интеропа с динамическими языками, такими как Python, JavaScript. Но их использование этим не ограничивается.
Dynamic member lookup
@dynamicMemberLookup c нами еще со swift 4.2 и позволяет динамически генерировать свойства у структур или классов, обращаясь к ним на самом деле через subscript.
Например:
struct Channel { let title: String } @dynamicMemberLookup struct Author { let name: String let channel: Channel subscript<T>(dynamicMember keyPath: KeyPath<Channel, T>) -> T { return channel[keyPath: keyPath] } } let channel = Channel(title: "iOS Broadcast") let author = Author(name: "Andrey Zonov", channel: channel) print(author.title) // iOS Broadcast
В данном примере видно, что у структуры автора нет свойства title, но мы смогли к нему обратиться. Где это может быть полезно в жизни?
Если вы используете Combine, то я надеюсь знаете, что методы sink и assign(to:on:) захватывает сильную ссылку. Чтобы оставить код читаемым и функциональным, я использую простую обертку
titleRequest = generator .randomTitlePublisher() .assign(to: \.obj.label.text, on: Unowned(obj: self)) struct Unowned<Object: AnyObject> { unowned var obj: Object }
⚠️ Важное уточнение ⚠️
Пример с использованием unowned
требует зависимости жизненного цикла подписки от жизненного цикла self
. Для других случав можно использовать onWeak, который под капотом работает через weak sink
titleRequest = generator .randomTitlePublisher() .assign(to: \.label.text, onWeak: self) extension Publisher where Failure == Never { func assign<Root: AnyObject>( to keyPath: ReferenceWritableKeyPath<Root, Output>, onWeak object: Root ) -> AnyCancellable { sink { [weak object] value in object?[keyPath: keyPath] = value } } }
Или более универсальную функцию,
titleRequest = generator .randomTitlePublisher() .sink(receiveValue: weak(self, ViewController.assignTitleToLabel)) func weak<T: AnyObject, Argument>(_ obj: T, _ block: @escaping (T) -> (Argument) -> Void) -> (Argument) -> Void { { [weak obj] a in obj.map(block)?(a) } }
Которая позволяет при вызове не захватывать сильной ссылкой self . Но в этом кейсе все портит obj
, и от него можно избавиться как раз с помощью @dynamicMemberLookup
titleRequest = generator .randomTitlePublisher() .assign(to: \.label.text, on: Unowned(obj: self)) @dynamicMemberLookup struct Unowned<Object: AnyObject> { unowned var obj: Object subscript<T>(dynamicMember keyPath: KeyPath<Object, T>) -> T { obj[keyPath: keyPath] } }
И это только один из примеров снижения когнитивной нагрузки ваших API для конечных пользователей.
Dynamic callable
@dynamicCallable в Swift позволяет динамически вызывать методы, используя упрощенный синтаксис, добавляя синтаксический сахар в наши API.
Например:
@dynamicCallable final class Conversation { private var messages = Set<String?>() @discardableResult func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> Bool { for (key, value) in args { switch key { case "send": return messages.insert(value).inserted case "delete": messages.remove(key) return true case "contains": return messages.contains(value) default: return false } } return false } } let сonversation = Conversation() сonversation(contains: "Привет!") // false сonversation(send: "Привет!") // true сonversation(contains: "Привет!") // true сonversation(delete: "Привет!") // true сonversation(test: "") // false
В отрыве это кажется не очень нужным, но, к примеру, если вы хотите сделать DSL для сборки URL можно сделать следующее:
@dynamicMemberLookup @dynamicCallable class Dsl<UrlsType> { private let urls: UrlsType private var components: [String] = [] init(_ urls: UrlsType) { self.urls = urls } subscript(dynamicMember keyPath: KeyPath<UrlsType, String>) -> Dsl { components.append(urls[keyPath: keyPath]) return self } func dynamicallyCall(withArguments args: [String]) -> Dsl { components.append(contentsOf: args) return self } func make(replacements: [String: String] = [:]) -> String { components.joined(separator: "/") } }
Использовать это можно следующим образом:
struct ProfileUrls { let user = "user" let posts = "posts" } let dsl = Dsl(ProfileUrls()) let url = dsl.user.posts("123").make() // user/posts/123
Это решение может помочь не ошибиться при конструировании URL, если у вас еще не прикручена кодогенерация по контрактам OpenAPI. К тому же его легко покрыть тестами.
Сall as function
И последнее по порядку, но не по важности, это callAsFunction. Опять же, синтаксический сахар, который приравнивает вызов () к вызову callAsFunction. Например:
struct RandomGenerator { func callAsFunction() -> Int { .random(in: 0...100) } } let generator = RandomGenerator() print(generator())// 58 print(generator())// 46
Универсальный DSL
Эти подходы можно использовать для создания DSL поверх UIKit в стиле SwiftUI:
@dynamicMemberLookup public struct DSL<T> { let obj: T public subscript<Value>(dynamicMember keyPath: WritableKeyPath<T, Value>) -> (Value) -> DSL<T> { { [obj] value in var object = obj object[keyPath: keyPath] = value return DSL(obj: object) } } public subscript<Value>(dynamicMember keyPath: WritableKeyPath<T, Value>) -> (Value) -> T { { [obj] value in var object = obj object[keyPath: keyPath] = value return object } } } public protocol DSLCompatible { associatedtype DSLType var dsl: DSL<DSLType> { get } } extension DSLCompatible { public var dsl: DSL<Self> { DSL(obj: self) } } extension NSObject: DSLCompatible {}
После чего можно обращаться ко всем свойствам любого UIKit элемента за счет ротового класса NSObject у них всех:
let label: UILabel = UILabel() .dsl .text("Привет") .font(.preferredFont(forTextStyle: .largeTitle)) .textAlignment(.center) .textColor(.blue)
Заключение
Начиная с версии 4.2, Swift становится более интероперабельным с динамическими языками, что приносит нам множество возможностей, все они доступны нам уже давно. Что же нас ждет с приходом макросов ?! Главное применять эти фичи языка там, где это действительно требуется, а не искать, где бы их применить.
Поделитесь в комментариях, в каких задачах вы используете эти фишки языка и в какой комбинации.
ссылка на оригинал статьи https://habr.com/ru/articles/752614/
Добавить комментарий