사이드 프로젝트를 진행하면서
대중교통을 통해 갈 수 있는 경로들을 보여주면서, 각각의 경로에 대한 실시간 도착정보가 제공되도록 하려고 했습니다.
이를 위해 지하철과 버스의 실시간 도착 정보 API를 사용하게 되었고, 각각의 API를 통해 도착까지 남은 시간을 초 단위의 Int값으로 받아와서 앱 상에서는 남은 시간을 실시간으로 카운트하며 표시해주려고 했습니다.
그렇기 때문에 도착 정보에 대한 데이터 타입은 단순하게 남은 시간(초)를 나타내는 Int값이었습니다.
지하철 도착 정보 API의 경우 도착 정보로 도착까지 남은 시간을 초 단위의 숫자값으로 제공했기 때문에 Int타입으로 쉽게 변환하여 사용할 수 있었습니다.
하지만 버스 도착 정보 API는 조금 달랐습니다.
해당 API의 응답 데이터에서 도착까지 얼마나 남았는지를 알 수 있는 정보는 아래의 도착 메세지가 유일했습니다.
이 데이터는 문자열 값으로 제공되었고, 이 문자열을 가공해서 남은 시간을 추출해야 했습니다.
"arrmsg1": "11분54초후[4번째 전]", // 첫번째 도착예정버스의 도착정보메세지
"arrmsg2": "출발대기", // 두번째 도착예정버스의 도착정보메세지
처음에는 단순하게 "00분00초후"형식의 메세지에서남은 시간을 Int 형식의 값으로 뽑아오면 되겠지 싶었습니다.
하지만 API를 여러번 호출해보며 확인한 결과, 이 도착 메세지 형식의 종류는 크게 4가지로 나뉘어진다는 것을 알게 되었습니다.
- "곧 도착"
- "00분00초후[0번째 전]"
- "운행종료"
- "출발대기"
그렇기 때문에 도착 정보로 남은 시간의 Int값만 사용할 경우
"곧 도착" 형식의 응답은 정확하게 남은 시간을 카운트할 수 없었습니다. 도착까지 1분 미만의 시간이 남았을 때 "곧 도착" 메세지가 표시되는 것으로 보여졌기 때문에 임의로 60초가 남았다고 할 수도 없었고, 그냥 곧 도착한다는 메세지를 보여주는 것이 제일 좋아보였습니다.
또한 "운행종료"와 "출발대기" 형식으로 응답이 왔을 때에 대한 처리도 할 수 없었습니다.
그렇기 때문에 도착까지 남은 시간만 활용하기에는 무리가 있었고, 도착 정보를 표현하기 위한 타입을 아래와 같이 새로 정의하게 되었습니다.
남은시간을 숫자로 계산할 수 있는 경우에는 숫자값을 활용하지만, 그렇지 않은 경우에는 별도의 처리를 할 수 있도록 열거형 타입을 정의했습니다.
enum ArrivalStatus: Comparable {
case arriveSoon // 곧 도착
case coming(remainingSecond: Int) // 운행중(남은시간(초))
case waiting // 출발대기
case finished // 운행종료
case unknown // 알 수 없음
}
이 타입을 보시면 열거형 타입이 Comparable 프로토콜을 채택한 것을 알 수 있습니다.
Comparable 프로토콜
Comparable 프로토콜은 이름대로 대소비교를 할 수 있도록 만들어주는 프로토콜입니다.
해당 프로토콜을 채택하게 되면 커스텀 타입에서도 <, >, <=, >= 연산자를 통한 대소비교가 가능해집니다.
Swift의 기본 타입인 Int, Double, String 등도 이 프로토콜을 채택하고 있었기 때문에 대소비교가 가능한 것입니다.
구조체나 클래스 같은 복합적인 타입에서는 비교의 기준을 무엇으로 할 지 명확하지 않기 때문에, Comparable 프로토콜 채택 시에 프로토콜 메서드를 구현해서 기준을 정해줘야 합니다.
프로토콜 채택만 하게 되면 에러가 발생합니다.
에러 메세지를 클릭해서 fix를 누르면 < 메서드를 작성하도록 만들어줍니다.
이 메서드의 내부에 비교 기준을 작성해주면 됩니다. < 메서드 하나만 작성해주면 이를 기준으로 나머지 연산자를 통한 비교도 모두 자동으로 처리됩니다.
짬을 더 많이 먹은 군인이 더 큰 값이 되도록 아래와 같이 구현해주면... 대소비교가 가능해집니다.
struct Soldier: Comparable {
static func < (lhs: Soldier, rhs: Soldier) -> Bool {
return lhs.zzam < rhs.zzam
}
var name = ""
var zzam = 0
}
let eren = Soldier(name: "에렌예거", zzam: 10)
let levi = Soldier(name: "리바이", zzam: 500)
eren < levi // true
eren > levi // false
📌 열거형의 경우
하지만 열거형의 경우는 조금 다릅니다.
Swift 5.3 미만 버전이라면 Comparable을 채택하고 구현도 해줘야 하지만,
Swift 5.3 이상의 버전에서는 연관값이 없거나 | 연관값이 모두 Comparable 프로토콜을 채택하고 있는 경우에는 Comparable을 채택하는 것만으로도 사용이 가능해집니다.
그렇기 때문에 위에서 제가 정의한 열거형 타입은 따로 구현을 해줄 필요가 없었습니다. 연관값으로 사용되는 건 Int값 하나 뿐이었는데, 이는 이미 프로토콜이 채택되어있기 때문이죠.
enum ArrivalStatus: Comparable {
case arriveSoon // 곧 도착
case coming(remainingSecond: Int) // 운행중(남은시간(초))
case waiting // 출발대기
case finished // 운행종료
case unknown // 알 수 없음
}
👉 그럼 대소비교의 기준은 어떻게 되냐?
별다른 구현이 없다면 열거형 case의 비교 기준은 아래로 갈수록 큰 값이 됩니다.
그렇기 때문에 ArrivalStatus 타입에서는 아래와 같은 대소관계가 성립하게 됩니다.
let a: ArrivalStatus = .arriveSoon // 곧 도착
let b: ArrivalStatus = .coming(remainingSecond: 10) // 10초 남음
let c: ArrivalStatus = .coming(remainingSecond: 16) // 16초 남음
let d: ArrivalStatus = .waiting // 출발대기
let e: ArrivalStatus = .finished // 운행종료
let f: ArrivalStatus = .unknown // 알 수 없음
// a < b < c < d < e < f
print(a<b) // true
print(a<c) // true
print(a<d) // true
print(a<e) // true
print(a<f) // true
print(b<c) // true
print(b<d) // true
print(b<e) // true
print(b<f) // true
print(c<d) // true
print(c<e) // true
print(c<f) // true
print(d<e) // true
print(d<f) // true
print(e<f) // true
✅ 직접 활용한 방식
위와 같은 대소관계가 성립하기 때문에 여러 개의 도착 정보가 배열에 들어있을 때,
- 정렬을 통해 가장 도착이 임박한 도착 정보를 맨 앞으로 가져올 수 있게 됩니다.
- 또한 현재 오고 있는 버스나 지하철이 없더라도, 출발대기>운행종료>알수없음 의 우선순위대로 적절한 메세지로 대체할 수 있게 됩니다.
버스의 도착 정보를 받기 위해 사용하는 API는 정류소 정보를 조회하는 API입니다.
그렇기 때문에 해당 정류소를 지나는 모든 노선에 대한 정보가 받아지게 됩니다.
버스의 경우 동일한 경로를 통하는 노선이 여러개일 수가 있기 때문에, 여러 노선의 도착 정보 중 도착이 가장 임박한 정보를 보여줄 수 있어야 했습니다.
ex) "불광역3.6호선" ~ "서대문역사거리.농협중앙회"의 동일한 경로를 지나는 버스노선은 총 3개 👉 720, 741, 705
그렇기 때문에 "불광역3.6호선" 정류소를 지나는 모든 노선의 도착 정보에서 720, 741, 705의 것들만 필터링하고 3가지 노선 중 가장 도착이 임박한 정보를 보여줘야 한다.
전체 코드를 상세하게 설명하기에는 복잡하기 때문에 이 부분이 적용되는 코드를 살펴보면
// 서울시 정류소정보조회 API 호출
self.apiService.fetchSeoulRealtimeBusStationInfo(arsID: arsID) { result in
switch result {
case .success(let seoulRealtimeBusStationDTO):
// 노선ID+노선명이 매칭되는 도착 정보들만 필터링
let filteredArrivals = seoulRealtimeBusStationDTO.arrivals.itemList.filter {
(routeIDs.contains($0.busRouteID)) && (routeNames.contains($0.busRouteName))
}
// 각 노선들에 대한 도착 정보에서 첫번째, 두번째 도착 메시지를 추출하고, ArrivalStatus의 우선순위가 높은(작은값) 순서대로 정렬
let firstArrivals = filteredArrivals.map { self.getBusArrivalStatusFromSeoulBusStation(arrivalMessage: $0.arrivalMessage1) }.sorted(by: <)
let secondArrivals = filteredArrivals.map { self.getBusArrivalStatusFromSeoulBusStation(arrivalMessage: $0.arrivalMessage2) }.sorted(by: <)
// 가장 가까운 도착 정보를 튜플에 반영
if let closestFirstArrival = firstArrivals.first {
realtimeArrival.first = closestFirstArrival
}
if let closestSecondArrival = secondArrivals.first {
realtimeArrival.second = closestSecondArrival
}
...
...
}
}
- 정류소정보조회 API를 호출해서 응답을 받아옵니다.
- 필요한 노선들의 정보만 필터링하고, 응답 메세지를 ArrivalStatus 타입의 값으로 변환하여 이를 배열로 만들어줍니다.
- sort 메서드로 정렬을 해주게 되면, firstArrivals와 secondArrivals 배열의 맨 앞(first)에 해당하는 원소에는 가장 도착이 임박한 데이터가 오게 되고, 이를 활용해줍니다.
레퍼런스
https://babbab2.tistory.com/150
'iOS' 카테고리의 다른 글
[ iOS ] Keychain 사용해보기 (0) | 2024.06.07 |
---|---|
[ iOS ] String에서 숫자 찾기 & 패턴 찾기 (2) | 2024.05.30 |
[iOS] SwiftUI의 TextField에 Clear Button 추가하기 (0) | 2024.05.28 |
[iOS] Build input file cannot be found: '~/Info.plist' 에러 발생 시 (0) | 2024.05.05 |
[iOS] Core Location 테스트하기 (외부 의존성의 응답을 테스트하기) (0) | 2024.05.03 |