카테고리 없음

UIKit) TIL # 37 포켓몬 연락처 앱 과제 (Lv 5 ~ 8)

yjuni22 2024. 12. 10. 20:59

Lv 1 ~ 4 Link

https://yjuni22.tistory.com/57

 

UIKit) TIL # 35 포켓몬 연락처 앱 과제 (Lv 1 ~ 4)

과제 시작 전 구성 예상 새로운 과제는 연락처 앱이다.포켓몬 API 를 통한 랜덤 이미지 생성과 CoreData를 활용해 연락처 데이터를 저장하는 방식이다.API 연결에 대한 실습과 CoreData를 활용해볼 수

yjuni22.tistory.com

 

Lv 5, 6

코어데이터를 활용하여 PhoneBookView에 입력된 정보 저장하기
저장과 동시에 메인화면으로 돌아오기
메인화면 돌아왔을 때 방금 저장된 데이터 보이기
이름순으로 정렬되게 하기

 

Level 5 는 상세 페이지인 PhoneBookView 에 유저가 입력한 정보들을

네비게이션 바 버튼인 '적용' 버튼을 누를 시 코어데이터에 저장, 메인화면이동(popView)

그리고 tableView reload() 를 ViewWillApear 에 해주면 될 것 같다.

 

우선 코어데이터에 무엇을 담을지 설정해주어야 한다.

 

코어데이터에 이미지 저장하기

Entity 를 추가하여 담아야할 데이터의 세가지 종류를 넣어주었다.

image의 경우 Url로 받아온 데이터를 저장할 수 있다고 한다.

이미지의 경우 타입은 Binary Data로 지정해주어야 한다고 한다.

이미지를 저장하는 법을 찾아보았지만 이미지의 경우 대부분 용량문제로 코어데이터에 직접 저장하지는 않는다고 한다.
과제의 경우 간단한 앱이기에 괜찮을 것 같다. ( 하지만 내가 받아온 이미지의 경우 과제에서 요구한 이미지보다 용량이 컸다. 이미지의 용량을 확인해보니 110KB 정도로 나와 처음엔 image?.pngData() 로 데이터를 저장하려 했으나
용량을 줄일 필요가 있어보여 찾아보니 image?.jpegData(compressionQuality:0.8) 와 같이 용량을 압축해주는 코드가 있었다.)

 

일단 그렇게 하기로 하고 다음 진행을 하였다.

 

엔티티의 codegen 을 manual/None 으로 설정 후 Subclass 생성

@objc(MemberData)
public class MemberData: NSManagedObject {
    public static let className = "MemberData"
    public enum Key {
        static let name = "name"
        static let phoneNumber = "phoneNumber"
        static let image = "image"
    }
}

코어데이터 클래스에 간편한 접근을 위해 저장속성 부여하기

 

코어데이터 매니저를 생성하여 코어데이터 관련 로직을 분리 구현

// 멤버 비즈니스 로직 모델
final class CoreDataManager {
    
    static let shared = CoreDataManager()
    private init() {}
    
    // 앱 델리게이트
    let appDelegate = UIApplication.shared.delegate as? AppDelegate
    
    // 임시저장소
    lazy var context = appDelegate?.persistentContainer.viewContext
    
    // MARK: - [Read] 코어데이터에 저장된 데이터 모두 읽어오기
    func getMemberFromCoreData() -> [MemberData] {
        var memberList: [MemberData] = []
        // 임시저장소 있는지 확인
        if let context = context {
            // 요청서 생성
            let request = NSFetchRequest<MemberData>(entityName: MemberData.className)
            // 정렬순서를 정해서 요청서에 넘겨주기 (이름순, 오름차순 true)
            let nameOrder = NSSortDescriptor(key: MemberData.Key.name, ascending: true)
            request.sortDescriptors = [nameOrder]
            
            do {
                // 임시저장소에서 (요청서를 통해서) 데이터 가져오기 (fetch메서드)
                let fetchedMemberList = try context.fetch(request)
                memberList = fetchedMemberList
            } catch {
                print("가져오는 것 실패")
            }
        }
        return memberList
        
    }
    // MARK: - [Create] 코어데이터에 데이터 생성하기
    func saveMemberData(name: String?, phone: String?, image: Data?, completion: @escaping () -> Void) {
        // 임시저장소 있는지 확인
        if let context = context {
            // 임시저장소에 있는 데이터를 그려줄 형태 파악하기
            if NSEntityDescription.entity(forEntityName: MemberData.className, in: context) != nil {
                
                let memberData = MemberData(context: context)
                    // MARK: - MemberData에 실제 데이터 할당
                    memberData.name = name
                    memberData.phoneNumber = phone
                    memberData.image = image
                    
                    appDelegate?.saveContext()
            }
        }
        completion()
    }

 

코어데이터의 경우 전에 따로 공부했던 자료를 참고로 코드를 구현하였다.

(context에 접근하는 방식)

데이터를 이름순으로 읽어오는 방법

   // 정렬순서를 정해서 요청서에 넘겨주기 (이름순, 오름차순 true)
            let nameOrder = NSSortDescriptor(key: "name", ascending: true)
            request.sortDescriptors = [nameOrder]

 

request 안에 sortDescriptors 를 활용해 코어데이터의 정보를 배열로 주면

ascending : 오름차순 에 따라 정렬한 데이터를 요청할 수 있다. (false = 내림차순)


기존 더미변수를 저장한 MemberListManager 를 셀 델리게이트에 넣어 두었으니

MemberListManager 를 CoreDataManager 로 변경 작업

 

테스트 데이터를 위해 필요했던 MemberListManager 는 이제 필요가 없어졌다.

CoreDataManager에서 MemberData 배열을 받아올 것이기 때문

 

final class ViewController: UIViewController {
  let memberListManager = CoreDataManager.shared
    
extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return memberListManager.getMemberFromCoreData().count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.id, for: indexPath) as! TableViewCell
        // 멤버리스트매니저를 통해 받아온 데이터를 셀에 할당
        let member = memberListManager.getMemberFromCoreData()
        cell.memberData = member[indexPath.row]
        cell.selectionStyle = .none
        return cell
    }

 

ViewController에서 코어데이터에 접근하여 

TableViewDataSource에 넣어주는 코드


 @objc func applyButtonTapped() {
        //적용버튼 눌렀을 때
        let name = phoneBookView.nameTextField.text
        let phone = phoneBookView.phoneNumberTextField.text
        let image = phoneBookView.randomImageView.image
        // 이미지의 경우 JPEG로 변환하여 코어데이터에 저장 후 pop
        let imageData = image?.jpegData(compressionQuality: 0.5)
        memberListManager.saveMemberData(name: name, phone: phone, image: imageData) {
            print("저장 완료")
            self.navigationController?.popViewController(animated: true)
        }
    }

 

PhoneBookViewController에서 네비바 '적용'버튼 클릭 메서드 안에 saveMemberData 와 동시에 popView 실행하도록 구현


ViewContoller 에서 뷰가 나타날 때마다 테이블 뷰를 리로드 하여 새로운 정보가 나오도록 설정

- 셀 데이터소스에서 getMemberFromCoreData() 를 통해 셀에 적용시키기 때문에

viewWillAppear에서 리로드 할 시 데이터를 뷰가 나타날 때마다 받아옴

  // 뷰가 나타나는 시점마다 리로드
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        tableView.reloadData()
    }

 


Lv 7

TableViewCell을 클릭했을 때 PhoneBookViewController 페이지로 이동
셀에 저장된 내용 나오게 하기
네비게이션 title -> 연락처의 이름으로

 

TableViewDelegate의 didSelectRowAt 을 이용한 화면이동 구현

// 델리게이트 선언
tableView.delegate = self

extension ViewController: UITableViewDelegate {
    
    //선택적인 메서드, 셀이 선택되었을때 동작이 전달
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let phoneBookVC = PhoneBookViewController()
    
        // 클릭된 셀의 데이터 넘기기
        let selectedMember = memberListManager.getMemberFromCoreData()[indexPath.row]
        phoneBookVC.memberData = selectedMember
        // 화면이동
        navigationController?.pushViewController(phoneBookVC, animated: true)
    }

 

테이블 뷰의 대리자 역할을 할 수 있도록 델리게이트를 채택하고 

PhoneBookViewController 에서 데이터를 받을 저장속성을 선언한 뒤

var memberData: MemberData?

 

didSelectRowAt 메서드를 재정의 하여 클릭된 셀의 [inexPath.row] 를 네비게이션 컨트롤러를 통해 다음 화면으로 전달할 수 있도록 구현했다.

 

memberData로 정보는 전달되었지만 PhoneBookVC 에서 받은 정보를 표시해주는 코드가 필요하다.

// MemberData를 전달받을 변수 (전달 받으면 ==> 표시하는 메서드 실행)
var memberData: MemberData? {
        didSet {
            configureSelectedData()
        }
    }
   // 선택된 셀의 정보를 받아 UI 표시하기
    func configureSelectedData() {
        
        if memberData != nil {
            title = memberData?.name
            phoneBookView.nameTextField.text = memberData?.name
            phoneBookView.phoneNumberTextField.text = memberData?.phoneNumber
            if let imageData = memberData?.image, let image = UIImage(data: imageData) {
                phoneBookView.randomImageView.image = image
            }
            // 정보가 있을 시 삭제버튼 생성
            let deleteButton: UIButton = {
                let button = UIButton()
                button.setTitle("삭제", for: .normal)
                button.backgroundColor = .lightGray
                button.titleLabel?.font = .systemFont(ofSize: 14)
                button.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
                return button
            }()
            phoneBookView.addSubview(deleteButton)
            deleteButton.snp.makeConstraints { make in
                make.top.equalTo(phoneBookView.phoneNumberTextField.snp.bottom).offset(20)
                make.centerX.equalToSuperview()
            }
            
        }

 

MemberData가 전달된 직후 호출되도록 속성감시자를 활용해  configureSelectedData() 메서드를 구현하였다.

if문으로 멤버 데이터가 있다면 셀에서 전달받은 정보를 채워주고 

이미지의 경우 데이터로 변환하여 코어데이터에 저장해두었기 때문에

데이터를 이미지로 다시 변환하여 적용해주었다.

네비게이션의 title 역시 이름으로 표시되도록 하였다.

 

추가적으로 삭제버튼을 구현하고 싶어 멤버의 정보가 있다면 삭제버튼을 생성하도록 구현해 보았다.


 

Lv 8

Update 기능 구현
'적용' 버튼을 눌렀을 때, 새로운 데이터를 추가하는 것이 아닌, 기존 데이터 수정(update)
수정 후 다시 메인화면으로 이동, 수정된 내용 노출 ( reload )

 

우선 코어데이터 매니저에 update와 delete 기능을 추가 구현하였다.

  // MARK: - [Delete] 코어데이터에서 데이터 삭제하기 (일치하는 데이터 찾아서 ===> 삭제)
    func deleteMember(data: MemberData, completion: @escaping () -> Void) {
        
        guard let name = data.name else {
            completion()
            return
        }
        
        // 임시저장소 있는지 확인
        if let context = context {
            // 요청서
            let request = NSFetchRequest<MemberData>(entityName: MemberData.className)
            // 단서 / 찾기 위한 조건 설정
            request.predicate = NSPredicate(format: "name = %@", name as CVarArg)
            
            do {
                // 요청서를 통해서 데이터 가져오기 (조건에 일치하는 데이터 찾기) (fetch메서드)
                let fetchedMemberList = try context.fetch(request)
                
                // 임시저장소에서 (요청서를 통해서) 데이터 삭제하기 (delete메서드)
                if let targetMember = fetchedMemberList.first {
                    context.delete(targetMember)
                    
                    appDelegate?.saveContext()
                }
                completion()
            } catch {
                print("지우는 것 실패")
                completion()
            }
        }
    }
    
    // MARK: - [Update] 코어데이터에서 데이터 수정하기 (일치하는 데이터 찾아서 ===> 수정)
    func updateMember(newMemberData: MemberData, completion: @escaping () -> Void) {
        // 이름 옵셔널 바인딩
        guard let name = newMemberData.name else {
            completion()
            return
        }
        
        // 임시저장소 있는지 확인
        if let context = context {
            // 요청서
            let request = NSFetchRequest<MemberData>(entityName: MemberData.className)
            // 단서 / 찾기 위한 조건 설정
            request.predicate = NSPredicate(format: "name = %@", name as CVarArg)
            
            do {
                // 요청서를 통해서 데이터 가져오기
                let fetchedMemberList = try context.fetch(request)
                    // 배열의 첫번째
                if var targetMember = fetchedMemberList.first {
                        
                        // MARK: - MemberData에 실제 데이터 재할당(바꾸기)
                        targetMember = newMemberData
                        
                        appDelegate?.saveContext()
                    }
                completion()
            } catch {
                print("지우는 것 실패")
                completion()
            }
        }
    }

 

데이터를 지우거나, 업데이트 하기 위해서는 코어데이터에 저장된 데이터를 찾을 수 있어야 한다.

 

데이터를 생성하는 코드와 비슷하지만 추가적으로 구현해줘야 하는 부분은

context 안에 있는 데이터를 찾기 위해서는 어떠한 기준으로 찾을지 설정해주어야 한다.

 

name을 기준으로 데이터를 찾기 위한 조건을 설정한 뒤 request를 생성하여

fetch(request)로 데이터를 가져오고

삭제하거나, 수정할 수 있다.

 

이제 코어데이터매니저의 메서드를 활용하면 데이터 관리를 할 수 있게 되었다.

// 정보 유무에 따른 네비바버튼 이름 변경
    func setupNaviBar() {
        if memberData != nil {
            let updateButton = UIBarButtonItem(title: "수정", style: .plain, target: self, action: #selector(updateButtonTapped))
            self.navigationItem.rightBarButtonItem = updateButton
        } else {
            let applyButton = UIBarButtonItem(title: "적용", style: .plain, target: self, action: #selector(applyButtonTapped))
            title = "연락처 추가"
            self.navigationItem.rightBarButtonItem = applyButton
        }
    }

 

네비 바 버튼의 경우 정보 유무에 따라 이름이 바뀌고 각각 다른 action을 하도록 구현하였다.

 

  // 수정 버튼 클릭 시
    @objc func updateButtonTapped() {
        
        if let memberData = self.memberData {
            memberData.name = phoneBookView.nameTextField.text
            memberData.phoneNumber = phoneBookView.phoneNumberTextField.text?.withHypen
            memberData.image = phoneBookView.randomImageView.image?.jpegData(compressionQuality: 0.5)
            // 데이터 업데이트
            coreDataManager.updateMember(newMemberData: memberData) {
                print("수정 완료")
                self.navigationController?.popViewController(animated: true)
            }
        }
    }
    // 삭제 버튼 클릭 시
    @objc func deleteButtonTapped() {
        // 알럿창 띄우기 - 확인 클릭 시 삭제
        let alert = UIAlertController(title: "연락처 삭제", message: "정말 삭제하시겠습니까?", preferredStyle: .alert)
        let succes = UIAlertAction(title: "확인", style: .default) { action in
            if let memberData = self.memberData {
            // 데이터 삭제
                self.coreDataManager.deleteMember(data: memberData) {
                    print("삭제 완료")
                    self.navigationController?.popViewController(animated: true)
                }
            }
        }
        let cancel = UIAlertAction(title: "취소", style: .cancel)
        alert.addAction(succes)
        alert.addAction(cancel)
        self.present(alert, animated: true)
    }

 

업데이트의 경우 버튼을 누르는 순간의 입력된 정보를 넘겨 업데이트 할 수 있도록 구현하였고

추가적으로 메인화면으로 이동하도록 했다.

 

삭제 버튼의 경우 UX를 고려하여 더블체크를 할 수 있도록

삭제 버튼 클릭 시 알럿 창을 띄워 확인을 누르면 deleteMember가 실행되도록 하고

메인화면으로 돌아가도록 구현하였다.

 

 


이미지 저장에 대해

 

https://jangsilverbaby.tistory.com/33

 

이미지를 코어 데이터에 저장할 때 주의할 점

이미지를 데이터베이스에 저장하는 것은 매우 무거운 작업이기 때문에, 대량의 데이터를 다룰 때 성능의 저하를 가져올 수도 있다. 실무에서는 데이터베이스가 아닌 문서 디렉터리에 파일로 저

jangsilverbaby.tistory.com

https://www.vadimbulavin.com/how-to-save-images-and-videos-to-core-data-efficiently/

 

How to Save Images and Videos to Core Data Efficiently

Core Data has been in iOS and macOS going back as far as anyone can recall. Nonetheless, there is no widely adopted strategy of storing images and videos in Core Data. In this article let's implement and benchmark most popular Core Data persistence strateg

www.vadimbulavin.com

 

https://www.hackingwithswift.com/forums/ios/what-is-the-best-way-to-save-an-image-using-coredata-cloudkit/22252


CoreData 참고자료

 

https://people-analysis.tistory.com/299

 

Core Data Context 이해와 활용 - 심화

그냥 Store에 저장하지 뭐하러 Context에다가 임시로 저장을 하냐? 데이터를 저장하거나 변경할 때 Store에 바로 하지 않고 Context에 먼저 임시로 해본 뒤에 save를 호출해야 해당 내용들이 Store에 반영

people-analysis.tistory.com

https://accompani-i.tistory.com/262

 

[Swift] Coredata로 data 저장하고 불러오기(CRUD의 시작)

1. DataModel로 이동해서 entity생성 entit는 Class와 거의 동일 (= 테이블과도 개념이 비슷해서 엑셀 단일 시트와 같이 단일 entity를 가진다) Attributes는 Properties와 동일 2. 이름 바꾸기 3. Attributes(속성 추

accompani-i.tistory.com