SwiftUI - CoreDataのRelationshipについて

1. Relationshipについて

ここでは、Relationshipについてまとめておこうと思います。Relationshipは、CoreDataで複数のエンティティを作成したときに、各エンティティを関連付けることができる機能です。

2. Relationshipの設定

image

Relationshipでは、各エンティティをどのような関係(To One または To Many)で関連付けをするか決める必要があります。その設定をするには、Relationshipの項目を選択すると右に表示されるメニューの中のTypeという項目から設定できます。その設定をどうするか、アイテム(Item)とタグ(Tags)というエンティティがある場合で考えてみます。

1 対 1 の関係のRelationshipの場合

「1 対 1」の関係の場合は、アイテムに入れられるタグはひとつ、タグもアイテムをひとつしか入れられない設定になります。

例えば、リンゴ(アイテム)に、果物というタグをつけた場合、オレンジには果物というタグつけられない設定になります(果物というタグはリンゴに使っているため)

  • リンゴ(アイテム) = 果物(タグ)OK
  • オレンジ(アイテム)= 果物(タグ)NG

1 対 多 の関係のRelationshipの場合

「1 対 多」の関係の場合は、アイテムに入れられるタグはひとつ、タグは複数アイテムを入れられる設定になります。

上記の例えに置き換えると、果物(タグ)の中には、リンゴとオレンジが入れられます。ですが、アイテムに複数のタグはつけられません。

  • リンゴ(アイテム) = 果物(タグ)OK
  • オレンジ(アイテム)= 果物(タグ)OK
  • イチゴ(アイテム) = 果物、甘い(タグ)NG

多 対 多 の関係のRelationshipの場合

「多 対 多」の関係の場合は、アイテムに複数のタグを入れられ、タグも複数アイテムを入れられる設定になります。

  • リンゴ(アイテム) = 果物, 甘い(タグ)OK
  • 果物(タグ) = リンゴ、オレンジ、イチゴ(アイテム)OK

3. Relationshipを実装してみる

実際に、Relationshipを使ってみようと思います。ここでは、「1 対 多」の関係のRelationshipを実装してみようと思います。下記がCoreDataの設定です。

image image

ENTITIES

  • Item
  • Tags

Attributes

  • Item -> iName : String
  • Tags -> tName : String

Relationships

  • Item -> tags - Tags - item (Type: To One)
  • Tags -> item - Item - tags (Type: To Many)

image

下記は、ビューが開いた時にタグにデータがない場合、データを読み込むようにして、ボタンを押すとタグごとにアイテムデータを出力するというような内容です。

import SwiftUI
import CoreData

struct ContentView: View {
  @Environment(\.managedObjectContext) private var moc
  
  @FetchRequest(
    entity: Tags.entity(),
    sortDescriptors: [NSSortDescriptor(keyPath: \Tags.tName, ascending: true)],
    animation: .default
  ) private var tags: FetchedResults<Tags>
  
  
  var body: some View {
    List {
      Section() {
        Button("Add") { addData() }
      }
      ForEach(tags, id:\.self) { tag in
        Section(header: Text(tag.tName ?? "")) {
          ForEach(tag.itemArray, id:\.self) {item in
            Text(item.iName ?? "")
          }
        }
      }
    }.listStyle(InsetGroupedListStyle())
    
    .onAppear {
      loadData()
    }
  }
  
  func loadData() {

    if tags.isEmpty {
      let tagsData = [
        ["Fruit"],
        ["Vegetable"]
      ]
      
      for tags in tagsData {
        let newTags = Tags(context: moc)
        newTags.tName = tags[0]
      }
      try? self.moc.save()
    }
  }
  
  func addData() {
    
    let itemData = [
      ["Apple", "Fruit"],
      ["Orange", "Fruit"],
      ["Tomato", "Vegetable"]
    ]
    
    for item in itemData {
      let newItem = Item(context: moc)
      newItem.iName = item[0]
      
      let tagsFetch: NSFetchRequest<Tags> = Tags.fetchRequest()
      tagsFetch.predicate = NSPredicate(format: "tName == %@", item[1])
      
      let result = try! moc.fetch(tagsFetch)
      
      if !result.isEmpty {
        result.first!.addToItem(newItem)
      }
    }
    try? moc.save()
  }
  
}

extension Tags {
  var itemArray: [Item] {
    return (item as? Set<Item> ?? [] ).sorted {$0.iName! < $1.iName!}
  }
}

fetch(_:) - 指定されたフェッチ要求の条件を満たすオブジェクトの配列を返す

4. iOS15から

ここでは、Relationshipを使ってアイテムをタグでセクション分割をするというようなことをしましたが、iOS15からSectionedFetchRequestというセクションの分割するためのものが使えるようになるみたいです。

fetch(_:) | Apple Developer Documentation
SectionedFetchRequest | Apple Developer Documentation
Type Casting — The Swift Programming Language (Swift 5.5)