2024年10月24日木曜日

(サンプルコード) SwiftUI で RSS フィードリーダ

(サンプルコード) SwiftUI で RSS フィードリーダ

概要

ポイントは

  • Swift 純正の XMLParser と XMLParserDelegate を使い RSS 情報をパースする
  • XMLParser で CDATA があるときは専用のメソッドを使う
  • CDATA 内にある html をパースして更に必要な情報が取得できる
  • XML の必要な情報はモデルにして SwiftUI 上でバインドする

環境

  • macOS 15.0.1
  • Xcode 16.0

RSSFeedParser.swift

RSS フィードをパースする処理を管理するクラスです

import Foundation
import Combine

class RSSFeedParser: NSObject, ObservableObject, XMLParserDelegate {
    @Published var items: [RSSItem] = []
    
    // 各種要素を管理する変数
    private var currentElement = ""
    private var currentTitle = ""
    private var currentLink = ""
    private var currentDescription = ""
    private var currentContent = ""
    private var parsingItem = false
    
    // RSS フィールドをロードしパースを開始する
    func loadRSSFeed(url: URL) {
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data else { return }
            
            let parser = XMLParser(data: data)
            parser.delegate = self
            parser.parse()
        }
        task.resume()
    }
    
    // XMLParser の Delegate メソッド、要素が見つかったら最初にコールされる
    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        currentElement = elementName
        if elementName == "item" {
            currentTitle = ""
            currentLink = ""
            currentDescription = ""
            currentContent = ""
            parsingItem = true
        }
    }
    
    // XMLParser の Delegate メソッド、要素内の一般タグでも見つかったらコールされる
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        if parsingItem {
            if currentElement == "title" {
                currentTitle += string
            } else if currentElement == "link" {
                currentLink += string
            } else if currentElement == "description" {
                currentDescription += string
            }
        }
    }
    
    // XMLParser の Delegate メソッド、要素内の CDATA タグでも見つかったらコールされる
    func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) {
        if parsingItem && currentElement == "content:encoded" {
            if let cdataString = String(data: CDATABlock, encoding: .utf8) {
                currentContent += cdataString
            }
        }
    }
    
    // XMLParser の Delegate メソッド、要素が終了したらコールされる
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if elementName == "item" {
            let imageSrc = extractImageSrc(from: currentContent)
            let item = RSSItem(title: currentTitle, link: currentLink, description: currentDescription, content: currentContent, imageSrc: imageSrc)
            DispatchQueue.main.async {
                self.items.append(item)
            }
            parsingItem = false
        }
    }
  
    // CDATA から特定の html 属性情報を抽出するヘルパーメソッド
    func extractImageSrc(from htmlString: String) -> String? {
        let pattern = "<img[^>]+src=\"([^\"]+)\""
        
        if let regex = try? NSRegularExpression(pattern: pattern, options: []) {
            let range = NSRange(htmlString.startIndex..., in: htmlString)
            if let match = regex.firstMatch(in: htmlString, options: [], range: range) {
                if let srcRange = Range(match.range(at: 1), in: htmlString) {
                    return String(htmlString[srcRange])
                }
            }
        }
        return nil
    }
}

RSSItem.swift

RSS フィードをパースした結果のデータを管理するモデルクラスです

import Foundation
import SwiftUI

struct RSSItem: Identifiable {
    let id = UUID()
    let title: String
    let link: String
    let description: String
    let content: String  // html 付きの description
    let imageSrc: String?
}

ContentView.swift

SwiftUI で RSS フィードのモデル情報を表示します

import SwiftUI

struct ContentView: View {
    @StateObject private var rssFeedParser = RSSFeedParser()
    
    var body: some View {
        NavigationView {
            List(rssFeedParser.items) { item in
                VStack(alignment: .leading) {
                    Text(item.title)
                        .font(.headline)
                    if let imageSrc = item.imageSrc {
                        AsyncImage(url: URL(string: imageSrc)) { image in
                            image
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(height: 200)
                        } placeholder: {
                            ProgressView()
                        }
                    }
                    Text(item.description)
                        .font(.body)
                    Text(item.link)
                        .font(.subheadline)
                        .foregroundColor(.blue)
                }
            }
            .navigationTitle("RSS Feed")
            .onAppear {
                if let url = URL(string: "https://figmanendoroiddb.blog.fc2.com/?xml") { // RSS URLをここに設定
                    rssFeedParser.loadRSSFeed(url: url)
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

testApp.swift

実行メインクラス

import SwiftUI

@main
struct testApp: App {  
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

最後に

SwiftUI を使った場合 UI とモデルのデータバインドがほぼ自動でやってくれるので楽です

表示するコンテンツの情報も SwiftUI 側で調整するだけで RSS フィードを取得する側のロジックでは全く気にする必要がないです

0 件のコメント:

コメントを投稿