
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 퍼블리셔 값을 계속해서 관찰하고 있기 때문에 해당 이벤트가 발생할 경우 모달 뷰가 프레젠트되기 때문
이렇게 잘 구조화된 코드를 보고 있으면 대단하다는 생각부터 든다... 자연스럽게 더 잘 이해하고, "내가" 더 잘 할 수 있기를!