How do you turn an opaque-return-type function into a closure?

e.g. I don't want makeView2 . I just want an inline-evaluated closure.

import Combine
import SwiftUI

struct View1: View {
  @State private var view2IsActive = false

  var body: some View {
    func makeView2() -> some View {
      let view2 = View2()
      return view2.onReceive(view2.completionPublisher) {
        view2IsActive = false
      }
    }

    return NavigationView {
      NavigationLink(
        destination: makeView2().navigationBarHidden(true),
        isActive: $view2IsActive
      ) {
        Button("Show View 2. 🐰") { view2IsActive = true }
      }
    }
  }
}

struct View2: View {
  private let completionSubject = PassthroughSubject<Void, Never>()
  var completionPublisher: AnyPublisher<Void, Never> { completionSubject.eraseToAnyPublisher() }

  var body: some View {
    Button("Disappear. 🎩", action: completionSubject.send)
  }
}

Opaque return type is not supported in the closure syntax, yet. So you can't really do multi-line closure returning opaque type at the moment.


Side notes:

  1. You don't need to use Button as the label, for NavigationLink. The act of clicking on it will automatically set isActive to true.
  2. If you just want to dismiss the view, you can use presentationMode, which would rid of a good chunk of publishers/bindings that are being passed around. You can keep isActive if you want to present/dismiss it from other interactions as well.
struct View1: View {
  var body: some View {
    NavigationView {
      NavigationLink(
        destination: View2().navigationBarHidden(true)
      ) {
        Text("Show View 2. 🐰")
      }
    }
  }
}

struct View2: View {
  @Environment(\.presentationMode) var presentation

  var body: some View {
    Button("Disappear. 🎩") {
      self.presentation.wrappedValue.dismiss()
    }
  }
}

Very cool. (Though not obvious, and undocumented.) Thank you!

These ideas all involve too much coupling, but if you think of any better equivalent to a "completion publisher", I'm all ears. :dancing_men:

That's why I suggest presentationMode environment since it's passed to the current view by the framework already, and can handle dismissal without any publishers/bindings passing around between the master/detail views (or should I say main/detail views).

The assumption of dismissal is not allowed! :smiley_cat:

Use case is like:

extension MCBrowserViewController.Delegate: MCBrowserViewControllerDelegate {
  func browserViewControllerDidFinish(_: MCBrowserViewController) {
    completionSubject.send()
  }

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

:thinking:Tricky. I can think of a few things.


If the acknowledgement of completion/cancellation is essential to View2. The handling needs to be passed into the init functions. Returning a not-yet-working type isn't great, doubly more so in a declarative structure like SwiftUI.

You could pass in closures:

struct View2: View {
  init(onComplete: () -> () = { }, onCancel: () -> () = {}) { ... }
}

You could pass in publishers:

struct View2<CompletionPublisher, CancellationPublisher>: View {
  init(completionPublisher: CompletionPublisher, cancellationPublisher: CancellationPublisher) { ... }
}

You can type-erase if you need to. Personally I'd minimize type-erasure as much as possible.

Or it could be a hidden ObservableObject all along:

class ViewState: ObservableObject {
  func complete() { ... }
  func cancel() { ... }
}

struct View2 {
  init(viewState: ViewState) { ... }
}

If the handling is not essential, we could do a few tricks:

We could add:

struct View2 {
  var completeSubject: ..., cancelSubject: ...

  func handle(onComplete: () -> (), onCancel: () -> ()) -> some View {
    self
      .onReceive(completeSubject, perform: onComplete)
      .onReceive(cancelSubject, perform: onCancel)
  }
}

We could use UserPreferenceKey:

struct View2StatePreferenceKey: PreferenceKey {
  enum Value {
    case active, completed, cancelled
  }

  static var defaultValue = Value.active

  static func reduce(value: inout Value, nextValue: () -> Value) {
    value = nextValue()
  }
}

struct View2: View {
  @State var state = View2StatePreferenceKey.Value.active

  var body: some View {
    ...
      .preference(View2StatePreferenceKey.self, value: state)
  }
}

struct View1: View {
  var body: some View {
    ...
      .onPreferenceChange(View2StatePreferenceKey.self) { newValue in
        ...
      }
  }
}

A few more sidenote:

  1. You don't need to type-erase completionSubject to completionPublisher in your original code. onReceive takes a generic Publisher, so completionSubject will work just fine.
  2. Personally, I don't like a structure that put View into variables. It can be hard to reason with, especially that View don't have any sense of identity outside of the view hierarchy.