The story of one modal window or moving from UIKit to SwiftUI. Part 1

Hi all. Today I want to tell you how I made a modal window on SwiftUI (in an application that is still entirely written in UIKit, with the exception of new features) and what difficulties arose, as well as how I dealt with them.

Here is the design, nothing unusual, by clicking on the TableViewCell we see a modal window in which the existing saved articles are displayed, there is also an option to get new articles from the server, display a progress view and then again display a modal window with a new article.

It would seem, what could go wrong?

Let's get started…

Design

Design

First, let's create a View and fill it in by design:

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 {}
}

I will not describe here the inits and other functions for rendering the View, since in this context it is not important (but if it is still important, then the full code is on my GitHub).

Then all we have to do is call this View in our existing UIViewController and enjoy the new feature. Called very simply:

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)
}

What result do we expect – a modal window like in UIKit, which will automatically adjust in height. What we get is a modal window, which will always fill the entire screen in height… (I specially tinted the background blue for clarity). Also, the rounded edges will a priori be on top, and not where the main screen begins.

And here the first disappointment awaited me. It turns out there is no way, no methods can be used to create the same modal window if you call it from UIKit. Then I still had attempts to use some dubious crutches, like this:

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)

Almost everything was bad here: View still did not recalculate in height, it worked crookedly and every now and then. Therefore, I quickly abandoned this idea and began to think about what could be done with the View itself. At some point I even wanted to give up and do everything on UIKit, but I came to my senses in time. Still, sooner or later everyone will switch to SwiftUI (as was the case with Objective-C) and it’s only a matter of time. Therefore, it was decided to make a small crutch that can be easily removed when the main UIViewController is also on SwiftUI.

Here's my solution:

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)
}

As a result, we get our modal window:

This is how we slowly and easily integrate SwiftUI into the project. Okay, in fact there are some difficulties, in the next parts I will show how to make ProgressView and SkeletonView.

I even made a video on this seemingly quick feature: https://t.me/NataWakeUp/434

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *