Всем привет!
На связи 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 становится более интероперабельным с динамическими языками, что приносит нам множество возможностей, все они доступны нам уже давно. Что же нас ждет с приходом макросов ?! Главное применять эти фичи языка там, где это действительно требуется, а не искать, где бы их применить.
Поделитесь в комментариях, в каких задачах вы используете эти фишки языка и в какой комбинации.