boostcampwm-2021 / ios09-bbus Goto Github PK
View Code? Open in Web Editor NEW실시간 버스 정보, 승하차 알람 기능을 제공하는 '카카오버스' 앱 클론 프로젝트 🚌 BoostBus 📲
실시간 버스 정보, 승하차 알람 기능을 제공하는 '카카오버스' 앱 클론 프로젝트 🚌 BoostBus 📲
커스텀 AlarmSettingButton 코드 참고
현재 Service에서 처리해야 할 에러는 다음과 같다.
그렇다면 Service 단의 get함수는
DataTaskPublisher가 반환하는 URLSession.URLError를 포함한 총 3가지 에러를 처리해야 하는 것이다.
URLSession.URLError만 처리하는 것이 아니기에 3가지 에러를 모두 전송하는 하나의 Publisher 타입이 필요하다.
func get(url: String, params: [String: String]) -> AnyPublisher<Data, Error> {
let bbusPublisher = PassthroughSubject<Data, Error>()
DispatchQueue.global().async {
// 여러 에러 혹은 결과값 전송 로직
}
return bbusPublisher.eraseToAnyPublisher()
}
그 로직을 위와 같이 생성해주었다.
그렇다면 여러 에러 혹은 결과값 전송 로직
위치에 AccessKey 접근 및 URL 생성, Request 요청 등이 이루어져야 한다. 내부 로직이 모두 비동기로 돌아간다는 것이다.
DataTaskPublisher를 사용하게되면 DataTaskPublisher에 바인딩을 해 DataTaskPublisher로 온 값을 위의 생성한 bbusPublisher
로 보내주는 로직이 필요하다.
처음 생성했던 본 로직은 다음과 같다.
// 생략
URLSession.shared.dataTaskPublisher(for: request)
.sink(receiveCompletion: { error in
bbusPublisher.send(completion: .failure(error))
}, receiveValue: { data in
bbusPublisher.send(data)
})
.store(in: &self.cancellables) // sink 함수의 리턴값이 무조건 어딘가에 저장되어 있어야 한다.
// 생략
문제점은 저 store
이다. sink의 리턴값이 get 요청을 할 때마다 Service 클래스에 지속해서 쌓이는 구조인 것이다. 호출이 끝난 경우 삭제를 해보려 했으나, 반환 값은 sink할 때는 모르기 때문에 해당 로직을 넣을 수 없었다. 코드로 보자.
URLSession.shared.dataTaskPublisher(for: request)
.sink(receiveCompletion: { error in
bbusPublisher.send(completion: .failure(error))
self.cancellables.remove(??sink의 반환 값??)
}, receiveValue: { data in
bbusPublisher.send(data)
self.cancellables.remove(??sink의 반환 값??)
})
.store(in: &self.cancellables)
sink의 파라미터에 sink의 반환 값을 넣자니 말도 안되는 것..
그래서 결론적으로 DataTaskPublisher를 사용하지 않는 구조를 선택했다.
그 코드는 다음과 같다
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
bbusPublisher.send?.send(completion: .failure(error))
return
}
guard let response = response as? HTTPURLResponse else {
bbusPublisher.send?.send(completion: .failure(NetworkError.noResponseError))
return
}
if response.statusCode != 200 {
bbusPublisher.send?.send(completion: .failure(NetworkError.responseError))
return
}
if let data = data {
bbusPublisher.send?.send(data)
}
else {
publisher?.send(completion: .failure(NetworkError.noDataError))
}
}.resume()
오히려 더 깔끔하다고 생각한다.
애플이 잘 만들어 놓은 DataTaskPublisher를 쓰지 못하는건 아쉽지만.. Publisher안의 Publisher보단 이게 더 좋은 구조라는 생각이 든다!
이 작업 이후에 논의된 결론이지만, 어차피 반환 값을 AnyPublisher<Data, Error>
로 변경해야 했으니 필연적인 선택이기도 했다.
최대한 자세히 설명했는데 이해에 도움이 되었으면.. !
없음.
없음.
없음.
없음.
정거장 종류 사이에는 순서가 정해져 있다.
현재는 즐겨찾기에 추가된 순서대로 정렬되어 보여져야 한다.
현재 즐겨찾기 DTO 는 다음과 같이 되어있다.
struct FavoriteItem: Codable, Equatable {
let stId: String
let busRouteId: String
let ord: String
let arsId: String
}
위 DTO는 하나의 정거장 + 버스 즐겨찾기를 나타낸다.
이것만 가지고는 표시될 정거장의 순서를 저장할 수 없다. 또한, 추후 이 순서를 바꿔야 하는 요구사항이 들어오게 될 경우 변경에 용이하지 않다.
따라서 위 DTO를 개선해야할 필요성을 느낌
현재는 즐겨찾기 하나하나가 독립된 객체라고 한다면, 변경되어야 하는 구조는 다음과 같다.
정거장 1
ㄴ 버스1
ㄴ 버스2
정거장 2
ㄴ 버스1
ㄴ 버스 2
따라서 psudo code를 작성해보면
struct FavoriteStation: Codable {
let order: Int
let stationId: String
let arsId: String
let buses: [FavoriteBusRoute]
}
struct FavoriteBusRoute: Codable, Equatable {
let busRouteId: String
let ord: String
}
이러한 구조가 되어야 할 것이다.
이러면 Persistent의 저장, 생성, 삭제 로직도 바꾸어야 한다.
일단 바꾸어서 구현해보고 팀원의 의견을 들어보도록 하자.
음.. 이렇게 될경우 Persistent의 create로직이 대폭 수정되어야 할 뿐만 아니라, 객체의 내부까지 접근해야하는 구체타입을 지정해줘야하는 문제가 발생한다.
이렇게 하지말고, 순서를 저장하는 객체를 독립적으로 둔다면? 기존 코드에서 추가로 가능할 것 같다.
가령
struct FavoriteOrder: Codable {
let orderByStationId: [String:Int]
}
이런식으로 선언해놓고 순서를 저장해두면 가능하지 않을까
NONO 지금 구조를 살리려면 아래같이 해야함
struct FavoriteOrder: Codable, Equatable {
static func == (lhs: FavoriteOrder, rhs: FavoriteOrder) {
return lhs.stationId == rhs.stationId
}
let stationId: String
let order: Int
}
없음.
없음.
없음.
없음.
없음.
TableView에서 스크롤해도 헤더 고정되는 기능 제거하기
AlarmButtonDelegate가 세 종류여야 함. -> 네이밍을 좀 더 구체적으로 할 필요가 있음.
shouldGoToAlarmSettingScene()
이고shouldSettingGetOnAlarm(?)
shouldGoToMovingStatusScene()
TableView SectionHeader inset 문제
TableView 스크롤 시 섹션 헤더가 상단에 고정되는 문제
UIButton을 항상 둥근 모양으로 유지해야하는 이슈
StackView에 Padding 적용 이슈
isLayoutMarginsRelativeArrangement = true
해주어야 패딩이 제대로 적용됨.
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3)
ImaveView 크기와 별개로 image의 크기를 작게 넢고 싶은 경우
let image = UIImage().withAlignmentRectInsets(UIEdgeInsets())
StationBodyCollectionViewCell
는 FavoriteCollectionViewCell
를 상속받는다.
이 때, height
같은 경우 static
하기 때문에 상속하여 override
할 수 없다.
한참을 찾아보다가 다음과 같은 해답을 얻게 되었다.
class 접근자
를 사용하면 된다.
FavoriteCollectionViewCell.swift
class var height: CGFloat { return 70 }
StationBodyCollectionViewCell.swift
override class var height: CGFloat { return 90 }
static
변수를 class
변수로 바꾸기 (단, 연산 프로퍼티여야 한다.)없음.
없음.
없음.
없음.
window.rootViewController = navigationController
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator?
var movingStatusViewController: MovingStatusViewController? // Here
}
"자식"
viewController는 가능하겠지만,
ㄴ parentView & parentViewController
ㄴ firstChildView & firstChildViewController
ㄴ secondChildView & secondChildViewController
ㄴ view & firstViewController & secondViewController
window.viewController
가 아니라 window.rootViewController
라고 해서 다른 뷰컨도 존재하는 것 같은 느낌을 줬을까?)현재 자신이 embed되어야 하는 navigationController
를 알아야하므로 Coordinator 내의 presenter
라는 이름의 프로퍼티에 현재 자신이 embed되어야하는 navigationController
를 저장해놓고, presenter.push()
함.앱 실행 -> 홈 화면 -> (홈 화면에서 알람버튼 터치) -> 알람 설정 화면 -> (하차 알람 버튼 터치) -> 이동 현황 화면
과 같은 시나리오가 있다면, 현재 구조에서는 계층이 아래와 같이 형성될 것임.
ㄴ AppCoordinator
ㄴ HomeCoordinator
ㄴ AlarmSettingCoordinator
ㄴ MovingStatusCoordinator
알람설정화면
에서 BackButton을 눌러 pop을 하면, AlarmSettinCoordinator
가 사라지므로 자식인 MovingStatusCoordinator
까지 사라져서 이동현황화면을 끌 수가 없는 문제 발생. (CooridnatorFinishDelegate가 weak이므로)AlarmSetting
(뿐만 아니라 AppCoordinator와 HomeCoordinator를 제외한 모든 Coordinator)의 자식으로 들어가면 안된다.window.addSubview(MovingStatusViewController.view)
를 해야함.class AppCoordinator: NSObject, Coordinator {
private let window: UIWindow // 윈도우
... 생략 ...
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appCoordinator: AppCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: windowScene)
guard let window = self.window else { return }
self.appCoordinator = AppCoordinator(window: window) // 윈도우 주입
appCoordinator?.start()
}
... 생략 ...
}
UIApplication.shared.windows.first
로 window 접근이 가능하긴 함. (현재 이 방법을 써서 window에 추가하도록 구현되어있음.)
guard let window = UIApplication.shared.windows.first else { return }
let movingStatusViewController = MovingStatusViewController()
window.addSubview(movingStatusViewController.view)
UIApplication.shared.windows.first
으로 window에 직접 접근 하는 것은 옳지 않은 방법이라고 생각됨.
- window와 MovingStatusViewController를 연관짓는 구조로의 개선이 필요하다.
- MovingStatus 화면은 항상 최상위에 띄워지므로 다른 화면의 push/pop에 종속적이면 안된다. 따라서 MovingStatusCoordinator가
AlarmSetting
(뿐만 아니라 AppCoordinator와 HomeCoordinator를 제외한 모든 Coordinator)의 자식으로 들어가면 안된다.
window.rootViewController = rootViewController
가 되는 구조.
ㄴ window & rootViewController -- AppCoordinator
ㄴ view & NavigationController
ㄴ view & MovingStatusViewController
- window를 주입받아 사용하는 구조로의 개선이 필요하다.
없음.
위에 보이는 Badge를 기존에는 BusCellTrailingView
에서 직접 생성하고 다뤘음
하지만 AttributedText 등을 적용하고 나니, padding이 제대로 적용되지 않는 현상 발견
그래서 그것을 해결하기 위해 constraint로 label의 width와 height을 직접 지정해 주었으나
10번째전 혼잡
-> 1번째전
이런식으로 Label의 크기가 런타임 시점에 바뀌는 경우가 대응이 안됨...
따라서 Padding을 가지고 있는 CustomView로 그것을 대체하기로 결정
없음.
없음.
#15 의 피드백을 반영하였음
SearchCoordinator
에서 NSObject, Coordinator 제거, self 붙이기translatesAutoresizingMaskIntoConstraints
과 addSubview
순서terminate()
사용KeyboardAccessoryView.swift
SearchViewController
에서 delegate configure 관련 코드 정리
KeyboardAccessoryView
, SearchNavigationView
매직 넘버 처리
#4 와 통합되었음.
없음.
- window와 MovingStatusViewController를 연관짓는 구조로의 개선이 필요하다.
- MovingStatus 화면은 항상 최상위에 띄워지므로 다른 화면의 push/pop에 종속적이면 안된다. 따라서 MovingStatusCoordinator가
AlarmSetting
(뿐만 아니라 AppCoordinator와 HomeCoordinator를 제외한 모든 Coordinator)의 자식으로 들어가면 안된다.
window.rootViewController = rootViewController
가 되는 구조.
ㄴ window & rootViewController -- AppCoordinator
ㄴ view & NavigationController
ㄴ view & MovingStatusViewController
- window를 주입받아 사용하는 구조로의 개선이 필요하다.
없음.
없음.
없음.
없음.
테스트 이슈1
없음.
없음.
없음.
없음.
없음.
없음.
없음.
Persistent에서 초기 모든 버스 정보 데이터를 받아오는 시점과,
받아왔다고 생각하고 버스 번호로 검색하는 시점이 다를 수 있다.
private func startSearch() {
self.cancellable = usecases.getRouteList()
.decode(type: [BusRouteDTO].self, decoder: JSONDecoder())
.sink(receiveCompletion: { error in
// 에러 처리
if case .failure(let error) = error {
print(error)
print("error")
}
}, receiveValue: { routeList in
self.routeList = routeList
})
}
self.cancellable = self.usecase?.$routeList
.sink(receiveValue: { did in
dump(self.usecase?.searchBus(by: "46"))
})
호출 순서는 1 -> 2로 보장이 된다.
하지만 1에서 저장했다고 생각한 self.routeList = routeList
이 시점보다 더 빨리
2번의 self.usecase?.searchBus(by: "46")
가 실행 될 수 있다.
이 경우에 self.routeList가 nil이므로, 데이터를 제대로 못받아오는 현상이 발생한다.
그래서 다음와 같이 해결하였다.
.receive(on: Self.thread)
combine의 sink 하는 과정에서 Thread를 일치시켜줘야 일의 순서가 보장된다는 것을 확인하였다.
자세한 코드는 635e734
커밋에 포함되어 있다.
몇주전에 개인적으로 정리해뒀던 노션 주소입니다..! 참고가 되었으면...
없음.
private lazy var firstBusTimeLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 14)
label.text = "1분 29초"
return label
}()
view.configure(firstBusTime: "1분 29초")
coordinator.terminate() 를 두 번 실행한 결과이다.
그럼에도 Viewcontroller와 각 하위 View들이 메모리에서 상주되어있다.
원인은 아직 찾지 못함
두 가지 assign 메소드
assign(to🔛) 하면 강한참조가 일어나는 이슈
assign(to🔛) 하면 강한참조가 일어나는 이슈2
assign(to:)를 사용하면 강한참조가 일어나지 않음
현재 Service에서 처리해야 할 에러는 다음과 같다.
하지만 DataTaskPublisher
는 (data: Data, response: URLResponse)
혹은 URLSession.URLError
만을 반환한다.
Service
의 get
함수는 DataTaskPublisher
가 반환하는 URLSession.URLError
를 포함한 총 3가지 에러를 처리해야 하기 때문에, 이를 포괄할 수 있는 Publisher
하나가 필요하다.
func get(url: String, params: [String: String]) -> AnyPublisher<Data, Error> {
let bbusPublisher = PassthroughSubject<Data, Error>() // 포괄하는 Publisher
DispatchQueue.global().async {
// 여러 에러 혹은 결과값 전송 로직
}
return bbusPublisher.eraseToAnyPublisher()
}
그 로직을 위와 같이 생성해주었다. 그렇다면 여러 에러 혹은 결과값 전송 로직
위치에 AccessKey 접근 및 URL 생성, Request 요청 등이 이루어져야 한다.
DataTaskPublisher
를 사용하게되면 DataTaskPublisher
에 바인딩해 DataTaskPublisher
로 온 값을 위의 생성한 bbusPublisher
로 보내주는 로직이 필요하다.
처음 생성했던 해당 로직은 다음과 같다.
// URL 생성 관련 에러 요청 로직 생략
URLSession.shared.dataTaskPublisher(for: request)
.sink(receiveCompletion: { error in
bbusPublisher.send(completion: .failure(error))
}, receiveValue: { data in
bbusPublisher.send(data)
})
.store(in: &self.cancellables)
// sink 함수의 리턴값이 무조건 어딘가에 저장되어 있어야 한다. store는 해당 반환 값을 저장해주는 함수이다
// 현재 그 어딘가는 Service.shared.cancellables이다.
문제점은 저 store
이다. get 요청을 할 때마다 Service
클래스에 sink의 리턴값이 계속해서 쌓이는 구조인 것이다.
호출이 끝난 경우 삭제해보려 했으나, 반환 값은 sink
할 때는 모르기 때문에 해당 로직을 넣을 수 없었다.
말이 요상하니 코드로 보자.
URLSession.shared.dataTaskPublisher(for: request)
.sink(receiveCompletion: { error in
bbusPublisher.send(completion: .failure(error))
self.cancellables.remove(??sink의 반환 값??)
}, receiveValue: { data in
bbusPublisher.send(data)
self.cancellables.remove(??sink의 반환 값??)
})
.store(in: &self.cancellables)
sink
의 매개변수 내부에 sink
의 반환 값을 넣자니 말도 안되는 것..
밥을 먹지도 않았는데 설거지를 하는 것과 비슷한..
그래서 결론적으로 DataTaskPublisher
를 사용하지 않는 구조를 선택했다.
그 코드는 다음과 같다
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error { // URLError 에러 처리
bbusPublisher.send?.send(completion: .failure(error))
return
}
guard let response = response as? HTTPURLResponse else { // Response 없는 경우 처리
bbusPublisher.send?.send(completion: .failure(NetworkError.noResponseError))
return
}
if response.statusCode != 200 { // Response 의 상태코드가 200 이 아닌 경우 처리
bbusPublisher.send?.send(completion: .failure(NetworkError.responseError))
return
}
if let data = data { // data가 정상적으로 온 경우 전송
bbusPublisher.send?.send(data)
}
else { // data가 없는 경우 처리
publisher?.send(completion: .failure(NetworkError.noDataError))
}
}.resume()
위 로직이 오히려 더 깔끔하다고 생각한다.
애플이 잘 만들어 놓은 DataTaskPublisher
를 쓰지 못하는건 아쉽지만.. bbusPublisher
안의 DataTaskPublisher
보단 이게 더 좋은 구조라는 생각이 든다!
이 작업 이후에 논의된 결론이지만, 어차피 반환 값을 AnyPublisher<Data, Error>
로 변경해야 했으니 필연적인 선택이기도 했다.
최대한 자세히 설명했는데 이해에 도움이 되었으면.. !
없음.
없음.
없음.
21.11.11 검색 화면 모델 관련 데이터 수집 이후
검색 화면 진입
없음.
store(&cancellable)
쪽에서 동시접근 이슈로 인해 터지는 것으로 추측되며, 전체를 큐로 감싸주니 serial queue이기 때문에 터지지 않음. -> 이것도 확인 필요없음.
없음.
없음.
없음.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.