Всем привет. Сегодня хочу рассказать, как я делала модальное окно на 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/
Добавить комментарий