0

I have an XMLParser set up in Swift in my app, and want to be able to within the widget extension, parse the RSS feed, and return that data in the widget. However, I'm having some issues getting the two Swift files to talk to each other. In the Parser, I have:

struct RSSItem {
    var title: String
    var description: String
    var link: String
    var pubDate: String
}

// download xml from the internet

class FeedParser: NSObject, XMLParserDelegate
{
    private var rssItems: [RSSItem] = []
    private var currentElement = ""
    private var currentTitle: String = ""
    private var currentDescription: String = ""
    private var currentPubDate: String = ""
    private var currentLink: String = ""
    
    private var parserCompletionHandler: (([RSSItem]) -> Void)?
    
    func parseFeed(url: String, completionHandler: (([RSSItem]) -> Void)?)
    {
        self.parserCompletionHandler = completionHandler
        
        let request = URLRequest(url: URL(string: url)!)
        let urlSession = URLSession.shared
        let task = urlSession.dataTask(with: request) { (data, response, error) in
            guard let data = data else {
                if let error = error {
                    print(error.localizedDescription)
                }
                
                return
            }
            
            /// parse our xml data
            let parser = XMLParser(data: data)
            parser.delegate = self
            parser.parse()
        }
        
        task.resume()
    }
    // MARK: - XML Parser Delegate

    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        if currentElement == "item" {
            currentTitle = ""
            currentDescription = ""
            currentPubDate = ""
            currentLink = ""
        }
    }
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        switch currentElement {
        case "title": currentTitle += string
        case "description": currentDescription += string
        case "pubDate" : currentPubDate += string
        case "link" : currentLink += string
        default: break
        }
    }
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if elementName == "item" {
            let rssItem = RSSItem(title: currentTitle, description: currentDescription, link: currentLink, pubDate: currentPubDate)
            self.rssItems.append(rssItem)
        }
    }
    func parserDidEndDocument(_ parser: XMLParser) {
        parserCompletionHandler?(rssItems)
    }
    
    func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
        print(parseError.localizedDescription)
    }
}

In the Widget, I have:

struct Provider: TimelineProvider {
    @State private var rssItems:[RSSItem]?
    let feedParser = FeedParser()
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), title:"News", description: "Stuff happened", link: "Http://link", pubDate: "The day it posted")
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), title:"News", description: "Stuff happened", link: "Http://link", pubDate: "The day it posted")
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        feedParser.parseFeed(url: "") {(rssItems) in
            self.rssItems = rssItems
            let currentDate = Date()
            for hourOffset in 0 ..< 5 {
                let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
                let entry = SimpleEntry(date: entryDate, title:rssItems.title, description: rssItems.description, link: rssItems.link, pubDate: rssItems.pubDate)
                entries.append(entry)
            
        }
        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
       
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let title: String
    let description: String
    let link: String
    let pubDate: String
}

However, in the TimelineProvider section, it tells me that rssItems has no member named title, description, pubDate, or link

user717452
  • 33
  • 14
  • 73
  • 149
  • This might help you: [How to refresh Widget data?](https://stackoverflow.com/questions/63976424/how-to-refresh-widget-data) – pawello2222 Sep 22 '20 at 19:37
  • @pawello2222 Thanks for the link, but I may be in too deep 'cause every bit of that went right over my head. First time in Swift not Obj-C, and this is all throwing me off. – user717452 Sep 22 '20 at 19:55
  • That answer can be easily applied to your case - instead of NetworkManager you just use FeedParser. But if it's your first time with Swift, I don't recommend creating widgets yet - start with a *standard* app and get used to Swift *and* SwiftUI first. – pawello2222 Sep 22 '20 at 19:59
  • @pawello2222 Trust me, I totally get that, but alas, this is where I am. I have the parser built, but just run into so many issues when trying to get its data to pass into the Widget file. I updated the SimpleEntry in the widget to include the values needed from the XML, but still run into issues trying to get the parser to run. Within the struct for the TimelineProvider, am I supposed to run the feed parser, because it throws so many errors when I attempt to do that. – user717452 Sep 22 '20 at 20:12
  • @pawello2222 Understood, I updated the OP to include the relevant code from the Widget file, showing what the problem is. I THINK(?) that is where I would call the code to parse the xml, but you'll see it isn't seeing any members for the title link pub date or description within the rssItems – user717452 Sep 22 '20 at 22:08

1 Answers1

0

You can't use @State variables outside SwiftUI views - which means you can't use them in a TimelineProvider.

You can do it with WidgetCenter.shared.reloadAllTimelines():

  1. Update FeedParser to store parsed RSSItems and notify WidgetCenter when finished:
class FeedParser: NSObject, XMLParserDelegate {
    var rssItems: [RSSItem] = [] // make public, here you will store parsed RSSItems

    func parseFeed(url: String) { // remove `completionHandler` form the function signature
        ...
    }

    func parserDidEndDocument(_ parser: XMLParser) {
        // instead of calling `completionHandler` force reload the timeline
        WidgetCenter.shared.reloadAllTimelines()
    }
}
  1. Get the first RSSItem from the FeedParser when creating the timeline and create an entry from it:
struct Provider: TimelineProvider {
    // no `@State` variables here

    let feedParser = FeedParser()

    ...

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        guard !feedParser.rssItems.isEmpty else { return }
        let entry = SimpleEntry(date: Date(), rssItem: feedParser.rssItems[0])
        
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}
  1. For all this to work you need a way to know when the parseFeed function must be called again (to refresh items).

You may try to observe notifications for your feed and, when received, call:

feedParser.parseFeed(url: "some_feed")

(You need to use the same FeedParser instance for this, so it might be necessary to move the FeedParser out of the Provider)

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • I think this will actually solve a problem I was having as well with Widgets not refreshing when new content was added. However, I can't add in `WidgetCenter.shared.reloadAllTimelines()` as it can't find WidgetCenter in scope – user717452 Sep 23 '20 at 21:02
  • @user717452 Did you import `WidgetKit`? – pawello2222 Sep 23 '20 at 21:03
  • Yes, but then it gives me an error that WidgetKit is only available in iOS 14.0 or newer. I definitely have the base SDK set to 14. – user717452 Sep 23 '20 at 21:04
  • @user717452 Then you can do `if #available(iOS 14.0, *)` – pawello2222 Sep 23 '20 at 21:05
  • Ok, so I had done some work through the day, and got the Widget to display how I wanted, except for it not refreshing with new ones. Adding in the WidgetCenter part instead of completionHandler only now gives me the placeholder view, with no content on the widget. Taking it back out and just having completionHandler makes it show the item that was the newest back when it was first working, and not the newer ones. – user717452 Sep 23 '20 at 21:12
  • My parser code is https://ghostbin.co/paste/rpgbx and my widget code is https://ghostbin.co/paste/4e34a It is working, but for some reason, never updates, based off time or shutting app down completely and relaunching, stuck with the old widget – user717452 Sep 23 '20 at 21:15
  • If I remove the completionHandler from the function signature, I get an extra trailing closure error on the line ` feedParser.parseFeed(url: "https://fritchcoc.wordpress.com/feed") {(rssItems) in ` within my widget – user717452 Sep 30 '20 at 12:43
  • And removing the completion handler in the didEndDocument simply gives me a black widget. – user717452 Sep 30 '20 at 12:45
  • And wouldn't the solution with completion handler show me a different news entry based on hour? That's not what I'm wanting. I'm simply only wanting the most recent entry which is why I have it as rssItem[0] – user717452 Sep 30 '20 at 12:46
  • @user717452 I removed unnecessary code, the completion handler version is better. What do you mean by *most recent entry*? Do you want your feedParser to refresh data automatically? – pawello2222 Sep 30 '20 at 12:48
  • The app is for a church and part of the app is showing users latest news, usually only one a day or every couple days. I'd like the widget to show whatever the last post was on the RSS feed, and update when there is a new one. – user717452 Sep 30 '20 at 13:03
  • @user717452 How will you know when there's a new one, ie when to refresh? Do you want to do it every day at let's say midnight? – pawello2222 Sep 30 '20 at 13:06
  • That's one of the reasons I was trying to trigger the reloadAllTimelines. I thought maybe I could do that when a push notification was received, but that hasn't been working. – user717452 Sep 30 '20 at 13:07
  • I don't want to do at midnight because there are times when there might be 3 posts in one day and nothing for a couple days after – user717452 Sep 30 '20 at 13:08
  • If there's only one entry I look for, wouldn't my code be constantly triggered for the .atEnd since only the one entry? – user717452 Sep 30 '20 at 13:13
  • @user717452 Ok, I think I understand your problem now - please see the updated answer. – pawello2222 Sep 30 '20 at 13:22
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/222314/discussion-between-user717452-and-pawello2222). – user717452 Sep 30 '20 at 13:56