[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を入力する

[Swift] 47 BBS閲覧アプリiOS版製作 その1 CocoaPods / Kanna

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

BBS閲覧アプリC++版をiOSへ移植します。

HTMLパーサーとしてKannaを使います。パッケージ管理システム CocoaPodsを使って導入しました。CocoaPodsはHomebrewでインストールしています。

悪戦苦闘しつつようやくビルドエラーにならない状態まで持ってこれましたが、この段階でApple純正のパッケージ管理システム Swift Package Manager(SPM)の存在を知りました。

次回、SPMでKannaを入れ直します。愚痴ってもしょうがないですが、Xcodeを使ったコーディングは色々とめんどくさいです。ライブラリ一つ導入するまでの時間的学習コストが高すぎます。ネット情報は古い陳腐化したものが優勢で油断するとミスリードされてしまいます。古い情報なのに最近の日付で更新となっているからタチが悪い。慣れているのもありますが、C++の方がのびのびと書けてストレスが少ないです。

掲示板リストはまだ表示できない

[Swift] 46 メモアプリ製作 その13 背景色のグラジエント

[Mac M2 Pro 12CPU, Ventura 13.6, watchOS 10.0.1, Xcode 15.0]

Apple Watchは表示面が小さくメモによっては全体を一度に表示できません。表示されているところがメモ全体のどこに当たるのか分かるようにするため、背景色をグラジエントにしました。

これでメモの下部を表示していても背景色が薄いので上部が隠れていると判断でき、メモの見落としを防ぐことができます。

スクロールバーでも同様の効果がありますが、watchOSでは常時表示にはできないため役に立ちません。

改良前:メモ1を見落とすおそれあり(実際は付番していないため)
改良後:メモ上部が隠れていることが分かる
.background(LinearGradient(gradient: Gradient(colors: 
[Color.blue, Color.blue.opacity(0.0)]), startPoint: .top, endPoint: .bottom))
struct ContentView: View {
    @Environment(\.managedObjectContext)var viewContext

    @FetchRequest(
    entity: Note.entity(),
    sortDescriptors: [NSSortDescriptor(key: "creationDate", ascending: false)])
    private var contents: FetchedResults<Note>

    var body: some View {
        NavigationView{
            List{
                ForEach(contents){content in
                    NavigationLink{
                        if((content.content?.isEmpty) == false){
                            DraftAppleWatch(text:content.content!, note: content)
                        }
                    }label:{
                        if((content.content?.isEmpty) == false){
                            Text(content.content!)
                            .font(.system(size: 20))
                            .background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.blue.opacity(0.0)]), startPoint: .top, endPoint: .bottom))
                        }
                    }
                }
                .onDelete(perform:deleteContent)
            }
            .navigationTitle("メモリスト")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar{
                ToolbarItem(placement:.bottomBar){
                     NavigationLink{
                        DraftAppleWatch()
                    }label:{
                        Text("+")
                        .font(.system(size: 24))
                    }
                }
            }
        }
        .accentColor(.blue)
    }
<以下略>
ScrollView {
       Text(content.content!)
       .font(.system(size: 20))
       .background(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.blue.opacity(0.0)]), startPoint: .top, endPoint: .bottom))
}
スクロールしないと
右上のバーは表示されない

[Swift] 45 Apple WatchのComplication改良 コーナー表示で効かない修飾子

[Mac M2 Pro 12CPU, Ventura 13.6, watchOS 10.0.1, Xcode 15.0]

自製カレンダーのコーナーComplicationを改良しました。日付と元号の位置を入れ替えています。日付を本体として配置すると曜日がはみ出るため、ラベルの方に移動させました。

最初は日付と曜日が本体として収まるようフォント指定あるいは自動調整しようとしたものの、関連する修飾子はどれも効きませんでした。

また右上の天気に色を付けたいのですが、類似アプリを探すか自分で作るしかなさそうです。自製するなら天気APIを調査するところから着手でしょうか。

右下コーナー:曜日がはみ出る
曜日はラベルへ移動
struct ComplicationCorner : View {
    var entry: Provider.Entry
    
    var body: some View {
        Text(getFormattedDate())
//        Text(getFormattedDate() + getFormattedWeekday()) // 曜日は収まらないのでラベルへ
        .foregroundColor(.green)
        .minimumScaleFactor(0.5) // 効かない
        .font(.system(size: 60)) // 効かない
        .lineLimit(nil)          // 効かない
        .widgetCurvesContent()
        .widgetLabel {
            Text(getFormattedYear() + " " + getFormattedWeekday())
            .foregroundColor(.yellow)
            .font(.system(size: 20))
        }
        .containerBackground(for: .widget){
            Color.blue
        }
     }
<以下略>

[Swift] 44 Apple WatchのComplication改良 / watchOS 10による表示不良 解決 / containerBackground

[Mac M2 Pro 12CPU, Ventura 13.6, watchOS 10.0.1, Xcode 15.0]

[Swift] 42で報告したComplicationの表示不良が解決しました。Viewコンテンツの新属性として.containerBackgroundを追加するだけでした。

watchOSの大型アップデートの際に新属性記述が必須になることがあるようです。Apple Developer ForumsとMac OTAKARAの記事を参考に修正しました。

WWDC23で前もって紹介されており、Appleのやらかしではありませんでした。

ただApple Watch SEのシミュレータでは新属性なしでも正常表示しているのが解せないです。アップデートしたwatchOS 10では対応が必要で、watchOS 10そのものには不要なのでしょうか。

解決しない場合は、検証用追加機としてApple Watch Series 8あたりを購入しようかと覚悟していたので助かりました。

SE 第2世代でもいいから追加機導入して2台開発体制にする方がいいかな。iOSとは違ってwatchOSアップデートはサブ機でお試しが無難ですから。

watchOS 10でも正常表示になった
struct MemoToolAW_ComplicationsEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Image(systemName: "square.and.pencil")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .foregroundColor(.white)
        .background(Color.blue)
        .containerBackground(for: .widget){  // watchOS 10から必須
            Color.blue
        }
    }
}

参考
Apple Developer Forums
Mac OTAKARA

[Swift] 43 Apple WatchのComplication改良 / accessoryCornerの円弧状表示 / watchOS 10

[Mac M2 Pro 12CPU, Ventura 13.6, watchOS 10.0.1, Xcode 15.0]

watchOS 10でComplicationのコーナー全体を円弧状表示できるようになりました。

これまではラベルのみ円弧状表示で本体は水平表示でした。水平表示では”令5″の2文字でスペース的に限界だったのを”令和5″の3文字に増やすことができました。

ただし前回記事に書いている通り、実機(Apple Watch SE)ではまともに表示できていません。シミュレータでのみ成功しています。

struct ComplicationCorner : View {
    var entry: Provider.Entry
    
    var body: some View {
        Text(getFormattedYear())
        .font(.system(size: 22))
        .widgetCurvesContent()  // これを追記、"令和5"が円弧状に表示される
        .foregroundColor(.green)
        .widgetLabel {
            Text(getFormattedDate() + getFormattedWeekday())
            .foregroundColor(.yellow)
        }
     }

[Swift] 42 Apple WatchのComplication改良 / watchOS 10による表示不良

[Mac M2 Pro 12CPU, Ventura 13.6, watchOS 10.0.1, Xcode 15.0]

watchOS 10になってAppleさん早速やらかしています。

自製のComplicationがまともに表示されなくなりました。

watchOSはAppleや専門業者以外ダウングレードできないので、Appleがバグ修正するまで放置するしかありません。

XcodeはApple Watch実機をなかなか認識できなくてビルドがままならず、watchOSアプリの開発は時間が掛かるばかりです。結局、iPhoneをMacに有線で再接続するとXcode上でApple Watch実機を認識できました。

iPhone & Apple Watchは家族のヘルスケア管理端末にして、普段使いはPixel & Pixel Watchにしようかと思い始めています。ファミリーウォッチ機能がなければとっくに見切りを付けているところです。

Complicationの
表示不良(内側上・左, 右下)

[Swift] 41 メモアプリ製作 その12 ToolbarItemのplacement新機能 watchOS 10

[Mac M2 Pro 12CPU, Ventura 13.6, watchOS 10.0.1, Xcode 15.0]

watchOSが10にバージョンアップし、watchOS版アプリのToolbarItemの位置をbottomに設定できるようになりました(.bottomBarと記述)。

これまでは”.automatic”にして上部にしか配置できませんでした。これでメモアプリwatchOS版のUIを改善できました。

この種の機能がバージョン10にしてようやく導入というのは結構遅いと思います。急ピッチで機能の拡充をお願いしたいものです。

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext)var viewContext

    @FetchRequest(
    entity: Note.entity(),
    sortDescriptors: [NSSortDescriptor(key: "creationDate", ascending: false)])
    private var contents: FetchedResults<Note>

    var body: some View {
        NavigationView{
            List{
                ForEach(contents){content in
                    NavigationLink{
                        if((content.content?.isEmpty) == false){
                            DraftAppleWatch(text:content.content!, note: content)
                        }
                    }label:{
                        if((content.content?.isEmpty) == false){
                            Text(content.content!)
                        }
                    }
                }
                .onDelete(perform:deleteContent)
            }
            .navigationTitle("メモリスト")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar{
                ToolbarItem(placement:.bottomBar){ // ここを変更
                     NavigationLink{
                        DraftAppleWatch()
                    }label:{
                        Text("+")
                        .font(.system(size: 24))
                        

                    }
                }
            }
        }
        .accentColor(.blue)
    }
<以下略>