SwiftUI Swipe Actions Note Working Correctly - swift

So I’m working on this shopping list and basically I have a list of tile views that have swipe actions.
The bought and delete actions work perfectly, but I am unable to get the edit button to work. The edit button pulls up a form already filled with the item’s information so you can edit it.
The code for the button works, when I make the tile itself a button and tap the tile the edit form comes up.
Is there a limit to swipe actions that limit you pulling up a form in a swipe action? Here is the code for the list. Thanks! (Program is coded using swift ui and swift)
ForEach(Item) { madeItem in
FoodListTileView(foodItem: madeItem)
.swipeActions(edge: .leading) {
Button {
madeItem.justBought()
FoodDataController.shared.saveItem(madeItem: madeItem)
} label: {
Label("Just Bought", systemImage: "checkmark.square")
}
.tint(.green)
}
.swipeActions(edge: .trailing) {
Button { FoodDataController.shared.deleteItem(madeItem: madeItem)} label : {
Label("Delete", systemImage:"trash")
}
.tint(.red)
Button(action: {showingCreateView = true}) {
Label("Edit", systemImage: "pencil.circle")
}
.sheet(isPresented: $showingCreateView) {
AddItemView(Item: madeItem)
}
.tint(.orange)
}
}
}
.listStyle(.plain)

#Raja is absolutely right. The .sheet inside swipeAction doesn't work, it has to be outside. Better even outside of the ForEach, as otherwise it will always only show the last value of the forEach.
If it's outside the ForEach, you have to use a different way to let sheet know which data to display: .sheet(item:) which is the better alternative in most cases anyway. For that go from #State var showingCreateView: Bool to something like #State var editingItem: Item? where Item should be your items Class. And in the button action, give it the current item value.
Here is the amended code:
// test item struct
struct Item: Identifiable {
let id = UUID()
var name: String
}
struct ContentView: View {
// test data
#State private var items = [
Item(name: "Butter"),
Item(name: "Honey"),
Item(name: "Bread"),
Item(name: "Milk"),
Item(name: "Ham")
]
#State private var editingItem: Item? // replace Item with your item type
var body: some View {
List {
ForEach(items) { madeItem in
Text(madeItem.name)
.swipeActions(edge: .leading){
Button {
} label: {
Label("Just Bought", systemImage: "checkmark.square")
}
.tint(.green)
}
.swipeActions(edge: .trailing){
Button {
} label : {
Label("Delete", systemImage:"trash")
}
.tint(.red)
Button {
editingItem = madeItem // setting editingItem (anything else but nil) brings up the sheet
} label: {
Label("Edit", systemImage: "pencil.circle")
}
.tint(.orange)
}
}
}
.sheet(item: $editingItem) { item in // change to item: init here
Text("Edit: \(item.name)")
}
.listStyle(.plain)
}
}

Related

How to manually set selection on SwiftUI List?

Same question here: How to stop showing Detail view when item in Master view deleted?.
Now I am developing a macOS app, which there's a List and a Detail view, also there's a selection binding the list row, which use to delete the row. But when I delete the row, the detail view didn't disappears.
Also there's an ADD button, when user click it, a new row will append to the last positon. But the selection always stay on the last postion, so I want to change to the new created one.
Here's the code:
struct DetailView: View {
var item: String
var body: some View {
Text("Detail of \(item)")
}
}
struct ContentView: View {
#State private var items = ["Item 1", "Item 2", "Item 3"]
#State private var selection: String?
var body: some View {
NavigationView {
VStack {
List {
ForEach(items, id: \.self, selection: $selection) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item)
}
}
.onDelete(perform: delete)
}
Button {
let newCreatedItem = add()
selection = newCreatedItem // not work!
} label: {
Image(systemName: "add")
}
Button {
if let item = selection {
delete(item: item) // some other function
}
} label: {
Image(systemName: "minus.rectangle.fill")
}
.disabled(selection == nil)
}
Text("Please select an item.")
}
}
func delete(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
}
I tried to set selection = nil on deletion, but it doesn't work.
Also I want to set selection = newCreatedItem, it doesn't work.

SwiftUI: Edit list row relement with swipe action and present modal sheet

Hi. I have a swiftui project with a list. I would now like to add 2 trailing swipe actions to this list, once the .onDelete and a edit swipe action to the left of it.
Like this:
Look:
To achieve this in swiftui I added the following code to my list:
List {
ForEach(timers, id: \.id) { timer in
TimerRow(timer: timer)
}
.onDelete(perform: { IndexSet in deleteTimer(IndexSet) })
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
// Open edit sheet
isShowEditTimer.toggle()
} label: {
Image(systemName: "pencil.circle")
}
}
}
But unfortunately only the edit function is displayed now:
Look 🙁
Do you know how I can solve my problem?
But now to my real problem:
I now want to open a modal sheet when the edit swipe action of a line is pressed. But how do I find out which line was swiped on? With the .onDelete function we get an IndexSet, but nothing here. I would also like to give the struct that is called in my sheet this certain swiped element (CoreData object):
.sheet(isPresented: $isShowEditTimer) {
EditTimerView(timerObject: ???)
}
By the way, this sheet is applied to my navigation view.
I would be really happy if someone could help me and if you didn't report my post. Maybe this question has been asked somewhere deep in StackOverflow, but I'm also relatively new to swiftui (always UIKit before) and don't understand every stackoverflow post yet.
Thanks!!! 😀
Hi Eric and welcome to SO! If you wanna get answers to your question pls read this article https://stackoverflow.com/help/minimal-reproducible-example
Anyway, I faced the same problem that you describing and if you don't mind I will show how to solve it on my code base
import SwiftUI
struct Car: Identifiable {
let id = UUID().uuidString
var model: String
}
class CarViewModel: ObservableObject {
#Published var cars: [Car] = [
.init(model: "Audi"),
.init(model: "BMW"),
.init(model: "Chevrolet"),
.init(model: "Honda"),
.init(model: "Toyota")
]
func del(_ car: Car) {
if let deletingCarIndex = self.cars.firstIndex(where: { $0.id == car.id}) {
self.cars.remove(at: deletingCarIndex)
}
}
}
struct ContentView: View {
#ObservedObject var vm = CarViewModel()
#State var editingCar: Car?
var body: some View {
List {
ForEach(vm.cars) { car in
Text(car.model)
.swipeActions(edge: .trailing) {
Button(role: .destructive) { // if you implement custom swipeAction you have no more access to .onDelete
vm.del(car) // if you stay in ForEach loop you have access to car instance and can find index of this instance in array
} label: {
Label("Delete", systemImage: "trash")
}
Button {
edit(car)
} label: {
Label("Edit", systemImage: "pencil.circle")
}
}
}
.sheet(item: $editingCar) {
reset()
} content: { car in
VStack(spacing: 20) {
Text(car.model)
Text(car.id)
}
}
}
}
func reset() {
editingCar = nil
}
func edit(_ car: Car) {
editingCar = car
}
}
Hope now it clear for you 🙂

How do I prevent the user from switching to certain tabs in a SwiftUI TabView

I am creating sort of a game in swift that gives the user a new clue every time they guess incorrectly. I would like to have a TabView for the clues, but I don’t want the player to have access to every clue at the start. I was thinking either locking the tabs in the TabView until a function is called or making an array of different views in the TabView that can be edited.
One note before starting my reply: in this community you need to be clear about the issue and show examples of your code, error messages, points where you get stuck. Your answer is quite generic, but I will give you some hints.
Each Tab is a view (almost) like any other one, it can be hidden conditionally.
You can just hide the tabs until the user reaches the point in the game where the clues must be shown. In addition, or in alternative, you can switch to different views to see each clue.
See an example below (there's room for improvement, this is the idea):
struct Tabs: View {
#State private var showTab2 = false // This will trigger the tab to be shown
// It can be stored in your view model
var body: some View {
TabView {
Example(showNextClue: $showTab2)
.tabItem {Label("Tab 1", systemImage: "checkmark")}
if showTab2 { // Tab 2 is hidden until you change the state variable
Tab2()
.tabItem {Label("Tab 2", systemImage: "flag")}
}
Example(showNextClue: $showTab2)
.tabItem {Label("Tab 3", systemImage: "trash")}
}
}
}
struct Tab2: View {
#State private var clueIndex = 0 // What clue will you show now?
var body: some View {
VStack {
switch clueIndex { // Show the applicable clue
// You can iterate through an array of views as you proposed, chose the best method
case 0:
Text("Now you can see this clue number 1")
case 1:
Text("Clue number 2")
default:
Text("Clue \(String(clueIndex))")
}
Button {
clueIndex += 1
} label: {
Text("Show next clue")
}
.padding()
}
}
}
struct Example: View {
#Binding var showNextClue: Bool
var body: some View {
VStack {
Text("This is open")
.padding()
Button {
showNextClue.toggle()
} label: {
Text(showNextClue ? "Now you can see the next clue" : "Click here to see your next clue")
}
}
}
}
Unfortunately you cannot disable the standard TabBar selectors. But you can do your own custom ones, e.g. like this:
struct ContentView: View {
#State private var currentTab = "Home"
#State private var activeTabs: Set<String> = ["Home"] // saves the activated tabs
var body: some View {
VStack {
TabView(selection: $currentTab) {
// Tab Home
VStack(spacing: 20) {
Text("Home Tab")
Button("Unlock Hint 1") { activeTabs.insert("Hint 1")}
Button("Unlock Hint 2") { activeTabs.insert("Hint 2")}
}
.tag("Home")
// Tab 1. Hint
VStack {
Text("Your first Hint")
}
.tag("Hint 1")
// Tab 2. Hint
VStack {
Text("Your Second Hint")
}
.tag("Hint 2")
}
// custom Tabbar buttons
Divider()
HStack {
OwnTabBarButton("Home", imageName: "house.fill")
OwnTabBarButton("Hint 1", imageName: "1.circle")
OwnTabBarButton("Hint 2", imageName: "2.circle")
}
}
}
func OwnTabBarButton(_ label: String, imageName: String) -> some View {
Button {
currentTab = label
} label: {
VStack {
Image(systemName: imageName)
Text(label)
}
}
.disabled(!activeTabs.contains(label))
.padding([.horizontal,.top])
}
}

TextField in a list not working well in SwiftUI

This problem is with SwiftUI for a iPhone 12 app, Using xcode 13.1.
I build a List with TextField in each row, but every time i try to edit the contents, it is only allow me tap one time and enter only one character then can not keep enter characters anymore, unless i tap again then enter another one character.Did i write something code wrong with it?
class PieChartViewModel: ObservableObject, Identifiable {
#Published var options = ["How are you", "你好", "Let's go to zoo", "OKKKKK", "什麼情況??", "yesssss", "二百五", "明天見"]
}
struct OptionsView: View {
#ObservedObject var viewModel: PieChartViewModel
var body: some View {
NavigationView {
List {
ForEach($viewModel.options, id: \.self) { $option in
TextField(option, text: $option)
}
}
.navigationTitle("Options")
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
addNewOption()
} label: {
HStack {
Image(systemName: "plus")
Text("Create a new option")
}
}
}
}
}
}
func addNewOption() {
viewModel.options.insert("", at: viewModel.options.count)
}
}
struct OptionsView_Previews: PreviewProvider {
static var previews: some View {
let pieChart = PieChartViewModel()
OptionsView(viewModel: pieChart)
}
}
Welcome to StackOverflow! Your issue is that you are directly updating an ObservableObject in the TextField. Every change you make to the model, causes a redraw of your view, which, of course, kicks your focus from the TextField. The easiest answer is to implement your own Binding on the TextField. That will cause the model to update, without constantly redrawing your view:
struct OptionsView: View {
// You should be using #StateObject instead of #ObservedObject, but either should work.
#StateObject var model = PieChartViewModel()
#State var newText = ""
var body: some View {
NavigationView {
VStack {
List {
ForEach(model.options, id: \.self) { option in
Text(option)
}
}
List {
//Using Array(zip()) allows you to sort by the element, but use the index.
//This matters if you are rearranging or deleting the elements in a list.
ForEach(Array(zip(model.options, model.options.indices)), id: \.0) { option, index in
// Binding implemented here.
TextField(option, text: Binding<String>(
get: {
model.options[index]
},
set: { newValue in
//You can't update the model here because you will get the same behavior
//that you were getting in the first place.
newText = newValue
}))
.onSubmit {
//The model is updated here.
model.options[index] = newText
newText = ""
}
}
}
.navigationTitle("Options")
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
addNewOption()
} label: {
HStack {
Image(systemName: "plus")
Text("Create a new option")
}
}
}
}
}
}
}
func addNewOption() {
model.options.insert("", at: model.options.count)
}
}

Insert, update and delete animations with ForEach in SwiftUI

I managed to have a nice insert and delete animation for items displayed in a ForEach (done via .transition(...) on Row). But sadly this animation is also triggered when I just update the name of Item in the observed array. Of course this is because it actually is a new view (you can see that, since onAppear() of Row is called).
As we all know the recommended way of managing lists with cool animations would be List but I think that many people would like to avoid the standard UI or the limitations that come along with this element.
A working SwiftUI example snippet is attached (Build with Xcode 11.4)
So, the question:
Is there a smart way to suppress the animation (or have another one) for just updated items that would keep the same position? Is there a cool possibility to "reuse" the row and just update it?
Or is the answer "Let's wait for the next WWDC and let's see if Apple will fix it..."? ;-)
Cheers,
Orlando 🍻
Edit
bonky fronks answer is actually a good approach when you can distinguish between edit/add/delete (e.g. by manual user actions). As soon as the items array gets updated in background (for example by synced updates coming from Core Data in your view model) you don't know if this is an update or not. But maybe in this case the answer would be to manually implement the insert/update/delete cases in the view model.
struct ContentView: View {
#State var items: [Item] = [
Item(name: "Tim"),
Item(name: "Steve"),
Item(name: "Bill")
]
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
Row(name: item.name)
}
}
}
.navigationBarItems(leading: AddButton, trailing: RenameButton)
}
}
private var AddButton: some View {
Button(action: {
self.items.insert(Item(name: "Jeff"), at: 0)
}) {
Text("Add")
}
}
private var RenameButton: some View {
Button(action: {
self.items[0].name = "Craigh"
}) {
Text("Rename first")
}
}
}
struct Row: View {
#State var name: String
var body: some View {
HStack {
Text(name)
Spacer()
}
.padding()
.animation(.spring())
.transition(.move(edge: .leading))
}
}
struct Item: Identifiable, Hashable {
let id: UUID
var name: String
init(id: UUID = UUID(), name: String) {
self.id = id
self.name = name
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Luckily this is actually really easy to do. Simply remove .animation(.spring()) on your Row, and wrap any changes in withAnimation(.spring()) { ... }.
So the add button will look like this:
private var AddButton: some View {
Button(action: {
withAnimation(.spring()) {
self.items.insert(Item(name: "Jeff"), at: 0)
}
}) {
Text("Add")
}
}
and your Row will look like this:
struct Row: View {
#State var name: String
var body: some View {
HStack {
Text(name)
Spacer()
}
.padding()
.transition(.move(edge: .leading))
}
}
The animation must be added on the VStack with the modifier animation(.spring, value: items) where items is the value with respect to which you want to animate the view. items must be an Equatable value.
This way, you can also animate values that you receive from your view model.
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(items, id: \.self) { item in
Row(name: item.name)
}
}
.animation(.spring(), value: items) // <<< here
}
.navigationBarItems(leading: AddButton, trailing: RenameButton)
}
}