Swift - How do I add a new object to an array inside another object that has a grouping field - arrays

I have an array of events of type.
struct Event { var id: UUID, var title: String, var date: Date }
this array is inside another object with an occurrence title
struct Category { var occurs: String, var events: [Event] }
occurs defines if the event.date is before or after Now(), so that i can make Section Headers in a table to show Past and Upcoming sections.
I am only persisting Event(need id for Notification changes).
I am able to save and load data into an array of Category but how do I add a new event and get it into the correct "occurs" array.
Data Example (Event struc has init that assigns the UUID to id. )
[
Category(occurs: "passed", events: [
Event(title: "Yesterday", date: Calendar.current.date(byAdding: .day, value: -1, to: Date())!)
]),
Category(occurs: "upcoming", events: [
Event(title: "Tomorrow", date: Calendar.current.date(byAdding: .day, value: 1, to: Date())!),
Event(title: "Future", date: Calendar.current.date(byAdding: .day, value: 5, to: Date())!)
])
]
Category array is then consumed by a List/ForEach to spit out the Section and Body.
Thanks

First you need a way to figure out if the event has passed or is upcoming. You could add an extension to your Event struct to compute this:
extension Event {
var occurs: String {
let diff = date.timeIntervalSince1970 - Date().timeIntervalSince1970
return diff >= 0 ? "upcoming" : "passed"
}
}
Next you need to find the index of the category that matches the event's occurs value in your data array. You can do this by using the firstIndex method:
let newEvent = Event(
title: "2 hours from now",
date: Calendar.current.date(byAdding: .hour, value: 2, to: Date())!)
let newEventOccurs = newEvent.occurs
if let index = data.firstIndex(where: { $0.occurs == newEventOccurs }) {
data[index].events.append(newEvent)
}
Notice we use an if-let binding because it's possible you don't have a category in your array for the event. In this case, you will want to add the category to your array with the new event.

Related

React: render objects based on value type

I have an object array with a structure similar to this:
export interface obj {
id: number,
date: string,
source: string,
}
const obj: obj[] | undefined = [
{ id: 1, date: "2021-01-17", source: "data" },
{ id: 2, date: "2021-11-23", source: "data" },
{ id: 3, date: "2020-05-03", source: "draft" },
{ id: 4, date: "2022-09-08", source: "draft" },
{ id: 5, date: "2021-12-04", source: "data" },
{ id: 6, date: "2021-09-08", source: "empty" },
];
const [objectData, setObjectData] = useState<obj[]>();
I'm trying to return and render the first occuranses of each source type, sorted by the nearest date. So in the example above I'd like to return the object with id: 5 for "data", id: 4 for "draft" and id: 6 for "empty".
This is what I got so far:
Sorting object by dates descending in useEffect and store variable in useState
const sortByDate = obj?.slice().sort((a, b) => {
return b.date.valueOf() - a.date.valueOf();
})
And then trying to map out my component like so:
{objectData?.map((m) =>
m.source === "draft" ? (
<Data id={m.id} date={m.date} source={m.source} />
) : m.applicationType === "data" ? (
<Data id={m.id} date={m.date} source={m.source} />
) : m.applicationType === "empty" ? (
<Data id={m.id} date={m.date} source={m.source} />
)
)}
But this will ofcourse render out every object..How can I render out only the one instance of each object source type that i want?
Given your approach, I would
a) Sort the items in the list in a descending manner.
b) Create a list of possible unique source names
c) Render the unqiue source names and get the first item matching the source name in my descending ordered array.
You can get rid of b) if you want to define your list of source names statically. If that is the case, you need to handle if there is no item found with the source name that is selectecd.
const items = [
{ id: 1, date: "2021-01-17", source: "data" },
{ id: 2, date: "2021-11-23", source: "data" },
{ id: 3, date: "2020-05-03", source: "draft" },
{ id: 4, date: "2022-09-08", source: "draft" },
{ id: 5, date: "2021-12-04", source: "data" },
{ id: 6, date: "2021-09-08", source: "empty" },
];
// a) Sort the array with date object (descending)
const sortedArray = items.sort((a, b) => {
return new Date(b.date).valueOf() - new Date(a.date).valueOf();
});
// b) This will let you get all possible sources dynamically and create an array of unique entries
const uniqueSources = [...new Set(sortedArray.map(item => item.source))];
// c) Map over (existing) unqiue entries in the sorted list and get the first one found
uniqueSources.map((source) => {
const firstObject = sortedArray.find((entry) => entry.source === source);
console.log('source:', source, firstObject);
});
I think you probably do (or should) know the possible values of source, and in fact those should be the type for source, ex:
export interface obj {
id: number,
date: string,
source: 'data' | 'draft' | 'empty',
}
Then in your useEffect you have the internal variable sortedByMostRecent which is your array of data sorted by the date value.
With that you can use a constant array (defined at the top of your file, outside the component definition around the interface definition) of the source options:
const availableSources: interface['source'][] = ['data', 'draft', 'empty'];
To find the first object in the sorted array matching each available (possible) source. Something like:
const topSourceItems = [];
for (source of availableSources) {
const topSource = sortedByMostRecent.find(item => item.source == source);
if (topSource) {
topSourceItems.push(topSource);
}
}
topSourceItems would then be stored in state and rendered with a regular iteration in the markup.
Notes:
Your .slice() isn't doing anything. Also calling an array "obj" is probably not the best decision, but I understand this is likely just dummy code for the example.

How to map/filter data correctly with luxon (DateTimeNow) and ReactJs

I would like to explain my problem of the day.
I have a array which looks like this
[{status: "closed",createdAt: "2022-01-13T15:28:25.239Z"},{status: "closed",createdAt: "2022-01-10T15:28:25.239Z"},{status: "open",createdAt: "2021-11-25T15:28:25.239Z"}]
I filter to retrieve only the data with the status "closed"
const countData = data?.filter(
(item) => item.status === "closed"
);
const count = countData?.length;
this works well, it correctly returns the number to me.
I would like to add a new filter with Luxon.
I would like to take the date of today with luxon, and, as a result, display the objects that correspond to this date
const dateNow = DateTime.now().toISO();
console.log(dateNow)
2022-01-13T16:23:44.873+01:00
How can I fix this issue?
If you want to check if a given Luxon DateTime object represent the same day of today, you can use hasSame passing date as second parameter
Return whether this DateTime is in the same unit of time as another DateTime. Higher-order units must also be identical for this function to return true. Note that time zones are ignored in this comparison, which compares the local calendar time. Use DateTime#setZone to convert one of the dates if needed.
In your case, you can have something like the following code:
const DateTime = luxon.DateTime;
const input = [{
status: "closed",
createdAt: "2022-01-13T15:28:25.239Z"
}, {
status: "closed",
createdAt: "2022-01-10T15:28:25.239Z"
}, {
status: "open",
createdAt: "2021-11-25T15:28:25.239Z"
}, {
status: "closed",
createdAt: new Date().toISOString()
}];
const todayItems = input.filter(item => {
return DateTime.fromISO(item.createdAt).hasSame(DateTime.now(), 'day') && item.status == "closed";
});
console.log(todayItems)
<script src="https://cdn.jsdelivr.net/npm/luxon#2.3.0/build/global/luxon.js"></script>
Luxon has a function that let's you get a date at certain point of a parameter that's called startOf. So you can do something like:
const todayDate = DateTime.now().startOf("day");
So your todayDate variable will be your current date, but at 00:00:00 time.
And you can make the transformation of the elements date during your filter function to compare them to todayDate like this:
//Please consider that the example for today was at 2022-01-13
const array = [
{
name: "this is for today",
date: "2022-01-13T15:28:25.239Z"
},
{
name: "this was for yesterday",
date: "2022-01-12T15:28:25.239Z"
}];
const todayDate = DateTime.now().startOf("day");
const todayElements = array.filter((element) => {
return DateTime.fromISO(element.date).startOf("day").equals(todayDate);
});

Filtering a #Binding array var in a ForEach in SwiftUI returns values based on unfiltered array

I'm a Windows C# developer, new to iOS/SwiftUI development and I think I've worked myself into a hole here.
I have a view with a #Binding variable:
struct DetailView: View {
#Binding var project: Project
The project is an object which contains an array of Tasks. I am looping through the tasks of the project to display its name and a toggle whose state is determined by the Task's variable, isComplete.
ForEach(filteredTasks.indices, id: \.self) { idx in
HStack {
Text(filteredTasks[idx].phase)
.font(.caption)
Spacer()
Text(filteredTasks[idx].name)
Spacer()
Toggle("", isOn: self.$filteredTasks[idx].isComplete)
}
}
}
This took quite a while for me to get to this piece of code, and I found that I had to follow an example with the 'indices' option to get the toggle to work on each Task individually, and to make sure that its isComplete value was saved.
Next, I wanted to filter the list of Tasks based on a Task variable, phase, which has values of Planning, Construction, or Final. So I created 4 buttons (one for each phase, and then an 'All Tasks' to get back to the full, unfiltered list), and after a lot of trial and error (creating filtered arrays that no longer were bound correctly, etc., etc.) I tried this, basically working only with the original array.
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
HStack {
Text(project.tasks[idx].phase)
.font(.caption)
Spacer()
Text(project.tasks[idx].name)
Spacer()
Toggle("", isOn: self.$project.tasks[idx].isComplete)
}
}
}
And of course, this seemed to work because I can do a test:
func CreateTestArray() {
let testFilterArray = project.tasks.filter({ $0.phase.contains(filterValue) })
}
And that will give me the filtered list I want. However, in my ForEach view, it's not working correctly and I'm not sure how to work around it.
For example, I have 128 tasks, 10 of which have a value of 'Final' and when I use a button setting the filterValue to Final, the testFilterArray actually contains the correct 10 tasks - but in the ForEach view I'm getting the first ten tasks in the original array (which are of the type 'Planning' - the original array is sorted by Planning/Construction/Final); obviously the ForEach, in spite of the filter statement, is working on the original array. The Planning button sends the filterValue = "Planning", and I get the correct results because the filter returns 0-19 indices for the 20 Planning tasks I have in the original array, and since they're first in the original array, it 'appears' that the Planning filter is working correctly, tho in actually it's just by chance that it works, if the array were sorted differently it would not.
Any ideas how I can approach this so that I can actually filter on this array, display the isComplete toggle correctly for each item in the array, as well as update the toggle state dynamically? I feel like I need to start from scratch once again here because I've let these constraints work me into a tiny Swift corner.
Thanks!
Update:
Thank you, #jnpdx, for your quick response - and I should definitely have included objects (which I list below). However, looking back over my object definitions, I wonder if I've made an even more basic error in managing the objects which is why I've gotten boxed in to the situation I have (i.e., in earlier iterations I'd attempted some of your suggestion). At any rate, my published object is 'projects', which is a list of projects I pass to the project list view, and then that view passes a single project to a project view, and then that view lists the tasks in that particular project.
I feel like your answer is pointing me in the right decision, I just need to back back up and look at those object definitions/management and see how to get to a situation where a straightforward solution is possible.
Task:
struct Task: Identifiable, Codable {
let id: UUID
var phase: String
var category: String
var name: String
var isComplete: Bool
init(id: UUID = UUID(), phase: String, category: String, name: String, isComplete: Bool) {
self.id = id
self.phase = phase
self.category = category
self.name = name
self.isComplete = isComplete
}
}
The Project:
struct Project: Identifiable, Codable {
var id: UUID
var name: String
var type: String
var tasks: [Task]
var isComplete: Bool
init(id: UUID = UUID(), name: String, type: String, tasks: [Task] = [], isComplete: Bool) {
self.id = id
self.name = name
self.type = type
self.tasks = tasks
self.isComplete = isComplete
}
}
and the Project model:
class ProjectData: ObservableObject {
// code to access the json file is here
// An accessible list of projects from the saved file
#Published var projects: [Project] = []
// load and save functions follow
Update:
Thanks, #jnpdx, your solution worked after making, as you said I would need to, the tweaks to get it to function within my particular model design. Here are the snippets that finally worked in my case.
In my view:
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) })) { task in
HStack {
Text(task.name)
Toggle("", isOn: self.makeBinding(item: task))
}
}
}
And the called function:
func makeBinding(item: Task) -> Binding<Bool> {
let i = self.project.tasks.firstIndex { $0.id == item.id }!
return .init(
get: { self.project.tasks[i].isComplete },
set: { self.project.tasks[i].isComplete = $0 }
)
}
Let's look at the following line from your code:
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
In the first part, you filter tasks and then ask for the indices. My suspicion is that you're hoping it would return something like [1, 5, 10, 11, 12], meaning their original positions in the array. But, in reality, you're going to get a contiguous array like [0,1,2,3,4] because it's giving you indices from the newly-created array (the result of filter).
There are a couple of ways to solve this, which also relate to the previous ForEach that you had.
It's more idiomatic to do ForEach and iterate over structs/objects rather than indices. You don't show what Task is made up of, but let's say it's this:
struct Task : Hashable {
var id = UUID()
var name: String
var phrase: String
var isComplete: Bool
}
To iterate on it, you could do:
ForEach(task, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id)) //explained later
}
I asked about the type of Project in my comment because I'm not totally clear why it's a #Binding. It seems like maybe it's an object? If it's a view model, which would be nice, you can handle your Toggle logic there. Something like:
class Project : ObservableObject {
#Published var tasks : [Task] = [Task(name: "1", phrase: "phase", isComplete: false),Task(name: "2", phrase: "phase", isComplete: true),Task(name: "3", phrase: "phase2", isComplete: false)]
var completedTasks : [Task] {
return tasks.filter { $0.isComplete }
}
func taskCompletedBinding(id: UUID) -> Binding<Bool> {
Binding<Bool>(get: {
self.tasks.first(where: { $0.id == id})?.isComplete ?? false
}, set: { newValue in
self.tasks = self.tasks.map { t in
if t.id == id {
var tCopy = t
tCopy.isComplete = newValue
return tCopy
} else {
return t
}
}
})
}
}
And you can test that it works doing this:
struct ContentView: View {
#ObservedObject var project = Project()
var body: some View {
ForEach(project.tasks, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id))
}
}
}
If Project is a struct and not an object, it might be good to wrap it in an ObservableObject view model like I did above.

Remove objects with duplicate date property from Array

I have this array of objects. Each object has, among other, a "startPoint" property, which is a Date.
Now, I want to make sure that there are no objects with the same "startPoint", regardless of the values of other properties.
So, I found a potential approach, that works for Strings, but not for Dates:
var uniqueValues = Set<String>()
result = result.filter{ uniqueValues.insert("\($0.startPoint)").inserted}
Could anyone of you point me in the right direction?
Use date components to unique on, so you ignore the time part
struct Record {
let date: Date
let name: String
}
let now = Date()
let soon = Date(timeIntervalSinceNow: 100)
let future = Calendar.current.date(byAdding: .day, value: 1, to: now)!
let records = [Record(date: now, name: "Now"),
Record(date: soon, name: "Soon"), // same day so should be removed
Record(date: future, name: "Future")]
var unique = Set<DateComponents>()
let result = records.filter {
let comps = Calendar.current.dateComponents([.day, .month, .year], from: $0.date)
if unique.contains(comps) {
return false
}
unique.insert(comps)
return true
}
print(result)
// [Record(date: 2018-10-11 13:47:17 +0000, name: "Now"),
// Record(date: 2018-10-12 13:47:17 +0000, name: "Future")]
My approach is similar to the other answer, but is slightly more performant since it uses a Dictionary instead of an Array. There is a cost, though: You lose ordering like you would with the Set approach.
struct MyObject {
let startPoint: Date
}
let values: [MyObject] = [...]
let unique = Array(
values.reduce(into: [Date: MyObject]()) { result, value in
result[value.startPoint] = result[value.startPoint] ?? value
}.values
)

adding new object inside of array of objects in react

I am having a little problem when trying to add a new object to an array of objects which is stored in state.
Below is a example of the object which is stored in state (scratchTickers):
id : uuid.v1(),
area : 'Scratch',
name : 'Untitled',
status : "",
size : "",
created : {
name : "username",
time : new Date()
},
modified : {
name : "username",
time : new Date()
},
content: [{
content: "Dummy content",
TowerRed: "",
TowerText: "Headlines"
}]
I need to dynamically add new objects in the content array, an object is structured like this:
{
content: "Dummy content",
TowerRed: "",
TowerText: "Headlines"
}
I am trying to use React's immutability helpers to accomplish this but It doesn't seem to be working as it should.
here is my function to add a new object to the content array and update the state:
_createTickerItem: function (id) {
var tickerID = id;
var key = null;
for (var i = 0; i < this.state.scratchTickers.length; i++) {
if(this.state.scratchTickers[i].id == tickerID) {
var ticker = this.state.scratchTickers[i];
var key = i;
}
}
var blankContent = ({
content: "Ticker Content",
TowerRed: "",
TowerText: "Headlines"
});
var oldContent = this.state.scratchTickers[key];
var newContent = update(oldContent, {
content : {
$push : [blankContent]
}
});
this.setState({
scratchTickers: newContent
});
},
So when i clicl the button tun run the _createTickerItem function the component re-renders and the main object has disappeared entirely.
Edit* Ok so I am now 1 step further making sure to set 'newContent' as an array during set state now adds new objects to the content array and renders them correctly.
this.setState({
scratchTickers: [newContent]
});
However if there were multiple objects in state, this will replace them all with juts a single object. So how would it work if I had say to parent objects in state but wanted to just modify the content array of object 1?
Edit 2: i should add that scratchTickers is an array of objects, so the for loop is needed to make sure i modify the correct object.
You're replacing the entire scratchTickers array with only the last updated object.
You have to update the entire array first.
var updates = {};
updates[key] = { $set: newContent }
var newScratchTickers = update(this.state.scratchTickers, updates);
this.setState({
scratchTickers: newScratchTickers
});

Resources