Firebase JSON Tree Data Retrieval in Swift - swift

My Firebase data structure looks like:
The following code works to update the value of the label to be the current value of condition:
import UIKit
import Firebase
class ViewController: UIViewController {
let conditionRef = Database.database().reference().child("condition")
#IBOutlet weak var conditionLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
conditionRef.observe(.value) {
(snap: DataSnapshot) in
self.conditionLabel.text = (snap.value as! String).description
}
}
I want to update the value of the label to be that of test2. I've tried a lot of things, but the thing that made most sense to me was changing the constant conditionRef to:
let conditionRef = Database.database().reference().child("test1").child("test2")
This didn't work, and I'm not sure what will. I've gone through the Firebase documentation and multiple YouTube tutorials but can't seem to figure this trivial detail out.
Any help will be greatly appreciated, thanks in advance.

One solution is to create a reference to the node you want the value of and then observe it with a single event like this:
class ViewController: UIViewController {
var ref: DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
self.ref = Database.database().reference()
let test2Ref = self.ref.child("fir-2-b1233").child("test1").child("test2")
test2Ref.observeSingleEvent(of: .value, with: { snapshot in
let val = snapshot.value as! Int
print(val)
})
}
}
The key is to ensure you include the entire path to the child node you want the data from.
and my Firebase Stucture
{
"fir-2-b1233" : {
"condition" : "foggy",
"test1" : {
"test2" : 0,
"test3" : 0
}
}
}

Related

Primary key can't be changed after an object is inserted

I'm very new to programming.
I am trying to update an object in my Realm database but I get always an error.
I have tried to find the issue but I can't find anyone with a similar issue.
What I'm trying to do is:
I have a Game-Score-App.
It should display the names on Tab1 and on the Tab2 I want to give the user the ability, to change the names of the players. As soon as the ViewDidDisappear I want to write the changes to Realm.
I already figured out how to update the names in the database. And it works properly the first time.
But as soon as I go a second time on the Tab2 and go back to Tab1 again, I get the message "Primary key can't be changed after an object is inserted."
Any Ideas?
class Games: Object {
#objc dynamic var game_id = UUID().uuidString
#objc dynamic var gameName: String = ""
var playerNames = List<String>()
override class func primaryKey() -> String? {
return "game_id"
}
}
class FirstPageVC: UIViewController {
#IBOutlet var playerNameLabels: [UILabel]!
#IBOutlet weak var gameNameLabel: UILabel!
let realm = try! Realm()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
let games = realm.objects(Games.self)
gameNameLabel.text = games[0].gameName
for i in 0...playerNameLabels.count - 1 {
playerNameLabels[i].text = games[0].playerNames[i]
}
}
}
class SecondPageVC: UIViewController {
#IBOutlet var playerNameTextbox: [UITextField]!
#IBOutlet weak var gameNameTextbox: UITextField!
#IBOutlet weak var numberOfIndex: UITextField!
let realm = try! Realm()
var playerNames: [String] = []
var gameName: String = ""
var game = Games()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if realm.objects(Games.self).count != 0 {
let games = realm.objects(Games.self)
gameNameTextbox.text = games[0].gameName
for i in 0...playerNameTextbox.count - 1 {
playerNameTextbox[i].text = games[0].playerNames[i]
}
}
}
#IBAction func addButton(_ sender: UIButton) {
gameName = gameNameTextbox.text!
for i in 0...playerNameTextbox.count - 1 {
playerNames.append(playerNameTextbox[i].text!)
}
let items = realm.objects(Games.self)
let number = Int(numberOfIndex.text!)
game.game_id = items[number!].game_id
game.gameName = gameName
game.playerNames.append(objectsIn: playerNames)
try! realm.write {
realm.add(game, update: .modified)
}
}
}
The problem is your Realm object structure. Anything that could possibly ever be changed should not be used as a primary key.
Also note from the Realm Documentation
Once an object with a primary key is added to a Realm, the primary key
cannot be changed.
To expand on that, it's often best practice to disassociate an objects key (e.g. primary key) from the rest of the properties of an object.
Here's how to do that
class Games: Object{
#objc dynamic var game_id = UUID().uuidString
#objc dynamic var gameName: String = ""
var playerNames = List<String>()
override class func primaryKey() -> String? {
return "game_id"
}
}
UUID().uuidString will generate a unique string for every object that's created and will look something like this string
CDEA69EA-AC84-4465-ABE3-DDA29D31B925
Once the object is created, you can use it to load that specific object or update it's properties.
See Objects with Primary Keys
Here's how to change the game name
let item = realm.object(ofType: Game.self, forPrimaryKey: "CDEA69EA-AC84-4465-ABE3-DDA29D31B925")!
try! realm.write {
game.gameName = "Pwn You!"
}
Try this solution:
Replace the code in viewDidDisappear after the end of for loop with the following code:
if let gameInRealm = realm.objects(Game.self).first{
try! realm.write {
gameInRealm.gameName = gameName
gameInRealm.playerNames = playerNames
}
}else{
game.gameName = gameName
game.playerNames.append(objectsIn: playerNames)
realm.add(game)
}
Explanation (if needed):
The code changes the existing Game properties in case a Game object exists. Otherwise, it creates a new one with the new properties.
Therefore, the else statement should get executed the first time you leave SecondPageVC, and then the if statement will get triggered every other time you leave SecondPageVC.

VIPER architecture using Swift to store data in presenter

So I'm setting up a simple VIPER architecture in Swift.
The Interactor gets some data from an API, and passes the data to the presenter that then passes formatted data to the view.
The presenter will process the data, and just count the number of objects that are downloaded. To do so I have stored a var in the presenter. The question is should I store data in the presenter?
Interactor:
class Interactor {
weak var presenter: Presenter?
func getData() {
ClosureDataManager.shared.fetchBreaches(withURLString: baseUrl + breachesExtensionURL, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
print(error)
case .success(let breaches):
self.presenter?.dataDidFetch(breaches: breaches)
self.presenter?.dataNumberDidFetch(number: breaches.count)
}
})
}
}
Presenter:
class Presenter {
var wireframe: Wireframe?
var view: ViewController?
var interactor: Interactor?
var dataDownloaded = 0
func viewDidLoad() {
print ("presenter vdl")
}
func loadData() {
interactor?.getData()
}
func dataDidFetch(breaches: [BreachModel]) {
view?.dataReady()
}
func showDetail(with text: String, from view: UIViewController) {
wireframe?.pushToDetail(with: text, from: view)
}
func dataNumberDidFetch(number: Int) {
dataDownloaded += number
view?.showData(number: String(dataDownloaded) )
}
}
View (ViewController)
protocol dataViewProtocol {
func showData(number: String)
}
class ViewController: UIViewController, dataViewProtocol {
#IBOutlet weak var showDetailButton: UIButton!
#IBOutlet weak var dataLabel: UILabel!
// weak here means it won't work
var presenter: Presenter?
#IBAction func buttonPressAction(_ sender: UIButton) {
presenter?.loadData()
}
#IBAction func buttonShowDetailAction(_ sender: UIButton) {
presenter?.showDetail(with: "AAA", from: self)
}
func dataReady() {
showDetailButton.isEnabled = true
}
func showData(number: String) {
dataLabel.text = number
}
override func viewDidLoad() {
super.viewDidLoad()
Wireframe.createViewModule(view: self)
presenter?.viewDidLoad()
}
}
Router (Wireframe)
class Wireframe {
static func createViewModule (view: ViewController) {
let presenterInst = Presenter()
view.presenter = presenterInst
view.presenter?.wireframe = Wireframe()
view.presenter?.view = view
view.presenter?.interactor = Interactor()
view.presenter?.interactor?.presenter = presenterInst
}
}
So should the presenter be used to store the number of objects downloaded?
What have you tried I've implemented the var, as shown above. This is a minimum example of the problem.
What resources have you used I've looked on StackOverflow, and Googled the issue. I can't find an answer, but know I could store the data in the view but I think this is incorrect. I could store the number of data in the Interactor, but this also doesn't seem right. It all seems...to violate separation of concerns...
I won't do your homework / use a different architecture / You should use protocols / Why is there a single protocol in your implementation This isn't homework, it is for my own self - study. There may be other architectures that can be used to do this (and coding to protocols is good practice) but this is about storing a variable in the presenter. I want to know if I should store the variable in the presenter, using VIPER and using Swift. Comments about trivia around the question are seldom helpful if they are about variable names, or the like.
What is the question? I want to know if I can store the number of downloaded data items in the presenter.

TextField will not load from RealmSwift

So here is the problem that I have been going crazy with. I have an app that uses Realm as its Database. I have used Realm in the past with tableviews and it works properly. I can't seem to get the textField data that I saved to load into my view on viewDidLoad. With tableViews there is a reloadData method, but that isn't the case with textFields. I need the data that is in my data base to load into the view on viewDidLoad and I have confirmed several times that data has been saved and is sitting in the dataBase just waiting to load into the view, but it will not load.
import Cocoa
import RealmSwift
class PublisherViewController: NSViewController {
#IBOutlet weak var publisherName: NSTextField!
let realm = try! Realm()
var pubData : Results<Pub>?
override func viewDidLoad() {
super.viewDidLoad()
// Here is where textField.stringValues should load into the viewDidLoad
let theData = Pub()
theData.pubName = publisherName.stringValue
print("print \(theData)")
}
func save(pubData: Pub) {
do {
try realm.write {
realm.add(pubData)
}
} catch {
print("there was an error saving pubData \(error)")
}
}
#IBAction func savePublisher(_ sender: NSButton) {
let publisherData = Pub()
publisherData.pubName = publisherName.stringValue
save(pubData: publisherData)
}
}
Here is my data model
import Foundation
import RealmSwift
class Pub: Object {
#objc dynamic var pubName : String = """
}
Thanks to Mrkrisher on Discord here is the answer on how to display the last saved textViews and textFields from Realm into your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
publisherName.stringValue = (pubData?.last?.pubName)?? ""
}

How to set up a NSComboBox using prepare for segue or init?

I am really struggling to pass the contents of one array from a view controller to another to set up the contents of a nscombobox. I have tried everything I can think of, prepare for segue, init; but nothing seems to work.
the program flow is as follows: the user enter a number into a text field and based on it an array with the size of the number is created. Once the user presses a button the next VC appears that has a combo box and inside that combo box those numbers need to appear. All my attempts result in an empty array being passed. Could someone please take a bit of time and help me out. Im sure I'm doing a silly mistake but cannot figure out what.
Code listing below:
Class that take the user input. At this stage I'm trying to pass the contents of the array in the next class as I gave up on prepare for segue because that one crashes because of nil error. Please note that prepare for segue is uncommented in the code listing just for formatting purposes here. Im my program it is commented out as I am using perform segue at the moment.
Any solution would be nice please. Thank you.
import Cocoa
class SetNumberOfFloorsVC: NSViewController {
//MARK: - Properties
#IBOutlet internal weak var declaredNumber: NSTextField!
internal var declaredFloorsArray = [String]()
private var floorValue: Int {
get {
return Int(declaredNumber.stringValue)!
}
}
//MARK: - Actions
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction private func setNumberOfFloors(_ sender: NSButton) {
if declaredNumber.stringValue.isEmpty {
let screenAlert = NSAlert.init()
screenAlert.messageText = "Please specify the number of floors!"
screenAlert.addButton(withTitle: "Got it!")
screenAlert.runModal()
} else if floorValue == 0 || floorValue < 0 {
let screenAlert = NSAlert.init()
screenAlert.messageText = "Please input a correct number of floors!"
screenAlert.addButton(withTitle: "Got it!")
screenAlert.runModal()
} else {
for i in 0...floorValue - 1 {
declaredFloorsArray.append(String(i))
}
print("\(declaredFloorsArray)")
let declareNumberOfRoomsVC = SetNumberOfRoomsForFloorVC(boxData: declaredFloorsArray)
declareNumberOfRoomsVC.boxData = declaredFloorsArray
performSegue(withIdentifier: "set number of rooms", sender: self)
}
}
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
if segue.identifier == "set number of rooms" {
if let addRoomsVC = segue.destinationController as? SetNumberOfRoomsForFloorVC {
addRoomsVC.floorBox.addItems(withObjectValues: declaredFloorsArray)
}
}
}
}
this is the class for the next VC with the combo box:
import Cocoa
class SetNumberOfRoomsForFloorVC: NSViewController, NSComboBoxDelegate, NSComboBoxDataSource {
//MARK: - Properties
#IBOutlet internal weak var floorBox: NSComboBox!
#IBOutlet private weak var numberOfRoomsTxtField: NSTextField!
internal var boxData = [String]()
//MARK: - Init
convenience init(boxData: [String]) {
self.init()
self.boxData = boxData
}
//MARK: - Actions
override func viewDidLoad() {
super.viewDidLoad()
floorBox.usesDataSource = true
floorBox.dataSource = self
floorBox.delegate = self
print("\(boxData)")
}
#IBAction private func setRoomsForFloor(_ sender: NSButton) {
}
//MARK: - Delegates
func numberOfItems(in comboBox: NSComboBox) -> Int {
return boxData.count
}
func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? {
return boxData[index]
}
}
First you should remove the following code.
let declareNumberOfRoomsVC = SetNumberOfRoomsForFloorVC(boxData: declaredFloorsArray)
declareNumberOfRoomsVC.boxData = declaredFloorsArray
I assume you think that the viewController you created here is passed to prepareForSegue. However the storyboard instantiates a new viewController for you.
After that you need to set your declaredFloorsArray as the the boxData of the new viewController in prepareForSegue and you should be good to go.

Using NSTreeController with NSOutlineView

I'm trying (unsuccessfully) to build a TreeController-controlled NSOutlineView. I've gone through a bunch of tutorials, but they all pre-load the data before starting anything, and this won't work for me.
I have a simple class for a device:
import Cocoa
class Device: NSObject {
let name : String
var children = [Service]()
var serviceNo = 1
var count = 0
init(name: String){
self.name = name
}
func addService(serviceName: String){
let serv = "\(serviceName) # \(serviceNo)"
children.append(Service(name: serv))
serviceNo += 1
count = children.count
}
func isLeaf() -> Bool {
return children.count < 1
}
}
I also have an even more simple class for the 'Service':
import Cocoa
class Service: NSObject {
let name: String
init(name: String){
self.name = name
}
}
Finally, I have the ViewController:
class ViewController: NSViewController {
var stepper = 0
dynamic var devices = [Device]()
override func viewDidLoad() {
super.viewDidLoad()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func addDeviceAction(_ sender: Any) {
let str = "New Device #\(stepper)"
devices.append(Device(name: str))
stepper += 1
print("Added Device: \(devices[devices.count-1].name)")
}
#IBAction func addService(_ sender: Any) {
for i in 0..<devices.count {
devices[i].addService(serviceName: "New Service")
}
}
}
Obviously I have 2 buttons, one that adds a 'device' and one that adds a 'service' to each device.
What I can't make happen is any of this data show up in the NSOutlineView. I've set the TreeController's Object Controller Property to Mode: Class and Class: Device, and without setting the Children, Count, or Leaf properties I get (predictably):
2017-01-04 17:20:19.337129 OutlineTest[12550:1536405] Warning: [object class: Device] childrenKeyPath cannot be nil. To eliminate this log message, set the childrenKeyPath attribute in Interface Builder
If I then set the Children property to 'children' things go very bad:
2017-01-04 17:23:11.150627 OutlineTest[12695:1548039] [General] [ addObserver:forKeyPath:options:context:] is not supported. Key path: children
All I'm trying to do is set up the NSOutlineView to take input from the NSTreeController so that when a new 'Device' is added to the devices[] array, it shows up in the Outline View.
If anyone could point me in the right direction here I'd be most grateful.
Much gratitude to Warren for the hugely helpful work. I've got it (mostly) working. A couple of things that I also needed to do, in addition to Warren's suggestions:
Set the datastore for the Tree Controller
Bind the OutlineView to the TreeController
Bind the Column to the TreeController
Bind the TableView Cell to the Table Cell View (yes, really)
Once all that was done, I had to play around with the actual datastore a bit:
var name = "Bluetooth Devices Root"
var deviceStore = [Device]()
#IBOutlet var treeController: NSTreeController!
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
deviceStore.append(Device(name: "Bluetooth Devices"))
self.treeController.content = self
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func addDeviceAction(_ sender: Any) {
if(deviceStore[0].name == "Bluetooth Devices"){
deviceStore.remove(at: 0)
}
Turns out the Root cannot be child-less at the beginning, at least as far as I can tell. Once I add a child, I can delete the place-holder value and the tree seems to work (mostly) as I want. One other thing is that I have to reload the data and redisplay the outline whenever the data changes:
outlineView.reloadData()
outlineView.setNeedsDisplay()
Without that, nothing. I still don't have the data updating correctly (see comments below Warren's answer) but I'm almost there.
To state the obvious, a NSTreeController manages a tree of objects all of which need to answer the following three questions/requests.
Are you a leaf i.e do you have no children? = leafKeyPath
If you are not a leaf, how many children do you have ? = countKeyPath
Give me your children! = childrenKeyPath
Its simple to set these up in IB or programatically. A fairly standard set of properties is respectively.
isLeaf
childCount
children
But its totally arbitrary and can be any set of properties that answer those questions.
I normally set up a protocol named something like TreeNode and make all my objects conform to it.
#objc protocol TreeNode:class {
var isLeaf:Bool { get }
var childCount:Int { get }
var children:[TreeNode] { get }
}
For your Device object you answer 2 out 3 question with isLeaf and children but don't answer the childCount question.
Your Device's children are Service objects and they answer none of that which is some of the reason why you are getting the exceptions.
So to fix up your code a possible solution is ...
The Service object
class Service: NSObject, TreeNode {
let name: String
init(name: String){
self.name = name
}
var isLeaf:Bool {
return true
}
var childCount:Int {
return 0
}
var children:[TreeNode] {
return []
}
}
The Device object
class Device: NSObject, TreeNode {
let name : String
var serviceStore = [Service]()
init(name: String){
self.name = name
}
var isLeaf:Bool {
return serviceStore.isEmpty
}
var childCount:Int {
return serviceStore.count
}
var children:[TreeNode] {
return serviceStore
}
}
And a horrible thing to do from a MVC perspective but convenient for this answer. The root object.
class ViewController: NSViewController, TreeNode {
var deviceStore = [Device]()
var name = "Henry" //whatever you want to name your root
var isLeaf:Bool {
return deviceStore.isEmpty
}
var childCount:Int {
return deviceStore.count
}
var children:[TreeNode] {
return deviceStore
}
}
So all you need to do is set the content of your treeController. Lets assume you have an IBOutlet to it in your ViewController.
class ViewController: NSViewController, TreeNode {
#IBOutlet var treeController:NSTreeController!
#IBOutlet var outlineView:NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
treeController.content = self
}
Now each time you append a Device or add a Service just call reloadItem on the outlineView (that you also need an outlet to)
#IBAction func addDeviceAction(_ sender: Any) {
let str = "New Device #\(stepper)"
devices.append(Device(name: str))
stepper += 1
print("Added Device: \(devices[devices.count-1].name)")
outlineView.reloadItem(self, reloadChildren: true)
}
Thats the basics and should get you started but the docs for NSOutlineView & NSTreeController have a lot more info.
EDIT
In addition to the stuff above you need to bind your outline view to your tree controller.
First ensure your Outline View is in view mode.
Next bind the table column to arrangedObjects on the tree controller.
Last bind the text cell to the relevant key path. In your case it's name. objectValue is the reference to your object in the cell.