1

Let's say that you don't really need SwiftUI features. I.e. you don't have import SwiftUI in your file. Instead, you only require

import protocol SwiftUI.UIViewControllerRepresentable

In general, you're going to have to involve a delegate object: an AnyObject at best, and usually, because the UIKit APIs are old, an NSObject.

The common pattern is to use a Coordinator class for that, and have the View itself be a struct, but is there always point in that indirection?

Here's an example which hasn't given me any trouble in practice:

import Combine
import MultipeerConnectivity
import protocol SwiftUI.UIViewControllerRepresentable

extension MCBrowserViewController {
  final class View: NSObject {
    init(
      serviceType: String,
      session: MCSession,
      peerCountRange: ClosedRange<Int>? = nil
    ) {
      self.serviceType = serviceType
      self.session = session
      self.peerCountRange = peerCountRange
    }

    private let serviceType: String
    private unowned let session: MCSession
    private let peerCountRange: ClosedRange<Int>?

    private let didFinishSubject = CompletionSubject()
    private let wasCancelledSubject = CompletionSubject()
  }
}

// MARK: - internal
extension MCBrowserViewController.View {
  var didFinishPublisher: AnyPublisher<Void, Never> { didFinishSubject.eraseToAnyPublisher() }
  var wasCancelledPublisher: AnyPublisher<Void, Never> { wasCancelledSubject.eraseToAnyPublisher() }
}

// MARK: - private
private extension MCBrowserViewController {
  typealias CompletionSubject = PassthroughSubject<Void, Never>
}

// MARK: - UIViewControllerRepresentable
extension MCBrowserViewController.View: UIViewControllerRepresentable {
  func makeUIViewController(context: Context) -> MCBrowserViewController {
    let browser = MCBrowserViewController(
      serviceType: serviceType,
      session: session
    )

    browser.delegate = self

    if let peerCountRange = peerCountRange {
      browser.minimumNumberOfPeers = peerCountRange.lowerBound
      browser.maximumNumberOfPeers = peerCountRange.upperBound
    }

    return browser
  }

  func updateUIViewController(_: MCBrowserViewController, context _: Context) { }
}

// MARK: - MCBrowserViewControllerDelegate
extension MCBrowserViewController.View: MCBrowserViewControllerDelegate {
  func browserViewControllerDidFinish(_: MCBrowserViewController) {
    didFinishSubject.send()
  }

  func browserViewControllerWasCancelled(_: MCBrowserViewController) {
    wasCancelledSubject.send()
  }
}

1 Answers1

3

I don't have a full detailed answer for your question, but your solution have some problems.

In SwiftUI, if we update a View, it calls init to recreate the View, and then call updateUIViewController.

In your case, whenever you update your View, not only your view is recreated, your two subjects will be recreated too, so anything attaches to the Publisher after the recreation won't receive events any more.

maybe that's the reason we prefer to use Coordinator.

JIE WANG
  • 1,875
  • 1
  • 17
  • 27
  • 1
    It sounds like you’re saying that makeCoordinator + makeUIViewController run one time, the same as State and Binding storage allocation, no matter how many times the view is created. Accurate? –  Dec 22 '20 at 18:47
  • @Jessy yes, it's accurate. – JIE WANG Dec 22 '20 at 20:00
  • So then, wouldn’t returning `self` from `makeCoordinator` solve the problem you’ve mentioned? I’ve never seen anyone do that either. –  Dec 22 '20 at 20:33
  • @Jessy you can test it yourself easily. – JIE WANG Dec 23 '20 at 09:35
  • The question wasn't for me. It was for you to think about. (And if you can wrap your head around it, please add it to the answer.) Consider how returning self as the coordinator would differ from what I'm proposing. –  Dec 23 '20 at 09:49
  • @Jessy it depends on how you use your `publisher`, if you use it like this `View { }.xxxPublisher.sink {}`, it won't work. – JIE WANG Dec 23 '20 at 16:25