본문 바로가기
ios 개발일지

[SwiftUI] CoreData를 사용하여 채팅창 구현하기: Update와 Delete

by 리트레서 2024. 7. 20.
 

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

이번 게시물에서는 채팅창을 만들어보면서  CRUD중 Create와 Read를 다루어보도록하겠습니다.CoreData를 사용해보기 위해 간단히 만든 프로젝트이기 때문에 서버를 사용하지 않아 사용자가 메시지를

to-continually-grow.tistory.com

저번 게시물에서 CoreData 프로젝트 추가 및 Create, Read를 다루어보았습니다. 이번 게시물에서는 UpdateDelete를 다루어보겠습니다.

 

 


 

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

 

 

 

 

chatInfoViewchatView를 합치고  updatedelete를 할 수 있게 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: 텍스트를 입력하는 뷰입니다. 수정 버튼을 누르면 isEditOptionEnabledtrue가 되어 수정 중임을 사용자가 알 수 있게 보여줍니다.

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

 

 

이미 작성된 채팅을 길게 누르면 위와 같이 수정 버튼과 삭제 버튼이 생성되고, 수정 버튼을 누르면 아래 채팅창에 "수정 중..." 이라는 문자와 함께 수정 하고자 하는 채팅의 텍스트가 입력됩니다.

 

아직 삭제 로직과 업데이트 로직을 구현하지 않았기 때문에 updatedelete를 구현해 보겠습니다.

 

 

 

 

 

 

2. Update, Delete 구현

updatedelete 함수를 구현했습니다.

// 특정 튜플을 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: Entitycontent를 받아 EntityobjectId 값과 일치하는 Entity를 가져와 content isEdit을 수정

- content: 수정할 채팅 내용

- isEdit: 채팅의 수정 유무 상태

 

deleteChatEnt: Entity를 받아 해당 Entity를 삭제

 

saveData: CoreData의 변경 사항을 저장

 

 

 

 

 

앞에 정의한 UI에서 updateChatEntdeleteChatEnt를 호출합니다.

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