본문 바로가기
ios 개발일지

[Swiftui] Core Data를 사용하여 채팅창 구현하기: Create와 Read

by 리트레서 2024. 7. 8.

이번 게시물에서는 채팅창을 만들어보면서  CRUD중 Create와 Read를 다루어보도록하겠습니다.

CoreData를 사용해보기 위해 간단히 만든 프로젝트이기 때문에 서버를 사용하지 않아 사용자가 메시지를 올리는 기능만 구현했습니다.


1. CoreData 프로젝트 생성 

프로젝트를 생성할 때 StorageCore Data로 설정합니다.

 

 

 

 

 

제가 사용하는 Xcode 15.3 버전에서는 CoreData를 선택하면 CoreData의 사용 방법을 보여주는 기본 프로젝트가 생성됩니다.

 

 

기본 프로젝트는 우상단의 "+" 버튼을 누르면 현재 시간을 CoreData에 추가하고 Edit을 누르면 CoreData 저장되어 있는 데이터를 삭제할 수도 있는 간단한 프로젝트입니다.

기본 프로젝트를 만들면 프로젝트 이름으로 모델명이 생성되는데 처음에 CoreData를 선택하지 않았다면 파일 추가에서 Data Model을 추가하면 됩니다.

 

 

 

 

 

2.  CoreData 채팅창 만들기

우선 기존 모델을 채팅창 프로젝트에 맞게 수정했습니다.

content:String -> 메시지의 내용

creationTime: String -> 메시지를 보낸 시간 ex) "오전 1시 32분"

isEdit: Boolean -> 메시지의 수정 여부

date: Date -> 메시지를 보낸 날짜 정보

 

 

 

 

이후 기본 프로젝트 코드를 전부 지우고 코드를 작성했습니다.

먼저 CoreData의 데이터를 불러오고 데이터를 저장하기 위한 변수를 선언했습니다.

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>
  
  var body: some View {
    VStack(content: {
      
    })
  }
}

 

@Environment(\.managedObjectContext) var viewContext를 통해 관리 객체 컨테스트(NSManagedObjectContext)를 가져옵니다.

 

@FetchRequest를 통해 CoreData에 저장되어 있는 데이터를 불러옵니다.
entity: ChatEnt.entity() : 데이터를 가져올 엔티티를 지정해줍니다.

 

 

sortDescriptors: [NSSortDescriptor(keyPath: \ChatEnt.date, ascending: true) : 가져올 데이터의 정렬 방법을 정의합니다. ChatEnt 엔티티의 date 속성을 기준으로 데이터를 불러오고 ascending true이므로 오름차순으로 데이터를 불러옵니다.

 

 

private var chattings: FetchedResults<ChatEnt> FetchRequeust에서 가져온 데이터를 chattings라는 변수에 저장합니다.

 

 

 

 

 

이후 CoreData에 저장 될 데이터를 확인하는 UI를 구현했습니다.

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 = ""

  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: {
            chatInfoView(chatting: chatting)
            chatView(chatting: chatting)
          }).padding(.bottom, 10)
        }
      }
    })
  }

  /// 채팅 입력 및 제출
  var inputChatView: some View {
    HStack(content: {
      TextField(
        "",
        text: $chat
      )
      .padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 0))
      .textInputAutocapitalization(.never)
      
      Button(action: {
        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 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))
  }
}

 

headerView: CoreData에 저장되어 있는 데이터의 개수를 보여주는 뷰

 

 

chattingView: CoreData에 저장되어 있는 데이터를 순차적으로 보여주는 뷰 (데이터가 없는 경우 입력된 채팅이 없다는 것을 알리는 TextView를 보여줌)

 

 

chatInfoView: 채팅이 입력 된 시간, 데이터의 수정 유무를 보여주는 뷰

 

 

chatView: 채팅 내용을 보여주는 뷰

 


inputChatView: CoreData에 저장할 데이터를 입력하고 전송하는 뷰

 

아직 데이터가 저장되지 않아 "아직 입력된 채팅이 없습니다." 라고 표시되고 있습니다.

 

 

 

 

 

CoreData에 데이터를 저장해보겠습니다. 우선 파일을 따로 만들고 CoreData에 접근하는 함수를 만들어보겠습니다.

//  CoreDataController.swift
//  TestCoreData

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)
}

/// CoreData에 변경사항을 적용
func saveData(viewContext: NSManagedObjectContext) {
  do {
    try viewContext.save()
  } catch {
    let nsError = error as NSError
    fatalError("error: \(nsError), \(nsError.userInfo)")
  }
}

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)
}

 

func createChatEnt: ChatEnt 엔티티의 인스턴스를 생성하고 CoreData에 저장하는 함수입니다.

viewContext와 저장할 내용(content)을 받아 인스턴스를 할당하고 데이터를 저장합니다.

 

 

func saveData: viewContext를 받아 변경된 데이터 저장 시도를 하는 함수입니다.

 

 

func formatTime: 현재 date를 받아 오전, 오후 현재 시간을 가져와 ex) "오전 4시 59분"와 같이 변환하는 함수입니다.

 

 

 

 

 

 

이후 전송 버튼에 있는 action에서 createChatEnt를 호출합니다.

Button(action: {
  createChatEnt(viewContext: viewContext, content: chat)
  chat = ""
}, label: {
  Image(systemName: "paperplane")
})

 

텍스트를 입력하고 전송 버튼을 누르면 정상적으로 저장되고 불러오는 것을 확인할 수 있습니다.

 

 

 

 

 

 

 

앱을 종료하고 다시 실행해도 데이터가 영구적으로 저장되는 것을 확인할 수 있습니다.


 

 

 


 

이번 게시물에서는 채팅창을 만들어보면서 CRUD중 CreateRead를 다루어보았는데요 다음 게시물에서는 채팅창에서 UpdateDelete를 사용하는 방법을 작성해보겠습니다.