Sponsor Link
環境&対象
- macOS Catalina 10.15.7
- Xcode 12.2
- iOS 14.2
いろいろと、コードを書いてきたので、一度 リファクタリング します。
ここまでの Model コード
//
// MyTODOModel.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import Foundation
import CoreData
import os
struct TODOItem {
var id: UUID? = nil
var title: String = ""
var detail: String = ""
let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.TODOItem", category: "TODOItem")
init(_ title: String,_ detail: String = "") {
self.id = UUID()
self.title = title
self.detail = detail
}
}
// depend on CoreData
struct TODOItemStore {
var items:[TODOItem] = []
let container: NSPersistentContainer
let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.TODOItemStore", category: "TODOItemStore")
init(_ inMemory:Bool ) {
container = NSPersistentContainer(name: "MyTODO")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
fetchFromCoreData()
}
mutating func fetchFromCoreData() {
let request:NSFetchRequest = CDTODOItem.fetchRequest()
do {
items = try container.viewContext.fetch(request).map(TODOItem.init)
} catch {
logger.error("error in fetching data from coredata")
}
}
}
// MARK: CoreData dependent part
extension TODOItem {
init(_ cdItem: CDTODOItem) {
self.id = cdItem.id
self.title = cdItem.title ?? ""
self.detail = cdItem.detail ?? ""
}
}
extension TODOItemStore {
// create Item , then put into coredata
mutating func createTODOItem(_ title: String, _ detail: String = "") {
let newItem = TODOItem(title, detail)
let newCDItem = CDTODOItem(context: container.viewContext)
newCDItem.id = newItem.id
newCDItem.title = newItem.title
newCDItem.detail = newItem.detail
items.append(newItem)
save()
}
mutating func removeTODOItem(_ item: TODOItem) {
guard let id = item.id else { return }
guard let index = items.firstIndex(where: {$0.id == item.id}) else { return }
let request:NSFetchRequest = CDTODOItem.fetchRequest()
request.predicate = NSPredicate.init(format: "id == %@", id as CVarArg)
let deleteRequest = NSBatchDeleteRequest.init(fetchRequest: request)
do {
try self.container.viewContext.execute(deleteRequest)
} catch {
print(error)
}
items.remove(at: index)
save()
}
func save() {
if !container.viewContext.hasChanges { return }
do {
try container.viewContext.save()
} catch {
print(error)
}
}
}
気になる点は、「TODOItemStore で TODOItem の配列を保持しておく必要はあるか?」です。
パフォーマンス上の問題が出るまでは、必要な時に、CoreData から fetch するようにしてみます。
こうすることで、TODOItemStore が保持している [TODOItem] と CoreData の保持している CDTODOItem の整合性を考える必要がなくなります。
なんとなく [TODOItem] を保持しておいて、@Published 指定すると、変更に対して 自動更新が起こりそうですが、Model 内部で持っている配列が変更されても、更新はされないです。
ということで、TODOItemStore は TODOItem を保持せず、リクエストに応じて、CoreData から fetch したものをベースに、TODOItem の配列を返すとしました。
//
// MyTODOModel.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import Foundation
import CoreData
import os
struct TODOItem {
var id: UUID? = nil
var title: String = ""
var detail: String = ""
let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.TODOItem", category: "TODOItem")
init(_ title: String,_ detail: String = "") {
self.id = UUID()
self.title = title
self.detail = detail
}
}
// depend on CoreData
struct TODOItemStore {
let container: NSPersistentContainer
let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.TODOItemStore", category: "TODOItemStore")
init(_ inMemory:Bool ) {
container = NSPersistentContainer(name: "MyTODO")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
func fetchFromCoreData() -> [TODOItem] {
var items:[TODOItem] = []
let request:NSFetchRequest = CDTODOItem.fetchRequest()
do {
items = try container.viewContext.fetch(request).map(TODOItem.init)
} catch {
logger.error("error in fetching data from coredata")
}
return items
}
}
// MARK: CoreData dependent part
extension TODOItem {
init(_ cdItem: CDTODOItem) {
self.id = cdItem.id
self.title = cdItem.title ?? ""
self.detail = cdItem.detail ?? ""
}
}
extension TODOItemStore {
// create Item , then put into coredata
func createTODOItem(_ title: String, _ detail: String = "") -> TODOItem {
let newItem = TODOItem(title, detail)
let description = NSEntityDescription.entity(forEntityName: "CDTODOItem", in: container.viewContext)!
let newCDItem = CDTODOItem(entity: description, insertInto: container.viewContext)
newCDItem.id = newItem.id
newCDItem.title = newItem.title
newCDItem.detail = newItem.detail
save()
return newItem
}
func removeTODOItem(_ item: TODOItem) {
guard let id = item.id else { return }
let request: NSFetchRequest = NSFetchRequest(entityName: "CDTODOItem")
request.predicate = NSPredicate.init(format: "id == %@", id as CVarArg)
let deleteRequest = NSBatchDeleteRequest.init(fetchRequest: request)
do {
try self.container.viewContext.execute(deleteRequest)
} catch {
print(error)
}
save()
}
func save() {
if !container.viewContext.hasChanges { return }
do {
try container.viewContext.save()
} catch {
print(error)
}
}
}
ViewModel の作成
ViewModel は、ObseravableObject に準拠し、Model である TODOItemStore を保持するようにします。
//
// MyTODOViewModel.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import Foundation
import SwiftUI
import CoreData
import os
import Combine
class MyTODOViewModel : ObservableObject {
@Published var todoItemStore: TODOItemStore
let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.MyTODOViewModel", category: "MyTODOViewModel")
init() {
self.todoItemStore = TODOItemStore(false) // not in-memory CoreData
}
}
TODOItem の配列も、@Published 指定で保持するようになると思いますが、必要になったら実装することにします。
App, View 実装前のクリーンアップ
App と View には、テンプレートコードが残っているので、ViewModel や View の実装を進める前にきれいにしておきます。
App のクリーンアップ
//
// MyTODOApp.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import SwiftUI
@main
struct MyTODOApp: App {
// (1)
@StateObject var viewModel: MyTODOViewModel = MyTODOViewModel()
var body: some Scene {
WindowGroup {
ContentView()
// (2)
.environmentObject(viewModel)
}
}
}
- App と同じライフサイクルを持つように、ViewModel を生成します。
- EnvironmentObject に登録し、すべての下位ビューから参照できるようにします
Persistence は使用しませんので、ファイルを削除して構いません。
View のクリーンアップ
ContentView は、以下のように修正しました。
- EnvironmentObject として、 ViewModel を参照します
- fetch 等の CoreData を直接操作する箇所を削除しました
- テンプレートで用意されているリストの追加・削除は、そのまま使えそうですので、ダミーのデータ(String) を用意して残しています。
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import SwiftUI
import CoreData
struct ContentView: View {
@EnvironmentObject var viewModel: MyTODOViewModel
@State private var items:[String] = ["test1", "test2"]
var body: some View {
NavigationView {
List {
ForEach(items, id:\.self) { item in
Text("\(item)")
}
.onDelete(perform: deleteItems)
}
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
#endif
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
private func addItem() {
withAnimation {
print("addItem called")
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
print("deleteItems called")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(MyTODOViewModel())
}
}
View, ViewModel 共に、実装を開始できる状態になったので、次回は、UITest, View, ViewModel を実装していきます。
テストを忘れずに
コードを変更したら、テストが通ることを確認します。
Model のコードを変えたのでそこに合わせるためにテストコードの修正も必要となりました。
//
// Model_Tests.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import XCTest
@testable import MyTODO
class Model_Tests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func test_createItem_newItem_withCorrectValues() {
// prep model with coredata
let model = TODOItemStore(true)
XCTAssertEqual(model.items.count, 0)
// create new item
_ = model.createTODOItem("Item Title", "item detail")
XCTAssertEqual(model.items.count, 1)
// get item from model
let item = model.items.first!
// test : compare properties
XCTAssertEqual(item.title, "Item Title")
XCTAssertEqual(item.detail, "item detail")
}
func test_removeItem_existingItem_shouldBeVanished() {
let model = TODOItemStore(true)
XCTAssertEqual(model.items.count, 0)
// create new item
_ = model.createTODOItem("Item Title", "item detail")
XCTAssertEqual(model.items.count, 1)
// get item from model
let item = model.items.first!
model.removeTODOItem(item)
XCTAssertEqual(model.items.count, 0)
}
}
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link