List with drag and drop to reorder on SwiftUI - swiftui

How can I add drag and drop to reorder rows on SwiftUI?
Just a clean solution without 'Edit mode'. Here an example:
UPDATE
I asked this question on The SwiftUI Lab and the author replied with this code. Only works on iPad
import SwiftUI
struct Fruit: Identifiable {
    let id = UUID()
    let name: String
    let image: String
}
struct ContentView: View {
    #State var selection: Set<UUID> = []
    #State private var fruits = [
        Fruit(name: "Apple", image: "apple"),
        Fruit(name: "Banana", image: "banana"),
        Fruit(name: "Grapes", image: "grapes"),
        Fruit(name: "Peach", image: "peach"),
        Fruit(name: "Kiwi", image: "kiwi"),
    ]
    var body: some View {
        VStack {
            NavigationView {
                List(selection: $selection) {
                    ForEach(fruits) { fruit in
                        HStack {
                            Image(fruit.image)
                                .resizable()
                                .frame(width: 30, height: 30)
                            Text(fruit.name)
                        }
                    }
                    .onMove { _, _ in }
                }
                .navigationBarTitle("Fruits (Top)")
            }
        }
    }
}

To put the list in the edit mode when the user long presses an item, you can use a state flag and set the edit environment value accordingly. It is important to make the flag changes animated in order not to look very weird.
struct ContentView: View {
#State private var fruits = ["Apple", "Banana", "Mango"]
#State private var isEditable = false
var body: some View {
List {
ForEach(fruits, id: \.self) { user in
Text(user)
}
.onMove(perform: move)
.onLongPressGesture {
withAnimation {
self.isEditable = true
}
}
}
.environment(\.editMode, isEditable ? .constant(.active) : .constant(.inactive))
}
func move(from source: IndexSet, to destination: Int) {
fruits.move(fromOffsets: source, toOffset: destination)
withAnimation {
isEditable = false
}
}
}

Draggable items in SwiftUI List with changing the order
I solved it with return NSItemProvider() when I try to drag an item.
And standard .onMove function.
If I understand correctly, I am grabbing a row container as an NSObject (thanks to initialization NSItemProvider), and .OnMove allows me the ability to reorder items in the List.
I'm still learning and may misunderstand some of the nuances. There must be a better explanation. But it definitely works (I only tested this on ios 15 because I use the .background property in my project).
// View
List {
ForEach(tasks) { task in
HStack { // Container of a row
NavigationLink {
Text("There will be an editing View")
} label: {
TaskListRowView(task: task)
}
}
.onDrag { // mean drag a row container
return NSItemProvider()
}
}
.onDelete(perform: deleteItems)
.onMove(perform: move)
}
// Function
func move(from source: IndexSet, to destination: Int) {
tasks.move(fromOffsets: source, toOffset: destination )
}

There is a great writeup on this at: https://www.hackingwithswift.com/quick-start/swiftui/how-to-let-users-move-rows-in-a-list
I don't want to copy paste Paul's code directly here so here's another example I wrote myself with a slightly different structure.
import SwiftUI
struct ReorderingView: View {
#State private var items = Array((0...10))
var body: some View {
NavigationView {
VStack {
List {
ForEach(items) { item in
Text("\(item)")
}.onMove { (source: IndexSet, destination: Int) -> Void in
self.items.move(fromOffsets: source, toOffset: destination)
}
}
}
//.environment(\.editMode, .constant(.active)) // use this to keep it editable always
.navigationBarItems(trailing: EditButton()) // can replace with the above
.navigationBarTitle("Reorder")
}
}
}
struct ReorderingView_Previews: PreviewProvider {
static var previews: some View {
ReorderingView()
}
}
extension Int: Identifiable { // Please don't use this in production
public var id: Int { return self }
}

Related

A coredata computed property used to sort an array of object is not updated. Working when I go from view1 to view2, but not from view 3 to view2

I am still quite new at SwiftUI and I am having trouble to understand why the array that I have created in my model “GameSession”, which is sorted according to a computed variable defined in my second model “GamePlayer”, is not “always” correctly updated.
I have simplified my code to make it more readable, in order to maximize the chances of someone helping me!
Description of my code:
In my App, I have 2 entities
1.     GameSession :which has Attributes "date", "place", "nbplayer" + a relationship called "players" with " GamePlayer "
2.     GamePlayer: which has Attributes "name","scorelevel1","scorelevel2","scorelevel3" + a relationship with "GameSession"
the relationship is "one to many": One GameSession  can have many GamePlayer
 
And basically, I have 3 views:
View 1: Just one navigation link which brings you to View2
View2: Display a list of “Game” played (on which you can click to go to “view3”). For each one, I want to be able to display the date of the game, and the names of the players who played the game, sorted according to a variable “total_score”
View3: You can visualize the game on which you clicked in View2. And you see the name of the players who played the game, and you can modify the score of each player for each one of the 3 levels. And a variable “total_score” sums for each player the score of level 1 to 3
 My problem:
My issue, is that when I go from View3 to View2, the list of the players won’t be correctly sorted according to the “total_score” variable.
But If I go from View1 to View2, the list of the player is correctly sorted… 
I don’t understand what I am doing wrong. 
Here is my code:
My Model for GameSession is like that:. You will notice that I have created in my model a variable which is an array of “GamePlayer” sorted according to the computed variable “total_score” of the GamePlayer entity
import Foundation
import CoreData
 
 
extension GameSession {
 
    #nonobjc public class func fetchRequest() -> NSFetchRequest<GameSession> {
        return NSFetchRequest<GameSession>(entityName: "GameSession")
    }
 
    #NSManaged public var date: Date?
    #NSManaged public var nbplayer: Int32
    #NSManaged public var place: String?
    #NSManaged public var players: NSSet?
    #NSManaged public var photo: NSData?
 
    // pour gerer le fait que "date" ne doit pas etre vide
    public var wrappedDate: Date {
        date ?? Date()
    }
    public var wrappedNbplayer: Int32 {
        nbplayer
    }
   
    // pour gerer le fait que "lieu" ne doit pas etre vide
    public var wrappedPlace: String {
        place ?? "Endroit inconnu"    }
 
   
    // pour gerer le fait que "photo" ne doit pas etre vide
    public var wrappedPhoto: NSData {
        photo ?? NSData()
    }
   
    }
    //
    public var playersArrayRang: [GamePlayer] {
        let playersSet = players as? Set< GamePlayer > ?? []
 
        // On retourne un array sorted en fonction du score total
        return playersSet.sorted {
            $0.totalscore < $1.totalscore
        }
    }
And my model for “GamePlayer” is like that:
extension GamePlayer {
 
    #nonobjc public class func fetchRequest() -> NSFetchRequest< GamePlayer > {
        return NSFetchRequest< GamePlayer >(entityName: " GamePlayer ")
    }
 
    #NSManaged public var name: String?
    #NSManaged public var scorelevel1: Int64
    #NSManaged public var scorelevel2: Int64
    #NSManaged public var scorelevel3: Int64
   #NSManaged public var sessiongame: GameSession?
   
    public var wrappedName: String {
        name ?? "player unknown"
    }
 
    public var total_score: Int64 {
        scorelevel1 + scorelevel2 + scorelevel3
    }
 
}
View 1: GameUIView
struct GameUIView: View {
    var body: some View {
            NavigationLink(destination: GameListePartieUIView ()) {
                Text(" Mes parties ")
            }
    }
}
View 2: GameListePartieUIView
import SwiftUI
import Foundation
 
struct GameListePartieUIView: View {
    #Environment(\.managedObjectContext) var moc
    #FetchRequest(entity: GameSession.entity(), sortDescriptors: [
        NSSortDescriptor(keyPath: \GameSession.date, ascending: false)
    ]) var gamesessions: FetchedResults<GameSession>
   
    #State var showingAddPartie = false
   
    var body: some View {
       
//        VStack {
            List {
                ForEach(gamesessions, id: \.date) { session in
                   
                    HStack{
 
                        NavigationLink (destination: DetailPartieSelecUIView2(session: session)){
                            HStack{
                                Text(" \(session.wrappedDate, formatter: itemFormatter) ")
                                    .frame(width: 120, height: 50, alignment: .top)
                                    .font(.system(size: 12, weight: .light, design: .serif))
 
                                VStack(alignment: .leading){
                                    ForEach(Array(session.playersArrayRang.enumerated()), id: \.offset) { index, player in
                                        if index == 0 {
                                            Text("\(index+1) - \(player.wrappedName)")
                                                .frame(alignment: .leading)
                                                .lineLimit(1)
                                                .font(.system(size: 12, weight: .light, design: .serif))
//                                                .foregroundColor(Color(red: 0.246, green: 0.605, blue: 0))
                                                .foregroundColor(Color("Colorjb1"))
                                                .bold()
                                        } else {
                                            Text("\(index+1) - \(player.wrappedName)")
                                                .frame(alignment: .leading)
                                                .lineLimit(1)
                                                .font(.system(size: 12, weight: .light, design: .serif))
                                        }
 
                                    }
                                }
                               
                            }
                           
                        }
                       
                       
                       
                    }
                   
                }
                .onDelete(perform: deleteSessions)
                .padding(1)
            }
 
            .navigationBarItems(trailing: Button("Ajouter") {
                self.showingAddPartie.toggle()
            })
            .navigationBarTitle("Parties", displayMode: .inline)
            .sheet(isPresented: $showingAddPartie) {
                AddPartieRamiUIView().environment(\.managedObjectContext, self.moc)
            }
            .frame( maxWidth: .infinity)
            .edgesIgnoringSafeArea(.all)
            .listStyle(GroupedListStyle()) // or PlainListStyle()
            .padding(1)
   
    }
   
   
    // --------
    private func deleteSessions(at offsets: IndexSet) {
        for index in offsets {
            let session = ramisessions[index]
            moc.delete(session)
        }
       
        do {
            try moc.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
   
}
View 3: DetailPartieSelecUIView2
struct DetailPartieSelecUIView2: View {
    #Environment(\.managedObjectContext) var moc
   
    #FetchRequest
    private var players: FetchedResults<GamePlayer>
   
    private var session_actuelle: GameSession
   
    init(session: GameSession) {
        let predicate = NSPredicate(format: "sessionramy = %#", session)
        let sortDescriptors = [SortDescriptor(\GamePlayer.name)] // need something to sort by.
        _players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
        session_actuelle = session
    }
   
    let formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        return formatter
    }()
 
   
    var body: some View {
       
        VStack {
            ScrollView(.vertical){
 
                Text("\(session_actuelle.wrappedLieu) - \(session_actuelle.wrappedDate, formatter: itemFormatter)")
               
                ScrollView([.horizontal, .vertical]) {
                    HStack(alignment:.center){
                        // 1er element de mon HStack
                        VStack {
                                Text(" ")
                                Divider()
                                Text("Level 1")
                                Text("Level 2")
                                Text("Level 3")
                                Divider()
                                Text("Total")
                            }
                        // 2eme element de mon HStack
                        ForEach(players, id: \.wrappedName) { player in
                            PlayerView(player: player)
                        }
                    } // HStack
                } //scrollView
 
                // bouton pour sauvegarder les scores de la partie
                Button("Save scores") {
                        do {
                            try self.moc.save()
                        } catch {
                            print("Whoops! \(error.localizedDescription)")
                        }
                    }
 
            }
        }
    }
}
 
struct PlayerView: View {
    #ObservedObject var player:GamePlayer
 
    #Environment(\.managedObjectContext) var moc
   
    #State private var numberFormatter: NumberFormatter = {
        var nf = NumberFormatter()
        nf.numberStyle = .decimal
        return nf
    }()
   
    var body: some View {
       
        VStack {
           
 
                Text(player.wrappedName)
                Divider()
                TextField("Score", value: $player.scorelevel1, formatter:NumberFormatter())
                TextField("Score", value: $player.scorelevel2, formatter:NumberFormatter())
                TextField("Score", value: $player.scorelevel3, formatter:NumberFormatter())
                Divider()
                Text(String(player.scoretotal))
               
            }
    }
}
 
extension Binding {
    func toUnwrapped<T>(defaultValue: T) -> Binding<T> where Value == Optional<T>  {
        Binding<T>(get: { self.wrappedValue ?? defaultValue }, set: { self.wrappedValue = $0 })
    }
}
 
When I am in my “View3” and I change values in the TextField (of PlayerView), player.scoretotal is updated accordingly.
Then I click on my button "Save scores", in order to save my “moc” (Core Data)
But when I click on “back” (on the top left corner of the view) and that I go back to the previous view (View2), the right part of my list, where I am supposed to sort players name according to their total_score, is not updated…
What is weird, is that, if I go “back” to my “View1”, and that I click on the navigation link which open my “View2” (DetailPartieSelecUIView2), Then the names of the players are sorted correctly…
Thanks a lot for reading my long post. Hopefully somebody can help me. Best regards!
I tried everything...
These lines are mistakes:
ForEach(gamesessions, id: \.date) { session in
NSManagedObjects implement the Identifiable protocol so no id param is needed and you wouldn't want to use a date for identity anyway.
ForEach(Array(session.playersArrayRang.enumerated()), id: \.offset) { index, player in
ForEach is a View not a for loop.
Try:
ForEach(gamesessions) { session in
ForEach(players) { player in
If you are trying to make a scoreboard then you need another entity in your model, e.g. ScoreboardEntry with an int rank and when you save any player you need to re-order these entries and assign the rank. You could also use the new derived attributes feature to automate this. Then you can make a FetchRequest that sorts by rank.

Getting the total for a number of fields

I have an app that records costs for a car. I can't work out how to create a field that keeps a running total for the ongoing costs. In the ContentView file I have a struct that defines what an expense is, which includes the 'amount'.
Any help is appreciated. Thanks.
There are 2 files, ContentView, and Addview;
struct ContentView: View {
#StateObject var expenseList = ExpenseList()
#State private var isShowingAddView = false
#State private var totalCost = 0.0
var body: some View {
NavigationView {
VStack {
VStack(alignment: .trailing) {
Text("Total Cost").font(.headline) //just holding a place for future code
}
Form {
List {
ForEach(expenseList.itemList) { trans in
HStack{
Text(trans.item)
.font(.headline)
Spacer()
VStack(alignment: .trailing) {
HStack {
Text("Amount: ")
.font(.caption).bold()
Text(trans.amount, format: .currency(code: "USD"))
.font(.caption)
}
}
}
}
.onDelete(perform: removeItems)
}
}
.navigationTitle("Expenditure")
.toolbar {
Button {
isShowingAddView = true
} label: {
Image(systemName: "plus")
}
}
.sheet(isPresented: $isShowingAddView) {
AddView(expenseList: expenseList)
}
}
}
}
func removeItems(at offsets: IndexSet) {
expenseList.itemList.remove(atOffsets: offsets)
}
}
class ExpenseList: ObservableObject {
#Published var itemList = [ExpenseItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(itemList) {
UserDefaults.standard.set(encoded, forKey: "Things")
}
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Things") {
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
itemList = decodedItems
return
}
}
itemList = []
}
}
struct ExpenseItem: Identifiable, Codable {
var id = UUID()
let item: String
let amount: Double
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct AddView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var expenseList: ExpenseList
#State private var item = "Fuel"
#State private var amount = 0.0
let itemType = ["Fuel", "Tyres"]
var body: some View {
NavigationView {
Form {
Picker("Type", selection: $item) {
ForEach(itemType, id: \.self) {
Text($0)
}
}
TextField("Enter amount...", value: $amount, format: .currency(code: "USD"))
}
.navigationTitle("Add an item...")
.toolbar {
Button("Save") {
let trans = ExpenseItem(item: item, amount: amount)
expenseList.itemList.append(trans)
dismiss()
}
}
}
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView(expenseList: ExpenseList())
}
}
There are many ways to do ... create a field that keeps a running total for the ongoing costs. This is just one way.
Try this approach, using an extra var totalCost in your ExpenseList and a summation.
class ExpenseList: ObservableObject {
#Published private (set) var totalCost = 0.0 // <-- here
#Published var itemList = [ExpenseItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(itemList) {
UserDefaults.standard.set(encoded, forKey: "Things")
}
totalCost = itemList.map{ $0.amount }.reduce(0.0, { $0 + $1 }) // <-- here
}
}
init() {
if let savedItems = UserDefaults.standard.data(forKey: "Things") {
if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems) {
itemList = decodedItems
return
}
}
itemList = []
}
}
And use it like this:
Text("Total Cost: \(expenseList.totalCost)").font(.headline)
You can of course do this, without adding any extra var:
Text("Total Cost: \(expenseList.itemList.map{ $0.amount }.reduce(0.0, { $0 + $1 }))")

How can I mimic the calling signature of NavigationLink

I have code like this:
List(datalist) { data in
    NavigationLink(destination: View1(data: data).headless()) {         
        Text(data.name)
        }
    }
where headless() is a way to avoid all the default top views of a NavigationLink and its corresponding init:
extension View {
// https://www.hackingwithswift.com/forums/swiftui/removing-unwanted-and-unknown-whitespace-possibly-a-navigation-bar-above-a-view/7343
func headless() -> some View {
return self.navigationBarTitle("")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
What I want to do is have a View struct that can be a customized call to NavigationLink that always calls the headless() modifier. I have written this, copying from the declaration of NavigationLink:
struct SimpleNavLink<Destination, Label>: View where Label : View, Destination : View {
private let label: () -> Label
private let destination: () -> Destination
init(#ViewBuilder destination: #escaping () -> Destination, #ViewBuilder label: #escaping () -> Label) {
self.label = label
self.destination = destination
}
var body: some View {
NavigationLink(destination: destination().headless, label: label)
}
}
With that in place I changed the NavigationLink line to this:
SimpleNavLink(destination: View1(data: data)) {
But that gave me the error
Cannot convert value of type 'View1' to expected argument type '() -> Destination'
That was easy enough to solve by just wrapping the destination in { }:
SimpleNavLink(destination: { View1(data: data) } ) {
But WHY? I didn't have to do that for the NavigationLink. When I tried adding #autoclosure to the destination parameter, the compiler said that didn't go with #ViewBuilder
You said
I have written this, copying from the declaration of :
I assume you meant “the declaration of NavigationLink”. But your original code uses a NavigationLink.init that is declared like this:
#available(iOS, introduced: 13.0, deprecated: 100000.0, message: "Pass a closure as the destination")
#available(macOS, introduced: 10.15, deprecated: 100000.0, message: "Pass a closure as the destination")
#available(tvOS, introduced: 13.0, deprecated: 100000.0, message: "Pass a closure as the destination")
#available(watchOS, introduced: 6.0, deprecated: 100000.0, message: "Pass a closure as the destination")
public init(destination: Destination, #ViewBuilder label: () -> Label)
This version of init takes the Destination by value instead of as a function. It's also going to be deprecated at some point.
So, if you want to mimic the (eventually deprecated) syntax, you need to change your init to take the Destination by value instead of as a function. Note also that NavigationLink does not require #escaping closures, so perhaps you shouldn't either. Thus:
struct SimpleNavLink<Destination: View, Label: View>: View {
private let label: Label
private let destination: Destination
init(
destination: Destination,
#ViewBuilder label: () -> Label
) {
self.label = label()
self.destination = destination
}
var body: some View {
NavigationLink(
destination: destination.headless(),
label: { label }
)
}
}

EditButton bug with StackNavigationViewStyle

when use StackNavigationViewStyle, EditButton doesn't work
var body: some View {
NavigationView {
List {
ForEach(books, id: \.self) { book in
....
}
.onDelete(perform: deleteBooks)
}
...
.navigationBarItems(leading: EditButton(), trailing: ...)
....
}
.navigationViewStyle(StackNavigationViewStyle())
}
// Press edit button first:
import SwiftUI
struct Order: Identifiable, Equatable {
let id = UUID()
}
struct ContentView: View {
#State private var orders = [Order(), Order(), Order()]
var body: some View {
NavigationView {
List {
ForEach(orders) { order in
NavigationLink(destination: DetailView(order: order, orders: $orders)) {
Text(order.id.uuidString)
}
}
.onDelete { indexSet in
orders.remove(atOffsets: indexSet)
}
}
.navigationBarItems( leading: EditButton())
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailView: View {
let order: Order
#Binding var orders: [Order]
var body: some View {
if orders.contains(order) {
Text(order.id.uuidString)
} else {
Text("No selection")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to call a function when a #State variable changes

I have a time selection subview with two wheel pickers, one for minutes and one for seconds. The values are bound to two (local) state variables in the parent view. I'm looking for a way to have a global state variable "time" changed every time when either one of the two local variables changes. I don't want to directly bind the two state variables for min/sec to the global state as I want only one variable there holding the time in seconds only. So there should be a conversion time = min * 60 + seconds.
I'm struggling to find a way of detecting a value change of the two local state variables.
Here's the subview:
struct TimePicker: View {
var min: Binding<Int>
var sec: Binding<Int>
var body: some View {
HStack() {
Spacer()
Picker(selection: min, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.frame(width: 40).clipped().pickerStyle(WheelPickerStyle())
Text("Min.")
Picker(selection: sec, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}
}.frame(width: 40).clipped().pickerStyle(WheelPickerStyle())
Text("Sec.")
Spacer()
}
}
}
You may use combineLatest in the main view.
struct MainTimeView: View{
#State private var min : Int = 0
#State private var sec : Int = 0
#State private var myTime: Int = 0
var body: some View {
TimePicker(min: $min, sec: $sec).onReceive(Publishers.CombineLatest(Just(min), Just(sec))) {
self.myTime = $0 * 60 + $1
print(self.myTime)
}
}
}
i think you need to wrapped your global variable kinda
class TimeObject : BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var time:Int = 0 {
didSet {
didChange.send(())
}
}
}
...
struct TimePicker: View {
var min: Binding<Int>
var sec: Binding<Int>
#EnvironmentObject var time : TimeObject
var body: some View {
HStack() {
Spacer()
Picker(selection: min, label: EmptyView()) {
ForEach((0...9), id: \.self) { ix in
Text("\(ix)").tag(ix)
}.gesture(TapGesture().onEnded(){
                        time.time = ix
                })
}.frame(width: 40).clipped().pickerStyle(WheelPickerStyle())
Text("Min.")
Picker(selection: sec, label: EmptyView()) {
ForEach((0...59), id: \.self) { ix in
Text("\(ix)").tag(ix)
}.gesture(TapGesture().onEnded(){
                        time.time = ix
                })
}.frame(width: 40).clipped().pickerStyle(WheelPickerStyle())
Text("Sec.")
Spacer()
}
}
}
...
TimePicker..environmentObject(TimeObject())
unfortunately don't have a chance to test