[내일배움캠프 35일차] Ch.3 앱 개발 숙련 - 개인 과제 시작

NH·2025년 4월 18일

내일배움캠프

목록 보기
35/62
post-thumbnail

☎️ 포켓몬 연락처 앱 - 개인 과제 시작!

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

Lv.1

🔹 요구사항

🧑🏻‍💻 Level 1 - 왼쪽 캡처 모습과 같게 UI 를 구현합니다.

  • UILabel, UITableView, UIButton 을 이용해서 기본적인 UI 를 구성합니다.
  • UITableViewCell 에는 프로필 이미지를 보여줄 UIImageView 와 이름을 보여줄 UILabel 을 넣습니다. 전화번호를 표시할 UILabel 도 넣습니다.
  • 프로필 이미지는 원모양이 되도록 합니다.
  • “추가” 버튼을 우상단에 위치 시킵니다.
  • Cell 의 높이는 80으로 지정합니다.
  • 원의 테두리는 layer.borderColor, layer.borderWidth 개념을 사용하면 구현할 수 있습니다.
  • 캡처된 화면은 임의의(더미) dataSource 를 끼워넣어 UI 를 확인한 모습입니다.

🔹 작성 코드

프로퍼티 선언

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

UI 설정 함수

``` 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에 대해서 강의와 실습을 해서 어려움 없이 해냈다.
  • 하지만 AutoLayout 설정에서 좀 어려움을 겪었지만 이겨냈다!
  • 익숙해진 줄 알았던 AutoLayout에 발목을 잡혀 힘들었지만, 이제 더 잘 할 자신이 있다!

Lv.2

🔹 요구사항

🧑🏻‍💻 Level2 - 연락처 추가화면을 구현합니다.

  • 메인화면에서 “추가”버튼을 누르면 이 페이지로 이동하도록 합니다. UINavigationController.push 사용
  • 연락처를 편집하거나 새롭게 추가할 페이지를 개발합니다. (PhoneBookViewController.swift)
  • 프로필 이미지: UIImageView
  • 랜덤 이미지 생성 버튼: UIButton
  • 이름: UITextView 또는 UITextField
  • 전화번호: UITextView 또는 UITextField

🔹 작성 코드

  • 새로운 뷰 컨트롤러를 생성했다.

프로퍼티

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

UI 설정 함수 작성

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

    }
}

🔹 결과 화면

🔹 느낀 점

  • 역시 어려움 없이 UI를 구성했다.

Lv.3

🔹 요구사항

🧑🏻‍💻 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)
    }
}

🔹 결과 화면

🔹 느낀 점

  • 네비게이션 바는 아직 다뤄본적이 없어서, 이 부분은 검색을 해보면서 구현했다.
  • 하지만 어렵진 않아서 바로 코드에 녹여서 사용했다!

Lv.4

🔹 요구사항

🧑🏻‍💻 Level 4 - API 연결

  • 랜덤 이미지 생성 버튼을 구현합니다.
  • 버튼을 클릭할때마다 랜덤한 포켓몬 이미지를 불러옵니다.
  • 포켓몬 API 를 생성해서 랜덤 포켓몬 이미지를 프로필로 지정해봅시다.
  • https://pokeapi.co/
  • API 명세
    - GET 메서드 사용
    - URL: https://pokeapi.co/api/v2/pokemon/{1~1000사이의랜덤숫자입력}
    Json Response 형태 (더 많은 정보를 받을 수 있지만 이 정도만 받아봅시다.)
    여기서 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"
     }
    }

🔹 작성 코드

API 데이터 모델

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

API 서버에서 데이터 받아오기

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

🔹 결과 화면

🔹 느낀 점

  • 강의를 들으며, 실습해본 URLSession 이지만 아직도 낯설고, 사용하는 내 자신이 어설픈게 느껴진다.
  • 아직 잘 몰라서 이전에 실습한 코드를 보면서 코딩했고, 기능을 녹여내는데에는 큰 어려움은 없었다.
profile
iOS 개발 블로그

0개의 댓글