Build Swift Charts Stocks App Part 3 - Ticker Symbol Sheet UI - SwiftUI iOS 16 App
struct StockTickerView: View {
@StateObject var quoteViewModel: TickerQuoteViewModel
@State private var selectedRange = ChartRange.oneDay
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 0) {
headerView.padding(.horizontal)
Divider()
.padding(.vertical, 8)
.padding(.horizontal)
scrollView
}
.padding(.top)
.background(Color(uiColor: .systemBackground))
.task {
await quoteViewModel.fetchQuotes()
}
}
}
TickerQuoteViewModel
을 통해 UI로 표현할 데이터를 받아들임task
메소드를 통해 뷰 모델이 가지고 있는 데이터 패칭func fetchQuotes() async {
phase = .fetching
do {
let response = try await stocksAPI.fetchQuotes(symbols: ticker.symbol)
if let quote = response.first {
phase = .success(quote)
} else {
phase = .empty
}
} catch {
print(error.localizedDescription)
phase = .failure(error)
}
}
async
함수이기 때문에 뷰 단의 task
내부에서 실행 가능phase
를 데이터 패치 중, 성공/실패, 빈 화면 유무 등 이넘으로 관리 중이며, 비동기적 흐름에 의해 변경되기 때문에 현재 상황을 UI로 곧바로 표현할 수 있음
struct MainListView: View {
@EnvironmentObject private var appViewModel: AppViewModel
@StateObject var quotesViewModel = QuotesViewModel()
@StateObject var searchViewModel = SearchViewModel()
var body: some View {
tickerListView
.listStyle(.plain)
.overlay { overlayView }
.toolbar {
titleToolbar
attributionToolbar
}
...
.sheet(item: $appViewModel.selectedTicker) {
StockTickerView(quoteViewModel: .init(ticker: $0, stocksAPI: quotesViewModel.stocksAPI))
.presentationDetents([.height(560)])
}
...
}
}
appViewModel
내부의 selectedTicker
퍼블리셔가 널 값이 아니라 값이 들어갈 때 자동으로 모달 프레젠트presentationDetents
를 통해 iOS 최신 버전부터 도입된 하프 모달을 통해 높이 조정@MainActor
struct SearchView: View {
@EnvironmentObject private var appViewModel: AppViewModel
@StateObject var quotesViewModel = QuotesViewModel()
@ObservedObject var searchViewModel: SearchViewModel
var body: some View {
List(searchViewModel.tickers) { ticker in
TickerListRowView(data: .init(symbol: ticker.symbol, name: ticker.shortname, price: quotesViewModel.priceForTicker(ticker), type: .search(isSaved: appViewModel.isAddedToMyTickers(ticker: ticker), onButtonDidTap: {
Task { @MainActor in
appViewModel.toggleTicker(ticker)
}
})))
.contentShape(Rectangle())
.onTapGesture {
Task { @MainActor in
appViewModel.selectedTicker = ticker
}
}
}
.background(Color(uiColor: .systemBackground))
.listStyle(.plain)
.refreshable {
await quotesViewModel.fetchQuotes(tickers: searchViewModel.tickers)
}
.task(id: searchViewModel.tickers, {
await quotesViewModel.fetchQuotes(tickers: searchViewModel.tickers)
})
.overlay {
listSearchOverlay
}
}
}
SearchView
는 메인 리스트 뷰에서 overlay
를 통해 나타나고 있기 때문에 별도의 sheet
메소드를 사용할 필요가 없음appViewModel
이 들고 있는 selectedTicker
퍼블리셔 값을 계속해서 관찰하고 있기 때문에 해당 이벤트가 발생할 경우 모달 뷰가 프레젠트되기 때문이렇게 잘 구조화된 코드를 보고 있으면 대단하다는 생각부터 든다... 자연스럽게 더 잘 이해하고, "내가" 더 잘 할 수 있기를!