首页 > 解决方案 > 在 iOS 14 的 WidgetView 扩展中解析 RSS XML

问题描述

我在我的应用程序中使用 Swift 设置了 XMLParser,并且希望能够在小部件扩展中解析 RSS 提要,并在小部件中返回该数据。但是,我在让两个 Swift 文件相互通信时遇到了一些问题。在解析器中,我有:

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)
    }
}

在小部件中,我有:

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
}

但是,在 TimelineProvider 部分,它告诉我 rssItems 没有名为 title、description、pubDate 或 link 的成员

标签: swiftxml-parsingwidgetios14widgetkit

解决方案


你不能@State在 SwiftUI 视图之外使用变量——这意味着你不能在TimelineProvider.

你可以这样做WidgetCenter.shared.reloadAllTimelines()

  1. 更新 FeedParser 以存储解析的 RSSItems 并WidgetCenter在完成时通知:
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. 在创建时间线时从 FeedParser 中获取第一个 RSSItem 并从中创建一个条目:
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. 要使所有这些工作,您需要一种方法来知道何时parseFeed必须再次调用该函数(以刷新项目)。

您可以尝试观察有关您的提要的通知,并在收到通知后致电:

feedParser.parseFeed(url: "some_feed")

(您需要为此使用相同的FeedParser实例,因此可能需要将 FeedParser 移出 Provider)


推荐阅读