SwiftUI List with sections using dictionary of array - ios

I have an array of users returned. I want to group them by created date and then display them in SwiftUI List with sections. The section title is the date. After I group the users I end up with a Dictionary of user arrays and the key is the date that group all users that have been created in the same date I want to use the key as a section and the value (user array) as List rows. It seems that List only works with array. Any clean solution to prepare the data for the List so it can display the sections and its content ?
import SwiftUI
import Combine
struct ContentView: View {
#EnvironmentObject var interactor: Interactor
var body: some View {
List(interactor.users) { user in // Error: Cannot convert value of type '[String : [User]]' to expected argument type 'Range<Int>'
}
.onAppear {
interactor.loadUsers()
}
}
}
class Interactor: ObservableObject {
#Published private(set) var users = [String: [User]]()
func loadUsers() {
let allUsers = [User(id: "1", name: "Joe", createdAt: Date()),
User(id: "2", name: "Jak", createdAt: Date())]
users = Dictionary(grouping: allUsers) { user in
let dateFormatted = DateFormatter()
dateFormatted.dateStyle = .short
return dateFormatted.string(from: user.createdAt)
}
}
}
struct User: Identifiable {
let id: String
let name: String
let createdAt: Date
}

You can use sections inside List, and make use of its header parameter to pass Dates. I have already fetched all dates as [String] by using separate function. One thing you need to figure is the way you are going to pass multiple keys to fetch data for different date keys that you will have. May be you can create that first by using [Users] objects and save all keys.
Below is the solution to your issue-:
ContentView-:
//// Created by TUSHAR SHARMA on 06/02/21.
////
//
import SwiftUI
struct ContentView: View {
#EnvironmentObject var interactor: Interactor
var body: some View {
List {
ForEach(getAllDates(),id:\.self) { dates in
Section(header: Text(dates)) {
ForEach(interactor.users[dates] ?? []) { users in
Text(users.name)
}
}
}
}
}
func getAllDates() -> [String]{
let getObjects = interactor.users[Date().stringFromDate()] ?? []
var dateArray : [String] = []
for getData in getObjects{
dateArray.append(getData.createdAt.stringFromDate())
}
let unique = Array(Set(dateArray))
return unique
}
}
extension Date{
func stringFromDate() -> String{
let dateFormatted = DateFormatter()
dateFormatted.dateStyle = .short
return dateFormatted.string(from: self)
}
}
class Interactor: ObservableObject {
#Published private(set) var users = [String: [User]]()
init() {
loadUsers()
}
func loadUsers() {
let allUsers = [User(id: "1", name: "Joe", createdAt: Date()),
User(id: "2", name: "Jak", createdAt: Date())]
users = Dictionary(grouping: allUsers) { user in
user.createdAt.stringFromDate()
}
}
}
struct User: Identifiable {
let id: String
let name: String
let createdAt: Date
}
#main view
// Created by TUSHAR SHARMA on 07/01/21.
//
import SwiftUI
#main
struct WaveViewApp: App {
let interactor = Interactor()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(interactor)
}
}
}

Related

How do I create a dynamic input list?

I have some accounts that I want to get a user to input a number to provide an initial balance.
Not sure how to do the binding. Here's my model:
class DefaultBalancesViewModel: ObservableObject {
#Published var accountBalances: [Account: String] = [:]
#Published var accounts: [Account] = []
private let dbCore: DBCore
init(dbCore: DBCore = DBCore.shared) {
self.dbCore = dbCore
self.accounts = dbCore.accounts
self.accountBalances = dbCore.accounts.reduce(into: [Account: String]()) { $0[$1] = "" }
}
}
and my basic SwiftUI:
struct DefaultBalancesView: View {
#StateObject var viewModel = DefaultBalancesViewModel()
var body: some View {
ForEach(viewModel.accounts) { account in
HStack {
Text(account.name)
Spacer()
TextField("", text: $viewModel.accountBalances[account]) // <-- error here: Cannot convert value of type 'Binding<[Account : String]>.SubSequence' (aka 'Slice<Binding<Dictionary<Account, String>>>') to expected argument type 'Binding<String>' (and a couple of other errors)
}
}
Button("Save") {
//
}
}
}
How should I structure this so I can dynamically enter a number for each account?
At the moment you have multiple copies of Account arrays, and a dictionary with String! There should be only one source of truth, eg #Published var accounts: [Account], and the Account should be able to hold (or calculate) its balance.
Try this approach (works well for me), where you have only one array of [Account] (one source of truth), that you can edit
using a TextField:
Customise the code to suit your needs, e.g the currency,
or simply a "normal" decimal formatter.
class DefaultBalancesViewModel: ObservableObject {
// for testing
#Published var accounts: [Account] = [Account(name: "account-1", balance: 1.1),
Account(name: "account-2", balance: 2.2),
Account(name: "account-3", balance: 3.3)]
private let dbCore: DBCore
init(dbCore: DBCore = DBCore.shared) {
self.dbCore = dbCore
self.accounts = dbCore.accounts
}
}
struct DefaultBalancesView: View {
#StateObject var viewModel = DefaultBalancesViewModel()
var body: some View {
ForEach($viewModel.accounts) { $account in // <-- here
HStack {
Text(account.name).foregroundColor(.red)
Spacer()
TextField("", value: $account.balance, format: .currency(code: "USD")) // <-- here
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Button("Save") {
// for testing
viewModel.accounts.forEach { print("--> name: \($0.name) balance: \($0.balance)")}
}
}
}
// for testing
struct Account: Identifiable {
let id = UUID()
var name: String
var balance: Double // <-- here
}

SwiftUI: Having a plist to display

I am trying to read a plist and having it displayed as a List on SwiftUI. It compiles with no errors or warnings, but nothing gets displayed. I am not sure what I am doing wrong or the mistake I am making here. I've tried multiple things, but I still get a blank display.
import Foundation
struct PresidentModel: Decodable{
var Name: String
var Number: Int
var StartDate: String
var EndDate: String
var Nickname: String
var PoliticalParty: String
enum CodingKeys: String, CodingKey{
case Name = "Name"
case Number = "Number"
case StartDate = "Start Date"
case EndDate = "End Date"
case Nickname = "Nickname"
case PoliticalParty = "Political Party"
}
}
class PresidentViewModel: ObservableObject{
#Published var PresArray: [PresidentModel] = []
#Published var name: String = ""
#Published var number: Int = 0
#Published var startDate: String = ""
#Published var endDate: String = ""
#Published var nickname: String = ""
#Published var politicalParty: String = ""
func loadProperityListData(){
guard let path = Bundle.main.path(forResource: "presidents", ofType: "plist"), let xml = FileManager.default.contents(atPath: path) else {
fatalError("Unable to access property list states.plist")
}
do{
PresArray = try PropertyListDecoder().decode([PresidentModel].self, from: xml)
name = PresArray[0].Name
number = PresArray[0].Number
startDate = PresArray[0].StartDate
endDate = PresArray[0].EndDate
nickname = PresArray[0].Nickname
politicalParty = PresArray[0].PoliticalParty
}
catch {
fatalError("Unable to decode property list states.plist")
}
}//func
}//class
Where the plist will be displayed as a List :
import SwiftUI
struct ContentView: View {
let presidents = PresidentViewModel()
var body: some View {
List(presidents.PresArray.indices, id: \.self){ president in
Text(presidents.PresArray[].Name)
}
}//Body
}//View
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First of all please conform to the naming convention and declare the struct member names with starting lowercase letters and add an id property
struct PresidentModel: Decodable, Identifiable {
let id = UUID()
let name: String
let number: Int
let startDate: String
let endDate: String
let nickname: String
let politicalParty: String
private enum CodingKeys: String, CodingKey {
case name = "Name"
case number = "Number"
case startDate = "Start Date"
case endDate = "End Date"
case nickname = "Nickname"
case politicalParty = "Political Party"
}
}
The main problem is that you don't load the data, a good place is the init method of the observable class. The properties are not needed because the array contains all data
class PresidentViewModel: ObservableObject{
#Published var presArray: [PresidentModel] = []
init() {
loadProperityListData()
}
func loadProperityListData(){
guard let url = Bundle.main.url(forResource: "presidents", withExtension: "plist"),
let data = try? Data(contentsOf: url) else {
fatalError("Unable to access property list states.plist")
}
do {
presArray = try PropertyListDecoder().decode([PresidentModel].self, from: data)
} catch {
print(error)
presArray = []
}
}//func
}//class
The second main problem is that PresidentViewModel is not declared as #StateObject. And due to the added id property you get rid of dealing with indices and specifying id: \.self
import SwiftUI
struct ContentView: View {
#StateObject var model = PresidentViewModel()
var body: some View {
List(model.presArray) { president in
Text(president.name)
}
}//Body
}//View

Why is the data from firebase Firestore not coming into my view | Swift UI

So I have tried for a couple of hours now and it is just not working. I have my firebase Firestore setup and have a collection called leaderboard, which is supposed to store users with their score. However, when I fetch this data and display it, my screen is blank.
Firebase
LeaderBoardModel
import Foundation
import Firebase
class LeaderBoardModel: ObservableObject{
#Published var users = [LeaderBoardItem]()
func getData(){
let db = Firestore.firestore()
db.collection("leaderboard").getDocuments { snapshot, error in
if error == nil{
if let snapshot = snapshot{
DispatchQueue.main.async {
self.users = snapshot.documents.map { d in
return LeaderBoardItem(id: d.documentID, score: d["score"] as? String ?? "", name: d["name"] as? String ?? "", email: d["email"] as? String ?? "")
}
}
}
}
else{
}
}
}
}
LeaderBoardView
import SwiftUI
struct LeaderBoardView: View {
#ObservedObject var leaderBoardModel = LeaderBoardModel()
var body: some View {
List(leaderBoardModel.users) { item in
Text(item.name)
}
}
}
LeaderBoardItem
import Foundation
struct LeaderBoardItem: Identifiable{
var id: String
var score: String
var name: String
var email: String
}
try this approach, using .onAppear{...}:
struct LeaderBoardView: View {
#StateObject var leaderBoardModel = LeaderBoardModel() // <-- here
var body: some View {
List(leaderBoardModel.users) { item in
Text(item.name)
}.onAppear {
leaderBoardModel.getData() // <-- here
}
}
}

#ObservedObject does not get updated

I'm trying to update a view with a simple Observable pattern, but it doesn't happen for some reason. The Publisher gets updated, but the subscriber doesn't. I've simplified to the code below. When you click the Add button, the view doesn't get updated and also the variable.
I'm using this function (NetworkManager.shared.saveNew()), because I update after a CloudKit notification. If you know a workaround, I'd be pleased to know!
import SwiftUI
import Combine
public class NetworkManager: ObservableObject {
static let shared = NetworkManager()
#Published var list = [DataSource]()
init() {
self.list = [DataSource(id: "Hello", action: [1], link: "http://hello.com", year: 2020)]
}
func saveNew(item: DataSource) {
self.list.append(item)
}
}
struct DataSource: Identifiable, Codable {
var id: String
var action: [Int]
var link: String
var year: Int
init(id: String, action: [Int], link: String, year: Int) {
self.id = id
self.action = action
self.link = link
self.year = year
}
}
struct ContentView: View {
#ObservedObject var list = NetworkManager()
var body: some View {
VStack {
Button("Add") {
NetworkManager.shared.saveNew(item: DataSource(id: "GoodBye", action: [1], link: "http://goodbye", year: 2030))
}
List(list.list, id:\.id) { item in
Text(item.id)
}
}
}
}
It should be used same instance of NetworkManager, so here is fixed variant (and better to name manager as manager but not as list)
struct ContentView: View {
#ObservedObject var manager = NetworkManager.shared // << fix !!
var body: some View {
VStack {
Button("Add") {
self.manager.saveNew(item: DataSource(id: "GoodBye", action: [1], link: "http://goodbye", year: 2030))
}
List(manager.list, id:\.id) { item in
Text(item.id)
}
}
}
}

SwiftUI With Firebase Using Xcode 11 GM seed 2 - Swift UI Firestore

I followed this https://www.youtube.com/watch?v=_N10q1iR2HQ&t=328s tutorial; however, adjusted for some changes made to Xcode 11 GM seed 2 syntax and terms, because some stuff is obsolete now. Build is successful, but crashes for some reason.
I would appreciate any help and suggestions, tried all trial and error I could think of.
Here is the code below (pasting all of it since GM seed 2 has many command changes):
import SwiftUI
import Combine
import FirebaseFirestore
struct dataset : Identifiable {
var id = ""
var name = ""
var phone = ""
var services = ""
}
class getData : ObservableObject {
var didChange = PassthroughSubject<getData, Never>()
var data = [dataset](){
didSet{
didChange.send(self)
}
}
init(){
let db = Firestore.firestore()
let settings = db.settings
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
db.collection("venues").addSnapshotListener {(snap,err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in (snap?.documentChanges)!{
let name = i.document.data()["name"] as! String
let phone = i.document.data()["phone"] as! String
let services = i.document.data()["services"] as! String
let id = i.document.documentID
DispatchQueue.main.async {
self.data.append(dataset(id: id, name: name, phone: phone, services:services))
}
}
}
}
}
struct ContentView: View {
#ObservedObject var data1 = getData()
var body: some View {
VStack {
Text("Hello World")
List(data1.data){ i in
cellView(name: i.name, phone: i.phone, services: i.services)
}
}
}
}
struct cellView : View {
#State var name = ""
#State var phone = ""
#State var services = ""
var body : some View{
VStack{
Text(name)
Text(phone)
Text(services)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
These are the results
2019-09-19 20:30:34.724594+0600 Venue[16661:712640] - [I-ACS036002] Analytics screen reporting is enabled. Call +[FIRAnalytics setScreenName:setScreenClass:] to set the screen name or override the default screen class name. To disable screen reporting, set the flag FirebaseScreenReportingEnabled to NO (boolean) in the Info.plist
2019-09-19 20:30:34.975422+0600 Venue[16661:712648] 5.12.0 - [Firebase/Analytics][I-ACS023007] Analytics v.50300000 started
2019-09-19 20:30:34.975634+0600 Venue[16661:712648] 5.12.0 - [Firebase/Analytics][I-ACS023008] To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled
Could not cast value of type '__NSCFNumber' (0x10b8a8610) to 'NSString' (0x10954f978).
2019-09-19 20:30:35.073897+0600 Venue[16661:711704] Could not cast value of type '__NSCFNumber' (0x10b8a8610) to 'NSString' (0x10954f978).
(lldb)
As mentioned in the first comment, it looks like you have a cast issue between types.
I tried the same example and was able to build and run successfully, but no data was shown on the screen. After some investigation, I re-wrote some parts of the code and was finally able to get results.
Instead of the video's dataset, I used the following structure:
struct Profile : Identifiable {
var id: Int
var name: String
var phoneNumber: String
init(id: Int, name: String, phoneNumber: String) {
self.id = id
self.name = name
self.phoneNumber = phoneNumber
}
}
And these are the SwiftUI views:
import SwiftUI
import FirebaseFirestore
struct ContentView: View {
#State var data1 = [Profile]()
var body: some View {
return VStack {
HStack {
Text("Penya").font(.largeTitle)
}
Divider()
List(data1) { i in
cellView(name: i.name, phoneNumber: i.phoneNumber)
}
}.onAppear {
self.getData()
}
}
func getData() {
var data = [Profile]()
let db = Firestore.firestore()
db.collection("patients").addSnapshotListener { (snap, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
for doc in (snap?.documentChanges)! {
let name = doc.document.data()["name"] as! String
let phoneNumber = doc.document.data()["phoneNumber"] as! String
print (name)
print (phoneNumber)
DispatchQueue.main.async {
data.append(Profile(id: 0,
name: name,
phoneNumber: phoneNumber))
self.data1 = data
}
}
}
}
}
struct cellView : View {
#State var name = ""
#State var phoneNumber = ""
var body: some View {
VStack {
Text("Valors:")
Text(name)
Text(phoneNumber)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Whenever a record is changed in Firestore, an update is automatically performed on the simulator screen. There is only one thing I am still working on: the updated values are correctly shown in the console (I included two print()), but I always see the same original value on the simulator screen even I change the record in Firestore.
Works with Sergi's Code but he was missing the following:
let id = doc.document.documentID
DispatchQueue.main.async {
data.append(Profile(id: id,
name: name,
phoneNumber: phoneNumber))
self.data1 = data
and change all values for id to be String instead of Int.
I suggest using CombineFirebase
with pod 'FirebaseFirestoreSwift'
Then you can use pure combine pattern for Firestore in your project

Resources