Skip to content

Commit f53a537

Browse files
committed
Update with asynchronous Timer alternative
1 parent a83de2c commit f53a537

File tree

10 files changed

+127
-31
lines changed

10 files changed

+127
-31
lines changed

.github/workflows/swift.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
steps:
1818
- run: |
19-
sudo xcode-select -s /Applications/Xcode_15.3.app
19+
sudo xcode-select -s /Applications/Xcode_16.2.app
2020
- uses: actions/checkout@v3
2121
- name: Build
2222
run: swift build -v

DIExample.xcodeproj/project.pbxproj

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
8F0250A02891ACC400F01348 /* Typicode+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F02509F2891ACC400F01348 /* Typicode+Mock.swift */; };
1111
8F2287B92A322F250095CD7E /* PostListView14.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2287B82A3220E90095CD7E /* PostListView14.swift */; };
1212
8F2287BA2A322F280095CD7E /* PostListViewModel14.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2287B72A3220D90095CD7E /* PostListViewModel14.swift */; };
13+
8F4498DE2D949896005B0D03 /* TimerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F4498DD2D949892005B0D03 /* TimerTask.swift */; };
1314
8F49DF732891C90E0017CFDF /* AsyncChannel+Void.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F49DF722891C90E0017CFDF /* AsyncChannel+Void.swift */; };
1415
8F49DF75289215780017CFDF /* PhotoGridViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F49DF74289215780017CFDF /* PhotoGridViewModelTests.swift */; };
1516
8F72EDF62A3A2C9500C733EF /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F72EDF52A3A2C9500C733EF /* Content.swift */; };
@@ -56,6 +57,7 @@
5657
8F02509F2891ACC400F01348 /* Typicode+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Typicode+Mock.swift"; sourceTree = "<group>"; };
5758
8F2287B72A3220D90095CD7E /* PostListViewModel14.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListViewModel14.swift; sourceTree = "<group>"; };
5859
8F2287B82A3220E90095CD7E /* PostListView14.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListView14.swift; sourceTree = "<group>"; };
60+
8F4498DD2D949892005B0D03 /* TimerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerTask.swift; sourceTree = "<group>"; };
5961
8F49DF722891C90E0017CFDF /* AsyncChannel+Void.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AsyncChannel+Void.swift"; sourceTree = "<group>"; };
6062
8F49DF74289215780017CFDF /* PhotoGridViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewModelTests.swift; sourceTree = "<group>"; };
6163
8F72EDF52A3A2C9500C733EF /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = "<group>"; };
@@ -119,6 +121,14 @@
119121
path = Posts14;
120122
sourceTree = "<group>";
121123
};
124+
8F4498DC2D94987C005B0D03 /* Types */ = {
125+
isa = PBXGroup;
126+
children = (
127+
8F4498DD2D949892005B0D03 /* TimerTask.swift */,
128+
);
129+
path = Types;
130+
sourceTree = "<group>";
131+
};
122132
8F72EDF72A3A2CDF00C733EF /* Extensions */ = {
123133
isa = PBXGroup;
124134
children = (
@@ -168,6 +178,7 @@
168178
8FE4F20228887BC800CF32A3 /* Photos */,
169179
8F72EDF72A3A2CDF00C733EF /* Extensions */,
170180
8FE4F1F828885EE400CF32A3 /* APIs */,
181+
8F4498DC2D94987C005B0D03 /* Types */,
171182
8FE4F1CE2888596A00CF32A3 /* Assets.xcassets */,
172183
8FE4F1D32888596A00CF32A3 /* DIExample.entitlements */,
173184
8FE4F1D02888596A00CF32A3 /* Preview Content */,
@@ -300,7 +311,7 @@
300311
attributes = {
301312
BuildIndependentTargetsInParallel = 1;
302313
LastSwiftUpdateCheck = 1400;
303-
LastUpgradeCheck = 1600;
314+
LastUpgradeCheck = 1630;
304315
TargetAttributes = {
305316
8FE4F1C62888596700CF32A3 = {
306317
CreatedOnToolsVersion = 14.0;
@@ -375,6 +386,7 @@
375386
8F2287BA2A322F280095CD7E /* PostListViewModel14.swift in Sources */,
376387
8FE4F20628888F4D00CF32A3 /* PhotoGridView.swift in Sources */,
377388
8F49DF732891C90E0017CFDF /* AsyncChannel+Void.swift in Sources */,
389+
8F4498DE2D949896005B0D03 /* TimerTask.swift in Sources */,
378390
8FE4F1FA28885F0100CF32A3 /* Typicode.swift in Sources */,
379391
8F72EDFA2A3A31E400C733EF /* ContentSidebarList.swift in Sources */,
380392
8FE4F1CD2888596700CF32A3 /* ContentView.swift in Sources */,
@@ -461,6 +473,7 @@
461473
COPY_PHASE_STRIP = NO;
462474
DEAD_CODE_STRIPPING = YES;
463475
DEBUG_INFORMATION_FORMAT = dwarf;
476+
DEVELOPMENT_TEAM = 483DWKW443;
464477
ENABLE_STRICT_OBJC_MSGSEND = YES;
465478
ENABLE_TESTABILITY = YES;
466479
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -478,14 +491,27 @@
478491
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
479492
GCC_WARN_UNUSED_FUNCTION = YES;
480493
GCC_WARN_UNUSED_VARIABLE = YES;
481-
MACOSX_DEPLOYMENT_TARGET = 12.0;
494+
MACOSX_DEPLOYMENT_TARGET = 13.5;
482495
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
483496
MTL_FAST_MATH = YES;
484497
ONLY_ACTIVE_ARCH = YES;
485498
SDKROOT = macosx;
486499
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
487500
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
488501
SWIFT_STRICT_CONCURRENCY = complete;
502+
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
503+
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
504+
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
505+
SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES;
506+
SWIFT_UPCOMING_FEATURE_GLOBAL_ACTOR_ISOLATED_TYPES_USABILITY = YES;
507+
SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES;
508+
SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES;
509+
SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES;
510+
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
511+
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
512+
SWIFT_UPCOMING_FEATURE_NONFROZEN_ENUM_EXHAUSTIVITY = YES;
513+
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
514+
SWIFT_VERSION = 6.0;
489515
};
490516
name = Debug;
491517
};
@@ -525,6 +551,7 @@
525551
COPY_PHASE_STRIP = NO;
526552
DEAD_CODE_STRIPPING = YES;
527553
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
554+
DEVELOPMENT_TEAM = 483DWKW443;
528555
ENABLE_NS_ASSERTIONS = NO;
529556
ENABLE_STRICT_OBJC_MSGSEND = YES;
530557
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -536,13 +563,26 @@
536563
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
537564
GCC_WARN_UNUSED_FUNCTION = YES;
538565
GCC_WARN_UNUSED_VARIABLE = YES;
539-
MACOSX_DEPLOYMENT_TARGET = 12.0;
566+
MACOSX_DEPLOYMENT_TARGET = 13.5;
540567
MTL_ENABLE_DEBUG_INFO = NO;
541568
MTL_FAST_MATH = YES;
542569
SDKROOT = macosx;
543570
SWIFT_COMPILATION_MODE = wholemodule;
544571
SWIFT_OPTIMIZATION_LEVEL = "-O";
545572
SWIFT_STRICT_CONCURRENCY = complete;
573+
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
574+
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
575+
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
576+
SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES;
577+
SWIFT_UPCOMING_FEATURE_GLOBAL_ACTOR_ISOLATED_TYPES_USABILITY = YES;
578+
SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES;
579+
SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES;
580+
SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES;
581+
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
582+
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
583+
SWIFT_UPCOMING_FEATURE_NONFROZEN_ENUM_EXHAUSTIVITY = YES;
584+
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
585+
SWIFT_VERSION = 6.0;
546586
};
547587
name = Release;
548588
};
@@ -557,7 +597,6 @@
557597
CURRENT_PROJECT_VERSION = 1;
558598
DEAD_CODE_STRIPPING = YES;
559599
DEVELOPMENT_ASSET_PATHS = "\"DIExample/Preview Content\"";
560-
DEVELOPMENT_TEAM = 483DWKW443;
561600
ENABLE_HARDENED_RUNTIME = YES;
562601
ENABLE_PREVIEWS = YES;
563602
GENERATE_INFOPLIST_FILE = YES;
@@ -570,7 +609,6 @@
570609
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.DIExample;
571610
PRODUCT_NAME = "$(TARGET_NAME)";
572611
SWIFT_EMIT_LOC_STRINGS = YES;
573-
SWIFT_VERSION = 5.0;
574612
};
575613
name = Debug;
576614
};
@@ -585,7 +623,6 @@
585623
CURRENT_PROJECT_VERSION = 1;
586624
DEAD_CODE_STRIPPING = YES;
587625
DEVELOPMENT_ASSET_PATHS = "\"DIExample/Preview Content\"";
588-
DEVELOPMENT_TEAM = 483DWKW443;
589626
ENABLE_HARDENED_RUNTIME = YES;
590627
ENABLE_PREVIEWS = YES;
591628
GENERATE_INFOPLIST_FILE = YES;
@@ -598,7 +635,6 @@
598635
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.DIExample;
599636
PRODUCT_NAME = "$(TARGET_NAME)";
600637
SWIFT_EMIT_LOC_STRINGS = YES;
601-
SWIFT_VERSION = 5.0;
602638
};
603639
name = Release;
604640
};
@@ -609,14 +645,11 @@
609645
CODE_SIGN_STYLE = Automatic;
610646
CURRENT_PROJECT_VERSION = 1;
611647
DEAD_CODE_STRIPPING = YES;
612-
DEVELOPMENT_TEAM = 483DWKW443;
613648
GENERATE_INFOPLIST_FILE = YES;
614-
MACOSX_DEPLOYMENT_TARGET = 12.4;
615649
MARKETING_VERSION = 1.0;
616650
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.DIExampleTests;
617651
PRODUCT_NAME = "$(TARGET_NAME)";
618652
SWIFT_EMIT_LOC_STRINGS = NO;
619-
SWIFT_VERSION = 5.0;
620653
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DIExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DIExample";
621654
};
622655
name = Debug;
@@ -628,14 +661,11 @@
628661
CODE_SIGN_STYLE = Automatic;
629662
CURRENT_PROJECT_VERSION = 1;
630663
DEAD_CODE_STRIPPING = YES;
631-
DEVELOPMENT_TEAM = 483DWKW443;
632664
GENERATE_INFOPLIST_FILE = YES;
633-
MACOSX_DEPLOYMENT_TARGET = 12.4;
634665
MARKETING_VERSION = 1.0;
635666
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.DIExampleTests;
636667
PRODUCT_NAME = "$(TARGET_NAME)";
637668
SWIFT_EMIT_LOC_STRINGS = NO;
638-
SWIFT_VERSION = 5.0;
639669
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DIExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DIExample";
640670
};
641671
name = Release;
@@ -646,13 +676,11 @@
646676
CODE_SIGN_STYLE = Automatic;
647677
CURRENT_PROJECT_VERSION = 1;
648678
DEAD_CODE_STRIPPING = YES;
649-
DEVELOPMENT_TEAM = 483DWKW443;
650679
GENERATE_INFOPLIST_FILE = YES;
651680
MARKETING_VERSION = 1.0;
652681
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.DIExampleUITests;
653682
PRODUCT_NAME = "$(TARGET_NAME)";
654683
SWIFT_EMIT_LOC_STRINGS = NO;
655-
SWIFT_VERSION = 5.0;
656684
TEST_TARGET_NAME = DIExample;
657685
};
658686
name = Debug;
@@ -663,13 +691,11 @@
663691
CODE_SIGN_STYLE = Automatic;
664692
CURRENT_PROJECT_VERSION = 1;
665693
DEAD_CODE_STRIPPING = YES;
666-
DEVELOPMENT_TEAM = 483DWKW443;
667694
GENERATE_INFOPLIST_FILE = YES;
668695
MARKETING_VERSION = 1.0;
669696
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.DIExampleUITests;
670697
PRODUCT_NAME = "$(TARGET_NAME)";
671698
SWIFT_EMIT_LOC_STRINGS = NO;
672-
SWIFT_VERSION = 5.0;
673699
TEST_TARGET_NAME = DIExample;
674700
};
675701
name = Release;

DIExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

DIExample.xcodeproj/xcshareddata/xcschemes/DIExample.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1600"
3+
LastUpgradeVersion = "1630"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

DIExample/Posts/PostList.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ struct PostList: View {
2424
}
2525
}
2626

27+
#if DEBUG
2728
@available(macOS 13.0, *)
2829
struct PostList_Previews: PreviewProvider {
2930
static var previews: some View {
@@ -34,3 +35,4 @@ struct PostList_Previews: PreviewProvider {
3435
}
3536
}
3637
}
38+
#endif

DIExample/Posts14/PostListView14.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ import SwiftUI
4141
}
4242
.navigationTitle("Posts14")
4343
.toolbar {
44+
ToolbarItem(placement: .automatic) {
45+
Text("\(viewModel.secondsSinceLastRefresh)")
46+
}
4447
ToolbarItem(placement: .automatic) {
4548
Button {
4649
viewModel.refresh()

DIExample/Posts14/PostListViewModel14.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,33 @@ import Factory
3131
showAlert = true
3232
}
3333
}
34+
private(set) var secondsSinceLastRefresh = 0
35+
@ObservationIgnored private var timerTask: TimerTask?
36+
37+
deinit {
38+
timerTask?.cancel()
39+
print("☠️ \(Self.self) \(#function)")
40+
}
41+
42+
private func startTimer() {
43+
timerTask?.cancel()
44+
timerTask = .repeating(interval: .seconds(1), operation: { [weak self] in
45+
self?.secondsSinceLastRefresh += 1
46+
})
47+
}
3448

3549
private func posts() async throws -> [Typicode.Post] {
50+
timerTask?.cancel()
51+
secondsSinceLastRefresh = 0
3652
isLoading = true
37-
defer { isLoading = false }
53+
defer {
54+
isLoading = false
55+
startTimer()
56+
}
3857

3958
do {
40-
try? await Task.sleep(for: .seconds(1))
59+
// uncomment to simulate a load delay
60+
// try? await Task.sleep(for: .seconds(1))
4161
// uncomment to simulate an error
4262
// throw CancellationError()
4363
return try await api.posts()
@@ -59,7 +79,9 @@ import Factory
5979
private func filterPosts() {
6080
withObservationTracking {
6181
filteredPosts = posts.filter { searchText.isEmpty || $0.title.localizedCaseInsensitiveContains(searchText) }
62-
} onChange: {
82+
} onChange: { [weak self] in
83+
guard let self else { return }
84+
6385
Task { @MainActor in
6486
self.filterPosts()
6587
}

DIExample/Types/TimerTask.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// TimerTask.swift
3+
// DIExample
4+
//
5+
// Created by Michael Link on 3/26/25.
6+
//
7+
8+
struct TimerTask: Sendable {
9+
let task: Task<Void, Never>
10+
11+
@discardableResult
12+
init<C>(priority: TaskPriority? = nil, interval duration: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C = ContinuousClock(), @_inheritActorContext @_implicitSelfCapture operation: sending @escaping @isolated(any) () async -> Void) where C : Clock {
13+
task = Task(priority: priority) {
14+
do {
15+
try await Task.sleep(for: duration, tolerance: tolerance, clock: clock)
16+
await operation()
17+
} catch {
18+
// return
19+
}
20+
}
21+
}
22+
23+
private init<C>(priority: TaskPriority? = nil, interval duration: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C = ContinuousClock(), repeats: Bool, @_inheritActorContext operation: sending @escaping @isolated(any) () async -> Void) where C : Clock {
24+
task = Task(priority: priority) {
25+
do {
26+
repeat {
27+
try await Task.sleep(for: duration, tolerance: tolerance, clock: clock)
28+
await operation()
29+
} while repeats
30+
} catch {
31+
// return
32+
}
33+
}
34+
}
35+
36+
@inlinable static func repeating<C>(priority: TaskPriority? = nil, interval duration: C.Instant.Duration, tolerance: C.Instant.Duration? = nil, clock: C = ContinuousClock(), @_inheritActorContext operation: sending @escaping @isolated(any) () async -> Void) -> Self where C : Clock {
37+
Self.init(priority: priority, interval: duration, tolerance: tolerance, clock: clock, repeats: true, operation: operation)
38+
}
39+
40+
func cancel() {
41+
task.cancel()
42+
}
43+
}

0 commit comments

Comments
 (0)