[Swift] 50 BBS閲覧アプリiOS版製作 その4 スレッド一覧表示 / kanna

[Mac M2 Pro 12CPU, Ventura 13.6, iOS 17.0.3, Xcode 15.0]

選択した掲示板のスレッド一覧を表示させました。

掲示板のHTMLをパースして要素を取り出し、ChatGPTでC++の関数をSwiftへ変換して動作させました。FLTKに相当するViewについては自分で叩き台を考える必要があります。

Swiftの文法が分かっていればもっとスムーズに書けるはずですが、まあこれから学んでいきます。

次は選択スレッドの表示およびdatファイルの保存に関する実装に取り組みます。

プロジェクトの構成
import Foundation
import Kanna

struct Thread : Identifiable {
    let id = UUID()
    let threadID: String
    let threadTitle: String
    let threadNum: Int
}

func getThreads(url: String) async -> [Thread] { // urlはboard.url
    var threads: [Thread] = []
    var threadNumDict: [String: Int] = [:]
    var threadNum = 0
    
    let boardURL = url + "subback.html"
    guard let url = URL(string: boardURL) else { return [] }

    do {
        let htmlString = try String(contentsOf: url, encoding: .shiftJIS)
        guard let doc = try? HTML(html: htmlString, encoding: .shiftJIS) else { return [] }

        for link in doc.xpath("//*[name()='small']/*[name()='a']") {
            guard let id = link["href"], let title = link.text else { continue }
            
            print(id)
            
            let truncatedID = String(id.dropLast(4))
                
            let idRegex = try! NSRegularExpression(pattern: "^[0-9]+$")
            let idRange = NSRange(location: 0, length: truncatedID.utf16.count)
            let isNumericID = idRegex.firstMatch(in: truncatedID, options: [], range: idRange) != nil
            
            if !isNumericID {
                continue
            }

            if threadNumDict[truncatedID] == nil {
                threadNum += 1
                threadNumDict[truncatedID] = threadNum
            }

            threads.append(Thread(threadID: truncatedID, threadTitle: title, threadNum: threadNumDict[truncatedID]!))
            
            
        }
    } catch {
        print("Error: \(error)")
    }

    return threads
}
import SwiftUI

struct BoardView: View {
    @State private var boardCategories: [BoardCategory] = []

    var body: some View {
        NavigationView {
            VStack{
                Text("板一覧")
                List(boardCategories) { boardCategory in
                    DisclosureGroup(boardCategory.category) {
                        ForEach(boardCategory.boards) { board in
                            NavigationLink(destination: ThreadView(board: board)) {
                                ScrollView {
                                    Text(board.name)
                                }
                            }
                        }
                    }
                }
            }
            Spacer()
        }
        .padding()
        .task {
            boardCategories = await getBoards()
        }
    }
}

struct BoardView_Previews: PreviewProvider {
    static var previews: some View {
        BoardView()
    }
}
import SwiftUI

struct ThreadView: View {
    @State private var threads: [Thread] = []
    var board: Board?

    var body: some View {
        VStack {
            Text("スレッド一覧")
            List{
                ForEach(threads) { thread in
                    Text(thread.threadTitle)
                }
            }
            Spacer()
        }
        .padding()
        .task {
            if let board = board {
               threads = await getThreads(url: board.url)
            }
        }
    }
}

struct ThreadView_Previews: PreviewProvider {
    static var previews: some View {
        ThreadView()
    }
}

[Swift] 49 BBS閲覧アプリiOS版製作 その3 掲示板一覧表示 / kanna(libxml2)でスクレイピング

[Mac M2 Pro 12CPU, Ventura 13.6, iOS 17.0.3, Xcode 15.0]

BBS閲覧アプリiOS版の初期画面として掲示板一覧をツリー表示させました。カテゴリーのリストを登場順に並べるのに少し手間取りました。

ChatGPT(gpt-4)をうまく誘導しながら完成させました。構造体で解決しなくて安易にクラスにしようとするなど一々大げさな反応をするので、こちらで手綱を引かないと無駄に大掛かりなコードになります。しかもどんどん的外れな方向に進んでいくので要注意です。

今日ここまでのgpt-4利用料は330円($2.19)ほど。新しい言語なのでgpt-3.5はなるべく避けたいところですが、やはりgpt-4はここぞという時に使うようにします。

import Foundation
import Kanna

struct Board : Identifiable {
    let id = UUID()
    let url: String
    let name: String
    let category: String
    let categoryNum: Int
}

struct BoardCategory : Identifiable {
    let id = UUID()
    let category: String
    let boards: [Board]
    let categoryNum: Int
}


func getBoards() async -> [BoardCategory] {
    var boards: [Board] = []
    var categoryNumDict: [String: Int] = [:]
    var categoryNum = 0
    guard let url = URL(string: "掲示板一覧のURL") else { return [] }

    do {
        let htmlString = try String(contentsOf: url, encoding: .shiftJIS)
        guard let doc = try? HTML(html: htmlString, encoding: .shiftJIS) else { return [] }

        for link in doc.xpath("//*[name()='a']") {
            guard let url = link["href"], let name = link.text else { continue }

            var category = "XXX" // カテゴリー名の初期値
            var prevElement = link.previousSibling
            while prevElement != nil && prevElement?.tagName != "b" {
                prevElement = prevElement?.previousSibling
            }
            if let prevElement = prevElement {
                category = prevElement.text ?? "XXX"
            }

            if categoryNumDict[category] == nil {
                categoryNum += 1
                categoryNumDict[category] = categoryNum
            }

            boards.append(Board(url: url, name: name, category: category, categoryNum: categoryNumDict[category]!))
        }
    } catch {
        print("Error: \(error)")
    }
    
    let groupedBoards = Dictionary(grouping: boards, by: { $0.category })
    
    var boardCategories: [BoardCategory] = groupedBoards.map { BoardCategory(category: $0.key, boards: $0.value, categoryNum: categoryNumDict[$0.key]!) }
    boardCategories.sort { $0.categoryNum < $1.categoryNum }

    return boardCategories
}

[Swift] 48 BBS閲覧アプリiOS版製作 その2 Swift Package Manager / Kanna

[Mac M2 Pro 12CPU, Ventura 13.6, iOS 17.0.3, Xcode 15.0]

BBS閲覧アプリiOS版のプロジェクトにHTMLパーサー Kannaを導入しました。パッケージ管理システム Swift Package Managerを使いました。

CocoaPodsとは打って変わっての使いやすさでした。さすが純正は違います。2019年10月リリースのXcode 11からコマンドラインだけでなくIDEで使用可能になったようです。

[File] – [Add Package Dependencies]から右上窓にGitHubのURLを入力する