-1

I've been having issues getting a UIViewControllerRepresentable wrapping a UIPageViewController to play nicely with SwiftUI. Specifically, I can't get the searchable modifier to work properly when the UIViewControllerRepresentable is in the view hierarchy. This modifier works properly if I replace the UIViewControllerRepresentable with a TabView with a .tabViewStyle(.page).

Here's some sample code:

import SwiftUI

struct ContentView: View {
  @State private var search = ""

  var body: some View {
    NavigationView {
      ControllerPager()
        .navigationTitle("Title")
    }
    .searchable(text: $search)
  }
}

struct ControllerPager: UIViewControllerRepresentable {
  func makeUIViewController(context: Context) -> UIPageViewController {
    UIPageViewController(
      transitionStyle: .scroll, navigationOrientation: .horizontal)
  }

  func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
    pageViewController.setViewControllers(
      [UIHostingController(rootView: Page())],
      direction: .forward,
      animated: true)
  }
}

struct NativePager: View {
  var body: some View {
    TabView {
      Page()
    }
    .tabViewStyle(.page(indexDisplayMode: .never))
  }
}

struct Page: View {
  var body: some View {
    List {
      ForEach(0..<100) { i in
        Text("\(i)")
      }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

With this code (where I'm not using a TabView), the searchable box doesn't appear in the NavigationView. If I replace the ControllerPager() with NativePager() it does.

For those wondering why I'm using UIViewControllerRepresentable instead of a TabView, I need a dynamic data source for the UIPageViewController that updates as the user swipes between pages. I tried this previous answer but the TabView behaved strangely as I was changing the state while the user was swiping between pages.

Thanks for any help!

jmacdonagh
  • 3,851
  • 3
  • 18
  • 14

1 Answers1

0

I have found a solution:

  var body: some View {
    NavigationView {
      ZStack {
        Text(" ")

        ControllerPager()
          .navigationTitle("Title")
        }
    }
    .searchable(text: $search)
  }

I used Xcode's Capture View Hierarchy debugging and saw that both the ControllerPager and NativePager were adding a UISearchBar, but the ControllerPager's height was larger, seemingly covering over the UISearchBar.

While looking through tutorials on SwiftUI layout I saw somewhere that UIViewControllerRepresentable needs a frame to properly work with the SwiftUI layout system. Some people solved this by adding fixed height frames to their UIViewControllerRepresentable but that seemed kinda hacky for something that should just take up as much space as it needs. I played around wrapping different parts in GeometryReaders to be able to compute a proper frame but it would always return a height that was too large.

I was then curious as to how Apple solved this in their Interfacing with UIKit tutorial. The key that I originally missed was that their end solution uses a ZStack with the UIViewControllerRepresentable wrapping a UIPageViewController and a UIViewRepresentable wrapping a UIPageControl. So something in that ZStack could properly interface with the SwiftUI layout system and the UIViewControllerRepresentable would simply inherit the frame that was computed for it.

A Text with a single space as content is a bit hacky but what I found was that I needed an element that would get "rendered". If I set .opacity(0), used an empty VStack, or used something like Color(uiColor: .clear) it wouldn't work. Even with something like Text("hello"), that text would never actually be visible, even as I swiped between pages.

Now that I have a handle on what the actual issue is I'll look for less hacky solutions, but for now, this works.

jmacdonagh
  • 3,851
  • 3
  • 18
  • 14