Search code examples
swiftviewswiftuiboilerplateswiftui-list

Require a SwitftUI View in a protocol without boilerplate


[ Ed: Once I had worked this out, I edited the title of this question to better reflect what I actually needed. - it wasn't until I answered my own question that I clarified what I needed :-) ]

I am developing an App using SwiftUI on IOS in which I have 6 situations where I will have a List of items which I can select and in all cases the action will be to move to a screen showing that Item.

I am a keen "DRY" advocate so rather than write the List Code 6 times I want to abstract away the list and select code and for each of the 6 scenarios I want to just provide what is unique to that instance.

I want to use a protocol but want to keep boilerplate to a minimum.

My protocol and associated support is this:

import SwiftUI

/// -----------------------------------------------------------------
/// ListAndSelect
/// -----------------------------------------------------------------

protocol ListAndSelectItem: Identifiable {
  var name: String { get set }
  var value: Int { get set }

  // For listView:
  static var listTitle: String { get }
  associatedtype ItemListView: View
  func itemListView() -> ItemListView

  // For detailView:
  var detailTitle: String { get }
  associatedtype DetailView: View
  func detailView() -> DetailView
}

extension Array where Element: ListAndSelectItem {
  func listAndSelect() -> some View {
    return ListView(items: self, itemName: Element.listTitle)
  }
}

struct ListView<Item: ListAndSelectItem>: View {
  var items: [Item]
  var itemName: String

  var body: some View {
    NavigationView {
      List(items) { item in
        NavigationLink(
          destination: DetailView(item: item, index: String(item.value))
        ) {
          VStack(alignment: .leading){
            item.itemListView()
              .font(.system(size: 15)) // Feasible that we should remove this
          }
        }
      }
      .navigationBarTitle(Text(itemName).foregroundColor(Color.black))
    }
  }
}

struct DetailView<Item: ListAndSelectItem>: View {
  @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
  var item: Item
  var index: String
  var body: some View {
    NavigationView(){
      item.detailView()
    }
    .navigationBarTitle(Text(item.name).foregroundColor(Color.black))
    .navigationBarItems(leading: Button(action: {
      self.presentationMode.wrappedValue.dismiss()
    }, label: { Text("<").foregroundColor(Color.black)}))
  }

}

which means I can then just write:

struct Person: ListAndSelectItem {
  var id  = UUID()
  var name: String
  var value: Int

  typealias ItemListView = PersonListView
  static var listTitle = "People"
  func itemListView() -> PersonListView {
    PersonListView(person: self)
  }

  typealias DetailView = PersonDetailView
  let detailTitle = "Detail Title"
  func detailView() -> DetailView {
    PersonDetailView(person: self)
  }
}

struct PersonListView: View {
  var person: Person
  var body: some View {
    Text("List View for \(person.name)")
  }
}

struct PersonDetailView: View {
  var person: Person
  var body: some View {
    Text("Detail View for \(person.name)")
  }
}

struct ContentView: View {
  let persons: [Person] = [
    Person(name: "Jane", value: 1),
    Person(name: "John", value: 2),
    Person(name: "Jemima", value: 3),
  ]

  var body: some View {
    persons.listAndSelect()
  }
}

which isn't bad but I feel I ought to be able to go further.

Having to write:

  typealias ItemListView = PersonListView
  static var listTitle = "People"
  func itemListView() -> PersonListView {
    PersonListView(person: self)
  }

with

struct PersonListView: View {
  var person: Person
  var body: some View {
    Text("List View for \(person.name)")
  }
}

still seems cumbersome to me. In each of my 6 cases I'd be writing very similar code. I feel like I ought to be able to just write:

  static var listTitle = "People"
  func itemListView() = {
    Text("List View for \(name)")
  }
}

because that's the unique bit. But that certainly won't compile.

And then the same for the Detail.

I can't get my head around how to simplify further. Any ideas welcome?


Solution

  • The key to this is, if you want to use a view in a protocol then:

    1) In the protocol:

      associatedtype SpecialView: View
      var specialView: SpecialView { get }
    

    2) In the struct using the protocol:

      var specialView: some View { Text("Special View") }
    

    So in the situation of the question:

    By changing my protocol to:

    protocol ListAndSelectItem: Identifiable {
      var name: String { get set }
      var value: Int { get set }
    
      // For listView:
      static var listTitle: String { get }
      associatedtype ListView: View
      var listView: ListView { get }
    
      // For detailView:
      var detailTitle: String { get }
      associatedtype DetailView: View
      var detailView: DetailView { get }
    }
    

    I can now define Person as:

    struct Person: ListAndSelectItem {
      var id  = UUID()
      var name: String
      var value: Int
    
      static var listTitle = "People"
      var listView: some View { Text("List View for \(name)") }
    
      var detailTitle = "Person"
      var detailView: some View { Text("Detail View for \(name)") }
    }
    

    which is suitable DRY and free of boilerplate!