List in SwiftUI won't show up - ios

I'm currently writing code for a scrollable sideways calendar that displays dates horizontally. I currently have the following code (this is a very simplified version):
struct ScrollableCalendar: View {
var body: some View {
var someArray = [["May", "10", "2020"],["May", "11", "2020"],["May", "12", "2020"]]
ScrollView(.horizontal, showsIndicators: false) {
CalendarDateHorizBase(dates: someArray)
}
}
}
struct CalendarDateHorizBase: View {
var dates: Array<Array<String>>
var body: some View {
HStack {
****THE LOGIC ERRROR OCCURS IN THIS LIST****
List(dates, id: \.description) { date in
CalendarDate(month: date[0], day: date[1], year: date[2])
}
}
}
}
*** CalendarDate() is another view that takes a month, day, and year (all strings) and displays them nicely. The error is not related to CalendarDate()***
When I attempt to hardcode the elements without the List, everything displays fine. However, when I use the List, the screen becomes completely blank. I have no idea why. Does anyone have any ideas? Thanks!

Your dates need to be a proper model as its not identifiable by List.
And also I believe you have some solid reason for using HStack, as I hope you know that list only goes vertically.

The issue is with the data. When you need to show a list of data in a List, each element of the data should be Identifiable. One way to achieve that is to define a structure for you data and make it identifiable:
struct DateEvent: Identifiable {
let id: String
let month: String
let day: String
let year: String
init(rawValue: [String]) {
month = rawValue[0]
day = rawValue[1]
year = rawValue[2]
id = rawValue.joined(separator: "/")
}
}
So now you can use it to build up your List
struct CalendarDateHorizBase: View {
var dates: [[String]] // same as 'Array<Array<String>>' but more swifty style
var body: some View {
HStack {
List(dates.map { DateEvent(rawValue: $0) }) { date in
CalendarDate(month: date.month, day: date.day, year: date.year)
}
}
}
}

Related

Binding a Stepper with a Dictionary SwiftUI

I'm not sure if this is possible but I'm currently trying to create a list of steppers using a dictionary[String: Int]. Using the stepper I'm hoping to change the qty amount in the dictionary. I tried binding the value to the stepper by first doing $basic[name] and then that didn't work and so I ended up with $basic[keyPath: name] which resulted in fewer errors but still wasn't working. In the beginning I was having problems of not wanting to change the order of the dictionary that I made, and so I ended up with the ForEach below which worked for not changing the order of dictionary, however, I'm wondering if that's one of the reasons that the binding isn't working.
import SwiftUI
struct AllSuppliesStruct {
#State var basic = ["Regular Staples": 0, "Big Staples": 0]
var body: some View {
Form {
//Basic Supplies
ForEach(basic.sorted(by: >), id: \.key) { name, qty in
Stepper("\(name), \(qty)", value: $basic[keyPath: name], in: 0...10)
}
}
}
}
Goal:
If I pressed on the stepper only once for both Regular and Big Staples then I expect this in the dictionary
basic = ["Regular Staples": 1, "Big Staples": 1]
You can manually create a Binding that acts on basic and pass that to Stepper:
struct AllSuppliesStruct: View {
#State var basic = ["Regular Staples": 0, "Big Staples": 0]
var body: some View {
Form {
ForEach(basic.sorted(by: >), id: \.key) { name, qty in
Stepper(
"\(name), \(qty)",
value: .init(
get: { basic[name]! },
set: { basic[name] = $0 }
),
in: 0...10
)
}
}
}
}

Dynamically create section for list in SwiftUI

I am trying to dynamically create sections for List with a header in SwiftUI.
here is my array:
var lists = [a list of names with A to Z] // array of strings
then I try to get first letter:
var firstCharacters: [Character] {
var firstCharacters = [Character]()
for list in lists.sorted(by: {$0 < $1}) {
if let character = list.first, !firstCharacters.contains(character) {
firstCharacters.append(character)
}
}
return firstCharacters
}
I created the list like this:
List {
ForEach(firstCharacters, id: \.self) { charachter in
Section(header: Text("\(charachter.description)")) {
ForEach(Array(lists.enumerated()), id:\.element) { index, element in
Text("Name \(element), Id: \(index)")
})
}
}
}
}
Now I have a problem adding section to the list now sure how can I combine with the list.
A better way to section this is to take your list of words and turn it into a dictionary keyed by the first letter like this:
var wordDict: [String:[String]] {
let letters = Set(lists.compactMap( { $0.first } ))
var dict: [String:[String]] = [:]
for letter in letters {
dict[String(letter)] = lists.filter( { $0.first == letter } ).sorted()
}
return dict
}
Then use the Dict in your List like this:
List {
// You then make an array of keys, sorted by lowest to highest
ForEach(Array(wordDict.keys).sorted(by: <), id: \.self) { character in
Section(header: Text("\(character)")) {
// Because a dictionary lookup can return nil, we need to provide a response
// if it fails. I used [""], though I could have just force unwrapped.
ForEach(wordDict[character] ?? [""], id: \.self) { word in
Text("Name \(word)")
}
}
}
}
This prevents you from having to iterate through the whole list for every letter. It is done once when creating the Dict. You no longer need var firstChar.
The following would print the names in each section:
struct ContactsView: View {
let names = ["James", "Steve", "Anna", "Baxter", "Greg", "Zendy", "Astro", "Jenny"]
var firstChar: [Character] {
let array = names
.compactMap({ $0.first })
// Set is just a quick way to remove duplicates.
return Array(Set(array))
.sorted(by: { $0 < $1 })
}
var body: some View {
List {
ForEach(firstChar, id: \.self) { char in
Section(header: Text("\(char.description)")) {
ForEach(names, id: \.self) { name in
if name.first == char {
Text(name)
}
}
}
}
}
}
}
Although a better way might be to not have 2 different arrays but merge them into one where the first letter is the key and the values are the names.
That way you don't have to iterate over every name for every section.
My recommendation is a view model which groups an array of strings into a Section struct with Dictionary(grouping:by:)
class ViewModel : ObservableObject {
struct Section: Identifiable {
let letter : String
let names : [String]
var id : String { letter }
}
#Published var sections = [Section]()
var names = [String]() {
didSet {
let grouped = Dictionary(grouping: names, by: {$0.prefix(1)})
sections = grouped.keys.sorted().map{Section(letter: String($0), names: grouped[$0]!)}
}
}
}
Whenever the content of the array is being modified the section array (and the view) is updated.
In the view in onAppear pass some names
struct ContentView: View {
#StateObject private var model = ViewModel()
var body: some View {
List(model.sections) { section in
Section(section.letter) {
ForEach(section.names, id: \.self, content: Text.init)
}
}
.onAppear {
model.names = ["Aaran", "Aaren", "Aarez", "Badsha", "Bailee", "Bailey", "Bailie", "Bailley", "Carlos", "Carrich", "Carrick", "Carson", "Carter", "Carwyn", "Dante", "Danyal", "Danyil", "Danys", "Daood", "Dara", "Darach", "Daragh", "Darcy", "D'arcy", "Dareh", "Eisa", "Eli", "Elias", "Elijah", "Eliot", "Elisau", "Finn", "Finnan", "Finnean", "Finnen", "Finnlay", "Geoff", "Geoffrey", "Geomer", "Geordan", "Hamad", "Hamid", "Hamish", "Hamza", "Hamzah", "Han", "Idris", "Iestyn", "Ieuan", "Igor", "Ihtisham", "Jarno", "Jarred", "Jarvi", "Jasey-Jay", "Jasim", "Jaskaran", "Jason", "Jasper", "Jaxon", "Kabeer", "Kabir", "Kacey", "Kacper", "Kade", "Kaden", "Kadin", "Kadyn", "Kaeden", "Kael", "Kaelan", "Kaelin", "Kaelum", "Kai", "Kaid", "Kaidan", "Kaiden", "Kaidinn", "Kaidyn", "Kaileb", "Kailin", "Karsyn", "Karthikeya", "Kasey", "Kash", "Kashif", "Kasim", "Kasper", "Kasra", "Kavin", "Kayam", "Leiten", "Leithen", "Leland", "Lenin", "Lennan", "Lennen", "Lennex", "Lennon", "Lennox", "Lenny", "Leno", "Lenon", "Lenyn", "Leo", "Leon", "Leonard", "Leonardas", "Leonardo", "Lepeng", "Leroy", "Leven", "Levi", "Levon", "Machlan", "Maciej", "Mack", "Mackenzie", "Mackenzy", "Mackie", "Macsen", "Macy", "Madaki", "Nickson", "Nicky", "Nico", "Nicodemus", "Nicol", "Nicolae", "Nicolas", "Nidhish", "Nihaal", "Nihal", "Nikash", "Olaoluwapolorimi", "Ole", "Olie", "Oliver", "Olivier", "Peter", "Phani", "Philip", "Philippos", "Phinehas", "Phoenix", "Phoevos", "Pierce", "Pierre-Antoine", "Pieter", "Pietro", "Piotr", "Porter", "Prabhjoit", "Prabodhan", "Praise", "Pranav", "Rasul", "Raul", "Raunaq", "Ravin", "Ray", "Rayaan", "Rayan", "Rayane", "Rayden", "Rayhan", "Santiago", "Santino", "Satveer", "Saul", "Saunders", "Savin", "Sayad", "Sayeed", "Sayf", "Scot", "Scott", "Scott-Alexander", "Seaan", "Seamas", "Seamus", "Sean", "Seane", "Sean-James", "Sean-Paul", "Sean-Ray", "Seb", "Sebastian", "Sebastien", "Selasi", "Seonaidh", "Sephiroth", "Sergei", "Sergio", "Seth", "Sethu", "Seumas", "Shaarvin", "Shadow", "Shae", "Shahmir", "Shai", "Shane", "Shannon", "Sharland", "Sharoz", "Shaughn", "Shaun", "Tadhg", "Taegan", "Taegen", "Tai", "Tait", "Uilleam", "Umair", "Umar", "Umer", "Umut", "Urban", "Uri", "Usman", "Uzair", "Uzayr", "Valen", "Valentin", "Valentino", "Valery", "Valo", "Vasyl", "Vedantsinh", "Veeran", "Victor", "Victory", "Vinay", "Vince", "Wen", "Wesley", "Wesley-Scott", "Wiktor", "Wilkie", "Will", "William", "William-John", "Willum", "Wilson", "Windsor", "Wojciech", "Woyenbrakemi", "Wyatt", "Wylie", "Wynn", "Xabier", "Xander", "Xavier", "Xiao", "Xida", "Xin", "Xue", "Yadgor", "Yago", "Yahya", "Yakup", "Yang", "Yanick", "Yann", "Yannick", "Yaseen", "Yasin", "Yasir", "Yassin", "Yoji", "Yong", "Yoolgeun", "Yorgos", "Youcef", "Yousif", "Youssef", "Yu", "Yuanyu", "Yuri", "Yusef", "Yusuf", "Yves", "Zaaine", "Zaak", "Zac", "Zach", "Zachariah", "Zacharias", "Ziyaan", "Zohaib", "Zohair", "Zoubaeir", "Zubair", "Zubayr", "Zuriel"]
}
}
}

Converting Array into Object

I have a list that's organized like : ["ImageUrl","Year","Credit","ImageUrl","Year","Credit"...] and I want to display a horizontal scrollview of the images with the year/credit below. I've tried using a while loop within SwiftUI like this but I receive an error stating the Closure containing control flow statement cannot be used with function builder 'ViewBuilder'.
Here's my code:
struct ImageList : View {
var listOfImages : Array<String>
#State var i = 0
var body: some View{
VStack{
while i < listOfImages.count {
VStack(){
KFImage(listOfImages[i]).resizable().frame(width: 200, height: 300).aspectRatio(contentMode: .fit)
Text(listOfImages[i+1])
Text(listOfImages[i+2])
}
i = i+3
}
}
}
}
I am unable to update how the list's are organized because it's already coming from our backend. My initial plan was to import the list elements into a list of objects like this:
struct HistoricalImages: Hashable {
let link : String
let year : String
let credit : String
}
but i'm not sure how to convert it effectively. Any help is appreciated. This is my first StackOverflow post so please let me know if anything needs to be added!
Use an index range to slice the array to groups of 3 elements and create an object of each slice
var index = 0
var items = [HistoricalImages]()
while index < array.count {
let end = index + 2
if end > array.count {
break
}
let slice = Array(array[index...end])
items.append(HistoricalImages(link: slice[0], year: slice[1], credit: slice[2]))
index += 3
}
You can't use a while loop within the body property:
while i < listOfImages.count {
...
}
You need to use a ForEach instead:
ForEach(0..<listOfImages.count) { idx in
...
}

Issue using ScrollView with Array

I'm trying to use an array to display some game info whenever an array gets updated by a call to a web service. The array is populating fine, but at runtime I get:
Generic struct 'ForEachWithIndex' requires that 'Binding<[MatchData]>'
conform to 'RandomAccessCollection'
My ScrollView:
ScrollView{
ForEach(GameCenterHelper.helper.$MatchList, id: \.self) {row in
ext("\(row.id)")
}
}
My declaration of the array:
#State var MatchList: [MatchData] = [MatchData]()
And MatchData:
class MatchData : Identifiable {
var id: String
var LocalPlayer: Player
var RemotePlayer: Player
init(_ match: GKTurnBasedMatch) {
let local = match.participants[0].player!
let remote = match.participants[1].player ?? GKPlayer()
self.id = match.matchID
LocalPlayer = Player(Alias: local.alias
, DisplayName: local.displayName
, TeamPlayerId: local.teamPlayerID
, PlayerId: local.gamePlayerID
)
RemotePlayer = Player(Alias: remote.alias
, DisplayName: remote.displayName
, TeamPlayerId: remote.teamPlayerID
, PlayerId: remote.gamePlayerID
)
}
}
I have to admit, this is the first time I've used state to try and refresh a ScrollView, but I am finding it much harder than I expected and I am not sure what I have gotten wrong in my code.
You don't need binding to iterate MatchList in ForEach, access it directly
ScrollView{
ForEach(GameCenterHelper.helper.MatchList, id: \.self) {row in // << no $
ext("\(row.id)")
}
}

Why is swift unable to render views with array[index+1] in ForEach loop?

I am getting an error related to accessing an item in an array using the provided Index in a ForEach loop with SwiftUI.
I have an array of information that is used to pass information to a struct to render a card. I need two of these cards per HStack, so I loop over the array and call the cards like so:
ForEach(0..<array.count){item in
Card(name: array[item].name)
Card(name: array[item+1].name)
}
But this throws the error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
What I'm trying to accomplish is a bunch of Horizontal stacks, with 2 items each, in a single VStack. This way i have a list of 2 side by side cards. This seemed like a simple way to just brute force that behavior, but I keep running into the error.
I am probably just going to switch to a different style of Hstack that will wrap to next line for every 3rd added the row, but I'd still like to know if anyone can tell me why this error occurs. It appears to be such a simple operation, yet it can't be done by the compiler
Here is the actual code I'm running, if the sample above doesn't cut it. The strangest thing about this code is that it only fails after the SECOND item +1. I can run this if i only do it once in the code.
ForEach(0..<self.decks.count){item in
HStack(spacing: 30){
if(item+1 < self.decks.count){
StudyCards(cardTitle: self.decks[item].deckTitle, cardAmt: self.decks[item].stackAmount, lastStdy: self.decks[item].lastStudied)
StudyCards(cardTitle: self.decks[item+1].deckTitle, cardAmt: self.decks[item+1].stackAmount, lastStdy: self.decks[item+1].lastStudied)
}
Spacer()
.padding(.bottom, 4)
} else{
StudyCards(cardTitle: self.decks[item].deckTitle, cardAmt: self.decks[item].stackAmount, lastStdy: self.decks[item].lastStudied)
}
}
}
I replicated your error, as you said, it only happens when you have more than one "item+1" inside ForEach, like this:
Text(self.array[item + 1] + " - " + self.array[item + 1])
So I think the problem is not related to your specific Views. One solution is to create a function that increments item and returns your view:
struct ContentView: View {
#State var array: [String] = ["zero","one","two", "three"];
var body: some View {
VStack {
ForEach(0..<array.count) { item in
Text(self.array[item])
if item + 1 < self.array.count {
self.makeView(item: item)
}
}
}
}
func makeView(item: Int) -> Text {
let nextItem = item + 1
return Text(self.array[nextItem] + " - " + self.array[nextItem])
}
}
Reference: swiftui-increment-variable-in-a-foreach-loop
Alright so I got a solution!
I used this implementation of a view builder here: (https://www.hackingwithswift.com/quick-start/swiftui/how-to-position-views-in-a-grid)
struct GridStack<Content: View>: View {
let rows: Int
let columns: Int
let content: (Int, Int) -> Content
var body: some View {
VStack(spacing: 30){
ForEach(0 ..< rows, id: \.self) { row in
HStack(spacing: 30){
ForEach(0 ..< self.columns, id: \.self) { column in
self.content(row, column)
}
}
}
}
}
init(rows: Int, columns: Int, #ViewBuilder content: #escaping (Int, Int) -> Content) {
self.rows = rows
self.columns = columns
self.content = content
}
}
And then i call it like so below. I had to add the if statement because without it, it was rendering an extra card. Now i have successfully implemented a grid with 2 columns, and any amount of rows! No need for 3rd party libs!
GridStack(rows: self.rows, columns: 2){ row, col in
if(row*2 + col < self.count){
StudyCards(cardTitle: self.decks[row*2 + col].deckTitle, cardAmt: self.decks[row*2 + col].stackAmount, lastStdy: self.decks[row*2 + col].lastStudied)
}

Resources