[README] FineDust

승아·2021년 9월 1일
0
post-thumbnail

FineDust

  • 미세먼지와 초미세먼지 정보를 실시간으로 확인할 수 있는 앱
  • github

기능

✅ 현재 위치 미세먼지, 초미세먼지 정보 조회

  • 현재 위치의 미세먼지와 초미세먼지의 정보 확인

✅ 지역 추가

  • 미세먼지와 초미세먼지의 정보를 알고 싶은 지역을 검색하여 추가

✅ 위젯으로 현재 위치 현재 위치 미세먼지, 초미세먼지 정보 조회

  • 위젯을 사용하여 현재 위치의 미세먼지와 초미세먼지의 정보 확인

설계

✅ ViewController 구성

✅ ViewCotroller의 역할

FineDustViewController

  • 해당 지역의 미세먼지와 초미세먼지의 정보를 보여준다.

LocationListViewController

  • 추가한 지역의 미세먼지와 초미세먼지의 정보를 보여준다.

SearchLocationViewController

  • 지역을 검색할 수 있다.

✅ ViewModel의 역할

FineDustViewModel

  • 미세먼지와 초미세먼지의 정보를 불러온다.
  • 미세먼지 관련 데이터들을 정의한다.

FineDustListViewModel

  • 추가한 지역의 미세먼지와 초미세먼지의 정보를 불러온다.
  • 추가한 지역의 미세먼지 관련 데이터들을 정의한다.

CurrentLocationViewModel

  • 해당 지역의 이름을 불러온다.

구현

✅ Alamofire

https://www.zehye.kr/ios/2020/04/01/12iOS_alamofire/

  • Swift 기반의 HTTP 네트워킹 라이브러리
  • URLSession 기반이며 URLSession 보다 더 깔끔하고 가독성 있게 구현 할 수 있음
func request(_ convertible: URLConvertible, // URL
             method: HTTPMethod = .get,
             parameters: Parameters? = nil,
             encoding: ParameterEncoding = URLEncoding.default,
             headers: HTTPHeaders? = nil,
             interceptor: RequestInterceptor? = nil,
             requestModifier: RequestModifier? = nil) -> DataRequest

URLSession 사용

var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { (data, response, error) in
  if let err = error { // 에러 체크
    print(" ---> error : \(APIError.stationAPIError)")
    onComplete(.failure(err))
  }
  guard let data = data else { // data nil 체크
    onComplete(.failure(APIError.stationAPIError))
    return
  }
  let json = JSON(data)
  guard let station: String = json["response"]["body"]["items"][0]["stationName"].string else {
    onComplete(.failure(APIError.stationAPIError))
    return
  }
  onComplete(.success(station))
}.resume()

Alamofire 사용

AF.request(url, method: .get, encoding: URLEncoding.default)
  .responseJSON{ (response) in
    switch response.result{ // result를 통해 성공 여부를 구분할 수 있음
    case .success(let data):
      let json = JSON(data)
      guard let station: String = json["response"]["body"]["items"][0]["stationName"].string else {
        onComplete(.failure(APIError.stationAPIError))
        return
      }
      onComplete(.success(station))
    case .failure(let error):
      print(" ---> error : \(APIError.stationAPIError)")
      onComplete(.failure(error))
    }
  }

✅ SwiftyJSON

  • Alamofire와 호환이 가능하며 JSON 파싱을 자동으로 해주는 라이브러리.

JSONSerialization 사용

// 받아온 Json 파일
// {"response":
//	{"body":
//		{"items":[

// 받아온 json을 [String: Any]로 변환하겠다.
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else{
    return
}

responses = json["response"] as! [String : Any] 
bodys = responses["body"] as! [String : Any]
items = bodys["items"] as! [[String : Any]]
let item: [String : Any] = items[0]
guard let station: String = item["stationName"] as? String else {
  onComplete(.failure(APIError.stationAPIError))
  return
}

SwiftyJSON 사용

guard let station: String = json["response"]["body"]["items"][0]["stationName"].string else {
  onComplete(.failure(APIError.stationAPIError))
  return
}

✅ 위치 자동완성 검색

여기에 포스팅

✅ WidgetKit

여기에 포스팅

✅ RxSwift

RxSwift란?

  • Swift에 ReactiveX를 적용시켜 비동기 프로그래밍을 직관적으로 작성할 수 있도록 도와주는 라이브러리.

Subject

  • Subject는 옵저버이기 때문에 하나 이상의 Observable을 구독할 수 있으며 동시에 Observable이기도 하기 때문에 항목들을 하나 하나 거치면서 재배출하고 관찰하며 새로운 항목들을 배출할 수도 있다.
  • Observable은 Unicast, Subjcet는 Multicast

Relay

  • Subject는 한 번 error나면 스트림이 끊기지만 Relay는 error가 나도 스트림이 끊어지지 않는다.
  • onNext(), onError()을 accept()로 표현해줘야 한다.

✅ RxSwift로 구현한 미세먼지 공공데이터API 비동기 처리

1. Alamofire을 활용해 미세먼지 공공데이터 API를 불러온다.

static func fetchFineDust(stationName: String, onComplete: @escaping (Result<FineDustAPIData, Error>) -> Void){
  let url = fineDustURL(stationName: stationName)
  
  AF.request(url, method: .get, encoding: URLEncoding.default)
    .responseJSON{ (response) in
      switch response.result{
      case let .success(data):
        let json = JSON(data)
        guard let fineDustValue: String = json["response"]["body"]["items"][0]["pm10Value"].string else {
          print("finedust Error")
          onComplete(.failure(APIError.finedustAPIError))
          return
        }
        guard let ultraFineDustValue: String = json["response"]["body"]["items"][0]["pm25Value"].string else {
          print("ultrafinedust Error")
          onComplete(.failure(APIError.finedustAPIError))
          return
        }
        guard let dateTime: String = json["response"]["body"]["items"][0]["dataTime"].string else {
          print("dateTime Error")
          onComplete(.failure(APIError.finedustAPIError))
          return
        }
        let response = fineDustAPIData(dateTime: dateTime, fineDustValue: fineDustValue, ultraFineDustValue: ultraFineDustValue, stationName: stationName)
        onComplete(.success(response))
      case let .failure(error):
        onComplete(.failure(error))
      }
    }
}

2. Observable을 생성한다.

static func loadFineDust(stationName: String) -> Observable<FineDustAPIData>{
  return Observable.create{ emitter in
    self.fetchFineDust(stationName: stationName){ result in
      switch result {
      case let .success(finedust):
        emitter.onNext(finedust)
        emitter.onCompleted()
      case let .failure(error):
        emitter.onError(error)
      }
    }
    return Disposables.create()
  }
}

3. PublishRelay 생성 후 Observable을 구독하여 결과값을 relay에거 넘겨준다.

  • errorr가 발생해도 끊기지 않도록 PublishRelay를 생성해주었다. 또한 이전 데이터가 필요없어 BehaviorRelay가 아닌 PublishRelay를 사용하였다.
  • Strog Reference Cycles을 막기 위해 weak self를 사용하였다.
  • 현재 위치의 미세먼지 데이터를 가져오려면 현 위치 위경도 얻기 -> 위경도를 토대로 TM좌표값 얻기 -> TM값을 토대로 현재 위치와 가까운 측정소 찾기 -> 해당 측정소의 미세먼지값 가져오기 4단계를 거쳐야 하기 때문에 Observable을 return하는 flatMap을 사용하여 처리해주었다.
lazy var observable = PublishRelay<FineDustAPIData>()

func loadFineDust(latitude: Double, longtitude: Double, mode: FineDustVCMode){
  _ = APIService.loadTM(latitude: latitude, longtitude: longtitude)
    .flatMap{ tm in APIService.loadStation(tmX: tm.tmX, tmY: tm.tmY)}
    .flatMap{ station in APIService.loadFineDust(stationName: station)}
    .take(1)
    .subscribe(onNext:{ [weak self] response in
      self?.observable.accept(response)
    })
}

4. FineDustViewController에서 PublishRelay 구독하여 미세먼지 데이터를 받은 후 화면에 표시한다.

  • View를 꾸며줘야 되므로 observe(on: MainScheduler.instance)을 통해 MainThread로 변경해주었다.
fineDustViewModel.observable
  .observe(on: MainScheduler.instance)
  .subscribe(onNext:{ [weak self] in
    self?.configureView($0)
    self?.setProgressView($0)
  })
  .disposed(by: disposeBag)

0개의 댓글