swiftuiswiftui-navigationviewswiftui-navigationstackviewbuilder

View protocol - any vs. some vs AnyView vs. generics


I'm looking to create a protocol upon View to group views that can be constructed from some specific data struct (MyData)

protocol EncodableView: View {
  /// Returns true if this view type can be decoded from data
  static func canDecode(from data: MyData) -> Bool

  /// Constructs a view from the given data
  init(from data: MyData) throws
}

I want to use this protocol in a view to route to different views based on their ability to decode MyData:

struct Example: EncodableView {
  // ... implements EncodableView
}

struct ContentView: View {
  private var encodableViews: [any EncodableView.Type] = [ 
    ExampleView.self,
    // ... others
  ]

  private func navigationDestination(for data: MyData) -> some View {
    for (type in encodableViews) {
      // Compile complains: Type 'any EncodableView' cannot conform to 'View'
      if (type.canDecode(data)) { 
        return type.init(from: data)
      }
    }

    return EmptyView()
  }

  var body: some View {
    NavigationStack {
      VStack {
        // ...
      }
      .navigationDestination(for: MyData.self) { data in
         navigationDestination(for: data)
      }
    }
  }
}

However, I'm having trouble finding the right combination of some View, any View, AnyView and generics to achieve this.

I've marked the spot in my code snippet above where the compile complains: Type 'any EncodableView' cannot conform to 'View'


Solution

  • One Solution but not ideal because while checking the suffix is fast you wouldn't want to check every time the view is redrawn.

    extension URL {
        @ViewBuilder var view : some View {
            if self.lastPathComponent.hasSuffix("mp4") {
                Text("shoe video player")
            } else if self.lastPathComponent.hasSuffix("png") {
                AsyncImage(url: self)
            } else {
                Text("unsupported")
            }
        }
    }
    

    Then you can use it something like

    url.view 
    

    but like I mentioned above the decision will be happening multiple times, views shouldn't be deciding.

    You could decide before and then tell the View what it is and what to show.

    enum URLTypes: Hashable, Codable {
        case mp4(URL)
        case png(URL)
        ///Creates a type
        static func decode(url: URL) -> Self {
            if url.lastPathComponent.hasSuffix("mp4") {
                return .mp4(url)
            } else if url.lastPathComponent.hasSuffix("png") {
                return.png(url)
            } else {
                return .unknown(url)
            }
        }
        //What to show
        @ViewBuilder var view: some View {
            switch self {
            case .mp4(let uRL):
                Text("show video player \(uRL)")
            case .png(let uRL):
                AsyncImage(url: uRL)
            case .unknown(let uRL):
                Text("unsupported \(uRL)")
            }
        }
    }
    

    And use it in your navigationDestination

    .navigationDestination(for: URLTypes.self) { data in
         data.view
    }
    

    But you could use generics too just be concrete when you get to the View

    struct MyData<Content>: Hashable, Codable where Content: Hashable & Codable {
        
        let value: Content
        
        init(double: Double) where Content == Double {
            value = double
        }
        
        init(string: String) where Content == String {
            value = string
        }
        init(int: Int) where Content == Int {
            value = int
        }
        init(url: URL) where Content == URLTypes {
            value = URLTypes.decode(url: url)
        }
    }
    

    And in the navigationDestination

    .navigationDestination(for: MyData<URLTypes>.self) { data in
        data.value.view
    }
    .navigationDestination(for: MyData<Double>.self) { data in
        Text(data.value, format: .number.precision(.fractionLength(2)))
    }
    

    The more decisions and processing your put on a View/main thread/ main actor the slower and more sluggish your app will be.