[Swiftui] Core Data를 사용하여 채팅창 구현하기: Create와 Read
이번 게시물에서는 채팅창을 만들어보면서 CRUD중 Create와 Read를 다루어보도록하겠습니다.CoreData를 사용해보기 위해 간단히 만든 프로젝트이기 때문에 서버를 사용하지 않아 사용자가 메시지를
to-continually-grow.tistory.com
저번 게시물에서 CoreData 프로젝트 추가 및 Create, Read를 다루어보았습니다. 이번 게시물에서는 Update와 Delete를 다루어보겠습니다.
1. Update, Delete를 위한 UI 구현
기존 코드는 CoreData ChatEnt에 저장된 데이터를 순차적으로 가져와 chatInfoView(채팅을 입력한 시간을 정의), chatView(채팅 내용을 정의)를 호출하여 채팅을 사용자가 확인 할 수 있게 했습니다.
// ChatEnt에 저장되어 있는 데이터를 반복적으로 가져옴
ForEach(chattings, id: \.self) { chatting in
HStack(content: {
chatInfoView(chatting: chatting)
chatView(chatting: chatting)
}).padding(.bottom, 10)
}
// 채팅 시간 및 수정 정보를 보여주는 뷰
fileprivate struct chatInfoView: View {
let chatting: ChatEnt
var body: some View {
VStack(content: {
if(chatting.isEdit) {
VStack(content: {
// 채팅을 수정했을 경우
HStack(content: {
Spacer()
Text("수정됨")
.font(.system(size:10))
.foregroundStyle(Color.gray)
})
Spacer()
})
}
VStack(content: {
Spacer()
// 채팅을 입력한 시간
HStack(content: {
Spacer()
Text(chatting.creationTime!)
.font(.system(size:11))
.foregroundStyle(Color.gray)
})
})
})
}
}
// 채팅 내용을 보여주는 뷰
fileprivate struct chatView: View {
let chatting: ChatEnt
var body: some View {
HStack(content: {
// 채팅 내용
Text(chatting.content!)
.font(.system(size:15))
.padding(.all, 7)
})
.background(Color(red: 249/255, green: 245/255, blue: 244/255))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
chatInfoView와 chatView를 합치고 update와 delete를 할 수 있게 UI를 구현했습니다.
// 입력 중인 채팅 내용
@State var chat: String = ""
// 수정 버튼과 삭제 버튼이 있는 지
@State var isOptionEnabled: Bool = false
@State var editingChatEnt: ChatEnt? = nil
// 현재 수정 중인 지
@State var isEditOptionEnabled = false
/// 채팅 입력 및 제출
var inputChatView: some View {
VStack(content: {
// 현재 수정 중이면
if(isEditOptionEnabled) {
Divider().background(Color.black)
HStack(content: {
Button(action: {
chat = ""
isEditOptionEnabled = false
isOptionEnabled = false
}, label: {
Image(systemName: "x.circle").resizable().frame(width: 20, height: 20)
})
Text("수정 중...")
.font(.system(size: 15))
.foregroundStyle(Color.gray)
Spacer()
})
.padding(EdgeInsets(top: 0, leading: 10, bottom: 3, trailing: 0))
}
HStack(content: {
TextField(
"",
text: $chat
)
.padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 0))
.textInputAutocapitalization(.never)
// 제출 버튼
Button(action: {
// 수정 중일 때
if(isEditOptionEnabled) {
// 업데이트 로직 추가 예정
chat = ""
isEditOptionEnabled = false
isOptionEnabled = false
} else {
createChatEnt(viewContext: viewContext, content: chat)
chat = ""
}
}, label: {
Image(systemName: "paperplane")
})
.padding(.all, 10)
})
.frame(width: 280, height: 40)
.background(Color(red: 249/255, green: 245/255, blue: 244/255))
.padding(.bottom, 30)
})
}
fileprivate struct chatView: View {
@Binding var chat: String
let chatting: ChatEnt
@Binding var isOptionEnabled: Bool
@Binding var editingChatEnt: ChatEnt?
@Binding var isEditOptionEnabled: Bool
var body: some View {
VStack(content: {
HStack(content: {
// 채팅 시간 및 수정 정보를 보여주는 뷰
VStack(spacing: 3, content: {
// 채팅을 수정했을 경우
if(chatting.isEdit) {
HStack(content: {
Spacer()
Text("수정됨")
.font(.system(size:10))
.foregroundStyle(Color.gray)
})
}
// 채팅을 입력한 시간
HStack(content: {
Spacer()
Text(chatting.creationTime!)
.font(.system(size:11))
.foregroundStyle(Color.gray)
})
})
// 채팅 내용
HStack(content: {
Text(chatting.content!)
.font(.system(size:15))
.padding(.all, 7)
})
.background(Color(red: 249/255, green: 245/255, blue: 244/255))
.clipShape(RoundedRectangle(cornerRadius: 10))
.onLongPressGesture(perform: {
isOptionEnabled = true
editingChatEnt = chatting
})
})
// 수정 버튼과 삭제 버튼
if(isOptionEnabled && editingChatEnt == chatting) {
HStack(content: {
Spacer()
Button(action: {
isEditOptionEnabled = true
chat = chatting.content!
}, label: {
Image(systemName: "square.and.pencil").resizable().frame(width: 16, height: 16)
})
Button(action: {
// 삭제 로직 추가 예정
}, label: {
Image(systemName: "trash").resizable().frame(width: 16, height: 16)
})
})
}
Spacer().frame(height: 7)
})
}
}
chat: String -> 채팅 창에 입력 중인 내용
isOptionEnabled: Bool -> 수정 버튼과 삭제 버튼의 활성화 상태
isEditOptionEnabled: Bool -> 현재 수정 중인지 상태를 나타냄
editingChatEnt: ChatEnt -> 현재 수정 버튼과 삭제 버튼이 나타난 ChatEnt를 저장
inputChatView: 텍스트를 입력하는 뷰입니다. 수정 버튼을 누르면 isEditOptionEnabled이 true가 되어 수정 중임을 사용자가 알 수 있게 보여줍니다.
if(isEditOptionEnabled) {
Divider().background(Color.black)
HStack(content: {
Button(action: {
chat = ""
isEditOptionEnabled = false
isOptionEnabled = false
}, label: {
Image(systemName: "x.circle").resizable().frame(width: 20, height: 20)
})
Text("수정 중...")
.font(.system(size: 15))
.foregroundStyle(Color.gray)
Spacer()
})
.padding(EdgeInsets(top: 0, leading: 10, bottom: 3, trailing: 0))
}
이미 작성된 채팅을 길게 누르면 위와 같이 수정 버튼과 삭제 버튼이 생성되고, 수정 버튼을 누르면 아래 채팅창에 "수정 중..." 이라는 문자와 함께 수정 하고자 하는 채팅의 텍스트가 입력됩니다.
아직 삭제 로직과 업데이트 로직을 구현하지 않았기 때문에 update와 delete를 구현해 보겠습니다.
2. Update, Delete 구현
update와 delete 함수를 구현했습니다.
// 특정 튜플을 update
func updateChatEnt(viewContext: NSManagedObjectContext, chatEnt: ChatEnt, content: String) {
do {
// NSManagedObjectID로 해당 objectID를 갖고 있는 ChatEnt 찾기
if let chatEntUpdate = try viewContext.existingObject(with: chatEnt.objectID) as? ChatEnt {
chatEntUpdate.content = content
chatEntUpdate.isEdit = true
saveData(viewContext: viewContext)
}
} catch {
let nsError = error as NSError
fatalError("error: \(nsError), \(nsError.userInfo)")
}
}
// 특정 튜플을 delete
func deleteChatEnt(viewContext: NSManagedObjectContext, chatEnt: ChatEnt) {
viewContext.delete(chatEnt)
saveData(viewContext: viewContext)
}
/// CoreData에 변경사항을 적용
func saveData(viewContext: NSManagedObjectContext) {
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("error: \(nsError), \(nsError.userInfo)")
}
}
updateChatEnt: Entity와 content를 받아 Entity의 objectId 값과 일치하는 Entity를 가져와 content와 isEdit을 수정
- content: 수정할 채팅 내용
- isEdit: 채팅의 수정 유무 상태
deleteChatEnt: Entity를 받아 해당 Entity를 삭제
saveData: CoreData의 변경 사항을 저장
앞에 정의한 UI에서 updateChatEnt와 deleteChatEnt를 호출합니다.
// updateChatEnt 추가
if(isEditOptionEnabled) {
updateChatEnt(viewContext: viewContext, chatEnt: editingChatEnt!, content: chat)
chat = ""
isEditOptionEnabled = false
isOptionEnabled = false
} else {
createChatEnt(viewContext: viewContext, content: chat)
chat = ""
}
// deleteChatEnt 추가
Button(action: {
deleteChatEnt(viewContext: viewContext, chatEnt: chatting)
}, label: {
Image(systemName: "trash").resizable().frame(width: 16, height: 16)
})
정상적으로 삭제와 수정이 작동되는 것을 확인 할 수 있습니다.
전체 코드
contentView
// ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) var viewContext
@FetchRequest(
entity: ChatEnt.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \ChatEnt.date, ascending: true)]
)
private var chattings: FetchedResults<ChatEnt>
// 입력 중인 채팅 내용
@State var chat: String = ""
// 수정 버튼과 삭제 버튼이 있는 지
@State var isOptionEnabled: Bool = false
@State var editingChatEnt: ChatEnt? = nil
// 현재 수정 중인 지
@State var isEditOptionEnabled = false
var body: some View {
VStack(content: {
headerView
chattingView
inputChatView
})
}
var headerView: some View {
VStack(content: {
Text("채팅 수: \(String(chattings.count))")
Divider()
})
}
var chattingView: some View {
ScrollView(content: {
if chattings.count == 0 {
Text("아직 입력된 채팅이 없습니다.")
} else {
ForEach(chattings, id: \.self) { chatting in
HStack(content: {
chatView(viewContext: viewContext ,chat: $chat, chatting: chatting, isOptionEnabled: $isOptionEnabled, editingChatEnt: $editingChatEnt, isEditOptionEnabled: $isEditOptionEnabled)
}).padding(.bottom, 10)
}
}
})
}
/// 채팅 입력 및 제출
var inputChatView: some View {
VStack(content: {
// 현재 수정 중이면
if(isEditOptionEnabled) {
Divider().background(Color.black)
HStack(content: {
Button(action: {
chat = ""
isEditOptionEnabled = false
isOptionEnabled = false
}, label: {
Image(systemName: "x.circle").resizable().frame(width: 20, height: 20)
})
Text("수정 중...")
.font(.system(size: 15))
.foregroundStyle(Color.gray)
Spacer()
})
.padding(EdgeInsets(top: 0, leading: 10, bottom: 3, trailing: 0))
}
HStack(content: {
TextField(
"",
text: $chat
)
.padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 0))
.textInputAutocapitalization(.never)
// 제출 버튼
Button(action: {
// 수정 중일 때
if(isEditOptionEnabled) {
updateChatEnt(viewContext: viewContext, chatEnt: editingChatEnt!, content: chat)
chat = ""
isEditOptionEnabled = false
isOptionEnabled = false
} else {
createChatEnt(viewContext: viewContext, content: chat)
chat = ""
}
}, label: {
Image(systemName: "paperplane")
})
.padding(.all, 10)
})
.frame(width: 280, height: 40)
.background(Color(red: 249/255, green: 245/255, blue: 244/255))
.padding(.bottom, 30)
})
}
}
fileprivate struct chatView: View {
@FetchRequest(
entity: ChatEnt.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \ChatEnt.date, ascending: true)]
)
private var chattings: FetchedResults<ChatEnt>
var viewContext: NSManagedObjectContext
@Binding var chat: String
let chatting: ChatEnt
@Binding var isOptionEnabled: Bool
@Binding var editingChatEnt: ChatEnt?
@Binding var isEditOptionEnabled: Bool
var body: some View {
VStack(content: {
HStack(content: {
// 채팅 시간 및 수정 정보를 보여주는 뷰
VStack(spacing: 3, content: {
// 채팅을 수정했을 경우
if(chatting.isEdit) {
HStack(content: {
Spacer()
Text("수정됨")
.font(.system(size:10))
.foregroundStyle(Color.gray)
})
}
// 채팅을 입력한 시간
HStack(content: {
Spacer()
Text(chatting.creationTime!)
.font(.system(size:11))
.foregroundStyle(Color.gray)
})
})
// 채팅 내용
HStack(content: {
Text(chatting.content!)
.font(.system(size:15))
.padding(.all, 7)
})
.background(Color(red: 249/255, green: 245/255, blue: 244/255))
.clipShape(RoundedRectangle(cornerRadius: 10))
.onLongPressGesture(perform: {
isOptionEnabled = true
editingChatEnt = chatting
})
})
if(isOptionEnabled && editingChatEnt == chatting) {
HStack(content: {
Spacer()
Button(action: {
isEditOptionEnabled = true
chat = chatting.content!
}, label: {
Image(systemName: "square.and.pencil").resizable().frame(width: 16, height: 16)
})
Button(action: {
deleteChatEnt(viewContext: viewContext, chatEnt: chatting)
}, label: {
Image(systemName: "trash").resizable().frame(width: 16, height: 16)
})
})
}
Spacer().frame(height: 7)
})
}
}
CoreDataController
// CoreDataController.swift
import CoreData
/// 채팅 튜플을 생성
func createChatEnt(viewContext: NSManagedObjectContext, content: String) {
let newChatting = ChatEnt(context: viewContext)
newChatting.content = content
newChatting.date = Date()
newChatting.creationTime = formatTime(from: Date())
newChatting.isEdit = false
saveData(viewContext: viewContext)
}
// 특정 튜플을 update
func updateChatEnt(viewContext: NSManagedObjectContext, chatEnt: ChatEnt, content: String) {
do {
// NSManagedObjectID로 해당 objectID를 갖고 있는 ChatEnt 찾기
if let chatEntUpdate = try viewContext.existingObject(with: chatEnt.objectID) as? ChatEnt {
chatEntUpdate.content = content
chatEntUpdate.isEdit = true
saveData(viewContext: viewContext)
}
} catch {
let nsError = error as NSError
fatalError("error: \(nsError), \(nsError.userInfo)")
}
}
// 특정 튜플을 delete
func deleteChatEnt(viewContext: NSManagedObjectContext, chatEnt: ChatEnt) {
viewContext.delete(chatEnt)
saveData(viewContext: viewContext)
}
/// CoreData에 변경사항을 적용
func saveData(viewContext: NSManagedObjectContext) {
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("error: \(nsError), \(nsError.userInfo)")
}
}
/// date를 입력 받아 시간을 보기 좋게 반환하는 함수
func formatTime(from date: Date) -> String {
// DateFormatter를 생성합니다.
let dateFormatter = DateFormatter()
// 시간 형식을 설정합니다. 'a'는 AM/PM을 나타냅니다.
dateFormatter.dateFormat = "a h:mm"
// 한국어로 설정합니다.
dateFormatter.locale = Locale(identifier: "ko_KR")
// 날짜를 문자열로 변환합니다.
return dateFormatter.string(from: date)
}
'ios 개발일지' 카테고리의 다른 글
[SwiftUI] 말풍선 구현하기: View 만들기, CustomTriangle, SpeechBubble (0) | 2025.03.11 |
---|---|
[SwiftUI, UIKit] 편지 구현하기: textEditor, UITextView, UIViewRepresentable, NSLayoutManager (0) | 2025.02.11 |
[Swiftui] Core Data를 사용하여 채팅창 구현하기: Create와 Read (0) | 2024.07.08 |
[Xcode] 탭 간격, 들여쓰기 간격 변경 및 Re-Indent (0) | 2024.07.06 |
[Xcode] Simulator 이전 버전 추가 (0) | 2024.04.12 |