I have a struct
struct Question {
var title: [String]
var additionalInfo: String?
var answers: [String]
}
and a variable to which i add the data
var questions = [
Question(title: ["What is the color of?", "additional color information"], additionalInfo: nil, answers: [
"Blue",
"Gray"
])
]
This data loads up in a tableView on AppleWatch. Two row types are separated - TitleRowType (for titles array) and AnswersRowType (for answers array).
When i insert values into struct's array - i want to the rows in the tableView be inserted with animation.
I know that there's a insertRowAtIndexes function, but i cannot wrap my head around it. The example provided in Apple's documentation doesn't work for me. That's what i came up with:
let indexSet = NSIndexSet(index: Int) // Int is passed via the function
tableView.insertRowsAtIndexes(indexSet, withRowType: "TitleRowType")
but when i run it - the table doesn't update.
Looking forward to your advices.
You have to do 3 steps:
Add the new data to your array
Insert a row into the table
Populate the row with the new data
Here is a simple example:
class InterfaceController: WKInterfaceController {
#IBOutlet var table: WKInterfaceTable!
var items = ["row1", "row2", "row3"]
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
loadTable()
}
func loadTable() {
table.setNumberOfRows(items.count, withRowType: "tableRow")
var rowIndex = 0
for item in items {
if let row = table.rowControllerAtIndex(rowIndex) as? TableRowController {
row.label.setText(item)
}
rowIndex++
}
}
#IBAction func insertRow() {
items.append("row4")
let newIndex = items.count
table.insertRowsAtIndexes(NSIndexSet(index: newIndex), withRowType: "tableRow")
if let row = table.rowControllerAtIndex(newIndex) as? TableRowController {
row.label.setText(items[newIndex])
}
}
}
TableRowController is a NSObject subclass that has one WKInterfaceLabel outlet to display the number of the row.
I used a button to trigger insertRow()
Related
I'm having a hard time trying to display/hide my collectionview cells depending on the items child nodes in FB realtime database. The problem consists of three parts: FB database, a collectionview and a segmented control. My goal is to show different cells in the collectionview depending on whether a the item has a child with a certain string value.
My database looks like this:
Items
category1
item1
name: item1
imageUrl: item1Url
logic
one
three
item2
name: item1
imageUrl: item1Url
logic
two
three
category2
item1
name: item1
imageUrl: item1Url
logic
two
four
item2
name: item1
imageUrl: item1Url
logic
one
two
I also have a custom Product class to display my items in their cells:
class Product {
var category: String?
var name: String?
var imageUrl: String?
init(rawData: [String: AnyObject]) {
name = rawData["name"] as? String
imageUrl = rawData["imageUrl"] as? String
category = rawData["category"] as? String
}
}
I load my items from firebase database with this funciton:
func loadCategoryName() {
ref = Database.database().reference().child("Items").child(selectedCategoryFromPreviousVC)
ref.observeSingleEvent(of: .value) { (snapshot) in
if let data = snapshot.value as? [String: AnyObject] {
self.itemArray = []
let rawValues = Array(data.values)
for item in rawValues {
let product = Product(rawData: item as! [String: AnyObject])
product.category = self.selectedCategoryFromPreviousVC
self.itemArray.append(product)
}
// Sort item array by rating; if rating is same, sort by name
self.itemArray.sort { (s1, s2) -> Bool in
if s1.rating == s2.rating {
return s1.name! < s2.name!
} else {
return s1.rating > s2.rating
}
}
self.collectionView?.reloadData()
}
}
}
My itemArray now contains all my items as custom Products and I can display them in their cell.
My segmented control:
func someFunc() {
let segmentController = UISegmentedControl(items: ["one", "two", "three", "four"])
segmentController.selectedSegmentIndex = 0
self.navigationItem.titleView = segmentController
segmentController.addTarget(self, action: #selector(handleSegment), for: .valueChanged)
}
#objc fileprivate func handleSegment() {
print(segmentController.selectedSegmentIndex)
}
with the handleSegment function I'm able to print out which segment has been selected. But this is where the problems occur. I've tried creating new arrays to split the items so that items are in an array depending on their "logic" child nodes. However I'm not able to make the arrays of type Product, so that I can use them to repopulate the collectionview. Also I'm not really sure what the best way of storing the logic part in my database would be.
extend your Product class:
class Product {
...
let logic: [String]?
init(...) {
...
logic = rawData["logic"] as? [String]
}
}
In your CollectionViewDataSource add some variables to store current state
var products: [Products]
var filteredProducts: [Product]
var currentFilter: String? {
didSet {
guard let currentFilter = currentFilter else {
filteredProducts = products
return
}
filteredProducts = products.filter { product in
return product.logic.contains(currentFilter)
}
}
}
extend your handleSegment func:
#objc fileprivate func handleSegment() {
print(segmentController.selectedSegmentIndex)
currentFilter = segmentController.titleForSegment(at: segmentController.selectedSegmentIndex)
collectionView.reloadData()
}
In your collectionView datasource, use filteredProducts to build the cells.
I am trying to make a comment section for my open source social media app. I have a table of posts. When you click a post in this table, it takes you to the MainViewController, where you can read the comments on those posts and post your own comment. The Post class is as follows:
import Foundation
class Post {
var id:String
var title: String
var text:String
var createdAt:Date
var comment: [String] = []
init(id: String, title: String,text:String, timestamp:Double, comment: [String] = []) {
self.id = id
self.title = title
self.text = text
self.createdAt = Date(timeIntervalSince1970: timestamp / 1000)
}
static func parse(_ key:String, data:[String:Any]) -> Post? {
if let title = data["text"] as? String,
let text = data["title"] as? String,
let timestamp = data["timestamp"] as? Double {
return Post(id: key, title: title, text: text, timestamp:timestamp, comment: [])
}
return nil
}
}
And the MainTextViewController has the following code:
import Foundation
import UIKit
import Firebase
class MainTextView: UIViewController, UITextViewDelegate{
#IBOutlet weak var titleText: UILabel!
#IBOutlet weak var mainText: UILabel!
#IBOutlet weak var commentsTable: UITableView!
#IBOutlet weak var commentPlaceHolder: UILabel!
#IBOutlet weak var newCommentLabel: UITextView!
weak var delegate:NewPostVCDelegate?
#IBAction func reply(_ sender: UIButton) {
// Firebase code here
let postRef = Database.database().reference().child("posts").childByAutoId()
let postObject = [
"comment": newCommentLabel.text,
"timestamp": [".sv": "timestamp"]
] as [String : Any]
postRef.setValue(postObject, withCompletionBlock: { error, ref in
if error == nil {
self.delegate!.didUploadPost(withID: ref.key!)
self.dismiss(animated: true, completion: nil)
} else {
// Handle error
}
})
newCommentLabel.text = String()
commentPlaceHolder.isHidden = false
}
var post: Post?
// MARK: - View Controller LifeCycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setMain()
}
override func viewDidLoad() {
super.viewDidLoad()
print(delegate!)
commentsTable.dataSource = post?.comment as? UITableViewDataSource
newCommentLabel.delegate = self
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return post!.comment.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let label = UILabel(frame: CGRect(x:0, y:0, width:200, height:50))
cell.addSubview(label)
return cell
}
// UITableViewDelegate Functions
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 50
}
private func setMain() {
guard let post = self.post else {
return
}
titleText.text = post.text
mainText.text = post.title
}
func textViewDidChange(_ commentView: UITextView) {
commentPlaceHolder.isHidden = !newCommentLabel.text.isEmpty
}
}
Here is my database structure:
{
"posts" : {
"-LhaxLOSY3UI7tDUrCA_" : {
"text" : "Nelson",
"timestamp" : 1560800488059,
"title" : "Hey\t"
},
"-LhaxbnjgDP7tdb7Eq_4" : {
"text" : "Lol",
"timestamp" : 1560800558514,
"title" : "How’s it going"
},
"comment" : {
"comment" : "Howdy"
}
}
}
I want that when I make a comment, it appends the comments array for the post with a new comment. Then everyone can see the comments in the commentsTable as a collection of strings from the array with the oldest one on top.
At the moment, creating a new comment makes a new post in Firebase with just a comment as a single string and a timestamp. How would you fix this issue so that the post button appends the table and the commentsTable shows the strings from the array? Let me know if you need me to post more details. Thank you for the help.
Without going overboard with a bunch of code, conceptually, if you want to have a series of posts, and then each post can additionally have comments, here's one option for a structure.
posts
post_id_0
text: "some text"
timestamp: "20190601"
title: "post title"
post_uid: "uid_0"
comments
comment_id_0
belongs_to_post: "post_id_0"
comment: "a comment about the post"
timestamp: "20190601"
comment_uid: "uid_49"
comment_id_1
belongs_to_post: "post_id_0"
comment: "check out that first post!"
timestamp: "20190602"
comment_uid: "uid_102"
users
uid_0
name: "Leroy"
then attach observers to posts and comments. When a new post is posted, you'll be notified of that post and can add it to your tableView datasource and refresh the tableView.
When a new comment is added, you'll be notified of that comment and add it to the comments dataSource and reload the comments tableView. To add a new post:
let thisPostRef = your_firebase.posts.childByAutoId()
thisPostRef.setValue(your_post_data)
and to add a comment
let postKey = the_post.key
let commentRef = your_firebase.comments.childByAutoId()
commentRef.setValue(your_comment_data)
and your_comment_data would include a child node 'belongs_to_post: postKey'
You can also watch for comments on certain posts, made by certain users or even query for comments by date or in a date range.
Code wise, both posts and comments nodes are created with .childByAutoId - it's best practice to disassociate node keys from the data they contain, unless it going to be static data, like a uid.
If you want to add a bit more flexibility, you could keep a child node within each posts of it's related comments as well.
posts
post_id_0
text: "some text"
timestamp: "20190601"
title: "post title"
post_uid: "uid_0"
comments:
comment_id_0: true
comment_id_1: true
but that depends on what kinds of queries you want to run.
Note: I separate the comments structure from the posts the posts node as denormalizing your data is very beneficial when running queries.
I'm trying to create tableview where the arrays are being sorted and put in there respective sections. I followed this tutorial: http://www.yudiz.com/creating-tableview-with-section-indexes/
I managed to make the first one work where the tableview arrays are sorted even though the sections without data still appear.
The second one is about solving the problem in which the sections without data still appear which did not work for me.
Upon following the second one, I could not run it because of this error
'[MyContact]' is not convertible to '[AnyObject]'
Here is my code:
Model for contact:
class MyContact: NSObject {
#objc var name: String!
#objc var mobile: String!
init(name: String, mob: String) {
self.name = name
self.mobile = mob
}
}
Extension for partitioning arrays into sorted subcategories
extension UILocalizedIndexedCollation {
func partitionObjects(array: [AnyObject], collationStringSelector: Selector) -> ([AnyObject], [String]) {
var unsortedSections = [[AnyObject]]()
for _ in self.sectionTitles {
unsortedSections.append([])
}
for item in array {
let index: Int = self.section(for: item, collationStringSelector: collationStringSelector)
unsortedSections[index].append(item)
}
var sectionTitles = [String]()
var sections = [AnyObject]()
for index in 0 ..< unsortedSections.count {
if unsortedSections[index].count > 0 {
sectionTitles.append(self.sectionTitles[index])
sections.append(self.sortedArray(from: unsortedSections[index], collationStringSelector: collationStringSelector) as AnyObject)
}
}
return (sections, sectionTitles)
}
}
Tuple for data source and the line which has the error
let (arrayContacts, arrayTitles) = collation.partitionObjects(array: self.myContacts, collationStringSelector: #selector(getter: MyContact.name)) as! [[MyContact]]
You're trying to force cast a tuple into an array of arrays.
let (arrayContacts, arrayTitles) = collation.partitionObjects(array: self.myContacts, collationStringSelector: #selector(getter: MyContact.name))
will return a tuple of type ([AnyObject], [String]).
Also, you shouldn't be using AnyObject unless you really need something to be a class type. You can re-write like this:
extension UILocalizedIndexedCollation {
func partitionObjects(array: [Any], collationStringSelector: Selector) -> ([Any], [String]) {
var unsortedSections = [[Any]](repeating: [], count: self.sectionTitles.count)
for item in array {
let index = self.section(for: item, collationStringSelector: collationStringSelector)
unsortedSections[index].append(item)
}
var sectionTitles = [String]()
var sections = [Any]()
for index in 0..<unsortedSections.count {
if unsortedSections[index].isEmpty == false {
sectionTitles.append(self.sectionTitles[index])
sections.append(self.sortedArray(from: unsortedSections[index], collationStringSelector: collationStringSelector))
}
}
return (sections, sectionTitles)
}
}
That way you could write MyContact as a struct and it would still work with this function.
Hello I have a tableviewcell where i can populate it with custom data from my pc, but i can't use my firebase data on the cell that i have made. I want to fill my cell with String and Int, not only Strings. My code is:
PlacesTableViewController Class
import UIKit
import FirebaseDatabase
class PlacesTableViewController: UITableViewController {
//MARK: Properties
#IBOutlet weak var placesTableView: UITableView!
//database reference
var dbRef:FIRDatabaseReference?
var places = [Places]()
var myList:[String] = []
//handler
var handle:FIRDatabaseHandle?
override func viewDidLoad() {
super.viewDidLoad()
dbRef = FIRDatabase.database().reference()
// Loads data to cell.
loadData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return places.count
//return myList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "PlacesTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? PlacesTableViewCell else {
fatalError("The dequeued cell is not an instance of PlacesTableView Cell.")
}
let place = places[indexPath.row]
cell.placeLabel.text = place.name
cell.ratingControl.rating = place.rating
//cell.placeLabel.text = myList[indexPath.row]
//cell.ratingControl.rating = myRatings[indexPath.row]
return cell
}
//MARK: Private Methods
private func loadData() {
handle = dbRef?.child("placeLabel").observe(.childAdded, with: { (snapshot) in
if let item = snapshot.value as? String
{
self.myList.append(item)
self.placesTableView.reloadData()
print (item)
}
})
/* handle = dbRef?.child("rating").observe(.childAdded, with: { (snapshot) in
if let item = snapshot.value as? String
{
self.myList.append(item)
self.placesTableView.reloadData()
}
})*/
/*guard let place1 = Places(name: "Veranda", rating: 4) else {
fatalError("Unable to instantiate place1")
}
places += [place1]*/
}
}
Places Class
import UIKit
class Places {
//MARK: Properties
var name: String
var rating: Int
//MARK:Types
struct PropertyKey {
static let name = "name"
static let rating = "rating"
}
//MARK: Initialization
init?(name: String, rating: Int) {
// Initialize stored properties.
self.name = name
self.rating = rating
// Initialization should fail if there is no name or if the rating is negative.
// The name must not be empty
guard !name.isEmpty else {
return nil
}
// The rating must be between 0 and 5 inclusively
guard (rating >= 0) && (rating <= 5) else {
return nil
}
}
}
PlacesTableViewCell Class
import UIKit
import FirebaseDatabase
class PlacesTableViewCell: UITableViewCell, UITableViewDelegate {
//MARK: Properties
#IBOutlet weak var placeLabel: UILabel!
#IBOutlet weak var ratingControl: RatingControl!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Firebase Database
Assuming your database layout should instead look like this (see comments above):
...
placeLabel
|
-- XXY: "Veranda"
-- YYY: "Dio Con Dio"
rating
|
-- XXX: 4
-- YYY: 1
...
then try this:
private func loadData() {
dbRef!.child("placeLabel").observe(.childAdded) {
(snapshot) in
let label = snapshot.value as! String
self.updatePlace(snapshot.key, label: label)
}
dbRef!.child("rating").observe(.childAdded) {
(snapshot) in
let rating = snapshot.value as! Int
self.updatePlace(snapshot.key, rating: rating)
}
}
private var loadedLabels = [String: String]()
private var loadedRatings = [String: Int]()
private func updatePlace(_ key: String, label: String? = nil, rating: Int? = nil) {
if let label = label {
loadedLabels[key] = label
}
if let rating = rating {
loadedRatings[key] = rating
}
guard let label = loadedLabels[key], let rating = loadedRatings[key] else {
return
}
if let place = Places(name: label, rating: rating) {
places.append(place)
placesTableView.reloadData()
}
}
By the way, you can temporarily hack your database — using Firebase (nice!) web console — if you want to quickly validate the above solution.
Writing to Database. Try the following code to write the nodes in your database (i.e., this code reuses the same key across all place properties):
let key = dbRef!.child("placeLabel").childByAutoId().key
dbRef!.child("placeLabel").child(key).setValue(placeLabel.text)
dbRef!.child("comment").child(key).setValue(commentTextField.text)
dbRef!.child("rating").child(key).setValue(ratingControl.rating)
Hacking the Database. To edit the database manually, try:
open http://console.firebase.google.com
select your app
open database option
add a new node with the right key
delete the old node
I am trying to build an app that is loading data from firebase-database.
Saving members to Firebase is working without any problems. Load members from Firebase to my UITableView is working then I am sorting the members and add respectively member under sections header according to first names first letter(A, B, C, etc as seen in iOS contact app) and this is working as well however my problem occur after I have loaded all my users and for example go to Tab 1 and then switch back to Members Tab all displayed members/cells are duplicated. If I repeat the same procedure switching tabs back and forth all cells triplicate and it goes on.
I have searched different sources for a solution but I can not find anything that is similar.
Does anyone know a solution or what I an doing wrong?
Thanks!
My Viewcontroller:
import Foundation
import UIKit
class MembersTableViewController: UITableViewController {
var FBref = FIRDatabaseReference()
var members: [Member] = []
var membersDict = [String: [String]]()
var memberSectionTitles = [String]()
// TODO: Implement user.
//var user: AdminUser!
let fakeuservariable = "fakeuser"
#IBOutlet var memberListTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
loadDataFromFirebase()
createFirstnameDict()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return memberSectionTitles.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let firstLetterKey = memberSectionTitles[section]
if let firstnameValues = membersDict[firstLetterKey] {
return firstnameValues.count
}
return 0
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return memberSectionTitles[section]
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "membercell", for: indexPath)
let firstLetterKey = memberSectionTitles[indexPath.section]
if let firstnameValues = membersDict[firstLetterKey] {
cell.textLabel?.text = firstnameValues[indexPath.row]
// Not working
//let memberDetails = members[indexPath.row]
//cell.detailTextLabel!.text = "Amount left: \(memberDetails.memberamount)"
}
return cell
}
func createFirstnameDict() {
for firstname in members {
var firstLetter = firstname.firstname
let firstnameKey = firstLetter.substring(to: firstLetter.characters.index(firstLetter.startIndex, offsetBy: 1))
if var memberValues = membersDict[firstnameKey] {
memberValues.append(firstLetter)
membersDict[firstnameKey] = memberValues
} else {
membersDict[firstnameKey] = [firstLetter]
}
}
memberSectionTitles = [String](membersDict.keys)
memberSectionTitles = memberSectionTitles.sorted { $0 < $1 }
}
func loadDataFromFirebase() {
let FBref = FIRDatabase.database().reference()
FBref.child("member-list").observeSingleEvent(of: .value, with: { (snapshot) in
var resultItem: [Member] = []
for item in snapshot.children {
let memberItem = Member(snapshot: item as! FIRDataSnapshot)
resultItem.append(memberItem)
}
self.members = resultItem
self.createFirstnameDict()
self.tableView.reloadData()
}) { (error) in
print(error.localizedDescription)
}
}
}
My Member model:
import Foundation
struct Member {
let firstname: String
let lastname: String
let email: String
let phonenumber: String
let socialsecuritynr: String
let memberamount: String
let addedByUser: String
let key: String
let ref: FIRDatabaseReference?
init(firstname: String, lastname: String, email: String, phonenumber: String, socialsecuritynr: String, memberamount: String, addedByUser: String, key: String = "") {
self.key = key
self.firstname = firstname
self.lastname = lastname
self.email = email
self.phonenumber = phonenumber
self.socialsecuritynr = socialsecuritynr
self.memberamount = memberamount
self.addedByUser = addedByUser
self.ref = nil
}
init(snapshot: FIRDataSnapshot) {
key = snapshot.key
let snapshotValue = snapshot.value as! [String: AnyObject]
firstname = snapshotValue["firstname"] as! String
lastname = snapshotValue["lastname"] as! String
email = snapshotValue["email"] as! String
phonenumber = snapshotValue["phonenumber"] as! String
socialsecuritynr = snapshotValue["socialsecuritynr"] as! String
memberamount = snapshotValue["memberamount"] as! String
addedByUser = snapshotValue["addedByUser"] as! String
ref = snapshot.ref
}
func toAnyObject() -> Any {
return ["firstname": firstname, "lastname": lastname, "email": email, "phonenumber": phonenumber, "socialsecuritynr": socialsecuritynr, "memberamount":memberamount, "addedByUser": addedByUser]
}
}
This is my TableView before and after:
The issue is arising from the placement of your methods that load the data which are wrongly in viewDidAppear:
loadDataFromFirebase()
createFirstnameDict()
This means that each time your view appears your data is loaded again and again. To fix the problem move these methods into viewDidLoad and you wont get the duplication issues. So you should now have:
override func viewDidLoad() {
super.viewDidLoad()
loadDataFromFirebase()
createFirstnameDict()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
What I recommend you, is that you clear all your arrays that you populate on your before event listener. This way you make sure that when it come back from another view it will not have old data. Something like this:
self.members.removeAll()
You are displaying the data from membersDict in your cell.textlabel .
Each time your view(when you switch tabs) loads, it calls loadDataFromFirebase() .
Here, all the values get loaded again and get appended to your membersValues which you then store in membersDict.
A new instance of membersDict will not get created since you are not declaring them inside of viewDidLoad(). You have declared them inside the class but outside any function.
What append does is add an element at the end of the array. It does not overwrite the element. So if you have an array with two names, appending a name will make that your third name and not overwrite any existing names.
Each time you load the view, you are appending the names to an array that already consists of the names. This is what is causing the duplication.
Try printing the value of your membersDict or membersValues, to check if you are duplicating.
You can solve this by declaring an instance of membersDict locally such that an empty variable is created each time and use that to display data.
Hope this helps.
From my understanding and experience, you load firebase data in
override func viewDidLoad() {
super.viewDidLoad()
loadFirebaseData()
}
Your newly created data from any other view controller will appear on your table when you return to it, because your observers are still listening unless you have told them to stop listening when moving to other views.
Therefore, anytime new data appears in Firebase, your table will automatically display it.