[내일배움캠프 38일차] Ch.3 앱 개발 숙련 - 개인 과제3 + 알고리즘

NH·2025년 4월 23일

내일배움캠프

목록 보기
38/62
post-thumbnail

☎️ 포켓몬 연락처 앱 - 개인 과제3

Lv.6

🔹 요구사항

🧑🏻‍💻 Level 6

  • 연락처 추가를 마치고 메인화면으로 돌아왔을 때 항상 이름 순으로 정렬되게 해봅시다.
  • 직접 검색을 통해 알아내고 구현해보세요 :)

🔹 작성 코드

  • NSSortDescriptor 사용하면 CoreData에 저장된 데이터를 정렬 가능
  • CoreData에서 데이터를 불러올때 정렬조건을 설정하면 됨

CoreData 저장 및 불러오기 기능 작성

func readAllData() {
	// 추가한 코드
	// 데이터 요청
	let request = NSFetchRequest<NSManagedObject>(entityName: "PhoneBook")
        
	// 이름 순으로 정렬 (오름차순)
	let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        
	// 정렬 기준 적용
	request.sortDescriptors = [sortDescriptor]
    
    // 기존 코드
	do {
		phoneBooks = try self.container.viewContext.fetch(request)
        
        // 로그 찍기    
		for phoneBook in phoneBooks as [NSManagedObject] {
			let name = phoneBook.value(forKey: "name") as? String ?? "이름 없음"
			let phoneNumber = phoneBook.value(forKey: "phoneNumber") as? String ?? "번호 없음"
			if let image = phoneBook.value(forKey: "imageUrl") as? String {
				print("imageUrl: \(image), name: \(name), phoneNumber: \(phoneNumber)")
			} else {
				print("imageUrl: 이미지 없음, name: \(name), phoneNumber: \(phoneNumber)")
			}
		}
	} catch {
		print("데이터 읽기 실패")
	}
}

🔹 결과 화면

🔹 느낀 점

  • CoreData에 정렬 기능이 있다는 것을 알게되었다.
  • 처음에는 막막했지만 구글링을 정렬을 해주는 속성을 확인하여 별 어려움 없이 기능 구현을 할 수 있었다.

Lv.7

🔹 요구사항

🧑🏻‍💻 Level 7

  • UITableViewCell 을 클릭했을 때도 PhoneBookViewController 페이지로 이동되게 합니다.
  • 이때, 추가 버튼을 눌러서 갔던 PhoneBookViewController 와 별개의 새로운 ViewController 클래스를 선언해서 생성하지 말아주세요. 그대로 PhoneBookViewController 를 사용해서 띄웁니다. 이미 구현되어있는 뷰컨트롤러를 재활용하도록 합니다.
  • 추가 버튼을 눌러서 이동했던 때와 다르게, 이미지, 이름, 전화번호가 입력된 상태로 화면이 띄워지게 해주세요. 이 요구사항의 의미가 헷갈린다면 영상을 참고해주세요.
  • 추가 버튼을 눌러서 이동했던 때와 다르게, 상단 네비게이션 바의 title 이 연락처의 이름이 되도록합니다.

🔹 작성 코드

Cell 탭 시, PhoneBookViewController 페이지로 이동

  • func tableView(_ tableView:, didSelectRowAt indexPath:) 사용하여, cell 탭 시, 화면이 이동하도록 구현
  • 이미지, 이름, 전화번호가 입력된 상태로 화면이 띄워지게 구현
extension ViewController: UITableViewDelegate {
    .
    .
    .
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let pv = PhoneBookViewController()
        let phoneBook = CoreDataManager.shared.phoneBooks[indexPath.row]
        let imageUrl = phoneBook.value(forKey: "imageUrl") as? String ?? ""
        let name = phoneBook.value(forKey: "name") as? String ?? ""
        let phoneNumber = phoneBook.value(forKey: "phoneNumber") as? String ?? ""
        
        // 예외처리: 이미지 URL이 없다면 기본 이미지로 설정
        guard let url = URL(string: imageUrl) else {
            pv.profileImageView.image = nil
            pv.nameTextField.text = name
        	pv.phoneNumTextField.text = phoneNumber
        
        	self.navigationController?.pushViewController(pv, animated: true)
            return
        }
        
        // URLSession으로 비동기 이미지 로드
        URLSession.shared.dataTask(with: url) { data, response, error in
            //guard let self = self else { return }
            
            if let data = data, let image = UIImage(data: data) {
                DispatchQueue.main.async {
                    pv.profileImageView.image = image
                }
            } else {
                DispatchQueue.main.async {
                    pv.profileImageView.image = nil // 실패 시 기본 이미지 설정
                }
            }
        }.resume()
        
        pv.nameTextField.text = name
        pv.phoneNumTextField.text = phoneNumber
        
        self.navigationController?.pushViewController(pv, animated: true)
    }
}

cell 탭 시,상단 네비게이션 바의 title 이 연락처의 이름으로 설정

  • PhoneBookViewController 객체에 navigationItem.title = name 을 설정 해주었지만 되지 않았다.
extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let pv = PhoneBookViewController()
        let phoneBook = CoreDataManager.shared.phoneBooks[indexPath.row]
        let imageUrl = phoneBook.value(forKey: "imageUrl") as? String ?? ""
        let name = phoneBook.value(forKey: "name") as? String ?? ""
        let phoneNumber = phoneBook.value(forKey: "phoneNumber") as? String ?? ""
        
        guard let url = URL(string: imageUrl) else {
            pv.profileImageView.image = nil
            pv.nameTextField.text = name
            pv.phoneNumTextField.text = phoneNumber
            
            self.navigationController?.pushViewController(pv, animated: true)
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data, let image = UIImage(data: data) {
                DispatchQueue.main.async {
                    pv.profileImageView.image = image
                }
            } else {
                DispatchQueue.main.async {
                    pv.profileImageView.image = nil
                }
            }
        }.resume()
        
        pv.nameTextField.text = name
        pv.phoneNumTextField.text = phoneNumber
        
        // 추가한 코드, 하지만 동작 안됨
        pv.navigationItem.title = "hhhh"
        
        self.navigationController?.pushViewController(pv, animated: true)
    }
}
  • 어떻게 해결했는지는 트러블 슈팅에서 작성!

🔹 결과 화면


🔫 트러블 슈팅

cell 탭 시,상단 네비게이션 바의 title 이 연락처의 이름으로 설정

원인 분석

  • PhoneBookViewControllerviewDidLoad()에서 Title의 값을 지정하고 있기 때문!!
  • ViewController 코드 추가
let pv = PhoneBookViewController()
pv.contactName = name // Title 값 넘겨주기
self.navigationController?.pushViewController(pv, animated: true) // 이때 같이 넘어감
  • PhoneBookViewController 코드 추가
class PhoneBookViewController: UIViewController {
    public var contactName: String? // title 변경에 사용할 변수
    
	override func viewDidLoad() {
		super.viewDidLoad()
		
        // 조건문으로 Title 값을 설정
        if let contactName = contactName {
            self.navigationItem.title = contactName
        } else {
            self.navigationItem.title = "연락처 추가"
        }
	}

결과: 성공!!!!


🔹 느낀 점

  • 레벨이 올라가고 있음에도 아직 네트워크 관련 코드와 CoreData 관련 코드를 잘 응용을 못하고 있다.
  • 아무래도 과제 제출 이후에 추가적으로 공부를 해놓아야 겠다....

Lv.8

🔹 요구사항

🧑🏻‍💻 Level 8

  • UITableViewCell 을 클릭해서 이동해온 연락처 편집 페이지에서, 실제로 기기 디스크 데이터에 Update 가 일어나도록 구현합니다.
  • “적용” 버튼을 클릭했을때, 새로운 전화번호 데이터를 추가(Create)하는 것이 아닌, 기존 데이터를 수정(Update) 하도록 합니다.
  • 그리고 적용 버튼을 클릭하면 다시 메인화면으로 이동하고, 이때 수정된 내용이 반영되어 노출되어야 합니다.

🔹 작성 코드

CoreData Update 기능 구현

// 데이터 읽기
func readAllData() {
    // 데이터 요청
    let request = NSFetchRequest<NSManagedObject>(entityName: PhoneBook.className)
    
    // 이름 순으로 정렬 (오름차순)
    let sortDescriptor = NSSortDescriptor(key: PhoneBook.Key.name, ascending: true)
    
    // 정렬 기준 적용
    request.sortDescriptors = [sortDescriptor]
    
    do {
        phoneBooks = try self.container.viewContext.fetch(request)
        
        for phoneBook in phoneBooks as [NSManagedObject] {
            let name = phoneBook.value(forKey: PhoneBook.Key.name) as? String ?? "이름 없음"
            let phoneNumber = phoneBook.value(forKey: PhoneBook.Key.phoneNumber) as? String ?? "번호 없음"
            if let image = phoneBook.value(forKey: PhoneBook.Key.imageUrl) as? String {
                print("imageUrl: \(image), name: \(name), phoneNumber: \(phoneNumber)")
            } else {
                print("imageUrl: 이미지 없음, name: \(name), phoneNumber: \(phoneNumber)")
            }
        }
    } catch {
        print("데이터 읽기 실패")
    }
}

데이터 추가 업데이트 분기 설정

  • isUpdate 변수를 사용하여 셀을 탭하면 데이터를 업데이트 하는 로직과 추가 버튼을 누르면 데이터를 추가하는 로직 분기 처리
import UIKit
import CoreData

class PhoneBookViewController: UIViewController {
    public var PhoneBookPhoneNumber: String?
    public var PhoneBookImageUrl: String?
    
    public var isUpdate: Bool = false // 적용 버튼 탭 시, 추가를 할 것인지 업데이트를 할 것인지 정하는 변수
    
    public var PhoneBookName: String? // title 변경에 사용할 변수

    @objc
    private func didApplyButtonTapped() {
        print("적용 버튼이 탭 되었습니다.")
            if isUpdate {
            CoreDataManager.shared.updateData(
                currentImageUrl: PhoneBookImageUrl ?? "",
                updateImageUrl: profileImageUrl ?? "",
                currentName: PhoneBookName ?? "",
                updateName: nameTextField.text ?? "",
                currentPhoneNumber: PhoneBookPhoneNumber ?? "",
                updatePhoneNumber: phoneNumTextField.text ?? ""
            )
        } else {
            CoreDataManager.shared.createData(
                imageUrl: profileImageUrl,
                name: nameTextField.text ?? "",
                phoneNumber: phoneNumTextField.text ?? ""
            )
        }
	}
}
  • 셀을 탭하면 isUpdate 변수가 true 로 변경됨
  • 데이터 업데이트 시, 기존 값을 전달하기 위한 코드 작성
    • pv.PhoneBookName = name
    • pv.PhoneBookPhoneNumber = phoneNumber
    • pv.PhoneBookImageUrl = imageUrl
extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        pv.PhoneBookName = name
        
        pv.PhoneBookPhoneNumber = phoneNumber
        pv.PhoneBookImageUrl = imageUrl
        
        pv.isUpdate = true
}

🔫 트러블 슈팅

오류 1

  • 이미지가 없은 연락처를 수정 시, 새로운 이미지가 적용이 안되는 현상..

원인 분석

  • 실제 CoreData에서 이미지 URL을 저장할 때, nil 값으로 저장
  • 하지만 코드에서는 nil 값일 때 "" 으로 데이터를 검색함.
  • 따라서 이미지가 없다고 나옴

오류 2

  • 그리고 특정 값이 같으면 같이 변경되는 현상

원인 분석

  • CoreData 조건을 한개씩 해서 값이 같은 값은 같이 바뀜

해결방안

  • CoreData 조건식을 AND로 합쳐서 조회
  • CoreData 조건식에서 imageUrl 조회 시, OR를 사용해서 ""과 nil을 찾게 수정

기존 코드

    // CoreData 에서 데이터 업데이트
    func updateData(currentImageUrl: String,
                    updateImageUrl: String,
                    currentName: String,
                    updateName: String,
                    currentPhoneNumber: String,
                    updatePhoneNumber: String) {
        let fetchRequest = PhoneBook.fetchRequest()
        
        // predicate 는 조건을 걸어주는 구문
        fetchRequest.predicate = NSPredicate(format: "imageUrl == %@", currentImageUrl)

        do {
            let result = try self.container.viewContext.fetch(fetchRequest)
            
            for data in result as [NSManagedObject] {
                data.setValue(updateImageUrl, forKey: PhoneBook.Key.imageUrl)
            }
            
            try self.container.viewContext.save()
            
            print("데이터 수정 성공")
        } catch {
            print("데이터 수정 실패")
        }
        
        fetchRequest.predicate = NSPredicate(format: "name == %@", currentName)
        do {
            let result = try self.container.viewContext.fetch(fetchRequest)
            
            for data in result as [NSManagedObject] {
                data.setValue(updateName, forKey: PhoneBook.Key.name)
            }
            
            try self.container.viewContext.save()
            
            print("데이터 수정 성공")
        } catch {
            print("데이터 수정 실패")
        }
        
        fetchRequest.predicate = NSPredicate(format: "phoneNumber == %@", currentPhoneNumber)
        do {
            let result = try self.container.viewContext.fetch(fetchRequest)
            
            for data in result as [NSManagedObject] {
                data.setValue(updatePhoneNumber, forKey: PhoneBook.Key.phoneNumber)
            }
            
            try self.container.viewContext.save()
            
            print("데이터 수정 성공")
        } catch {
            print("데이터 수정 실패")
        }
    }
}

변경 코드

    func updateData(currentImageUrl: String,
                    updateImageUrl: String,
                    currentName: String,
                    updateName: String,
                    currentPhoneNumber: String,
                    updatePhoneNumber: String) {
        let fetchRequest = PhoneBook.fetchRequest()
        
        // predicate 는 조건을 걸어주는 구문
        // 예외처리: imageUrl 이 "" 면 nil 인 값을 찾게 함.
        fetchRequest.predicate = NSPredicate(format: "(imageUrl == %@ OR imageUrl == nil) AND name == %@ AND phoneNumber == %@", currentImageUrl, currentName, currentPhoneNumber)
        do {
            let result = try self.container.viewContext.fetch(fetchRequest)

            if let objectToUpdate = result.first {
                objectToUpdate.setValue(updateImageUrl, forKey: "imageUrl")
                objectToUpdate.setValue(updateName, forKey: "name")
                objectToUpdate.setValue(updatePhoneNumber, forKey: "phoneNumber")
                
                try self.container.viewContext.save()
                print("데이터 수정 성공")
            } else {
                print("일치하는 데이터 없음")
            }
        } catch {
            print("데이터 수정 실패")
        }
    }
}

결과: 성공!!!!


🔹 결과 화면


🔹 느낀 점

  • 브레이크 포인트를 사용하면서 CoreData의 흐름을 보니, 코드 이해가 더 잘 되었다.
  • 따라서 업데이트 문을 사용하는 데에 큰 어려움은 없었다.

Challenge 디테일 키우기!!!

🔹 요구사항

🧑🏻‍💻 Challenge

  1. 포켓몬 덩치가 클 때, 이미지 원 영역을 벗어나는 경우가 있습니다. 이 때 원 밖을 벗어나지 않도록 구현해볼 수 있을까요? 원래라면 아래 포켓몬은 날개 부분이 살짝 밖으로 삐져나오게 됩니다.
  1. 연락처를 매우 많이 추가했을 경우(20개 이상), 테이블 뷰 스크롤을 쭉 내리다보면, 이미지가 겹쳐보이거나 텍스트가 제대로 노출되지 않는 문제를 마주칠 수 있습니다.

🔹 작성 코드

이미지 영역 벗어나는 현상 방지 기능 구현

  • clipsToBounds 라는 속성을 사용하면, View 영역 밖의 image를 표시하지 않는다!!
public let profileImageView: UIImageView = {
	let imageView = UIImageView()
	imageView.contentMode = .scaleAspectFit
	imageView.backgroundColor = .white
	imageView.layer.borderColor = UIColor.gray.cgColor
	imageView.layer.borderWidth = 3
	imageView.layer.cornerRadius = 80
    // 추가한 코드
	imageView.clipsToBounds = true
	return imageView
}()

재사용 셀 초기화

prepareForReuse()를 사용하여 재사용 셀 초기화

  • 사용하는 이유
    • Cell의 개수가 많아지면 셀을 재사용을 함.
    • 재사용 하면서 스크롤 시, 이전 이미지나 텍스트가 잠간 보이는 현상이 발생함!
    • prepareForReuse() 사용하면 셀을 다시 사용하기 전에 초기화 가능!!
    • 즉, "이 셀, 다른 데이터 보여주기 전에 깨끗이 지워 놓는다!!" 라는 개념.
class PhoneBookTableViewCell: UITableViewCell {
    override func prepareForReuse() {
        super.prepareForReuse()
        
        // 셀을 초기화
        pokemonImageView.image = nil
        nameLabel.text = ""
        phoneNumLabel.text = ""
    }
}

🔹 결과 화면


🏆 알고리즘 풀기!

✂️ 잘라서 배열로 저장하기

🔹 문제 설명

문자열 my_str과 n이 매개변수로 주어질 때, my_str을 길이 n씩 잘라서 저장한 배열을 return하도록 solution 함수를 완성해주세요.

🔹 제한 조건

1 ≤ my_str의 길이 ≤ 100
1 ≤ n ≤ my_str의 길이
my_str은 알파벳 소문자, 대문자, 숫자로 이루어져 있습니다.

🔹 첫 시도

import Foundation

func solution(_ my_str:String, _ n:Int) -> [String] {
    return []
}
  • 어떻게 해야될지 몰라서 건들지도 못함....
  • 하루종일 고민만 하다가 끝남...😭
  • 처음으로 문제를 못풀어봄

🔹 두번째 시도

다음날 튜터님께서 힌트를 주셨다!!!!
스트링 변수를 선언 후 변수 n과 스트링 변수 길이가 같으면 배열에 넣는 방법

import Foundation
    
func solution(_ my_str:String, _ n:Int) -> [String] {
	var string: String = ""
	var result:[String] = []
    
    for i in my_str {
        string += String(i)
        if string.count == n {
            result.append(string)
            string = ""
        }
    }
    return result
}
  • 힌트를 듣자마자 바로 코난의 머리 번쩍 처럼 코드를 줄줄이 써버렸다.
  • 될줄 알았으나, 실패.
  • my_str 의 길이가 n으로 나누어 떨어지지 않으면 result 배열에 남은 문자열을 저장하지 않는다.

🔹 세번째 시도

import Foundation

func solution(_ my_str:String, _ n:Int) -> [String] {

    var string: String = ""
    var result:[String] = []
    var count = 1
    
    for i in my_str {
        string += String(i)
        if string.count == n {
            result.append(string)
            string = ""
        } else if my_str - count < n {
            if my_str - count == 0 {
                result.append(string)
                string = ""
            } 
        }
        count += 1
    }
    return result
}
  • 머리를 겨우 짜내서 만든 로직
  • 나머지 문자열을 배열에 저장하는 로직을 만들었다.
    • count라는 변수를 만들어서 반복문이 돌때마다 숫자가 1씩 증가하게 했다.
    • 그리고 my_str에서 count 값을 뺀 값이 n 보다 작을때의 조건문을 만들었다.
    • 조건문 안에 또 my_str에서 count 값을 뺀 값이 0과 같으면 배열에 저장하게 했다.
  • 하지만 결과가 에러????
  • 이유는 my_str에서 count 값을 빼려고 해서!!!
  • my_str.count를 빼야지!!

🔹 최종 코드

import Foundation

func solution(_ my_str:String, _ n:Int) -> [String] {

    var string: String = ""
    var result:[String] = []
    var count = 1
    
    for i in my_str {
        string += String(i)
        if string.count == n {
            result.append(string)
            string = ""
        } else if my_str.count - count < n {
            if my_str.count - count == 0 {
                result.append(string)
                string = ""
            } 
        }
        count += 1
    }
    return result
}
  • 모든 오류를 잡아벼린 코드.

🔹 결과!!

오늘도 해냈다!!!


✍️ 느낀점 & 배운점

  • 과제를 벌써 다 끝냈다!!!
  • 과제를 하면서 계속 네트워크 쪽과 CoreData 쪽을 잘 모른다는 느낌이 들었다.
  • 주말에 두개를 공부하여 다른 코드에서도 응용할 수 있을 정도로 숙달하고 말겠다.
  • 알고리즘은 정말 처음으로 하루만에 못 푼 문제였지만 튜터님의 힌트를 듣자마자 바로 클리어!
  • 역시 안되면 도움을 청하는게 맞다는 것을 배웠다.
  • 오늘 하루도 알찼다!
profile
iOS 개발 블로그

0개의 댓글