Open API 기반 iOS앱 개발(4)

CDH·2025년 5월 14일

파싱을 쉽게 하기 위한 MovieData형 구조체 만들기


(중요!) 파싱을 쉽게 하기 위한 MovieData형 구조체 만들기

import UIKit

let movie = ["야당", "마인크래프트", "썬더볼츠", "진격의 거인", "야당2"]
struct MovieData : Codable { //
    let boxOfficeResult : BoxOfficeResult
}
struct BoxOfficeResult : Codable {
    let dailyBoxOfficeList : [DailyBoxOfficeList]
}
struct DailyBoxOfficeList : Codable {
    let movieNm : String
    let audiCnt : String
    let audiAcc : String
    let rank : String
}

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet weak var table: UITableView!
    let movieURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=자신의 키&targetDt=20250513"
    // url 주소 변수 추가
    
    override func viewDidLoad() {
        super.viewDidLoad()
        table.delegate = self // self 두 줄 없으면 동작 안 함
        table.dataSource = self
        getData()
    }
    
    func getData() { // 옵셔널 형이라 풀어줘야 된다
        guard let url = URL(string: movieURL) else { return } // guard let : 거짓일 경우를 먼저 작성
        let session = URLSession(configuration: .default) // .default 를 가장 많이 사용
        let task = session.dataTask(with: url) { data, response, error in
            if error != nil {
                print(error!)
                return
            } // if 문을 이용하여 에러처리
            guard let JSONdata = data else { return }
            let dataString = String(data: JSONdata, encoding: .utf8)
            // utf8 방식으로 인코딩된 데이터를 String형으로 변경하여 자료의 크기가 아닌 자료 자체를 출력
            // print(dataString!) // 전체 영화 순위 데이터
            
            let decoder = JSONDecoder()
            do {
                let decodedData = try decoder.decode(MovieData.self, from: JSONdata)
                // MovieData는 자료형이기 때문에 값도 써줘야 된다  // + 예외처리도 해줘야 한다.
                print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm)
                print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audiAcc)

            } catch {
                print(error)
            }
            

        }
        task.resume()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5 // 5칸짜리 테이블 뷰
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell // 다운캐스팅
        
        cell.movieName.text = movie[indexPath.row]
        // print(indexPath.description) // 추가 (현재 보이는 셀들의 인덱스를 개발자만 보이도록 출력)
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print(indexPath.description) // 특정 셀을 클릭(선택) 했을 때 해당하는 row 값을 개발자만 보이도록 출력
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        return 5 // 섹션의 수를 2칸으로 나눔 (총 10칸)
    }
    
    
}


(중요!) JSONDecoder : JSON데이터를 원하는 데이터 타입으로 변환

https://developer.apple.com/documentation/foundation/jsondecoder



(중요!) 클로저 안에서 프로퍼티 선언(접근) 시 주의사항

movieData 선언

클로저 안에서 프로퍼티 접근 시 self. 을 사용해야 한다


reloadData() : 테이블뷰 다시 그리기


(중요!) UITableView.reloadData()

UITableView.reloadData() must be used from main thread only
-> UITableView.reloadData()를 메인 스레드(Main Thread)에서 호출하지 않아서 발생한 에러
UiView 에서 동작하는 소스는 background에서 동작하면 안 되고 메인 쓰레드에서 동작해야 된다

DispatchQueue.main.async { } 를 사용하여 메인 스레드에서 작업을 비동기적으로 실행


개선 : 앱을 실행하면 어제 날짜로 자동 조회하기

어제 날짜에 대한 데이터를 가져오는 함수 만들기


전체 코드

movieURL 의 값에서 targetDt 값을 비워둬야 한다

import UIKit

let movie = ["야당", "마인크래프트", "썬더볼츠", "진격의 거인", "야당2"]
struct MovieData : Codable { //
    let boxOfficeResult : BoxOfficeResult
}
struct BoxOfficeResult : Codable {
    let dailyBoxOfficeList : [DailyBoxOfficeList]
}
struct DailyBoxOfficeList : Codable {
    let movieNm : String
    let audiCnt : String
    let audiAcc : String
    let rank : String
}

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    @IBOutlet weak var table: UITableView!
    
    var movieData : MovieData?
    var movieURL = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=자신의 키값&targetDt="
    // url 주소 변수 추가
    
    override func viewDidLoad() {
        super.viewDidLoad()
        table.delegate = self // self 두 줄 없으면 동작 안 함
        table.dataSource = self
        movieURL = movieURL + makeYesterdayString() // 어제 날짜를 불러오기 위한 함수 만들고 호출하기
        getData()
    }
    func makeYesterdayString() -> String {
        let y = Calendar.current.date(byAdding:.day, value : -1, to : Date())!
        let dateF = DateFormatter()
        dateF.dateFormat = "yyyyMMdd"
        let day = dateF.string(from: y)
        return day
    }
    
    func getData() { // 옵셔널 형이라 풀어줘야 된다
        guard let url = URL(string: movieURL) else { return } // guard let : 거짓일 경우를 먼저 작성
        let session = URLSession(configuration: .default) // .default 를 가장 많이 사용
        let task = session.dataTask(with: url) { data, response, error in
            if error != nil {
                print(error!)
                return
            } // if 문을 이용하여 에러처리
            guard let JSONdata = data else { return }
            let dataString = String(data: JSONdata, encoding: .utf8)
            // utf8 방식으로 인코딩된 데이터를 String형으로 변경하여 자료의 크기가 아닌 자료 자체를 출력
            // print(dataString!) // 전체 영화 순위 데이터
            
            let decoder = JSONDecoder()
            do {
                let decodedData = try decoder.decode(MovieData.self, from: JSONdata)
                // MovieData는 자료형이기 때문에 값도 써줘야 된다  // + 예외처리도 해줘야 한다.
                self.movieData = decodedData // 중요!
                print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].movieNm) // 영화 이름
                print(decodedData.boxOfficeResult.dailyBoxOfficeList[0].audiAcc) // 누적 관객수
                DispatchQueue.main.async {
                    self.table.reloadData()
                    // (중요!) UiView 에서 동작하는 소스는 background에서 동작하면 안 되고 메인 쓰레드에서 동작해야 된다
                }
                
            } catch {
                print(error)
            }
            
            
        }
        task.resume()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10 // 5칸짜리 테이블 뷰
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell // 다운캐스팅
        
        cell.movieName.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm
        // print(indexPath.description) // 추가 (현재 보이는 셀들의 인덱스를 개발자만 보이도록 출력)
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print(indexPath.description) // 특정 셀을 클릭(선택) 했을 때 해당하는 row 값을 개발자만 보이도록 출력
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1 // 섹션의 수 설정
    }
    
    
}

Label 디자인 변경시 : Resolve Auto Layout Issues


더 좋은 방법 : 현재 Constraint 삭제하고 Stack View 활용

Constraint 삭제 시 출력되는 화면


레이블 2개를 새로 만들어 Stack View에 넣기

실행화면



기본 옵션


새로운 Stack View로 레이블과 기존 Stack View 포함시킴

실행화면

스택뷰 안에 스택뷰가 있음




Label의 text 설정


바깥쪽 스택뷰의 Alignment를 Fill로 지정


두 스택뷰의 Distribution을 Fill Equally로 지정

실행화면



MyTableViewCell에 Outlet 추가 : audiAccumulate


MyTableViewCell에 Outlet 추가 : audiCount

cell.audiAccumulate.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiAcc
cell.audiCount.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiCnt

등수와 천(3자리)단위 콤마 추가

기존 코드 주석 후 새로운 코드 추가

    //    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell // 다운캐스팅
    //
    //        cell.movieName.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm
    //        // print(indexPath.description) // 추가 (현재 보이는 셀들의 인덱스를 개발자만 보이도록 출력)
    //        cell.audiAccumulate.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiAcc
    //        cell.audiCount.text = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiCnt
    //        return cell
    //    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! MyTableViewCell
        guard let mRank = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].rank else {return UITableViewCell()}
        guard let mName = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].movieNm else {return UITableViewCell()}
        cell.movieName.text = "[\(mRank)위] \(mName)"
        if let aCnt = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiCnt {
            let numF = NumberFormatter()
            numF.numberStyle = .decimal
            let aCount = Int(aCnt)!
            let result = numF.string(for: aCount)!+"명"
            cell.audiCount.text = "어제: \(result)"
        }
        if let aAcc = movieData?.boxOfficeResult.dailyBoxOfficeList[indexPath.row].audiAcc {
            let numF = NumberFormatter()
            numF.numberStyle = .decimal
            let aAcc1 = Int(aAcc)!
            let result = numF.string(for: aAcc1)!+"명"
            cell.audiAccumulate.text = "누적: \(result)"
        }
        return cell
    }

+ 추가 수정 & 최종 코드

tableView(_:titleForHeaderInSection:)

코드 추가 (헤더와 푸터)

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "박스오피스(영화진흥위원회 제공: " + makeYesterdayString() + ")"
    } // 헤더 부분
    
    func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
        return "made by CDH"
    } // 푸터 부분
    


영화 제목이 길어서 제목에 ...이 나온다면

Lines 의 수를 0으로 할 경우 글씨의 크기가 고정된다 (2줄 이상이 되어도)


Lines 의 수를 2로 설정

0개의 댓글