#ios #swift #rx-swift #reactive-swift
#iOS #swift #rx-swift #реактивный-быстрый
Вопрос:
У меня есть простой сигнал в одном из компонентов приложения, который возвращает массив элементов:
var itemsSignal: Signal<[Item], Never>
Эти элементы могут содержать обновления для данных, которые отображаются на экране в виде табличного представления. Задача состоит в том, чтобы применить обновления к ячейкам, если они присутствуют на экране.
Я могу придумать два возможных способа, как это можно сделать. Приложение написано в стиле MVVM, но я просто для примера.
Первый способ — подписаться на этот сигнал один раз на уровне кода контроллера представления, затем при observeValues
проверке блока везде, где мы получаем обновления для элементов на экране с помощью некоторого for-цикла, и обновлять состояния для соответствующих ячеек. Таким образом, у нас будет только одна подписка, но это приведет к ненужному, на мой взгляд, связыванию кода, когда мы в основном используем код уровня контроллера представления для передачи обновлений из источника в отдельные ячейки на экране.
Второй способ — подписаться на этот сигнал из каждой отдельной ячейки (в модели представления ячейки в реальности) и применить некоторую фильтрацию следующим образом:
disposables = COMPONENT.itemsSignal
.flatten()
.filter({ $0.itemId == itemId })
.observeValues({
...
})
Но это создает несколько подписок — по одной для каждой отдельной ячейки.
На самом деле я предпочитаю второй метод, поскольку, на мой взгляд, он намного чище с точки зрения дизайна, поскольку он не пропускает никаких ненужных знаний для просмотра кода уровня контроллера. И везде, где одна и та же ячейка повторно используется на другом экране, это самообновляющееся поведение будет унаследовано и будет работать «из коробки».
Вопрос в том, насколько дороже второй метод памяти / процессора из-за нескольких подписок? В этом проекте мы используем ReactiveSwift
, но я думаю, что это актуально и для других Rx
библиотек.
Ответ №1:
В RxSwift наша библиотека RxCocoa уже реализует вашу первую идею и выполняет простую повторную загрузку данных в TableView.
items
.bind(to: tableView.rx.items) { (tableView, row, element) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "(element) @ row (row)"
return cell
}
или вы можете указать библиотеке, какого типа будет ячейка, и она создаст ячейку для вас:
items
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
cell.textLabel?.text = "(element) @ row (row)"
}
У нас также есть опция, о которой вы не упомянули. Одна подписка, но более интеллектуальный источник данных, который способен добавлять и удалять отдельные ячейки на основе равенства элементов передаваемой последовательности. Это в отдельной библиотеке называется RxDataSources
.
В качестве ответа на ваш основной вопрос о ресурсах… Я часто использую гибридное решение, в котором есть наблюдаемый объект, который просто содержит последовательность идентификаторов объектов; это ваша первая идея, но она заботится только о вставке и удалении элементов. Я сделаю второе наблюдение [ID: Info]
за тем, на что подписывается каждая существующая в настоящее время ячейка. При создании / повторном использовании ячейки ей присваивается идентификатор, она подписывается на этот второй наблюдаемый объект и отфильтровывает только интересующую ее информацию. В ячейке prepareForReuse
он отписывается.
Поскольку это только одна подписка на существующую ячейку, на самом деле не так много новых подписок (в зависимости от высоты ячейки и высоты табличного представления). В моих приложениях обычно одновременно выполняются тысячи подписок, поэтому количество дополнительных подписок, добавленных при таком подходе «на ячейку», даже не заметно.
Ответ №2:
Чисто с точки зрения реактивного программирования, я понимаю, почему второй подход привлекателен. Но природа UITableView
такова, что вам действительно нужно включать таблицу в обновления строк. Самый простой способ сделать это — использовать новомодный UITableViewDiffableDataSource
, представленный в iOS 13, но если вы его не используете, тогда необходимо вызвать либо reloadData
или reloadRows(at:)
сообщить таблице обновить все строки или определенные строки при изменении их данных.
Если вы подписываетесь на каждую ячейку, может показаться, что это работает в определенных или большинстве ситуаций, но если у вас когда-либо будет обновление данных, которое изменяет высоту ячейки (например, путем отображения более длинного текста в метке и переноса его во вторую строку), размер ячейки не будет изменен должным образом, посколькутаблица не знает, что строка была обновлена.
Я согласен с вами, что неприятно, что строки не могут управлять своими собственными обновлениями совершенно автономно, но UITableView
, насколько я понимаю, это работает по соображениям производительности.
Комментарии:
1. Высота содержимого здесь не является фактором, мне интересно только о занимаемой памяти и, возможно, о некоторых других накладных расходах обоих решений.
2. Если вы создадите подписку
tableView(_:cellForRowAt:)
и отмените ее при повторном использовании ячейки (может быть, с чем-то вроде.take(until: cell.reactive.prepareForReuse)
?), Я не могу представить, что объем памяти будет большой проблемой, потому что подписок будет столько, сколько ячеек видно на экране. Но если вы пытаетесь заранее создать и подписаться на модель представления каждой ячейки, это определенно создаст ненужные накладные расходы, в зависимости от количества элементов.3. Но это также, вероятно, потребует от вас создания
itemSignal
MutableProperty<[Item]>
, чтобы вы всегда могли получить последний массив элементовtableView(_:cellForRowAt:)
.