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

904 views Asked by At

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'

1

There are 1 answers

2
lorem ipsum On BEST ANSWER

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.