Group Group Group Group Group Group Group Group Group

Conditional views + ViewBuilder

Anyone successfully use a @ViewBuilder for spouting out different destination views (conforming to a protocol) as an argument for the navigation link?

Re: conforming to a protocol -> ability to return an optional view; https://developer.apple.com/documentation/swiftui/viewbuilder/buildif(_:) :thinking:

struct MyRoutesSectionViewObject: Hashable {
  var title: String
  var iconString: String
  var count: Int
  var badgeIconString: String?
}

protocol MyRoutesSectionViewControllerRepresentable {
  var myRoutesSectionViewObject: MyRoutesSectionViewObject { get set }
}

enum Destination: CaseIterable, Hashable {
  case savedRoutes
  case plannedRoutes
  case recordedRoutes
  case offlineRoutes
}

// This obviously throws a compile error
struct MyRoutesSectionView: View {
  var body: some View {
    ForEach(Destination.allCases, id: \.self) { destination in
      if let destinationView = self.destinationView(for: destination) as? MyRoutesSectionViewControllerRepresentable {
        NavigationLink(destination: destinationView) {
          MyRoutesNavigationLinkView(myRoutesObject: destinationView.myRoutesObject)
        }
      } else {
        assert(false, "Destination view needs to conform to `MyRoutesSectionViewControllerRepresentable`")
        return AnyView(EmptyView())
      }

    }
  }
}

private extension MyRoutesSectionView {
  @ViewBuilder
  func destinationView(for destination: Destination) -> some View {
    switch destination {
    case .savedRoutes:
      BMSavedRoutesView()
    case .plannedRoutes:
      BMPlannedRoutesView()
    case .recordedRoutes:
      BMRecordedRoutesView()
    case .offlineRoutes:
      BMOfflineRoutesView()
    }
  }
}
1 Like

No, I don’t know how to do it without relying on type erasure. And the easiest way to do that is with a tuple.

Also, you should be enforcing this requirement, right?

protocol MyRoutesSectionViewControllerRepresentable: UIViewControllerRepresentable {
ForEach(Destination.allCases, id: \.self) { destination in
  let (object, view) = destinationView(for: destination)

  NavigationLink(destination: view) {
    MyRoutesNavigationLinkView(myRoutesObject: object)
  }
}
func destinationView(for destination: Destination)
-> (MyRoutesSectionViewObject, AnyView) {
  func erase<View: MyRoutesSectionViewControllerRepresentable>(_ view: View)
  -> (MyRoutesSectionViewObject, AnyView) {
    (view.myRoutesSectionViewObject, .init(view))
  }

  switch destination {
  case .savedRoutes:
    return erase(BMSavedRoutesView())
  case .plannedRoutes:
    return erase(BMPlannedRoutesView())
  case .recordedRoutes:
    return erase(BMRecordedRoutesView())
  case .offlineRoutes:
    return erase(BMOfflineRoutesView())
  }
}
1 Like

Aha! Makes sense; thanks Jessy :sun_with_face:

Re: Also, you should be enforcing this requirement, right?

protocol MyRoutesSectionViewControllerRepresentable: UIViewControllerRepresentable { }

I was conforming each of the views to UIViewControllerRepresentable in the previous setup; on another note . . .

how should I go about changing the count of the
myRoutesSectionViewObject to a property of the uiViewController?

  • Using a Coordinator doesn’t make sense to me as I am not delegating off any work
  • Should I have a publisher setup and subscribe? (Even that doesn’t make sense to me in this case)
  • I am thinking of using UserDefaults :confused:
struct BMPlannedRoutesView: MyRoutesSectionViewControllerRepresentable {
  var myRoutesSectionViewObject = MyRoutesSectionViewObject(title: "Planned",
                                  iconString: "plannedIcon",
                                  count: 0)
  
  func makeUIViewController(context: Context) -> BMPlannedRoutesViewController {
    return BMPlannedRoutesViewController.instantiateFromStoryboard(name: "MyRoutes")
  }
  
  func updateUIViewController(_ uiViewController: BMPlannedRoutesViewController, context: Context) {
    uiViewController.dottedArrowView.isHidden = true
  }
}

First, I don’t know what you’re representing, with count, so can’t give targeted advice.

But a struct instance is not an object; “object” specifically means “class instance”. Maybe that’s the right solution: have both the struct and class hold a reference to it.

If not, then a Coordinator could work. You’ll need to use the delegate pattern for it. I never thought the delegate pattern was modern, so Bindings or Combine would be my preference.

I would not go with UserDefaults.

1 Like

Sweet, shall give it a go, thanks!

1 Like