강의로 배운 데이터 관리하는 방법을 사용하여, 포켓몬 연락처 앱을 구현하는 과제를 시작한다!
API 서버에서 데이터를 가져오는 방법과, cell 에 데이터를 보여주는 법 그리고 디스크에 영구적으로 데이터를 저장하는 방법을 다 사용해야된다.

🧑🏻💻 Level 1 - 왼쪽 캡처 모습과 같게 UI 를 구현합니다.
UILabel, UITableView, UIButton 을 이용해서 기본적인 UI 를 구성합니다.UITableViewCell 에는 프로필 이미지를 보여줄 UIImageView 와 이름을 보여줄 UILabel 을 넣습니다. 전화번호를 표시할 UILabel 도 넣습니다.layer.borderColor, layer.borderWidth 개념을 사용하면 구현할 수 있습니다.import UIKit
import SnapKit
class ViewController: UIViewController {
let titleLabel: UILabel = {
let label = UILabel()
label.text = "친구 목록"
label.font = .boldSystemFont(ofSize: 20)
label.textColor = .black
label.textAlignment = .center
return label
}()
let addButton: UIButton = {
let button = UIButton()
button.setTitle("추가", for: .normal)
button.setTitleColor(.gray, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 20)
button.titleLabel?.textAlignment = .center
return button
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.backgroundColor = .cyan
tableView.dataSource = self
tableView.delegate = self
//tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.register(PhoneBookTableViewCell.self, forCellReuseIdentifier: PhoneBookTableViewCell.id)
return tableView
}()
``` swift
import UIKit
import SnapKit
class ViewController: UIViewController {
.
.
.
private func setTitleLabel() {
view.addSubview(titleLabel)
titleLabel.snp.makeConstraints {
$0.width.equalTo(80)
//$0.top.equalTo(view.safeAreaLayoutGuide)
$0.top.equalToSuperview().offset(80)
$0.centerX.equalToSuperview()
}
}
private func setAddButton() {
view.addSubview(addButton)
addButton.snp.makeConstraints {
$0.width.equalTo(80)
$0.height.equalTo(titleLabel.snp.height)
$0.top.equalToSuperview().offset(80)
//$0.centerY.equalTo(tableView.snp.centerY)
$0.trailing.equalToSuperview()
}
}
private func setTableView() {
view.addSubview(tableView)
tableView.snp.makeConstraints {
$0.top.equalTo(titleLabel.snp.bottom)
$0.bottom.equalToSuperview()
$0.horizontalEdges.equalToSuperview()
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: PhoneBookTableViewCell.id, for: indexPath) as? PhoneBookTableViewCell else { return UITableViewCell() }
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
}
CollectionView와 TableView에 대해서 강의와 실습을 해서 어려움 없이 해냈다.
🧑🏻💻 Level2 - 연락처 추가화면을 구현합니다.
UINavigationController.push 사용PhoneBookViewController.swift)UIImageViewUIButtonUITextView 또는 UITextFieldUITextView 또는 UITextFieldimport UIKit
class PhoneBookViewController: UIViewController {
private 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
return imageView
}()
private let ramdomImageGenerationButton: UIButton = {
let button = UIButton()
button.setTitle("랜덤 이미지 생성", for: .normal)
button.setTitleColor(.gray, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14)
button.titleLabel?.textAlignment = .center
//button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
return button
}()
private let nameTextField: UITextField = {
let textField = UITextField()
textField.backgroundColor = .white
textField.borderStyle = .roundedRect
return textField
}()
private let phoneNumTextField: UITextField = {
let textField = UITextField()
textField.borderStyle = .roundedRect
return textField
}()
class PhoneBookViewController: UIViewController {
.
.
.
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.navigationItem.title = "연락처 추가"
setProfileImageView()
setRamdomImageGenerationButton()
setNameTextField()
setPhoneNumTextField()
}
private func setProfileImageView() {
self.view.addSubview(profileImageView)
profileImageView.snp.makeConstraints {
$0.top.equalTo(view.safeAreaLayoutGuide).offset(18)
$0.centerX.equalToSuperview()
$0.width.height.equalTo(160)
}
}
private func setRamdomImageGenerationButton() {
self.view.addSubview(ramdomImageGenerationButton)
ramdomImageGenerationButton.snp.makeConstraints {
$0.top.equalTo(profileImageView.snp.bottom).offset(20)
$0.centerX.equalToSuperview()
}
}
private func setNameTextField() {
self.view.addSubview(nameTextField)
nameTextField.snp.makeConstraints {
$0.top.equalTo(ramdomImageGenerationButton.snp.bottom).offset(20)
$0.leading.equalToSuperview().offset(20)
$0.trailing.equalToSuperview().inset(20)
$0.centerX.equalToSuperview()
}
}
private func setPhoneNumTextField() {
self.view.addSubview(phoneNumTextField)
phoneNumTextField.snp.makeConstraints {
$0.top.equalTo(nameTextField.snp.bottom).offset(10)
$0.leading.equalToSuperview().offset(20)
$0.trailing.equalToSuperview().inset(20)
$0.centerX.equalToSuperview()
}
}
}


🧑🏻💻 Level 3 - 상단 네비게이션 바 영역을 구현합니다.
UINavigationController 의 상단에는 UINavigationBar 가 자동으로 생성됩니다.UINavigationItem , UINavigationBar 의 개념을 공부하면 개발할 수 있게 됩니다. 직접 공부하고 구현을 성공해봅시다.class PhoneBookViewController: UIViewController {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "적용", style: .done, target: self, action: #selector(Tapped))
@objc
private func buttonTapped() {
self.navigationController?.pushViewController(PhoneBookViewController(), animated: true)
}
}

🧑🏻💻 Level 4 - API 연결
랜덤 이미지 생성 버튼을 구현합니다.GET 메서드 사용https://pokeapi.co/api/v2/pokemon/{1~1000사이의랜덤숫자입력}front_default 가 포켓몬의 정면 이미지 url 입니다.{
"id": 25,
"name": "pikachu",
"height": 4,
"weight": 60,
"sprites": {
"front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png"
}
}import Foundation
struct PokemonImageResult: Codable {
let id: Int
let name: String
let height: Int
let weight: Int
let sprites: PokemonSprites
}
struct PokemonSprites: Codable {
let frontDefault: String
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
}
}
@objc
private func ramdomImageGenerationButtonTapped() {
print("랜덤 이미지를 생성합니다.")
fetchPokemonImage()
}
// MARK: - API 데이터 가져오기
private func fetchData<T: Decodable>(url: URL, completion: @escaping (T?) -> Void) {
let session = URLSession(configuration: .default)
session.dataTask(with: URLRequest(url: url)) { data, response, error in
guard let data, error == nil else {
print("데이터 로드 실패")
completion(nil)
return
}
let successRange = 200..<300
if let response = response as? HTTPURLResponse, successRange.contains(response.statusCode) {
guard let decodedData = try? JSONDecoder().decode(T.self, from: data) else {
print(response.statusCode, "JSON 디코딩 실패")
completion(nil)
return
}
completion(decodedData)
} else {
print("응답 오류")
completion(nil)
}
}.resume()
}
private func fetchPokemonImage() {
let ramdomNumber = String(Int.random(in: 1...1000))
let urlComponents = URLComponents(string: "https://pokeapi.co/api/v2/pokemon/\(ramdomNumber)")
guard let url = urlComponents?.url else {
print("잘못된 URL")
return
}
fetchData(url: url) { [weak self] (result: PokemonImageResult?) in
guard let self, let result else { return }
guard let imageUrl = URL(string: "\(result.sprites.frontDefault)") else {
print("이미지 불러오기 실패")
return
}
if let data = try? Data(contentsOf: imageUrl) {
if let image = UIImage(data: data) {
DispatchQueue.main.async {
self.profileImageView.image = image
}
}
}
}
}
