Sorting data by Newest and Oldest - swiftui

I want to sort my custom array by the last tapped(Newest). I can sort by name or by id, but I have trouble sorting by time. I couldn't find similar issues on the internet, and it gives me a lot of headaches. As you can see in my code below, I want to sort the favorite list by the last tapped. Thanks in advance for your help.
import Foundation
import SwiftUI
struct DataArray: Identifiable {
let id: Int
let cities: String
let name1: String
let name2: String
let isFavorite: Bool
var date = Date()
}
public struct ListDataArray {
static let dot = [
DataArray(id: 1,
cities: "Baltimore"
name1: "John",
name2: "Mike",
isFavorite: False),
DataArray(id: 2,
cities: "Frederick"),
name1: "Joe",
name2: "Swift",
isFavorite: False),
DataArray(id: 3,
cities: "Catonsville"
name1: "Susan",
name2: "Oliver",
isFavorite: False),
// There will be a lot of data
]
}
class Prospect: ObservableObject {
#Published var datas: [DataArray] = []
init() {
fetchDataArrays()
}
private func fetchDataArrays() {
let items = ListDataArray.dot
datas = items
}
// To View Favorite
#Published var showFavorite: Bool = false
}
struct Home: View {
#EnvironmentObject var items: Prospect
var body: some View {
ScrollView {
LazyVStack {
ForEach(items.datas) { data in
VStack {
ButtonView(data: data)
.environmentObject(items)
Text("\(data.id)")
.font(.title3)
Text(data.cities)
.font(.subheadline)
Text(data.name1)
.font(.subheadline)
Text(data.name2)
.font(.subheadline)
}
padding()
}
.padding()
}
}
}
}
struct ButtonView: View {
#EnvironmentObject var items: Prospect
let data: DataArray
var index: Int {
items.datas.firstIndex(where: { $0.id == data.id }) ?? 0
}
var body: some View {
HStack {
Button(action: {
self.items.datas[self.index].isFavorite.toggle()
} label: {
Image(systemName: self.items.datas[self.index].isFavorite ? "suit.heart.fill" : "suit.heart").padding()
})
Spacer()
Button(action: {
items.showFavorite.toggle()
} label: {
Image(systemName: "person").padding()
})
.sheet(isPresented: $items.showFavorite) {
FavoriteView()
.environmentObject(items)
}
}
}
}
struct FavoriteView: View {
#EnvironmentObject var items: Prospect
#State var selection = 0
let sortingNames = ["Newest", "Oldest", "A to Z", "Z to A"]
var body: some View {
VStack {
HStack {
ForEach(0...<4) { num in
Button(action: {
selection = num
}, label: {
Spacer()
Text(sortingNames[num])
.forgroundColor(selection == num ? Color.blue: Color.black)
Spacer()
})
}
}
List {
ForEach(sorting()) { data in
if data.isFavorite {
VStack(spacing: 10) {
Text(data.cities)
Text(data.name1)
Text(data.name2)
}
.font(.body)
}
}
.padding()
}
Spacer()
}
}
private func sorting() -> [DataArray] {
switch selection {
case 0:
// Here I want to sort by the Newest...
return items.datas.sorted(by: { $0.date < $1.date })
case 1:
// Here I want to sort by the Oldest...
return items.datas.sorted(by: { $0.date < $1.date })
case 2:
return items.datas.sorted(by: { $0.cities < $1.cities })
case 3:
return items.datas.sorted(by: { $0.cities > $1.cities })
}
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(Prospect())
}
}

Focusing only on the sorting as per the question, the following works well for me.
The only significant change, was the sort by Oldest. Here is my test code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct DataArray: Identifiable {
let id: Int
let cities: String
let name1: String
let name2: String
let isFavorite: Bool
var date = Date()
}
struct ContentView: View {
let dataArr = [DataArray(id: 1,cities: "Baltimore", name1: "John", name2: "Mike", isFavorite: true,
date: Date()),
DataArray(id: 2,cities: "Frederick" ,name1: "Joe", name2: "Swift", isFavorite: true,
date: .now+1),
DataArray(id: 3,cities: "Catonsville", name1: "Susan", name2: "Oliver", isFavorite: true,
date: .now+2)
]
#State var selection = 0
let sortingNames = ["Newest", "Oldest", "A to Z", "Z to A"]
var body: some View {
VStack {
HStack {
ForEach(0..<4) { num in
Button(action: { selection = num}) {
Text(sortingNames[num])
.foregroundColor(selection == num ? Color.blue: Color.black)
}
}
}
List {
ForEach(sorting()) { data in
if data.isFavorite {
VStack(spacing: 10) {
Text("\(data.id)").foregroundColor(.red) // <-- for testing
Text(data.cities)
Text(data.name1)
Text(data.name2)
}
}
}
}
}
}
func sorting() -> [DataArray] {
switch selection {
case 0:
// Here I want to sort by the Newest...
return dataArr.sorted(by: { $0.date < $1.date })
case 1:
// Here I want to sort by the Oldest...
return dataArr.sorted(by: { $0.date > $1.date }) // <--- here
case 2:
return dataArr.sorted(by: { $0.cities < $1.cities })
case 3:
return dataArr.sorted(by: { $0.cities > $1.cities })
default:
return dataArr // <--- here need a default
}
}
}
EDIT1:
Since I don't know what you really want to do with your app,
I just modified your (test) code to show you an approach for getting your data sorted.
The main emphasis here is to have
one source of truth of the data, starting in ContenView #StateObject var items = Prospect().
Also made sorting() sorting the data in-place, so that it appear sorted everywhere.
Moved sorting() into your Prospect model.
I also renamed the badly named DataArray to CityData.
struct ContentView: View {
#StateObject var items = Prospect()
var body: some View {
Home().environmentObject(items)
}
}
struct Home: View {
#EnvironmentObject var items: Prospect
var body: some View {
ScrollView {
LazyVStack {
ForEach(items.datas) { data in
VStack {
ButtonView(data: data)
Text("\(data.id)").font(.title3)
Text(data.cities).font(.subheadline)
Text(data.name1).font(.subheadline)
Text(data.name2).font(.subheadline)
}.padding()
}.padding()
}
}
}
}
struct ButtonView: View {
#EnvironmentObject var items: Prospect
#State var data: CityData // <--- here
#State var index: Int = 0 // <--- here
var body: some View {
HStack {
Button(action: { items.datas[index].isFavorite.toggle() }) {
Image(systemName: items.datas[index].isFavorite ? "suit.heart.fill" : "suit.heart").padding()
}
Spacer()
Button(action: { items.showFavorite.toggle() }) {
Image(systemName: "person").padding()
}
}
.onAppear {
if let ndx = items.datas.firstIndex(where: { $0.id == data.id }) {
index = ndx
}
}
.sheet(isPresented: $items.showFavorite) {
FavoriteView().environmentObject(items)
}
}
}
struct FavoriteView: View {
#Environment(\.dismiss) var dismiss
#EnvironmentObject var items: Prospect
#State var selection = 0
let sortingNames = ["Newest", "Oldest", "A to Z", "Z to A"]
var body: some View {
VStack {
Button(action: {dismiss()}) {
Text("Done").foregroundColor(.blue) // <-- for testing on macos
}
HStack {
ForEach(0..<4) { num in
Button(action: {
selection = num
items.sorting(selection) // <--- here
}) {
Text(sortingNames[num])
.foregroundColor(selection == num ? Color.blue: Color.black)
}
}
}
List {
ForEach(items.datas) { data in // <--- here
if data.isFavorite {
VStack(spacing: 10) {
Text("\(data.id)").foregroundColor(.red) // <-- for testing
Text(data.cities)
Text(data.name1)
Text(data.name2)
}
}
}
}
}
}
}
class Prospect: ObservableObject {
#Published var datas: [CityData] = []
#Published var showFavorite: Bool = false
init() {
fetchCityDatas()
}
private func fetchCityDatas() {
datas = ListCityData.dot
}
// --- sort in place note sort() not sorted() ---
func sorting(_ selection: Int) {
switch selection {
case 0:
// Here I want to sort by the Newest...
datas.sort(by: { $0.date < $1.date })
case 1:
// Here I want to sort by the Oldest...
datas.sort(by: { $0.date > $1.date }) // <--- here
case 2:
datas.sort(by: { $0.cities < $1.cities })
case 3:
datas.sort(by: { $0.cities > $1.cities })
default:
break
}
}
}
public struct ListCityData {
static let dot = [CityData(id: 1,cities: "Baltimore", name1: "John", name2: "Mike", isFavorite: true,
date: .now),
CityData(id: 2,cities: "Frederick" ,name1: "Joe", name2: "Swift", isFavorite: true,
date: .now+10),
CityData(id: 3,cities: "Catonsville", name1: "Susan", name2: "Oliver", isFavorite: true,
date: .now+20)
]
}
// --- here renamed and using var ---
struct CityData: Identifiable {
let id: Int
var cities: String
var name1: String
var name2: String
var isFavorite: Bool
var date: Date
}

Related

SwiftUI - Binding var updates the original value only the first time, and then it doesn't update anymore

I have a Picker that updates a #Binding value, that is linked to the original #State value. The problem is that it gets updated only the first time I change it, and then it always remains like that. Not only in the sheet, but also in the ContentView, which is the original view in which the #State variable is declared. Here's a video showing the problem, and here's the code:
ContentView:
struct ContentView: View {
#State var cityForTheView = lisbon
#State var showCitySelection = false
var body: some View {
VStack {
//View
}
.sheet(isPresented: $showCitySelection) {
MapView(cityFromCV: $cityForTheView)
}
}
}
MapView:
struct MapView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var cityFromCV : CityModel
#State var availableCities = [String]()
#State var selectedCity = "Lisbon"
var body: some View {
NavigationView {
ZStack {
VStack {
Form {
HStack {
Text("Select a city:")
Spacer()
Picker("", selection: $selectedCity) {
ForEach(availableCities, id: \.self) {
Text($0)
}
}
.accentColor(.purple)
}
.pickerStyle(MenuPickerStyle())
Section {
Text("Current city: \(cityFromCV.cityName)")
}
}
}
}
}
.interactiveDismissDisabled()
.onAppear {
availableCities = []
for ct in cities {
availableCities.append(ct.cityName)
}
}
.onChange(of: selectedCity) { newv in
self.cityFromCV = cities.first(where: { $0.cityName == newv })!
}
}
}
CityModel:
class CityModel : Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var cityName : String
var country : String
var imageName : String
init(cityName: String, country: String, imageName : String) {
self.cityName = cityName
self.country = country
self.imageName = imageName
}
static func == (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
static func < (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
}
var cities = [
lisbon,
CityModel(cityName: "Madrid", country: "Spain", imageName: "Madrid"),
CityModel(cityName: "Barcelona", country: "Spain", imageName: "Barcelona"),
CityModel(cityName: "Paris", country: "France", imageName: "Paris")
]
var lisbon = CityModel(cityName: "Lisbon", country: "Portugal", imageName: "Lisbon")
What am I doing wrong?
The problem are:
at line 35: you use selectedCity to store the selected data from your picker.
Picker("", selection: $selectedCity) {
then at your line 46: you use cityFromCV.cityName to display the data which will always show the wrong data because you store your selected data in the variable selectedCity.
Section {
Text("Current city: \(cityFromCV.cityName)")
}
Solution: Just change from cityFromCV.cityName to selectedCity.

SwiftUI: Textfield shake animation when input is not valid

I want to create a shake animation when the User presses the "save"-button and the input is not valid. My first approach is this (to simplify I removed the modifiers and not for this case relevant attributes):
View:
struct CreateDeckView: View {
#StateObject var viewModel = CreateDeckViewModel()
HStack {
TextField("Enter title", text: $viewModel.title)
.offset(x: viewModel.isValid ? 0 : 10) //
.animation(Animation.default.repeatCount(5).speed(4)) // shake animation
Button(action: {
viewModel.buttonPressed = true
viewModel.saveDeck(){
self.presentationMode.wrappedValue.dismiss()
}
}, label: {
Text("Save")
})
}
}
ViewModel:
class CreateDeckViewModel: ObservableObject{
#Published var title: String = ""
#Published var buttonPressed = false
var validTitle: Bool {
buttonPressed && !(title.trimmingCharacters(in: .whitespacesAndNewlines) == "")
}
public func saveDeck(completion: #escaping () -> ()){ ... }
}
But this solution doesn't really work. For the first time when I press the button nothing happens. After that when I change the textfield it starts to shake.
using GeometryEffect,
struct ContentView: View {
#StateObject var viewModel = CreateDeckViewModel()
var body: some View {
HStack {
TextField("Enter title", text: $viewModel.title)
.modifier(ShakeEffect(shakes: viewModel.shouldShake ? 2 : 0)) //<- here
.animation(Animation.default.repeatCount(6).speed(3))
Button(action: {
viewModel.saveDeck(){
...
}
}, label: {
Text("Save")
})
}
}
}
//here
struct ShakeEffect: GeometryEffect {
func effectValue(size: CGSize) -> ProjectionTransform {
return ProjectionTransform(CGAffineTransform(translationX: -30 * sin(position * 2 * .pi), y: 0))
}
init(shakes: Int) {
position = CGFloat(shakes)
}
var position: CGFloat
var animatableData: CGFloat {
get { position }
set { position = newValue }
}
}
class CreateDeckViewModel: ObservableObject{
#Published var title: String = ""
#Published var shouldShake = false
var validTitle: Bool {
!(title.trimmingCharacters(in: .whitespacesAndNewlines) == "")
}
public func saveDeck(completion: #escaping () -> ()){
if !validTitle {
shouldShake.toggle() //<- here (you can use PassThrough subject insteadof toggling.)
}
}
}

How can my selection go right in the list I created in SwiftUI

I created the list showing the array of digits. When you select multiple digits and hit the button below, it should tell you the sum of the selected digits.
However, when you select the same digits in the different rows, both of them get checked at a time. I understand that this should be fixed by using UUID, but the final result that I want is the sum of the digits, which is Int. Therefore, I have been lost... Can someone tell me how to make it right, please?
Also, the last row doesn't get checked even when selected for some reason, which is so weird.
Here is the link to the gif showing the current situation and the entire code below.
import SwiftUI
struct MultipleSelectionList: View {
#State var items: [Int] = [20, 20, 50, 23, 3442, 332]
#State var selections: [Int] = []
#State var result: Int = 0
var body: some View {
VStack {
List {
ForEach(items, id: \.self) { item in
MultipleSelectionRow(value: item, isSelected: self.selections.contains(item)) {
if self.selections.contains(item) {
self.selections.removeAll(where: { $0 == item })
}
else {
self.selections.append(item)
}
}
}
}
Button(action: {
result = selections.reduce(0, +)
}, label: {
Text("Show result")
})
.padding()
Text("Result is \(result)")
Text("The number of items in the array is \(selections.count)")
.padding()
Spacer()
}
}
}
struct MultipleSelectionRow: View {
var value: Int
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text("\(self.value)")
if self.isSelected {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MultipleSelectionList()
}
}
You could just store the index of the items in selections, instead of the actual items.
You could do something like:
ForEach(items.indices) { i in
MultipleSelectionRow(value: items[i], isSelected: self.selections.contains(i)) {
if self.selections.contains(i) {
self.selections.removeAll(where: { $0 == i })
}
else {
self.selections.append(i)
}
}
}
I have not run this so there may be errors but you get the idea.
Before you look at the code below try making your items: [Int] into a items: [UUID: Int] this would mimic having an Identifiable object.
Programming is about figuring how to make things happen.
import SwiftUI
struct MultipleSelectionList: View {
#State var items: [UUID:Int] = [UUID():20, UUID():20, UUID():50, UUID():23, UUID():3442, UUID():332]
#State var selections: [UUID:Int] = [:]
#State var result: Int = 0
var body: some View {
VStack {
List {
ForEach(items.sorted(by: { $0.1 < $1.1 }), id: \.key) { key, value in
MultipleSelectionRow(value: value, isSelected: self.selections[key] != nil) {
if self.selections[key] != nil {
self.selections.removeValue(forKey: key)
}
else {
self.selections[key] = value
}
}
}
}
Button(action: {
result = selections.values.reduce(0, +)
}, label: {
Text("Show result")
})
.padding()
Text("Result is \(result)")
Text("The number of items in the array is \(selections.count)")
.padding()
Spacer()
}
}
}
struct MultipleSelectionRow: View {
var value: Int
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text("\(self.value)")
if self.isSelected {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
struct MultipleSelect_Previews: PreviewProvider {
static var previews: some View {
MultipleSelectionList()
}
}

Why won't my bound [String] not change my multi-select list SwiftUI

I am trying to create a multi-select list:
#Binding var selection:[String]
List {
ForEach(self.items, id: \.self) { item in
MultipleSelectionRow(title: item, isSelected: self.selection.contains(item)) {
if self.selection.contains(item) {
self.selection.removeAll(where: { $0 == item }) <=== NO AFFECT
}
else {
self.selection.append(item). <=== NO AFFECT
}
self.queryCallback()
}
}//ForEach
.listRowBackground(Color("TPDarkGrey"))
}//list
I have a row which is a button that calls the above action
struct MultipleSelectionRow: View {
var title: String
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text(self.title)
Spacer()
if self.isSelected {
Image(systemName: "checkmark")
}
}
.font(.system(size: 14))
}
}
}
Why does it not append or remote the item in the bound array? It seems to change on the second time through the view
I managed to produce an example from your code that works:
I don't know how the rest of your code is setup so I cannot hint you to anything unfortunately.
struct MultipleSelectionRow: View {
var title: String
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text(self.title)
Spacer()
if self.isSelected {
Image(systemName: "checkmark")
}
}
.font(.system(size: 14))
}
}
}
struct ContentView: View {
#State var selection:[String] = []
#State var items:[String] = ["Hello", "my", "friend", "did", "I", "solve", "your", "question", "?"]
var body: some View {
List {
ForEach(self.items, id: \.self) { item in
MultipleSelectionRow(title: item, isSelected: self.selection.contains(item)) {
if self.selection.contains(item) {
self.selection.removeAll(where: { $0 == item })
}
else {
self.selection.append(item)
}
}
}
.listRowBackground(Color("TPDarkGrey"))
}
}
}
I hope this helps to clarify things.

How to have a List in SwiftUI able to point to multiple views based upon the row

A simple way to express this would be to go back to UITableView and have a didSelectRowAt(indexPath) function that behaved like this:
if (indexPath.row == 0) { ... } else { ... }
Where based upon the indexPath.row value, I can call a unique view controller (ex: the first one is a TableView and the others are CollectionViews.
Currently, based upon the two answers thus far, I can produce the following code:
import SwiftUI
struct MenuItem {
let title: String
let isEnabled: Bool
}
struct HomeList: View {
let menuItems = [
MenuItem(title: "ABC", isEnabled: true),
MenuItem(title: "DEF", isEnabled: false),
MenuItem(title: "GHI", isEnabled: true)
]
var body: some View {
NavigationView {
List {
ForEach(menuItems.indices, id: \.self) { index in
NavigationLink(destination: menuItems[index].title == "ABC" ?
FirstList() :
SecondView(menuItem: menuItems[index])) {
HomeRow(menuItem: menuItems[index])
}
}
}
}
}
}
struct HomeRow: View {
var menuItem: MenuItem
var body: some View {
HStack {
Text(verbatim: menuItem.title)
}
}
}
struct FirstList: View {
var body: some View {
List(1 ..< 5) { index in
Text("Row \(index)")
}
.listStyle(GroupedListStyle())
}
}
struct SecondView: View {
var menuItem: MenuItem
var body: some View {
Text(menuItem.title)
}
}
However, I get the following error with my NavigationLink:
Result values in '? :' expression have mismatching types 'FirstList'
and 'SecondView'
Since my goal here is to have two different views I point to based upon the title, I'd like to find some way to make that work.
The answer posted by superpuccio seems to be pretty close to what I want, but with the expected complexity of the target views, I do not think it would be feasible to compose them entirely within NavigationLink.
Since you have a dynamic List I suggest you use a ForEach inside a List this way:
import SwiftUI
struct MenuItem {
let title: String
let isEnabled: Bool
}
struct HomeList: View {
let menuItems = [
MenuItem(title: "ABC", isEnabled: true),
MenuItem(title: "DEF", isEnabled: false),
MenuItem(title: "GHI", isEnabled: true)
]
var body: some View {
let firstRowModel = menuItems[0]
let actualModel = menuItems[1...menuItems.count-1]
return NavigationView {
List {
NavigationLink(destination: FirstList()) {
HomeRow(menuItem: firstRowModel)
}
ForEach(actualModel.indices, id: \.self) { index in
NavigationLink(destination: SecondView(menuItem: actualModel[index])) {
HomeRow(menuItem: actualModel[index])
}
}
}
}
}
}
struct HomeRow: View {
var menuItem: MenuItem
var body: some View {
HStack {
Text(verbatim: menuItem.title)
}
}
}
struct FirstList: View {
var body: some View {
List(1 ..< 5) { index in
Text("Row \(index)")
}
.listStyle(GroupedListStyle())
}
}
struct SecondView: View {
var menuItem: MenuItem
var body: some View {
Text(menuItem.title)
}
}
I would include the condition in the destination.
var body: some View {
NavigationView {
List(1 ..< 5) { idx in
NavigationLink(destination:
idx < 3 ? Text("1111") : Text("2222") ) {
Text("Row \(idx)")
}
}
.listStyle(GroupedListStyle())
}
}