Reusability support in List in SwiftUI - ios

I'm currently working on a project that uses SwiftUI. I was trying to use List to display a list of let's say 10 items.
Does List supports reusability just like the UITableview?
I went through multiple posts and all of them says that List supports reusability: Does the List in SwiftUI reuse cells similar to UITableView?
But the memory map of the project says something else. All the Views in the List are created at once and not reused.
Edit
Here is how I created the List:
List {
Section(header: TableHeader()) {
ForEach(0..<10) {_ in
TableRow()
}
}
}
TableHeader and TableRow are the custom views created.

List actually provides same technique as UITableView for reusable identifier. Your code make it like a scroll view.
The proper way to do is providing items as iterating data.
struct Item: Identifiable {
var id = UUID().uuidString
var name: String
}
#State private var items = (1...1000).map { Item(name: "Item \($0)") }
...
List(items) {
Text($0.name)
}
View hierarchy debugger shows only 17 rows in memory

Related

SwiftUI NavigationSplitView column visibility on iPhone?

I'm trying to create a 3-column layout in SwiftUI.
The first column is a LazyVGrid of selectable items. The selection then impacts a list of items in a content view (second column) which also isn't a SwiftUI list but a VStack + other views in a scrollview. Selecting items on that column impacts the detail view.
I got it all to work on the iPad, but this is because the iPad displays multiple columns at a time and NavigationSplitView supports gestures on the iPad as well as column visibility settings in code.
The problem is that I can't find a way to programmatically navigate from one column to another on the iPhone as it doesn't seem to respond to column visibility bindings.
I initially had it working with navigation links for each item on my grid where the destination was set to a view, but the code smelled pretty bad.
Eventually, I came up with the code below. In the first column I have my grid view which has a custom onSelect modifier that I trigger whenever an item is selected. That's where I'm trying to change column visibility. I tried setting vm.navigationColumnVisibility = .detailOnly, but the iPhone seems to ignore it.
I was able to get it to work exactly as expected by changing the grid to a List. The selection property/Binding in the List view seemed to trigger navigation on the iPhone. However, that's not the desired UI/UX.
Any advice on how to trigger navigation between columns programatically on the iPhone or a better way to adapt this code to achieve the described UI/UX?
struct JNavSplitView: View {
#EnvironmentObject var vm: JNavSplitViewModel
#State private var book: Book? = nil
#State private var entry: Entry? = nil
var sheet: some View {
#if os(macOS)
JEntryCreateFormMacOS(book: book)
#else
JEntryCreateForm(book: book)
.onCreate { entry in
self.entry = entry
}
#endif
}
var body: some View {
NavigationSplitView(columnVisibility: $vm.navigationColumnVisibility) {
BooksGridView(selected: $book)
.onSelect { book in
self.entry = book.getEntries(forDate: Date()).first
}
} content: {
BookEntriesView(book: book, selected: $entry)
} detail: {
PageCollectionView(entry: $entry)
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
#if os(macOS)
.toolbarBackground(Color("Purple 1000"), for: .windowToolbar)
#endif
.environmentObject(vm)
.sheet(isPresented: $vm.presentNewEntryForm, content: {
sheet
})
}
}

How to use Transferable for Table Rows in SwiftUI

With WWDC 2022, Apple introduced the Transferable protocol to support Drag & Drop operations in an easy way. How can I use this new technique (in combination with the new draggable and dropDestination modifiers) for SwiftUI tables (not lists)?
The TableRowContent does not support the draggable and dropDestination modifiers. Also, when applying the modifiers directly to the views in the TableColumns, the drag / drop operation will only work on for that specific cell, and not the entire row. When adding the modifiers to all cells, it still does not work when dragging e.g. in an empty space inside of a row.
struct Item: Identifiable, Codable, Transferable {
let id = UUID()
let text: String
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .text)
}
}
struct Test: View {
var body: some View {
Table {
TableColumn("Column 1") { item in
Text(item.text)
.draggable(item) // This does only work when dragging not in the space between two columns
}
TableColumn("Column 2") { item in
Text(item.text)
.draggable(item) // This does only work when dragging not in the space between two columns
}
} rows: {
ForEach([Item(text: "Hello"), Item(text: "World")]) { item in
TableRow(item)
.draggable(item) // This causes a compile-time error
.itemProvider { ... } // This does not work with Transferable and thus support my use case
}
}
}
}
I want a similar behavior as the itemProvider modifier and the recently added contextMenu modifier on the TableRowContent , which allow the respective operation on the whole table row. I cannot use itemProvider, since it requires to return an NSItemProvider, which does not support my use case of dragging a file from a network drive to the Mac hard drive.
I found out that you can register a Transferable object in the NSItemProvider:
TableRow(item)
.itemProvider {
let provider = NSItemProvider()
provider.register(item)
return provider
}
Unfortunately the itemProvider closure is already called when the dragging session starts instead of when the drop was performed. But I guess this problem is a different question to answer.

Swift: (How) Can I use two different Data Model Structs in one View?

I have a UI with a list of items that the user can tap. This opens a detail view listing all details for one item.
However, I want to include 2 values in that detail view that are stored in a different collection in Firestore and that also have their own Data Model struct. The reason for this is that a different app works with that collection and I want to separate "shared" collections from the rest.
I got a function that is pulling these 2 values from Firestore done
This function is passing the values to a struct called CareData in my Data Model done
I think I set up everything correctly in the detail view, but the problem is passing that data from the tabable list to the detail view.
Let me try to explain what I did with my code:
Data Model
Just simple arrays, nothing complex.
struct Items: Decodable, Identifiable {
var id: String
var name: String
…
}
struct CareData {
var avHeight: Int
var avWater: Int
}
Detail View
struct Detail: View {
#EnvironmentObject var model: ViewModel
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
// Passing the model instances
let item: Items
let care: CareData
var body: some View {
// Some (working) code where data like item.name is shown.
...
// The 2 values using the CareData Data Model
Text("Height: \(care.avHeight)")
Text("Water: \(care.avWater)")
}
}
View Model
An important note here: The documents in my shared collection are named after the item name of the not shared collection. When I call the function, I use item.name to query to the correct document in the shared collection.
class ViewModel: ObservableObject {
#Published var itemList = [Items]()
#Published var careData = [CareData]()
...
func getCareData(item: String) {
// Some code that gets the data in Firestore from the shared collection and appends it to careData. It is working all well.
}
}
Problematic List View
Detail() in the NavigationLink is expecting me to pass 2 parameters because I am trying to pass both model instances in the Detail View. I understand to use item: item, as I am looping through all items and need to define what is needed for the Detail View. But what do I need to add for care:?
struct PlantList: View {
#EnvironmentObject var model: ViewModel
var body: some View {
ForEach(model.itemList) { item in
NavigationLink(destination: Detail(item: item, care: ?????? )) { // XCode proposing to go for "CareData", but then throws the error "Cannot convert value of type 'CareData.Type' to expected argument type 'CareData'"
Text(item.name)
}
.onAppear {
model.getCareData(item: item.name)
}
}
}
}
I tried weird things like CareDate.init(avHeight: , avWater:) and it worked when I wrote numbers straight into the code, but I need the variables to be there not some static numbers I came up with.
I hope someone can help. All I want is to show the 2 values in the detail view. This is probably a stupid issue, but I'm frustrated as I seem to not understand the very basics of Swift programming yet.
try something like this (untested of course, since I don't have your database), if you only ever want the first element:
EDIT-1: update
struct PlantList: View {
#EnvironmentObject var model: ViewModel
var body: some View {
ForEach(model.itemList) { item in
NavigationLink(destination: Detail(item: item, care: model.careData.first ?? CareData(avHeight: 0, avWater: 0))) {
Text(item.name)
}
.onAppear {
model.getCareData(item: item.name)
}
}
}
}

SwiftUI load very long list in async way

I've a navigation and detail view that is sending a dictionary of date (key) and array of an struct (but it's not important the struct, it contains array of string and other stuff.
If I send a very very long dictionary, the app is freezing in the selected row and the detail appears once the List finished to load each record.
struct DetailView: View {
var selectedChat: [Date: [TextStruct]]? // you can try with [Date: [String]]?
var body: some View {
List
{
ForEach(self.selectedChat.keys.sorted(), id: \.self)
{ key in //section data
Section(header:
Text("\(self.selectedChat[key]![0].date)
{
ForEach(self.selectedChat[key]!, id:\.self) {sText in
// my ChatView(sText) ....
}
}
}
}
I've tried to load some rows at the start by adding this var
#State private var dateAndText: [Date: [TextStruct]] = [:]
substitute the code above (self.selectedChat) whit self.dateAndText and on .onAppear:
.onAppear {
if let chat = self.selectedChat {
let keysDateSorted = chat.allText.keys.sorted()
self.chatLeader = chat.chatLeader
for key in keysDateSorted.prefix(30) {
self.dateAndText[key] = chat.allText[key]
}
DispatchQueue.global(qos: .utility).async {
self.dateAndText = chat.allText
self.progressBarValue = 1
}
}
}
With this solution, once I push the row, immediately I can see the first 30 records, and it is ok, but I can't scroll until all the records are loaded. I know there is a way to load the array only if the user is scrolling at the end of the list, but I want to load all the list also if the user don't scroll at the end.
So, there is a way to load the list partially (like send and update the array each 100 records) and in async way (in order to don't freeze the display for bad user experience)?
You are almost certainly running into the issues described and fixed here by Paul Hudson.
SwiftUI is trying to animate all of the changes so if you use his hack around the issue it should work but you will lost all animations between changes of the list.
Apple devs responded to him and Dave DeLong who were discussing it on Twitter, they said that it is definitely an issue on their end that they hope to fix.
tldr of the article:
Add .id(UUID()) to the end of your List's initializer.

SwiftUI: NavigationLink pops immediately if used within ForEach

I'm using a NavigationLink inside of a ForEach in a List to build a basic list of buttons each leading to a separate detail screen.
When I tap on any of the list cells, it transitions to the detail view of that cell but then immediately pops back to the main menu screen.
Not using the ForEach helps to avoid this behavior, but not desired.
Here is the relevant code:
struct MainMenuView: View {
...
private let menuItems: [MainMenuItem] = [
MainMenuItem(type: .type1),
MainMenuItem(type: .type2),
MainMenuItem(type: .typeN),
]
var body: some View {
List {
ForEach(menuItems) { item in
NavigationLink(destination: self.destination(item.destination)) {
MainMenuCell(menuItem: item)
}
}
}
}
// Constructs destination views for the navigation link
private func destination(_ destination: ScreenDestination) -> AnyView {
switch destination {
case .type1:
return factory.makeType1Screen()
case .type2:
return factory.makeType2Screen()
case .typeN:
return factory.makeTypeNScreen()
}
}
If you have a #State, #Binding or #ObservedObject in MainMenuView, the body itself is regenerated (menuItems get computed again) which causes the NavigationLink to invalidate (actually the id change does that). So you must not modify the menuItems arrays id-s from the detail view.
If they are generated every time consider setting a constant id or store in a non modifying part, like in a viewmodel.
Maybe I found the reason of this bug...
if you use iOS 15 (not found iOS 14),
and you write the code NavigationLink to go to same View in different locations in your projects, then this bug appear.
So I simply made another View that has different destination View name but the same contents... then it works..
you can try....
sorry for my poor English...

Resources