I am using swiftUI in my app that uses coredata to store FoodItems. In my app I have a list of all the foodItems stored on the users device. Supposedly they are supposed to be able to delete any one of the foodItems by long holding and pressing either eat or throw away, but each time I try to delete any of the foodItems, it always deletes the first one in the list instead of the one I long holded. Can anyone help me? Here is a snippet of my code:
ForEach(self.storageIndex.foodItemArray, id:\.self) { item in
RefrigeratorItemCell(icon: item.wrappedSymbol, title: item.wrappedName, lastsUntil: self.addDays(days: Int(item.wrappedStaysFreshFor), dateCreated: item.wrappedInStorageSince))
.gesture(LongPressGesture()
.onEnded({ i in
self.showEatActionSheet.toggle()
})
)
//TODO: Make a diffrence between eat all and throw away
.actionSheet(isPresented: self.$showEatActionSheet, content: {
ActionSheet(title: Text("More Options"), message: Text("Chose what to do with this food item"), buttons: [
.default(Text("Eat All"), action: {
self.managedObjectContext.delete(item)
try? self.managedObjectContext.save()
})
,.default(Text("Throw Away"), action: {
self.managedObjectContext.delete(item)
try? self.managedObjectContext.save()
})
,.default(Text("Cancel"))
])
})
}
You should use instead .actionSheet(item: and move it out of ForEach cycle, as on below scratchy example
#State private var foodItem: FoodItem? // << tapped item (use your type)
// .. other code
VStack { // << some your parent container
ForEach(self.storageIndex.foodItemArray, id:\.self) { item in
RefrigeratorItemCell(icon: item.wrappedSymbol, title: item.wrappedName, lastsUntil: self.addDays(days: Int(item.wrappedStaysFreshFor), dateCreated: item.wrappedInStorageSince))
.gesture(LongPressGesture()
.onEnded({ i in
self.foodItem = item // << tapped item
})
)
//TODO: Make a diffrence between eat all and throw away
}
} // actionSheet attach to parent
.actionSheet(item: self.$foodItem, content: { item in // << activated on item
ActionSheet(title: Text("More Options"), message: Text("Chose what to do with this food item"), buttons: [
.default(Text("Eat All"), action: {
self.managedObjectContext.delete(item)
try? self.managedObjectContext.save()
})
,.default(Text("Throw Away"), action: {
self.managedObjectContext.delete(item)
try? self.managedObjectContext.save()
})
,.default(Text("Cancel"))
])
})
Related
In Jetpack/Desktop Compose I want a coroutine to run in response to changes to a SnapshotStateList.
In this example:
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
#Composable
fun TestMutableList() {
val list = remember { mutableStateListOf(1, 2, 3) }
LaunchedEffect(list) {
println("List was changed.")
}
Column {
Button(onClick = { list[0] = 0 }) {
Text("Change List")
}
list.forEach { Text(it.toString()) }
}
}
the LaunchedEffect was run on the first composition. And the Composable recomposes when I click the button, so it knows that the SnapshotStateList<Int> changed. However, it was not run when clicking the button. I understand that this is because the key is the reference to the SnapshotStateList<Int> and that did not change.
How can I have the LaunchedEffect run every time that the list is modified?
You can update an integer for anytime you change list so it will trigger when that value is changed
val list = remember { mutableStateListOf(1, 2, 3) }
var changeIndex by remember {
mutableStateOf(0)
}
LaunchedEffect(list.size, changeIndex) {
// add an if here if you don't want to trigger when changeIndex is 0
println("List was changed.")
}
Column {
Button(onClick = { list[0] = 0 }) {
changeIndex ++
Text("Change List")
}
list.forEach { Text(it.toString()) }
}
I had the same problem and got it working using the list size instead of the list itself.
Like this:
val list = remember { mutableStateListOf(1, 2, 3) }
LaunchedEffect(list.size) {
println("List was changed.")
}
With convert SnapshotStateList to ImmutableList, you can achieve to aim.
#Composable
fun TestMutableList() {
val list = remember { mutableStateListOf(1, 2, 3) }
LaunchedEffect(list.toList()) {
println("List was changed.")
}
Column {
Button(onClick = { list[0] = 0 }) {
Text("Change List")
}
list.forEach { Text(it.toString()) }
}
}
The below code is for Jetbrains Desktop Compose. It shows a card with a button on it, right now if you click the card "clicked card" will be echoed to console. If you click the button it will echo "Clicked button"
However, I'm looking for a way for the card to detect the click on the button. I'd like to do this without changing the button so the button doesn't need to know about the card it's on. I wish to do this so the card knows something on it's surface is handled and for example show a differently colored border..
The desired result is that when you click on the button the log will echo both the "Card clicked" and "Button clicked" lines. I understand why mouseClickable isn't called, button declares the click handled. So I'm expecting that I'd need to use another mouse method than mouseClickable. But I can't for the life of me figure out what I should be using.
#OptIn(ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalDesktopApi::class)
#Composable
fun example() {
Card(
modifier = Modifier
.width(150.dp).height(64.dp)
.mouseClickable { println("Clicked card") }
) {
Column {
Button({ println("Clicked button")}) { Text("Click me") }
}
}
}
When button finds tap event, it marks it as consumed, which prevents other views from receiving it. This is done with consumeDownChange(), you can see detectTapAndPress method where this is done with Button here
To override the default behaviour, you had to reimplement some of gesture tracking. List of changes comparing to system detectTapAndPress:
I use awaitFirstDown(requireUnconsumed = false) instead of default requireUnconsumed = true to make sure we get even a consumed even
I use my own waitForUpOrCancellationInitial instead of waitForUpOrCancellation: here I use awaitPointerEvent(PointerEventPass.Initial) instead of awaitPointerEvent(PointerEventPass.Main), in order to get the event even if an other view will get it.
Remove up.consumeDownChange() to allow the button to process the touch.
Final code:
suspend fun PointerInputScope.detectTapAndPressUnconsumed(
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
onTap: ((Offset) -> Unit)? = null
) {
val pressScope = PressGestureScopeImpl(this)
forEachGesture {
coroutineScope {
pressScope.reset()
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false).also { it.consumeDownChange() }
if (onPress !== NoPressGesture) {
launch { pressScope.onPress(down.position) }
}
val up = waitForUpOrCancellationInitial()
if (up == null) {
pressScope.cancel() // tap-up was canceled
} else {
pressScope.release()
onTap?.invoke(up.position)
}
}
}
}
}
suspend fun AwaitPointerEventScope.waitForUpOrCancellationInitial(): PointerInputChange? {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
if (event.changes.fastAll { it.changedToUp() }) {
// All pointers are up
return event.changes[0]
}
if (event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }) {
return null // Canceled
}
// Check for cancel by position consumption. We can look on the Final pass of the
// existing pointer event because it comes after the Main pass we checked above.
val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) {
return null
}
}
}
P.S. you need to add implementation("androidx.compose.ui:ui-util:$compose_version") for Android Compose or implementation(compose("org.jetbrains.compose.ui:ui-util")) for Desktop Compose into your build.gradle.kts to use fastAll/fastAny.
Usage:
Card(
modifier = Modifier
.width(150.dp).height(64.dp)
.clickable { }
.pointerInput(Unit) {
detectTapAndPressUnconsumed(onTap = {
println("tap")
})
}
) {
Column {
Button({ println("Clicked button") }) { Text("Click me") }
}
}
I am working on a SwiftUI app (Xcode Version 12.4 and iOS 14.4.2) and am having problems to properly handle buttons in a list. I hope someone can point out the way to go.
Here is the relevant code:
struct CustomListView: View {
var localList:[SomeManagedObject], moc:NSManagedObjectContext
#State var showingOtherView = false
#State var selectNbr:Int!
func handleCustomItem(_ argument: SomeManagedObject) {
print(#function+" (1):\(showingOtherView):")
selectNbr = localList.firstIndex(of: argument)
self.showingOtherView.toggle()
print(#function+" (2):\(showingOtherView):\(selectNbr):")
..... Do useful things .....
}
var body: some View {
List {
ForEach(self.localList) {
item in
HStack {
Spacer()
Button(action: {
self.handleCustomItem(item)
})
{
Text(item.expression!)
.foregroundColor(Color.red))
.font(.headline)
.padding(.horizontal, 11).padding(.vertical, 15)
}
Spacer()
}
}
}.sheet(isPresented: $showingOtherView) {
OtherView(parameter: localList[selectNbr])
}
}
}
When I run the app this is what I get as ouput, which is what I expect:
handleCustomItem(_:) (1):false:
handleCustomItem(_:) (2):true:Optional(4):
Then I have a break point before running this line of code (to avoid a crash):
OtherView(parameter: localList[selectNbr])
This is where things get weird (unexpected for me), in the debugger I can see:
(lldb) p showingOtherView
(Bool) $R0 = false
(lldb) p selectNbr
(Int?) $R2 = nil
(lldb)
But I would expect showingOtherView to be true, and (more important) selectNbr to hold the value 4. What is going on here that I am missing ?
the SwiftUI App I'm writing has large blocks of text, so I'm trying to create a "markup language" I can use in my JSON files that hold the text, to define when a word should be bolded. The issue is, I can't seem to stop new text blocks going to a different line.
Here is the relevant code as I currently have it.
struct formattedText: View {
var text: String
var body: some View {
let split = text.components(separatedBy: "**")
Group {
ForEach(split, id: \.self) { line in
if line.hasPrefix("$$") {
Text(line.trimmingCharacters(in: CharacterSet(charactersIn: "$")))
.bold()
} else {
Text(line)
}
}
}
.lineLimit(nil)
.multilineTextAlignment(.leading)
}
Using this code, I can put a **$$ before a word and ** after, to define it as bold.
Only issue is every time I bold a word it goes to a new line. I know the traditional way to fix this is:
Text("Simple ") + Text("Swift ") + Text("Guide")
This does not work with my ForEach loop though. Any suggestions?
ForEach creates separate Views. You really just want one Text, so you mean a for...in loop:
var body: some View {
let split = text.components(separatedBy: "**")
var result = Text("")
for line in split {
if line.hasPrefix("$$") {
result = result + Text(line.trimmingCharacters(in: CharacterSet(charactersIn: "$")))
.bold()
} else {
result = result + Text(line)
}
}
return result
.lineLimit(nil)
.multilineTextAlignment(.leading)
}
Since you may want a lot more things than just bold, you might find it useful to extract that part into its own function or collection of functions:
private func applyAttributes(line: String) -> Text {
if line.hasPrefix("$$") {
return Text(line.trimmingCharacters(in: CharacterSet(charactersIn: "$")))
.bold()
} else {
return Text(line)
}
}
With that, constructing this is simpler:
var body: some View {
text.components(separatedBy: "**")
.map(applyAttributes)
.reduce(Text(""), +)
.lineLimit(nil)
.multilineTextAlignment(.leading)
}
I'm trying to run through a list of items that all have a field called "yes" which is an integer. Essentially this loop runs through and if the yes field for an item is more than 0, it will show up, and if it is 0 it won't. Works as I want it to so far but what I would like to do is show a different message if ALL items have a 0 value, so that the section of the screen is not simply empty. How would I go about doing this? I tried putting the message into the "else" but (obviously) it just repeated the message the amount of times there are items in the db.
ForEach(items.indices, id: \.self) { i in
if (items[i].yes != 0) {
HStack {
Text(items[i].name)
Spacer()
Text("\(items[i].yes)")
}
Divider()
} else {}
}
You need something like below (typed in place, so might be typos):
if items.filter({ $0.yes != 0}).isEmpty {
Text("Message for ALL are 0")
} else {
ForEach(items.indices, id: \.self) { i in
if (items[i].yes != 0) {
HStack {
Text(items[i].name)
Spacer()
Text("\(items[i].yes)")
}
Divider()
} else {}
}
}