기상정보 앱 만들어 보기

고라니·2023년 8월 4일
0

TIL

목록 보기
20/67

BoostCourse iOS 프로그래밍에서 JSON을 디코딩하여 기상정보 앱을 만드는 단계가 있었다. 물론 기간이 지나서 리뷰는 받아볼 수 없지만 직접 고민해서 만들어 보면 좋을듯 하여 만들어 보았다.

목표

제공받은 JSON파일과 이미지들을 활용하여 3가지의 뷰를 구현하는 것이 목표다.

  1. 국가 선택 리스트 화면
  2. 도시, 기상정보 리스트 화면
  3. 기상정보 디테일 화면

UI 구성 및 연결

UI 구성

먼저 배운대로 테이블뷰와 셀들 그리고 ImageView와 Label들을 추가하고 오토레이아웃을 적절하게 잡아줬다. (계속 해보니 오토레이아웃 잘잡아요~)

그리고 루트뷰에 네비게이션컨트롤러를 연결해주고 순서대로 Segue를 show속성으로 연결해줬다.

나중에 알았는데 코드로 Title을 지정해주기 때문에 따로 스토리보드에서 Title을 생성해줄 필요가 없었다.

UI 연결

  • 각 뷰컨트롤러 생성 후 인터페이스빌더의 뷰들과 연결
    CountrySelectionViewController
    CitySelectionViewController
    WeatherDetailViewController

  • 각 셀들을 테이블뷰셀 컨트롤러에 연결
    CountryTableViewCell
    CityTableViewCell

  • Segue와 Cell Identifier 설정
    "CountryToCity"
    "CityToDetail"
    "CountryCell"
    "CityCell"

JSON Decoding할 Model 만들기

Country 모델

struct Country: Codable {
    var countryName: String
    var assetName: String

    var countryImage: UIImage {
        guard let image = UIImage(named: "flag_\(assetName)") else {
            return UIImage()
        }
        return image
    }

    enum CodingKeys: String, CodingKey {
        case countryName = "korean_name"
        case assetName = "asset_name"
    }
}

Codingkey를 이용하여 Swift에 적합하게 맵핑 해주었다.
UIImage로 간편하게 사용하기 위해 연산프로퍼티로 countryImage를 정의하였다.

City 모델

struct City: Codable {
    var cityName: String
    var weatherState: Int
    var celsius: Double
    var rainfallProbability: Int

    var temperature: String {
        let fahrenheit = celsius * 9 / 5 + 32
        let formatFahrenheit = String(format: "%.1f", fahrenheit)
        return String("\(celsius) / \(formatFahrenheit)")
    }

    var rainfallProbabilityString: String {
        return String("강수확률 \(rainfallProbability)%")
    }

    var weatherImage: UIImage {
        switch weatherState {
        case 10:
            return UIImage(named: "sunny") ?? UIImage()
        case 11:
            return UIImage(named: "cloudy") ?? UIImage()
        case 12:
            return UIImage(named: "rainy") ?? UIImage()
        default:
            return UIImage(named: "snowy") ?? UIImage()
        }
    }

    var weatherText: String {
        switch weatherState {
        case 10:
            return "맑음"
        case 11:
            return "구름"
        case 12:
            return "비"
        default:
            return "눈"
        }
    }


    enum CodingKeys: String, CodingKey {
        case cityName = "city_name"
        case weatherState = "state"
        case celsius
        case rainfallProbability = "rainfall_probability"
    }
}

코드가 Country 모델보다 길고 복잡하지만 결국 데이터를 편하게 쓰기 위한 연산프로퍼티 작성과, Codingkey를 이용한 속성명 맵핑작업 들로 거의 동일하다.

컨트롤러 구현

let countryCellIdentifier: String = "CountryCell"
let countryToCitySegueIdentifier: String = "countryToCity"
var countries: [Country] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let jsonDecoder = JSONDecoder()
        
        guard let dataAsset: NSDataAsset = NSDataAsset(name: "countries") else { return }

        do {
            countries = try jsonDecoder.decode([Country].self, from: dataAsset.data)
        } catch {
            print(error)
        }
    }

Assets에 저장한 Country JSON파일을 디코딩하여 변수 country에 저장해준다.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        countries.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: countryCellIdentifier, for: indexPath) as! CountryTableViewCell
        let country = countries[indexPath.row]

        cell.accessoryType = .disclosureIndicator
        cell.countryTextLabel.text = country.countryName
        cell.countryImageView.image = country.countryImage
        return cell
    }

테이블뷰 델리게이트 메서드를 구현한다.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == countryToCitySegueIdentifier {
            
            guard let nextViewController: CitySelectionViewController = segue.destination as? CitySelectionViewController else {
                return
            }

            guard let indexPath = countryTableView.indexPathForSelectedRow else {
                return
            }

            nextViewController.countryName = countries[indexPath.row].countryName
            nextViewController.dataName = countries[indexPath.row].assetName
        }
    }
    
    class CitySellectionViewcontroller: UIViewController {
    	var countryName: String = ""
    	var dataName: String = ""
        // ...
        

Segue를 사용하여 화면전환을 하기 때문에 'prepare'메서드를 사용해서 다음 뷰에 필요한 정보들을 넘겨준다.
이 때 Identifier을 이용하여 적절한 Segue를 식별해주고, 'indexPathForSelectedRow'를 통해 어떤 국가가 선택되었는지 다음 뷰에 넘겨준다. 그리고 이전에 주의할점으로 언급했던 내용대로 다음 뷰의 UI에 직접 접근하지 않고 프로퍼티에 값을 넘겨준다.

선택한 도시의 정보를 받아 CitySellectionViewcontroller도 CountrySellectionViewContlloer러와 거의 동일하게 작성했다.

class WeatherDetailViewController: UIViewController {
    
    @IBOutlet weak var weatherImageView: UIImageView!
    @IBOutlet weak var weatherLabel: UILabel!
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var rainfallprobablityLabel: UILabel!

    var cityName: String = ""
    var weatherImage: UIImage = UIImage()
    var weather: String = ""
    var temperature: String = ""
    var rainfallprobablity: String = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = cityName
        weatherImageView.image = weatherImage
        weatherLabel.text = weather
        temperatureLabel.text = temperature
        rainfallprobablityLabel.text = rainfallprobablity
    }
}

마지막 WeatherDetailViewController는 이전 뷰에서 prepare메서드를 통해 받은 데이터를 UI에 적용시킨다.

확인

의도한대로 잘 작동한다.

리팩토링

약간의 리팩토링이 필요해 보여서 손을좀 봤다.

1. jsonDecoder 중복 생성 해결

우선 동일한 jsonDecoder가 sellectionView마다 생성되고 있었다.

코드 중복을 줄이기 위해 BaseViewController를 생성하여 jsonDecoder프로퍼티를 생성해주었고, 필요한 뷰컨트롤러에서 BaseViewContrller를 상속받아 jsonDecoder을 사용하게 수정하였다.

class BaseViewController: UIViewController {
    let jsonDecoder = JSONDecoder()
}

2. Identifier 관리

여러 식별자들을 관리하는 구조체를 따로 만들어서 관리할 수 있도록 하였다.

struct Identifier {
    struct Segue {
        static let countryToCity: String = "CountryToCity"
        static let cityToDetail: String = "CityToDetail"
    }

    struct Cell {
        static let countryCell: String = "CountryCell"
        static let cityCell: String = "CityCell"
    }
}

// 사용
class CountrySellectionViewController: BaseViewController {
	// ... Segue Identifier 사용
	if segue.identifier == Identifier.Segue.countryToCity...
    // ... Cell Identifier 사용
    let cell = tableView.dequeueReusableCell(withIdentifier: Identifier.Cell.countryCell, for: indexPath) as! CountryTableViewCell...
    // ... 

3. 그룹 나누기

폴더들을 만들어서 역할별로 구분하기 쉽게 구룹핑 하였다.

코드리뷰 기간이 지나서 아쉽지만 그래도 만들어 보았다. 항상 느끼지만 개념만 알고 넘어가는거랑 직접 만들어보는게 정말 큰 차이가 있다. 이번에도 많이 배웠다.
제대로 만든게 맞는지 궁굼하긴 하다.
누군가 잘못된게 보인다면 알려주셨으면 감사하겠습니다~

자료 출처: https://www.boostcourse.org/mo326/project/22/content/20?isDesc=false#summary

profile
🍎 무럭무럭

0개의 댓글