Routinely in my various projects, I have to deal with iterating over hierarchical data. Being as common as it is, it always frustrated me that I had to write so much boilerplate code to do it.
Well thanks to Swifts ability to write custom Sequence classes, I decided to see if I could write one that would achieve this goal in a reusable fashion. Below is my result.
I decided to post this here per Jeff Atwood's [own comments on encouraging posting your own answers][1] where he says...
It is not merely OK to ask and answer your own question, it is explicitly encouraged [...] I do it all the time!
As such, I'm providing this solution here in hopes of helping others when they come to search this site.
Enjoy! :)
As stated above, I wrote a class that allows you to iterate over a hierarchical set of data, while keeping that hierarchy in order. You do this by specifying one or more root elements (either via an array or a variadic), and a closure that returns the children for a given element.
Since it's implemented as a generic, you can specify an explicit type to use if you know the hierarchy is homogenous, but if not, specify Any for the type, then in the closure, perform the logic to determine what child type it is.
In addition, the implementation, via recursion, not only returns things in the correct hierarchical order, but it also returns a level so you know how deep the items are. If you don't care about the level, simply append .map{ $0.item } when initializing the sequence to extract the items directly.
Here's the code for the custom hierarchical sequence...
struct HierarchicalSequence<TItem> : Sequence {
typealias GetChildItemsDelegate = (TItem) -> [TItem]?
init(_ rootItems:TItem..., getChildItems: #escaping GetChildItemsDelegate){
self.init(rootItems, getChildItems: getChildItems)
}
init(rootItems:[TItem], getChildItems: #escaping GetChildItemsDelegate){
self.rootItems = rootItems
self.getChildItems = getChildItems
}
let rootItems : [TItem]
let getChildItems : GetChildItemsDelegate
class Iterator : IteratorProtocol {
typealias Element = (level:Int, item:TItem)
init(level:Int, items:[TItem], getChildItems: #escaping GetChildItemsDelegate){
self.level = level
self.items = items
self.getChildItems = getChildItems
}
let level : Int
let items : [TItem]
let getChildItems : GetChildItemsDelegate
private var nextIndex = 0
var childIterator:Iterator?
func next() -> Element? {
// If there's a child iterator, use it to see if there's a 'next' item
if let childIterator = childIterator {
if let childIteratorResult = childIterator.next(){
return childIteratorResult
}
// No more children so let's clear out the iterator
self.childIterator = nil
}
if nextIndex == items.count {
return nil
}
let item = items[nextIndex]
nextIndex += 1
// Set up the child iterator for the next call to 'next' but still return 'item' from this call
if let childItems = getChildItems(item),
childItems.count > 0 {
childIterator = Iterator(
level : level + 1,
items : childItems,
getChildItems : getChildItems)
}
return (level, item)
}
}
func makeIterator() -> Iterator {
return Iterator(level: 0, items: rootItems, getChildItems: getChildItems)
}
}
Let's see an example of how to use it. First, let's start with some JSON data...
public let jsonString = """
[
{
"name" : "Section A",
"subCategories" : [
{
"name" : "Category A1",
"subCategories" : [
{ "name" : "Component A1a" },
{ "name" : "Component A1b" }
]
},
{
"name" : "Category A2",
"subCategories" : [
{ "name" : "Component A2a" },
{ "name" : "Component A2b" }
]
}
]
},
{
"name" : "Section B",
"subCategories" : [
{
"name" : "Category B1",
"subCategories" : [
{ "name" : "Component B1a" },
{ "name" : "Component B1b" }
]
},
{
"name" : "Category B2",
"subCategories" : [
{ "name" : "Component B2a" },
{ "name" : "Component B2b" }
]
}
]
}
]
"""
Here's the models and code to load that data
class Category : Codable {
let name : String
let subCategories : [Category]?
}
public let jsonData = jsonString.data(using: .utf8)!
var rootCategories = try! JSONDecoder().decode([Category].self, from: jsonData)
Here's how you use the sequence getting all the categories along with their depths...
let allCategoriesWithDepth = HierarchicalSequence(rootItems:rootCategories){ $0.subCategories }
for (depth, category) in allCategoriesWithDepth {
print("\(String(repeating: " ", count: depth * 2))\(depth): \(category.name)")
}
And finally, here's the output...
0: Section A
1: Category A1
2: Component A1a
2: Component A1b
1: Category A2
2: Component A2a
2: Component A2b
0: Section B
1: Category B1
2: Component B1a
2: Component B1b
1: Category B2
2: Component B2a
2: Component B2b
Enjoy!
Related
I want to create a nested comment section. I am using Firebase as my database. In my app I have a comment section on each post. Logged in users have the ability to comment on a post and their comments can also be commented on, creating a nested effect.
So first I display the comments that were made to the original post. What I want to do is to go through each comment and check to see if there is a comment for that comment and if there is a comment, I want it to display directly under that comment. Just like Instagram or Facebook.
Here is a JSON example of what a nested comment would look like in Firebase
{
"author" : "patient0",
"comments" : {
"comment-487" : {
"author" : "Doctor1",
"comments" : {
"comment-489" : {
"content" : "Your internal capsule in your cerebrum was affected by the stroke",
"id" : "comment-489",
"reply_to" : "comment-487",
"reply_to_type" : "comment"
},
"comment-490" : {
"author" : "Doctor2",
"content" : "Your internal capsule is closely associated with your basal ganglia structures",
"id" : "comment-490",
"reply_to" : "comment-487",
"reply_to_type" : "comment"
}
},
"content" : "I recently had a stroke",
"id" : "comment-487",
"post_id" : "post-1069",
"reply_to" : "post-1069",
"reply_to_type" : "post"
},
"comment-491" : {
"author" : "MedStudent",
"comments" : {
"c_1531642274921" : {
"content" : "Wow! I wonder what cranial nerves were affected due to the hemorrhage",
"id" : "c_1531642274921",
"post_id" : "post-1069",
"pub_time" : 1531642274922,
"reply_to" : "comment-491",
"reply_to_type" : "comment"
}
},
"content" : "The hemorrhage was by the pons and cranial nerve 3 is by the pons, maybe the patient lost the ability to accommodate their eye sight and keep their eyes open.",
"id" : "comment-491",
"num_likes" : 0,
"post_id" : "post-1069",
"reply_to" : "post-1069",
"reply_to_type" : "post"
}
},
"content" : "I have a headache",
"id" : "post-1069",
"num_comments" : 5,
"title" : "I have a headache, should I go to the hospital",
}
As of now I am able to get the inital comments to print (the comments made directly to the post)
func loadComments(){
Database.database().reference().child("main").child("posts").child(postID!).child("comments").queryOrdered(byChild: "id").observeSingleEvent(of: .value, with: { (snapshot:DataSnapshot) in
if let postsDictionary = snapshot .value as? [String: AnyObject] {
for testingkey in postsDictionary.keys {
Database.database().reference().child("main").child("posts").child(self.postID!).child("comments").child(testingkey).child("comments").queryOrdered(byChild: "post_id").observeSingleEvent(of: .value, with: { (snapshot:DataSnapshot) in
if let postsDictionary = snapshot .value as? [String: AnyObject] {
for post in postsDictionary {
}
DispatchQueue.main.async(execute: {
self.TableView.reloadData()
})
}
})
}
for post in postsDictionary {
//main comments
self.Comments.add(post.value)
}
DispatchQueue.main.async(execute: {
self.TableView.reloadData()
})
}
})
}
I just don't know how to go through each post to check to see if there is a comment associated with it. Also if there is a comment associated with the original comment, I want it to print out in a custom cell.
I'd create a class or struct for comment, with an array of comments as a property.
class Comment {
let id: String
let author: String
var content: String
var comments: [Comment]
}
Then I'd create a TopLevelComment class as a subclass of Comment
class TopLevelComment: Comment {
// Whatever special properties you want your top level comments to have
}
You can now check if a comment is replying to a post or a comment by simply using
comment is TopLevelComment
Then you should restructure your database appropriately so you can cast it to the Comment class
For your tableView, I'd use a tableview for each top level comment, maybe even a section for each.
You can create an element for comment elements .
var commentElements = [CustomStruct]()
After creating Custom Element pull the variables from Firebase and save .
if let postsDictionary = snapshot .value as? [String: AnyObject] {
guard let comment = postsDictionary["comments"] as? NSArray else { return }
for com in comment {
guard let commentObject = com as? [String:Any] else { return }
let id = commentObject["id"]
let type = commentObject["reply_to_type"]
let replyTo = commentObject["reply_to"]
let content = commentObject["content"]
let element = CustomStruct(id:id , type:type , ....)
commentElements.append(element)
}
for post in postsDictionary {
}
DispatchQueue.main.async(execute: {
self.TableView.reloadData()
})
}
After pulling all elements , you may group based on comment Id . And you can show with Section in TableView.You sort the first element "reply_to_type" : "post"
As per your question, I believe that you are having difficulty in figuring out how to parse the JSON to a format (or view model) which can be used to display your posts and comments.
You can use the following model sample (with reworks or tweaks of your own, if needed) to parse and organize your posts and it's comments.
class Post {
var author: String?
var comments: [Post] = []
var content: String?
var id: String?
var numComments: Int?
var title: String?
init(dict: [String: AnyObject]?) {
author = dict?["author"] as? String
content = dict?["content"] as? String
id = dict?["id"] as? String
numComments = dict?["num_comments"] as? Int
title = dict?["title"] as? String
if let commentsDict = dict?["comments"] as? [String: AnyObject] {
// Sort the comments based on the id which seems to be appended to the comment key.
let commentIds = commentsDict.keys.sorted()
for id in commentIds {
let comment = commentsDict[id] as? [String : AnyObject]
comments.append(Post(dict: comment))
}
}
}
}
Usage:
//
// postDict is your dictionary object corresponding to one post.
//
// Assign your post's dictionary item to this variable.
//
var postDict: [String: AnyObject]?
// "Post" object which has recursive comments within up to any number of levels.
// Comments are also using the same model object.
// If you want to use another, you can create one with the corresponding elements.
let post = Post(dict: postDict)
P.S: The JSON structure looks to be not of a unique structure. You might want to rework on this structure to make sure that your content gets mapped neatly.
I have a data structure in firebase which I am showing that data on a tableview. I am getting the data from firebase. The tableview sections are hard coded like Motivation, Success etc... What is the best way to parse this data so that, when I add a new section in firebase console, it will add that section and data for the section on the tableview, without me hard coding the sections? Any help in the right direction would be appreciated, read the firebase doc but can't seem to figure it out.
Data Structure
{
"categories" : {
"motivation" : {
"one" : {
"name" : "Bob",
"title" : "Get up stand up"
},
"two" : {
"name" : "Arsitotle",
"title" : "Great philosopher"
}
},
"success" : {
"one" : {
"name" : "Les",
"title" : "You're great"
},
"three" : {
"name" : "Bob",
"title" : "One love"
},
"two" : {
"name" : "Wayne",
"title" : "You will be great"
}
}
}
}
** Retrieving the data**
ref.child("categories/motivation").observe(.childAdded, with: {(snapshot:DataSnapshot) in
if let values = snapshot.value as? [String:String] {
self.motivationDictionary.insert(values, at: 0)
}
})
}
ref.child("categories/success").observe(.childAdded, with: {(snapshot:DataSnapshot) in
if let values = snapshot.value as? [String:String] {
self.successDictionary.insert(values, at: 0)
}
})
}
I know this isn't the best way, but it works. Kind of redundant, but I am new to firebase and databases.
So you are having a fixed number of dictionaries where each dictionary has a name (motivationDictionary, successDictionary).
Instead you could have a dictionary of dictionaries (like in your data structure), such that the top dictionary contains categories, and under each key you have a dictionary of values for that category, i.e. the new self.categories["motivation"] is the same as the old self.motivationDictionary and so on.
This should work, but it is not the best practice to just operate on raw dictionaries and strings. This approach might be typical for some other languages (like Lisps), but not the way to go for Swift.
In Swift you should define your model classes, and parse your DataSnapshot as instances of those classes. For example if you start from:
struct Item {
let name: String
let title: String
}
class Category {
let name: String = ""
var items: [String: Item] = [:]
}
class TableDataModel {
var sections: [Category] = []
}
Then inside your observe, you can fill your TableDataModel, and then reload the table from the model. This way the Swift compiler helps you more to ensure that your program is correct, and the code is somewhat clearer.
I have a number of dictionarys in my swift code that have a standard naming convention. What I am trying to do is programmatically selection which dictionary to extract the data from. As as example
var listSelectionValue = "aaaa"
let aaaalist : [Int : String ] = [1: "first value in aaaaa", 2 : "second value in aaaa"]
let bbbblist : [Int : String ] =[1: "first value in bbbb", 2 : "second value in bbbb"]
I then want to use the value in listSelectionValue for pull data from the correct dictionary. Sorry if this is exceedingly obvious, maybe I don't know right terminology to search for !!
Cheers,
Cameron
An if then else question?
var listSelectionValue = "aaaa"
let aaaalist = [Int : String ]()
if listSelectionValue == "aaaa"{
aaaalist : [Int : String ] = [1: "first value in aaaaa", 2 : "second value in aaaa"]
}
else{
let bbbblist : [Int : String ] =[1: "first value in bbbb", 2 : "second value in bbbb"]
}
Note: My issue #4417 was closed, but I didn't want to be that guy who opens another issue for the same thing.
Based on #3132, [ { "a": 1, "b": 2 }, { "a": 2 } ] doesn't compile unless you specifically type it to Array<Dynamic> or whatever type encompasses both. That's fine I guess, but inside of the build macro below, there is nowhere for me to type the array, and I get an error.
In general, I can make map literal notation work using untyped (http://try.haxe.org/#3dBf5), but I can't do that here since my types haven't been constructed yet.
macro public static function test():Array<Field> {
var fields = Context.getBuildFields();
// parse the JSON
var o = Context.parseInlineString('{ "arr": [ { "a": 1, "b": 2 }, { "a": 2 } ] }', Context.currentPos());
// ["test" => json] map literal notation
var a = [{ expr : EBinop(OpArrow, macro $v { "test" }, o), pos : Context.currentPos() }];
// creates: "public var json:StringMap<Dynamic> = ['test' => json];"
var nf:Field = {
name : "json",
doc : "docs",
meta : [],
access : [APublic],
kind : FVar(macro : haxe.ds.StringMap<Dynamic>, { expr : EArrayDecl(a), pos : Context.currentPos() } ),
pos : Context.currentPos()
};
fields.push(nf);
return fields;
// error: Arrays of mixed types...
}
Without knowing ahead of time what the structure of the json is, is there anything I can do?
You can still use untyped, by constructing an intermediate EUntyped(o) expression (more simply macro untyped $o).
Alternatively, you can traverse the parsed object and add ECheckType to Dynamic expressions to every array, generating something like to ([...]:Array<Dynamic>).
The implementation of this would look something like calling the following checkTypeArrays function with your parsed o object, before building the map literal expression.
static function checkTypeArrays(e:Expr):Expr
{
return switch (e) {
case { expr : EArrayDecl(vs), pos : pos }:
macro ($a{vs.map(checkTypeArrays)}:Array<Dynamic>);
case _:
haxe.macro.ExprTools.map(e, checkTypeArrays);
}
}
An improvement to this would be to only wrap in (:Array<Dynamic>) the arrays that fail Context.typeof(expr).
I'm trying to filter a term to be matching one of the values in an array.
relaying on the ES https://www.elastic.co/guide/en/elasticsearch/guide/current/_finding_multiple_exact_values.html
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"terms" : {
"price" : [20, 30]
}
}
}
}
}
I tried this:
val res = ESclient.execute {
search in "index" query {
filteredQuery query {
matchall
} filter {
termsFilter("category", Array(1,2))
}
}
But got an error from ES.
How can I do that?
When calling termsFilter, the method is expecting a var args invocation of Any*, so termsFilter("category", 1, 2) would work. But termsFilter("category", Array(1,2)) is treated as a single argument, since Array is a subclass of Any of course. By adding : _ * we force scala to see it as a vars arg invocation.
So this will work:
val res = ESclient.execute {
search in "index" query {
filteredQuery query {
matchall
} filter {
termsFilter("category", Array(1,2) : _ *)
}
}
Maybe the best solution of all is to update the client to be overloaded on Iterables.