I am trying to make an ordered list in SwiftUI using CoreData records.
How to print running numbers in such list?
In the following example I have one entity named SomeEntity with a String attribute named title.
import SwiftUI
struct ContentView: View {
var fetchRequest: FetchRequest<SomeEntity>
var items: FetchedResults<SomeEntity> { fetchRequest.wrappedValue }
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) {item in
NavigationLink(destination: ItemDetailsView(item: item)) {
HStack {
Text("99")
// How to print running number instead of "99" in this ordered list of CoreData records?
// I was thinking about something like this:
// Text(items.id) - but this doesn't work. Is there something similar?
.multilineTextAlignment(.center)
.frame(width: 60)
Text(item.title!)
}
}
}
}
}
}
}
Probably you need something like the following
struct ContentView: View {
var fetchRequest: FetchRequest<SomeEntity>
var items: FetchedResults<SomeEntity> { fetchRequest.wrappedValue }
var body: some View {
NavigationView {
List {
ForEach(Array(items.enumerated()), id: \.element) {(i, item) in
NavigationLink(destination: ItemDetailsView(item: item)) {
HStack {
Text("\(i + 1)")
.multilineTextAlignment(.center)
.frame(width: 60)
Text(item.title!)
}
}
}
}
}
}
}
Based on your comments, this should work: You need to use a different init of ForEach, which takes a Range<Int> as first argument:
ForEach(-items.count..<0, id: \.self) { i in
NavigationLink(destination: ItemDetailsView(item: items[-i])) {
HStack {
Text("\(items[-i].itemName)")
.multiLineTextAlignment(.center)
.frame(width: 60)
Text("\(items[-i].title!)")
}
}
}
Going from -items.count to 0 also ensures the reversed order.
I've tested it and with #FetchRequest this solution seems to be the best.
List {
ForEach(self.contacts.indices, id: \.self) { i in
Button(action: {
self.selectedId = self.contacts[i].id!
}) {
ContactRow(contact: self.contacts[i])
.equatable()
.background(Color.white.opacity(0.01))
}
.buttonStyle(ListButtonStyle())
.frame(height: 64)
.listRowBackground( (i%2 == 0) ? Color("DarkRowBackground") : .white)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
I've tested also solution with Array(self.contacts.enumerated()) but it doesn't work as well. If you there is small number of records it can be ok but for large number of records it is suboptimal.
If you use
request.fetchBatchSize = 10
to load records (entities) in batches while scrolling the list enumerated() doesn't work executes all needed SELECT ... LIMIT 10 requests at once
.indices makes it possible to fetch additional items while scrolling.
Related
In my application I am creating various arrays of Identifiable structs using my API response. I am iterating over said array to build lists within the Content and Sidebar columns of my Navigation Split View. If I print the array before my ForEach call, the array is normal. When printing each item from within the ForEach (let _ = print(item)) the item prints twice. However the item is only added to the List once. Is this normal behavior? It appears to be happening with all of my ForEach calls. Visually the view looks correct, just want to be sure there isn’t any additional looping or view updated occurring.
Printing each item of array. Resulting in duplicate prints.
//
// TeamView.swift
// myDashboard
//
// Created by nl492k on 10/18/22.
//
import SwiftUI
struct TeamView: View {
var user: loggedInUser
var viewData = apiData()
// viewData is an instance of the apiData struct that includes 2 Arrays of identifieable structs ("gauges" & "trends") and a "team" struct that containts an array of idenfifiable structs "teamMembers" viewData is a singular object that is updated by the completion handler of my API call.
// struct apiData {
// var gauges : Array<gaugeObj>
// var trends : Array<trendObj>
// var team : teamObj
//
// init(gauges : Array<gaugeObj> = Array<gaugeObj>(), trends: Array<trendObj> = Array<trendObj>(), team: teamObj = teamObj()) {
// self.gauges = gauges
// self.trends = trends
// self.team = team
// }
// }
#Binding var uid_selection: String?
var emulation_uid: String
var body: some View {
if viewData.team.attuid == "" {
Label("Not Signed In", systemImage: "person.crop.circle.fill.badge.questionmark")
}
else {
List(selection: $uid_selection){
HStack {
NavigationLink(value: viewData.team.superv) {
AsyncImage(url: URL(string: "\(userImageUrl)\(viewData.team.attuid)")) { image in
image.resizable()
.clipShape(Circle())
.shadow(radius: 10)
.overlay(Circle().stroke(Color.gray, lineWidth: 2))
} placeholder: {
ProgressView()
}
.frame(width:30, height: 35)
VStack (alignment: .leading){
Text("\(viewData.team.fName) \(viewData.team.lName)")
Text("\(viewData.team.jobTitle)")
.font(.system(size: 10, weight: .thin))
}
}
Label("", systemImage:"arrow.up.and.person.rectangle.portrait")
}
Divider()
//------ This prints the Array of identifiable structs, as expected, with no issues --------
let _ = print(viewData.team.teamMembers)
ForEach(viewData.team.teamMembers) { employee in
//----- This prints multiple times per employee in array ------.
let _ = print(employee)
NavigationLink(value: employee.attuid) {
AsyncImage(url: URL(string: "\(userImageUrl)\(employee.attuid)")) { image in
image.resizable()
.clipShape(Circle())
.shadow(radius: 10)
.overlay(Circle().stroke(Color.gray, lineWidth: 2))
} placeholder: {
ProgressView()
}
.frame(width:30, height: 35)
VStack (alignment: .leading){
Text("\(employee.fName) \(employee.lName)")
Text("\(employee.jobTitle)")
.font(.system(size: 10, weight: .thin))
}
}
}
}
.background(Color("ContentColumn"))
.scrollContentBackground(.hidden)
}
}
}
struct TeamView_Previews: PreviewProvider {
static var previews: some View {
TeamView(user: loggedInUser.shared,
viewData: apiData(gauges:gaugesTest,
trends: trendsTest,
team: teamTest),
uid_selection: .constant(loggedInUser.shared.attuid),
emulation_uid: "")
}
}
From your code snippet, it looks like you're doing a print in the body of the ForEach, and seeing multiple prints per item.
Actually, this is completely normal behaviour because SwiftUI may render a view multiple times (which will cause your print statement to be called each time). There is no need to worry about such rerenders (unless you're debugging performance issues). SwiftUI's rendering heuristics isn't known to the public, and may sometimes choose to make multiple rendering passes even though no state variables have changed.
So I have a ScrollView that contains a list of all the contacts imported from a user's phone. Above the ScrollView, I have a 'filter search bar' that has a binding that causes the list to show only contacts where the name contains the same string as the search bar filter. For some reason, the last two contacts in the list always pop up at the bottom of the list, no matter what the string is (even if it's a string not contained in any of the contact names on the phone). I tried deleting a contact and the problem persists, because the original contact was just replaced with the new second to last contact. Any help fixing this would be much appreciated!
struct SomeView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: CustomContact = []
var body: some View {
var filteredContactsCount = 0
if friendsFilterText.count != 0 {
for contact in appState.savedContacts {
if contact.name.lowercased().contains(friendsFilterText.lowercased()) {
filteredContactsCount += 1
}
}
} else {
filteredContactsCount = savedContacts.count
}
return HStack {
Image(systemName: "magnifyingglass")
ZStack {
HStack {
Text("Type a name...")
.opacity(friendsFilterText.count > 0 ? 0 : 1)
Spacer()
}
CocoaTextField("", text: $friendsFilterText)
.background(Color.clear)
}
Button(action: {
friendsFilterText = ""
}, label: {
Image(systemName: "multiply.circle.fill")
})
}.frame(height: 38)
HStack(spacing: 10) {
Text("Your contacts (\(filteredContactsCount))")
Spacer()
Button(action: {
fetchContacts()
}, label: {
Image(systemName: "square.and.arrow.down")
})
Button(action: {
// edit button action
}, label: {
Text("Edit")
})
}
ScrollView {
VStack {
ForEach(savedContacts, id: \.self.name) { contact in
if contact.name.lowercased().contains(friendsFilterText.lowercased()) || friendsFilterText.count == 0 {
Button(action: {
// contact button action
}, label: {
HStack(spacing: 20) {
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 41))
.frame(width: 41, height: 41)
VStack(alignment: .leading, spacing: 4) {
Text(contact.name)
Text(contact.phoneNumber)
}
Spacer()
}.frame(height: 67)
})
}
}
}
}
}
}
CustomContact is a custom struct with properties phoneNumber and name. I've attached images below of the issue I'm experiencing. I'm thinking MAYBE it's because there's something off timing-wise with the friendsFilterText and the ForEach rendering but I'm really not sure.
In the image set below, the 'Extra Contact 1' and 'Extra Contact 2' are ALWAYS rendered, unless I add a filter, then switch to a different view, then back to this view (which leads me to believe it's a timing thing again).
https://imgur.com/a/CJW2CUS
You should move the count calculation out of the view into a computed var.
And if CustomContact is your single contact struct, it should actually read #State var savedContacts: [CustomContact] = [] i.e. an array of CustomContact.
The rest worked fine with me, no extra contacts showing.
struct ContentView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: [CustomContact] = []
// computed var
var filteredContactsCount: Int {
if friendsFilterText.isEmpty { return savedContacts.count }
return savedContacts.filter({ $0.name.lowercased().contains(friendsFilterText.lowercased()) }).count
}
var body: some View {
...
It's very hard to explain without a recording from a second device that I don't have, but when I try to slide my slider, it will stop when my finger is definitely still moving.
I have my code posted below. I'd be happy to answer any questions and explain whatever. I'm sure it's something really simple that I should know. Any help would be very much appreciated, thanks!
import SwiftUI
class SettingsViewModel: ObservableObject {
#Published var selectedTips = [
10.0,
15.0,
18.0,
20.0,
25.0
]
func addTip() {
selectedTips.append(0.0)
selectedTips.sort()
}
func removeTip(index: Int) {
selectedTips.remove(at: index)
selectedTips = selectedTips.compactMap{ $0 }
}
}
struct SettingsTipsView: View {
#StateObject var model = SettingsViewModel()
var body: some View {
List {
HStack {
Text("Edit Suggested Tips")
.font(.title2)
.fontWeight(.semibold)
Spacer()
if(model.selectedTips.count < 5) {
Button(action: { model.addTip() }, label: {
Image(systemName: "plus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
ForEach(model.selectedTips, id: \.self) { tip in
let i = model.selectedTips.firstIndex(of: tip)!
//If I don't have this debug line here then the LAST slider in the list tries to force the value to 1 constantly, even if I remove the last one, the new last slider does the same. It's from a separate file but it's pretty much the same as the array above. An explanation would be great.
Text("\(CalculatorViewModel.suggestedTips[i])")
HStack {
Text("\(tip, specifier: "%.0f")%")
Slider(value: $model.selectedTips[i], in: 1...99, label: { Text("Label") })
if(model.selectedTips.count > 1) {
Button(action: { model.removeTip(index: i) }, label: {
Image(systemName: "minus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
}
}
Using id: \.self within a List or ForEach is a dangerous idea in SwiftUI. The system uses it to identify what it expects to be unique elements. But, as soon as you move the slider, you have a change of ending up with a tip value that is equal to another value in the list. Then, SwiftUI gets confused about which element is which.
To fix this, you can use items with truly unique IDs. You should also try to avoid using indexes to refer to certain items in the list. I've used list bindings to avoid that issue.
struct Tip : Identifiable {
var id = UUID()
var tip : Double
}
class SettingsViewModel: ObservableObject {
#Published var selectedTips : [Tip] = [
.init(tip:10.0),
.init(tip:15.0),
.init(tip:18.0),
.init(tip:20.0),
.init(tip:25.0)
]
func addTip() {
selectedTips.append(.init(tip:0.0))
selectedTips = selectedTips.sorted(by: { a, b in
a.tip < b.tip
})
}
func removeTip(id: UUID) {
selectedTips = selectedTips.filter { $0.id != id }
}
}
struct SettingsTipsView: View {
#StateObject var model = SettingsViewModel()
var body: some View {
List {
HStack {
Text("Edit Suggested Tips")
.font(.title2)
.fontWeight(.semibold)
Spacer()
if(model.selectedTips.count < 5) {
Button(action: { model.addTip() }, label: {
Image(systemName: "plus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
ForEach($model.selectedTips, id: \.id) { $tip in
HStack {
Text("\(tip.tip, specifier: "%.0f")%")
.frame(width: 50) //Otherwise, the width changes while moving the slider. You could get fancier and try to use alignment guides for a more robust solution
Slider(value: $tip.tip, in: 1...99, label: { Text("Label") })
if(model.selectedTips.count > 1) {
Button(action: { model.removeTip(id: tip.id) }, label: {
Image(systemName: "minus.circle.fill")
.renderingMode(.original)
.font(.title3)
.padding(.horizontal, 10)
})
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
}
}
I'm trying to build a ForEach that is looping through an array of objects. Everything is working fine, but I cannot figure out how to add a Divider between the elements.
The layout for the rows is in a separate view, and I have tried adding a Divider to the row, which is causing the end of the list to look pretty bad, because of the Divider below the last item.
I cannot use a List, because it is not the only view on the page. Everything is inside a ScrollView.
For reference, here is the code as well as the UI so far.
This is the code of the List view:
VStack {
ForEach (manufacturers) { manufacturer in
NavigationLink(destination: Text("test")) {
Row(manufacturer: manufacturer)
}
}
}
.background(Color("ListBackground"))
.cornerRadius(12)
This is the code of the Row view:
VStack {
HStack {
Text(manufacturer.name)
.font(.system(size: 18, weight: .regular))
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
}
.padding()
.buttonStyle(PlainButtonStyle())
Is there a way to add a Divider between every item in the ForEach loop, or am I able to remove the Divider from the last item?
I'm happy about every help I can get.
Here is a possible approach
ForEach (manufacturers) { manufacturer in
VStack {
NavigationLink(destination: Text("test")) {
Row(manufacturer: manufacturer)
}
// I don't known your manufacturer type so below condition might
// require fix during when you adapt it, but idea should be clear
if manufacturer.id != manufacturers.last?.id {
Divider()
}
}
}
You can remove last line with compare count.
struct Row: View {
var manufacturer: String
var isLast: Bool = false
var body: some View {
VStack {
HStack {
Text(manufacturer)
.font(.system(size: 18, weight: .regular))
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
if !isLast {
Divider()
}
}
.padding()
.buttonStyle(PlainButtonStyle())
}
}
struct ContentView5: View {
private var manufacturers = ["1", "2", "3"]
var body: some View {
VStack {
ForEach (manufacturers.indices, id: \.self) { idx in
NavigationLink(destination: Text("test")) {
Row(manufacturer: manufacturers[idx], isLast: idx == manufacturers.count - 1)
}
}
}
.background(Color("ListBackground"))
.cornerRadius(12)
}
}
Or you can remove last var from Row.
VStack {
ForEach (manufacturers.indices, id: \.self) { idx in
NavigationLink(destination: Text("test")) {
Row(manufacturer: manufacturers[idx])
}
if idx != manufacturers.count - 1 {
Divider()
}
}
}
You could use a generic "DividedForEach" View which inserts a divider after every element except the last.
struct DividedForEach<Data: RandomAccessCollection, ID: Hashable, Content: View, D: View>: View {
let data: Data
let id: KeyPath<Data.Element, ID>
let content: (Data.Element) -> Content
let divider: (() -> D)
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (Data.Element) -> Content, divider: #escaping () -> D) {
self.data = data
self.id = id
self.content = content
self.divider = divider
}
var body: some View {
ForEach(data, id: id) { element in
content(element)
if element[keyPath: id] != data.last![keyPath: id] {
divider()
}
}
}
}
I have a line of code that sets the background of Text to an Image that is fetched by finding the first three letters of the string. For some reason this won't run and keeps giving me the error above. Any ideas on how I can fix this?
There are a lot of images that need to be set as the backgrounds for multiple different pieces of text. I believe I have the right idea by using the prefix of the string, but it seems like Xcode is having difficulty/won't run this.
Pretty sure this specific line is giving me issues, but would love some feedback.
.background(Image(colorOption.prefix(3)).resizable())
import SwiftUI
struct ColorView: View {
// #ObservedObject var survey = Survey()
#ObservedObject var api = ColorAPIRequest(survey: DataStore.instance.currentSurvey!)
#State var showingConfirmation = true
#State var showingColorView = false
#State var tempSelection = ""
#EnvironmentObject var survey: Survey
//#EnvironmentObject var api: APIRequest
var colorOptionsGrid: [[String]] {
var result: [[String]] = [[]]
let optionsPerRow = 4
api.colorOptions.dropFirst().forEach { colorOption in
if result.last!.count == optionsPerRow { result.append([]) }
result[result.count - 1].append(colorOption)
}
return result
}
var body: some View {
VStack {
Text("Select Tape Color")
.font(.system(size:70))
.bold()
.padding(.top, 20)
NavigationLink("", destination: LengthView(), isActive: $showingColorView)
HStack {
List {
ForEach(colorOptionsGrid, id: \.self) { colorOptionRow in
HStack {
ForEach(colorOptionRow, id: \.self) { colorOption in
Button(action: {
// self.survey.length = lengthOption
self.tempSelection = colorOption
self.showingConfirmation = false
}
) {
ZStack {
Color.clear
Text(colorOption.prefix(3))
.font(.title)
.foregroundColor(self.tempSelection == colorOption ? Color.white : Color.black)
.frame(width: 200, height: 100)
.background(Image(colorOption.prefix(3)).resizable())
//Image(colorOption.prefix(3)).resizable()
}
}.listRowBackground(self.tempSelection == colorOption ? Color.pink : Color.white)
.multilineTextAlignment(.center)
}
}
}
}.buttonStyle(PlainButtonStyle())
}
Button(action: {
self.survey.color = self.tempSelection
self.showingColorView = true
self.showingConfirmation = true
}) {
Text("Press to confirm \(tempSelection)")
.bold()
.padding(50)
.background(Color.pink)
.foregroundColor(.white)
.font(.system(size:40))
.cornerRadius(90)
}.isHidden(showingConfirmation)
.padding(.bottom, 50)
}
}
}
The compiler actually gives a fairly decent suggestion when it tells you to break the expression up. The simplest you can do is extract the background image into a separate function like this:
func backgroundImage(for colorOption: String) -> some View {
Image(String(colorOption.prefix(3))).resizable()
}
and then replace the call to
.background(Image(colorOption.prefix(3)).resizable())
with
.background(self.backgroundImage(for: colorOption))
Also note that I wrapped colorOption.prefix(3) in a String constructor, simply because .prefix(_:) returns a Substring, but the Image(_:) constructor requires a String.