SwiftUI - リストとお気に入りとフィルター

1. はじめに

この記事は以前書いたリストとページとJSONという記事の続きです。なのでファイルやファイル名などはその記事の内容から使用しています。この記事は、SwiftUIチュートリアルのリストにお気に入り機能をつけてフィルターをかけるという処理を理解するために書き直してみたものです。

2. リストをコンポーネント化

リストとページとJSONではリストの内容をContentViewにそのまま出力していたので、SwiftUIチュートリアルの内容に合わせて、新しくListView.swiftというファイルを作成して、そこにリストの内容を記述するようにします。

import SwiftUI

struct ListView: View {
  var body: some View {
    NavigationView {
      List(allData, id: \.id) {item in
        NavigationLink(
          destination: DetailView(data: item)) {
          RowView(data: item)
        }
      }
      .navigationTitle("List")
    }
  }
}

struct ListView_Previews: PreviewProvider {
    static var previews: some View {
        ListView()
    }
}

ContentViewListView()を呼び出します。

struct ContentView: View {
  var body: some View {
    ListView()
  }
}

3. お気に入り機能の設定

はじめにJSONファイルのContentData.jsonisFavoriteを追加します。

[
  {
    "id": 1,
    "title": "title1",
    "content": "content1",
    "isFavorite": true,
  },
  {
    "id": 2,
    "title": "title2",
    "content": "content2",
    "isFavorite": false,
  },
  {
    "id": 3,
    "title": "title3",
    "content": "content3",
    "isFavorite": false,
  }
]

DataType.swiftにもisFavoriteという項目を追加し、HashableIdentifiableも一緒に追加します。これらを追加することでList(allData, id: \.id)id: \.idの部分が不要になります。

import Foundation

struct DataType: Hashable, Codable, Identifiable {
  var id: Int
  var title: String
  var content: String
  var isFavorite: Bool
}

Hashable - オプション、配列、範囲などの他のいくつかの型は、型引数が同じものを実装すると、自動的にハッシュ可能になる

RowView.swiftisFavoritetrueなら星マークを表示する処理を追加します。

struct RowView: View {
  var data: DataType
  
  var body: some View {
    HStack {
      Text(data.title)
      
      Spacer()
      //vv追加
      if data.isFavorite {
        Image(systemName: "star.fill")
          .foregroundColor(.yellow)
      }
    }
  }
}

struct RowView_Previews: PreviewProvider {
  static var previews: some View {
    Group {
      RowView(data: allData[0])
      RowView(data: allData[1])
    }
    .previewLayout(.fixed(width: 300, height: 70))
  }
}

4. フィルターをかける

ListView.swiftに星マークがついたものだけ表示するというフィルター処理を追加します。この時点でリストに星マークがついたものだけ表示されていればちゃんとフィルターが動作しています。

struct ListView: View {
  @State private var showFavoritesOnly = true //<<追加
  
  //vv追加
  var filterData: [DataType] {
    allData.filter {item in
      (!showFavoritesOnly || item.isFavorite)
    }
  }
  
  var body: some View {
    NavigationView {
      //vv追加
      List(filterData) { item in
        NavigationLink(destination: DetailView(data: item)) {
          RowView(data: item)
        }
      }
      .navigationTitle("List")
    }
  }
}

5. フィルターのON/OFFボタンを追加する

alt

Toggle()を使ってshowFavoritesOnlyの値(初期値をtrueからfalseに変更しておく)を切り替えできるようにします。Listの出力もForEachに変更します。これでフィルターのON/OFFが可能になります。

struct ListView: View {
  @State private var showFavoritesOnly = false //<<変更
  
  var filterData: [DataType] {
    allData.filter {item in
      (!showFavoritesOnly || item.isFavorite)
    }
  }
  
  var body: some View {
    NavigationView {
      List{

        //vv追加
        Toggle(isOn: $showFavoritesOnly) {
          Text("Filter Toggle")
        }
        //vv変更
        ForEach(filterData) { item in
          NavigationLink(destination: DetailView(data: item)) {
            RowView(data: item)
          }
        }
      }
      .navigationTitle("List")
    }
  }
}

6. オブジェクトを監視可能にする

ここからは、お気に入りのON/OFFボタンを追加するため、先に環境作りをします。Combineフレームワーク、ObservableObject@PublishedでJSONデータの変更を監視するクラスをModelData.swiftに作成します。

import Foundation
import Combine

final class ModelData: ObservableObject {
  @Published var allData: [DataType] = load("ContentData.json")
}

func load<T: Decodable>(_ filename: String) -> T {
  ...
}

Combine - 時間の経過とともに変化する可能性のある値を非同期で処理する
ObservableObject - プロパティが変更される前に、変更された値を発行するパブリッシャーを合成する
@Published - 属性でマークされたプロパティを公開する型

7. EnvironmentObjectを使ってデータを共有させる

EnvironmentObjectを使って監視対象に変更あったときに各ビューにデータを共有できるようにします。

struct ListView: View {
  @EnvironmentObject var modelData: ModelData //<<追加
  @State private var showFavoritesOnly = false
  
  var filterData: [DataType] {
    //VV追加
    modelData.allData.filter {item in
      (!showFavoritesOnly || item.isFavorite)
    }
  }
  
  var body: some View {
    NavigationView {
      List{
        
        Toggle(isOn: $showFavoritesOnly) {
          Text("Filter Toggle")
        }
        
        ForEach(filterData) { item in
          NavigationLink(destination: DetailView(data: item)) {
            RowView(data: item)
          }
        }
      }
      .navigationTitle("List")
    }
  }
}

struct ListView_Previews: PreviewProvider {
  static var previews: some View {
    ListView()
      .environmentObject(ModelData()) //<<追加
  }
}
struct DetailView: View {
  ...
}

struct DetailView_Previews: PreviewProvider {
  static var previews: some View {
    DetailView(data: ModelData().allData[0]) //追加
  }
}
struct RowView: View {
  ...
}

struct RowView_Previews: PreviewProvider {
  static var allData = ModelData().allData //追加
  
  static var previews: some View {
    Group {
      RowView(data: allData[0])
      RowView(data: allData[1])
    }
    .previewLayout(.fixed(width: 300, height: 70))
  }
}
struct ContentView: View {
  
  var body: some View {
    ListView()
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
      .environmentObject(ModelData()) //追加
  }
}

EnvironmentObject - 親ビューまたは祖先ビューによって提供される監視可能なオブジェクトのプロパティラッパー

8. StateObjectを使用してインスタンス化

Appファイル(Appファイルは「プロジェクト名 + App」というファイル名でプロジェクト作成時に自動で作成されます)にStateObjectを追加しModelDataのインスタンス化をします。この時点で、各ビューでエラーがでていないか確認します。

@main
struct TestApp: App {
  @StateObject private var modelData = ModelData() //追加
  
  var body: some Scene {
    WindowGroup {
        ContentView()
          .environmentObject(modelData) //追加
    }
  }
}

StateObject - 監視可能なオブジェクトをインスタンス化するプロパティラッパー

9. お気に入りのON/OFFボタンを作成

FavoriteButton.swiftというファイルを作成して、お気に入りのON/OFFができるボタンを作成します。

struct FavoriteButton: View {
  @Binding var isSet: Bool

  var body: some View {
    Button(action: {
      isSet.toggle()
    }) {
      Image(systemName: isSet ? "star.fill" : "star")
      .foregroundColor(isSet ? Color.yellow : Color.gray)
    }
  }
}

struct FavoriteButton_Previews: PreviewProvider {
  static var previews: some View {
    FavoriteButton(isSet: .constant(true))
  }
}

10. お気に入りのON/OFFボタンを詳細ページに追加

alt

お気に入りのON/OFFボタンを詳細ページに追加します。リストと詳細ページのidの紐付けをして詳細ページでの変更をリストの方にも反映させるというような処理を下記でしています。

struct DetailView: View {
  @EnvironmentObject var modelData: ModelData //<<追加
  var data: DataType
  
  //vv追加
  var dataIndex: Int {
    modelData.allData.firstIndex(where: { $0.id == data.id })!
  }
  var body: some View {
    HStack {
      Text(data.content)
      
      FavoriteButton(isSet: $modelData.allData[dataIndex].isFavorite) //<<追加
    }
    .navigationTitle(Text(verbatim: data.title))
    .navigationBarTitleDisplayMode(.inline)
  }
}

struct DetailView_Previews: PreviewProvider {
  static let modelData = ModelData() //<<追加
  
  static var previews: some View {
    DetailView(data: ModelData().allData[0])
      .environmentObject(modelData) //<<追加
  }
}

firstIndex(where:) - 条件に合う最初のインデックスを返す

続き??の記事>> ユーザー入力と編集

Hashable | Apple Developer Documentation
Combine | Apple Developer Documentation
ObservableObject | Apple Developer Documentation
Published | Apple Developer Documentation
Environmentobject | Apple Developer Documentation
Stateobject | Apple Developer Documentation