How to get the previous element in a SwiftUI ForEach? - foreach

I have a ForEach like this in SwiftUI:
ForEach(entries) { (e: MyType) in
NavigationLinkItem(entry: e)
}
now I need to also pass the previous Entry to my View (NavigationLinkItem) to do something like this:
ForEach(entries) { (e: MyType) in
NavigationLinkItem(entry: e, sucessor: ...)
}
How to realise that?

To keep ForEach dynamic, find below possible approach (assuming MyType is Identifiable, and successor is optional)
ForEach(Array(entries.enumerated()), id: \.1.id) { (index, e) in
NavigationLinkItem(entry: e, sucessor: (index == 0 ? nil : self.entries[index - 1]))
}

I would do this. Assuming you need to pass an object to your successor. I would suggest you make successor an optional
ForEach((0...entries.count-1), id:\.self) {i in
NavigationLinkItem(entry: self.entries[i], sucessor: i == 0 ? self.entries[0] : self.entries[i-1])
}
EDIT: As suggested in the comments, its better to use indices than count.
ForEach((0...entries.indices), id:\.self) {i in
NavigationLinkItem(entry: self.entries[i], sucessor: i == self.entries.startIndex ? self.entries.first : self.entries[i-1])
}

Related

SplayTreeSet contains duplicate values

I have a SplayTreeSet of the objects ChatRoomListModel. I'd like to order my set based on the objects DateTime createTime value.
I'm not sure where I'm going wrong because there's duplicate items being added item.
I later down the line perform a _latestMessages.add(newMessage) and it's not actually calling the overloads and there's duplicates being added.
I tested by using _latestMessage.contains(newMessageButSameChatRoomId), it returns false.
When I perform _latestMessage.toSet() every duplicate goes away.
How can I get SplayTreeSet to use my overloading equals?
Thanks!
ObservableSet<ChatRoomListModel> _latestMessages = ObservableSet.splayTreeSetFrom(
ObservableSet(),
compare: (a, b) => b.compareTo(a),
);
The ChatRoomListModel model has the following methods and overloads:
int compareTo(ChatRoomListModel other){
return messagesModel.createTime.compareTo(other.messagesModel.createTime);
}
ChatRoomListModel copyWith({
String? firstName,
String? otherUserId,
MessagesModel? messagesModel,
}) {
return ChatRoomListModel(
firstName: firstName ?? this.firstName,
otherUserId: otherUserId ?? this.otherUserId,
messagesModel: messagesModel ?? this.messagesModel,
);
}
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is ChatRoomListModel &&
runtimeType == other.runtimeType &&
messagesModel.chatRoomId == other.messagesModel.chatRoomId;
#override
int get hashCode => messagesModel.chatRoomId.hashCode;
Your issue is that you have two completely different notions of what it means for two ChatRoomListModel objects to be "equal". You provide both compareTo and operator == implementations, but they consider different sets of properties, so compareTo can return 0 when operator == returns false, which is confusing at best. SplayTreeMap considers only compareTo, not operator ==. From the SplayTreeSet documentation:
Elements of the set are compared using the compare function passed in the constructor, both for ordering and for equality. If the set contains only an object a, then set.contains(b) will return true if and only if compare(a, b) == 0, and the value of a == b is not even checked.
I'm presuming what you call "duplicates" are elements that have equal chatRoomIds but that have different creation times, and creation times are the only things that your SplayTreeSet cares about.
If your goal is to maintain only the latest message per chatRoomId, you need to maintain a data structure that uses the chatRoomId (a String) as a key. The natural collection for that would be a Map<String, ChatRoomListModel>. (Since the ChatRoomListModel knows its own chatRoomId, it also could just be a HashSet with explicit equals and hashCode callbacks.)
If you additionally want to keep messages in chronological order, you either will need to explicitly sort them afterward or maintain a separate data structure that keeps them in chronological order. You could continue using a SplayTreeSet for that. Basically before you add any entry to the SplayTreeSet, check the Map first to see if an existing entry for that chatRoomId.
I don't fully understand your data structures, but here's an example that you presumably can adapt:
import 'dart:collection';
class Message {
Message(this.creationTime, {required this.chatRoomId, required this.text});
final DateTime creationTime;
final String chatRoomId;
final String text;
#override
String toString() => '$creationTime: [$chatRoomId] $text';
}
class Messages {
final _latestMessages = <String, Message>{};
final _orderedMessages = SplayTreeSet<Message>((message1, message2) {
var result = message1.creationTime.compareTo(message2.creationTime);
if (result != 0) {
return result;
}
result = message1.chatRoomId.compareTo(message2.chatRoomId);
if (result != 0) {
return result;
}
return message1.text.compareTo(message2.text);
});
void add(Message message) {
var existingMessage = _latestMessages[message.chatRoomId];
if (existingMessage != null &&
message.creationTime.compareTo(existingMessage.creationTime) < 0) {
// An existing message exists with a later creation date. Ignore the
// incoming message.
return;
}
_latestMessages[message.chatRoomId] = message;
_orderedMessages.remove(existingMessage);
_orderedMessages.add(message);
}
void printAll() => _orderedMessages.forEach(print);
}
void main() {
var messages = Messages();
messages.add(Message(
DateTime(2023, 1, 1),
chatRoomId: 'foo',
text: 'Hello foo!',
));
messages.add(Message(
DateTime(2023, 1, 2),
chatRoomId: 'bar',
text: 'Goodbye bar!',
));
messages.add(Message(
DateTime(2023, 1, 2),
chatRoomId: 'foo',
text: 'Goodbye foo!',
));
messages.add(Message(
DateTime(2023, 1, 1),
chatRoomId: 'bar',
text: 'Hello bar!',
));
messages.printAll();
}

How to use LazyColumn's animateItemPlacement() without autoscrolling on changes?

I am using a LazyColumn in a checklist like style. The list shows all to-be-done items first and all done items last. Tapping on an item toggles whether it is done.
Here is an MWE of what I am doing:
data class TodoItem(val id: Int, val label: String, var isDone: Boolean)
#Composable
fun TodoCard(item: TodoItem, modifier: Modifier, onClick: () -> Unit) {
val imagePainterDone = rememberVectorPainter(Icons.Outlined.Done)
val imagePainterNotDone = rememberVectorPainter(Icons.Outlined.Add)
Card(
modifier
.padding(8.dp)
.fillMaxWidth()
.clickable {
onClick()
}) {
Row {
Image(
if (item.isDone) imagePainterDone else imagePainterNotDone,
null,
modifier = Modifier.size(80.dp)
)
Text(text = item.label)
}
}
}
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun ExampleColumn() {
val todoItems = remember {
val list = mutableStateListOf<TodoItem>()
for (i in 0..20) {
list.add(TodoItem(i, "Todo $i", isDone = false))
}
list
}
val sortedTodoItems by remember {
derivedStateOf { todoItems.sortedWith(compareBy({ it.isDone }, { it.id })) }
}
LazyColumn {
items(sortedTodoItems, key = {it.label}) { item ->
TodoCard(item = item, modifier = Modifier.animateItemPlacement()) {
val index = todoItems.indexOfFirst { it.label == item.label }
if (index < 0) return#TodoCard
todoItems[index] = todoItems[index].copy(isDone = !todoItems[index].isDone)
}
}
}
}
This work well except for one side effect introduced with Modifier.animateItemPlacement(): When toggling the first currently visible list element, the LazyListState will scroll to follow the element.
Which is not what I want (I would prefer it to stay at the same index instead).
I found this workaround, suggesting to scroll back to the first element if it changes, but this only solves the problem if the first item of the column is the first one to be currently displayed. If one scrolls down such that the third element is the topmost visible and taps that element, the column will still scroll to follow it.
Is there any way to decouple automatic scrolling from item placement animations? Both seem to rely on the LazyColumn's key parameter?
I haven't tried to compile/run your code, but it looks like your'e having a similar issue like this, and it looks like because of this in LazyListState
/**
* When the user provided custom keys for the items we can try to detect when there were
* items added or removed before our current first visible item and keep this item
* as the first visible one even given that its index has been changed.
*/
internal fun updateScrollPositionIfTheFirstItemWasMoved(itemsProvider: LazyListItemsProvider) {
scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
}
I suggested a possible work around where the first item's animation will be sacrificed by setting its key as its index instead of a unique identifier of the backing data.
itemsIndexed(
items = checkItems.sortedBy { it.checked.value },
key = { index, item -> if (index == 0) index else item.id }
) { index, entry ->
...
}
Though I haven't regressed any use-case with this kind of set-up of Lazy keys yet nor could solve your issue, but you can consider trying it.

How to perform an "all" operation on an arbitrary number of Bool Publishers in Combine?

I've got a situation where I'm trying to determine if an arbitrary number of boolean checks (user defined conditionals) are true. It seems like a job for CombineLatest combined with AllSatisfy... the problem being that CombineLatest only supports a fixed number of elements (2, 3 or 4). Is there some pattern other than using repeated 2-element CombineLatest publishers which combine each result with another of the publishers?
Ideally, I'd like something along the lines of:
Publishers.CombineLatestMany(arrayOfPublishers).allSatisfy { ... }
and not:
Publishers.CombineLatest(pubA, pubB).combineLatest(pubC).combineLatest(pubD) ...
This should solve your need, unless your publishers are of different types:
extension Publisher where Output == Bool {
func and<P: Publisher>(_ rest: P...) -> some Publisher where P.Output == Output, P.Failure == Failure {
rest.reduce(AnyPublisher(self)) { acc, next in
AnyPublisher(acc.combineLatest(next).map { $0 && $1 })
}
}
}

adding objects to list if it doesn't contain it

I'm trying to add object to a list only if it wasn't added already .. like this:
for (var interest in poll.poll.interests) {
InterestDrop n = new InterestDrop(interest, false);
print(interest);
print(dropInterestsList);
print(dropInterestsList.contains(n));
if (!(dropInterestsList.contains(n))){
dropInterestsList.add(n);
}
}
but this always return false and add the object even when its already there ... how to solve this?
ANSWER:
class InterestDrop {
String name;
bool isClicked;
InterestDrop(this.name, this.isClicked);
bool operator == (o) => o is InterestDrop && name == o.name && isClicked == o.isClicked;
int get hashCode => hash2(name.hashCode, isClicked.hashCode);
}
You need to override the equality operator in your custom class.
From the docs:
The default behavior for all Objects is to return true if and only if
this and other are the same object.
So your contains method will only return true if your array contains the exact object you are comparing against.
You need to do something like this:
class InterestDrop {
operator ==(InterestDrop other) => identifier == other.identifier;
}

Web form for creating custom filters with boolean logic

I have a Rails application in which I'd like a user to be able to create custom boolean combinations from a list of categories, which is itself dynamically loaded. For example, if I have the categories foo bar and baz, I would like the user to be able to select (foo AND bar) OR baz or foo OR baz or any other logical combination of those categories. The most common use case I anticipate is something like (a OR b OR c) AND (d OR e OR f) AND (g OR h); that is, a conjunction of several disjunctions.
The results would then be stored in some data structure which could be later used as a filter. Ideally this would all be done through a graphical interface, rather than having the user construct the boolean logic in text.
Before I attempt to implement this myself, is there a library that I can pull in that already implements this or something similar? Or something open-source that could get me part of the way there, and that I could fork and modify?
You could implement a Boolean Retrieval with four ways to select a given category:
not selected
positive selected (+ prefix; category is mandatory)
negative selected (- prefix in Google search; category must not occur)
selected (no prefix)
Not in ruby-on-rails but probably easy to translate from c#:
public bool IsMatch(string s)
{
s = s.ToLower();
bool ret = ((negWords.Count == 0) || !hasMatchInSet(s, negWords, true))
&& ((posWords.Count == 0) || hasMatchInSet(s, posWords, false))
&& ((words.Count == 0) || hasMatchInSet(s, words, true));
return ret;
}
private bool hasMatchInSet(string s, HashSet<string> setOfWords, bool breakOnFirstMatch)
{
int matches = 0;
bool ret = false;
foreach (string w in setOfWords)
{
if (s.IndexOf(w, StringComparison.InvariantCultureIgnoreCase) >= 0)
{
matches++;
if (breakOnFirstMatch)
{
break;
}
}
}
ret = (breakOnFirstMatch && (matches > 0)) || (matches == setOfWords.Count);
return ret;
}

Resources