How to connect AnyView with String name? - ios

I want to make my first app - it will be my resume(cv). Here is what I created in my mind:
I want to have multiple buttons connected with many views. all displayed in grid with 2 columns
inside of each view Ii will have different things, like education info, gallery, and later - my portfolio apps.
I want to join somehow views with titles - how can I do it better without json?
I did it like this: views and titles are done separately. Is there any better way to do this without json?
One important thing: I want to have more than 10 buttons, so I want to do it with arrays instead of placing just buttons separately.
import SwiftUI
struct ContentView: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible())
]
let buttonsViews: [AnyView] = [
AnyView(AboutView()),
AnyView(EducationView()),
AnyView(GalleryView())
]
let titles: [String] = [
"About",
"Education",
"Gallery"
]
var body: some View {
NavigationStack{
LazyVGrid(columns: columns) {
ForEach(buttonsViews.indices, id: \.self) { ind in
NavigationLink("\(titles[ind])") {
buttonsViews[ind]
}
.frame(height: 50)
.frame(minWidth: 100)
.foregroundColor(.red)
.padding()
.background(Color.black)
.cornerRadius(5)
}
}
}
}
}

If you're using AnyView, you're probably doing it wrong. Try an array of enums representing each view (about, education, gallery, etc), then ForEach on the array, switch on their enum, and return the relevant View.
enum ButtonType: Identifiable, CaseIterable {
var id: Self { self }
case about, education, gallery
var title: String {
switch self {
case .about: return "About"
case .education: return "Education"
case .gallery: return "Gallery"
}
}
}
struct ContentView: View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
NavigationStack{
LazyVGrid(columns: columns) {
ForEach(ButtonType.allCases) { type in
NavigationLink(type.title) {
switch type {
case .about:
Text("AboutView")
case .education:
Text("EducationView")
case .gallery:
Text("GalleryViewView")
}
}
.frame(height: 50)
.frame(minWidth: 100)
.foregroundColor(.red)
.padding()
.background(Color.black)
.cornerRadius(5)
}
}
}
}
}

Related

SwiftUI ForEach printing duplicate items

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.

Two extra views put inside of a ForEach with a filtering search bar

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 {
...

SwiftUI - Prevent Sections from flying/zooming to the right in List when dynamically filtering them

I originally asked this question:
SwiftUI - Dynamic List filtering animation flies to right side when data source is empty
There, I had a List without sections. I was filtering them so that it only showed the rows that contained the text inside a TextField. The solution was to wrap everything inside the List in a Section.
Unfortunately, I now need to filter Sections. Here's my code:
struct Group: Identifiable {
let id = UUID() /// required for the List
var groupName = ""
var people = [Person]()
}
struct Person: Identifiable {
let id = UUID() /// required for the List
var name = ""
}
struct ContentView: View {
#State var searchText = ""
var groups = [
Group(groupName: "A People", people: [
Person(name: "Alex"),
Person(name: "Ally"),
Person(name: "Allie")
]),
Group(groupName: "B People", people: [
Person(name: "Bob")
]),
Group(groupName: "T People", people: [
Person(name: "Tim"),
Person(name: "Timothy")
])
]
var body: some View {
VStack {
TextField("Search here", text: $searchText) /// text field
.padding()
List {
ForEach(
/// Filter the groups for those that contain searchText
groups.filter { group in
searchText.isEmpty || group.groupName.localizedStandardContains(searchText)
}
) { group in
Section(header: Text(group.groupName)) {
ForEach(group.people) { person in
Text(person.name)
}
}
}
}
.animation(.default) /// apply the animation
}
}
}
Result:
I pass in a filtered array in the ForEach to determine the Sections. However, whenever that array changes, the List animates really weirdly. The Sections zoom/fly to the right side, and come back from the left when the array includes them again. How can I avoid this animation?
If I remove .animation(.default), it doesn't animate at all, as expected. But, I would still like an animation. Is there a way to fade the changes, or slide them instead?
The solution is not using List. As long as you're not using selection and row deleting a ScrollView is basically the same.
If you want to style it a bit like the List that's also not that hard:
struct SearchAnimationExample: View {
...
var body: some View {
VStack {
TextField("Search here", text: $searchText) /// text field
.padding()
ScrollView {
VStack(spacing: 0) {
ForEach(
groups.filter { group in
searchText.isEmpty || group.groupName.localizedStandardContains(searchText)
}
) { group in
Section(header: header(title: group.groupName)) {
ForEach(group.people) { person in
row(for: person)
Divider()
}
}
}.transition(.opacity) // Set which transition you would like
// Always full width
HStack { Spacer() }
}
}
.animation(.default)
}
}
func header(title: String) -> some View {
HStack {
Text(title).font(.headline)
Spacer()
}
.padding(.horizontal)
.background(Color.gray.opacity(0.4))
}
func row(for person: Person) -> some View {
HStack {
Text(person.name)
Spacer()
}.padding()
}
}
Looks practically the same as the default list:

SwiftUI static List weird reuse behavior

I'm facing a strange behavior using a static List in SwiftUI. I can't determine if it's a SwiftUI bug or something I'm doing wrong. I have a very simple List that looks like this :
var body: some View {
List {
SettingsPickerView<TrigonometryUnit>(title: "Trigonometry Units", selection: $viewModel.trigonometryUnitIndex, items: TrigonometryUnit.allCases)
SettingsPickerView<DecimalSeparator>(title: "Decimal Separator", selection: $viewModel.decimalSeparatorIndex, items: DecimalSeparator.allCases)
SettingsPickerView<GroupingSeparator>(title: "Grouping Separator", selection: $viewModel.groupingSeparatorIndex, items: GroupingSeparator.allCases)
SettingsPickerView<ExponentSymbol>(title: "Exponent Symbol", selection: $viewModel.exponentSymbolIndex, items: ExponentSymbol.allCases)
}
}
Each cell of the List looks like this :
struct SettingsPickerView<T: Segmentable>: View {
let title: String
#Binding var selection: Int
let items: [T]
var body: some View {
Section(header: Text(title)) {
ForEach(items.indices) { index in
self.cell(for: self.items[index], index: index)
}
}
}
private func cell(for item: T, index: Int) -> some View {
print(title, item.title, items.map({ $0.title }))
return Button(action: {
self.selection = index
}, label: {
HStack {
Text(item.title)
Spacer()
if index == self.selection {
Image(systemName: "checkmark")
.font(.headline)
.foregroundColor(.rpnCalculatorOrange)
}
}
})
}
}
And finally, this is what a Segmentable object looks like:
enum GroupingSeparator: Int, CaseIterable {
case defaultSeparator
case space
case comma
}
extension GroupingSeparator: Segmentable {
var id: String {
switch self {
case .defaultSeparator:
return "groupingSeparator.default"
case .space:
return "groupingSeparator.space"
case .comma:
return "groupingSeparator.comma"
}
}
var title: String {
switch self {
case .defaultSeparator:
return "Default"
case .space:
return "Space"
case .comma:
return "Comma"
}
}
}
When the SettingsView is loaded. everything looks fine. But as soon as I start scrolling, and some other cells are instantiated, there are some cell displayed, but not the proper ones. Here is some screenshots and logs.
When the view is loaded, no scrolling, here is what the screen looks like:
But, what I got on the console is pretty weird and doesn't follow the order of the SettingsPickerView written in the main View:
Trigonometry Units Radians ["Radians", "Degrees"] <-- Fine
Trigonometry Units Degrees ["Radians", "Degrees"] <-- Fine
Decimal Separator Default ["Default", "Dot", "Comma"] <-- Fine
Decimal Separator Default ["Default", "Dot", "Comma"] <-- Fine
Trigonometry Units Degrees ["Radians", "Degrees"] <-- Not expected. Should be Grouping Separator
Trigonometry Units Radians ["Radians", "Degrees"] <-- Not expected. Should be Grouping Separator
The second section is ok and properly displayed:
But the third section is completely broken:
The third section displays its title properly, but display some of the data of the first section. I tried to add an identifier to the button in the cell because the issue looks like SwiftUI can't identify the proper data. But adding an identifier to the button broke the binding, and the checkbox don't change anymore.
private func cell(for item: T, index: Int) -> some View {
print(title, item.title, items.map({ $0.title }))
return Button(action: {
self.selection = index
}, label: {
HStack {
Text(item.title)
Spacer()
if index == self.selection {
Image(systemName: "checkmark")
.font(.headline)
.foregroundColor(.rpnCalculatorOrange)
}
}
})
.id(UUID().uuidString) // This solve the display issue but broke the binding.
}
Does someone experienced something like this before ?
Thanks in advance for your help.
Here is fixed block of code (due to used indexes only List is confused and reuses rows, so solution is to make rows identifiable by items).
Tested with Xcode 11.4
struct PickerView<T: Segmentable>: View {
// ... other code here
var body: some View {
Section(header: Text(title)) {
// Corrected section construction !!
ForEach(Array(items.enumerated()), id: \.element.id) { index, _ in
self.cell(for: self.items[index], index: index)
}
}
}
// ... other code here

SwiftUI & CoreData - ordered list

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.

Resources