Not append new data to an array - swift

I want to add new data to a list in my project
but I can't do it
I have a ContentView.swift view for showing products list
and in another view (ShopView) I want to add data to products array
My products array and my addProduct() function
in the Products.swift file
Please help me
Thanks
ContentView.swift
struct ContentView: View {
#ObservedObject var cart = Products()
var body: some View {
NavigationView{
List {
ForEach(cart.products) { product in
Text("\(product.name) \(product.price)$")
}
}
.navigationBarItems(
trailing: NavigationLink(destination: Shop()) {
Text("Go Shop")
})
.navigationBarTitle("Cart")
}
}
}
Product.swift
struct Product: Identifiable {
var id = UUID()
var name: String
var price: Int
}
Shop.swift
struct Shop: View {
#ObservedObject var cart = Products()
var body: some View {
VStack{
Button("Add Product To Cart") {
cart.addProduct(product: Product(name: "Name", price: 399))
}
}
}
}
Products.swift
class Products: ObservableObject {
#Published var products = [Product]()
func addProduct(product: Product) {
products.append(product)
print("Product Added")
}
}

Right now, you're creating two different instances of Products. If you want the data to be shared, you have to use the same instance.
struct ContentView: View {
#ObservedObject var cart = Products()
var body: some View {
NavigationView{
List {
ForEach(cart.products) { product in
Text("\(product.name) \(product.price)$")
}
}
.navigationBarItems(
trailing: NavigationLink(destination: Shop(cart: cart)) { //<-- HERE
Text("Go Shop")
})
.navigationBarTitle("Cart")
}
}
}
struct Shop: View {
#ObservedObject var cart : Products //<-- HERE
var body: some View {
VStack{
Button("Add Product To Cart") {
cart.addProduct(product: Product(name: "Name", price: 399))
}
}
}
}
Another way to achieve this type of functionality is by using an environment object. Additional reading on that approach: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views

I think you will find the simplest implementation will be to define your model as:
class Products: ObservableObject {
//Implement a shared resource here
public static let shared = Products()
// I would also make the published var name something different then the class name.
// Otherwise you will be calling a lot of products.products.
// Or you could change the name of the class to "Cart"
#Published var products: [Product] = []
func addProduct(product: Product) {
products.append(product)
print("Product Added")
}
}
Use it in your structs with:
#ObservedObject var cart = Products.shared
This will give you one common shared Products class. You don't have to worry about passing is around between views.

Related

How do I get a textfield to display attributes of a core data entity?

I have a list of fruits. the struct FruitRowView provides the layout for the view of each row. In this FruitRowView, there's a TextField which I want to display the name of each fruit. I am having trouble doing this. The reason why I want to use a TextField to display the name of each fruit rather than a Text is so that users can easily edit the name of the fruit right from that TextField. In this case, fruits are the Core Data entity and the fruit name is an attribute of this entity.
Here is my core data class:
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntities: [FruitEntity] = []
init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error with coreData. \(error)")
}
}
fetchFruits()
}
func fetchFruits() {
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addFruit(text: String) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
saveData()
}
func saveData() {
do {
try container.viewContext.save()
fetchFruits()
} catch let error {
print("Error saving. \(error)")
}
}
}
Here is my contentView:
struct ContentView: View {
//sheet variable
#State var showSheet: Bool = false
#StateObject var vm = CoreDataViewModel()
#State var refresh: Bool
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: {
showSheet.toggle()
}, label: {
Text("Add Fruit")
})
List {
ForEach(vm.savedEntities) { fruit in
FruitRowView(vm: vm, fruit: fruit)
}
}
}
.navigationTitle("Fruits")
.sheet(isPresented: $showSheet, content: {
SecondScreen(refresh: $refresh, vm: vm)
})
}
}
}
Here is my popup screen (used to create a new fruit)
struct SecondScreen: View {
#Binding var refresh: Bool
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: CoreDataViewModel
#State var textFieldText: String = ""
var body: some View {
TextField("Add fruit here...", text: $textFieldText)
.font(.headline)
.padding(.horizontal)
Button(action: {
guard !textFieldText.isEmpty else { return }
vm.addFruit(text: textFieldText)
textFieldText = ""
presentationMode.wrappedValue.dismiss()
refresh.toggle()
}, label: {
Text("Save")
})
}
}
Here is my FruitRowView:
struct FruitRowView: View {
//instance of core data model
#ObservedObject var vm: CoreDataViewModel
var fruit: FruitEntity
#State var fruitName = fruit.name
var body: some View {
TextField("Enter fruit name", text: $fruitName)
}
}
So the error that I'm getting is: 'Cannot use instance member 'fruit' within property initializer; property initializers run before 'self' is available'. This error occurs in the FruitRowView when I try to assign fruitName to fruit.name. I assume that there's an easy workaround for this but I haven't been able to figure it out.
Since the fruitEntities in the view model is a published property, you don't need a state variable in the row. You need the binding for the row view, and you should pass it in the content view.
You don't need to pass the view model to the row view as well.
struct FruitRowView: View {
// No need to pass view model to child, only pass the data
// #ObservedObject var vm: CoreDataViewModel
#Binding var fruitName: String
// You don't need this.
// #State var fruitName = fruit.name
var body: some View {
TextField("Enter fruit name", text: $fruitName)
}
}
struct ContentView: View {
...
List {
ForEach(vm.savedEntities) { fruit in
FruitRowView(fruitName: $fruit.name)
}
}
...
}

How to inject a Model from the Environment into a ViewModel in SwiftUI

I am trying to MVVM my SwiftUI app, but am unable to find a working solution for injecting a shared Model from #EnvironmentObject into the app's various Views' ViewModels.
The simplified code below creates a Model object in the init() of an example View, but I feel like I am supposed to be creating the model at the top of the app so that it can be shared among multiple Views and will trigger redraws when Model changes.
My question is whether this is the correct strategy, if so how to do it right, and if not what do I have wrong and how do I do it instead. I haven't found any examples that demonstrate this realistically beginning to end, and I can't tell if I am just a couple of property wrappers off, or it I am approaching this completely wrong.
import SwiftUI
#main
struct DIApp: App {
// This is where it SEEMS I should be creating and sharing Model:
// #StateObject var dataModel = DataModel()
var body: some Scene {
WindowGroup {
ListView()
// .environmentObject(dataModel)
}
}
}
struct Item: Identifiable {
let id: Int
let title: String
}
class DataModel: ObservableObject {
#Published var items = [Item]()
init() {
items.append(Item(id: 1, title: "First Item"))
items.append(Item(id: 2, title: "Second Item"))
items.append(Item(id: 3, title: "Third Item"))
}
func addItem(_ item: Item) {
items.append(item)
print("DM adding \(item.title)")
}
}
struct ListView: View {
// Creating the StateObject here compiles, but it will not work
// in a realistic app with other views that need to share it.
// It should be an app-wide ObservableObject created elsewhere
// and accessible everywhere, right?
#StateObject private var vm: ViewModel
init() {
_vm = StateObject(wrappedValue: ViewModel(dataModel: DataModel()))
}
var body: some View {
NavigationView {
List {
ForEach(vm.items) { item in
Text(item.title)
}
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: {
addItem()
}) {
Image(systemName: "plus.circle")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func addItem() {
vm.addRandomItem()
}
}
extension ListView {
class ViewModel: ObservableObject {
#Published var items: [Item]
let dataModel: DataModel
init(dataModel: DataModel) {
self.dataModel = dataModel
items = dataModel.items
}
func addRandomItem() {
let newID = Int.random(in: 100..<999)
let newItem = Item(id: newID, title: "New Item \(newID)")
// The line below causes Model to be successfully updated --
// dataModel.addItem print statement happens -- but Model change
// is not reflected in View.
dataModel.addItem(newItem)
// The line below causes the View to redraw and reflect additions, but the fact
// that I need it means I am not doing doing this right. It seems like I should
// be making changes to the Model and having them automatically update View.
items.append(newItem)
}
}
}
There are a few different issues here and multiple strategies to handle them.
From the top, yes, you can create your data model at the App level:
#main
struct DIApp: App {
var dataModel = DataModel()
var body: some Scene {
WindowGroup {
ListView(dataModel: dataModel)
.environmentObject(dataModel)
}
}
}
Notice that I've passed dataModel explicitly to ListView and as an environmentObject. This is because if you want to use it in init, it has to be passed explicitly. But, perhaps subviews will want a reference to it as well, so environmentObject will get it sent down the hierarchy automatically.
The next issue is that your ListView won't update because you have nested ObservableObjects. If you change the child object (DataModel in this case), the parent doesn't know to update the view unless you explicitly call objectWillChange.send().
struct ListView: View {
#StateObject private var vm: ViewModel
init(dataModel: DataModel) {
_vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
}
var body: some View {
NavigationView {
List {
ForEach(vm.dataModel.items) { item in
Text(item.title)
}
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: {
addItem()
}) {
Image(systemName: "plus.circle")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func addItem() {
vm.addRandomItem()
}
}
extension ListView {
class ViewModel: ObservableObject {
let dataModel: DataModel
init(dataModel: DataModel) {
self.dataModel = dataModel
}
func addRandomItem() {
let newID = Int.random(in: 100..<999)
let newItem = Item(id: newID, title: "New Item \(newID)")
dataModel.addItem(newItem)
self.objectWillChange.send()
}
}
}
An alternate approach would be including DataModel on your ListView as an #ObservedObject. That way, when it changes, the view will update, even if ViewModel doesn't have any #Published properties:
struct ListView: View {
#StateObject private var vm: ViewModel
#ObservedObject private var dataModel: DataModel
init(dataModel: DataModel) {
_dataModel = ObservedObject(wrappedValue: dataModel)
_vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
}
var body: some View {
NavigationView {
List {
ForEach(vm.dataModel.items) { item in
Text(item.title)
}
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: {
addItem()
}) {
Image(systemName: "plus.circle")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func addItem() {
vm.addRandomItem()
}
}
extension ListView {
class ViewModel: ObservableObject {
let dataModel: DataModel
init(dataModel: DataModel) {
self.dataModel = dataModel
}
func addRandomItem() {
let newID = Int.random(in: 100..<999)
let newItem = Item(id: newID, title: "New Item \(newID)")
dataModel.addItem(newItem)
}
}
}
Yet another object would be using Combine to automatically send objectWilLChange updates when items is updated:
struct ListView: View {
#StateObject private var vm: ViewModel
init(dataModel: DataModel) {
_vm = StateObject(wrappedValue: ViewModel(dataModel: dataModel))
}
var body: some View {
NavigationView {
List {
ForEach(vm.dataModel.items) { item in
Text(item.title)
}
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: {
addItem()
}) {
Image(systemName: "plus.circle")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func addItem() {
vm.addRandomItem()
}
}
import Combine
extension ListView {
class ViewModel: ObservableObject {
let dataModel: DataModel
private var cancellable : AnyCancellable?
init(dataModel: DataModel) {
self.dataModel = dataModel
cancellable = dataModel.$items.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
func addRandomItem() {
let newID = Int.random(in: 100..<999)
let newItem = Item(id: newID, title: "New Item \(newID)")
dataModel.addItem(newItem)
}
}
}
As you can see, there are a few options (these, and others). You can pick the design pattern that works best for you.
You are probably unable to find a working solution because it is not a valid approach. In SwiftUI we do not use MVVM pattern of view model objects. The View data structs are already the view model that SwiftUI uses to create and update actual views like UILabels, etc. on the screen. You should also be aware that when you use property wrappers like #State it makes our super efficient View data struct behave like an object, but without the memory hog of an actual heap object. If you create extra objects then you are slowing SwiftUI down and will lose the magic like dependency tracking etc.
Here is your fixed code:
import SwiftUI
#main
struct DIApp: App {
#StateObject var dataModel = DataModel()
var body: some Scene {
WindowGroup {
ListView()
.environmentObject(dataModel)
}
}
}
struct Item: Identifiable {
let id: Int
let title: String
}
class DataModel: ObservableObject {
#Published var items = [Item]()
init() {
items.append(Item(id: 1, title: "First Item"))
items.append(Item(id: 2, title: "Second Item"))
items.append(Item(id: 3, title: "Third Item"))
}
func addItem(_ item: Item) {
items.append(item)
print("DM adding \(item.title)")
}
}
struct ListView: View {
#EnvironmentObject private var dataModel: DataModel
var body: some View {
NavigationView {
List {
// ForEach($dataModel.items) { $item in // if you want write access
ForEach(dataModel.items) { item in
Text(item.title)
}
}
.navigationTitle("List")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
Button(action: {
addItem()
}) {
Image(systemName: "plus.circle")
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func addItem() {
let newID = Int.random(in: 100..<999)
let newItem = Item(id: newID, title: "New Item \(newID)")
dataModel.addItem(newItem)
}
}

Create a dynamic list of editable objects in swiftUI, store in the mobile itself

I tried to create a list of editable objects in SwiftUI. Here is my idea.
First of all, the editable item is as follows:
struct Item: Identifiable {
var id: UUID
var ItemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [
.init(id: .init(), ItemNum: 55),
.init(id: .init(), ItemNum: 57),
.init(id: .init(), ItemNum: 87)
]
}
After that I created a list that get data from the ItemStore:
struct ItemView: View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore
var body: some View {
NavigationView {
List {
ForEach(store.items.indexed(), id:\.1.id) {index, item in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
//.onAppear(perform: store.fetch) // want to fetch the data from the store whenever the list appear, however, no idea to perform the function?!
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
The editView:
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text(Text("Item Num: \(item.itemNum)"))
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
My question here:
I would like to fetch the data from 'store' onAppear. but it fails.
After I quit the app, all the previous data gone. How can I make them to keep inside my app, even the app is kill?
Your second question first: In terms of storing (persisting your data), you have many options. The easiest would be to store it in UserDefaults, which I'll show in my example. You could also choose to use CoreData, which would be more of a process to set up, but would give you a more robust solution later on. Many more options like Realm, Firebase, SQLite, etc. exist as well.
struct Item: Identifiable, Codable {
var id: UUID = UUID()
var itemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "savedItems")
}
}
}
let defaultValues : [Item] = [
.init(itemNum: 55),
.init(itemNum: 57),
.init(itemNum: 87)
]
func fetch() {
let decoder = JSONDecoder()
if let savedItems = UserDefaults.standard.object(forKey: "savedItems") as? Data,
let loadedItems = try? decoder.decode([Item].self, from: savedItems) {
items = loadedItems
} else {
items = defaultValues
}
}
}
struct ContentView : View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore = ItemStore()
var body: some View {
NavigationView {
List {
ForEach(Array(store.items.enumerated()), id:\.1.id) { (index,item) in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
.onAppear(perform: store.fetch)
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text("Item Num: \(item.itemNum)")
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
Regarding your first question, the reason that fetch failed is you had no fetch method. Plus, there was nothing to fetch, since the array of items just got populated upon creation of the ItemStore each time.
Notes:
Item now conforms to Codable -- this is what allows it to get transformed into a value that can be saved/loaded from UserDefaults
fetch is now called on onAppear.
Every time the data is changed, didSet is called, saving the new data to UserDefaults
There were a number of typos and things that just plain wouldn't compile in the original code, so make sure that the changes are reflected. Some of those include: enumerated instead of indexed in the ForEach, not calling Text(Text( with nested values, using the same capitalization of itemNum throughout, etc
Important: when testing this, make sure to give the simulator a few seconds after a change to save the data into UserDefaults before killing the app and opening it again.

Swift View not updating when Observed Object changes

I have some code like this:
class Data: ObservableObject {
#Published var data = dbContent
init(){
let db = Firestore.firestore()
db.collection("collection").document(userID).addSnapshotListener {
//getting data from DB and storing them as objects by appending them to data
}
}
}
struct 1View: View {
#ObservedObject var myData: Data = Data()
var body: some View {
2View(myData: self.myData)
3View(myData: self.myData)
}
}
struct 2View: View {
#State var myData: Data
var body: some View {
List(){
ForEach(data.count){ data in
Text(data)
}.onDelete(perform: deleteData) //Deletes the item
}
}
}
struct 3View: View {
#State var myData: Data
var body: some View {
List(){
ForEach(data.count){ data in
Text(data)
}.onDelete(perform: deleteData) //Deletes the item
}
}
}
Now the issue is, that I can delete the the item in the 2View. This is then also shown and I implemented the functionality that it deletes the Item in the DB as well.
So the DB data gets altered but this is not shown in the 3View until I refresh it by e.g. revisiting it.
I have no idea what the cause is. Maybe I got a wrong understanding of #Published and ObservedObject ?
#State means that the view owns the data and manages the state. Try using #ObservedObject in your child views as well. Here is an example:
Model
struct Book: Codable, Identifiable {
#DocumentID var id: String?
var title: String
var author: String
var numberOfPages: Int
enum CodingKeys: String, CodingKey {
case id
case title
case author
case numberOfPages = "pages"
}
}
ViewModel
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
private var cancellables = Set<AnyCancellable>()
init() {
fetchData()
}
deinit {
unregister()
}
func unregister() {
if listenerRegistration != nil {
listenerRegistration?.remove()
}
}
func fetchData() {
unregister()
listenerRegistration = db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBooks(at offsets: IndexSet) {
self.books.remove(atOffsets: offsets)
}
}
Views
import SwiftUI
struct SampleView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
InnerListView1(viewModel: viewModel)
InnerListView2(viewModel: viewModel)
}
}
}
struct InnerListView1: View {
#ObservedObject var viewModel: BooksViewModel
var body: some View {
List {
ForEach(viewModel.books) { book in
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.onDelete { indexSet in
self.viewModel.deleteBooks(at: indexSet)
}
}
}
}
struct InnerListView2: View {
#ObservedObject var viewModel: BooksViewModel
var body: some View {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
}
}
One thing I noticed when trying to reproduce your issue: if you're using CodingKeys (which you only need to do if your the attribute names on the Firestore documents are different from the attribute names on your Swift structs), you need to make sure that the id is also included. Otherwise, id will be nil, which will result in the List view not being abel to tell the items apart.

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!