Доброго всем времени суток! С вами я, Анна Жаркова, ведущий мобильный разработчик компании «Usetech»
В этой части мы уже поговорим по делу, как можно адаптировать готовое решение к проекту на SwiftUI. Если вы еще не особо знакомы с этой технологией, то советую ознакомиться с кратким введением в тему.
Итак, рассмотрим простой пример, как можно использовать готовую библиотеку под стандартное iOS приложение в приложении на SwiftUI.
Возьмем классическое решение: асинхронная загрузка изображений с помощью библиотеки SDWebImage.

Для удобства работа с библиотекой инкапсулирована в ImageManager, который вызывает:
для скачивания изображений и кеширования.
По традиции, связь с принимающим результат UIImageView реализуется 2мя способами:

Обращение к ImageManager обычно инкапсулируется либо в расширении UIImageView:
либо в классе-наследнике:
Теперь попробуем прикрутить это решение к SwiftUI. Однако при адаптации мы должны учесть следующие особенности фреймворка:
— View – структура. Наследование не поддерживается

— Extension в привычном смысле бесполезны. Мы, конечно, можем написать некоторые методы для расширения функционала, но нам нужно как-то привязать это к DataFlow;
Получаем проблему получения обратной связи и необходимость адаптировать всю логику взаимодействия с UI к DataDriven Flow.
Для решения мы можем пойти как со стороны View, так и со стороны адаптации Data Flow.
Начнем с View.
Для начала вспомним, что SwiftUI существует не сам по себе, а как надстройка над UIKit. Разработчики SwiftUI предусмотрели механизм для использования в SwiftUI UIView, аналогов которых нет среди готовых контролов. Для таких случаев существуют протоколы UIViewRepresentable и UIViewControllerRepresentable для адаптации UIView и UIViewController соответственно.
Создадим структуру View, реализующую UIViewRepresentable, в котором переопределим методы:
в которых укажем, какие именно UIView мы используем, и зададим их базовые настройки. И не забудем PropertyWrappers для изменяемых свойств.
Полученный новый контрол можем встраивать в View SwiftUI:

У такого подхода есть преимущества:
Но появляются и новые обязанности. Во-первых, необходимо следить за управлением памятью в связке View-UIView. Т.к View структура, то вся работа с ними ведется фоново самим фреймворком. А вот очистка новых объектов ложится на плечи разработчика.
Во-вторых, необходимы дополнительные действия для настройки (размеры, стили). Если для View эти параметры включены по умолчанию, то с UIView их надо синхронизировать.
Например, для настройки размеров мы можем использовать GeometryReader, чтобы наше изображение занимало всю ширину экрана и определенную нами высоту:
В принципе для таких случаев использование встраиваемых UIView может быть расценено, как оверинженеринг. Поэтому теперь попробуем решить через DataFlow SwiftUI.
View у нас зависит от переменной состояния или группы переменных, т.е. от некой модели, которая сама может этой переменной состояния являться. По сути, это взаимодействие построено на паттерне MVVM.
Реализуем следующим образом:
Для того, чтобы между View и ViewModel была связь, ViewModel должна реализовывать протокол ObservableObject и подключаться к View как ObservedObject.
View в методе onAppear своего life-cycle вызывает метод ViewModel и получает итоговое изображение из ее свойства @Published:
Также для работы с DataFlow SwiftUI есть декларативный API Combine. Работа с ним очень похожа на работу с реактивными фреймворками (тот же RxSwift): есть субъекты, есть подписчики, есть похожие методы управления, есть cancellable (вместо Disposable).
Если бы наш ImageManager изначально был написан с использованием Combine, то решение бы имело такой вид.
Но т.к. ImageManager реализован у нас по другим принципам, то попробуем другой способ. Для генерации события мы будем использовать механизм PasstroughSubject, поддерживающий автозавершение подписок.
Новое значение будем отправлять при присвоении значения свойству UIImage нашей модели:
Итоговое значение наш View «слушает» в методе onReceive:
Итак, мы разобрали простой пример, как можно адаптировать существующий код под SwiftUI.
Что остается добавить. Если существовавшее iOS решение больше затрагивает UI-часть, то лучше использовать адаптацию через UIViewRepresentable. В остальных случаях нужна адаптация со стороны View-модель состояния.
В следующих частях мы рассмотрим, как адаптировать бизнес-логику существующего проекта к SwiftUI, работу с навигацией и затем копнем адаптацию к Combine немного глубже.
Более подробно про работу с View под SwiftUI смотрите тут.
В этой части мы уже поговорим по делу, как можно адаптировать готовое решение к проекту на SwiftUI. Если вы еще не особо знакомы с этой технологией, то советую ознакомиться с кратким введением в тему.
Итак, рассмотрим простой пример, как можно использовать готовую библиотеку под стандартное iOS приложение в приложении на SwiftUI.
Возьмем классическое решение: асинхронная загрузка изображений с помощью библиотеки SDWebImage.

Для удобства работа с библиотекой инкапсулирована в ImageManager, который вызывает:
- SDWebImageDownloader
- SDImageCache
для скачивания изображений и кеширования.
По традиции, связь с принимающим результат UIImageView реализуется 2мя способами:
- через передачу weak ссылки на этот самый UIImageView;
- через передачу closure-блока в метод ImageManager

Обращение к ImageManager обычно инкапсулируется либо в расширении UIImageView:
extension UIImageView { func setup(by key: String) { ImageManager.sharedInstance.setImage(toImageView: self, forKey: key) } }
либо в классе-наследнике:
class CachedImageView : UIImageView { private var _imageUrl: String? var imageUrl: String? { get { return _imageUrl } set { self._imageUrl = newValue if let url = newValue, !url.isEmpty { self.setup(by: url) } } } func setup(by key: String) { ImageManager.sharedInstance.setImage(toImageView: self, forKey: key) } }
Теперь попробуем прикрутить это решение к SwiftUI. Однако при адаптации мы должны учесть следующие особенности фреймворка:
— View – структура. Наследование не поддерживается

— Extension в привычном смысле бесполезны. Мы, конечно, можем написать некоторые методы для расширения функционала, но нам нужно как-то привязать это к DataFlow;
Получаем проблему получения обратной связи и необходимость адаптировать всю логику взаимодействия с UI к DataDriven Flow.
Для решения мы можем пойти как со стороны View, так и со стороны адаптации Data Flow.
Начнем с View.
Для начала вспомним, что SwiftUI существует не сам по себе, а как надстройка над UIKit. Разработчики SwiftUI предусмотрели механизм для использования в SwiftUI UIView, аналогов которых нет среди готовых контролов. Для таких случаев существуют протоколы UIViewRepresentable и UIViewControllerRepresentable для адаптации UIView и UIViewController соответственно.
Создадим структуру View, реализующую UIViewRepresentable, в котором переопределим методы:
- makeUiView;
- updateUIView
в которых укажем, какие именно UIView мы используем, и зададим их базовые настройки. И не забудем PropertyWrappers для изменяемых свойств.
struct WrappedCachedImage : UIViewRepresentable { let height: CGFloat @State var imageUrl: String func makeUIView(context: Context) -> CachedImageView { let frame = CGRect(x: 20, y: 0, width: UIScreen.main.bounds.size.width - 40, height: height) return CachedImageView(frame: frame) } func updateUIView(_ uiView: CachedImageView, context: Context) { uiView.imageUrl = imageUrl uiView.contentMode = .scaleToFill } }
Полученный новый контрол можем встраивать в View SwiftUI:

У такого подхода есть преимущества:
- Не надо менять работу существующей библиотеки
- Логика инкапсулирована во встроенном UIView.
Но появляются и новые обязанности. Во-первых, необходимо следить за управлением памятью в связке View-UIView. Т.к View структура, то вся работа с ними ведется фоново самим фреймворком. А вот очистка новых объектов ложится на плечи разработчика.
Во-вторых, необходимы дополнительные действия для настройки (размеры, стили). Если для View эти параметры включены по умолчанию, то с UIView их надо синхронизировать.
Например, для настройки размеров мы можем использовать GeometryReader, чтобы наше изображение занимало всю ширину экрана и определенную нами высоту:
var body: some View { GeometryReader { geometry in VStack { WrappedCachedImage(height:300, imageUrl: imageUrl) .frame(minWidth: 0, maxWidth: geometry.size.width, minHeight: 0, maxHeight: 300) } } }
В принципе для таких случаев использование встраиваемых UIView может быть расценено, как оверинженеринг. Поэтому теперь попробуем решить через DataFlow SwiftUI.
View у нас зависит от переменной состояния или группы переменных, т.е. от некой модели, которая сама может этой переменной состояния являться. По сути, это взаимодействие построено на паттерне MVVM.
Реализуем следующим образом:
- создадим кастомный View, внутри которого будем использовать контрол SwiftUI;
- создадим ViewModel, в которую перенесем логику работы с Model (ImageManager).

Для того, чтобы между View и ViewModel была связь, ViewModel должна реализовывать протокол ObservableObject и подключаться к View как ObservedObject.
class CachedImageModel : ObservableObject { @Published var image: UIImage = UIImage() private var urlString: String = "" init(urlString:String) { self.urlString = urlString } func loadImage() { ImageManager.sharedInstance .receiveImage(forKey: urlString) {[weak self] (im) in guard let self = self else {return} DispatchQueue.main.async { self.image = im } } } }
View в методе onAppear своего life-cycle вызывает метод ViewModel и получает итоговое изображение из ее свойства @Published:
struct CachedLoaderImage : View { @ObservedObject var model:CachedImageModel init(withURL url:String) { self.model = CachedImageModel(urlString: url) } var body: some View { Image(uiImage: model.image) .resizable() .onAppear{ self.model.loadImage() } } }
Также для работы с DataFlow SwiftUI есть декларативный API Combine. Работа с ним очень похожа на работу с реактивными фреймворками (тот же RxSwift): есть субъекты, есть подписчики, есть похожие методы управления, есть cancellable (вместо Disposable).
class ImageLoader: ObservableObject { @Published var image: UIImage? private var cancellable: AnyCancellable? func load(url: String) { cancellable = ImageManager.sharedInstance.publisher(for: url) .map { UIImage(data: $0.data) } .replaceError(with: nil) .receive(on: DispatchQueue.main) .assign(to: \.image, on: self) }
Если бы наш ImageManager изначально был написан с использованием Combine, то решение бы имело такой вид.
Но т.к. ImageManager реализован у нас по другим принципам, то попробуем другой способ. Для генерации события мы будем использовать механизм PasstroughSubject, поддерживающий автозавершение подписок.
var didChange = PassthroughSubject<UIImage, Never>()
Новое значение будем отправлять при присвоении значения свойству UIImage нашей модели:
var data = UIImage() { didSet { didChange.send(data) } } Обратите внимание, здесь нет модификатора свойств.
Итоговое значение наш View «слушает» в методе onReceive:
var body: some View { Image(uiImage: image) .onReceive(imageLoader.didChange) { im in self.image = im //какие-то действия с изображением } }
Итак, мы разобрали простой пример, как можно адаптировать существующий код под SwiftUI.
Что остается добавить. Если существовавшее iOS решение больше затрагивает UI-часть, то лучше использовать адаптацию через UIViewRepresentable. В остальных случаях нужна адаптация со стороны View-модель состояния.
В следующих частях мы рассмотрим, как адаптировать бизнес-логику существующего проекта к SwiftUI, работу с навигацией и затем копнем адаптацию к Combine немного глубже.
Более подробно про работу с View под SwiftUI смотрите тут.
