nalexn / ViewInspector

Runtime introspection and unit testing of SwiftUI views
MIT License
2.09k stars 145 forks source link

View for `UIViewControllerRepresentable` is absent #300

Open quanshousio opened 2 months ago

quanshousio commented 2 months ago

Hello @nalexn,

Awesome library by the way. I'm currently trying to use it to test one of the libraries that I'm maintaining. However, I've encountered an issue wherein .viewController() fails to locate the underlying UIViewController in a UIViewControllerRepresentable. I'm entirely not sure if I'm overlooking something in my approach. Hence, I'd greatly appreciate any help.

Here's a minimal reproducible example based on my library's implementation. I use background() modifier as a bridge for the UIKit implementation behind the scene.

Test was run using Xcode 15.3, iOS 17.4 simulator on macOS 14.4.1.

Let me know if you need additional information. Thank you!

import Combine
import SwiftUI
import ViewInspector
import XCTest

final class Test: XCTestCase {
  func testBridge() throws {
    let sut = ContentView()
    let exp = sut.inspection.inspect(after: 0.1) { view in
      XCTAssertTrue(try view.actualView().isPresented)

      let bridge = try view.find(UIViewControllerBridge<Text>.self).actualView()
      XCTAssertTrue(bridge.isPresented)

      let viewController = try bridge.viewController() // failed - View for UIViewControllerBridge<Text> is absent
      XCTAssertTrue(viewController.internalState)
    }

    ViewHosting.host(view: sut)
    wait(for: [exp], timeout: 1.0)
  }
}

struct ContentView: View {
  @State var isPresented = false
  internal let inspection = Inspection<Self>()

  var body: some View {
    Text("text")
      .onReceive(inspection.notice) {
        inspection.visit(self, $0)
      }
      .onAppear {
        isPresented = true
      }
      .background(
        UIViewControllerBridge(isPresented: $isPresented) {
          Text("pop up")
        }
      )
  }
}

struct UIViewControllerBridge<Content>: UIViewControllerRepresentable where Content: View {
  @Binding var isPresented: Bool
  let content: () -> Content

  func makeUIViewController(context _: Context) -> CustomUIViewController<Content> {
    return CustomUIViewController()
  }

  func updateUIViewController(
    _ viewController: CustomUIViewController<Content>,
    context: Context
  ) {
    viewController.update(
      $isPresented,
      content
    )
  }
}

class CustomUIViewController<Content>: UIViewController where Content: View {
  var internalState = false

  func update(
    _ isPresented: Binding<Bool>,
    _ content: () -> Content
  ) {
    internalState = isPresented.wrappedValue
    print("isPresented: \(isPresented.wrappedValue)")

    // present the content() in another UIWindow
  }
}

internal final class Inspection<V> {
  let notice = PassthroughSubject<UInt, Never>()
  var callbacks: [UInt: (V) -> Void] = [:]

  func visit(_ view: V, _ line: UInt) {
    if let callback = callbacks.removeValue(forKey: line) {
      callback(view)
    }
  }
}

extension Inspection: InspectionEmissary {}