Function returns an empty array but the same function can return a filled NSMenu

I'm building a RSS parser in Swift for MacOS. It won't use any windows, everything will be shown in the Menu Bar.

I have a problem where I'm parsing multiple URL:s using Alamofire and AlamofireRSSParser and I would like to add the result into a list and thereafter sort the articles from the feed according to the date it was published.

For now it works and everything loads into the sub menu (NSMenu) and into the NSStatusBar but when I try load all the articles from the feeds then list returns empty.

I'm guessing this occurs because of Alamofire runs async. If that's the problem do I have to write my own parser or can I run a separate thread handling Alamofires url request and AlamofireRSSparser parsing?

I've tried to execute the URL request and RSS/XML parsing using DispatchQueue to make the call to loadRSS wait until the list has been filled but alas no luck.

This is how the code looks for now. The program loads all the feeds from a local xml file where I have separated the different using a tag called category. The categories can be seen in the picture "Sport", "Ekonomi" and so on. Each category can hold a different amount of URL:s. Each URL are sent to the AlamofireRSS parser and the result is returned back into the NSMenu that I sent in as a parameter (the line "let sub = loadRss(outline: outline, subMenu: sub"). The NSMenu is then added as a submenu for each category, so the category named "Sport" will only show sport articles.

If I try to print the NSMenu "sub" then it's always empty and also if I change the output from loadRSS to a list instead of NSMenu, the list is always empty even though the list is filled in loadRSS (if I use list.append where I now use subMenu.addItem()). What should I do to be able to sync the parsing of the URL:s with the return of a list so I can sort the resulting articles, and also how is it possible for the NSMenu to print empty and still be able to display the articles in the Menu Bar?

Sorry if the code is bad, it's my first Swift project.

      ///Used to read the RSS. Is called when the user presses the "Refresh item"
    @objc func refresh() {
        createMenu()
        
        for i in 0...categories.endIndex-1 {
            var sub = NSMenu()
            var categoryItem = NSMenuItem()
            categoryItem.title = categories[i].title
            
            for outline in categories[i].outlines {
                let sub = loadRss(outline: outline, subMenu: sub)
                
                print(sub.items)
            }
            categoryItem.submenu = sub
            categoryItem.target = self
            statusBarMenu.addItem(categoryItem)
            statusItem?.menu = statusBarMenu
        }
        statusBarMenu.addItem(quitItem)
        statusBarMenu.addItem(refreshItem)
    }



     
    func laodRss(outline: Outline, subMenu: NSMenu) -> NSMenu {
        var articleList = [NSMenuItem]()

        let url = URL(string: outline.xmlUrl)!
    

        AF.request(url).responseRSS() { (response) -> Void in
            if let feed: RSSFeed = response.value {
                for item in feed.items {

                    let article = NSMenuItem()
                    let timeString = self.formatDate(item: item)
                    if timeString != "" {
                        var title = self.shortenText(item: item.title!)
                        title = title + " " + timeString

                        let someObj: NSString = item.link! as NSString
                        article.representedObject = someObj
                        article.action = #selector(self.openBrowser(urlSender:))
                        article.title = title

                        //// Get the url from the article and add /favicon.ico to get the image
                        //// Will add the image to each article to indicate the source
                        let url = URL(string: outline.icon)

                        self.getData(from: url!) { data, response, error in
                            guard let data = data, error == nil else { return }


                            DispatchQueue.main.async() { [weak self] in
                                article.image = NSImage(data: data)
                                article.image?.size = CGSize(width: 15, height: 15)
                            }
                        }
                        subMenu.addItem(article)
                    }
                }
            }
        }
        return subMenu
    }

Answered by Claude31 in 696052022

Problem is that you return submenu immediately, before any item has been added:

  • line 6 dispatches to a thread
  • then code continues immediately at line 38, where subMenu is not yet filled
1. func laodRss(outline: Outline, subMenu: NSMenu) -> NSMenu {
2.     var articleList = [NSMenuItem] ()
3. 
4.     let url = URL(string: outline.xmlUrl)!
5. 
6.     AF.request(url).responseRSS() { (response) -> Void in
7.         if let feed: RSSFeed = response.value {
8.             for item in feed.items {
9.                 let article = NSMenuItem()
10.                 let timeString = self.formatDate(item: item)
11.                 if timeString != "" {
12.                     var title = self.shortenText(item: item.title!)
13.                     title = title + " " + timeString
14. 
15.                     let someObj: NSString = item.link! as NSString
16.                     article.representedObject = someObj
17.                     article.action = #selector(self.openBrowser(urlSender:))
18.                     article.title = title
19. 
20.                     //// Get the url from the article and add /favicon.ico to get the image
21.                     //// Will add the image to each article to indicate the source
22.                     let url = URL(string: outline.icon)
23. 
24.                     self.getData(from: url!) { data, response, error in
25.                         guard let data = data, error == nil else { return }
26. 
27. 
28.                         DispatchQueue.main.async() { [weak self] in
29.                             article.image = NSImage(data: data)
30.                             article.image?.size = CGSize(width: 15, height: 15)
31.                         }
32.                     }
33.                     subMenu.addItem(article)
34.                 }
35.             }
36.         }
37.     }
38.     return subMenu
39. }

You have a few ways to correct this:

  • for a very quick test, add a sleep(20) just before line 38. But that's just for test, not a solution for real app.
  • use semaphore, to make sure all operations requests are completed
  • use completion handler
  • or, with iOS 15, use await/async

For completion handler, should be like this (sorry, I could not test in app, hope there's no error here:

var theMenu: NSMenu?
typealias FinishedDownload = (NSMenu) -> Void

func loadRss(outline: Outline, subMenu: NSMenu, completed : @escaping FinishedDownload) {  // Corrected name
    var articleList = [NSMenuItem]()

    let url = URL(string: outline.xmlUrl)!

    AF.request(url).responseRSS() { (response) -> Void in
        if let feed: RSSFeed = response.value {
            for item in feed.items {

                let article = NSMenuItem()
                let timeString = self.formatDate(item: item)
                if timeString != "" {
                    var title = self.shortenText(item: item.title!)
                    title = title + " " + timeString

                    let someObj: NSString = item.link! as NSString
                    article.representedObject = someObj
                    article.action = #selector(self.openBrowser(urlSender:))
                    article.title = title

                    //// Get the url from the article and add /favicon.ico to get the image
                    //// Will add the image to each article to indicate the source
                    let url = URL(string: outline.icon)

                    self.getData(from: url!) { data, response, error in
                        guard let data = data, error == nil else { return }


                        DispatchQueue.main.async() { [weak self] in
                            article.image = NSImage(data: data)
                            article.image?.size = CGSize(width: 15, height: 15)
                        }
                    }
                    subMenu.addItem(article)
                }
            }
            completed(subMenu)
        }
    }
    // No more return return subMenu
}

// Then you call as:
func callIt() {
    theMenu = NSMenu() // Or the initial value of your submenu
    loadRss(outline: someOutline, subMenu: theMenu) {
        subMenu in theMenu = subMenu
    }
}

.

You will find investing discussion here: https://stackoverflow.com/questions/36829749/how-to-wait-for-a-function-to-end-on-ios-swift-before-starting-the-second-one

Note: you have probably misspelled loadRss as laodRss. Not an immediate issue, but may cause you trouble some time.

Accepted Answer

Problem is that you return submenu immediately, before any item has been added:

  • line 6 dispatches to a thread
  • then code continues immediately at line 38, where subMenu is not yet filled
1. func laodRss(outline: Outline, subMenu: NSMenu) -> NSMenu {
2.     var articleList = [NSMenuItem] ()
3. 
4.     let url = URL(string: outline.xmlUrl)!
5. 
6.     AF.request(url).responseRSS() { (response) -> Void in
7.         if let feed: RSSFeed = response.value {
8.             for item in feed.items {
9.                 let article = NSMenuItem()
10.                 let timeString = self.formatDate(item: item)
11.                 if timeString != "" {
12.                     var title = self.shortenText(item: item.title!)
13.                     title = title + " " + timeString
14. 
15.                     let someObj: NSString = item.link! as NSString
16.                     article.representedObject = someObj
17.                     article.action = #selector(self.openBrowser(urlSender:))
18.                     article.title = title
19. 
20.                     //// Get the url from the article and add /favicon.ico to get the image
21.                     //// Will add the image to each article to indicate the source
22.                     let url = URL(string: outline.icon)
23. 
24.                     self.getData(from: url!) { data, response, error in
25.                         guard let data = data, error == nil else { return }
26. 
27. 
28.                         DispatchQueue.main.async() { [weak self] in
29.                             article.image = NSImage(data: data)
30.                             article.image?.size = CGSize(width: 15, height: 15)
31.                         }
32.                     }
33.                     subMenu.addItem(article)
34.                 }
35.             }
36.         }
37.     }
38.     return subMenu
39. }

You have a few ways to correct this:

  • for a very quick test, add a sleep(20) just before line 38. But that's just for test, not a solution for real app.
  • use semaphore, to make sure all operations requests are completed
  • use completion handler
  • or, with iOS 15, use await/async

For completion handler, should be like this (sorry, I could not test in app, hope there's no error here:

var theMenu: NSMenu?
typealias FinishedDownload = (NSMenu) -> Void

func loadRss(outline: Outline, subMenu: NSMenu, completed : @escaping FinishedDownload) {  // Corrected name
    var articleList = [NSMenuItem]()

    let url = URL(string: outline.xmlUrl)!

    AF.request(url).responseRSS() { (response) -> Void in
        if let feed: RSSFeed = response.value {
            for item in feed.items {

                let article = NSMenuItem()
                let timeString = self.formatDate(item: item)
                if timeString != "" {
                    var title = self.shortenText(item: item.title!)
                    title = title + " " + timeString

                    let someObj: NSString = item.link! as NSString
                    article.representedObject = someObj
                    article.action = #selector(self.openBrowser(urlSender:))
                    article.title = title

                    //// Get the url from the article and add /favicon.ico to get the image
                    //// Will add the image to each article to indicate the source
                    let url = URL(string: outline.icon)

                    self.getData(from: url!) { data, response, error in
                        guard let data = data, error == nil else { return }


                        DispatchQueue.main.async() { [weak self] in
                            article.image = NSImage(data: data)
                            article.image?.size = CGSize(width: 15, height: 15)
                        }
                    }
                    subMenu.addItem(article)
                }
            }
            completed(subMenu)
        }
    }
    // No more return return subMenu
}

// Then you call as:
func callIt() {
    theMenu = NSMenu() // Or the initial value of your submenu
    loadRss(outline: someOutline, subMenu: theMenu) {
        subMenu in theMenu = subMenu
    }
}

.

You will find investing discussion here: https://stackoverflow.com/questions/36829749/how-to-wait-for-a-function-to-end-on-ios-swift-before-starting-the-second-one

Note: you have probably misspelled loadRss as laodRss. Not an immediate issue, but may cause you trouble some time.

Thanks for your answer. It took longer than it should but after som rewriting I finally got the program to work as intended. Thank you for your help and guidning and showing the escaping closures.

Function returns an empty array but the same function can return a filled NSMenu
 
 
Q