経緯
Swift Playgroundsでメモアプリを作る際、SQLiteやRealmなどを使おうとしたのですがObjective-Cを含んでいる為、使用できませんでした。
という事でCoreDataを使おうと思います。
しかし、Swift PlaygroundsではCoreDataを設定する事ができない為、コードだけで実装します。
作る物
簡単なメモアプリを作ってみようと思います。
CoreData設定
MemoModel.swift
import CoreData
import SwiftUI
@objc(Memo)
class Memo: NSManagedObject {
@NSManaged var index: Int
@NSManaged var title: String
@NSManaged var text: String
}
extension Memo: Identifiable {
var id: Int {
index
}
}
PersistenceController.swift
import CoreData
import SwiftUI
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
let memoEntity = NSEntityDescription()
memoEntity.name = "Memo"
memoEntity.managedObjectClassName = "Memo"
let indexAttribute = NSAttributeDescription()
indexAttribute.name = "index"
indexAttribute.type = .integer64
memoEntity.properties.append(indexAttribute)
let titleAttribute = NSAttributeDescription()
titleAttribute.name = "title"
titleAttribute.type = .string
memoEntity.properties.append(titleAttribute)
let textAttribute = NSAttributeDescription()
textAttribute.name = "text"
textAttribute.type = .string
memoEntity.properties.append(textAttribute)
let model = NSManagedObjectModel()
model.entities = [memoEntity]
let container = NSPersistentContainer(name: "MemoModel", managedObjectModel: model)
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { description, error in
if let error = error {
fatalError("failed with: \(error.localizedDescription)")
}
}
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = true
self.container = container
}
}
ViewModel.swift
import SwiftUI
import CoreData
class ViewModel: ObservableObject {
@Published var index: Int = 0
@Published var title: String = ""
@Published var text: String = ""
@Published var isNewMemo = false
@Published var Item: Memo!
//編集確定 or 新規作成
func CreateMemo(context : NSManagedObjectContext) {
if Item != nil {
Item.index = UnixTime()
Item.title = title
Item.text = text
try! context.save()
Item = nil
isNewMemo.toggle()
index = UnixTime()
title = ""
text = ""
return
}
let newMemo = Memo(context: context)
newMemo.index = UnixTime()
newMemo.title = title
newMemo.text = text
do {
try context.save()
isNewMemo.toggle()
index = UnixTime()
title = ""
text = ""
}
catch {
print(error.localizedDescription)
}
}
//編集時
func EditMemo(item: Memo) {
Item = item
index = item.index
title = item.title
text = item.text
isNewMemo.toggle()
}
//IDの重複を避ける為,UnixTimeをIDに適用
func UnixTime() -> Int {
let dt = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yyyy-MM-dd HH:mm:ss", options: 0, locale: Locale(identifier: "ja_JP"))
let date: Date? = dateFormatter.date(from: dateFormatter.string(from: dt))
let dateUnix: TimeInterval? = date?.timeIntervalSince1970
return Int(dateUnix!)
}
}
Viewに紐付け
MemoApp.swift
import SwiftUI
@main
struct MemoApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
View
ContentView.swift
import SwiftUI
struct ContentView: View {
@StateObject var viewModel = ViewModel()
@Environment(\.managedObjectContext) private var context
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Memo.index, ascending: true)], animation: .spring()) private var results: FetchedResults<Memo>
@State var TrashMemo: FetchedResults<Memo>.Element = FetchedResults<Memo>.Element()
var body: some View {
ZStack(alignment: .bottomTrailing) {
if results.isEmpty {
VStack {
Spacer()
HStack {
Spacer()
Text("メモがありません。")
.fontWeight(.black)
.font(.system(size: 35))
Spacer()
}
Spacer()
}
} else {
List(results) { memo in
Button(action: {
viewModel.EditMemo(item: memo)
}) {
HStack {
Text(memo.title)
Spacer()
Button(action: {
context.delete(memo)
try! context.save()
}) {
Image(systemName: "xmark")
}
.padding(.trailing, 10)
.buttonStyle(PlainButtonStyle())
}
}
}
}
Button(action: {
viewModel.Item = nil
viewModel.title = ""
viewModel.text = ""
viewModel.isNewMemo.toggle()
}) {
ZStack {
Circle()
.frame(width: 60, height: 60)
.foregroundColor(.blue)
.padding()
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.foregroundColor(.white)
}
}
}
.fullScreenCover(isPresented: $viewModel.isNewMemo) {
MemoView(viewModel: viewModel)
}
}
}
MemoView.swift
import SwiftUI
struct MemoView: View {
@ObservedObject var viewModel : ViewModel
@Environment(\.managedObjectContext) var context
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
GeometryReader { geo in
VStack {
HStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Back")
}
Spacer()
Button(action: {
viewModel.CreateMemo(context: context)
}) {
Text("Save")
.foregroundColor(viewModel.title == "" ? Color.gray : .blue)
}
.disabled(viewModel.title == "" ? true : false)
}
.padding(.vertical)
.padding(.horizontal)
ZStack(alignment: .topLeading) {
if viewModel.title.isEmpty {
Text("Title")
.font(.system(size: 30, weight: .black, design: .default))
.foregroundColor(.gray)
.padding(.top, 2)
.padding(.leading, 3)
}
TextField("", text: $viewModel.title)
.font(.system(size: 30, weight: .black, design: .default))
}
.padding(.horizontal)
ZStack(alignment: .topLeading) {
if viewModel.text.isEmpty {
Text("Text")
.font(.system(size: 15, weight: .bold, design: .default))
.foregroundColor(.gray)
.padding(.top, 7)
.padding(.leading, 5)
}
TextEditor(text: $viewModel.text)
.font(.system(size: 15, weight: .bold, design: .default))
.frame(height: geo.size.height-100, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.top)
.padding(.horizontal)
Spacer()
}
.onAppear() {
UITextView.appearance().backgroundColor = .clear
}
.onDisappear() {
UITextView.appearance().backgroundColor = nil
}
}
}
}
完成サンプル