Skip to content

Latest commit

 

History

History
459 lines (324 loc) · 19.3 KB

4.1.4.3 ConveniencePublishers.md

File metadata and controls

459 lines (324 loc) · 19.3 KB

Convenience Publishers

  1. Result<Int, Error>.Publisher - это паблишер, генерирует событие либо ошибку всего один раз, а потом замолкает.
    • Just<Int> - отправляет выходные данные каждому подписчику только один раз, а после завершает работу с finished статусом. (Value, Never). Value - если nil, то попадаем в receiveValue. Само value = nil
    • Fail<Output, Failure> - не отправляет значения подписчику, а немедленно завершает работу с указанной ошибкой. P.S.: отправляет значение со статусом finished если добавить replaceError

    • Optional<Int?>.Publisher — это паблишер, который генерирует событие всего один раз, а потом замолкает. (Value?, Never). Value? - если nil, то не попадаем в receiveValue
    • Empty<nil!, Error> - никогда не отправляет никаких значений и завершает работу немедленно. P.S.: отправляет значение если добавить .replaceEmpty

  2. Publishers for closure:
    • Future<Int, Never> - он начинает выполняться сразу при инициализации, не дожидаясь подписчика. Подписчик может «пропустить» нужное ему событие.
    • Deferred - что заставить Future вести себя менее эгоистично и дождаться подписчика оборачиваем его в блок Deferred { }.
  3. Publishers for value: Subject — это паблишер, который позволяет отправлять в поток данных события (value, completion) извне ручками. Могут поддерживать несколько подписчиков, что означает, что они находятся в отношениях «один ко многим подписчикам»
    • PassthroughSubject - паблишер, от которого подписчик получит все события, которые сгенерировал паблишер ПОСЛЕ того, как подписчик на него подписался. Этот паблишер не хранит события для подписчиков — «отправил и забыл». Поэтому у него нет первоначального значения
    • CurrentValueSubject -
    • @Published -
  4. ConnectablePublisher
    • Autoconnect -
    • MakeConnectable -
    • Multicast -

1. Combine Result Publishers

Open

Result

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

enum ResultError: Error {
    case testError
}
let error: ResultError = .testError
let value = 10
var result: Result<Int, Error> = .failure(error)
let resultPublisher: Result<Int, Error>.Publisher = result.publisher
resultPublisher
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("completion status: \(completion)")
            case .failure(let error):
                print("recieved error: \(error)")
            }
        }, receiveValue: { value in
            print("received value: \(value)")
        }
    )

Консоль:

recieved error: testError

Если изменим var result: Result<Int, Error> = .success(value), то консоль:

received value: 10 
completion status: finished


Just

У Just есть несколько особенностей. Во-первых, он всегда генерирует событие (в отличие от опционала). Например, если в качестве output мы поставим опциональный String, а остальной код оставим как есть, то в консоли увидим следующее:

let stringPublisher: Just<String?> = Just(nil)

stringPublisher
    .sink(
        receiveCompletion: { completion in
            print("completion status: \(completion)")
        }, receiveValue: { value in
            print("recieved value: \(value)")
        }
    )

Консоль:

recieved value: nil
completion status: finished

Второй особенностью Just является то, что его тип ошибки — Never, то есть он никогда не может завершиться с ошибкой. Даже если мы укажем опциональный тип данных и придет nil, подписчик решит, что это не ошибка и с этими данными можно работать.

Если нам все-таки нужно как-то разграничивать успешный Output и Error, паблишер Just не подойдет. Для этих целей используется паблишер Result.


Fail -


Optional

Его особенностью является то, что если value == nil, то паблишер вообще ничего не пришлет, а в блоке recieveCompletion уведомит о том, что он «всё».

var intValue: Int? = nil
let optionalPublisher: Optional<Int>.Publisher = intValue.publisher

optionalPublisher
    .sink(
        receiveCompletion: { completion in
            print("completion status: \(completion)")
        }, receiveValue: { value in
            print("received value: \(value)")
        }
    )

Консоль:

completion status: finished

В то же время, если значение не будет равно nil, то в блоке receiveValue оно уже будет извлечено и нам не надо будет использовать оператор guard или if-let.

var intValue: Int? = 10

Консоль:

received value: 10
completion status: finished


Empty

Помимо Empty(), вы можете добавить .append(Empty) к любому издателю, чтобы создать пустой паблишер:

Just(1)
    .append(Empty(completeImmediately: false))
    .sink(
        receiveCompletion: { print("completion: \($0)") },
        receiveValue: { print("value: \($0)") })

Output: value: 2

Just(1)
    .append(Empty(completeImmediately: true))
    .sink(
        receiveCompletion: { print("completion: \($0)") },
        receiveValue: { print("value: \($0)") })

Output: value: 2 completion: finished


2. Publishers for closure: Future, Deferred

Open


Future

Future<Int, Never> - в качестве параметра он принимает замыкание, и разработчики могут использовать promise внутри замыкания для отправки результата, выполняется сразу при инициализации, не дожидаясь подписчика. Подписчик может «пропустить» нужное ему событие.

Future можно использваоть когда мы объединяем некоторые асинхронные задачи в издателе и получаем только один результат. например, получение данных из локальной базы данных или загрузка данных с удаленного сервера.

Future { promise in
    AF.request("https://domain/path/to").response { response in
        let id model = toModel(response) else {
            promise(.success(model))
        } else {
            promise(.failure(someError))
        }
    }
}.sink { x in
    print(x) /// [model]
}


Deferred-

  1. Использование промисов и фьючерсов в сочетании


3. Publishers for value: Subject

Open

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

Он выражен протоколом с двумя методами:

.send() — чтобы отправлять в поток конкретные данные.

.send(completion: ) — чтобы уведомить подписчиков о том, что паблишер завершил генерацию данных и больше ничего присылать не будет.

Subject создан для использования в качестве обертки над императивными кусками кода. В отличие от Future, им удобно оборачивать свойства, а не методы с @escaping closure. Еще одно отличие от Future заключается в том, что Subject не является one-shot и мы можем отправлять в Data-stream сколько угодно событий.

У этого паблишера есть 2 имплементации: PassthroughSubject и CurrentValueSubject.

Кстати, в случае с обоими паблишерами Subject, если мы попробуем отправить после комплишена еще какое-нибудь значение, то ничего не произойдет (ни крашей, ни ошибок компилятора), а подписчики просто его не получат.

subject.send(4)
subject.send(completion: .finished)
subject.send(10)
PassthroughSubject

PassthroughSubject — паблишер, от которого подписчик получит все события, которые сгенерировал паблишер ПОСЛЕ того, как подписчик на него подписался. Этот паблишер не хранит события для подписчиков — «отправил и забыл». Поэтому у него нет первоначального значения:

let passthru = PassthroughSubject<Int, Never>()

passthru.send(2)
passthru.send(3)

let c1 = passthru.sink { print($0) }
let c2 = passthru.sink { print($0) }
let c3 = passthru.sink { print($0) }

passthru.send(4)
passthru.send(completion: .finished)

Вывод:

4 4 4

На примере выше мы инициализировали паблишер, который генерит объекты типа Int и не может зафейлиться.

Отправили с помощью метода .send объекты (2,3), после чего подписались, отправили еще один объект (4) и в конце отправили подписчикам событие того, что паблишер завершил генерацию событий. Но в консоль вывелось только 2 последних события: объект типа Int и сообщение finished.


CurrentValueSubject

CurrentValueSubject — почти тоже самое, что и PassthroughSubject, но с рядом отличий:

  1. При инициализации имеет начальное значение и обновляет его при вызове методов отправки. Это гарантирует, что подписчикам будет доставлено последнее значение сразу после оформления подписки.

  2. У паблишера есть свойство value, которое хранит в себе последнее актуальное значение. Мы можем к нему обратиться через точечный синтаксис.

  3. Подписчик не только получает от паблишера все новые значения ПОСЛЕ подписки, но и то значение, которое в себе хранит паблишер ДО того, как на него подписался подписчик (это может быть первоначальное значение, указанное при инициализации паблишера, либо последнее сгенерированное событие, если паблишер успел что-то отправить в поток до того, как на него кто-то подписался.

  4. У него есть буфер для оптимизации — если паблишер часто генерит одни и те же значения.

let currentVal = CurrentValueSubject<Int, Never>(1) // затерлось .send(2)

currentVal.send(2) // затерлось .send(3)
currentVal.send(3)

let c1 = currentVal.sink { print($0) }
let c2 = currentVal.sink { print($0) }
let c3 = currentVal.sink { print($0) }

currentVal.send(4)

Вывод:

3 3 3 4 4 4


4. ConnectablePublisher

Open

Autoconnect

Издатель AutoConnect используется для автоматического «открытия клапана» при вызове метода подписки (sink).

let timerPublisher = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()

let cancellable = timerPublisher.sink { value in
    print(value)
}

Звучит бесполезно, но это полезно, когда нам нужно отменить подключаемую функцию, если ее восходящим потоком является ConnectablePublisher:

DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
    cancell.cancel()
}

Избегайте автоматического подключения, если порядок подписки имеет решающее значение, избегайте использования автоматического подключения, поскольку это может привести к недетерминированному поведению. Вместо этого вручную подключите издателя с помощью метода Connect().


MakeConnectable

Напротив, предположим, что у вас есть приложение, которое отображает актуальную информацию о погоде. ConnectablePublisher предоставляет информацию о погоде. Но предположим, что перед отображением информации о погоде происходит некоторый сложный рендеринг или анимация пользовательского интерфейса. В этом случае вы можете вызвать метод connect() после завершения обновлений пользовательского интерфейса, чтобы пользователю была предоставлена самая последняя информация. `AutoConnect здесь не подойдет.

let passthru = PassthroughSubject<Int, Never>()
let connectable = passthru.makeConnectable()
let cancellable1 = connectable.sink { x in
    print(x) /// [3,4]
}
passthru.send(1)
passthru.send(2)
let cancellable2 = connectable.connect()
passthru.send(3)
passthru.send(4)

Когда вызывается метод connect, мы обновляем флаг connected до true и вызываем закрытие, чтобы установить отношения подписки, которые "откроют клапан". Возвращаемая cancellable используется, чтобы решить, когда уничтожить (cancel) отношения, которые "закроют клапан".ё


Multicast

Multicast полезен, когда мы хотим преобразовать обычного издателя в subject, который может отправлять событие нескольким подписчикам одновременно.

let multi = [ 1 , 2 , 3 , 4 ].publisher.multicast(subject: PassthroughSubject ()) 
let c1 = multi.sink { x in 
    print ( "sink1 \(x) " ) /// [1,2,3,4]
 } 
let c2 = multi.sink { x in 
    print ( "sink2 \(x) " ) /// [1,2,3,4]
 } 
let c3 = multi.sink { x в 
    печати ( "sink3 \(x) " ) /// [1,2,3,4]
 } 
let cancellable = multi.connect()

@Published

Open

Чаще всего @Published используется для изменения свойств ObservableObject, потому что @Published создаст внутри себя CurrentValueSubject, который будет отправлять события каждый раз, когда это свойство изменяется.

ObservableObject использует в связке с @Published для агрегирования всех событий, сгенерированных @Published, что означает, что мы можем подписаться на издателя ObservableObject вместо подписки на каждое из его @Published.

Издатели (SwiftUI предлагает StateObject, ObservedObject и EnvironmentObject, все из которых подписываются на агрегированного издателя ObservableObject и автоматически обновляют пользовательский интерфейс).

Имплементация @Published:

@propertyWrapper
struct Published<T> {
    
    let currentValue: CurrentValueSubject<T, Never>
    
    init(wrappedValue: T) {
        currentValue = CurrentValueSubject(wrappedValue)
    }
    
    var wrappedValue: T {
        get { currentValue.value }
        set { currentValue.send(newValue) }
    }
    
    var projectedValue: CurrentValueSubject<T, Never> { currentValue }
}


4.1.4.2 Publisher Theme | Back To iOSWiki Contents | 4.1.4.4 Operators Theme