Add CI/CD configuration and API documentation

This commit is contained in:
2026-07-01 21:40:53 +08:00
commit c590135d68
4168 changed files with 740252 additions and 0 deletions
@@ -0,0 +1,154 @@
import GRDBQuery
import SwiftUI
/// The main application view
struct AppView: View {
/// Write access to the database
@Environment(\.appDatabase) private var appDatabase
/// The `players` property is automatically updated when the database changes
@Query(PlayerRequest(ordering: .byScore)) private var players: [Player]
/// We'll need to leave edit mode in several occasions.
@State private var editMode = EditMode.inactive
/// Tracks the presentation of the player creation sheet.
@State private var newPlayerIsPresented = false
// If you want to define the query on initialization, you will prefer:
//
// @Query<PlayerRequest> private var players: [Player]
//
// init(initialOrdering: PlayerRequest.Ordering) {
// _players = Query(PlayerRequest(ordering: initialOrdering))
// }
var body: some View {
NavigationView {
PlayerList(players: players)
.navigationBarTitle(Text("\(players.count) Players"))
.navigationBarItems(
leading: HStack {
EditButton()
newPlayerButton
},
trailing: ToggleOrderingButton(
ordering: $players.ordering,
willChange: {
// onChange(of: $players.wrappedValue.ordering)
// is not able to leave the editing mode during
// the animation of the list content.
// Workaround: stop editing before the ordering
// is changed, and the list content is updated.
stopEditing()
}))
.toolbar { toolbarContent }
.onChange(of: players) {
if players.isEmpty {
stopEditing()
}
}
.environment(\.editMode, $editMode)
}
}
private var toolbarContent: some ToolbarContent {
ToolbarItemGroup(placement: .bottomBar) {
Button {
// Don't stopEditing() here because this is
// performed `onChange(of: players)`
Task {
try? await appDatabase.deleteAllPlayers()
}
} label: {
Image(systemName: "trash").imageScale(.large)
}
Spacer()
Button {
stopEditing()
Task {
try? await appDatabase.refreshPlayers()
}
} label: {
Image(systemName: "arrow.clockwise").imageScale(.large)
}
Spacer()
Button {
stopEditing()
// Perform 50 refreshes in parallel
Task {
try? await withThrowingTaskGroup(of: Void.self) { group in
for _ in 0..<50 {
_ = group.addTaskUnlessCancelled {
try await appDatabase.refreshPlayers()
}
}
try await group.waitForAll()
}
}
} label: {
Image(systemName: "tornado").imageScale(.large)
}
}
}
/// The button that presents the player creation sheet.
private var newPlayerButton: some View {
Button {
stopEditing()
newPlayerIsPresented = true
} label: {
Image(systemName: "plus")
}
.accessibility(label: Text("New Player"))
.sheet(isPresented: $newPlayerIsPresented) {
PlayerCreationView()
}
}
private func stopEditing() {
withAnimation {
editMode = .inactive
}
}
}
private struct ToggleOrderingButton: View {
@Binding var ordering: PlayerRequest.Ordering
let willChange: () -> Void
var body: some View {
switch ordering {
case .byName:
Button {
willChange()
ordering = .byScore
} label: {
Label("Name", systemImage: "arrowtriangle.up.fill").labelStyle(.titleAndIcon)
}
case .byScore:
Button {
willChange()
ordering = .byName
} label: {
Label("Score", systemImage: "arrowtriangle.down.fill").labelStyle(.titleAndIcon)
}
}
}
}
// MARK: - Previews
#Preview("Empty") {
// Preview the default, empty database
AppView()
}
#Preview("Populated") {
// Preview a database of random players
AppView().appDatabase(.random())
}
@@ -0,0 +1,50 @@
import SwiftUI
/// The view that creates a new player.
struct PlayerCreationView: View {
/// Write access to the database
@Environment(\.appDatabase) private var appDatabase
@Environment(\.dismiss) private var dismiss
@State private var form = PlayerForm(name: "", score: "")
@State private var errorAlertIsPresented = false
@State private var errorAlertTitle = ""
var body: some View {
NavigationView {
PlayerFormView(form: $form)
.alert(
isPresented: $errorAlertIsPresented,
content: { Alert(title: Text(errorAlertTitle)) })
.navigationBarTitle("New Player")
.navigationBarItems(
leading: Button(role: .cancel) {
dismiss()
} label: {
Text("Cancel")
},
trailing: Button {
Task { await save() }
} label: {
Text("Save")
})
}
}
private func save() async {
do {
var player = Player(id: nil, name: "", score: 0)
form.apply(to: &player)
try await appDatabase.savePlayer(&player)
dismiss()
} catch {
errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
errorAlertIsPresented = true
}
}
}
// MARK: - Previews
#Preview {
PlayerCreationView()
}
@@ -0,0 +1,40 @@
import SwiftUI
/// The view that edits an existing player.
struct PlayerEditionView: View {
/// Write access to the database
@Environment(\.appDatabase) private var appDatabase
@Environment(\.isPresented) private var isPresented
private let player: Player
@State private var form: PlayerForm
init(player: Player) {
self.player = player
self.form = PlayerForm(player)
}
var body: some View {
PlayerFormView(form: $form)
.onChange(of: isPresented) {
// Save when back button is pressed
if !isPresented {
Task {
var savedPlayer = player
form.apply(to: &savedPlayer)
// Ignore error because I don't know how to cancel the
// back button and present the error
try? await appDatabase.savePlayer(&savedPlayer)
}
}
}
}
}
// MARK: - Previews
#Preview {
NavigationView {
PlayerEditionView(player: Player.makeRandom())
.navigationBarTitle("Player Edition")
}
}
@@ -0,0 +1,48 @@
import SwiftUI
/// The Player editing form, embedded in both
/// `PlayerCreationView` and `PlayerEditionView`.
struct PlayerFormView: View {
@Binding var form: PlayerForm
var body: some View {
List {
TextField("Name", text: $form.name)
.accessibility(label: Text("Player Name"))
TextField("Score", text: $form.score).keyboardType(.numberPad)
.accessibility(label: Text("Player Score"))
}
.listStyle(InsetGroupedListStyle())
}
}
struct PlayerForm {
var name: String
var score: String
}
extension PlayerForm {
init(_ player: Player) {
self.name = player.name
self.score = "\(player.score)"
}
func apply(to player: inout Player) {
player.name = name
player.score = Int(score) ?? 0
}
}
// MARK: - Previews
#Preview("Empty") {
PlayerFormView(form: .constant(PlayerForm(
name: "",
score: "")))
}
#Preview("Prefilled") {
PlayerFormView(form: .constant(PlayerForm(
name: Player.randomName(),
score: "\(Player.randomScore())")))
}
@@ -0,0 +1,59 @@
import SwiftUI
struct PlayerList: View {
/// Write access to the database
@Environment(\.appDatabase) private var appDatabase
/// The players in the list
var players: [Player]
var body: some View {
List {
ForEach(players) { player in
NavigationLink(destination: editionView(for: player)) {
PlayerRow(player: player)
// Don't animate player update
.animation(nil, value: player)
}
}
.onDelete { offsets in
let playerIds = offsets.compactMap { players[$0].id }
Task {
try? await appDatabase.deletePlayers(ids: playerIds)
}
}
}
// Animate list updates
.animation(.default, value: players)
.listStyle(.plain)
}
/// The view that edits a player in the list.
private func editionView(for player: Player) -> some View {
PlayerEditionView(player: player).navigationBarTitle(player.name)
}
}
private struct PlayerRow: View {
var player: Player
var body: some View {
HStack {
Text(player.name)
Spacer()
Text("\(player.score) points").foregroundColor(.gray)
}
}
}
// MARK: - Previews
#Preview {
NavigationView {
PlayerList(players: [
Player(id: 1, name: "Arthur", score: 100),
Player(id: 2, name: "Barbara", score: 1000),
])
.navigationTitle("Preview")
}
}