Как работать с готовыми библиотеками под UIKit? | OTUS
⚡ Подписка на курсы OTUS!
Интенсивная прокачка навыков для IT-специалистов!
Подробнее

Курсы

Программирование
Python Developer. Professional
-3%
Разработчик на Spring Framework
-5%
iOS Developer. Professional
-8%
Golang Developer. Professional
-6%
Базы данных
-12%
Agile Project Manager
-5%
Android Developer. Professional
-11%
Microservice Architecture
-5%
C++ Developer. Professional
-5%
Highload Architect
-6%
JavaScript Developer. Basic
-8%
Backend-разработчик на PHP
-9%
Архитектура и шаблоны проектирования C# Developer. Professional
-9%
Team Lead
-6%
Kotlin Backend Developer
-9%
Разработчик программных роботов (RPA) на базе UiPath и PIX Unity Game Developer. Basic Разработчик голосовых ассистентов и чат-ботов Node.js Developer Интенсив «Оптимизация в Java» Супер - интенсив по паттернам проектирования Супер - интенсив по Kubernetes iOS Developer. Basic Супер-интенсив «СУБД в высоконагруженных системах» Супер-интенсив "Tarantool"
Инфраструктура
DevOps практики и инструменты
-12%
Базы данных
-12%
Network engineer. Basic
-10%
Network engineer
-4%
Инфраструктурная платформа на основе Kubernetes
-6%
Экспресс-курс по управлению миграциями (DBVC)
-10%
Мониторинг и логирование: Zabbix, Prometheus, ELK
-10%
Administrator Linux. Professional
-6%
Разработчик IoT
-13%
Основы Windows Server Cloud Solution Architecture Разработчик голосовых ассистентов и чат-ботов VOIP инженер Супер-практикум по работе с протоколом BGP NoSQL Супер-практикум по использованию и настройке GIT Супер-интенсив «СУБД в высоконагруженных системах» Экспресс-курс «IaC Ansible»
Специализации Курсы в разработке Подготовительные курсы
+7 499 938-92-02

Как работать с готовыми библиотеками под UIKit?

В прошлой статье мы начали разговор об адаптации существующих бизнес-решений под SwiftUI. В этот раз рассмотрим простой пример, как можно использовать готовую библиотеку под стандартное iOS-приложение в приложении на SwiftUI. И возьмем для этого классическое решение: асинхронную загрузку изображений с помощью библиотеки SDWebImage.

m8b2wexc6kdy6r2vrbs28x2thsq_1-1801-2ba625.png

Для удобства работа с библиотекой инкапсулирована в ImageManager, который вызывает:

  • SDWebImageDownloader;
  • SDImageCache; для скачивания изображений и кеширования.

По традиции, связь с принимающим результат UIImageView реализуется 2-мя способами:

  • через передачу weak-ссылки на этот самый UIImageView;
  • через передачу closure-блока в метод ImageManager.

nbgs_gqfxcqejinm7hcprmlhx14_1-1801-f82668.png

Обращение к 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 – структура. Наследование не поддерживается; ym8xhcod2mar5nwlioi6eo60kxc_1-1801-565973.png — 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:

lfsgerqzeqovuxkyomwuwh5ll1w_1-1801-ed9fd8.png

У такого подхода есть преимущества:

  • Не надо менять работу существующей библиотеки;
  • Логика инкапсулирована во встроенном 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).

exe6am9d0tuyhoau1v8nezurrnm_1-1801-152d5d.png

Для того, чтобы между 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 немного глубже.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться