Move/Reorder specific item in Array - SwiftUI - arrays

I have a bunch or arrays in a data struct that I am combining into one array and deleting any duplicates. Is it possible to move an item to the top of the array so it is first in the list? I want All to appear at the top of the list but it is currently sitting in the second position because of Agriculture.
Here are some of the arrays in the data struct:
let productData: [ProductModel] = [
ProductModel(application: ["All", "Clean Chemistry", "Food", "Agriculture", "Polymers"]),
ProductModel(application: ["All", "Food", "Agriculture", "Gin", "Polymers"]),
ProductModel(application: ["All", "Metals", "Polymers"]),
]
Here is where I am organising the array and presenting it in a HStack:
struct ProductList: View {
var applicationsArray = Array(Set(productData.flatMap(\.application))).sorted()
var body: some View {
ScrollView(.horizontal){
HStack{
ForEach(applicationsArray, id: \.self) { item in
Button(action: {
// SELECT ITEM
}) {
VStack(alignment: .center){
Image(item)
Text(item)
}
}
}
}
}
}
}

Use the closure provided to sorted(by:) to sort "All" to the front of the list:
var applicationsArray = Set(productData.flatMap(\.application))
.sorted { (a: String, b: String) -> Bool in
a == "All" ? true : b == "All" ? false : a < b
}
Explanation
The closure that you provide to sorted(by:) is called repeatedly by sorted and it takes two elements at a time and decides if the first element should come before the second element. Since you want "All" to appear at the start of the list, then if the closure receives "All" as the first element, it should return true because "All" comes before every other element. If the closure receives "All" as the second element, it returns false because no elements come before "All". Finally, if "All" isn't one of the elements sent to the closure, it just compares to see if the first comes before the second lexicographically using a < b.

First, remove duplicates from your list, then filter out the item(s) you want to show at the top of your list and filter out the rest of your items except the top ones and add them together. Easy right?
var yourList = ["All", "Clean Chemistry", "Food", "Agriculture", "Polymers", "All", "Food", "Agriculture”, "Gin”, "Polymers”, "All”, "Metals”, "Polymers”]
var removeDuplicates = Array(Set(yourList)).sorted()
print(removeDuplicates)
var getAllFromTheList = removeDuplicates.filter(){$0 == "All"}
print(getAllFromTheList) // [“All”]
var removeAllFromTheList = removeDuplicates.filter(){$0 != "All"}
print(removeAllFromTheList) // [“Agriculture”, “Clean Chemistry”, “Food”, “Gin”, “Metals”, “Polymers”]
let newList = getAllFromTheList + removeAllFromTheList
print(newList) // [“All”, “Agriculture”, “Clean Chemistry”, “Food”, “Gin”, “Metals”, “Polymers”]

Related

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.

Sort array of structs by a type and then alphabetically by another property

Imagine I have the below setup.
I am trying to sort [first, second, third] alphabetically and also by category.
I'd like featured items first and then everything else sorted by title.
I tried let output = [first, second, third].sort { $0.category == $1.category ? $0.category && $1.category == .featured : $0.title < $1.title }
But it was a complete disaster.
The end result should be that the sorted titles read foo bar baz
enum Category {
case featured, standard
}
struct Article {
var title: String
var category: Category
}
let first = Article(title: "bar", category: .standard)
let second = Article(title: "foo", category: .featured)
let third = Article(title: "boo", category: .standard)
let output = [first, second, third]
More Scalable Solution
When you have nested sorting, you need to group items first and then sort by those groups. For example we can define priority of the categories with an Int value just by constraint the enum to Int:
enum Category: Int {
case featured // Higher level(value) -> Higher priority in sort
case standard // Lower level(value) -> Lower priority in sort
}
Now you should:
Group the data before sort:
let grouped = Dictionary(grouping: input, by: { $0.category })
Sort groups: (So Category cases should have sortable value like an Int)
let sortedGroups = grouped.sorted { $0.key.rawValue < $1.key.rawValue }
Sort each group's data and map it back to the original array:
let result = sortedGroups.map { $0.value.sorted { $0.title < $1.title) } }
This approach is highly scalable and automated. You just need to define new categories in place of their priority. The rest of the code always works as expected.
If you want the Articles to be filtered inside the .featured category as well (in addition to listing featured before the rest):
let res = output.sorted {
if $0.category == $1.category {
return $0.title < $1.title
} else {
return $0.category == .featured
}
}

Infinite loop on JS for

My code stays in the second for forever, testing the same category every step and decrementing every time.
I have two arrays, one of them is called categoriesToUpdate and is a list of category ids (string values) for categories that I have to update, and the other is called categories, containing all the actual category data I'm working with.
I have to test if the id value for a category that I have to update is the same as the database and if it is, decrement the attribute position of its object and update the database. But it is infinitely decrementing the bank.
let newCategory;
let name;
let position;
for(let id of categoriesToUpdate) {
for(let cat of categories) {
if(id === cat.id) {
position = cat.category.category.lastPosition - 1;
name = cat.category.category.categoryName;
newCategory = {
category: {
categoryName: name,
lastPosition: position,
}
}
cRef.child(id).update(newCategory);
}
}
}
Examples of the two arrays:
categoriesToUpdate = ['-lGz4j77...', '-uEbKO3...', ...]
and
categories = [
{
category: {
category: {
categoryName: "name",
lastPosition: "number",
}
},
id: "category id";
},
{
...
}
]
it is difficult to explain how I get the arrays, but basically, categoriesToUpdate is an array of ids that I add to my logic, I have to do update in each of these categoriesand categories is an array that has all categories of the database, comes from Firebase.
let id of categoriesToUpdate. I'm assuming categoriesToUpdate is an array of Category objects. So id is actually a Category object. To compare it should be id.id === cat.id.
Also you can try filter instead of a second loop.
Something like
var matched = categories.filter(c => c.id === id.id)[0];
Then compare matched. Nested loops are hard to read, imo.

How to filter an array to correspond other array

I've two arrays:
var filteredTitles = [String]()
var filteredTypes = [String]()
I filter the first array as a part of using searchbar. The order of the elements might change completely. However, I can't filter the second array the same way I did the first one, because I don't want to take it in to count when searching. But I would like for the second array to be in the same order as the first one. So, to recap. How can I filter an array to match another one by indexes perfectly?
An example:
var filteredArray = ["One", "Two", "Three"]
//Sort the below array to ["1", "2", "3"], the order of the upper array
var toBeFilteredArray = ["2", "1", "3"]
WITHOUT using alphabetical or numerical order, as that won't do in this case.
EDIT:
TO Russell:
How do I sort the titles like this:
// When there is no text, filteredData is the same as the original data
// When user has entered text into the search box
// Use the filter method to iterate over all items in the data array
// For each item, return true if the item should be included and false if the
// item should NOT be included
searchActive = true
filteredData = searchText.isEmpty ? original : original.filter({(dataString: String) -> Bool in
// If dataItem matches the searchText, return true to include it
return dataString.range(of: searchText, options: .caseInsensitive) != nil
})
don't have two arrays - have a single array of a custom type, containing both variables that you need
Define your struct
struct MyCustomData
{
var dataTitle : String = ""
var dataType : String = ""
}
and then declare it
var dataArray : [MyCustomData] = []
populate it and sort it where required - I have populated in reverse order just so that we can see it being sorted
dataArray.append(MyCustomData(dataTitle: "Third", dataType: "3"))
dataArray.append(MyCustomData(dataTitle: "Second", dataType: "2"))
dataArray.append(MyCustomData(dataTitle: "First", dataType: "1"))
let filteredArray = dataArray.sorted {$0.dataTitle < $1.dataTitle}
for filteredElement in filteredArray
{
print("\(filteredElement.dataTitle), \(filteredElement.dataType)")
}
// or, to print a specific entry
print("\(filteredArray[0].dataTitle), \(filteredArray[0].dataType)")
An example of keeping two separate arrays in sync using zip:
let titles = ["title1", "title3", "title4", "title2"]
let types = ["typeA", "typeB", "typeC", "typeD"]
let zipped = zip(titles, types)
// prints [("title4", "typeC"), ("title2", "typeD")]
print(zipped.filter { Int(String($0.0.characters.last!))! % 2 == 0 })
You can use map on the filtered result to get back two separate filtered arrays for the titles and types.

how can i join in array same id in swift?

i want to make like a group array
struct songST {
var singerid:[Int]
var songname:[String]
var songembeded:[String]
}
**fist item;**
songST(singerid: "1", songname: ["Its my life"], songembeded: ["url1"])
**seconditem=**
songST(singerid: "1", songname: ["Always"], songembeded: ["url2"])
**i want to make join like this**
songST(singerid: "1", songname: ["Its my life","Always"], songembeded: ["url1","url2"])
if there was same singerid than will join
how can i do that? Please Help me.
I think you can improve how data is modeled.
A song consists of the following fields:
singer id
song name
song url
So it would make sense to create a model with exactly those fields, as opposed to creating an array for each of them:
struct Song {
let singerId: Int
let songName: String
let songEmbedded: String
}
Next, you need a data container for all your songs, which must be grouped by singer. The easiest way is to use a dictionary, whose key is the singer id and the value is an array of Song.
Rather than manually creating and handling the dictionary, it's better if a proper data container is created:
struct Singers {
var singers: [Int : [Song]] = [:]
mutating func addSong(song: Song) {
if self.singers[song.singerId] == nil {
self.singers[song.singerId] = []
}
self.singers[song.singerId]?.append(song)
}
func getSongs(singerId: Int) -> [Song] {
if self.singers[singerId] == nil {
return []
}
return self.singers[singerId]!
}
}
The Singers struct contains a dictionary property, and two method:
addSong adds a song, assigning it to the proper array identified by the key (singer id)
getSongs returns an array of songs for a singer id
Some sample code taken from a storyboard:
let s1 = Song(singerId: 1, songName: "Its my life", songEmbedded: "url1")
let s2 = Song(singerId: 1, songName: "Always", songEmbedded: "url2")
let s3 = Song(singerId: 2, songName: "Another", songEmbedded: "url3")
var singers = Singers()
singers.addSong(s1)
singers.addSong(s2)
singers.addSong(s3)
singers.getSongs(1) // Returns an array of 2 items
singers.getSongs(2) // Returns an array of 1 item
singers.getSongs(3) // Returns an empty array (no song for singer 3)
Easy.
You asked - "if there was same singerid than will join".
Make a function that uses that if statement:
func combineFirstSongST(firstSongST: songST, withSecondSongST secondSongST: songST) -> songST?
{
if firstSongST.singerid != secondSongST.singerid
{
return nil
}
else
{
return songST(singerid: firstSongST.singerid, songname: firstSongST.songName + secondSongST.songName, songembeded: firstSongST.songembeded + secondSongST.songembeded) //single statement
}
}
You should use camel case.

Resources