SwiftUI: Group data by name and display in expanded list - swiftui

I tried to group the data get from API and display it in a expanding list like How to create expanding lists – SwiftUI
Here is some sample data .
[
{
"name": "Hair Cut",
"createDate": 1600423060,
"voucherID": 12311,
"type": "service",
"expiryDate": 1600682257,
"trxnID": 2683125
},
{
"name": "Hair Cut",
"createDate": 1600423060,
"voucherID": 31231,
"type": "service",
"expiryDate": 1600682257,
"trxnID": 2683124
},
{
"name": "Package B",
"createDate": 1596217117,
"voucherID": 12312,
"type": "service",
"expiryDate": 0,
"trxnID": 2423309
}
]
Now I already group the data by using the dictionary Dictionary(grouping: self.vouchers, by: {$0.name!}) into #Published var gVouchers = Dictionary<String, Any>() . So the problem now is how I display them in a expanding list. The expanding list use a children which I don't have in dictionary. So is there any method to do so? Or I cannot use the dictionary to do the grouping?

I have followed the tutorial(YouTube link), can you please try below code?(as I'm unable to check the as I haven't upgraded the Xcode). In below code, I'm just parsing the JSON file stored locally with your data.
struct ListItem: Decodable, Identifiable {
let id = UUID()
var name: String
var createDate: TimeInterval?
var voucherID: Int?
var type: String?
var expiryDate: TimeInterval?
var trxnID: Int?
var items: [ListItem]?
init(name: String, items: [ListItem]) {
self.name = name
self.items = items
}
}
struct ContentView: View {
#State private var items: [ListItem] = []
var body: some View {
List(items, children: \.items) { item in
Text(item.name)
}.onAppear {
self.parseJSON()
}
}
func parseJSON() {
guard let filePath = Bundle.main.url(forResource: "demo", withExtension: "json") else {
items = []
return
}
do {
let data = try Data(contentsOf: filePath)
let results = try JSONDecoder().decode([ListItem].self, from: data)
let dict = Dictionary(grouping: results, by: { $0.name })
self.items = dict.map { (key, value) -> ListItem in
ListItem(name: key, items: value)
}
} catch {
print(error)
}
}
}

Related

Conditional use in view for SwiftUI

I have a model with data string of name and a bool of UE. I'm trying to display item.name whenever UE is true. My issue is when whenever I run the code the data doesn't seem to read the UE. I got an error displaying the item.UE as a text view. The data that I am getting it from is from a database the item.name works without the conditional.
struct AttendingUsersView: View {
#ObservedObject var model = UserViewModel()
var body: some View {
VStack {
List (model.list) { item in
if item.UE == true {
Text(item.name)
} else {
Text("This isnt working")
}
}
DismissButton
}
}
I've tried displaying the item.UE to see what it would display but I get an error saying "No exact matches in call to initializer".
UserViewModel file
class UserViewModel: ObservableObject {
#Published var list = [Username]()
func addData(name: String, UE: Bool) {
//get a reference to the database
let db = Firestore.firestore()
// Add a new document in collection "username"
db.collection("usernames").document(UserDefaults.standard.object(forKey: "value") as! String).setData([
// MARK: Change the parameters to the users inputed choices
"name": name,
"UE": UE
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
func getData() {
//get a reference to the database
let db = Firestore.firestore()
//Read the documents at a specific path
db.collection("usernames").getDocuments { snapshot, error in
//checking for errors
if error == nil {
//no errors
if let snapshot = snapshot {
// update
DispatchQueue.main.async {
// Get all the documents and create usernames
self.list = snapshot.documents.map { d in
//Create a Username
return Username(id: d.documentID, name: d["name"] as? String ?? "", UE: (d["UE"] != nil)) //cast as a string and if not found return as a empty string
}
}
}
} else {
//Handle the error
}
}
}
}
Username model
struct Username: Identifiable {
var id: String
var name: String
var ue: Bool
}
Try this, with fixes for your ue in your getData, and
in the view display.
struct ContentView: View {
var body: some View {
AttendingUsersView()
}
}
struct AttendingUsersView: View {
#StateObject var model = UserViewModel() // <-- here
var body: some View {
VStack {
List (model.list) { item in
if item.ue { // <-- here
Text(item.name)
} else {
Text("This is working ue is false")
}
}
// DismissButton
}
}
}
class UserViewModel: ObservableObject {
// for testing
#Published var list:[Username] = [Username(id: "1", name: "item-1", ue: false),
Username(id: "2", name: "item-2", ue: true),
Username(id: "3", name: "item-3", ue: false)]
func addData(name: String, UE: Bool) {
//get a reference to the database
let db = Firestore.firestore()
// Add a new document in collection "username"
db.collection("usernames").document(UserDefaults.standard.object(forKey: "value") as! String).setData([
// MARK: Change the parameters to the users inputed choices
"name": name,
"UE": UE
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
func getData() {
//get a reference to the database
let db = Firestore.firestore()
//Read the documents at a specific path
db.collection("usernames").getDocuments { snapshot, error in
//checking for errors
if error == nil {
//no errors
if let snapshot = snapshot {
// update
DispatchQueue.main.async {
// Get all the documents and create usernames
self.list = snapshot.documents.map { d in
//Create a Username
// -- here, ue:
return Username(id: d.documentID, name: d["name"] as? String ?? "", ue: (d["UE"] != nil)) // <-- here ue:
}
}
}
} else {
//Handle the error
}
}
}
}
struct Username: Identifiable {
var id: String
var name: String
var ue: Bool
}

How to read and filter large Realm dataset in SwiftUI?

I'm storing ~100.000 dictionary entries in a realm database and would like to display them. Additionally I want to filter them by a search field. Now I'm running in a problem: The search function is really inefficient although I've tried to debounce the search.
View Model:
class DictionaryViewModel : ObservableObject {
let realm = DatabaseManager.sharedInstance
#Published var entries: Results<DictionaryEntry>?
#Published var filteredEntries: Results<DictionaryEntry>?
#Published var searchText: String = ""
#Published var isSearching: Bool = false
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(800), scheduler: RunLoop.main) // debounces the string publisher, such that it delays the process of sending request to remote server.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.filteredEntries = nil
return nil
}
return string
})
.compactMap{ $0 }
.sink { (_) in
} receiveValue: { [self] (searchField) in
filter(with: searchField)
}.store(in: &subscription)
self.fetch()
}
public func fetch(){
self.entries = DatabaseManager.sharedInstance.fetchData(type: DictionaryEntry.self).sorted(byKeyPath: "pinyin", ascending: true)
self.filteredEntries = entries
}
public func filter(with condition: String){
self.filteredEntries = self.entries?.filter("pinyin CONTAINS[cd] %#", searchText).sorted(byKeyPath: "pinyin", ascending: true)
}
In my View I'm just displaying the filteredEtries in a ScrollView
The debouncing works well for short text inputs like "hello", but when I filter for "this is a very long string" my UI freezes. I'm not sure whether something with my debounce function is wrong or the way I handle the data filtering in very inefficient.
EDIT: I've noticed that the UI freezes especially when the result is empty.
EDIT 2:
The .fetchData() function is just this here:
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = realm.objects(type)
return results
}
All realm objects have a primary key. The structure looks like this:
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var character: String = ""
#objc dynamic var pinyin: String = ""
#objc dynamic var translation: String = ""
override class func primaryKey() -> String {
return "id"
}
EDIT 3: The filtered results are displayed this way:
ScrollView{
LazyVGrid(columns: gridItems, spacing: 0){
if (dictionaryViewModel.filteredEntries != nil) {
ForEach(dictionaryViewModel.filteredEntries!){ entry in
Text("\(entry.translation)")
}
} else {
Text("No results found")
}
}

Nested Struct models not causing view to re-render SwiftUI

I have a view that listens to a Model via and ObservableObject:
class Feed : ObservableObject {
// Posts to be displayed
#Published var posts = [Posts]()
...
...
}
And the Posts model looks like:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
}
Which contains multiple other Struct models like Friends and Bars. However, when I do change a value within one of these other models, it doesn't trigger the #Published to fire, so the view isn't redrawn. For example, the Friends model looks like:
struct Friends : Hashable {
static func == (lhs: Friends, rhs: Friends) -> Bool {
return lhs.id == rhs.id
}
let name: String
let username: String
let id : String
var thumbnail : UIImage?
var profileImgURL : String?
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
but when I change the thumbnail, the views are not redrawn. But when I change something directly apart of the Posts model, like the description attribute, the view is redrawn. How am I able to have the view redraw when the underlying model values are changed?
I change the thumbnail as shown:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
}
}
}
}
But if I were to change the description doing the same thing:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
And when I do this, the view does update and change. I can see that the thumbnails are being loaded correctly, too, because I can print out the data sent, and sometimes the thumbnails are redrawn for the view correctly.
EDIT
As suggested I tried adding a mutating func to the struct:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
mutating func addInitThumbnail(img : UIImage) {
self.intializer.thumbnail = img
}
}
and then using it:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
if let thumbnailImg = UIImage(data: data) {
self.posts[indexOfPost].addInitThumbnail(img: thumbnailImg)
}
}
}
}
}
}
but it did not work either.
However, when I do:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
}
the images are loaded and set correctly...? So I think it might have something to do with UIImages directly?
I tried using mutating function and also updating value directly, both cases it worked.
UPDATED CODE (Added UIImage in new struct)
import SwiftUI
import Foundation
//Employee
struct Employee : Identifiable{
var id: String = ""
var name: String = ""
var address: Address
var userImage: UserIcon
init(name: String, id: String, address: Address, userImage: UserIcon) {
self.id = id
self.name = name
self.address = address
self.userImage = userImage
}
mutating func updateAddress(with value: Address){
address = value
}
}
//User profile image
struct UserIcon {
var profile: UIImage?
init(profile: UIImage) {
self.profile = profile
}
mutating func updateProfile(image: UIImage) {
self.profile = image
}
}
//Address
struct Address {
var houseName: String = ""
var houseNumber: String = ""
var place: String = ""
init(houseName: String, houseNumber: String, place: String) {
self.houseName = houseName
self.houseNumber = houseNumber
self.place = place
}
func getCompleteAddress() -> String{
let addressArray = [self.houseName, self.houseNumber, self.place]
return addressArray.joined(separator: ",")
}
}
//EmployeeViewModel
class EmployeeViewModel: ObservableObject {
#Published var users : [Employee] = []
func initialize() {
self.users = [Employee(name: "ABC", id: "100", address: Address(houseName: "Beautiful Villa1", houseNumber: "17ABC", place: "USA"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "XYZ", id: "101", address: Address(houseName: "Beautiful Villa2", houseNumber: "18ABC", place: "UAE"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "QWE", id: "102", address: Address(houseName: "Beautiful Villa3", houseNumber: "19ABC", place: "UK"), userImage: UserIcon(profile: UIImage(named: "discover")!))]
}
func update() { //both below cases worked
self.users[0].address.houseName = "My Villa"
//self.users[0].updateAddress(with: Address(houseName: "My Villa", houseNumber: "123", place: "London"))
self.updateImage()
}
func updateImage() {
self.users[0].userImage.updateProfile(image: UIImage(named: "home")!)
}
}
//EmployeeView
struct EmployeeView: View {
#ObservedObject var vm = EmployeeViewModel()
var body: some View {
NavigationView {
List {
ForEach(self.vm.users) { user in
VStack {
Image(uiImage: user.userImage.profile!)
Text("\(user.name) - \(user.address.getCompleteAddress())")
}
}.listRowBackground(Color.white)
}.onAppear(perform: fetch)
.navigationBarItems(trailing:
Button("Update") {
self.vm.update()
}.foregroundColor(Color.blue)
)
.navigationBarTitle("Users", displayMode: .inline)
}.accentColor(Color.init("blackTextColor"))
}
func fetch() {
self.vm.initialize()
}
}
it's been a long time but still :
1 - mutating func is not necessary.
2 - The re-rendering won't happen if you only change the nested object and not the "observed" object it self.
3 - You can play with the getters and setters as well, to pull the wanted value to change and update it back.
Considering we have a complex object such as :
struct Content{
var listOfStuff : [Any] = ["List", 2, "Of", "Stuff"]
var isTheSkyGrey : Bool = false
var doYouLikeMyMom : Bool = false
var status : UIImage? = UIImage(systemName: "paperplane")
}
Now let's wrap/nest this object into a ContentModel for the View. If, while using the #State var contentModel : ContentModel in the View, we change change one of the properties directly by accessing the nested object(like so : model.content.status = "Tchak"), it will not trigger a re-rendering because the ContentModel itself didn't change.
Understanding this, we need to trigger a tiny useless change in the ContentModel :
struct ContentModel {
private var change : Bool = false
private var content : Content = Content() {
didSet{
// this will trigger the view to re-render
change.toggle()
}
}
//the value you want to change
var status : UIImage?{
get{
contentModel.status
}
set{
contentModel.status = newValue
}
}
}
Now what's left to do is to observe the change of the content in the view.
struct ContentPouf: View {
#State var contentModel = ContentModel()
var body: some View {
Image(uiImage: contentModel.status)
.onTapGesture {
contentModel.status = UIImage(systemName: "pencil")
}
}
}
and using an ObservableObject it would be :
class ContentObservable : ObservableObject {
#Published var content : ContentModel = ContentModel()
func handleTap(){
content.status = UIImage(systemName: "pencil")
}
}
and
#StateObject var viewModel : ContentObservable = ContentObservable()
var body: some View {
Image(uiImage :viewModel.content.status)
.onTapGesture {
viewModel.handleTap()
}
}

SwiftUI parse Multilayer JSON Data from URL and build list

First an information: I am actually on the way learning swiftUI, I'm a total newbie. For my first project i decided to create a small app that loads articles from a joomla website. My API will respond to a query in the following structure:
{
"source": "my AppConnector for Joomla!",
"offset": 0,
"count": 0,
"results": [
{
"id": "8",
"title": "Article 1",
...
},
{
"id": "8",
"title": "Article 2",
...
}
]
}
In the future the API will return more complex structures but actually i'm struggling already with that one. All swiftUI examples & videos i've found are just explaining how to retreive an array of items or are to old and shows depreacet code examples (with the one-dimensional examples i have already successfully created a list view of items but that's not what i want).
I've created the following structs:
struct Welcome: Codable {
let source: String
let offset, count: Int
let results: [Result]
}
// MARK: - Result
struct Result: Codable {
let id, title, alias, introtext: String
let fulltext, state, catid, created: String
let createdBy, createdByAlias, modified, modifiedBy: String
let checkedOut, checkedOutTime, publishUp, publishDown: String
let images, urls, attribs, version: String
let ordering, metakey, metadesc, access: String
let hits, metadata, featured, language: String
let xreference, note, slug, articleID: String
let introImage, fullImage: String
}
and the following fetcher:
import Foundation
import SwiftUI
import Combine
public class ArticlesFetcher: ObservableObject {
#Published var articles = [Welcome]()
init(){
load()
}
func load() {
let url = URL(string: "https://nx-productions.ch/index.php/news")! //This is a public demo url feel free to check the jsondata (SecurityToken temporary disabled)
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([Welcome].self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
My view looks like this:
struct ContentView: View {
#ObservedObject var fetcher = ArticlesFetcher()
var body: some View {
VStack {
List(fetcher.articles.results) { article in
VStack (alignment: .leading) {
Text(article.title)
Text(article.articleId)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
What i don't understand is the view part - i am not able to point into the fields, with the example above i get compiler errors like "Value of type '[Welcome]' has no member 'results'" or "Value of type 'Int' has no member 'title'"
I think i may just not understand something aboutmy structure or how to loop through it.
Thanks for any advise.
The JSON starts with a { so it's a dictionary. And the type of articles is wrong.
Replace
#Published var articles = [Welcome]()
with
#Published var articles = [Result]()
and replace
let decodedLists = try JSONDecoder().decode([Welcome].self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists
}
with
let decodedLists = try JSONDecoder().decode(Welcome.self, from: d)
DispatchQueue.main.async {
self.articles = decodedLists.results
}
Finally but not related replace meaningless
print ("Error")
with
print(error)

How to publish changes to a single object in a object array

I have the following classes
class ListItem: Identifiable {
var id: UUID
var name: String
var description: String
var isFavorite: Bool
var debugDescription: String {
return "Name: \(self.name) | Favorite?: \(self.isFavorite)"
}
public init(name: String) {
self.name = name
id = UUID()
self.description = "Some text describing why \(self.name.lowercased()) is awesome"
self.isFavorite = false
}
}
class ListItems: ObservableObject {
#Published var items: [ListItem]
let defaultAnimals = ["Ant", "Bear", "Cat", "Dog", "Elephant",
"Fish", "Giraffe", "Hyena", "Iguana", "Jackal", "Kingfisher", "Leopard", "Monkey"]
public init(animals: [String] = []) {
let animalList: [String] = animals.count > 0 ? animals : defaultAnimals
self.items = animalList.sorted {
$0.lowercased() < $1.lowercased()
}.map {
ListItem(name: $0.firstUppercased)
}
}
}
and the following image view in ContentView
struct ContentView: View {
#ObservedObject var list: ListItems = ListItems()
var body: some View {
List(list.items) {
animal in HStack {
// ...
Image(systemName: animal.isFavorite ? "heart.fill" : "heart").foregroundColor(.pink).onTapGesture {
let index = self.list.items.firstIndex { $0.id == animal.id } ?? -1
if (index >= 0) {
self.list.items[index].isFavorite = !animal.isFavorite
self.list.items = Array(self.list.items[0...self.list.items.count-1]) // <--
}
}
// ...
}
}
}
}
Everytime, the image view is tapped, I am basically reassigning the entire array like this so that the changes can be reflected in the UI
self.list.items = Array(self.list.items[0...self.list.items.count-1])
My question: How can I refactor my code to prevent reassigning the entire object array every time some object property changes?
I am fairly new to Swift & iOS development, not sure if I am missing something basic.
Declare ListItem as an struct instead of a class, this way the view will be notified when isFavorite changes. And just a little suggestion; you can use toggle to change the value of a boolean: self.list.items[index].isFavorite.toggle()