為了能讓 SwiftUI 的 View 可以 observer StockTradingRecordStore 的 property,要讓 StockTradingRecordStore conform Observable protocol。並把要被 observer 的 property,設定成 @Published。
// StockTradingRecordStore.swift
/// conform ObservableObject
class StockTradingRecordStore: ObservableObject {
/// 將 records 設定成 @Published
@Published var records: [StockTradingRecord] = [] {
使用原生 UI 快整的刻畫一個列表
// TradingRecordListView.swift
import SwiftUI
struct TradingRecordListView: View {
@StateObject var store: StockTradingRecordStore = .init()
var body: some View {
VStack {
Text("台股交易紀錄")
List(store.records) { record in
/// 將 record 呈現出來,未來可以寫一個 cell 型的 view 來裝載 record
HStack {
/// 股票名字和股票代號垂直排列,並對齊 leading
VStack(alignment: .leading) {
Text(record.stockName)
Text(record.stockID)
}
Spacer()
/// 交易金額
Text("\(record.tradingAmount)")
.padding(.trailing, 10)
/// 買賣方向,未來可能會改寫這一段
if record.tradingSide == .buy {
Text("買進")
} else {
Text("賣出")
}
}
}
}
}
}
不過在刻畫時, preview 沒有設定好對應的 object,就不會有資料。
在 Preview 下方加上 mock store 的 code
// TradingRecordListView.swift
struct TradingRecordListView_Previews: PreviewProvider {
static var previews: some View {
TradingRecordListView(store: store)
}
private static var store: StockTradingRecordStore = {
let store = StockTradingRecordStore()
store.add(getRecord())
store.add(getRecord())
return store
}()
private static func getRecord() -> StockTradingRecord {
StockTradingRecord(stockID: "0050", stockName: "元大50", tradingSide: .buy, tradingShares: 1000, tradingAmount: 120000, tradingDateStr: "2023-09-07")
}
}
這樣在右方,就可以看到對應的 UI,在 preview 的時候,我們加上了兩筆資料,所以 preview 有正確的顯示出來。
當你需要渲染一系列資料時,通常需要讓 Data Model conform Identifible,這邊將 Identifible 相關宣告加上去。
// SettlementModel.swift
struct StockTradingRecord: Identifiable {
let id: String = UUID().uuidString
let stockID: String
let stockName: String
let tradingSide: TradingSide
/// 成交股數
let tradingShares: Int
/// 成交金額
let tradingAmount: Int
/// 成交日期,格式為 yyyy-mm-dd
let tradingDateStr: String
}
然後,我們真正的 run 這個 app,畫面會是一片空白,什麼都沒有。因為我們沒有將增加紀錄的功能作上去。所以我們補一個右上方的 Add 新增按鈕,並再給一個空值時的提示畫面。
// EmptyRecordView.swift
struct EmptyRecordView: View {
var body: some View {
VStack {
Text("請按右上角按鈕\n新增股票交易紀錄")
.multilineTextAlignment(.center)
.font(.system(size: 30))
.padding(.vertical)
Image(systemName: "list.clipboard")
.font(.system(size: 200))
Spacer()
}
}
}
把這個 EmptyRecordView 組裝進紀錄 List,並加上「新增」的按鈕。這個新增的按鈕是用 sheet 的方式呈現,很像 UIKit 時代的 present UI 感。
// TradingRecordListView.swift
@State private var showingAddRecordSheet = false
var recordList: some View {
VStack {
Text("台股交易紀錄")
List(store.records) { record in
/// 將 record 呈現出來,未來可以寫一個 cell 型的 view 來裝載 record
HStack {
/// 股票名字和股票代號垂直排列,並對齊 leading
VStack(alignment: .leading) {
Text(record.stockName)
Text(record.stockID)
}
Spacer()
/// 交易金額
Text("\(record.tradingAmount)")
.padding(.trailing, 10)
/// 買賣方向,未來可能會改寫這一段
if record.tradingSide == .buy {
Text("買進")
} else {
Text("賣出")
}
}
}
}
}
var topContainer: some View {
HStack {
Spacer()
Button {
print("新增按鈕被點擊")
showingAddRecordSheet.toggle()
} label: {
Image(systemName: "plus.circle")
.font(.system(size: 37))
}
.padding()
.sheet(isPresented: $showingAddRecordSheet) {
StockTradingInputView()
}
}
}
var body: some View {
VStack {
topContainer
if store.records.isEmpty {
EmptyRecordView()
} else {
recordList
}
}
}
最後,再對 StockTradingInputView 進行修改,將 store 傳進去,並把 private 宣告改為 internal。即可。