Is it possible to reorder columns in SwiftUI Tables using Drag and Drop?
Apple has introduced "Table" in SwiftUI 3.0 in summer 2021. It's the SwiftUI equivalent of NSTableView.
I was not able to find any hints in apples documentation regarding column reordering.
I'm speaking of reordering as we all know from NSTableViews like the one in Finder.
I used the sample code provided by Apple.
struct Person: Identifiable {
let givenName: String
let familyName: String
let id = UUID()
}
#State private var people = [
Person(givenName: "Juan", familyName: "Chavez"),
Person(givenName: "Mei", familyName: "Chen"),
Person(givenName: "Tom", familyName: "Clark"),
Person(givenName: "Gita", familyName: "Kumar"),
]
var body: some View {
Table(people, selection: $selectedPeople, sortOrder: $sortOrder) {
TableColumn("Given Name", value: \.givenName)
TableColumn("Family Name", value: \.familyName)
}
}
I also tried to build the table columns dynamically with for each. but that throws a bunch of debug errors.
#State private var columns = [
TableColumn("Given Name", value: \Person.givenName),
TableColumn("Family Name", value: \Person.familyName)
]
var body: some View {
Table(people, selection: $selectedPeople, sortOrder: $sortOrder) {
ForEach (columns, id: \.self) { column in
column
}
}
}
Seems like ForEach is not compatible with TableColumns:
Generic struct 'Table' requires that 'ForEach<[TableColumn<Person, Never, Text, Text>], TableColumn<Person, Never, Text, Text>, some AccessibilityRotorContent>' conform to 'TableColumnContent'
Static method 'buildBlock' requires that 'ForEach<[TableColumn<Person, Never, Text, Text>], TableColumn<Person, Never, Text, Text>, some AccessibilityRotorContent>' conform to 'TableColumnContent'
First of all, you should know that UITableView has only one column.
So to help you with your problem, I can suggest you do these steps.
To have a tableView with multiple columns, create multiple UITableView, and put them together on the UI. (in a way that each tableView will play as one column!)
Now you have some tables that sit next to each other, and each table will be playing as one column.
Now problem is that each table has its own scroll.
As UITableview is derived from UIScrollview you can get the scroll amounts in the 'scrollViewDidScroll' delegate method.
On this delegate method, get the offset of the scrollView which is being scrolled, and assign that value to the other tableViews.
firstTableview.contentOffset = scrollview.contentOffset
Now all tableViews will be scrolling simultaneously with the same px height and feels that they play as a single TableView with multiple columns.
In the end, you want to make the user be able to reorder the table columns (actually you have to make tableViews to be able to change their position)
To achieve this, you have to create a UICollectionView that every cell of a collectionView will be one of your tableView. and then use some packages like 'Reorder' to make cells to be reordered.
(Reorder GitHub address: https://github.com/pikachu987/Reorder)
summary:
Create a UICollectionView with 3 cells. (cell.hight = yourPageHeigh, cell.width = yourPageWidth/3)
Use 'Reorder' package to make cells reorderable.
Each cell of the collectionView will contain one of your UITableView.
By using 'scrollViewDidScroll' method, make all tables scroll simultaneously.
Now you have a table that has some columns and columns can be reordered.
Related
I want a two column-view in a Swift UI app, where the left column displays a list and the right column displays a detail view.
This works fine like this:
struct ContentView: View {
#State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn
var body: some View {
NavigationSplitView( columnVisibility: $columnVisibility) {
List {
ForEach(itemList, id: \.self ){item in
NavigationLink(item.name){ ItemDetail(item: item)}
}
} } detail: {
Text("detail placeholder ")
}
.navigationSplitViewStyle(.automatic)
}
}
In iPad portrait mode I want to show only the list column first. This should then behave like a stack navigation.
In addition, on the Mac, if a certain window width is not reached, only the list column should be displayed. This should then also behave like a stack navigation.
How is this feasible?
This is on iOS 16. I'm on Xcode 14.0.
I have the following view:
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
let id = UUID()
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var multiSelection = Set<UUID>()
var body: some View {
NavigationView {
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.toolbar { EditButton() }
}
Text("\(multiSelection.count) selections")
}
}
This code is taken from https://developer.apple.com/documentation/SwiftUI/List.
I am expecting to see that whenever I click on the "Edit" button, I should be able to select a few items, press "Done", then the bottom would still show the number of items I have selected. However, this is not the case:
I tried to use a debugger, and I found out that whenever I click on "Done" after selecting the items, the multiSelection resets itself to be empty. This used to work on Xcode 13. I can't really find anything on Apple's documentation regarding changes to the EditButton or changes to the List struct.
Update
I filed a bug report and Apple got back to me, they said this is expected behaviour. I guess I misinterpreted the use case for this list selection here.
Before iOS 16 the selection only worked when in editing mode. Now it works also when not editing so I believe the problem is now the selection is being cleared when done is being tapped (so it can be used when not editing).
I think we need to send feedback to request 2 selection bindings, one for editing and one for when not-editing.
i think the issue here is that your identifiers for your list are not stable. Basically every time an ocean object is created, it gets a new UUID. You dont want that UUID to change.
Any time your state property changes, the view may or may not get rebuilt, causing your oceans to get regenerated.
Try storing your oceans in like this:
#State var oceans: [Ocean] = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
Alternatively, you can use the ocean's name as its identifier instead of a UUID that is generated each time its created.
Trying to figure out why my ForEach loop isn’t updating. I have the model marked as ObservedObject and have done everything I could to make sure the updates were happening. I even saw that the model was being updated while printing.
class Model {
var array: [Int]
}
…
struct ModelView: View {
#ObservedObject var model: Model
var body: some View {
List {
ForEach(model.array.indices, id:\.self) { index in
…
}.onDelete(perform: delete)
}
}
The row is animating and acting like it is deleting and does delete in the model, however the deleted row animates back in and the original data set is shown!
I figured it out. I needed to make the array in my model object with the #Published property wrapper. Doing that fixed everything!
I have been creating SwiftUI Views in a Swift Package such as this one:
public struct DottedLoaderView: View {
#State private var isOn = false
let numOfDots: Int
let dotWidth: CGFloat
let dotsColor: Color
public init(numOfDots: Int, dotWidth: CGFloat, dotsColor: Color){
self.numOfDots = numOfDots
self.dotWidth = dotWidth
self.dotsColor = dotsColor
}
public var body: some View {
HStack{
ForEach(0..<numOfDots){ i in
DottedLoaderDotView(dotWidth: dotWidth, dotsColor: dotsColor, delay: Double(i) / 2)
}
}
}
I want to make an application which will use this Package as a catalog of the Views I have implemented in the Package. I would like to display all the available Views from the package inside of a ListView in the catalog app to achieve a result similar to this:
The list row is calling the View struct from the Package. This works fine with a low amount of Views, however, I want to have hundreds of them in the future and would ideally want the app to dynamically display all the available Views and be able to iterate over them inside of a ForEach loop in the body. Is there any convenient approach that comes to mind on how to essentially "look through the Package and fetch all the Views available and have them as some variable ideally in a list?" so I can treat them as data to feed into a ForEach loop? I apologise if this is a really vague question.
I'm looking to move an item in a tableview to a new position. I have models acting as my data source.
var models = [Model]
var dataTableView = UITableView!
A model class looks like this
class Model {
timestamp: Int64
...
}
The models data source is sorted by timestamp, any item inserted should be in it's right position.
When an item's timestamp changes, I want to move it to the corrected sorted position.
What's the best way to do this?
I'm considering just sorting the array to get the item to it's correct position then find it's index and do the following
beginUpdates()
reloadRows(at: [INSERTED_ITEM_INDEX, OLD_INDEX], with: .bottom)
endUpdates()
So that by reloading both index paths, both cells will display correctly.
Another option would be to find which index the item will occupy if inserted into the list (without actually touching the list) and then do the following:
beginUpdates()
moveRow(at: ORIGINAL_INDEX, to: NEW_INDEX)
reloadRows(at: [ORIGINAL_INDEX, NEW_INDEX], with: .bottom)
endUpdates()
Which is the better approach?
None? would be happy to learn new approaches, thanks :)