Skip to content

Commit 50f0d81

Browse files
authored
Merge pull request #168 from srvarma7/enhancement/uploads-list-screen
Enhancement/uploads list screen
2 parents 37b8db4 + 115f738 commit 50f0d81

13 files changed

+695
-106
lines changed

TUSKitExample/TUSKitExample.xcodeproj/project.pbxproj

+60-8
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@
1717
2EB9149F26F09D7800548562 /* TUSKitExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB9149E26F09D7800548562 /* TUSKitExampleUITests.swift */; };
1818
2EB914B826F0A2CE00548562 /* TUSKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2EB914B726F0A2CE00548562 /* TUSKit */; };
1919
2EB914BA26F0B11C00548562 /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB914B926F0B11C00548562 /* PhotoPicker.swift */; };
20-
978A729D29ACE487002A9440 /* UploadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978A729C29ACE487002A9440 /* UploadsView.swift */; };
2120
978A729F29ACE4D2002A9440 /* FilePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978A729E29ACE4D2002A9440 /* FilePickerView.swift */; };
2221
978A72A229ACE5F7002A9440 /* TUSWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978A72A129ACE5F7002A9440 /* TUSWrapper.swift */; };
2322
97B438502987C770001C802F /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B4384F2987C770001C802F /* DocumentPicker.swift */; };
23+
E54341522A611F2900EC3B28 /* UploadStatusIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54341512A611F2900EC3B28 /* UploadStatusIndicator.swift */; };
24+
E54341542A6122DC00EC3B28 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54341532A6122DC00EC3B28 /* Assets.swift */; };
25+
E54341562A617A2D00EC3B28 /* UploadActionImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54341552A617A2D00EC3B28 /* UploadActionImage.swift */; };
26+
E543415A2A62784200EC3B28 /* UploadsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54341592A62784200EC3B28 /* UploadsListView.swift */; };
27+
E54341612A62A8C300EC3B28 /* UploadedRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54341602A62A8C300EC3B28 /* UploadedRowView.swift */; };
28+
E54341632A62A93C00EC3B28 /* FailedRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54341622A62A93C00EC3B28 /* FailedRowView.swift */; };
29+
E54341662A62AC8E00EC3B28 /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54341652A62AC8E00EC3B28 /* DestructiveButton.swift */; };
30+
E57FD3E52A6D705C00FBB84A /* ProgressRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E57FD3E42A6D705C00FBB84A /* ProgressRowView.swift */; };
2431
/* End PBXBuildFile section */
2532

2633
/* Begin PBXContainerItemProxy section */
@@ -57,10 +64,17 @@
5764
2EB914A026F09D7800548562 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5865
2EB914B626F0A29E00548562 /* TUSKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = TUSKit; path = ..; sourceTree = "<group>"; };
5966
2EB914B926F0B11C00548562 /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = "<group>"; };
60-
978A729C29ACE487002A9440 /* UploadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadsView.swift; sourceTree = "<group>"; };
6167
978A729E29ACE4D2002A9440 /* FilePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePickerView.swift; sourceTree = "<group>"; };
6268
978A72A129ACE5F7002A9440 /* TUSWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TUSWrapper.swift; sourceTree = "<group>"; };
6369
97B4384F2987C770001C802F /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = "<group>"; };
70+
E54341512A611F2900EC3B28 /* UploadStatusIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadStatusIndicator.swift; sourceTree = "<group>"; };
71+
E54341532A6122DC00EC3B28 /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
72+
E54341552A617A2D00EC3B28 /* UploadActionImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadActionImage.swift; sourceTree = "<group>"; };
73+
E54341592A62784200EC3B28 /* UploadsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadsListView.swift; sourceTree = "<group>"; };
74+
E54341602A62A8C300EC3B28 /* UploadedRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadedRowView.swift; sourceTree = "<group>"; };
75+
E54341622A62A93C00EC3B28 /* FailedRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedRowView.swift; sourceTree = "<group>"; };
76+
E54341652A62AC8E00EC3B28 /* DestructiveButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveButton.swift; sourceTree = "<group>"; };
77+
E57FD3E42A6D705C00FBB84A /* ProgressRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressRowView.swift; sourceTree = "<group>"; };
6478
/* End PBXFileReference section */
6579

6680
/* Begin PBXFrameworksBuildPhase section */
@@ -114,10 +128,10 @@
114128
2EB9147B26F09D7600548562 /* TUSKitExample */ = {
115129
isa = PBXGroup;
116130
children = (
117-
978A72A029ACE5DB002A9440 /* Helpers */,
118-
978A729B29ACE471002A9440 /* Screens */,
119131
2EB9147C26F09D7600548562 /* AppDelegate.swift */,
120132
2EB9147E26F09D7600548562 /* SceneDelegate.swift */,
133+
978A72A029ACE5DB002A9440 /* Helpers */,
134+
978A729B29ACE471002A9440 /* Screens */,
121135
2EB9148226F09D7800548562 /* Assets.xcassets */,
122136
2EB9148726F09D7800548562 /* LaunchScreen.storyboard */,
123137
2EB9148A26F09D7800548562 /* Info.plist */,
@@ -163,8 +177,8 @@
163177
isa = PBXGroup;
164178
children = (
165179
2EB9148026F09D7600548562 /* ContentView.swift */,
166-
978A729C29ACE487002A9440 /* UploadsView.swift */,
167180
978A729E29ACE4D2002A9440 /* FilePickerView.swift */,
181+
E54341502A61181B00EC3B28 /* Upload */,
168182
);
169183
path = Screens;
170184
sourceTree = "<group>";
@@ -175,10 +189,41 @@
175189
2EB914B926F0B11C00548562 /* PhotoPicker.swift */,
176190
97B4384F2987C770001C802F /* DocumentPicker.swift */,
177191
978A72A129ACE5F7002A9440 /* TUSWrapper.swift */,
192+
E54341532A6122DC00EC3B28 /* Assets.swift */,
178193
);
179194
path = Helpers;
180195
sourceTree = "<group>";
181196
};
197+
E54341502A61181B00EC3B28 /* Upload */ = {
198+
isa = PBXGroup;
199+
children = (
200+
E54341592A62784200EC3B28 /* UploadsListView.swift */,
201+
E543415B2A62A73500EC3B28 /* RowViews */,
202+
E54341642A62AB9E00EC3B28 /* Components */,
203+
);
204+
path = Upload;
205+
sourceTree = "<group>";
206+
};
207+
E543415B2A62A73500EC3B28 /* RowViews */ = {
208+
isa = PBXGroup;
209+
children = (
210+
E54341602A62A8C300EC3B28 /* UploadedRowView.swift */,
211+
E54341622A62A93C00EC3B28 /* FailedRowView.swift */,
212+
E57FD3E42A6D705C00FBB84A /* ProgressRowView.swift */,
213+
);
214+
path = RowViews;
215+
sourceTree = "<group>";
216+
};
217+
E54341642A62AB9E00EC3B28 /* Components */ = {
218+
isa = PBXGroup;
219+
children = (
220+
E54341512A611F2900EC3B28 /* UploadStatusIndicator.swift */,
221+
E54341552A617A2D00EC3B28 /* UploadActionImage.swift */,
222+
E54341652A62AC8E00EC3B28 /* DestructiveButton.swift */,
223+
);
224+
path = Components;
225+
sourceTree = "<group>";
226+
};
182227
/* End PBXGroup section */
183228

184229
/* Begin PBXNativeTarget section */
@@ -314,13 +359,20 @@
314359
isa = PBXSourcesBuildPhase;
315360
buildActionMask = 2147483647;
316361
files = (
362+
E54341662A62AC8E00EC3B28 /* DestructiveButton.swift in Sources */,
317363
978A72A229ACE5F7002A9440 /* TUSWrapper.swift in Sources */,
364+
E543415A2A62784200EC3B28 /* UploadsListView.swift in Sources */,
318365
2EB914BA26F0B11C00548562 /* PhotoPicker.swift in Sources */,
319366
2EB9147D26F09D7600548562 /* AppDelegate.swift in Sources */,
320367
2EB9147F26F09D7600548562 /* SceneDelegate.swift in Sources */,
321-
978A729D29ACE487002A9440 /* UploadsView.swift in Sources */,
368+
E54341522A611F2900EC3B28 /* UploadStatusIndicator.swift in Sources */,
322369
978A729F29ACE4D2002A9440 /* FilePickerView.swift in Sources */,
370+
E54341632A62A93C00EC3B28 /* FailedRowView.swift in Sources */,
371+
E54341612A62A8C300EC3B28 /* UploadedRowView.swift in Sources */,
372+
E54341562A617A2D00EC3B28 /* UploadActionImage.swift in Sources */,
373+
E54341542A6122DC00EC3B28 /* Assets.swift in Sources */,
323374
97B438502987C770001C802F /* DocumentPicker.swift in Sources */,
375+
E57FD3E52A6D705C00FBB84A /* ProgressRowView.swift in Sources */,
324376
2EB9148126F09D7600548562 /* ContentView.swift in Sources */,
325377
);
326378
runOnlyForDeploymentPostprocessing = 0;
@@ -491,7 +543,7 @@
491543
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
492544
CODE_SIGN_STYLE = Automatic;
493545
DEVELOPMENT_ASSET_PATHS = "\"TUSKitExample/Preview Content\"";
494-
DEVELOPMENT_TEAM = 4JMM8JMG3H;
546+
DEVELOPMENT_TEAM = G43DLT797F;
495547
ENABLE_PREVIEWS = YES;
496548
INFOPLIST_FILE = TUSKitExample/Info.plist;
497549
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@@ -513,7 +565,7 @@
513565
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
514566
CODE_SIGN_STYLE = Automatic;
515567
DEVELOPMENT_ASSET_PATHS = "\"TUSKitExample/Preview Content\"";
516-
DEVELOPMENT_TEAM = 4JMM8JMG3H;
568+
DEVELOPMENT_TEAM = G43DLT797F;
517569
ENABLE_PREVIEWS = YES;
518570
INFOPLIST_FILE = TUSKitExample/Info.plist;
519571
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Assets.swift
3+
// TUSKitExample
4+
//
5+
// Created by Sai Raghu Varma Kallepalli on 14/07/23.
6+
//
7+
8+
import SwiftUI
9+
10+
enum Icon: String {
11+
// Upload action button
12+
case resume = "play.circle"
13+
case pause = "pause.circle"
14+
case trash = "trash"
15+
case clear = "xmark.circle"
16+
17+
// Tab items
18+
case uploadFile = "square.and.arrow.up"
19+
case uploadFileFilled = "square.and.arrow.up.fill"
20+
case uploadList = "list.bullet"
21+
22+
// Nav bar
23+
case options = "ellipsis.circle"
24+
case checkmark = "checkmark.circle"
25+
26+
var color: Color {
27+
switch self {
28+
case .resume:
29+
return .blue
30+
case .pause, .clear:
31+
return .blue
32+
case .trash:
33+
return .red
34+
default:
35+
return .clear
36+
}
37+
}
38+
}

TUSKitExample/TUSKitExample/Helpers/TUSWrapper.swift

+72-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import Foundation
99
import TUSKit
10+
import SwiftUI
1011

1112
enum UploadStatus {
1213
case paused(bytesUploaded: Int, totalBytes: Int)
@@ -15,7 +16,7 @@ enum UploadStatus {
1516
case uploaded(url: URL)
1617
}
1718

18-
class TUSWrapper: TUSClientDelegate, ObservableObject {
19+
class TUSWrapper: ObservableObject {
1920
let client: TUSClient
2021

2122
@MainActor
@@ -31,7 +32,9 @@ class TUSWrapper: TUSClientDelegate, ObservableObject {
3132
try? client.cancel(id: id)
3233

3334
if case let .uploading(bytesUploaded, totalBytes) = uploads[id] {
34-
uploads[id] = .paused(bytesUploaded: bytesUploaded, totalBytes: totalBytes)
35+
withAnimation {
36+
uploads[id] = .paused(bytesUploaded: bytesUploaded, totalBytes: totalBytes)
37+
}
3538
}
3639
}
3740

@@ -40,17 +43,37 @@ class TUSWrapper: TUSClientDelegate, ObservableObject {
4043
_ = try? client.retry(id: id)
4144

4245
if case let .paused(bytesUploaded, totalBytes) = uploads[id] {
43-
uploads[id] = .uploading(bytesUploaded: bytesUploaded, totalBytes: totalBytes)
46+
withAnimation {
47+
uploads[id] = .uploading(bytesUploaded: bytesUploaded, totalBytes: totalBytes)
48+
}
4449
}
4550
}
4651

4752
@MainActor
4853
func clearUpload(id: UUID) {
4954
_ = try? client.cancel(id: id)
5055
_ = try? client.removeCacheFor(id: id)
51-
uploads[id] = nil
56+
57+
withAnimation {
58+
uploads[id] = nil
59+
}
5260
}
5361

62+
@MainActor
63+
func removeUpload(id: UUID) {
64+
_ = try? client.removeCacheFor(id: id)
65+
66+
withAnimation {
67+
uploads[id] = nil
68+
}
69+
}
70+
}
71+
72+
73+
// MARK: - TUSClientDelegate
74+
75+
76+
extension TUSWrapper: TUSClientDelegate {
5477
func progressFor(id: UUID, context: [String: String]?, bytesUploaded: Int, totalBytes: Int, client: TUSClient) {
5578
Task { @MainActor in
5679
uploads[id] = .uploading(bytesUploaded: bytesUploaded, totalBytes: totalBytes)
@@ -59,19 +82,26 @@ class TUSWrapper: TUSClientDelegate, ObservableObject {
5982

6083
func didStartUpload(id: UUID, context: [String : String]?, client: TUSClient) {
6184
Task { @MainActor in
62-
uploads[id] = .uploading(bytesUploaded: 0, totalBytes: Int.max)
85+
withAnimation {
86+
uploads[id] = .uploading(bytesUploaded: 0, totalBytes: Int.max)
87+
}
6388
}
6489
}
6590

6691
func didFinishUpload(id: UUID, url: URL, context: [String : String]?, client: TUSClient) {
6792
Task { @MainActor in
68-
uploads[id] = .uploaded(url: url)
93+
withAnimation {
94+
uploads[id] = .uploaded(url: url)
95+
}
6996
}
7097
}
7198

7299
func uploadFailed(id: UUID, error: Error, context: [String : String]?, client: TUSClient) {
73100
Task { @MainActor in
74-
uploads[id] = .failed(error: error)
101+
102+
withAnimation {
103+
uploads[id] = .failed(error: error)
104+
}
75105

76106
if case TUSClientError.couldNotUploadFile(underlyingError: let underlyingError) = error,
77107
case TUSAPIError.failedRequest(let response) = underlyingError {
@@ -83,3 +113,38 @@ class TUSWrapper: TUSClientDelegate, ObservableObject {
83113
func fileError(error: TUSClientError, client: TUSClient) { }
84114
func totalProgress(bytesUploaded: Int, totalBytes: Int, client: TUSClient) { }
85115
}
116+
117+
118+
// MARK: - Mock upload records
119+
120+
121+
extension TUSWrapper {
122+
@MainActor
123+
func setMockUploadRecords() {
124+
let sampleURL = URL(string: "https://www.google.com/search?client=safari&q=image&tbm=isch&sa=X&ved=2ahUKEwie6t7IyZCAAxXQcmwGHerJAG0Q0pQJegQIHhAB&biw=1680&bih=888&dpr=2#imgrc=cSb7xvw-0talCM")!
125+
let uploadStatusSample: [UUID: UploadStatus] = [
126+
UUID(): UploadStatus.uploading(bytesUploaded: 0, totalBytes: 100),
127+
UUID(): UploadStatus.paused(bytesUploaded: 60, totalBytes: 100),
128+
UUID(): UploadStatus.uploaded(url: sampleURL),
129+
UUID(): UploadStatus.failed(error: TUSAPIError.couldNotFetchServerInfo),
130+
131+
UUID(): UploadStatus.uploading(bytesUploaded: 25, totalBytes: 100),
132+
UUID(): UploadStatus.paused(bytesUploaded: 90, totalBytes: 100),
133+
UUID(): UploadStatus.uploaded(url: sampleURL),
134+
UUID(): UploadStatus.failed(error: TUSAPIError.underlyingError(NSError(domain: "invalid offset", code: 8))),
135+
136+
UUID(): UploadStatus.uploading(bytesUploaded: 50, totalBytes: 100),
137+
UUID(): UploadStatus.paused(bytesUploaded: 10, totalBytes: 100),
138+
UUID(): UploadStatus.uploaded(url: sampleURL),
139+
UUID(): UploadStatus.failed(error: TUSClientError.emptyUploadRange),
140+
141+
UUID(): UploadStatus.uploading(bytesUploaded: 75, totalBytes: 100),
142+
UUID(): UploadStatus.paused(bytesUploaded: 0, totalBytes: 100),
143+
UUID(): UploadStatus.uploaded(url: sampleURL),
144+
UUID(): UploadStatus.failed(error: TUSClientError.uploadIsAlreadyFinished)
145+
]
146+
withAnimation {
147+
uploads = uploadStatusSample
148+
}
149+
}
150+
}

TUSKitExample/TUSKitExample/SceneDelegate.swift

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
2222

2323

2424
wrapper = TUSWrapper(client: AppDelegate.tusClient)
25+
/// Set this to begin with mock data in uploads list screen
26+
// wrapper.setMockUploadRecords()
2527
let contentView = ContentView(tusWrapper: wrapper)
2628

2729
// Use a UIHostingController as window root view controller.

TUSKitExample/TUSKitExample/Screens/ContentView.swift

+31-12
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,56 @@ import PhotosUI
1111

1212
struct ContentView: View {
1313
let tusWrapper: TUSWrapper
14-
14+
15+
/// Can be helpful to set default tab while developing
16+
@State private var activeTab = 0
17+
1518
var body: some View {
16-
TabView {
19+
TabView(selection: $activeTab) {
1720
FilePickerView(
1821
photoPicker: PhotoPicker(tusClient: tusWrapper.client),
1922
filePicker: DocumentPicker(tusClient: tusWrapper.client)
2023
)
2124
.tabItem {
2225
VStack {
23-
Image(systemName: "square.and.arrow.up")
26+
Image(systemName: Icon.uploadFile.rawValue)
2427
Text("Upload files")
2528
}
26-
}
29+
}.tag(0)
2730

28-
UploadsView(
29-
tusWrapper: tusWrapper
30-
)
31+
NavigationView {
32+
UploadsListView()
33+
.environmentObject(tusWrapper)
34+
.navigationTitle("Uploads")
35+
.navigationBarTitleDisplayMode(.inline)
36+
}
37+
.navigationViewStyle(.stack)
3138
.tabItem {
3239
VStack {
33-
Image(systemName: "list.bullet")
40+
Image(systemName: Icon.uploadList.rawValue)
3441
Text("Uploads")
3542
}
36-
}
43+
}.tag(1)
3744
}
3845
}
3946
}
4047

4148
struct ContentView_Previews: PreviewProvider {
42-
@State static var isPresented = false
43-
static let tusClient = try! TUSClient(server: URL(string: "https://tusd.tusdemo.net/files")!, sessionIdentifier: "TUSClient", storageDirectory: URL(string: "TUS")!)
49+
static var tusWrapper: TUSWrapper = {
50+
let client = try! TUSClient(
51+
server: URL(string: "https://tusd.tusdemo.net/files")!,
52+
sessionIdentifier: "TUSClient",
53+
sessionConfiguration: .default,
54+
storageDirectory: URL(string: "TUS")!,
55+
chunkSize: 0
56+
)
57+
let wrapper = TUSWrapper(client: client)
58+
/// Set this to begin with mock data in uploads list screen in Preview
59+
// wrapper.setMockUploadRecords()
60+
return wrapper
61+
}()
62+
4463
static var previews: some View {
45-
ContentView(tusWrapper: TUSWrapper(client: tusClient))
64+
ContentView(tusWrapper: tusWrapper)
4665
}
4766
}

0 commit comments

Comments
 (0)