[swift]project_포켓몬 연락처

Jeff·2024년 12월 11일
2
post-thumbnail

이번 과제는 포켓몬 이미지를 활용한 연락처를 만드는 것이다.
이번엔 MVC패턴을 활용해 프로젝트를 진행하기로 결정했기에 View, ViewController, Model을 나누어 구현을 하였다.

Lv.1 ~ 5

  • 💡 Lv.1 - 연락처 화면 구현 (TableView, NavigationController)
  • 💡 Lv.2 - 연락처 추가화면 구현 (TextField)
  • 💡 Lv.3 - 상단바 구현 (NavigationItem)
  • 💡 Lv.4 - API 연결 (Alamofire)
  • 💡 Lv.5 - 데이터 저장 (CoreData)
Lv.1Lv.2,3Lv.4

💡 Lv.1 - 연락처 화면 구현 (TableView, NavigationController)

📌 Point

  • 아래 사진과 같이 NavigationViewController를 사용해주면 우리는 간단하게 사용할 수 있게 된다. 가장 먼저 보여줄 Controller를 rootViewController에 넣어주면 된다.
  • 초기에는 MainView에 TableView가 들어가기에 TableView를 사용한다면 꼭 구현해줘야하는 dataSource, delegate 프로토콜들을 채택해 View에서 구현을 했다. 하지만 View는 오로지 UI를 구현하는데 역할이 집중되니 비즈니스적인 로직을 처리하거나 알 필요가 없다는걸 알게되었고, 데이터를 관리하고 View와 Data를 연결하는 ViewController에 쓰는게 적합하다고 생각해 코드를 수정해 옮겨왔다.(나중에는 ViewController가 정말 비대해지는게 느껴졌다.)

💡 Lv.2 - 연락처 추가화면 구현 (TextField)

📌 Point

  • MainViewController에서 추가버튼을 눌렀을 시 PhoneBookViewController로 이동할 수 있게 구현
 rightButton.action = #selector(tappedAddButton)
 
 @objc func tappedAddButton() {
        navigationController?.pushViewController(phoneBookViewController, animated: true)
    }
  • 이름과 전화번호를 입력할 수 있는 TextField를 구현
    • 우리는 사용자의 입력을 받는 TextField와 TextView가 존재한다. 여기서 두개 모두 각자의 장단점을 가지고 있기에 지금 상황에 맞는 TextField를 채택하게 되었다.
    • placeholder을 통해 사용자가 직관적으로 무엇을 입력해야하는지 인식할 수 있고, 이름과 전화번호라는 짧은 텍스트 입력을 받아오기에 TextField를 사용했다.

💡 Lv.3 - 상단바 구현 (NavigationItem)

📌 Point

  • 위 사진에 보시면 앱 상단에 친구목록, 연락처 추가, 생성이 있는 부분이 NavigationBar부분이다.
  • NavigationBar 부분은 NavigationController를 생성하면 자동으로 생성이 된다.

🎯 트러블 슈팅

  • 이 프로젝트 초반에는 navigationBar의 존재를 모르고 Label과 Button을 직접 만들어서 사용했었다. 그렇기에 위의 사진처럼 navigationBar부분에 걸쳐서 생성이 되어져 있는걸 볼 수 있다.
  • 검색을 통해 navigationBar을 생성해서 만들 수 있다는걸 알아 직접 생성해서 추가한 결과 navigationBar가 두개가 되어있는 모습을 발견했다.
  • 더 찾아본 결과 NavigationController를 사용하면 기본적으로 제공을 해주고 접근하는거 또한 ViewController에서 간단하게 설정할 수 있어 마지막 사진처럼 제대로된 자리에 생성이 되었다.
 navigationItem.title = "친구 목록"		// 가운데에 들어가는 타이틀
 navigationItem.rightBarButtonItem = rightButton  // 오른쪽에 들어갈 Item을 앞서 생성한 Button(rightButton)을 넣어주면 된다. 

💡 Lv.4 - API 연결 (Alamofire)

📌 Point

  • API 통신을 하는 방법으론 보통 3가지(URLSession, Alamofire, Moya)를 사용하는데 그 중 URLSession은 애플에서 제공해주는 Class이다. 이 URLSession을 더 편하게 사용하려고 만든 라이브러리가 Alamofire이며, 이번엔 Alamofire에 대해 알아보기 위해 사용해보았다.
  • 우리는 API 요청을 통해 받아온 json 데이터에서 이미지가 저장된 url를 가지고 이미지를 보여주면 된다.
  • 우선 다양한 네트워킹을 하기위해 범용적인 함수를 생성해준다.
 func fetchData<T: Decodable>(url: URL, completion: @escaping (Result<T, AFError>) -> Void) {
        AF.request(url).responseDecodable(of: T.self) { response in
            completion(response.result)
        }
    }
  • 위에 생성한 메서드를 활용해 api 요청 함수를 생성해준다. (URLSession에서 정의해주고 조건을 설정해줘야하는 부분을 알아서 처리가 되어있고 case문으로 간단하게 구현하면 된다.)
  // pokemon api 요청 함수
    func fetchPokemonApi(completion: @escaping (UIImage?) -> Void) {
        let urlAdress = "https://pokeapi.co/api/v2/pokemon/"
        let randomNum = String(Int.random(in: 1...1000))
        
        // 요청할 url주소를 생성
        guard let url = URL(string: urlAdress + randomNum) else {
            completion(nil)
            return
        }
        
        // 바로 위에서 생성한 url주소를 가지고 결과값을 받아오는 메서드
        fetchData(url: url) { [weak self] (result: Result<PokemonData, AFError>) in
            guard let self = self else { return }
            switch result {
            case .success(let result) :
                guard let imageUrl = URL(string: result.sprites.frontDefault) else { return }
                
                // 이미지가 저장된 url을 이용해 UIImage로 변환해서 completion(콜백함수)로 Image의 값을 넘겨준다.
                AF.request(imageUrl).responseData { response in
                    if let data = response.data, let image = UIImage(data: data) {
                        DispatchQueue.main.async {
                            completion(image)
                        }
                    }
                }
            case .failure(let error) :
                print("데이터 로드에 실패 \(error)")
                completion(nil)
            }
        }

💡 Lv.5 - 데이터 저장 (CoreData)

📌 Point

  • 데이터를 디스크에 저장하는 방법으로는 CoreData, UserDefaults가 존재한다. 간단하게 아이디, 비밀번호와 같은 데이터는 UserDefaults로 사용이 가능하지만 사진도 저장하고 추후에 더 추가될 여지가 있다고 생각해 CoreData를 사용해보았습니다.
  • 기본적으로 앱을 생성하는 동시에 CoreData를 사용한다고 하면 아래와 같이 AppDelegate에 코드가 추가가 된다.


    첫번째 사진은 AppDelegate파일에 생성된 코드이고 두번째 사진은 우리가 AppDelegate에 있는 persistentContainer을 사용하기위해 타입캐스팅을 통해서 접근하며 인스턴스에 접근하기 쉽게 변수에 랩핑을 했다.
  • 싱글톤으로 선언해 여러곳에서 쉽게 persistentContainer에 접근하기가 가능하다.
     // 싱글톤으로 생성
       static let shared = CoreDataManger()
  • 앞서 Create를 통해 저장된 데이터를 DataSource라는 구조체에 값을 넣어줘 테이블 뷰에 보여줄 수 있다. -> DataSource(name: "이름", phoneNumber: "전화번호" , profilesImage: ImageData) 형식으로 DataSource에 생성해준다.

🎯 트러블 슈팅

  • 초기에는 Attribute를 name: String, phoneNumber: String, profilesImage: String으로 생성을 했었다. 그 후 create를 통해 테스트를 거치며, 잘 작동하는걸 확인했다.

  • 하지만 image를 url로 저장하려고 했던 초기 계획이 아닌거 같아 profilesImage의 속성을 Binary Data로 변경을 했다. 그때까지는 아무런 이상이 없어보였지만 빌드를 하고나서 장문의 에러메세지를 받게 되었다.

    아래의 에러들은 결론적으론 기존의 저장된 데이터의 형식과 현재 빌드하는 데이터의 형식이 맞지 않아서 즉, Core Data의 모델의 변경으로 빌드를 할 수 없다는 말이였다.

  • 이 상황에서 해결방법은 1. 데이터를 초기화, 2. 자동 마이그레이션 활성화, 3. 수동으로 마이그레이션 와 같은 선택지가 주어졌다. 저의 경우에는 개발단계이기도 하고 앞서 저장한 데이터는 테스트를 위한 데이터였기에 데이터를 초기화하는 결정을 내렸다. 가장 빠르고 개발환경에서 할 수 있는 방법이다.

  • 아래의 사진은 AppDelegate에서 보았던 persistentContainer 변수이다. 이 코드를 통해 기존에 저장되어 있던 데이터를 지우고 새롭게 변경된 값으로 저장을 할 수 있게 되었다.


Lv.6 ~ 8

  • 💡 Lv.6 - 저장된 데이터 이름순으로 나열(CoreData - NSSortDescriptor)
  • 💡 Lv.7 - 상세보기 화면 구현(TableView, ViewController)
  • 💡 Lv.8 - 저장된 데이터 수정(CoreData)

💡 Lv.6 - 저장된 데이터 이름순으로 나열(CoreData - NSSortDescriptor)

📌 Point

  • 사실 이미 NSSortDescriptor라는 클래스를 지원해 CoreData의 값을 fetch할때 정렬을 한 후 사용할 수 있게 된다.
     // NSSortDescriptor을 이용해 오름차순으로 정렬
           let fetchRequest = PhoneBook.fetchRequest()
           fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]	// true : 오름차순, false : 내림차순
  • 아래의 사진처럼 fetchRequest를 DataSource에 추가하기 전에 먼저 정렬을 마친 후에 추가하는 방식으로 구현했다.

💡 Lv.7 - 상세보기 화면 구현(TableView, ViewController)

📌 Point

  • 우선 기존에 생성해 사용하던 PhoneBookViewController를 재사용하기 위해서 데이터가 없는 화면(연락처 추가 화면)과 데이터가 들어간 화면(상세보기 화면)을 구분하기위해서 Mode를 enum으로 정의해 스위칭할 수 있게 구현했다.
  • 연락처 추가 버튼을 눌렀을 시 mode를 변경해주고 화면전환을 해준다. 그리고 TableView의 특정 cell을 선택했을 시 didSelectRowAt 메서드를 통해서 화면전환과 함께 해당 cell의 데이터도 함께 넘겨주며, mode를 변경해주는 방식으로 구현했다.

  • 변경된 Mode의 값을 기준으로 View와 NavigationBar를 업데이트할 수 있도록 viewWillAppear에 구현해 화면전환시 Mode에 맞게 구현해놓았다.

🎯 트러블 슈팅

  • TableView Cell을 선택했을 시 저장되어있는 데이터를 띄워주는데 이름, 전화번호, 이미지까지 잘 받아서 보여주는데 앱 상단의 NavigationBar에 있는 title부분이 바뀌지 않는 이슈가 있었다.

  • navigationBarSetup함수에서 navigationItem.title을 미리 "연락처 추가"로 저장해 놓은 바람에 configureUI함수에서의 navigationItem.title 값을 변화를 주어도 바뀌지 않았다. 여기서 viewDidLoad에 configureUI 다음에 navigationBarSetup을 호출하게 정의해 놓아서 덮어씌워진거였다. 그래서 navigationBarSetup함수에서 정의해놓은 title값을 제거한 후 제대로 작동이 되었다.


💡 Lv.8 - 저장된 데이터 수정(CoreData)

📌 Point

  • 상세보기 화면을 통해 View에 진입을 하면 CoreData에 저장되어있던 데이터가 보여지는데 여기서 TextField의 값과 Image가 변경이 되고난 후 앞서 정의해 놓은 updateData메서드를 통해 다시한번 값을 수정해서 저장하도록 만들었다. DataSource의 값을 모두 받아서 업데이트가 되도록 구현했다.


마치며..

이번에 MVC패턴을 사용하고자 View와 ViewController의 역할을 분리하는 과정에서 많은 시행착오가 있었고 delegate와 클로저를 활용한 콜백함수를 구현하는 등 여러 방법으로 구현해보려고 노력했다. 물론 네트워킹과 데이터를 저장하고 다루는 부분에서 시간을 많이 소요해서 앞으로 수정할 것도 다시 공부해야할 것도 많이 남아있는 프로젝트였다.

profile
기본에 충실한 개발자가 목표!

4개의 댓글

comment-user-thumbnail
2024년 12월 12일

추가 버튼 파랑색 아닌 거 너무 신걍 쓰이는데 수정해주세요 개발자님

1개의 답글
comment-user-thumbnail
2024년 12월 12일

시뮬레이터 키보드 마우스로 일일이 누르는 거 너무 느리고 킹받아요 개발자님

1개의 답글