Merge branch 'tiebreak'

paca_championship
Raz 12 months ago
commit 420b5e9bc2
  1. 8
      PadelClub.xcodeproj/project.pbxproj
  2. 12
      PadelClub/Data/AppSettings.swift
  3. 6
      PadelClub/Data/GroupStage.swift
  4. 6
      PadelClub/Data/Match.swift
  5. 38
      PadelClub/Data/MatchScheduler.swift
  6. 4
      PadelClub/ViewModel/MatchDescriptor.swift
  7. 10
      PadelClub/ViewModel/SearchViewModel.swift
  8. 19
      PadelClub/ViewModel/SetDescriptor.swift
  9. 2
      PadelClub/Views/Navigation/Toolbox/ToolboxView.swift
  10. 8
      PadelClub/Views/Planning/PlanningSettingsView.swift
  11. 34
      PadelClub/Views/Shared/SelectablePlayerListView.swift

@ -3278,7 +3278,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 3;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
@ -3302,7 +3302,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.32; MARKETING_VERSION = 1.0.33;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3323,7 +3323,7 @@
CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements; CODE_SIGN_ENTITLEMENTS = PadelClub/PadelClub.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 3;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"PadelClub/Preview Content\"";
DEVELOPMENT_TEAM = BQ3Y44M3Q6; DEVELOPMENT_TEAM = BQ3Y44M3Q6;
@ -3346,7 +3346,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.32; MARKETING_VERSION = 1.0.33;
PRODUCT_BUNDLE_IDENTIFIER = app.padelclub; PRODUCT_BUNDLE_IDENTIFIER = app.padelclub;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

@ -30,6 +30,18 @@ final class AppSettings: MicroStorable {
var dayDuration: Int? var dayDuration: Int?
var dayPeriod: DayPeriod var dayPeriod: DayPeriod
func lastDataSourceDate() -> Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
func localizedLastDataSource() -> String? {
guard let lastDataSource else { return nil }
guard let date = URL.importDateFormatter.date(from: lastDataSource) else { return nil }
return date.monthYearFormatted
}
func resetSearch() { func resetSearch() {
tournamentAges = Set() tournamentAges = Set()
tournamentTypes = Set() tournamentTypes = Set()

@ -203,6 +203,10 @@ final class GroupStage: ModelObject, Storable {
} }
} }
func orderedIndexOfMatch(_ match: Match) -> Int {
_matchOrder()[safe: match.index] ?? match.index
}
func updateGroupStageState() { func updateGroupStageState() {
clearScoreCache() clearScoreCache()
@ -368,7 +372,7 @@ final class GroupStage: ModelObject, Storable {
_matchOrder().firstIndex(of: matchIndex) ?? matchIndex _matchOrder().firstIndex(of: matchIndex) ?? matchIndex
} }
private func _matchUp(for matchIndex: Int) -> [Int] { func _matchUp(for matchIndex: Int) -> [Int] {
let combinations = Array((0..<size).combinations(ofCount: 2)) let combinations = Array((0..<size).combinations(ofCount: 2))
return combinations[safe: matchIndex%matchCount] ?? [] return combinations[safe: matchIndex%matchCount] ?? []
} }

@ -738,7 +738,7 @@ defer {
func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool { func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool {
let teams = teamScores let teams = teamScores
guard teams.count == 2 else { guard teams.count == 2 else {
print("teams.count != 2") //print("teams.count != 2")
return false return false
} }
guard hasEnded() == false else { return false } guard hasEnded() == false else { return false }
@ -832,8 +832,8 @@ defer {
if teamPosition == team(.two)?.groupStagePositionAtStep(step) { if teamPosition == team(.two)?.groupStagePositionAtStep(step) {
reverseValue = -1 reverseValue = -1
} }
let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())
let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut()) let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut())
var setDifference : Int = 0 var setDifference : Int = 0
let zip = zip(endedSetsOne, endedSetsTwo) let zip = zip(endedSetsOne, endedSetsTwo)
if matchFormat.setsToWin == 1 { if matchFormat.setsToWin == 1 {

@ -32,6 +32,7 @@ final class MatchScheduler : ModelObject, Storable {
var overrideCourtsUnavailability: Bool = false var overrideCourtsUnavailability: Bool = false
var shouldTryToFillUpCourtsAvailable: Bool = true var shouldTryToFillUpCourtsAvailable: Bool = true
var courtsAvailable: Set<Int> = Set<Int>() var courtsAvailable: Set<Int> = Set<Int>()
var simultaneousStart: Bool = true
init(tournament: String, init(tournament: String,
timeDifferenceLimit: Int = 5, timeDifferenceLimit: Int = 5,
@ -46,7 +47,8 @@ final class MatchScheduler : ModelObject, Storable {
groupStageChunkCount: Int? = nil, groupStageChunkCount: Int? = nil,
overrideCourtsUnavailability: Bool = false, overrideCourtsUnavailability: Bool = false,
shouldTryToFillUpCourtsAvailable: Bool = true, shouldTryToFillUpCourtsAvailable: Bool = true,
courtsAvailable: Set<Int> = Set<Int>()) { courtsAvailable: Set<Int> = Set<Int>(),
simultaneousStart: Bool = true) {
self.tournament = tournament self.tournament = tournament
self.timeDifferenceLimit = timeDifferenceLimit self.timeDifferenceLimit = timeDifferenceLimit
self.loserBracketRotationDifference = loserBracketRotationDifference self.loserBracketRotationDifference = loserBracketRotationDifference
@ -61,6 +63,7 @@ final class MatchScheduler : ModelObject, Storable {
self.overrideCourtsUnavailability = overrideCourtsUnavailability self.overrideCourtsUnavailability = overrideCourtsUnavailability
self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable self.shouldTryToFillUpCourtsAvailable = shouldTryToFillUpCourtsAvailable
self.courtsAvailable = courtsAvailable self.courtsAvailable = courtsAvailable
self.simultaneousStart = simultaneousStart
} }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
@ -79,6 +82,7 @@ final class MatchScheduler : ModelObject, Storable {
case _overrideCourtsUnavailability = "overrideCourtsUnavailability" case _overrideCourtsUnavailability = "overrideCourtsUnavailability"
case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable" case _shouldTryToFillUpCourtsAvailable = "shouldTryToFillUpCourtsAvailable"
case _courtsAvailable = "courtsAvailable" case _courtsAvailable = "courtsAvailable"
case _simultaneousStart = "simultaneousStart"
} }
var courtsUnavailability: [DateInterval]? { var courtsUnavailability: [DateInterval]? {
@ -185,15 +189,19 @@ final class MatchScheduler : ModelObject, Storable {
// Get the maximum count of matches in any group // Get the maximum count of matches in any group
let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0 let maxMatchesCount = _groupStages.map { $0._matches().count }.max() ?? 0
var flattenedMatches = [Match]()
if simultaneousStart {
// Flatten matches in a round-robin order by cycling through each group // Flatten matches in a round-robin order by cycling through each group
let flattenedMatches = (0..<maxMatchesCount).flatMap { index in flattenedMatches = (0..<maxMatchesCount).flatMap { index in
_groupStages.compactMap { group in _groupStages.compactMap { group in
// Safely access matches, return nil if index is out of bounds // Safely access matches, return nil if index is out of bounds
let playedMatches = group.playedMatches() let playedMatches = group.playedMatches()
return playedMatches.indices.contains(index) ? playedMatches[index] : nil return playedMatches.indices.contains(index) ? playedMatches[index] : nil
} }
} }
} else {
flattenedMatches = _groupStages.flatMap({ $0.playedMatches() })
}
var slots = [GroupStageTimeMatch]() var slots = [GroupStageTimeMatch]()
var availableMatches = flattenedMatches var availableMatches = flattenedMatches
@ -214,17 +222,21 @@ final class MatchScheduler : ModelObject, Storable {
let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +) let counts = Dictionary(previousRotationBracketIndexes, uniquingKeysWith: +)
var rotationMatches = Array(availableMatches.filter({ match in var rotationMatches = Array(availableMatches.filter({ match in
// Check if all teams from the match are not already scheduled in the current rotation // Check if all teams from the match are not already scheduled in the current rotation
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) }) let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
if !teamsAvailable { if !teamsAvailable {
print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)") print("Match \(match.roundAndMatchTitle()) has teams already scheduled in rotation \(rotationIndex)")
} }
return teamsAvailable return teamsAvailable
}).prefix(courtsAvailable.count)) }))
if rotationIndex > 0 { if rotationIndex > 0 {
rotationMatches = rotationMatches.sorted(by: { rotationMatches = rotationMatches.sorted(by: {
if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 { if counts[$0.groupStageObject!.index] ?? 0 == counts[$1.groupStageObject!.index] ?? 0 {
if simultaneousStart {
return $0.groupStageObject!.orderedIndexOfMatch($0) < $1.groupStageObject!.orderedIndexOfMatch($1)
} else {
return $0.groupStageObject!.index < $1.groupStageObject!.index return $0.groupStageObject!.index < $1.groupStageObject!.index
}
} else { } else {
return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0 return counts[$0.groupStageObject!.index] ?? 0 < counts[$1.groupStageObject!.index] ?? 0
} }
@ -245,7 +257,7 @@ final class MatchScheduler : ModelObject, Storable {
return false return false
} }
let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamId($0) }) let teamsAvailable = teamsPerRotation[rotationIndex]!.allSatisfy({ !match.containsTeamIndex($0) })
if !teamsAvailable { if !teamsAvailable {
print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation") print("Teams from match \(match.roundAndMatchTitle()) are already scheduled in this rotation")
return false return false
@ -259,7 +271,7 @@ final class MatchScheduler : ModelObject, Storable {
print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)") print("Scheduled match: \(first.roundAndMatchTitle()) on court \(courtIndex) at rotation \(rotationIndex)")
slots.append(timeMatch) slots.append(timeMatch)
teamsPerRotation[rotationIndex]!.append(contentsOf: first.teamIds()) teamsPerRotation[rotationIndex]!.append(contentsOf: first.matchUp())
rotationMatches.removeAll(where: { $0.id == first.id }) rotationMatches.removeAll(where: { $0.id == first.id })
availableMatches.removeAll(where: { $0.id == first.id }) availableMatches.removeAll(where: { $0.id == first.id })
@ -891,4 +903,16 @@ extension Match {
func containsTeamId(_ id: String) -> Bool { func containsTeamId(_ id: String) -> Bool {
return teamIds().contains(id) return teamIds().contains(id)
} }
func containsTeamIndex(_ id: String) -> Bool {
matchUp().contains(id)
}
func matchUp() -> [String] {
guard let groupStageObject else {
return []
}
return groupStageObject._matchUp(for: index).map { groupStageObject.id + "_\($0)" }
}
} }

@ -94,11 +94,11 @@ class MatchDescriptor: ObservableObject {
} }
var teamOneScores: [String] { var teamOneScores: [String] {
setDescriptors.compactMap { $0.valueTeamOne }.map { "\($0)" } setDescriptors.compactMap { $0.getValue(teamPosition: .one) }
} }
var teamTwoScores: [String] { var teamTwoScores: [String] {
setDescriptors.compactMap { $0.valueTeamTwo }.map { "\($0)" } setDescriptors.compactMap { $0.getValue(teamPosition: .two) }
} }
var scoreTeamOne: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .one }.count } var scoreTeamOne: Int { setDescriptors.compactMap { $0.winner }.filter { $0 == .one }.count }

@ -36,8 +36,7 @@ class SearchViewModel: ObservableObject, Identifiable {
@Published var filterSelectionEnabled: Bool = false @Published var filterSelectionEnabled: Bool = false
@Published var isPresented: Bool = false @Published var isPresented: Bool = false
@Published var selectedAgeCategory: FederalTournamentAge = .unlisted @Published var selectedAgeCategory: FederalTournamentAge = .unlisted
@Published var mostRecentDate: Date? = nil
var mostRecentDate: Date? = nil
var selectionIsOver: Bool { var selectionIsOver: Bool {
if allowSingleSelection && selectedPlayers.count == 1 { if allowSingleSelection && selectedPlayers.count == 1 {
@ -69,9 +68,6 @@ class SearchViewModel: ObservableObject, Identifiable {
var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."] var message = ["Vérifiez l'ortographe ou lancez une nouvelle recherche."]
if tokens.isEmpty { if tokens.isEmpty {
message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.") message.append("Il est possible que cette personne n'est joué aucun tournoi depuis les 12 derniers mois, dans ce cas, Padel Club ne pourra pas le trouver.")
if filterOption == .male {
message.append("Depuis août 2024, le classement fédérale disponible est limité aux 40.000 premiers joueurs. Si le joueur n'a pas encore assez de points pour être visible, Padel Club ne pourra pas non plus le trouver.")
}
} }
return message.joined(separator: "\n") return message.joined(separator: "\n")
} }
@ -231,7 +227,7 @@ class SearchViewModel: ObservableObject, Identifiable {
] ]
if let mostRecentDate { if let mostRecentDate {
//predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) predicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
} }
if hideAssimilation { if hideAssimilation {
@ -344,7 +340,7 @@ class SearchViewModel: ObservableObject, Identifiable {
} }
if let mostRecentDate { if let mostRecentDate {
//andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg)) andPredicates.append(NSPredicate(format: "importDate == %@", mostRecentDate as CVarArg))
} }
if nameComponents.count > 1 { if nameComponents.count > 1 {

@ -40,4 +40,23 @@ struct SetDescriptor: Identifiable, Equatable {
var shouldTieBreak: Bool { var shouldTieBreak: Bool {
setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0) setFormat.shouldTiebreak(scoreTeamOne: valueTeamOne ?? 0, scoreTeamTwo: valueTeamTwo ?? 0)
} }
func getValue(teamPosition: TeamPosition) -> String? {
switch teamPosition {
case .one:
if let valueTeamOne {
if let tieBreakValueTeamOne {
return "\(valueTeamOne)-\(tieBreakValueTeamOne)"
}
}
case .two:
if let valueTeamTwo {
if let tieBreakValueTeamTwo {
return "\(valueTeamTwo)-\(tieBreakValueTeamTwo)"
}
}
}
return nil
}
} }

@ -129,7 +129,7 @@ struct ToolboxView: View {
Section { Section {
NavigationLink { NavigationLink {
SelectablePlayerListView(isPresented: false) SelectablePlayerListView(isPresented: false, lastDataSource: true)
} label: { } label: {
Label("Rechercher un joueur", systemImage: "person.fill.viewfinder") Label("Rechercher un joueur", systemImage: "person.fill.viewfinder")
} }

@ -385,6 +385,14 @@ struct PlanningSettingsView: View {
} footer: { } footer: {
Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.") Text("Vous pouvez indiquer le nombre de poule démarrant en même temps.")
} }
Section {
Toggle(isOn: $matchScheduler.simultaneousStart) {
Text("Démarrage simultané")
}
} footer: {
Text("En simultané, un match de chaque poule d'un groupe de poule sera joué avant de passer à la suite de la programmation. Si l'option est désactivée, un maximum de matchs simultanés d'une poule sera programmé avant de passer à la poule suivante.")
}
} }
Section { Section {

@ -24,23 +24,19 @@ struct SelectablePlayerListView: View {
@StateObject private var searchViewModel: SearchViewModel @StateObject private var searchViewModel: SearchViewModel
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
var lastDataSource: String? {
dataStore.appSettings.lastDataSource
}
@State private var searchText: String = "" @State private var searchText: String = ""
var mostRecentDate: Date? {
guard let lastDataSource else { return nil }
return URL.importDateFormatter.date(from: lastDataSource)
}
init(allowSelection: Int = 0, isPresented: Bool = true, searchField: String? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, tokens: [SearchToken] = [], hidePlayers: [String]? = nil, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) { init(allowSelection: Int = 0, isPresented: Bool = true, searchField: String? = nil, dataSet: DataSet = .national, filterOption: PlayerFilterOption = .all, hideAssimilation: Bool = false, ascending: Bool = true, sortOption: SortOption = .rank, fromPlayer: FederalPlayer? = nil, codeClub: String? = nil, ligue: String? = nil, showFemaleInMaleAssimilation: Bool = false, tokens: [SearchToken] = [], hidePlayers: [String]? = nil, lastDataSource: Bool = false, playerSelectionAction: PlayerSelectionAction? = nil, contentUnavailableAction: ContentUnavailableAction? = nil) {
self.allowSelection = allowSelection self.allowSelection = allowSelection
self.playerSelectionAction = playerSelectionAction self.playerSelectionAction = playerSelectionAction
self.contentUnavailableAction = contentUnavailableAction self.contentUnavailableAction = contentUnavailableAction
self.searchText = searchField ?? "" self.searchText = searchField ?? ""
let searchViewModel = SearchViewModel() let searchViewModel = SearchViewModel()
searchViewModel.tokens = tokens searchViewModel.tokens = tokens
if lastDataSource {
searchViewModel.mostRecentDate = DataStore.shared.appSettings.lastDataSourceDate()
}
searchViewModel.searchText = searchField ?? "" searchViewModel.searchText = searchField ?? ""
searchViewModel.debouncableText = searchField ?? "" searchViewModel.debouncableText = searchField ?? ""
searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation searchViewModel.showFemaleInMaleAssimilation = showFemaleInMaleAssimilation
@ -59,6 +55,18 @@ struct SelectablePlayerListView: View {
_searchViewModel = StateObject(wrappedValue: searchViewModel) _searchViewModel = StateObject(wrappedValue: searchViewModel)
} }
var enableSourceCheck: Binding<Bool> {
Binding {
searchViewModel.mostRecentDate != nil
} set: { value in
if value == false {
searchViewModel.mostRecentDate = nil
} else {
searchViewModel.mostRecentDate = dataStore.appSettings.lastDataSourceDate()
}
}
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
if importObserver.isImportingFile() == false { if importObserver.isImportingFile() == false {
@ -73,6 +81,13 @@ struct SelectablePlayerListView: View {
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
Menu { Menu {
if let lastDataSource = dataStore.appSettings.localizedLastDataSource() {
Section {
Toggle(isOn: enableSourceCheck) {
Text("Limité à \(lastDataSource)")
}
}
}
Section { Section {
ForEach(SourceFileManager.getSortOption()) { option in ForEach(SourceFileManager.getSortOption()) { option in
Toggle(isOn: .init(get: { Toggle(isOn: .init(get: {
@ -191,7 +206,6 @@ struct SelectablePlayerListView: View {
} }
} }
.onAppear { .onAppear {
searchViewModel.mostRecentDate = mostRecentDate
if searchViewModel.tokens.isEmpty && searchText.isEmpty { if searchViewModel.tokens.isEmpty && searchText.isEmpty {
searchViewModel.debouncableText.removeAll() searchViewModel.debouncableText.removeAll()
searchViewModel.searchText.removeAll() searchViewModel.searchText.removeAll()

Loading…
Cancel
Save