How to sort List ForEach Swiftui - sorting

Hi there
I'm newbie for SwiftUI, and I want to sort the "expireDate" , then use forEach to display the view according to the expireDate, how to???
sorry for my messy code, coding is really not easy.
will be much appreciate if someone can help
Here is the data
import Foundation
struct CardData: Identifiable, Codable {
let id: UUID
var cardName: String
var cardNumber: String
var expireDate: Date
var theme: Theme
var history: [History] = []
init(id: UUID = UUID(), cardName: String, cardNumber: String, expireDate: Date, theme: Theme) {
self.id = id
self.cardName = cardName
self.cardNumber = cardNumber
self.expireDate = expireDate
self.theme = theme
}
}
extension CardData {
struct Data {
var cardName: String = ""
var cardNumber: String = ""
var expireDate: Date = Date.now
var theme: Theme = .orange
}
var data: Data {
Data(cardName: cardName, cardNumber: cardNumber, expireDate: expireDate, theme: theme)
}
mutating func update(from data: Data) {
cardName = data.cardName
cardNumber = data.cardNumber
expireDate = data.expireDate
theme = data.theme
}
init(data: Data) {
cardName = data.cardName
cardNumber = data.cardNumber
expireDate = data.expireDate
theme = data.theme
id = UUID()
}
}
And here is the view
import SwiftUI
struct CardView: View {
#Binding var datas: [CardData]
#Environment(\.scenePhase) private var scenePhase
#State private var isPresentingNewCardView = false
#State private var newCardData = CardData.Data()
let saveAction: () -> Void
#EnvironmentObject var launchScreenManager: LaunchScreenManager
#State private var confirmationShow = false
var body: some View {
List {
ForEach($datas) { $data in
NavigationLink(destination: DetailView(cardData: $data)){
CardDataView(cardData: data)
}
.listRowBackground(data.theme.mainColor)
}
.onDelete(perform: deleteItems)
}
.navigationTitle("Expiry Date")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button(action: {
isPresentingNewCardView = true
}) {
Image(systemName: "plus")
}
.accessibilityLabel("New data")
}
.sheet(isPresented: $isPresentingNewCardView) {
NavigationView {
DetailEditView(data: $newCardData)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
isPresentingNewCardView = false
newCardData = CardData.Data()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
let newData = CardData(data: newCardData)
datas.append(newData)
isPresentingNewCardView = false
newCardData = CardData.Data()
}
}
}
}
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
launchScreenManager.dismiss()
}
}
}
func deleteItems(at offsets: IndexSet) {
datas.remove(atOffsets: offsets)
}
}
Hi there
I'm newbie for SwiftUI, and I want to sort the "expireDate" , then use forEach to display the view according to the expireDate, how to???
sorry for my messy code, coding is really not easy.
will be much appreciate if someone can help

You can sort the datas in place, before you use it in the ForEach,
when you create the datas for example. Like this:
datas.sort(by: { $0.expireDate > $1.expireDate}).
Or
you can sort the datas just in the ForEach,
like this, since you have bindings,
ForEach($datas.sorted(by: { $0.expireDate.wrappedValue > $1.expireDate.wrappedValue})) { $data ...}
Note with this ForEach($datas.sorted(by: ...), when you do your func deleteItems(at offsets: IndexSet),
you will have to get the index in the sorted array, and delete the equivalent in the original one.
EDIT-1:
updated func deleteItems:
func deleteItems(at offsets: IndexSet) {
let sortedArr = datas.sorted(by: { $0.expireDate > $1.expireDate})
for ndx in offsets {
if let cardIndex = datas.firstIndex(where: { $0.id == sortedArr[ndx].id }) {
datas.remove(at: cardIndex)
}
}
}
Note you may want to put let sortedArr = datas.sorted(by: { $0.expireDate > $1.expireDate}) somewhere else (eg. .onAppear) instead of evaluating this, every time you use deleteItems

Related

Is it possible to create 5 oscillators (an array?) working simultaneously and control their frequency and amplitude independently of each other?

import SwiftUI
import AudioKit
class ToneGenerator {
let engine = AudioEngine()
let osc = PlaygroundOscillator()
init(){
engine.output = osc
try! engine.start()
}
}
struct ContentView: View {
let toneGenerator = ToneGenerator()
var freq01: Float = 200
var volume01: Float = 0.5
#State var isPressed = false
var body: some View {
Text("BEEP")
.font((.title))
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
isPressed = true
toneGenerator.osc.frequency = freq01
toneGenerator.osc.amplitude = volume01
toneGenerator.osc.start()
})
.onEnded({ _ in
isPressed = false
toneGenerator.osc.stop()
})
)
}
}
I tried to create several generators, but did not understand how to do it.
All the materials I found on the Internet are related to the Playground and do not work in Xcode.
I'm not 100% sure what you're trying to achieve, but how about something like this. The key is to use $toneGenerators[index] to create a binding to the tone generator so you can change the volume and frequency.
class ToneGenerator: Identifiable {
let id = UUID()
let engine = AudioEngine()
var osc = PlaygroundOscillator()
init(){
engine.output = osc
try! engine.start()
}
}
struct ContentView: View {
#State var toneGenerators: [ToneGenerator] = []
let freqs = [Float(100), 200, 300, 400, 500]
var body: some View {
VStack {
HStack {
Button("BEEP") {
let toneGenerator = ToneGenerator()
toneGenerator.osc.frequency = Float.random(in: 100...1000)
toneGenerator.osc.amplitude = 0.5
toneGenerator.osc.start()
toneGenerators.append(toneGenerator)
}
.font((.title))
Button("STOP") {
toneGenerators.forEach { toneGenerator in
toneGenerator.osc.stop()
}
toneGenerators = []
}
.tint(.red)
.font((.title))
}
Text(toneGenerators.count, format: .number) + Text(" generators")
Grid {
GridRow {
Text("Freq").bold()
Text("Volume").bold()
}
ForEach(toneGenerators) { toneGenerator in
let index = toneGenerators.firstIndex(where: { $0.id == toneGenerator.id })!
GridRow {
Slider(value: $toneGenerators[index].osc.frequency, in: 100...1000)
Slider(value: $toneGenerators[index].osc.amplitude, in: 0...1)
}
}
}
.padding()
Spacer()
}
}
}

How to search a Table using SwiftUI on macOS?

In SwiftUI on iOS and iPadOS 15, we can add a search bar to filter a list using the searchable modifier:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var searchTerm = ""
#State private var selection = Set<Video.ID>()
private var fetchRequest: FetchRequest<Video>
private var searchResults: [Video] {
if searchTerm.isEmpty {
return fetchRequest.wrappedValue.filter { _ in true }
} else {
return fetchRequest.wrappedValue.filter { $0.matching(searchTerm) }
}
}
var body: some View {
NavigationView {
List {
ForEach(searchResults) { item in
VideoListCellView(video: item)
}
}.searchable(text: $searchTerm, prompt: "Video name") // <-- HERE
}
}
}
However, on macOS, the searchable modifier is not supported in the new Table container:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [SortDescriptor(\.addDate, order: .reverse)], animation: .default)
private var videos: FetchedResults<Video>
#State
private var selection = Set<Video.ID>()
var body: some View {
NavigationView {
Table(videos, selection: $selection, sortOrder: $videos.sortDescriptors) {
TableColumn("Title") {
Text($0.title)
}
TableColumn("Added") {
Text($0.addDate)
}.width(120)
TableColumn("Published") {
Text($0.publishedAt)
}.width(120)
TableColumn("Duration") {
Text($0.duration)
}.width(50)
}.searchable(text: $searchTerm, prompt: "Video name") // <-- GENERATES ERROR
}
}
}
Trying to use it generates a compile error in the var body: some View:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
Is there another way to search a Table on macOS, or is this feature not supported yet?
The solution was to add the .searchable modifier to the NavigationView instead of the Table, as Scott suggested:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [SortDescriptor(\.addDate, order: .reverse)], animation: .default)
private var videos: FetchedResults<Video>
#State private var selection = Set<Video.ID>()
#State private var searchTerm = ""
private var searchResults: [Video] {
if searchTerm.isEmpty {
return videos.filter { _ in true }
} else {
return videos.filter { $0.matching(searchTerm) }
}
}
var body: some View {
NavigationView {
Table(searchResults, selection: $selection, sortOrder: $videos.sortDescriptors) {
TableColumn("Title", value: \.title) {
Text($0.title)
}
TableColumn("Added", value: \.addDate) {
Text($0.addDate)
}.width(120)
TableColumn("Published", value: \.publishedAt) {
Text($0.publishedAt)
}.width(120)
TableColumn("Duration") {
Text($0.duration)
}.width(50)
}
}.searchable(text: $searchTerm, prompt: "Video name") // <-- HERE
}
}
You can solve this by updating the predicate of the fetch request using a specific Binding variable.
The below solution is based on an example from the 2021 WWDC video Bring Core Data concurrency to Swift and SwiftUI where it was used on a List which is what I also used it for but I tested it on one of my tables and it works equally well.
#State private var searchText: String = ""
var query: Binding<String> {
Binding {
searchText
} set: { newValue in
searchText = newValue
if newValue.isEmpty {
videos.nsPredicate = NSPredicate(value: true)
} else {
videos.nsPredicate = NSPredicate(format: "name BEGINSWITH[c] %#", newValue)
}
}
}
And then you use pass this variable to .searchable
Table(videos, selection: $selection, sortOrder: $videos.sortDescriptors) {
// ...
}
.searchable(text: query, prompt: "Search instrument")
The downside of this solution is that a new fetch request is executed for each typed letter. I tried a quick fix by adding if newValue.count < 3 { return } in the else of the query set method and it works but it might be a bad restriction, maybe something more advanced can be implemented by using Combine.

Deleting an item from a list based on the element UUID

I feel a bit embarrassed for asking this, but after more than a day trying I'm stuck. I've had a few changes on the code based on replies to other issues. The latest code essentially selects the items on a list based on the UUID.
This has caused my delete function to stop working since I was working with passing an Int as the selected element to be deleted. I was originally implementing things like this.
Code follows, I'm still trying to figure out my way around SwiftUI, but question is, how can I now delete items on a list (and on the array behind it) based on a UUID as opposed to the usual selected item.
In case it makes a difference, this is for macOS Big Sur.
Code:
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
#State var selectedNoteId: UUID?
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
ToolbarItem(placement: .automatic) {
Button(action: {
// Delete here????
}) {
Image(systemName: "trash")
}
}
}
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
}
.onChange(of: data.notes) { notes in
if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
selectedNoteId = data.notes.first?.id
}
}
}
}
The original removeNote I had is the following:
func removeNote() {
if let selection = self.selectedItem,
let selectionIndex = data.notes.firstIndex(of: selection) {
print("delete item: \(selectionIndex)")
data.notes.remove(at: selectionIndex)
}
}
could you try this:
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID // <--- here
var text: String
var date = Date()
var dateText: String = ""
var tags: [String] = []
}
func removeNote() {
if let selection = selectedNoteId,
let selectionIndex = data.notes.firstIndex(where: { $0.id == selection }) {
print("delete item: \(selectionIndex)")
data.notes.remove(at: selectionIndex)
}
}

Work out time 1.5 hours ahead using current time and display this in the list

I have the following code someone presses on the Table and its displays the current time which is the arrival time of a customer.
I want to display the time they must leave by, next to the current time this will always be 1.5 hours ahead I can not work out how to do this. everything I have tried comes back with an error.
Still new to Xcode
any help would be great
import SwiftUI
struct TimeListView: View {
#State var tableOne = false
#State var tableTwo = false
#State var tableThree = false
var body: some View {
// Title
VStack {
Text("Arrival Times")
.font(.title)
.fontWeight(.bold)
// List View
List {
// Table 1
HStack {
Button(action: {
self.tableOne.toggle()
}, label: {
Text("Table 1 -")
})
if tableOne {
Text(getCurrentTime())
}
}
// Table 2
HStack {
Button(action: {
self.tableTwo.toggle()
}, label: {
Text("Table 2 -")
})
if tableTwo {
Text(getCurrentTime())
}
}
// Table 3
HStack {
Button(action: {
self.tableThree.toggle()
}, label: {
Text("Table 3 -")
})
if tableThree {
Text(getCurrentTime())
}
}
}
}
}
}
struct TimeListView_Previews: PreviewProvider {
static var previews: some View {
TimeListView()
}
}
// Get Current Time Function
func getCurrentTime() -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_UK_POSIX")
dateFormatter.dateFormat = "HH:mm"
return dateFormatter.string(from: Date())
you need make a date and add 1.5 hour to it, also you forgot create 3 deferent State for them.
import SwiftUI
struct ContentView: View {
var body: some View {
TimeListView()
}
}
struct TimeListView: View {
#State private var table1: Bool = false
#State private var table2: Bool = false
#State private var table3: Bool = false
#State private var timeForShow1: String?
#State private var timeForShow2: String?
#State private var timeForShow3: String?
var body: some View {
VStack {
Text("Arrival Times")
.font(.title)
.fontWeight(.bold)
List {
HStack {
Text("Table 1 -")
.onTapGesture {
if table1 { table1.toggle() }
else { timeForShow1 = getCurrentTime; table1.toggle() }
}
if table1 { Text(timeForShow1 ?? "not available!") }
}
HStack {
Text("Table 2 -")
.onTapGesture {
if table2 { table2.toggle() }
else { timeForShow2 = getCurrentTime; table2.toggle() }
}
if table2 { Text(timeForShow2 ?? "not available!") }
}
HStack {
Text("Table 3 -")
.onTapGesture {
if table3 { table3.toggle() }
else { timeForShow3 = getCurrentTime; table3.toggle() }
}
if table3 { Text(timeForShow3 ?? "not available!") }
}
}
}
}
}
var getCurrentTime: String? {
if let date = Calendar.current.date(byAdding: .minute, value: 90, to: Date()) {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_UK_POSIX")
dateFormatter.dateFormat = "HH:mm"
return dateFormatter.string(from: date)
}
else {
return nil
}
}
You'll probably get a half dozen different ways of doing this but this version allows for adaptive/reusable code. You can easily add a Table by adding one to the var tables: [Table] everything would adjust automatically.
import SwiftUI
class Table: ObservableObject {
let id: UUID = UUID()
#Published var name: String
#Published var status: Bool
#Published var entryTime: Date
var exitTime: Date{
return entryTime.addingTimeInterval(60*60*1.5)
}
init(name: String, status: Bool = false, entryTime: Date = Date.init(timeIntervalSince1970: 0)) {
self.name = name
self.status = status
self.entryTime = entryTime
}
}
struct TimeListView: View {
#State var tables: [Table] = [Table(name: "Table 1 -"), Table(name: "Table 2 -"), Table(name: "Table 3 -")]
var body: some View {
VStack{
Text("Arrival Times")
.font(.title)
.fontWeight(.bold)
List {
ForEach(tables, id: \.id, content: { table in
TableView(table: table)
})
}
}
}
}
struct TableView: View {
#ObservedObject var table: Table
var body: some View {
HStack {
Button(action: {
table.status.toggle()
table.entryTime = Date()
}, label: {
Text(table.name)
})
if table.status{
Text(table.entryTime, formatter: dateFormatter)
Text(table.exitTime, formatter: dateFormatter)
}
}
}
}
struct TimeListView_Previews: PreviewProvider {
static var previews: some View {
TimeListView()
}
}
var dateFormatter: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_UK_POSIX")
dateFormatter.dateFormat = "HH:mm"
return dateFormatter
}

Reset scroll SwiftUI List to top when reloaded with new data / MacOS

I have a List whose contents can be updated. If the contents are changed after the list has been scrolled down screen, the updated List does not reset to the top.
import SwiftUI
struct MyData {
let a: [String]
let b: [String]
init() {
var pa = [String]()
var pb = [String]()
for i in 0...100 {
pa.append("A: \(i)")
pb.append("B: \(i)")
}
self.a = pa
self.b = pb
}
}
struct ListNotResetingToTop: View {
let data = MyData()
#State var showA = true
var body: some View {
VStack {
Button("Switch") { showA.toggle() }
List(showA ? data.a : data.b, id: \.self) { value in
Text(value)
}
}
}
}
I've tried wrapping the List in a ScrollReader, but this did not work either:
struct ListNotResetingToTop: View {
let data = MyData()
#State var showA = true
var body: some View {
VStack {
Button("Switch") { showA.toggle() }
ScrollViewReader { proxy in
List(showA ? data.a : data.b, id: \.self) { value in
Text(value)
}.onChange(of: showA) { _ in
proxy.scrollTo(showA ? data.a.first : data.b.first)
}
}
}
}
}
Probably you need something like
VStack {
Button("Switch") { showA.toggle() }
List(showA ? data.a : data.b, id: \.self) { value in
Text(value)
}.id(showA) // just make id depend on modified data
}

Resources