add a bracket calling view

fix lag when opening planning view with a lot of matches
paca_championship
Raz 1 year ago
parent 1d70070a68
commit 47198e9b88
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 152
      PadelClub/Views/Calling/BracketCallingView.swift
  3. 235
      PadelClub/Views/Planning/PlanningView.swift
  4. 22
      PadelClub/Views/Tournament/Screen/TournamentCallView.swift

@ -87,6 +87,9 @@
FF17CA4D2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; };
FF17CA4E2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; };
FF17CA4F2CB9243E003C7323 /* FollowUpMatchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */; };
FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; };
FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; };
FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF17CA522CBE4788003C7323 /* BracketCallingView.swift */; };
FF1CBC1B2BB53D1F0036DAAB /* FederalTournament.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */; };
FF1CBC1D2BB53DC10036DAAB /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */; };
FF1CBC1F2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */; };
@ -985,6 +988,7 @@
FF11628B2BD05267000C4809 /* LoserRoundStepScheduleEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoserRoundStepScheduleEditorView.swift; sourceTree = "<group>"; };
FF17CA482CB915A1003C7323 /* MultiCourtPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiCourtPickerView.swift; sourceTree = "<group>"; };
FF17CA4C2CB9243E003C7323 /* FollowUpMatchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUpMatchView.swift; sourceTree = "<group>"; };
FF17CA522CBE4788003C7323 /* BracketCallingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketCallingView.swift; sourceTree = "<group>"; };
FF1CBC182BB53D1F0036DAAB /* FederalTournament.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournament.swift; sourceTree = "<group>"; };
FF1CBC1C2BB53DC10036DAAB /* Calendar+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FederalTournamentSearchScope.swift; sourceTree = "<group>"; };
@ -1772,6 +1776,7 @@
FF9268082BCEDC2C0080F940 /* CallView.swift */,
FF1162792BCF8109000C4809 /* CallMessageCustomizationView.swift */,
FF2EFBEF2BDE295E0049CE3B /* SendToAllView.swift */,
FF17CA522CBE4788003C7323 /* BracketCallingView.swift */,
FFEF7F4C2BDE68F80033D0F0 /* Components */,
);
path = Calling;
@ -2426,6 +2431,7 @@
FF5DA18F2BB9268800A33061 /* GroupStagesSettingsView.swift in Sources */,
FF663FBE2BE019EC0031AE83 /* TournamentFilterView.swift in Sources */,
FF1F4B752BFA00FC000B4573 /* HtmlGenerator.swift in Sources */,
FF17CA532CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF8F26382BAD523300650388 /* PadelRule.swift in Sources */,
FF967CF42BAECC0B00A9A3BD /* TeamRegistration.swift in Sources */,
FFF8ACDB2B923F48008466FA /* Date+Extensions.swift in Sources */,
@ -2697,6 +2703,7 @@
FF4CC0022C996C0600151637 /* GroupStagesSettingsView.swift in Sources */,
FF4CC0032C996C0600151637 /* TournamentFilterView.swift in Sources */,
FF4CC0042C996C0600151637 /* HtmlGenerator.swift in Sources */,
FF17CA542CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF4CC0052C996C0600151637 /* PadelRule.swift in Sources */,
FF4CC0062C996C0600151637 /* TeamRegistration.swift in Sources */,
FF4CC0072C996C0600151637 /* Date+Extensions.swift in Sources */,
@ -2947,6 +2954,7 @@
FF70FB812C90584900129CC2 /* GroupStagesSettingsView.swift in Sources */,
FF70FB822C90584900129CC2 /* TournamentFilterView.swift in Sources */,
FF70FB832C90584900129CC2 /* HtmlGenerator.swift in Sources */,
FF17CA552CBE4788003C7323 /* BracketCallingView.swift in Sources */,
FF70FB842C90584900129CC2 /* PadelRule.swift in Sources */,
FF70FB852C90584900129CC2 /* TeamRegistration.swift in Sources */,
FF70FB862C90584900129CC2 /* Date+Extensions.swift in Sources */,

@ -0,0 +1,152 @@
//
// BracketCallingView.swift
// PadelClub
//
// Created by razmig on 15/10/2024.
//
import SwiftUI
struct BracketCallingView: View {
@Environment(Tournament.self) var tournament: Tournament
@State private var displayByMatch: Bool = true
@State private var initialSeedRound: Int = 0
@State private var initialSeedCount: Int = 0
let tournamentRounds: [Round]
let teams: [TeamRegistration]
init(tournament: Tournament) {
let rounds = tournament.rounds()
self.tournamentRounds = rounds
self.teams = tournament.availableSeeds()
let index = rounds.count - 1
_initialSeedRound = .init(wrappedValue: index)
_initialSeedCount = .init(wrappedValue: RoundRule.numberOfMatches(forRoundIndex: index))
}
var initialRound: Round {
tournamentRounds.first(where: { $0.index == initialSeedRound })!
}
func filteredRounds() -> [Round] {
tournamentRounds.filter({ $0.index >= initialSeedRound }).reversed()
}
func seedCount(forRoundIndex roundIndex: Int) -> Int {
if roundIndex < initialSeedRound { return 0 }
if roundIndex == initialSeedRound {
return initialSeedCount
}
let seedCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let previousSeedCount = self.seedCount(forRoundIndex: roundIndex - 1)
let total = seedCount - previousSeedCount
if total < 0 { return 0 }
return total
}
func seeds(forRoundIndex roundIndex: Int) -> [TeamRegistration] {
let previousSeeds: Int = (initialSeedRound..<roundIndex).map { seedCount(forRoundIndex: $0) }.reduce(0, +)
if roundIndex == tournamentRounds.count - 1 {
return Array(teams.dropFirst(previousSeeds))
} else {
return Array(teams.dropFirst(previousSeeds).prefix(seedCount(forRoundIndex: roundIndex)))
}
}
var body: some View {
List {
NavigationLink {
TeamsCallingView(teams: teams.filter({ $0.callDate == nil }))
.environment(tournament)
} label: {
LabeledContent("Équipes non contactées", value: teams.filter({ $0.callDate == nil }).count.formatted())
}
PlayersWithoutContactView(players: teams.flatMap({ $0.unsortedPlayers() }).sorted(by: \.computedRank))
Section {
Picker(selection: $initialSeedRound) {
ForEach(tournamentRounds) {
Text($0.roundTitle()).tag($0.index)
}
} label: {
Text("Premier tour")
}
.onChange(of: initialSeedRound) {
initialSeedCount = RoundRule.numberOfMatches(forRoundIndex: initialSeedRound)
}
LabeledContent {
StepperView(count: $initialSeedCount, minimum: 0, maximum: RoundRule.numberOfMatches(forRoundIndex: initialSeedRound))
} label: {
Text("Têtes de série")
}
}
ForEach(filteredRounds()) { round in
let seeds = seeds(forRoundIndex: round.index)
let callSeeds = seeds.filter({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
if seeds.isEmpty == false {
Section {
NavigationLink {
_roundView(round: round, seeds: seeds)
.environment(tournament)
} label: {
CallView.CallStatusView(count: callSeeds.count, total: seeds.count, startDate: round.playedMatches().first?.startDate)
}
} header: {
Text(round.roundTitle())
} footer: {
if let startDate = round.startDate ?? round.playedMatches().first?.startDate {
CallView(teams: seeds, callDate: startDate, matchFormat: round.matchFormat, roundLabel: round.roundTitle())
}
}
}
}
}
.headerProminence(.increased)
.navigationTitle("Prévision")
}
@ViewBuilder
private func _roundView(round: Round, seeds: [TeamRegistration]) -> some View {
List {
NavigationLink("Équipes non contactées") {
TeamsCallingView(teams: seeds.filter({ $0.callDate == nil }))
}
Section {
ForEach(seeds) { team in
CallView.TeamView(team: team)
}
} header: {
Text(round.roundTitle())
}
}
.overlay {
if seeds.isEmpty {
ContentUnavailableView {
Label("Aucune équipe dans ce tour", systemImage: "clock.badge.questionmark")
} description: {
Text("Padel Club n'a pas réussi à déterminer quelles équipes jouent ce tour.")
} actions: {
// RowButtonView("Horaire intelligent") {
// selectedScheduleDestination = nil
// }
}
}
}
.headerProminence(.increased)
.navigationTitle(round.roundTitle())
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
}
}
//#Preview {
// SeedsCallingView()
//}

@ -30,11 +30,11 @@ struct PlanningView: View {
Dictionary(grouping: matches) { $0.startDate ?? .distantFuture }
}
var days: [Date] {
func days(timeSlots: [Date:[Match]]) -> [Date] {
Set(timeSlots.keys.map { $0.startOfDay }).sorted()
}
var keys: [Date] {
func keys(timeSlots: [Date:[Match]]) -> [Date] {
timeSlots.keys.sorted()
}
@ -54,7 +54,7 @@ struct PlanningView: View {
}
}
private func _computedTitle() -> String {
private func _computedTitle(days: [Date]) -> String {
if let selectedDay {
return selectedDay.formatted(.dateTime.day().weekday().month())
} else {
@ -67,143 +67,160 @@ struct PlanningView: View {
}
var body: some View {
List {
_bySlotView()
}
.navigationTitle(Text(_computedTitle()))
.toolbar(content: {
if days.count > 1 {
ToolbarTitleMenu {
Picker(selection: $selectedDay) {
Text("Tous les jours").tag(nil as Date?)
ForEach(days, id: \.self) { day in
Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?)
let timeSlots = self.timeSlots
let keys = self.keys(timeSlots: timeSlots)
let days = self.days(timeSlots: timeSlots)
let matches = matches
BySlotView(days: days, keys: keys, timeSlots: timeSlots, matches: matches, selectedDay: selectedDay, filterOption: filterOption, showFinishedMatches: showFinishedMatches)
.navigationTitle(Text(_computedTitle(days: days)))
.toolbar(content: {
if days.count > 1 {
ToolbarTitleMenu {
Picker(selection: $selectedDay) {
Text("Tous les jours").tag(nil as Date?)
ForEach(days, id: \.self) { day in
Text(day.formatted(.dateTime.day().weekday().month())).tag(day as Date?)
}
} label: {
Text("Jour")
}
} label: {
Text("Jour")
.pickerStyle(.automatic)
}
.pickerStyle(.automatic)
}
}
ToolbarItemGroup(placement: .topBarTrailing) {
Menu {
Picker(selection: $showFinishedMatches) {
Text("Afficher tous les matchs").tag(true)
Text("Masquer les matchs terminés").tag(false)
ToolbarItemGroup(placement: .topBarTrailing) {
Menu {
Picker(selection: $showFinishedMatches) {
Text("Afficher tous les matchs").tag(true)
Text("Masquer les matchs terminés").tag(false)
} label: {
Text("Option de filtrage")
}
.labelsHidden()
.pickerStyle(.inline)
} label: {
Text("Option de filtrage")
Label("Filtrer", systemImage: "clock.badge.checkmark")
.symbolVariant(showFinishedMatches ? .fill : .none)
}
.labelsHidden()
.pickerStyle(.inline)
} label: {
Label("Filtrer", systemImage: "clock.badge.checkmark")
.symbolVariant(showFinishedMatches ? .fill : .none)
}
Menu {
Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) {
Text($0.localizedPlanningLabel()).tag($0)
Menu {
Picker(selection: $filterOption) {
ForEach(PlanningFilterOption.allCases) {
Text($0.localizedPlanningLabel()).tag($0)
}
} label: {
Text("Option de triage")
}
.labelsHidden()
.pickerStyle(.inline)
} label: {
Text("Option de triage")
Label("Trier", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(filterOption == .byCourt ? .fill : .none)
}
.labelsHidden()
.pickerStyle(.inline)
} label: {
Label("Trier", systemImage: "line.3.horizontal.decrease.circle")
.symbolVariant(filterOption == .byCourt ? .fill : .none)
}
}
})
.overlay {
if matches.allSatisfy({ $0.startDate == nil }) {
ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: {
Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi")
} actions: {
RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil
}
})
.overlay {
if matches.allSatisfy({ $0.startDate == nil }) {
ContentUnavailableView {
Label("Aucun horaire défini", systemImage: "clock.badge.questionmark")
} description: {
Text("Vous n'avez pas encore défini d'horaire pour les différentes phases du tournoi")
} actions: {
RowButtonView("Horaire intelligent") {
selectedScheduleDestination = nil
}
}
}
}
}
}
@ViewBuilder
func _bySlotView() -> some View {
if matches.allSatisfy({ $0.startDate == nil }) == false {
ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in
Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
if let _matches = timeSlots[key] {
DisclosureGroup {
ForEach(_matches.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting)) { match in
NavigationLink {
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle)
} label: {
LabeledContent {
if let courtName = match.courtName() {
Text(courtName)
}
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle(.title))
} else if let round = match.roundObject {
Text(round.roundTitle())
struct BySlotView: View {
@Environment(Tournament.self) var tournament: Tournament
let days: [Date]
let keys: [Date]
let timeSlots: [Date:[Match]]
let matches: [Match]
let selectedDay: Date?
let filterOption: PlanningFilterOption
let showFinishedMatches: Bool
var body: some View {
List {
if matches.allSatisfy({ $0.startDate == nil }) == false {
ForEach(days.filter({ selectedDay == nil || selectedDay == $0 }), id: \.self) { day in
Section {
ForEach(keys.filter({ $0.dayInt == day.dayInt }), id: \.self) { key in
if let _matches = timeSlots[key]?.sorted(by: filterOption == .byDefault ? \.computedOrder : \.courtIndexForSorting) {
DisclosureGroup {
ForEach(_matches) { match in
NavigationLink {
MatchDetailView(match: match, matchViewStyle: .sectionedStandardStyle)
} label: {
LabeledContent {
if let courtName = match.courtName() {
Text(courtName)
}
} label: {
if let groupStage = match.groupStageObject {
Text(groupStage.groupStageTitle(.title))
} else if let round = match.roundObject {
Text(round.roundTitle())
}
Text(match.matchTitle())
}
}
Text(match.matchTitle())
}
} label: {
_timeSlotView(key: key, matches: _matches)
}
}
} label: {
_timeSlotView(key: key, matches: _matches)
}
} header: {
HStack {
Text(day.formatted(.dateTime.day().weekday().month()))
Spacer()
let count = _matchesCount(inDayInt: day.dayInt, timeSlots: timeSlots)
if showFinishedMatches {
Text(self._formattedMatchCount(count))
} else {
Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)")
}
}
}
}
} header: {
HStack {
Text(day.formatted(.dateTime.day().weekday().month()))
Spacer()
let count = _matchesCount(inDayInt: day.dayInt)
if showFinishedMatches {
Text(self._formattedMatchCount(count))
} else {
Text(self._formattedMatchCount(count) + " restant\(count.pluralSuffix)")
}
.headerProminence(.increased)
}
}
.headerProminence(.increased)
}
}
}
private func _matchesCount(inDayInt dayInt: Int) -> Int {
timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count
}
private func _matchesCount(inDayInt dayInt: Int, timeSlots: [Date:[Match]]) -> Int {
timeSlots.filter { $0.key.dayInt == dayInt }.flatMap({ $0.value }).count
}
private func _timeSlotView(key: Date, matches: [Match]) -> some View {
LabeledContent {
Text(self._formattedMatchCount(matches.count))
} label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold)
let names = matches.sorted(by: \.computedOrder)
.compactMap({ $0.roundTitle() })
.reduce(into: [String]()) { uniqueNames, name in
if !uniqueNames.contains(name) {
uniqueNames.append(name)
}
private func _timeSlotView(key: Date, matches: [Match]) -> some View {
LabeledContent {
Text(self._formattedMatchCount(matches.count))
} label: {
Text(key.formatted(date: .omitted, time: .shortened)).font(.title).fontWeight(.semibold)
if matches.count <= tournament.courtCount {
let names = matches.sorted(by: \.computedOrder)
.compactMap({ $0.roundTitle() })
.reduce(into: [String]()) { uniqueNames, name in
if !uniqueNames.contains(name) {
uniqueNames.append(name)
}
}
Text(names.joined(separator: ", "))
} else {
Text(matches.count.formatted().appending(" matchs"))
}
Text(names.joined(separator: ", "))
}
}
}
fileprivate func _formattedMatchCount(_ count: Int) -> String {
return "\(count.formatted()) match\(count.pluralSuffix)"
fileprivate func _formattedMatchCount(_ count: Int) -> String {
return "\(count.formatted()) match\(count.pluralSuffix)"
}
}
}
//#Preview {

@ -16,6 +16,7 @@ enum CallDestination: Identifiable, Selectable, Equatable {
case teams(Tournament)
case seeds(Tournament)
case groupStages(Tournament)
case brackets(Tournament)
var id: String {
switch self {
@ -25,6 +26,8 @@ enum CallDestination: Identifiable, Selectable, Equatable {
return "seed"
case .groupStages:
return "groupStage"
case .brackets:
return "bracket"
}
}
@ -36,6 +39,8 @@ enum CallDestination: Identifiable, Selectable, Equatable {
return "Têtes de série"
case .groupStages:
return "Poules"
case .brackets:
return "Tableau"
}
}
@ -47,6 +52,9 @@ enum CallDestination: Identifiable, Selectable, Equatable {
case .seeds(let tournament):
let allSeedCalled = tournament.seeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil })
return allSeedCalled.count
case .brackets(let tournament):
let availableSeeds = tournament.availableSeeds().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil })
return availableSeeds.count
case .groupStages(let tournament):
let allSeedCalled = tournament.groupStageTeams().filter({ tournament.isStartDateIsDifferentThanCallDate($0) || $0.callDate == nil })
return allSeedCalled.count
@ -65,6 +73,9 @@ enum CallDestination: Identifiable, Selectable, Equatable {
case .seeds(let tournament):
let allSeedCalled = tournament.seeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
return allSeedCalled ? .checkmark : nil
case .brackets(let tournament):
let availableSeeds = tournament.availableSeeds().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
return availableSeeds ? .checkmark : nil
case .groupStages(let tournament):
let allSeedCalled = tournament.groupStageTeams().allSatisfy({ tournament.isStartDateIsDifferentThanCallDate($0) == false })
return allSeedCalled ? .checkmark : nil
@ -83,16 +94,23 @@ struct TournamentCallView: View {
self.tournament = tournament
var destinations = [CallDestination]()
let groupStageTeams = tournament.groupStageTeams()
let seededTeams = tournament.seededTeams()
if groupStageTeams.isEmpty == false {
destinations.append(.groupStages(tournament))
self._selectedDestination = State(wrappedValue: .groupStages(tournament))
}
if tournament.seededTeams().isEmpty == false {
if seededTeams.isEmpty == false {
destinations.append(.seeds(tournament))
if groupStageTeams.isEmpty {
self._selectedDestination = State(wrappedValue: .seeds(tournament))
}
}
if tournament.availableSeeds().isEmpty == false {
destinations.append(.brackets(tournament))
if seededTeams.isEmpty {
self._selectedDestination = State(wrappedValue: .brackets(tournament))
}
}
destinations.append(.teams(tournament))
self.allDestinations = destinations
}
@ -109,6 +127,8 @@ struct TournamentCallView: View {
TeamsCallingView(teams: tournament.selectedSortedTeams())
case .groupStages:
GroupStageCallingView()
case .brackets:
BracketCallingView(tournament: tournament)
case .seeds:
SeedsCallingView()
}

Loading…
Cancel
Save