概要
ポイントは
- 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 件のコメント:
コメントを投稿