Change of selected row in Table for macOS - macos

I use Table in my CoreData app for macOS
#FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)]) private var categories: FetchedResults<Categories>
#State private var selectedCategoryID: Categories.ID?
and I'm wondering how I could catch change of selected row. If I try .onChange() as shown below then code can't be compiled due to error "Cannot call value of non-function type 'Categories.ID?' (aka 'Optional<Optional>')"
Table(categories, selection: $selectedCategoryID) {
TableColumn("Name", value \.name!)
TableColumn("Type", value: \.type!)
}.onChange(of: selectedCategoryID {
print("Selected row changed.")
}
Thank you.

onChange is "to trigger a side effect", e.g. something unrelated. If you want to update a related value, then a trick is to move it into a struct with some logic, e.g.
struct TableConfig {
var selectedCategoryID: CategoryID? {
didSet {
counter++
}
}
var counter = 0
mutating func reset() {
selectedCategoryID = nil
counter = 0
}
}
...
#State var config = TableConfig()
...
Table(categories, selection: $config.selectedCategoryID)

I finally found the root cause .onChange() must looks like
.onChange(of: selectedCategoryID) { selected in
print("Selected row is \(selected)")
}

Related

Swiftui TextEditor seems not to respond to state changes

I am having trouble getting the SwiftUI TextEditor to work when it is in a Child View.
This is a small example that demonstrates the issue for me:
import SwiftUI
struct ContentView: View {
#State private var someText: String = "Hello World"
var body: some View {
VStack {
HStack {
Button("Text 1", action: {someText = "hello"})
Button("Text 2", action: {someText = "world"})
}
ViewWithEditor(entry: $someText)
}
}
}
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
When I click the buttons I expected the Editor text to change, however it remains with its initial value.
The print statement show that the "localString" variable is being updated.
Is TextEditor broken or am I missing something fundamental ??
If you move the buttons into the same view as the TextEditor, directly changing local state var it works as expected.
This is being run under MacOS in case it makes a difference.
TIA Alan.
Ok. So a proxy Binding does the job for me. See updated editor view below:
struct ViewWithEditor: View {
#Binding var entry: String
var body: some View
{
let localString = Binding<String> (
get: {
entry
},
set: {
entry = $0
}
)
return VStack {
Text(entry)
TextEditor(text: localString)
}
}
A bit more ugly (proxy bindings just seem clutter), but in some ways simpler..
It allows for the result of the edit to be reviewed / rejected before being pushed into the bound var.
This is occurring because the binding entry var is not actually being used after initialization of ViewWithEditor. In order to make this work without using the proxy binding add onChange to the ViewWithEditor as below:
struct ViewWithEditor: View {
#Binding var entry: String
#State private var localString: String
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
}
init(entry: Binding<String>) {
self._entry = entry
self._localString = State(initialValue: entry.wrappedValue)
print("init set local String to: \(localString)")
}
}
The problem here is that now entry is not updating if localString changes. One could just use the same approach as before:
var body: some View
{
VStack {
TextEditor(text: $localString)
}
.onChange(of: entry) {
localString = $0
}
.onChange(of: localString) {
entry = $0
}
}
but why not just use $entry as the binding string for TextEditor?

SwiftUI - Dynamic NSPredicate If Statement

How can I create a predicate so that when the user selects "Full Body" it returns the entire list with no predicate? Right now, it is returning "part" which corresponds to the muscle groups I have set (Abs, Legs, Push, Pull). I want to return all of the options when "Full Body" is selected. How could I write an If statement so that the predicate is not used?
import SwiftUI
var parts = ["Abs", "Legs", "Push", "Pull", "Full Body"]
struct ExerciseList: View {
#State private var selectedPart = " "
var body: some View {
NavigationView {
VStack (alignment: .leading) {
NavigationLink(destination: AddExerciseView()){
Text("Add Exercise")
.fontWeight(.bold)
}
Picker("Body Part", selection: $selectedPart) {
ForEach(parts, id:\.self) { part in
Text(part)
}
}.pickerStyle(.segmented)
ListView(part:selectedPart)
}
}
}
}
import SwiftUI
struct ListView: View {
var part: String
#FetchRequest var exercises: FetchedResults<Exercise>
init(part: String) {
self.part = part
self._exercises = FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
)
}
var body: some View {
List(exercises) { e in
Text(e.exercisename)
}
}
}
It's not a good idea to init objects inside View structs because the heap allocation slows things down. You could either have all the predicates created before hand or create one when the picker value changes, e.g. something like this:
// all the Picker samples in the docs tend to use enums.
enum Part: String, Identifiable, CaseIterable {
case abs
case legs
case push
case pull
case fullBody
var id: Self { self }
// Apple sometimes does it like this
// var localizedName: LocalizedStringKey {
// switch self {
// case .abs: return "Abs"
// case .legs: return "Legs"
// case .push: return "Push"
// case .pull: return "Pull"
// case .fullBody: return "Full Body"
// }
// }
}
struct ExerciseListConfig {
var selectedPart: Part = .fullBody {
didSet {
if selectedPart == .fullBody {
predicate = nil
}
else {
// note this will use the lower case string
predicate = NSPredicate(format: "musclegroup == %#", selectedPart.rawValue)
}
}
}
var predicate: NSPredicate?
}
struct ExerciseList: View {
#State private var config = ExerciseListConfig()
var body: some View {
NavigationView {
VStack (alignment: .leading) {
Picker("Body Part", selection: $config.selectedPart) {
//ForEach(Part.allCases) // Apple sometimes does this but means you can't easily change the display order.
Text("Abs").tag(Part.abs)
Text("Legs").tag(Part.legs)
Text("Push").tag(Part.push)
Text("Pull").tag(Part.pull)
Text("Full Body").tag(Part.fullBody)
}.pickerStyle(.segmented)
ExerciseListView(predicate:config.predicate)
}
}
}
}
struct ExerciseListView: View {
// var part: String
let predicate: NSPredicate?
// #FetchRequest var exercises: FetchedResults<Exercise>
init(predicate: NSPredicate?) {
self.predicate = predicate
// self._exercises = FetchRequest(
// entity: Exercise.entity(),
// sortDescriptors: [],
//
// predicate: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
// )
}
var body: some View {
Text(predicate?.description ?? "")
// List(exercises) { e in
// Text(e.exercisename)
// }
}
}
Since you are using Core Data you might want to use an Int enum in the entity for less storage and faster queries.
you could try this simple approach in ListView:
init(part: String) {
self.part = part
self._exercises = FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [],
predicate: (part == "Full Body")
? nil
: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
)
}

How to change the sort comparator or display Int? values on a Table with SwiftUI on macOS

I have the following table that mostly works when all the column values are strings:
struct Person : Identifiable {
let id: String // assume names are unique for this example
let name: String
let rank: String
}
#State var people = []
#State var sort = [KeyPathComparitor(\Person.rank)]
#State var selection: Person.ID? = nil
private var rankingsCancellable: AnyCancellable? = nil
init (event: Event) {
rankingsCancellable = event.rankingsSubject
.receive(on: DispatchQueue.main)
.sink { rankings in
var newRankings: [Person] = []
for ranking in rankings {
newRankings.append(Person(id: ranking.name, name: ranking.name, ranking: ranking.rank
}
newRankings.sort(by: sort)
people = newRankings
}
}
var body : some View {
Table(people, selection: $selection, sortOrder: $sort) {
TableColumn("Name", value: \Person.name)
TableColumn("Rank", value: \Person.rank)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
This works well enough with one exception when the rank is unknown coming into the sink, its sent as an empty string and the empty strings sort before those with valid ranking. Ideally, I'd like to have those without a ranking sort after until their rank is received.
I've tried a couple of things, changing Person.rank to an Int but the compiler gives the typical 'too complex' error when:
TableColumn("Rank", value: \Person.rank) { Text(\($0.rank)) } // rank is an int here
Alternatively I've tried to create a custom sort comparator when creating the KeyPathComparator(\Person.rank) but the documentation around that is limited and haven't been able to suss out a workable solution.
Any ideas how to get the Table to simply display Ints (which I imagine would have to be optionals in this instance) or add a string comparator that moves empty strings below populated strings?
TIA
This is the best answer I could find, tho admit it does seem a bit like a kludge. Basically adding Int property for sorting and setting the sort to initially sort on this Person property
struct Person : Identifiable {
let id: String // assume names are unique for this example
let name: String
let rank: String
//Add rankForSort as an int which is a non displayed column
let rankForSort: Int
}
#State var people = []
//Change to initial sort to non-displayed rank field
#State var sort = [KeyPathComparitor(\Person.rankForSort)]
#State var selection: Person.ID? = nil
private var rankingsCancellable: AnyCancellable? = nil
init (event: Event) {
rankingsCancellable = event.rankingsSubject
.receive(on: DispatchQueue.main)
.sink { rankings in
var newRankings: [Person] = []
for ranking in rankings {
// Ranking now coming through in the subject as Int?
let rankString = ranking.rank != nil ? "\(ranking.rank!)" : ""
newRankings.append(Person(id: ranking.name, name: ranking.name, ranking: rankString, rankForSort: ranking.rank ?? Int.max)
}
newRankings.sort(by: sort)
people = newRankings
}
}
var body : some View {
Table(people, selection: $selection, sortOrder: $sort) {
TableColumn("Name", value: \Person.name)
TableColumn("Rank", value: \Person.rank)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
Here is a way to do an initial sort and uses didSet which has the added benefit it doesn't use onChange which results in body called twice when resorting.
struct ProductsData {
var sortedProducts: [Product]
var sortOrder = [KeyPathComparator(\Product.name)] {
didSet {
sortedProducts.sort(using: sortOrder)
}
}
init() {
sortedProducts = myProducts.sorted(using: sortOrder)
}
}
struct ContentView: View {
#State private var data = ProductsData()
var body: some View {
ProductsTable(data: $data)
}
}
struct ProductsTable: View {
#Environment(\.horizontalSizeClass) private var horizontalSizeClass
#Binding var data: ProductsData
var body: some View {
Table(data.sortedProducts, sortOrder: $data.sortOrder) {
See this answer to a different question for the full sample.

Display subview from an array in SwiftUI

I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.

SwiftUI onCommit doesnt move cursor to next textfield - MacOS app

I have two TextFields that use onCommit. Pressing enter saves the value. However, this does not automatically move the cursor to the next TextField. I want it to work like the tab button which moves the cursor to next TextField but this doesn't save the value (due to onCommit requiring enter/return to be pressed). The best solution I have found is using a button but that results in poor usability as I would be using a ForLoop over this view.
struct ModuleDetailView: View {
#Binding var subjectDetails: [Subjects]
#State private var subject = ""
#State private var grade = ""
var body: some View {
VStack {
TextField("Subject", text: $subject, onCommit: appendData)
TextField("Grade", text: $grade, onCommit: appendData)
VStack {
Text("Output")
ForEach(subjectDetails) { subject in
HStack {
Text(subject.name)
Text(subject.grade)
}
}
}
}
}
func appendData() {
if subject != "" && grade != "" {
let module = Subjects(name: subject, grade: grade)
subjectDetails.append(module)
}
}
}
The preview code:
struct ModuleDetailView_Previews: PreviewProvider {
static var previews: some View {
PreviewWrapper()
}
struct PreviewWrapper: View {
#State var modules = [Subjects]()
var body: some View {
ModuleDetailView(subjectDetails: $modules)
}
}
}
both textfields are visible at the same time. What happens is that after I press enter from the first textField the cursor just vanishes, which works differently from when we press TAB - simply moves to the next. And I want it to work similar to how it behaves when TAB is pressed. Therefore, in this case using a firstResponder might not be a good option
Subjects struct:
struct Subjects: Identifiable {
let id = UUID()
var name: String
var grade: String
}

Resources