55// Created by Lessica on 2024/7/19.
66//
77
8+ import Combine
89import SwiftUI
910
1011final class App : Identifiable , ObservableObject {
@@ -51,17 +52,54 @@ final class App: Identifiable, ObservableObject {
5152
5253final class AppListModel : ObservableObject {
5354 static let shared = AppListModel ( )
55+ private var _allApplications : [ App ] = [ ]
56+
57+ @Published var filter = FilterOptions ( )
58+ @Published var userApplications : [ App ] = [ ]
59+ @Published var trollApplications : [ App ] = [ ]
60+ @Published var appleApplications : [ App ] = [ ]
5461
55- @Published var allApplications : [ App ] = [ ]
5662 @Published var hasTrollRecorder : Bool = false
5763 @Published var unsupportedCount : Int = 0
5864
65+ private var cancellables = Set < AnyCancellable > ( )
66+
5967 private init ( ) {
60- refresh ( )
68+ reload ( )
69+
70+ filter. $searchKeyword
71+ . combineLatest ( filter. $showPatchedOnly)
72+ . throttle ( for: 0.5 , scheduler: DispatchQueue . main, latest: true )
73+ . sink { _ in
74+ withAnimation {
75+ self . performFilter ( )
76+ }
77+ }
78+ . store ( in: & cancellables)
6179 }
6280
63- func refresh( ) {
64- self . allApplications = Self . fetchApplications ( & hasTrollRecorder, & unsupportedCount)
81+ func reload( ) {
82+ let allApplications = Self . fetchApplications ( & hasTrollRecorder, & unsupportedCount)
83+ self . _allApplications = allApplications
84+ performFilter ( )
85+ }
86+
87+ func performFilter( ) {
88+ var filteredApplications = _allApplications
89+
90+ if !filter. searchKeyword. isEmpty {
91+ filteredApplications = filteredApplications. filter {
92+ $0. name. localizedCaseInsensitiveContains ( filter. searchKeyword) || $0. id. localizedCaseInsensitiveContains ( filter. searchKeyword)
93+ }
94+ }
95+
96+ if filter. showPatchedOnly {
97+ filteredApplications = filteredApplications. filter { $0. isInjected }
98+ }
99+
100+ userApplications = filteredApplications. filter { $0. isUser }
101+ trollApplications = filteredApplications. filter { $0. isFromTroll }
102+ appleApplications = filteredApplications. filter { $0. isFromApple }
65103 }
66104
67105 private static let excludedIdentifiers : Set < String > = [
@@ -125,23 +163,27 @@ final class AppListModel: ObservableObject {
125163 }
126164}
127165
128- final class SearchOptions : ObservableObject {
129- @Published var keyword = " "
166+ final class FilterOptions : ObservableObject {
167+ @Published var searchKeyword = " "
168+ @Published var showPatchedOnly = false
169+
170+ var isSearching : Bool { !searchKeyword. isEmpty }
130171
131172 func reset( ) {
132- keyword = " "
173+ searchKeyword = " "
174+ showPatchedOnly = false
133175 }
134176}
135177
136178struct AppListCell : View {
137179 @StateObject var app : App
138- @EnvironmentObject var searchOptions : SearchOptions
180+ @EnvironmentObject var filter : FilterOptions
139181
140182 @available ( iOS 15 . 0 , * )
141183 var highlightedName : AttributedString {
142184 let name = app. name
143185 var attributedString = AttributedString ( name)
144- if let range = attributedString. range ( of: searchOptions . keyword , options: [ . caseInsensitive, . diacriticInsensitive] ) {
186+ if let range = attributedString. range ( of: filter . searchKeyword , options: [ . caseInsensitive, . diacriticInsensitive] ) {
145187 attributedString [ range] . foregroundColor = . accentColor
146188 }
147189 return attributedString
@@ -151,7 +193,7 @@ struct AppListCell: View {
151193 var highlightedId : AttributedString {
152194 let id = app. id
153195 var attributedString = AttributedString ( id)
154- if let range = attributedString. range ( of: searchOptions . keyword , options: [ . caseInsensitive, . diacriticInsensitive] ) {
196+ if let range = attributedString. range ( of: filter . searchKeyword , options: [ . caseInsensitive, . diacriticInsensitive] ) {
155197 attributedString [ range] . foregroundColor = . accentColor
156198 }
157199 return attributedString
@@ -206,11 +248,6 @@ struct AppListCell: View {
206248struct AppListView : View {
207249 @StateObject var vm = AppListModel . shared
208250
209- @State var showPatchedOnly = false
210- @State var searchResults : [ App ] = [ ]
211-
212- @StateObject var searchOptions = SearchOptions ( )
213-
214251 var appNameString : String {
215252 Bundle . main. infoDictionary ? [ " CFBundleName " ] as? String ?? " TrollFools "
216253 }
@@ -230,46 +267,17 @@ struct AppListView: View {
230267
231268 let repoURL = URL ( string: " https://github.com/Lessica/TrollFools " )
232269
233- var isSearching : Bool {
234- return !searchOptions. keyword. isEmpty
235- }
236-
237- var filteredApps : [ App ] {
238- if showPatchedOnly {
239- ( isSearching ? searchResults : vm. allApplications)
240- . filter { $0. isInjected }
241- } else {
242- isSearching ? searchResults : vm. allApplications
243- }
244- }
245-
246- var filteredUserApps : [ App ] {
247- filteredApps. filter { $0. isUser }
248- }
249-
250- var filteredSystemApps : [ App ] {
251- filteredApps. filter { $0. isSystem }
252- }
253-
254- var filteredTrollApps : [ App ] {
255- filteredSystemApps. filter { !$0. id. hasPrefix ( " com.apple. " ) }
256- }
257-
258- var filteredAppleApps : [ App ] {
259- filteredSystemApps. filter { $0. id. hasPrefix ( " com.apple. " ) }
260- }
261-
262270 func filteredAppList( _ apps: [ App ] ) -> some View {
263271 ForEach ( apps, id: \. id) { app in
264272 NavigationLink {
265273 OptionView ( app)
266274 } label: {
267275 if #available( iOS 16 . 0 , * ) {
268276 AppListCell ( app: app)
269- . environmentObject ( searchOptions )
277+ . environmentObject ( vm . filter )
270278 } else {
271279 AppListCell ( app: app)
272- . environmentObject ( searchOptions )
280+ . environmentObject ( vm . filter )
273281 . padding ( . vertical, 4 )
274282 }
275283 }
@@ -296,33 +304,33 @@ struct AppListView: View {
296304 var appList : some View {
297305 List {
298306 Section {
299- filteredAppList ( filteredUserApps )
307+ filteredAppList ( vm . userApplications )
300308 } header: {
301309 Text ( NSLocalizedString ( " User Applications " , comment: " " ) )
302310 . font ( . footnote)
303311 } footer: {
304- if !isSearching && !showPatchedOnly && vm. unsupportedCount > 0 {
312+ if !vm . filter . isSearching && !vm . filter . showPatchedOnly && vm. unsupportedCount > 0 {
305313 Text ( String ( format: NSLocalizedString ( " And %d more unsupported user applications. " , comment: " " ) , vm. unsupportedCount) )
306314 . font ( . footnote)
307315 }
308316 }
309317
310318 Section {
311- filteredAppList ( filteredTrollApps )
319+ filteredAppList ( vm . trollApplications )
312320 } header: {
313321 Text ( NSLocalizedString ( " TrollStore Applications " , comment: " " ) )
314322 . font ( . footnote)
315323 }
316324
317325 Section {
318- filteredAppList ( filteredAppleApps )
326+ filteredAppList ( vm . appleApplications )
319327 } header: {
320328 Text ( NSLocalizedString ( " Injectable System Applications " , comment: " " ) )
321329 . font ( . footnote)
322330 } footer: {
323- if !isSearching {
331+ if !vm . filter . isSearching {
324332 VStack ( alignment: . leading, spacing: 20 ) {
325- if !showPatchedOnly {
333+ if !vm . filter . showPatchedOnly {
326334 Text ( NSLocalizedString ( " Only removable system applications are eligible and listed. " , comment: " " ) )
327335 . font ( . footnote)
328336 }
@@ -343,14 +351,16 @@ struct AppListView: View {
343351 . toolbar {
344352 ToolbarItem ( placement: . navigationBarTrailing) {
345353 Button {
346- withAnimation {
347- showPatchedOnly. toggle ( )
348- }
354+ vm. filter. showPatchedOnly. toggle ( )
349355 } label: {
350356 if #available( iOS 15 . 0 , * ) {
351- Image ( systemName: showPatchedOnly ? " line.3.horizontal.decrease.circle.fill " : " line.3.horizontal.decrease.circle " )
357+ Image ( systemName: vm. filter. showPatchedOnly
358+ ? " line.3.horizontal.decrease.circle.fill "
359+ : " line.3.horizontal.decrease.circle " )
352360 } else {
353- Image ( systemName: showPatchedOnly ? " eject.circle.fill " : " eject.circle " )
361+ Image ( systemName: vm. filter. showPatchedOnly
362+ ? " eject.circle.fill "
363+ : " eject.circle " )
354364 }
355365 }
356366 . accessibilityLabel ( NSLocalizedString ( " Show Patched Only " , comment: " " ) )
@@ -363,32 +373,20 @@ struct AppListView: View {
363373 if #available( iOS 15 . 0 , * ) {
364374 appList
365375 . refreshable {
366- withAnimation {
367- vm. refresh ( )
368- }
376+ vm. reload ( )
369377 }
370378 . searchable (
371- text: $searchOptions . keyword ,
379+ text: $vm . filter . searchKeyword ,
372380 placement: . automatic,
373- prompt: ( showPatchedOnly
381+ prompt: ( vm . filter . showPatchedOnly
374382 ? NSLocalizedString ( " Search Patched… " , comment: " " )
375383 : NSLocalizedString ( " Search… " , comment: " " ) )
376384 )
377385 . textInputAutocapitalization ( . never)
378- . onChange ( of: searchOptions. keyword) { keyword in
379- fetchSearchResults ( for: keyword)
380- }
381386 } else {
382387 // Fallback on earlier versions
383388 appList
384389 }
385390 }
386391 }
387-
388- func fetchSearchResults( for query: String ) {
389- searchResults = vm. allApplications. filter { app in
390- app. name. localizedCaseInsensitiveContains ( query) ||
391- app. id. localizedCaseInsensitiveContains ( query)
392- }
393- }
394392}
0 commit comments