SwiftUI with Xcode 11 beta 7 not updating contents of List / ForEach - swift

I've been trying a simple feature to add new entries to a List. The View will just add a new generated. item (no need for user input).
struct PeopleList: View {
#ObservedObject var people: PersonStore
var body: some View {
NavigationView {
VStack {
Section {
Button(action: add) {
Text("Add")
}
}
Section {
List {
ForEach(people.people) { person in
NavigationLink(destination: PersonDetail(person: person)) {
PersonRow(person: person)
}
}
}
}
}
}
.navigationBarTitle(Text("People"))
.listStyle(GroupedListStyle())
}
func add() {
let newID = (people.people.last?.id ?? 0) + 1
self.people.people.append(Person(id: newID, name: ""))
}
}
This used to work in previous betas, but for some reason it's not working anymore. When I click on Add, the App does call the add() function and adds the new entry to the array, but the View is not updated at all.
These are the support classes:
class PersonStore: ObservableObject {
var people: [Person] {
willSet {
willChange.send()
}
}
init(people: [Person] = []) {
self.people = people
}
var willChange = PassthroughSubject<Void, Never>()
}
class Person: ObservableObject, Identifiable {
var id: Int = 0 {
willSet {
willChange.send()
}
}
var name: String {
willSet {
willChange.send()
}
}
init(id: Int, name: String) {
self.id = id
self.name = name
}
var willChange = PassthroughSubject<Void, Never>()
}
#if DEBUG
let data = [
Person(id: 1, name: "David"),
Person(id: 2, name: "Anne"),
Person(id: 3, name: "Carl"),
Person(id: 4, name: "Amy"),
Person(id: 5, name: "Daisy"),
Person(id: 6, name: "Mike"),
]
#endif
And the support views:
struct PersonRow: View {
#ObservedObject var person: Person
var body: some View {
HStack {
Image(systemName: "\(person.id).circle")
Text(person.name)
}.font(.title)
}
}
struct PersonDetail: View {
#ObservedObject var person: Person
var body: some View {
HStack {
Text("This is \(person.name)")
}.font(.title)
}
}
I've found someone with a problem that looks a bit related here:
SwiftUI: View content not reloading if #ObservedObject is a subclass of UIViewController. Is this a bug or am I missing something?
and here:
SwiftUI #Binding doesn't refresh View

The problem is that when you implemented your own ObservableObject you used the wrong publisher. The ObservableObject protocol creates the objectWillChange publisher but you never use it so that SwiftUI is never informed that anything has changed.
class Person: ObservableObject, Identifiable {
let id: Int
#Published
var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
I haven't run that through the compiler so there might be some typos.
You don't have to use #Published but for a simple case like yours it's easier. Of course you need to update your other class as well.
Another small thing, id is required to never change, List et al. use it to connect your data with the view they create.

Related

How to observer a property in swift ui

How to observe property value in SwiftUI.
I know some basic publisher and observer patterns. But here is a scenario i am not able to implement.
class ScanedDevice: NSObject, Identifiable {
//some variables
var currentStatusText: String = "Pending"
}
here CurrentStatusText is changed by some other callback method that update the status.
Here there is Model class i am using
class SampleModel: ObservableObject{
#Published var devicesToUpdated : [ScanedDevice] = []
}
swiftui component:
struct ReviewView: View {
#ObservedObject var model: SampleModel
var body: some View {
ForEach(model.devicesToUpdated){ device in
Text(device.currentStatusText)
}
}
}
Here in UI I want to see the real-time status
I tried using publisher inside ScanDevice class but sure can to use it in 2 layer
You can observe your class ScanedDevice, however you need to manually use a objectWillChange.send(),
to action the observable change, as shown in this example code.
class ScanedDevice: NSObject, Identifiable {
var name: String = "some name"
var currentStatusText: String = "Pending"
init(name: String) {
self.name = name
}
}
class SampleViewModel: ObservableObject{
#Published var devicesToUpdated: [ScanedDevice] = []
}
struct ReviewView: View {
#ObservedObject var viewmodel: SampleViewModel
var body: some View {
VStack (spacing: 33) {
ForEach(viewmodel.devicesToUpdated){ device in
HStack {
Text(device.name)
Text(device.currentStatusText).foregroundColor(.red)
}
Button("Change \(device.name)") {
viewmodel.objectWillChange.send() // <--- here
device.currentStatusText = UUID().uuidString
}.buttonStyle(.bordered)
}
}
}
}
struct ContentView: View {
#StateObject var viewmodel = SampleViewModel()
var body: some View {
ReviewView(viewmodel: viewmodel)
.onAppear {
viewmodel.devicesToUpdated = [ScanedDevice(name: "device-1"), ScanedDevice(name: "device-2")]
}
}
}

SwiftUI: Missing argument for parameter 'x' in call

My content view has a StateObject to a data model "Pilot." The complier generates an error of "Missing argument for parameter 'pilot' in call. Xcode offers the following fix: var pilot = Pilot(pilot: [Employee])... however, the complier generates a new error on that fix: "Cannot convert value of type '[Employee].Type' to expected argument type '[Employee]'"
Here is my Content View:
struct ContentView: View {
#StateObject var pilot = Pilot(pilot: [Employee])
var body: some View {
NavigationView {
ZStack {
Color.gray.ignoresSafeArea()
.navigationBarHidden(true)
TabView {
ProfileFormView()
.tabItem {
Image(systemName: "square.and.pencil")
Text("Profile")
}
EmployeeView()
.tabItem {
Image(systemName: "house")
Text("Home")
}
.padding()
}
.environmentObject(pilot)
}
}
}
}
Here is my data model:
class Employee: Identifiable, Codable {
var id = UUID()
var age: Int
var yearGroup: Int
var category: String
init(age: Int, yearGroup: Int, category: String) {
self.age = age
self.yearGroup = yearGroup
self.category = category
}
struct Data {
var age: Int = 35
var yearGroup: Int = 1
var category: String = ""
}
var data: Data {
Data(age: age, yearGroup: yearGroup, category: category)
}
}
#MainActor class Pilot: ObservableObject {
#Published var pilot: [Employee]
init(pilot: [Employee]) {
self.pilot = pilot
}
}
class Data: Employee {
static let sampleData: [Employee] = [
Employee(age: 35, yearGroup: 1, category: "B717")
]
}
I am also getting a similar compiler error in my Content view for the "Employee View()" which states: "Missing argument for parameter 'data' in call"
Here is my EmployeeView code:
struct EmployeeView: View {
#EnvironmentObject var pilot: Pilot
let data: [Employee]
var body: some View {
ZStack {
List {
ForEach(data) {line in
EmployeeCardView(employee: line)
}
}
}
}
}
/// UPDATE1 ///
I tried to pass an instance of Employee to Pilot but I've hit a new wall. Here is my new code.
Here is my data model:
struct Employee: Identifiable, Codable {
var id = UUID()
var age: Int
var yearGroup: Int
var category: String
init(age: Int, yearGroup: Int, category: String) {
self.age = age
self.yearGroup = yearGroup
self.category = category
}
struct UserInfo {
var age: Int = 35
var yearGroup: Int = 1
var category: String = ""
}
var userInfo: UserInfo {
UserInfo(age: age, yearGroup: yearGroup, category: category)
}
}
#MainActor class Pilot: ObservableObject {
#Published var pilot: [Employee]
init(pilot: [Employee]) {
self.pilot = pilot
}
let pilotInfo = Employee(age: 35, yearGroup: 1, category: "B717")
}
And here is my Content view:
struct ContentView: View {
#StateObject var pilot = Pilot(pilotInfo) //<error here>
Now getting an error in the Content view: "Cannot find 'pilotInfo' in scope"
/// UPDATE 2 ///
I removed the UserInfo section of the model data and followed the guidance to take one Employee (not an array) and change the Content view variable. That fixed those associated errors.
In an effort to comply with principles outlined in developer.apple.com/tutorials/app-dev-training/displaying-data-in-a-list, I've tried to match the following from Apple's tutorial:
struct ScrumsView: View {
let scrums: [DailyScrum]
var body: some View {
List {
ForEach(scrums, id: \.title) { scrum in
CardView(scrum: scrum)
.listRowBackground(scrum.theme.mainColor)
}
}
}
}
That's why my Employee view looks like this:
struct EmployeeView: View {
#EnvironmentObject var pilot: Pilot
let userInfo: [Pilot]
var body: some View {
ZStack {
List {
ForEach(userInfo, id: \.age) {line in //ERRORs
EmployeeCardView(employee: line)
}
}
}
}
}
The complier errors are:
Cannot convert value of type '[Pilot]' to expected argument type 'Binding'
Generic parameter 'C' could not be inferred
Key path value type '' cannot be converted to contextual type ''
///UPDATE 3///
struct ProfileFormView: View {
#EnvironmentObject var pilot: Pilot
#StateObject var vm: EmployeeViewModel = EmployeeViewModel()
var body: some View {
NavigationView {
Form {
Section(header: Text("Personal Information")) {
DatePicker("Birthdate", selection: $vm.birthdate, displayedComponents: .date)
DatePicker("New Hire Date", selection: $vm.newHireDate, displayedComponents: .date)
Picker("Your Current Aircraft", selection: $vm.chosenAircraft) {
ForEach(vm.currentAircraft, id: \.self) {
Text ($0)
}
}
}
}
}
}
}
As you can see, my first attempt was much more complex but since I could not get my previous version views to take user input updates, I decided to start over with a more basic app to better learn the fundamentals of data management.

Using a protocol array with ForEach and bindable syntax

I've got an #Published protocol array that I am looping through with a ForEach to present the elements in some view. I'd like to be able to use SwiftUI bindable syntax with the ForEach to generate a binding for me so I can mutate each element and have it reflected in the original array.
This seems to work for the properties that are implemented in the protocol, but I am unsure how I would go about accessing properties that are unique to the protocol's conforming type. In the example code below, that would be the Animal's owner property or the Human's age property. I figured some sort of type casting might be necessary, but can't figure out how to retain the reference to the underlying array via the binding.
Let me know if you need more detail.
import SwiftUI
protocol Testable {
var id: UUID { get }
var name: String { get set }
}
struct Human: Testable {
let id: UUID
var name: String
var age: Int
}
struct Animal: Testable {
let id: UUID
var name: String
var owner: String
}
class ContentViewModel: ObservableObject {
#Published var animalsAndHumans: [Testable] = []
}
struct ContentView: View {
#StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
ForEach($vm.animalsAndHumans, id: \AnyTestable.id) { $object in
TextField("textfield", text: $object.name)
// if the object is an Animal, how can I get it's owner?
}
Button("Add animal") {
vm.animalsAndHumans.append(Animal(id: UUID(), name: "Mick", owner: "harry"))
}
Button("Add Human") {
vm.animalsAndHumans.append(Human(id: UUID(), name: "Ash", age: 26))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is a thorny problem with your data types.
If you can change your data types, you can make this easier to solve.
For example, maybe you can model your data like this instead, using an enum instead of a protocol to represent the variants:
struct Testable {
let id: UUID
var name: String
var variant: Variant
enum Variant {
case animal(Animal)
case human(Human)
}
struct Animal {
var owner: String
}
struct Human {
var age: Int
}
}
It will also help to add accessors for the two variants' associated data:
extension Testable {
var animal: Animal? {
get {
guard case .animal(let animal) = variant else { return nil }
return animal
}
set {
guard let newValue = newValue, case .animal(_) = variant else { return }
variant = .animal(newValue)
}
}
var human: Human? {
get {
guard case .human(let human) = variant else { return nil }
return human
}
set {
guard let newValue = newValue, case .human(_) = variant else { return }
variant = .human(newValue)
}
}
}
Then you can write your view like this:
class ContentViewModel: ObservableObject {
#Published var testables: [Testable] = []
}
struct ContentView: View {
#StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
List {
ForEach($vm.testables, id: \.id) { $testable in
VStack {
TextField("Name", text: $testable.name)
if let human = Binding($testable.human) {
Stepper("Age: \(human.wrappedValue.age)", value: human.age)
}
else if let animal = Binding($testable.animal) {
HStack {
Text("Owner: ")
TextField("Owner", text: animal.owner)
}
}
}
}
}
HStack {
Button("Add animal") {
vm.testables.append(Testable(
id: UUID(),
name: "Mick",
variant: .animal(.init(owner: "harry"))
))
}
Button("Add Human") {
vm.testables.append(Testable(
id: UUID(),
name: "Ash",
variant: .human(.init(age: 26))
))
}
}
}
}
}
Simple way to solve is extending your Testable protocol. Something likes
protocol Testable {
var id: UUID { get }
var name: String { get set }
var extra: String? { get }
}
struct Human: Testable {
let id: UUID
var name: String
var age: Int
var extra: String? { nil }
}
struct Animal: Testable {
let id: UUID
var name: String
var owner: String
var extra: String? { return owner }
}
Your ForEach block doesn't need to know the concrete type: Animal or Human, just check the extra of Testable to decide to add new element or not.

How to manage SwiftUI state with nested structs?

I have the following structs generated from protobufs so they're not modifiable directly:
// This file can not be modified
// It's auto-generated from protobufs
struct Shelf {
var id: Int
var title: String
var books: [Books]
}
struct Book {
var id: Int
var title: String
var pages: [Pages]
var shelfId: Int
}
struct Page {
var id: Int
var content: String
var bookId: Int
}
What's the proper way of passing the state between 3 nested SwiftUI views when creating or modifying a new Shelf with Books+Pages? I want to allow the user to create the whole shelf at once going through nested view and save Shelf+Books+Pages to the backend only once when he's on the top view and clicking "Save".
I tried to create an extension for objects to conform to 'ObservableObject' but failed with:
Non-class type 'Shelf' cannot conform to class protocol 'ObservableObject'
I made the full project, to demonstrate how to pass the data.
It is available on GitHub at GeorgeElsham/BookshelvesExample if you want to download the full project to see all the code. This is what the project looks like:
This project is quite similar to my answer for SwiftUI - pass data to different views.
As a summary, I created an ObservableObject which is used with #EnvironmentObject. It looks like this:
class BookshelvesModel: ObservableObject {
#Published var shelves = [...]
var books: [Book] {
shelves[shelfId].books
}
var pages: [Page] {
shelves[shelfId].books[bookId].pages
}
var shelfId = 0
var bookId = 0
func addShelf(title: String) {
/* ... */
}
func addBook(title: String) {
/* ... */
}
func addPage(content: String) {
/* ... */
}
func totalBooks(for shelf: Shelf) -> String {
/* ... */
}
func totalPages(for book: Book) -> String {
/* ... */
}
}
The views are then all connected using NavigationLink. Hope this works for you!
If you are remaking this manually, make sure you replace
let contentView = ContentView()
with
let contentView = ContentView().environmentObject(BookshelvesModel())
in the SceneDelegate.swift.
Basically, you need a storage for your books/pages, and preferably that storage can be uniquely referenced among your views. This means a class :)
class State: ObservableObject {
#Published var shelves = [Shelf]()
func add(shelf: Shelf) { ... }
func add(book: Book, to shelf: Shelf) { ... }
func add(page: Page, to book: Book) { ... }
func update(text: String, for page: Page) { ... }
}
You can then either inject the State instance downstream in the view hierarchy, on inject parts of it, like a Shelf instance:
struct ShelvesList: View {
#ObserverdObject var state: State
var body: some View {
ForEach(state.shelves) { ShelfView(shelf: $0, shelfOperator: state) }
}
}
// this conceptually decouples the storage and the operations, allowing
// downstream views to see only parts of the entire functionality
protocol ShelfOperator: BookOperator {
func add(book: Book, to shelf: Shelf)
}
extension State: ShelfOperator { }
struct ShelfView: View
var shelf: Shelf
#State var selectedBook: Book
var shelfOperator: ShelfOperator
var body: some View {
ForEach(shelf.books) { book in
Text(book.title).tapGesture {
// intercepting tap to update the book view with the new selected book
self.selectedBook = book
}
}
BookView(book: selectedBook, bookOperator: operator)
}
}
// This might seem redundant to ShelfOperator, however it's not
// A view that renders a book doesn't need to know about shelf operations
// Interface Segregation Principle FTW :)
protocol BookOperator: PageOperator {
func add(page: Page, to book: Book)
}
struct BookView: View {
var book: Book
var bookOperator: BookOperator
var body: some View { ... }
}
// Segregating the functionality via protocols has multiple advantages:
// 1. this "leaf" view is not polluted with all kind of operations the big
// State would have
// 2. PageView is highly reusable, since it only depends on entities it needs
// to do its job.
protocol PageOperator {
func update(text: String, for page: Page)
}
struct PageView: View {
var page: Page
var pageOperator: PageOperator
var body: some View { ... }
What happens with the code above is that data flows downstream, and events propagate upstream, and any changes caused by events are then propagated downstream, meaning your views are always in sync with the data.
Once you're done with the editing, just grab the list of shelves from the State instance and send them to the backend.
Well, the preferable design in this case would be to use MVVM based on ObservableObject for view model (it allows do not touch/change generated model, but wrap it into convenient way for use in View).
It would look like following
class Library: ObservableObject {
#Published var shelves: [Shelf] = []
}
However, of course, if required, all can be done with structs only based on #State/#Binding only.
Assuming (from mockup) that the initial shelf is loaded in some other place the view hierarchy (in simplified presentation just to show the direction) can be:
struct ShelfView: View {
#State private var shelf: Shelf
init(_ shelf: Shelf) {
_shelf = State<Shelf>(initialValue: shelf)
}
var body: some View {
NavigationView {
List {
ForEach(Array(shelf.books.enumerated()), id: \.1.id) { (i, book) in
NavigationLink("Book \(i)", destination: BookView(book: self.$shelf.books[i]))
}
.navigationBarTitle(shelf.title)
}
}
}
}
struct BookView: View {
#Binding var book: Book
var body: some View {
List {
ForEach(Array(book.pages.enumerated()), id: \.1.id) { (i, page) in
NavigationLink("Page \(i)", destination: PageView(page: page))
}
.navigationBarTitle(book.title)
}
}
}
struct PageView: View {
var page: Page
var body: some View {
ScrollView {
Text(page.content)
}
}
}
I made you a minimal example with NavigationView and #ObservedObjects in each View. This shows the basic usage of ObservableObject with nested class. This works because each View gets passed the Model and "observes" it.
If you have any questions please read the documentation first before asking them. You should find most of the stuff under Combine and SwiftUI.
Please Note int64 does not exist as far as I know and your Array declarations are wrong too! I corrected them in the example I provided.
class PageModel: ObservableObject {
#Published var id: Int
#Published var content: String
init(id: Int, content: String) {
self.id = id
self.content = content
}
}
class BookModel: ObservableObject {
#Published var id: Int
#Published var title: String
#Published var pages: [PageModel] = []
init(id: Int, title: String) {
self.id = id
self.title = title
}
func addDummies() {
DispatchQueue.main.async {
self.pages.append(PageModel(id: 0, content: "To"))
self.pages.append(PageModel(id: 1, content: "tell"))
self.pages.append(PageModel(id: 2, content: "you"))
self.pages.append(PageModel(id: 3, content: "I'm"))
self.pages.append(PageModel(id: 4, content: "sorry..."))
self.pages.append(PageModel(id: 5, content: "for"))
self.pages.append(PageModel(id: 6, content: "everything"))
self.pages.append(PageModel(id: 7, content: "that"))
self.pages.append(PageModel(id: 8, content: "I've"))
self.pages.append(PageModel(id: 9, content: "done..."))
}
}
}
class ShelfModel: ObservableObject {
#Published var id: Int
#Published var title: String
#Published var books: [BookModel] = []
init(id: Int, title: String) {
self.id = id
self.title = title
}
func add() {
DispatchQueue.main.async {
self.books.append(BookModel(id: self.books.count, title: "frick I am new"))
}
}
func addDummies() {
DispatchQueue.main.async {
self.books.append(BookModel(id: 0, title: "Hello"))
self.books.append(BookModel(id: 1, title: "from"))
self.books.append(BookModel(id: 2, title: "the"))
self.books.append(BookModel(id: 3, title: "other"))
self.books.append(BookModel(id: 4, title: "side..."))
self.books.append(BookModel(id: 5, title: "I"))
self.books.append(BookModel(id: 6, title: "must"))
self.books.append(BookModel(id: 7, title: "have"))
self.books.append(BookModel(id: 8, title: "called"))
self.books.append(BookModel(id: 9, title: "a thousand"))
self.books.append(BookModel(id: 10, title: "times..."))
}
}
}
struct PageView: View {
#ObservedObject var page: PageModel
var body: some View {
HStack {
Text("\(page.id)")
Text("\(page.content)")
}
}
}
struct BookView: View {
#ObservedObject var book: BookModel
var body: some View {
VStack {
HStack {
Text("\(book.id)")
Text("\(book.title)")
}
List(book.pages, id: \.id) { page in
PageView(page: page)
}
}
.navigationBarItems(trailing: Button("Add Page") {
self.book.addDummies()
})
}
}
struct ContentView: View {
#ObservedObject var shelf = ShelfModel(id: 0, title: "Lolz")
var body: some View {
VStack {
NavigationView {
List(self.shelf.books, id: \.id) { book in
NavigationLink(destination: BookView(book: book)) {
Text("\(book.title)")
}.navigationBarItems(trailing: Button("Add Book") {
self.shelf.add()
})
}
}
}.onAppear {
self.shelf.addDummies()
}
}
}
Tested on iPad Pro.
I hope this helps!

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?
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!