wheatandcatの開発ブログ

技術系記事を投稿してます

SwiftUIでMacアプリを作ってみる②:UserDefaultsでデータを保持する

概要

前回の記事では、SwiftUI で RSS リーダーを作ってみた。
今回はその拡張として、RSS フィードの表示 / 非表示を切り替える設定を追加したので紹介する。

www.wheatandcat.me

PR

github.com

追加機能

  • Config 画面に RSS フィードの表示 / 非表示を切り替えるトグルを追加
  • 設定データの保持には UserDefaults を使用

UserDefaultsとは

developer.apple.com

  • UserDefaults はアプリ上のデータを永続的に保存したい時に使用
  • 現状の SwiftUI には AppStorage という、同じくデータを永続的に保存できるものがあるが、こちらは View 用に簡略化して使用できるものという認識

実装

Model を変更時に UserDefaults で保存するように修正

RssFeedReader/Modules/FeedViewModel.swift

final class FeedViewModel: ObservableObject {
    // 省略

    private enum DefaultsKey {
        static let feeds = "feeds.v1"
    }

    func saveFeeds() {
        do {
            let data = try JSONEncoder().encode(feeds)
            UserDefaults.standard.set(data, forKey: DefaultsKey.feeds)
        } catch {
            print("❌ Failed to save feeds:", error)
        }
    }
}
  • FeedViewModel クラスに saveFeeds メソッドを追加
  • UserDefaultsfeeds.v1 のキーで feeds の JSON エンコードしたデータを保存
final class FeedViewModel: ObservableObject {
    @Published var feeds: [Feed] = [] {
        didSet {
            saveFeeds()
        }
    }
}
  • feedsdidSet を設定
  • 新しい値が代入されるたびに saveFeeds が呼ばれ、UserDefaults に保存される

起動時に Model に保存したデータを読み込ませる

RssFeedReader/Modules/FeedViewModel.swift

final class FeedViewModel: ObservableObject {
    // 省略
    func loadFeeds() {
        guard
            let data = UserDefaults.standard.data(forKey: DefaultsKey.feeds)
        else {
            feeds = []
            return
        }

        do {
            feeds = try JSONDecoder().decode([Feed].self, from: data)
        } catch {
            print("❌ Failed to load feeds:", error)
            feeds = []
        }
    }
}
  • UserDefaultsfeeds.v1 のキーにデータが存在すれば JSON デコードして feeds に設定
init() {
    loadFeeds()

    // 初回起動で空ならデフォルトを入れる
    if feeds.isEmpty {
        feeds = Self.defaultFeeds
    }
}
  • 起動時に保存されていたデータが feeds に復元される

表示 / 非表示の UI を作成

RssFeedReader/Pages/SettingView.swift

struct SettingView: View {
    @ObservedObject var vm: FeedViewModel

    List {
        Section("RSSフィード") {
            ForEach($vm.feeds, id: \.self) { $feed in
                Toggle(isOn: $feed.show) {
                    Text(feed.url)
                        .lineLimit(1)
                        .truncationMode(.middle)
                }
                .toggleStyle(.switch)
            }
        }
    }
}
  • show の値を Toggle にバインドして表示 / 非表示を切り替え
func reloadAll() async {
    await withTaskGroup(of: (String, [FeedItem]).self) { group in
        for feed in feeds {
            group.addTask {
                if feed.show == false { return (feed.url, []) }
                return (feed.url, [])
            }
        }
    }
}
  • RSS フィード取得時に show == false のものは取得しない

まとめ

  • 上記の実装で RSS フィードの表示 / 非表示の機能が実装できた
  • 実際にアプリを作ってみると、React で実装する時との思想の違いがよく分かって面白かった
  • React と SwiftUI を比較すると、SwiftUI の方が View とロジックをしっかり分割する思想に感じている