История одного модального окна или переходим с UIKit на SwiftUI. Часть 1

от автора

Всем привет. Сегодня хочу рассказать, как я делала модальное окно на SwiftUI (в приложении, которое полностью пока написано на UIKit, за исключением новых фич) и какие возникли сложности, а так же как с ними справилась. 

Вот дизайн, ничего необычного, по нажатию на TableViewCell мы видим модальное окно, в котором отображены имеющиеся сохраненные статьи, так же есть вариант получить с сервера новые статьи, отобразить progress view и затем опять вывести модальное окно с новой статьёй. 

Казалось бы, что может пойти не так?

Давайте начнём…

Дизайн

Дизайн

Для начала создадим View и наполним её по дизайну:

import SwiftUI  struct ReportsModalView: View {     @Environment(\.presentationMode) var presentationMode         // Переменные          init() {        // Здесь инит      }          var body: some View {         VStack {             VStack(spacing: 0) {                 setUpTopView()                 setUpTextView()                 setUpLikeShareButtons()                                  Divider()                                  HStack {                     setupLimitButtonsView()                     Spacer()                     setupNextPreviousButtonsView()                 }                 .padding(.top, 16)             }             .padding()             .frame(maxWidth: .infinity, alignment: .bottom)             .background(                 LinearGradient()         }     }    private func setUpTopView() -> some View {}   private func setUpTextView() -> some View {}   private func setUpDeleteAndQuestionView() -> some View {}   private func setUpLikeShareButtons() -> some View {}   private func setupNextPreviousButtonsView() -> some View {}   private func setupLimitButtonsView() -> some View {} }

Не буду здесь расписывать иниты и прочие функции для отрисовки View, так как в данном контексте это не важно (но если всё же важно, то полный код есть на моём GitHub).

Дальше нам остаётся только вызвать эту View в нашем существующем UIViewController и наслаждаться новой фичей. Вызывается очень просто:

let swiftUIView = ReportsModalView() let hostingController = UIHostingController(rootView: swiftUIView) hostingController.modalPresentationStyle = .automatic          DispatchQueue.main.async { [weak self] in     guard let self else { return }     self.present(hostingController, animated: true, completion: nil) }

Какой итог мы ожидаем — модальное окно как в UIKit, которое автоматически подстроится по высоте. Что мы получаем — модальное окно, которое по высоте всегда будет на весь экран… (специально подкрасила фон синим для наглядности). А так же скругленные края априори будут сверху, а не там, где начинается основной экран.

И вот тут меня ждало первое разочарование. Оказывается никак, никакими методами нельзя сделать такое же модальное окно, если вызывать его из UIKit. Дальше у меня ещё были попытки использовать какие-то сомнительные костыли, типа такого:

let bottomSheetView = ReportsView() let hostingController = UIHostingController(rootView: bottomSheetView)    // Make sure the SwiftUI view has the correct intrinsic size  hostingController.view.translatesAutoresizingMaskIntoConstraints = false  // Add the view temporarily to the view hierarchy (not visible) to measure its size  self.view.addSubview(hostingController.view)  hostingController.view.layoutIfNeeded()  // Calculate the target size based on the system layout fitting  let targetSize = hostingController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)  hostingController.preferredContentSize = targetSize    // Remove the temporarily added view after calculation  hostingController.view.removeFromSuperview()    // Set the corner radius for the hosting controller's view  hostingController.view.layer.cornerRadius = 16  hostingController.view.layer.masksToBounds = true  hostingController.view.backgroundColor = UIColor(hex: "#EBF5FF")    if let sheet = hostingController.sheetPresentationController {    if #available(iOS 16.0, *) {      sheet.detents = [.custom(resolver: { _ in (targetSize.height) })]    } else {      // Fallback on earlier versions      sheet.detents = [.medium()]    }  }  present(hostingController, animated: true, completion: nil)

Тут было плохо примерно всё: View все равно не пересчитывалась по высоте, работало криво и через раз. Поэтому я довольно быстро бросила эту затею и начала думать уже что можно сделать с самой View. В какой-то момент я даже хотела плюнуть и сделать уже всё на UIKit, но вовремя опомнилась. Всё же рано или поздно все перейдут на SwiftUI (как это было с Objective-C) и это только вопрос времени. Поэтому было решено сделать маленький костыль, который легко убрать, когда основной UIViewController так же будет на SwiftUI.

Вот моё решение:

var body: some View {         VStack {                 setUpTopView()                                 ... контент без изменений         }           // Добавляем прозрачность для фона                    .background(Color(white: 0, opacity: 0.4))              }          private func setUpTopView() -> some View {         return HStack {             ... без изменений         }              // Добавляем RoundedRectangle в background                .background(             RoundedRectangle(                 cornerRadius: 20,                 style: .continuous             )             .fill(Color(UIColor(hex: "#ECEBFF")))             .frame(height: 64)             .frame(width: UIScreen.main.bounds.width)             .padding([.top], -64)         )     }   }   // В UIViewController: let swiftUIView = ReportsModalView() let hostingController = UIHostingController(rootView: swiftUIView)  // Добавим clear background и modalPresentationStyle - overFullScreen  hostingController.view.backgroundColor = .clear hostingController.modalPresentationStyle = .overFullScreen  hostingController.hidesBottomBarWhenPushed = true          DispatchQueue.main.async { [weak self] in   guard let self else { return }   self.present(hostingController, animated: true, completion: nil) }

В итоге получаем наше модальное окно:

Вот как-то так, легко и непринужденно встраиваем SwiftUI потихоньку в проект. Ладно, на самом деле есть некоторые сложности, в следующих частях покажу как делать ProgressView и SkeletonView.

Я даже сделала рилс на тему этой, на первой взгляд, быстрой фичи: https://t.me/NataWakeUp/434


ссылка на оригинал статьи https://habr.com/ru/articles/848972/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *