4
votes

I'm integrating some new SwiftUI Views in a UIKit application but I ran into a problem. I've looked at the following issue for quite a while but am yet to find a cause and solution. The problem specifically occurs when integrating the View in UIKit. I'm trying to create a simple tappable View that expands/collapses vertically when tapped.

This is how the preview of just the SwiftUI View looks like (and exactly how it should behave):
Screenshot
Video

And here is what I get when I implement the SwiftUI View in UIKit:
Screenshot
Video

It seems like even though I constrain the top op the UIHostingController view to the parent's view, the UIHostingController view gets vertically centered.

As mentioned in the comments, I could constrain the bottom of the HostController's view to the bottom of its parent, but that would make the content below uninteractable.

What I'm looking for is a solution where the HostController view constraints (specifically the height) matches the SwiftUI View frame.

The code for the SwiftUI View:

import SwiftUI

struct ColorView: View {
  @State var isCollapsed = true

  var body: some View {
    VStack {
      VStack(spacing: 5) {
        HStack {
          Spacer()
          Text("Title")
          Spacer()
        }
        .frame(height: 100)

        if !isCollapsed {
          HStack {
            Spacer()
            Text("description")
            Spacer()
          }
          .padding(40)
        }
      }
      .background(Color(isCollapsed ? UIColor.red : UIColor.blue))
      .onTapGesture {
        withAnimation {
          self.isCollapsed.toggle()
        }
      }

      Spacer()
    }
  }
}

struct ColorView_Previews: PreviewProvider {
  static var previews: some View {
    return ColorView()
  }
}

And the UIKit implementation in the ViewController of the above-mentioned SwiftUI View:

struct ViewControllerRepresentable: UIViewControllerRepresentable {
  typealias UIViewControllerType = ViewController

  func makeUIViewController(context: UIViewControllerRepresentableContext<ViewControllerRepresentable>) -> ViewControllerRepresentable.UIViewControllerType {
    return ViewController()
  }

  func updateUIViewController(_ uiViewController: ViewControllerRepresentable.UIViewControllerType, context: UIViewControllerRepresentableContext<ViewControllerRepresentable>) { }
}

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let colorView = ColorView()
    let colorController = UIHostingController(rootView: colorView)

    addChild(colorController)
    view.addSubview(colorController.view)
    colorController.didMove(toParent: self)

    colorController.view.translatesAutoresizingMaskIntoConstraints = false
    colorController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    colorController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    colorController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
  }
}

struct ViewControllerRepresentable_Previews: PreviewProvider {
  static var previews: some View {
    Group {
      ViewControllerRepresentable()
    }
  }
}

Any help is greatly appreciated.

1

1 Answers

0
votes

Here is the snapshot of code that was tested as worked in iOS Single View Playground (Xcode 11.2). The original ColorView was not changed, so not duplicated below.

The behaviour of integrated SwiftUI view inside UIKit UIViewController is the same as in pure SwiftUI environment.

import UIKit
import PlaygroundSupport
import SwiftUI

class MyViewController : UIViewController {
    override func loadView() {
        let view = UIView()
        view.backgroundColor = .white
        self.view = view
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let child = UIHostingController(rootView: ColorView())
        addChild(child)
        self.view.addSubview(child.view)
        child.didMove(toParent: self)

        child.view.translatesAutoresizingMaskIntoConstraints = false
        child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        child.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
    }
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()