SwiftUI - Picker .onChange and didSet - sorting

I'm trying to achieve the behavior in the attached GIF:
Sorry for the High Speed, I had to compress it dramatically to be able to upload it here. The App is "Documents" from Readdle if you want to have a look on your own.
Anyways: I'm exactly trying to achieve this behavior (sorting and filtering, including the dynamic arrow up down icon).
I tried the following approach, however I'm not able to achieve this "ontap" expierience. On Change only triggers when I change the value but when I want to sort an existing value ascending and descending it's not working (which is obvious because it's not changing). I already played around with "didSet" but this also did not work.
Do you have an idea how this can be accomplished?
Below is my code:
import SwiftUI
struct ContentView: View {
#State var selection = 0
#State var sortByAsc = true
#State var filterColumn = "A"
//Test to set case via picker but picter doesnt execute didSet
#State var myFilterTest: MyFilters = .alphabetical {
didSet {
switch myFilterTest {
case .creationDate:
sortByAsc.toggle()
print("c")
case .rating:
sortByAsc.toggle()
print("b")
case .alphabetical:
sortByAsc.toggle()
print("a")
}
}
}
var body: some View {
NavigationView {
Text("Hello, World!")
.padding()
.navigationTitle("SwiftUI")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu(content: {
Picker("My Picker", selection: $selection) {
Label("Title", systemImage: sortByAsc ? "arrow.down" : "arrow.up")
.tag(0)
Label("Rating", systemImage: sortByAsc ? "arrow.down" : "arrow.up")
.tag(1)
.onTapGesture {
print("tap")
}
}
.onChange(of: selection) { tag in
print("Selected Tag: \(tag)")
sortByAsc.toggle()
if(tag == 0) {
filterColumn = "Title"
}
if(tag == 1) {
filterColumn = "Rating"
}
}
}, label: {
Image(systemName: "ellipsis.circle")
})
}
}
}
}
}
enum MyFilters: CaseIterable {
case alphabetical
case rating
case creationDate
}

Solved It. Here's the Code:
struct PickerView: View {
#State private var pickerIndex = 0
#State private var previousPickerIndex = 0
#State var sortByAsc = true
var body: some View {
let pickerSelection = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
if(pickerIndex == previousPickerIndex) {
sortByAsc.toggle()
}
previousPickerIndex = pickerIndex
})
NavigationView {
Text("Hello, World!")
.padding()
.navigationTitle("SwiftUI")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu(content: {
Picker("My Picker", selection: pickerSelection) {
ForEach(0..<4, id: \.self) { index in
Label("Title \(index)", systemImage: getSortingImage(menuItem: index))
.tag(index)
}
}
}, label: {
Image(systemName: "ellipsis.circle")
})
}
}
}
}
func getSortingImage(menuItem: Int) -> String {
if(menuItem == pickerIndex) {
if(sortByAsc) {
return "arrow.down"}
else {
return "arrow.up"
}
}
else {
return ""
}
}
}

Related

How do I set or disable main menu keyboard shortcut programmatically in SwiftUI for a macOS app?

Say, I want to add the following main menu item to my macOS app - Next:
#main
struct MyApp: App {
#ObservedObject var appState = DataViewModel.shared
var body: some Scene {
WindowGroup
{
ContentView()
}
.commands {
CommandGroup(replacing: .pasteboard) {
Button(action: {
appState.nextCurrentID()
}) { Text("Next")}
.keyboardShortcut("V", modifiers: [.command, .option, .shift])
}
}
}
}
But I want to do the following depending on two variables:
enable/disable that menu item's keyboard shortcut depending on appState.EnableShortcutKey
Set the shortcut key depending on appState.ShortcutKey
Set modifiers depending on appState.Modifiers
Where those state variables are declared as such:
#Published public var EnableShortcutKey : Bool = false
#Published public var ShortcutKey : String = "A"
#Published public var Modifiers : NSEvent.ModifierFlags = [.command, .shift]
How do I do that?
Like this:
#main
struct SO_mac_TestsApp: App {
#StateObject var menuOptions = MenuOptions()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(menuOptions)
}
.commands {
CommandGroup(replacing: .pasteboard) {
Button(action: {
print("appState.nextCurrentID()")
}) { Text("Next")}
.keyboardShortcut(menuOptions.shortcutKey, modifiers: menuOptions.modifiers)
.disabled(menuOptions.enableShortcutKey == false)
}
}
}
}
class MenuOptions: ObservableObject {
#Published public var enableShortcutKey : Bool = false
#Published public var shortcutKey : KeyEquivalent = "A"
#Published public var modifiers : EventModifiers = [.command, .shift]
}
truct ContentView: View {
#EnvironmentObject var menuOptions: MenuOptions
#State private var shortcut = ""
var body: some View {
Form {
Toggle("Enable Shortcut", isOn: $menuOptions.enableShortcutKey)
TextField("Shortcut", text: $shortcut)
.onSubmit {
menuOptions.shortcutKey = KeyEquivalent(shortcut.first ?? "V")
}
Divider()
Text("Modifiers:")
Toggle("command:", isOn: Binding(
get: { menuOptions.modifiers.contains(.command) },
set: { new,_ in
if new { menuOptions.modifiers.insert(.command)
} else { menuOptions.modifiers.subtract(.command) }
}
) )
Toggle("option:", isOn: Binding(
get: { menuOptions.modifiers.contains(.option) },
set: { new,_ in
if new { menuOptions.modifiers.insert(.option)
} else { menuOptions.modifiers.subtract(.option) }
}
) )
Toggle("shift:", isOn: Binding(
get: { menuOptions.modifiers.contains(.shift) },
set: { new,_ in
if new { menuOptions.modifiers.insert(.shift)
} else { menuOptions.modifiers.subtract(.shift) }
}
) )
Toggle("control:", isOn: Binding(
get: { menuOptions.modifiers.contains(.control) },
set: { new,_ in
if new { menuOptions.modifiers.insert(.control)
} else { menuOptions.modifiers.subtract(.control) }
}
) )
}
.padding()
.onAppear {
shortcut = String(menuOptions.shortcutKey.character)
}
}
}

Coredata dynamic filters (predicate) in SwiftUI

I found many tutorials pointing out how to create a dynamic filter in Swiftui and Coredata, but none solved my problem, which is to create an entity extension like:
extension Food {
var myFetchRequest: NSFetchRequest<Food> {
let request: NSFetchRequest<Food> = Food.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
request.predicate = NSPredicate(format: "name CONTAINS[c] %#", searchText)
}
}
because obviously I get the following error:
I would like to get the following result:
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjContext
#FetchRequest(fetchRequest: Food.myFetchRequest)
var food: FetchedResults<Food>
#State private var searchText = ""
var body: some View {
NavigationView {
List {
ForEach(food) { food in
NavigationLink(destination: EditFoodView(food: food)) {
HStack {
VStack(alignment: .leading, spacing: 6) {
Text(food.name!)
.bold()
Text("\(Int(food.calories))") + Text(" calories").foregroundColor(.red)
}
Spacer()
Text(calcTimeSince(date: food.date!))
.foregroundColor(.gray)
.italic()
}
}
}
.onDelete(perform: deleteFood)
}
.listStyle(.plain)
}
.navigationTitle("iCalories")
.searchable(text: $searchText)
}
// Deletes food at the current offset
private func deleteFood(offsets: IndexSet) {
withAnimation {
offsets.map { food[$0] }
.forEach(managedObjContext.delete)
// Saves to our database
DataController().save(context: managedObjContext)
}
}
}
How do I have to change extension Food { to make the code work?

SwiftUI Splicing color picker code into another picker

I'm trying to splice some code I found into a current SwiftUI view I have.
Basically, I want to make my segmented picker have colors that correspond to priority colors in a to-do list view.
Here is the sample code for the colored segmented picker. (Taken from HWS)
import SwiftUI
enum Colors: String, CaseIterable{
case red, yellow, green, blue
func displayColor() -> String {
self.rawValue.capitalized
}
}
struct TestView: View {
#State private var selectedColor = Colors.red
var body: some View {
Picker(selection: $selectedColor, label: Text("Colors")) {
ForEach(Colors.allCases, id: \.self) { color in
Text(color.displayColor())
}
}
.padding()
.colorMultiply(color(selectedColor))
.pickerStyle(SegmentedPickerStyle())
}
func color(_ selected: Colors) -> Color {
switch selected {
case .red:
return .red
case .yellow:
return .yellow
case .green:
return .green
case .blue:
return .blue
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Then, here is the (complete because I don't have the chops to make MRE's yet– I'm still learning) code for the to-do list view (Taken from YouTube– I can't remember the creator's name, but I'll post it below once I find it again.):
import SwiftUI
enum Priority: String, Identifiable, CaseIterable {
var id: UUID {
return UUID()
}
case one = "Priority 1"
case two = "Priority 2"
case three = "Priority 3"
case four = "Priority 4"
}
extension Priority { //"priority.title"
var title: String {
switch self {
case .alloc:
return "Priority 1"
case .aoc:
return "Priority 2"
case .charting:
return "Priority 3"
case .clinical:
return "Priority 4"
}
}
}
struct ToDoView: View {
#State private var title: String = ""
#State private var selectedPriority: Priority = .charting
#FocusState private var isTextFieldFocused: Bool
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: Task.entity(), sortDescriptors: [NSSortDescriptor(key: "dateCreated", ascending: true)]) private var allTasks: FetchedResults<Task>
private func saveTask() {
do {
let task = Task(context: viewContext)
task.title = title
task.priority = selectedPriority.rawValue
task.dateCreated = Date()
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
private func styleForPriority(_ value: String) -> Color {
let priority = Priority(rawValue: value)
switch priority {
case .one:
return Color.green
case .two:
return Color.red
case .three:
return Color.blue
case .four:
return Color.yellow
default:
return Color.black
}
}
private func updateTask(_ task: Task) {
task.isFavorite = !task.isFavorite
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
private func deleteTask(at offsets: IndexSet) {
offsets.forEach { index in
let task = allTasks[index]
viewContext.delete(task)
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
var body: some View {
NavigationView {
VStack {
TextField("Enter task...", text: $title)
.textFieldStyle(.roundedBorder)
.focused($isTextFieldFocused)
.foregroundColor(Color(UIColor.systemBlue))
.modifier(TextFieldClearButton(text: $title))
.multilineTextAlignment(.leading)
Picker("Type", selection: $selectedPriority) {
ForEach(Priority.allCases) { priority in
Text(priority.title).tag(priority)
}
}.pickerStyle(.segmented)
Button("Save") {
saveTask()
}
.padding(10)
.frame(maxWidth: .infinity)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 10.0, style: .continuous))
List {
ForEach(allTasks) { task in
HStack {
Circle()
.fill(styleForPriority(task.priority!))
.frame(width: 15, height: 15)
Spacer().frame(width: 20)
Text(task.title ?? "")
Spacer()
Image(systemName: task.isFavorite ? "checkmark.circle.fill": "circle")
.foregroundColor(.red)
.onTapGesture {
updateTask(task)
}
}
}.onDelete(perform: deleteTask)
}
Spacer()
}
.padding()
.navigationTitle("To-Do List")
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(action: {
isTextFieldFocused = false
}) { Text("Done")}
}
}
}
}
}

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)
}
}

How to toggle the visibility of the third pane of NavigationView?

Assuming the following NavigationView:
Struct ContentView: View {
#State var showRigthPane: Bool = true
var body: some View {
NavigationView {
Sidebar()
MiddlePane()
RightPane()
}.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: toggleSidebar, label: {Image(systemName: "sidebar.left")})
}
ToolbarItem(placement: .primaryAction) {
Button(action: self.toggleRightPane, label: { Image() })
}
}
}
private func toggleRightPane() {
// ?
}
// collapsing sidebar - this works
private func toggleSidebar() {
NSApp.keyWindow?.initialFirstResponder?.tryToPerform(
#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
}
}
How can I implement the toggleRightPane() function to toggle the visibility of the right pane?
Updated to use a calculated property returning two different navigation views. Still odd behavior with sidebar, but with a work-around it is functional. Hopefully someone can figure out the sidebar behavior.
struct ToggleThirdPaneView: View {
#State var showRigthPane: Bool = true
var body: some View {
VStack {
navigationView
}
.navigationTitle("Show and Hide")
}
var navigationView : some View {
if showRigthPane {
return AnyView(NavigationView {
VStack {
Text("left")
}
.toolbar {
Button(action: { showRigthPane.toggle() }) {
Label("Add Item", systemImage: showRigthPane ? "rectangle.split.3x1" : "rectangle.split.2x1")
}
}
Text("middle")
}
)
} else {
return AnyView(NavigationView {
VStack {
Text("left")
}
.toolbar {
Button(action: { showRigthPane.toggle() }) {
Label("Add Item", systemImage: showRigthPane ? "rectangle.split.3x1" : "rectangle.split.2x1")
}
}
Text("middle")
Text("right")
})
}
}
}
Try the following (cannot test)
struct ContentView: View {
#State private var showRigthPane = true
var body: some View {
NavigationView {
Sidebar()
MiddlePane()
if showRigthPane { // << here !!
RightPane()
}
}.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: self.toggleRightPane, label: { Image() })
}
}
}
private func toggleRightPane() {
withAnimation {
self.showRigthPane.toggle() // << here !!
}
}
}

Resources