Can not insert items into NSOutlineView - swift

I've been fighting with this NSOutlineView that should insert new items to the end of a group. My code is being called/hitting breakpoints but the new items don't appear in the NSOutlineView until I kill and restart the app.Not sure what I'm missing.
#objc func didReceaveNewFeeds(aNotification: Notification) {
guard let userinfo: [AnyHashable : Any] = aNotification.userInfo,
let newFeed: ManagedFeed = userinfo[Notification.Name.newFeedKey] as? ManagedFeed else {
return
}
let index: Int = sidebarDataSource?.allFeeds.count ?? 0
unowned let unownedSelf: MainViewController = self
DispatchQueue.main.async {
unownedSelf.sidebarDataSource?.allFeeds.append(newFeed)
unownedSelf.outlineview.insertItems(at: IndexSet([index]),
inParent: unownedSelf.sidebarDataSource?.allFeeds,
withAnimation: .slideRight)
}
}

I suspect the trouble you are having is because of this line:
unownedSelf.outlineview.insertItems(at: IndexSet([index]),
inParent: unownedSelf.sidebarDataSource?.allFeeds,
withAnimation: .slideRight)
I assume allFeeds is an array of data objects, and inParent argument expects an item (row) in the outline view's tree, or nil which would mean the root item.
You can get a reference to an outline view item with this method:
func item(atRow row: Int) -> Any?
You are correct in adding the new data to your data source with this line:
unownedSelf.sidebarDataSource?.allFeeds.append(newFeed)
So when you reload the application you are likely calling reloadData somewhere on the outline view and seeing the new data appear.

Because you are missing this sentence
unownedSelf.outlineview.expandItem(unownedSelf.sidebarDataSource?.allFeeds, expandChildren: true)

Related

Function creating 2 identical instances of class instead of only 1

I am relatively new to iOS development, any help will be greatly appreciated!
I'm trying to create a new instance of a class 'Event'.
class Event {
var EventName: String
var EventPhoto: UIImage?
init?(EventName: String, EventPhoto: UIImage?) {
guard !EventName.isEmpty else {
return nil
}
// Initial initilization of the values
self.EventName = EventName
self.EventPhoto = EventPhoto
// If some of the values are left blank, this will return nil to signal the problem
}
}
Below is the override function, which from my understanding is responsible for creating the instance:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
guard let button = sender as? UIBarButtonItem, button === saveButton else {
os_log("Cancelling Action, The Save Button Was not Pressed", log: OSLog.default,type: .debug)
return
}
let EventName = NewEventNameField.text ?? ""
let EventPhoto = NewEventImage.image
event = Event(EventName: EventName, EventPhoto: EventPhoto)
}
From my understanding, the override function should create a new instance of the class, which would then be displayed in a table view controller displaying a table of 'events'. My problem here is; when the function is called by the "create instance" button, it creates 2 identical instances with the same EventName and EventPhoto extracted from a textfield and an image in the view controller. In the tableview, there are basically 2 events that are exactly the same being displayed, which is what I am having trouble with since I don't see the code calling init twice anywhere, and can't figure out why the instances was created twice. After being created the 2 instances act independently and function like 2 separate instances would.
Thanks!
Thank You for the help, I found the issue in TableViewController's code file:
#IBAction func unwindToEventList(sender: UIStoryboardSegue){
if let sourceViewController = sender.source as?
NewEventViewController, let event = sourceViewController.event{
if let selectedIndexPath = tableView.indexPathForSelectedRow {
events[selectedIndexPath.row] = event
tableView.reloadRows(at: [selectedIndexPath], with: .none)
} else {
//Adding a new event instead of editing it.
let newIndexPath = IndexPath(row: events.count, section: 0)
events.append(event)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
//let newIndexPath = IndexPath(row: events.count, section: 0)
//events.append(event)
//tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
Turns out that I accidentally appended the instance to the list events and inserted it into the table an extra time outside the if statement.

Getting data out of a firebase function in Swift [duplicate]

In my iOS app, I have two Firebase-related functions that I want to call within viewDidLoad(). The first picks a random child with .queryOrderedByKey() and outputs the child's key as a string. The second uses that key and observeEventType to retrieve child values and store it in a dict. When I trigger these functions with a button in my UI, they work as expected.
However, when I put both functions inside viewDidLoad(), I get this error:
Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
The offending line of code is in my AppDelegate.swift, highlighted in red:
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate
When I comment out the second function and leave the first inside viewDidLoad, the app loads fine, and subsequent calls of both functions (triggered by the button action) work as expected.
I added a line at the end of the first function to print out the URL string, and it doesn't have any offending characters: https://mydomain.firebaseio.com/myStuff/-KO_iaQNa-bIZpqe5xlg
I also added a line between the functions in viewDidLoad to hard-code the string, and I ran into the same InvalidPathException issue.
Here is my viewDidLoad() func:
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
pickRandomChild()
getChildValues()
}
Here is the first function:
func pickRandomChild () -> String {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
})
return movieToGuess
}
Here is the second function:
func getChildValues() -> [String : AnyObject] {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
})
return movieDict
)
And for good measure, here's the relevant portion of my AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
return true
}
I'm guessing Swift is executing the code not in the order I expect. Does Swift not automatically wait for the first function to finish before running the second? If that's the case, why does this pairing work elsewhere in the app but not in viewDidLoad?
Edit: The issue is that closures are not called in order.
I'm not sure what your pickRandomChild() and getChildValues() methods are, so please post them as well, but the way I fixed this type issue was by sending the data through a closure that can be called in your ViewController.
For example when I wanted to grab data for a Full Name and Industry I used this. This method takes a Firebase User, and contains a closure that will be called upon completion. This was defined in a class specifically for pulling data.
func grabDataDict(fromUser user: FIRUser, completion: (data: [String: String]) -> ()) {
var myData = [String: String]()
let uid = user.uid
let ref = Constants.References.users.child(uid)
ref.observeEventType(.Value) { (snapshot, error) in
if error != nil {
ErrorHandling.defaultErrorHandler(NSError.init(coder: NSCoder())!)
return
}
let fullName = snapshot.value!["fullName"] as! String
let industry = snapshot.value!["industry"] as! String
myData["fullName"] = fullName
myData["industry"] = industry
completion(data: myData)
}
}
Then I defined an empty array of strings in the Viewcontroller and called the method, setting the variable to my data inside the closure.
messages.grabRecentSenderIds(fromUser: currentUser!) { (userIds) in
self.userIds = userIds
print(self.userIds)
}
If you post your methods, however I can help you with those specifically.
Edit: Fixed Methods
1.
func pickRandomChild (completion: (movieToGuess: String) -> ()) {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
// Put whatever you want to return here.
completion(movieToGuess)
})
}
2.
func getChildValues(completion: (movieDict: [String: AnyObject]) -> ()) {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
// Put whatever you want to return here.
completion(movieDict)
})
}
Define these methods in some model class, and when you call them in your viewcontroller, you should be able to set your View Controller variables to movieDict and movieToGuess inside each closure. I made these in playground, so let me know if you get any errors.
Your functions pickRandomChild() and getChildValues() are asynchronous, therefore they only get executed at a later stage, so if getChildValues() needs the result of pickRandomChild(), it should be called in pickRandomChild()'s completion handler / delegate callback instead, because when one of those are called it is guaranteed that the function has finished.
It works when you comment out the second function and only trigger it with a button press because there has been enough time between the app loading and you pushing the button for the asynchronous pickRandomChild() to perform it action entirely, allowing getChildValues() to use its returned value for its request.

How to reload TableView in other View?

I have some CoreData base wich I'm used in my TableView.
When I'm tried to clear those base in other View I have a message in my console log.
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
for deleting CoreData Array I'm used this code
self.historyArray.removeAll()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "History")
fetchRequest.returnsObjectsAsFaults = false
do {
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
}
} catch {
print("Detele all data")
}
I know I need to reload TableView, but how can I do this in other View?
ill tried this, but this code don't work.
var tableViewHistoryClass = HistoryView()
self.tableViewHistoryClass.tableView.reloadData()
Please help me to fix this message.
You can achieve this by using notification.
create observer in viewDidLoad method where you can display your table view data.
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"refreshTableView", name: "reloadTable", object: nil)
}
func refreshTableView () {
self.tableView.reloadData()
}
Second view controller
-> In this view controller you can change your data( if you want to do) or send data object
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil)
so like this it will reload your table view.
One solution is to notify your tableview when data is removed.
When data is removed your code post notifications :
do {
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
NSNotificationCenter
.defaultCenter()
.postNotificationName("dataDeleted", object: self)
}
}
And in controller where is your tableview add an observer for this notification:
override func viewDidLoad() {
NSNotificationCenter
.defaultCenter()
.addObserver(
self,
selector: #selector(viewController.reloadTableView),
name: "dataDeleted",
object: nil)
}
func reloadTableView() {
self.tableview.reloadData
}
Thanks all for answers!
I'm created new method, all my Clear CoreData function i added to my View in which i have TableView for showing all data from CoreData :P
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"clearCoreDataArray", name: "clearAllData", object: nil)
}
func clearCoreDataArray() {
historyArray.removeAll()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "History")
fetchRequest.returnsObjectsAsFaults = false
do
{
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
}
} catch {
print("Detele all data")
}
self.tableView.reloadData()
}
and in View when I'm need to use this method i use this code
NSNotificationCenter.defaultCenter().postNotificationName("clearAllData", object: self)
now i don't have any CoreData warnings

Core Data - Change NSManagedObject array into array of Strings using valueForKey -OSX

So iv using an NSTokenField to allow data entry, the TokenField will suggest thing when the user starts typing. I want it to suggest things that are already inside core data.
To do this i have this function being called when the cell moves to superview (This is all happening inside a custom table view cell)
var subjectInformation = [NSManagedObject]()
let appDel = NSApplication.sharedApplication().delegate as! AppDelegate
let context = appDel.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "SubjectInformation")
do {
let results = try context.executeFetchRequest(fetchRequest)
subjectInformation = results as! [NSManagedObject]
} catch {
}
this returns an array of NSManagedObjects, now i want for every object in managed object get get the valueForKey("subjectName") as insert it into a array of string so that i can return that inside this token field Function
func tokenField(tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>) -> [AnyObject]? {
return subjectInformation //this is where is should return an array eg; ["English","Maths","Science"]
How would i do this? Thanks :)
If you properly subclassed your NSManagedObject you can use expressive Swift style filters and maps. You would cast your results array to [SubjectInformation] and
let subjectList = subjectInformation.map { $0.subjectName }
Try this:
(subjectInformation as! NSArray).valueForKeyPath("#unionOfObjects.subjectName")
This should return an array of the subjectNames of all the subjectInformation items.

In a macOS Cocoa Application that has two identical NSOutlineViews side by side, is there a way to expand / collapse items in sync between the two?

I am developing a macOS Cocoa Application in Swift 5.1 . In the main window, I have two identical NSOutlineViews that have the same contents exactly. I would like to enable a sync mode, where if items are expanded/collapsed in one of the two NSOutlineViews, the corresponding item is expanded/collapsed in the other simultaneously. I tried to do this by implementing shouldExpandItem and shouldCollapseItem in the delegate. The delegate is the same object for both NSOutlineViews, and I have outlets that reference the two NSOutlineViews for distinguishing the two. The problem is, if I call expandItem programmatically in shouldExpandItem, the method is again called for the other NSOutlineView, leading to an infinite loop and a stack overflow. I have found a dirty solution that works, by momentarily setting the delegate of the relevant NSOutlineView to nil, expand/collapse the item, then set the delegate back. The code is the following:
func outlineView(_ outlineView: NSOutlineView, shouldExpandItem item: Any) -> Bool {
let filePath = (item as! FileSystemObject).fullPath
let trueItem = item as! FileSystemObject
trueItem.children = Array()
do {
let contents = try FileManager.default.contentsOfDirectory(atPath: filePath!)
for (_, item) in contents.enumerated() {
let entry = FileSystemObject.init()
entry.fullPath = URL.init(fileURLWithPath: filePath!).appendingPathComponent(item).path
if entry.exists {
trueItem.children.append(entry)
}
}
} catch {
}
if outlineView == self.leftOutlineView {
self.rightOutlineView.delegate = nil;
self.rightOutlineView.expandItem(item)
self.rightOutlineView.delegate = self;
} else {
self.leftOutlineView.delegate = nil;
self.leftOutlineView.expandItem(item)
self.leftOutlineView.delegate = self;
}
return true
}
func outlineView(_ outlineView: NSOutlineView, shouldCollapseItem item: Any) -> Bool {
if outlineView == self.leftOutlineView {
self.rightOutlineView.delegate = nil;
self.rightOutlineView.collapseItem(item)
self.rightOutlineView.delegate = self;
} else {
self.leftOutlineView.delegate = nil;
self.leftOutlineView.collapseItem(item)
self.leftOutlineView.delegate = self;
}
return true
}
It seems to work, but I am afraid that something could go wrong with this approach. Is setting the delegate momentarily to nil a possible solution, or should I be aware of any caveats ? is there another pattern that you can suggest to achieve this ? Thanks
EDIT: According to below comments and answers
I found the simplest solution thanks to the answers / comments I received.
Instead of implementing the sync logic in the outlineViewShouldExpand/Collapse methods, it is possible to implement outlineViewDidExpand and outlineViewDidCollapse and place the sync logic there. the latter methods are not called when expanding/collapsing items programmatically, so there is no risk of infinite loop or stack overflow.
The code is as follows:
func outlineViewItemDidExpand(_ notification: Notification) {
let outlineView = notification.object as! NSOutlineView
let userInfo = notification.userInfo as! Dictionary<String, Any>
let item = userInfo["NSObject"]
if outlineView == self.leftOutlineView {
self.rightOutlineView.animator().expandItem(item)
} else {
self.leftOutlineView.animator().expandItem(item)
}
}
func outlineViewItemDidCollapse(_ notification: Notification) {
let outlineView = notification.object as! NSOutlineView
let userInfo = notification.userInfo as! Dictionary<String, Any>
let item = userInfo["NSObject"]
if outlineView == self.leftOutlineView {
self.rightOutlineView.animator().collapseItem(item)
} else {
self.leftOutlineView.animator().collapseItem(item)
}
}
Furthermore, now, I cannot understand why, the expansion / collapse of items is animated, which did not work with my original approach.
I hope this can be helpful for somebody, it was very helpful to me. Thanks a lot.
My app does something similar. I need to keep many views in sync, across windows. One of those views is an NSOutlineView. I have run into a few quirks with NSOutlineView, but I don't believe they are related to syncing.
I do take a different approach, though. Instead of manipulating the delegate, I just have a flag that suppresses certain actions. In my case, a flag makes more sense, since lots of other views are being affected. But, the effect is very similar to what you are doing.
I think the only risk would be missing a delegate call during your synchronization. Assuming your logic does not depend on this, I believe your approach will work fine.
outlineView(_:shouldExpandItem:) is called before the item is expanded and calling expandItem() back and forth causes an infite loop. outlineViewItemDidExpand(_:) is called after the item is expanded and NSOutlineView.expandItem(_:) does nothing when the item is already expanded (documented behaviour). When expanding the left outline view, expandItem() of the right outline view does call outlineViewItemDidExpand(_:) but the expandItem() of the left outline view doensn't call outlineViewItemDidExpand(_:) again.
func outlineViewItemDidExpand(_ notification: Notification) {
let outlineView = notification.object as! NSOutlineView
let item = notification.userInfo!["NSObject"]
if outlineView == self.leftOutlineView {
self.rightOutlineView.animator().expandItem(item)
} else {
self.leftOutlineView.animator().expandItem(item)
}
}