Всем привет! 

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

Поделитесь в комментариях, в каких задачах вы используете эти фишки языка и в какой комбинации.

Комментарии (2)


  1. kedosipa2
    07.08.2023 16:09

    Привет можно у тебя взять кое какой комментарий касаемо свифт и состыковкой с телеграммом


    1. Azon Автор
      07.08.2023 16:09

      Добавил в дисклеймер ассоциацию с каналом iOS Broadcast, надеюсь, поможет состыковать