How to make 'Published<[String]>.Publisher' conform to 'RandomAccessCollection' - xcode

I am trying to make a view that updates based on if the user toggles the favorite button or not. I want the entire view to reconstruct in order to display an array of values whenever that array of values is changed. Inside the view, a for each loop should display every value in the array.
The view that I want to update every time savedArray is changed is FavView. But when I try to use a foreach loop to display every value is savedArray(which I created as a #Published so the view would reconstruct), it gives me the error Generic struct 'ForEach' requires that 'Published<[String]>.Publisher' conform to 'RandomAccessCollection'. I am confused because I thought that String arrays were able to be used in for each loops. Is this not true? How do I loop through a #Published array? Thank you!
This is my code for the savedArray(in ViewModel) and the FavView I want to display it in with the for each.
struct ContentView: View {
#StateObject private var statNavManager = StatsNavigationManager()
#State private var saved: [String] = []
var body: some View {
TabView {
StatsView(saved: $saved)
.tabItem {
Label("Home", systemImage: "house")
}
FavView(saved: $saved)
.tabItem {
Label("Saved", systemImage: "bookmark")
}
}
.environmentObject(statNavManager)
}
}
final class ViewModel: ObservableObject {
#Published var items = [Item]()
#Published var showingFavs = true
#Published var savedItems: Set<String> = []
#Published var savedArray: [String]
// Filter saved items
var filteredItems: [String] {
//return self.items
return savedArray
}
var db = Database()
init() {
self.savedItems = db.load()
self.items = db.returnList()//the items
self.savedArray = Array(db.load())
print("savedarray", savedArray)
print("important!", self.savedItems, self.items)
}
func contains(_ item: Item) -> Bool {
savedItems.contains(item.id)
}
// Toggle saved items
func toggleFav(item: Item) {
print("Toggled!", item)
if contains(item) {
savedItems.remove(item.id)
if let index = savedArray.firstIndex(of: item.id) {
savedArray.remove(at: index)
}
} else {
savedItems.insert(item.id)
savedArray.append(item.id)
}
db.save(items: savedItems)
}
}
struct FavView: View {
#StateObject private var vm = ViewModel()
var body: some View {
VStack {
List {
var x = print("testing",vm.savedArray)//this only prints once at the start
ForEach($vm.savedArray, id: \.self) { string in
let item = vm.db.returnItem(input: string.wrappedValue)
HStack {
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.description)
.font(.subheadline)
}
Spacer()
Image(systemName: vm.contains(item) ? "bookmark.fill" : "bookmark")
.foregroundColor(.blue)
.onTapGesture {
vm.toggleFav(item: item)
}
}
}
}
.cornerRadius(10)
}
}
}

in ForEach, you are using $ symbol to access savedArray you have to use the vm itself
struct FavView: View {
#StateObject private var vm = ViewModel()
var body: some View {
VStack {
List {
ForEach($vm.savedArray, id: \.self) { string in //< here $vm.savedArray not vm.$savedArray
let item = vm.db.returnItem(input: string)
HStack {
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.description)
.font(.subheadline)
}
Spacer()
Image(systemName: vm.contains(item) ? "bookmark.fill" : "bookmark")
.foregroundColor(.blue)
.onTapGesture {
vm.toggleFav(item: item)
}
}
}
}
.cornerRadius(10)
}
}
}
this should work.

Related

Select the first item on a list and highlight it when item changes

I have a simple notes app that uses a list on the left and a texteditor on the right. The list has the titles of the notes, and the texteditor its text. When the user changes the text on the note, the list gets sorted to display the most current note (by date) on top, as its first item.
Started with Ventura, if I'm working on a note other than the first one, then that note (the item in the list) jumps to the top when the text is changed, however, if the first item in the list not visible (I'm working on a note that is way down), then when I change the text the item, it jumps to the top, but you don't jump with it. You are now in this state where you have to scroll up to get to the top and reselect the first item.
I tried using DispatchQueue.main.async to force to reselect the item onchange, but regardless of what I try, it doesn't scroll to the top, even when the selected note id is the correct one.
I ran out of ideas or things to try. How can I go back to the first item once the text is changed?
Here's the 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] = []
}
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
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")
}
}
}
.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
}
}
}
}
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
data.sortList()
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

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?

why is a passed parameter displaying the previous content of an EnvironmentObject

this is a Macos app where the parsclass is setup in a previous view that contains the YardageRowView below. That previous view is responsible for changing the contents of the parsclass. This is working is other views that use a NavigationLink to display the views.
When the parsclass is changed, this view is refreshed, but the previous value is put in the text field on the holeValueTestView.
I cannot comprehend how the value is not being passed into the holeValueTestView correctly
This is a view shown as a .sheet, and if I dismiss it and display it again, everything is fine.
if you create a macOS project called YardageSample and replace the ContentView.swift and YardageSampleApp.swift with the two files below, you can see that the display in red changes and the black numbers do not change until you click Done and redisplay the .sheet
//
// YardageSampleApp.swift
// YardageSample
//
// Created by Brian Quick on 2021-04-12.
//
import SwiftUI
#main
struct YardageSampleApp: App {
#StateObject var parsclass = parsClass()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(parsclass)
}
}
}
//
// ContentView.swift
// YardageSample
//
// Created by Brian Quick on 2021-04-12.
//
import SwiftUI
struct ContentView: View {
#StateObject var parsclass = parsClass()
enum ActiveSheet : String , Identifiable {
case CourseMaintenance
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil
var body: some View {
Button(action: {
self.activeSheet = .CourseMaintenance
}) {
Text("Course Maintenance")
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .CourseMaintenance:
CourseMaintenance()
}
}.frame(width: 200, height: 200, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
}
}
class parsClass: ObservableObject {
#Published var pars = [parsRec]()
init() {
self.pars = [parsRec]()
self.pars.append(parsRec())
}
func create(newpars: [parsRec]) {
pars.removeAll()
pars = newpars
}
}
class parsRec: Identifiable, Codable {
var id = UUID()
var Hole = 1
var Yardage = 1
}
struct CourseMaintenance: View {
#EnvironmentObject var parsclass: parsClass
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button(action: {presentationMode.wrappedValue.dismiss()}, label: {
Text("Done")
})
Button(action: {switchScores(number: 1)}, label: {
Text("Button 1")
})
Button(action: {switchScores(number: 2)}, label: {
Text("Button 2")
})
Button(action: {switchScores(number: 3)}, label: {
Text("Button 3")
})
CourseDetail().environmentObject(parsclass)
}.frame(width: 400, height: 400, alignment: .center)
}
func switchScores(number: Int) {
var newparRecs = [parsRec]()
for i in 0..<17 {
let myrec = parsRec()
myrec.Hole = i
myrec.Yardage = number
newparRecs.append(myrec)
}
parsclass.create(newpars: newparRecs)
}
}
struct CourseDetail: View {
#EnvironmentObject var parsclass: parsClass
var body: some View {
HStack(spacing: 0) {
ForEach(parsclass.pars.indices, id: \.self) { indice in
// this displays the previous value
holeValueTestView(value: String(parsclass.pars[indice].Yardage))
// this displays the correct value after parsclass has changed
Text(String(parsclass.pars[indice].Yardage))
.foregroundColor(.red)
}
}
}
}
struct holeValueTestView: View {
#State var value: String
var body: some View {
//TextField(String(value), text: $value)
Text(value)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There are a couple of issues going on:
You have multiple instances of parsClass. One is defined in YardageSampleApp and passed into the view hierarchy as a #EnvironmentObject. The second is defined in ContentView as a #StateObject. Make sure you're only using one.
On holeValueTestView, you defined value as a #State variable. That gets set initially when the view is created by its parent and then it maintains its own state. So, when the environmentObject changed, because it was in charge of its own state at this point, it didn't update the value. You can simply remove #State and see the behavior that you want.
struct ContentView: View {
#EnvironmentObject var parsclass : parsClass //<-- Here
enum ActiveSheet : String , Identifiable {
case CourseMaintenance
var id: String {
return self.rawValue
}
}
#State var activeSheet : ActiveSheet? = nil
var body: some View {
Button(action: {
self.activeSheet = .CourseMaintenance
}) {
Text("Course Maintenance")
}
.sheet(item: $activeSheet) { sheet in
switch sheet {
case .CourseMaintenance:
CourseMaintenance()
}
}.frame(width: 200, height: 200, alignment: .center)
}
}
struct holeValueTestView: View {
var value: String //<-- Here
var body: some View {
Text(value)
}
}
As a side note:
In Swift, normally type names are capitalized. If you want to write idiomatic Swift, you would change your parsClass to ParsClass for example.

SwiftUI on macOS: list with detail view and multiple selection

TL;DR:
I cannot have a list with a detail view and multiple selections on macOS.
In more detail:
For demonstration purposes of my issue, I made a small example project. The UI looks as follows:
This is the "app" when launched, with a list on top and a detail representation below. Because I am using the List's initialiser init(_:selection:rowContent:), where selection is of type Binding<SelectionValue?>? according to Apple's documentation, I get selecting items with the keyboard arrow keys for free.
Here's the complete code:
import SwiftUI
#main
struct UseCurorsInLisstApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ViewModel())
}
}
}
class ViewModel: ObservableObject {
#Published var items = [Item(), Item(), Item(), Item(), Item()]
#Published var selectedItem: Item? = nil
}
struct Item: Identifiable, Hashable {
let id = UUID()
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
VStack {
List(vm.items, id: \.self, selection: $vm.selectedItem) { item in
VStack {
Text("Item \(item.id.uuidString)")
Divider()
}
}
Divider()
Group {
if let item = vm.selectedItem {
Text("Detail item \(item.id.uuidString)")
} else {
Text("No selection…")
}
}
.frame(minHeight: 200.0, maxHeight: .infinity)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Now, having had success with this so far, I figured being able to select more than one row would be useful, so I took a closer look into List(_:selection:rowContent:), where selection is of type Binding<Set<SelectionValue>>?. To be able to have a detail view, I just made a few minor changes to
the ViewModel:
class ViewModel: ObservableObject {
#Published var items = [Item(), Item(), Item(), Item(), Item()]
#Published var selectedItem: Item? = nil
#Published var selectedItems: Set<Item>? = nil {
didSet {
if selectedItems?.count == 1, let item = selectedItems?.first {
selectedItem = item
}
}
}
}
and the ContentView:
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
VStack {
List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
VStack {
Text("Item \(item.id.uuidString)")
Divider()
}
}
Divider()
Group {
if vm.selectedItems?.count == 1, let item = vm.selectedItems?.first {
Text("Detail item \(item.id.uuidString)")
} else {
Text("No or multiple selection…")
}
}
.frame(minHeight: 200.0, maxHeight: .infinity)
}
}
}
The problem now is that I cannot select an item of the row any more, neither by clicking, nor by arrow keys. Is this a limitation I am running into or am I "holding it wrong"?
Use the button and insert it into the set. Keyboard selection also works with shift + (up/down arrow)
class ViewModel: ObservableObject {
#Published var items = [Item(), Item(), Item(), Item(), Item()]
#Published var selectedItem: Item? = nil
#Published var selectedItems: Set<Item> = []
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
VStack {
List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
Button {
vm.selectedItem = item
vm.selectedItems.insert(item)
} label: {
VStack {
Text("Item \(item.id.uuidString)")
Divider()
}
}
.buttonStyle(PlainButtonStyle())
}
Divider()
Group {
if let item = vm.selectedItem {
Text("Detail item \(item.id.uuidString)")
} else {
Text("No or multiple selection…")
}
}
.frame(minHeight: 200.0, maxHeight: .infinity)
}
}
}
Add remove:
Button {
vm.selectedItem = item
if vm.selectedItems.contains(item) {
vm.selectedItems.remove(item)
} else {
vm.selectedItems.insert(item)
}
}
Edit
In simple need to give a blank default value to set. because in nil it will never append to set need initialization.
#Published var selectedItems: Set<Item> = [] {
Actually my error was pretty dumb – making the selectedItems-set optional prevents the list from working correctly. Shoutout to #Raja Kishan, who pushed me into the right direction with his proposal.
Here's the complete working code:
import SwiftUI
#main
struct UseCurorsInLisstApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ViewModel())
}
}
}
class ViewModel: ObservableObject {
#Published var items = [Item(), Item(), Item(), Item(), Item()]
#Published var selectedItems = Set<Item>()
}
struct Item: Identifiable, Hashable {
let id = UUID()
}
struct ContentView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
VStack {
List(vm.items, id: \.self, selection: $vm.selectedItems) { item in
VStack {
Text("Item \(item.id.uuidString)")
Divider()
}
}
Divider()
Group {
if vm.selectedItems.count == 1, let item = vm.selectedItems.first {
Text("Detail item \(item.id.uuidString)")
} else {
Text("No or multiple selection…")
}
}
.frame(minHeight: 200.0, maxHeight: .infinity)
}
}
}

How to access value from an item in ForEach list

How to access values from particular item on the list made with ForEach?
As you can see I was trying something like this (and many other options):
Text(item[2].onOff ? "On" : "Off")
I wanted to check the value of toggle of 2nd list item (for example) and update text on the screen saying if it's on or off.
And I know that it's something to do with #Binding and I was searching examples of this and trying few things, but I cannot make it to work. Maybe it is a beginner question. I would appreciate if someone could help me.
My ContentView:
struct ContentView: View {
// #Binding var onOff : Bool
#State private var onOff = false
#State private var test = false
var body: some View {
NavigationView {
List {
HStack {
Text("Is 2nd item on or off? ")
Text(onOff ? "On" : "Off")
// Text(item[2].onOff ? "On" : "Off")
}
ForEach((1...15), id: \.self) {item in
ListItemView()
}
}
.navigationBarTitle(Text("List"))
}
}
}
And ListItemView:
import SwiftUI
struct ListItemView: View {
#State private var onOff : Bool = false
// #Binding var onOff : Bool
var body: some View {
HStack {
Text("99")
.font(.title)
Text("List item")
Spacer()
Toggle(isOn: self.$onOff) {
Text("Label")
}
.labelsHidden()
}
}
}
I don't know what exactly you would like to achieve, but I made you a working example:
struct ListItemView: View {
#ObservedObject var model: ListItemModel
var body: some View {
HStack {
Text("99")
.font(.title)
Text("List item")
Spacer()
Toggle(isOn: self.$model.switchedOnOff) {
Text("Label")
}
.labelsHidden()
}
}
}
class ListItemModel: ObservableObject {
#Published var switchedOnOff: Bool = false
}
struct ContentView: View {
#State private var onOff = false
#State private var test = false
#State private var list = [
(id: 0, model: ListItemModel()),
(id: 1, model: ListItemModel()),
(id: 2, model: ListItemModel()),
(id: 3, model: ListItemModel()),
(id: 4, model: ListItemModel())
]
var body: some View {
NavigationView {
List {
HStack {
Text("Is 2nd item on or off? ")
Text(onOff ? "On" : "Off")
// Text(item[2].onOff ? "On" : "Off")
}
ForEach(self.list, id: \.id) {item in
ListItemView(model: item.model)
}
}
.navigationBarTitle(Text("List"))
}.onReceive(self.list[1].model.$switchedOnOff, perform: { switchedOnOff_second_item in
self.onOff = switchedOnOff_second_item
})
}
}
The #Published basically creates a Publisher, which the UI can listen to per onReceive().
Play around with this and you will figure out what these things do!
Good luck :)
import SwiftUI
struct ContentView: View {
#State private var onOffList = Array(repeating: true, count: 15)
var body: some View {
NavigationView {
List {
HStack {
Text("Is 2nd item on or off? ")
Text(onOffList[1] ? "On" : "Off")
}
ForEach((onOffList.indices), id: \.self) {idx in
ListItemView(onOff: self.$onOffList[idx])
}
}
.navigationBarTitle(Text("List"))
}
}
}
struct ListItemView: View {
#Binding var onOff : Bool
var body: some View {
HStack {
Text("99")
.font(.title)
Text("List item")
Spacer()
Toggle(isOn: $onOff) {
Text("Label")
}
.labelsHidden()
}
}
}
I understand that you are directing me to use ObservableObject. And probably it's the best way to go with final product. But I am still thinking about #Binding as I just need to pass values better between 2 views only. Maybe I still don't understand binding, but I came to this solution.
struct ContentView: View {
// #Binding var onOff : Bool
#State private var onOff = false
// #State private var test = false
var body: some View {
NavigationView {
List {
HStack {
Text("Is 2nd item on or off? ")
Text(onOff ? "On" : "Off")
// Text(self.item[2].$onOff ? "On" : "Off")
// Text(item[2].onOff ? "On" : "Off")
}
ForEach((1...15), id: \.self) {item in
ListItemView(onOff: self.$onOff)
}
}
.navigationBarTitle(Text("List"))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and ListItemView:
import SwiftUI
struct ListItemView: View {
// #State private var onOff : Bool = false
#Binding var onOff : Bool
var body: some View {
HStack {
Text("99")
.font(.title)
Text("List item")
Spacer()
Toggle(isOn: self.$onOff) {
Text("Label")
}
.labelsHidden()
}
}
}
What is happening now is text is being updated after I tap toggle. But I have 2 problems:
tapping 1 toggle changes all of them. I think it's because of this line:
ListItemView(onOff: self.$onOff)
I still cannot access value of just one row. In my understanding ForEach((1...15), id: .self) make each row have their own id, but I don't know how to access it later on.

Resources