SwiftUI .swipeactions and Core Data - swiftui

Currently using Xcode 13 and IOS 15.
I am using CoreData to display a list of upcoming trips. On each trip I have 2 swipe actions, one to delete the trip, and the second one is to edit the trip. When the Edit button is selected I then trigger the showingEditTripScreen to true so that the EditTripScreen sheet is shown. I am passing the trip into this sheet to be edited. The problem is that no matter what Trip from the ForEach row is selected, it is always the first trip data that is being sent to the EditTripScreen. Am I doing this properly, or is their another solution.
Thanks
ForEach(tripVM.trips, id: \.id) { trip in
TripCardView(trip: trip)
.listRowSeparator(.hidden)
//.padding(.horizontal)
.swipeActions(allowsFullSwipe: false) {
// Edit Trip
Button {
showingEditTripScreen = true
} label: {
Label("Edit", systemImage: "pencil.circle.fill")
}
.tint(.green)
// Delete Trip
Button {
tripVM.deleteTrip(trip: trip)
tripVM.getAllTrips()
} label: {
Label("Delete", systemImage: "trash.circle.fill")
}
.tint(.red)
}
.sheet(isPresented: $showingEditTripScreen, onDismiss: {
}, content: {
EditTripScreen(trip: trip)
})
}

You're adding sheet for each cell of your table. So when you set showingEditTripScreen variable to true, all sheets for visible views gets triggered, and only one of them will be shown, maybe the first one or just a random one.
Instead you need to store selected trip and use sheet(item:onDismiss:content:), which will pass you unwrapped item to your content. And this sheet should be single one for your list, not need to add it to each item.
Also onDismiss is an optional parameter, you don't need to pass it if you're not using it. editingTrip will be set to nil when you dismiss it automatically.
#State
var editingTrip: Trip?
var body: some View {
ForEach(tripVM.trips, id: \.id) { trip in
TripCardView(trip: trip)
.listRowSeparator(.hidden)
//.padding(.horizontal)
.swipeActions(allowsFullSwipe: false) {
// Edit Trip
Button {
editingTrip = trip
} label: {
Label("Edit", systemImage: "pencil.circle.fill")
}
.tint(.green)
// Delete Trip
Button {
tripVM.deleteTrip(trip: trip)
tripVM.getAllTrips()
} label: {
Label("Delete", systemImage: "trash.circle.fill")
}
.tint(.red)
}
}
.sheet(item: $editingTrip, content: { editingTrip in
EditTripScreen(trip: editingTrip)
})
}

Related

Add button to picker label

I want to add a button to swiftui picker's label.
But the button is not clickable.
When I click on the button the picker is clicked.
How Do I make the picker take clicks only in the area of the selected value?
and the buttons take his clicks?
import SwiftUI
enum Animal: String, CaseIterable, Identifiable {
case dog
case cat
case bird
var id: String { self.rawValue }
}
struct ContentView: View {
#State private var selectedAnimal = Animal.dog
var body: some View {
Form {
Group {
Section(header: Text("Animales")) {
VStack{
Picker(
selection: $selectedAnimal,
content: {
ForEach(Animal.allCases, id:\.self) {
Text($0.rawValue)
}},
label: {
HStack {
Text ("Chose Animale")
Spacer ()
Button (
action: {
print ("clicked")
},
label: {
Image(systemName: "arrow.clockwise")
})
Spacer ()
}
}
)
}
}
}
}
}
}
To solve this issue we need to separate picker and button and block Form tracking click inside row (which is by default track entire row).
For first move button out of picker and place everything in HStack, for second we need couple of tricks like tapGesture on label and non-default button style for button (for simplicity I used primitive button style, but it's better to create custom with appropriate highlight, etc.)
Here is a simplified updated and tested your code (Xcode 13 / iOS 15):
var body: some View {
Form {
Group {
Section(header: Text("Animales")) {
HStack{
HStack {
Text ("Chose Animale")
Spacer ()
}
.contentShape(Rectangle())
.onTapGesture {
// just blocker for label click
}
.overlay(
Button (
action: {
print ("clicked")
},
label: {
Image(systemName: "arrow.clockwise").foregroundColor(.blue)
})
.buttonStyle(PlainButtonStyle()) // << needed custom !!
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.layoutPriority(1) // << to cover much area
//.border(Color.red) // << for testing area
Picker("",
selection: $selectedAnimal,
content: {
ForEach(Animal.allCases, id:\.self) {
Text($0.rawValue)
}}
)
.labelsHidden() // << hide own label
.fixedSize() // << limit size !!
}
.listRowInsets(EdgeInsets()) // << consume row space !!
}
}
}
}

Multiple sheet(item: ) triggered by SwipeActions button and ToolBar buttons gets nil object in the first time in SwiftUI

I am using swipeActions in ForEach loop of a List and toolbar buttons to show different sheets on my SwiftUI view. But every first time I swipe left and click the Edit button, the object of that line is nil. If I do the same swipe and click thing again, everything goes well. Anyone else had this kind of bug before? Thank you.
Here is the related code:
struct LanguagesView: View {
#State var activeSheet: ActiveSheet?
#State var toBeEdit: MeLanguage?
var body: some View {
NavigationView {
List {
ForEach(self.meLanguages, id: \.id) { lan in
HStack {
Text("\(lan.wrappedName)")
.font(.headline)
}.swipeActions(allowsFullSwipe: false) {
Button(
action: {
self.activeSheet = .editLanguage
self.toBeEdit = lan
},
label: { Label("Edit", systemImage: "pencil") }
) .tint(.indigo)
}
}
}
.sheet(item: $activeSheet,
onDismiss: {
self.toBeEdit = nil
}
){
item in
switch item {
case .addLanguage:
AddLanguage()
case .sortLanguages:
SortLanguagesView()
case .editLanguage:
if self.toBeEdit != nil {
EditLanguageView( meLanguage: self.toBeEdit! )
}
else {
Text("self.toBeEdit is nil")
}
default:
Text("No such button on ContentView.")
}
}
.toolbar {
ToolbarItemGroup {
HStack {
Text("\(self.meLanguages.count) Languages on Card").font(.headline)
self.barButtons
}
}
}
}
}
var barButtons: some View {
HStack {
Button(
action: {
self.activeSheet = .sortLanguages
},
label: { Label("Sort Languages", systemImage: "arrow.up.arrow.down.circle")
}
).id("sortLanguages")
Button(
action: {
self.activeSheet = .addLanguage
},
label: { Label("Add Language",
systemImage: "plus")
.imageScale(.large)
}
)
}
}
}
If I only think of the sheet triggered by swipeActions Edit button, the code below works perfectly. But I still need other sheets triggered by ToolBar buttons.
.sheet(item: self.$toBeEdit, content: { toBeEdit in
EditLanguageView( meLanguage: toBeEdit)
})
After more searching I realised it's not something about SwipeActions. This is actually similar to this question:
SwiftUI presenting sheet with Binding variable doesn't work when first presented
So I added an hidden Text after the ForEach loop and the problem solved.
ForEach(self.meLanguages, id: \.id) { ... }
if self.toBeEdit != nil {
Text("\(self.toBeEdit!.wrappedName)").hidden()
}

Get selected item in SwiftUI list without using a navigation link

I'm writing a SwiftUI Mac app that is similar to a kanban board. The app has three lists: Todo, Doing, and Done. At the bottom of each list is a button to move a task to another list. For example the todo list has a Start Doing button. Selecting a task from the todo list and clicking the button should move the task from the todo list to the doing list.
Every SwiftUI list selection example I have seen uses a navigation link. Selecting a list item takes you to another view. But I don't want to want to navigate to another view when selecting a list item. I want the selected task so I can change its status and move it to the correct list when clicking the button.
Here's the code for one of my lists.
struct TodoList: View {
// The board has an array of tasks.
#Binding var board: KanbanBoard
#State private var selection: Task? = nil
#State private var showAddSheet = false
var body: some View {
VStack {
Text("Todo")
.font(.title)
List(todoTasks, selection: $selection) { task in
Text(task.title)
}
HStack {
Button(action: { showAddSheet = true }, label: {
Label("Add", systemImage: "plus.square")
})
Spacer()
Button(action: { selection?.status = .doing}, label: {
Label("Start Doing", systemImage: "play.circle")
})
}
}
.sheet(isPresented: $showAddSheet) {
AddTaskView(board: $board)
}
}
var todoTasks: [Task] {
// Task conforms to Identifiable.
// A task has a status that is an enum: todo, doing, or done.
return board.tasks.filter { $0.status == .todo}
}
}
When I click on a list item, it is not selected.
How do I get the selected item from the list without using a navigation link?
Workaround
Tamas Sengel's answer led me to a workaround. Give each list item a Start Doing button so I don't have to track the selection.
List(todoTasks, id: \.self) { task in
HStack {
Text(task.title)
Button {
task.status = .doing
} label: {
Text("Start Doing")
}
}
}
The workaround helps for my specific case. But I'm going to keep the question open in hopes of an answer that provides a better alternative to using a button for people who want a way to get the selected list item.
Use a Button in the List and in the action, set a #State variable to the current list item.
#State var currentTask: Task?
List(todoTasks, id: \.self) { task in
Button {
currentTask = task
} label: {
Text(task.title)
}
}
Use .environment(\.editMode, .constant(.active)) to turn on selecting capability.
import SwiftUI
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")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// This is a walk-around: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
print(multiSelection)
})
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Put your 3 List with same data array but filtering by status on each one something like:
task.filter({ $0.status == .toDo })
Then on your row add the modifier .onTapGesture be sure to cover all the available space.
Inside the code block introduce your logic or func to change the item status. changeTaskStatus(item: task)

SwiftUI: Toggling a JSON property in a List

I have a local JSON file I am importing and decoding. I am them iterating through that data to create a list. I have a Button and I want to toggle the value of the favorite property when the button is tapped. I realize that would be mutating a JSON value which wouldnt work so I am trying to figure out how to accomplish this.
Towns.json
[
{
"display_name": "California",
"favorite": false,
},
{
"display_name": "Colorado",
"favorite": false,
}
]
Town.swift
struct Town: Codable, Identifiable {
var id: String {image}
let display_name: String
let favorite: Bool
}
MainView.swift
ForEach(towns) { town in
LazyVStack(spacing: 20) {
HStack {
Text(town.display_name)
Spacer()
Button {
town.favorite.toggle()
} label: {
if town.favorite {
Image(systemName: "flame").foregroundColor(.red)
} else {
Image(systemName: "flame.fill").foregroundColor(.red)
}
}
}
}
}
You'll need a way to access the original element from the array. In SwiftUI 3 (just announced), this has become much easier, but until that's out, generally people use indices or enumerated to get an index of the original item (there's also a .indexed() from Swift Collections, but it requires importing an SPM package to use it):
struct ContentView : View {
#State var towns : [Town] = []
var body: some View {
ForEach(Array(towns.enumerated()), id: \.1.id) { (index,town) in
LazyVStack(spacing: 20) {
HStack {
Text(town.display_name)
Spacer()
Button {
towns[index].favorite.toggle()
} label: {
if town.favorite {
Image(systemName: "flame").foregroundColor(.red)
} else {
Image(systemName: "flame.fill").foregroundColor(.red)
}
}
}
}
}
}
}
You'll also need to change let favorite to var favorite in your model, since now it's a mutable property.

SwiftUI Picker onReceive() called every time the body is rendered

i am using a Picker to show a segmented control and wish to know when the picker value changes so i can perform a non-UI action. Using the proposed onReceive() modifier (as suggested here) does not work as it is called every time the body is rendered.
Here's the code i have:
struct PickerView: View {
#State private var weather = 0
#State private var showMessage = false
var body: some View {
VStack(spacing: 24) {
Picker(selection: $weather, label: Text("Weather")) {
Image(systemName: "sun.max.fill").tag(0)
Image(systemName: "cloud.sun.rain.fill").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: 120, height: 48)
.onReceive([weather].publisher.first()) { connectionType in
print("connection type is: \(connectionType)")
}
Button(action: { self.showMessage.toggle() }) {
Text("Press Me")
}
if showMessage {
Text("Hello World")
}
}
}
}
The onReceive() block will get called any time the body is rendered, including the first time and any time the button (which toggles showing a message) is pressed.
Any ideas why this is happening and how i can only react to when the picker value is changed?
Here is possible solution instead of .onReceive
Picker(selection: Binding( // << proxy binding
get: { self.weather },
set: { self.weather = $0
print("connection type is: \($0)") // side-effect
})
, label: Text("Weather")) {
Image(systemName: "sun.max.fill").tag(0)
Image(systemName: "cloud.sun.rain.fill").tag(1)
}