ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 날씨앱만들기 (4) 5일간의 날씨데이터로 구성된 테이블뷰
    카테고리 없음 2024. 7. 15. 17:28

    (이전 게시물)

    이제는 날씨 이미지 아래에 들어갈 5일간의 날씨데이터로 구성된 테이블뷰에 대해 작업해줘야 한다. 

     

     

    이번에 사용할 API는 openWeather에서 forecastWeather API이다. 

     

    기존에 프로퍼티들을 등록한 곳에 tableView도 하나 만들어준다. 

     

    ViewController.swift

        //5일간의 날씨데이터를 담을 테이블뷰
        private lazy var tableView: UITableView = {
            let tableView = UITableView()
            tableView.backgroundColor = .black
            // delegate: "대리자, 대신 수행 해주는 사람", Table View의 여러가지 속성 세팅을 이 View Controller에서 대신 세팅하는 코드를 작성해주겠다.
            tableView.delegate = self
            // dateSource: 테이블뷰 안에 집어넣을 데이터들, 이 View Controller에서 세팅해주겠다.
            tableView.dataSource = self
        }()

     

    tableView는 delegate와 dataSource를 세팅해줘야 온전히 구현할 수 있다. 

     

    처음에는 Private let으로 작성했지만 오류가 난다. 이유는, 위 코드는 UI 초기화 설정단계이기 때문에 ViewController가 완성되지 않았기 때문이라고 한다. 그래서 private lazy var로 작성해준다. 

     

    위까지 작성하게 되면 tableView delegate 세팅하지 않았다는 오류 메시지가 뜬다. 

     

    View Controller class 밖에서 extension으로 View Controller가 TableViewDelegate를 만족할 수 있도록 코드를 작성해줘야 한다. 

    extension ViewController: UITableViewDelegate {
        // 테이블뷰 셀 높이 크기 지정
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            40
        }
    }

     

    TableViewDataSource도 마찬가지로 코드를 작성해줘야 한다. 

    extension ViewController: UITableViewDataSource {
        // 테이블 뷰의 indexPath마다 테이블 뷰 셀을 지정
        // indextPath는 테이블뷰의 행과 섹션을 의미
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.id) as? TableViewCell else {
                return UITableViewCell()
            }
            cell.configureCell(forecastWeather: dataSource[indexPath.row])
            return cell
            }
    
        // 테이블 뷰 섹션에 행이 몇 개 들어가는가. 여기서 섹션은 없으니 총 행 개수만 입력하면 된다.
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            dataSource.count
        }
    }

     

    UITableViewDataSource 안에는 반드시 

    cellForRowAt과 NumberOfRowsInSection메서드를 포함해야 한다. (주석 참고)

     

    이제 TableViewCell로 가서 정의해준다. 

     

    TableViewCell.swift

    import UIKit
    import SnapKit
    
    final class TableViewCell: UITableViewCell {
        
        static let id = "TableViewCell"
        
        private let dtTxtLabel: UILabel = {
            let label = UILabel()
            label.backgroundColor = .black
            label.textColor = .white
            return label
        }()
        
        private let tempLabel: UILabel = {
            let label = UILabel()
            label.backgroundColor = .black
            label.textColor = .white
            return label
        }()
        // Table View의 style과 id로 초기화할 때 사용하는 코드
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            configureUI()
        }
        // 인터페이스 빌더를 통해 셀을 초기화할 때 사용하는 코드
        // 여기서는 fatalError를 통해서 명시적으로 인터페이스 빌더로 초기화하지 않음을 나타냄
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        private func configureUI() {
            
            contentView.backgroundColor = .black
            
            [
                dtTxtLabel,
                tempLabel
            ].forEach { contentView.addSubview($0) }
            
            dtTxtLabel.snp.makeConstraints {
                $0.centerY.equalToSuperview()
                $0.leading.equalToSuperview().inset(20)
            }
            
            tempLabel.snp.makeConstraints {
                $0.centerY.equalToSuperview()
                $0.trailing.equalToSuperview().inset(20)
            }
        }
        
        public func configureCell(forecastWeather: ForecastWeather) {
            dtTxtLabel.text = "\(forecastWeather.dtTxt)"
            tempLabel.text = "\(forecastWeather.main.temp)°C"
        }
    
    }

     

    처음에는 configureUI()를 만들어 놓고 초기화 공간에서 호출하지 않아서 하얀 테이블뷰만 나왔었다.. ㅎㅎ

     

    커스텀한 UI class를 생성할 때는 override init과 required init을 모두 써줘야 한다. (주석 참고) 

     

    이제 테이블뷰에 대한 UI는 작업이 어느정도 되었기 때문에 

     

    ForecastWeatherData에다가 서버에서 가져올 데이터 형식에 대해 정의해준다. 

     

    ForecastWeatherResult.swift

    import Foundation
    
    
    //Codable을 채택하는 구조체 생성
    struct ForecastWeatherResult: Codable {
        let list: [ForecastWeather]
        
    }
    
    struct ForecastWeather: Codable {
        let main: WeatherMain
        let dtTxt: String
        
        enum CodingKeys: String, CodingKey {
            case main
            case dtTxt = "dt_txt"
        }
    }

     

    여기서도 마찬가지로 openWeather에서는 dtTxt에 대해 스네이크 표기법을 쓰고 있기 때문에 camel 형식을 쓰는 Swift에서 한번에 JSON Decoding이 안 되기 때문에 파싱이 되도록 enum과 case로 코드를 입력해준다. 

     

    currentWeather 때와 마찬가지로 fetchData메서드를 통해 서버데이터를 불러와준다. 

    ViewController.swift

    // 서버에서 5일 간 날씨 데이터를 불러오는 메서드
        private func fetchForecastData() {
            var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/forecast")
            urlComponents?.queryItems = self.urlQueryItems
            
            guard let url = urlComponents?.url else {
                print("잘못된 URL")
                return
            }
            
            fetchData(url: url) { [weak self] (result: ForecastWeatherResult?) in
                guard let self, let result else { return }
                
                // 콘솔에다가 데이터 잘 불러왔는지 찍어보기
                for forecastWeather in result.list {
                    print("\(forecastWeather.main)\n\(forecastWeather.dtTxt)\n\n")
                }
                DispatchQueue.main.async {
                    self.dataSource = result.list
                    self.tableView.reloadData() // reloadData()를 해야 반영 됨
                }  
            }
        }

     

    마찬가지로 url을 불러올 때 쿼리 이전까지 불러온다. 쿼리아이템은 위에서 선언해준 것을 재활용한다. (url 바로 뒤 코드 참고) 

     

    위 코드를 보면 콘솔에다가 데이터를 잘 불러왔는지 확인할 수 있도록 DataSource를 선언해준 것을 불러온다. 

     

    아래는 DataSource 선언해준 코드이다. 

    ViewController.swift

        //데이터소스 타입은 항상 리스트
        private var dataSource = [ForecastWeather]()

     

    작업해준 테이블뷰가 보일 수 있도록 

     

    뷰생성을 해주고, 제약조건을 마무리한다. 

     

    테이블뷰의 뷰생성과, 제약조건이 포함된 configureUI 메서드

    ViewController.swift

    private func configureUI() {
            view.backgroundColor = .black
            
            //각 프로퍼티 뷰 생성
            [
                titleLabel,
                tempLabel,
                tempStackView,
                imageView,
                tableView
            ].forEach { view.addSubview($0) }
            
            //tempStackView에 들어갈 최저기온과 최고기온 뷰 추가
            [
                tempMinLabel,
                tempMaxLabel
            ].forEach { tempStackView.addArrangedSubview($0) }
            
            //각 프로퍼티 제약 조건
            titleLabel.snp.makeConstraints {
                $0.centerX.equalToSuperview()
                $0.top.equalToSuperview().offset(120)
            }
            
            tempLabel.snp.makeConstraints {
                $0.centerX.equalToSuperview()
                $0.top.equalTo(titleLabel.snp.bottom).offset(10)
            }
            
            tempStackView.snp.makeConstraints {
                $0.centerX.equalToSuperview()
                $0.top.equalTo(tempLabel.snp.bottom).offset(10)
            }
            
            imageView.snp.makeConstraints {
                $0.centerX.equalToSuperview()
                $0.width.height.equalTo(160)
                $0.top.equalTo(tempStackView.snp.bottom).offset(20)
            }
            
            tableView.snp.makeConstraints {
                $0.top.equalTo(imageView.snp.bottom).offset(30)
                $0.leading.trailing.equalToSuperview().inset(20)
                $0.bottom.equalToSuperview().inset(50)
            }
            
        }
    }

     

    forecastWeather에 대해서도 fetchData 작업을 끝냈으니 viewDidLoad에서 호출해준다. 

     

    마지막으로 테이블뷰에 해줘야 하는 작업으로 테이블뷰에다 만들어준 테이블셀을 등록하는 것이 남았다. 

    ViewController.swift

        //5일간의 날씨데이터를 담을 테이블뷰
        private lazy var tableView: UITableView = {
            let tableView = UITableView()
            tableView.backgroundColor = .black
            // delegate: "대리자, 대신 수행 해주는 사람", Table View의 여러가지 속성 세팅을 이 View Controller에서 대신 세팅하는 코드를 작성해주겠다.
            tableView.delegate = self
            // dateSource: 테이블뷰 안에 집어넣을 데이터들, 이 View Controller에서 세팅해주겠다.
            tableView.dataSource = self
            // 테이블뷰에다가 테이블 뷰 셀 등록
            tableView.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.id)
            return tableView
        }()

     

    실행하면 콘솔창에서도 데이터 로드가 확인되고, UI도 원하는대로 구성된 것을 확인할 수 있다.