Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download support #33

Merged
merged 13 commits into from
Jun 6, 2023
40 changes: 40 additions & 0 deletions NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
23EA9CF9292FB70A00B8E418 /* SampleUserResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CEF292FB70A00B8E418 /* SampleUserResponse.swift */; };
23EA9CFA292FB70A00B8E418 /* SampleUserAuthRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CF1292FB70A00B8E418 /* SampleUserAuthRequest.swift */; };
23EA9CFB292FB70A00B8E418 /* SampleUserRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EA9CF2292FB70A00B8E418 /* SampleUserRequest.swift */; };
58C3E75E29B78EE6004FD1CD /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */; };
58C3E75F29B78EE8004FD1CD /* DownloadsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E75D29B78ED3004FD1CD /* DownloadsViewModel.swift */; };
58C3E76129B79259004FD1CD /* SampleDownloadRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E76029B79259004FD1CD /* SampleDownloadRouter.swift */; };
58C3E76529B7D709004FD1CD /* DownloadProgressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3E76429B7D709004FD1CD /* DownloadProgressViewModel.swift */; };
58E4E0ED2982D884000ACBC0 /* SampleAuthorizationStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E4E0EC2982D884000ACBC0 /* SampleAuthorizationStorageManager.swift */; };
58E4E0EF29843B42000ACBC0 /* NetworkingSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E4E0EE29843B42000ACBC0 /* NetworkingSampleApp.swift */; };
58E4E0F129850E86000ACBC0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E4E0F029850E86000ACBC0 /* ContentView.swift */; };
58FB80C7298521FF0031FC59 /* AuthorizationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB80C6298521FF0031FC59 /* AuthorizationView.swift */; };
58FB80CE29895ABF0031FC59 /* TestData.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 58FB80CD29895ABF0031FC59 /* TestData.xcassets */; };
DD410D6F293F2E6E006D8E31 /* AuthorizationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD410D6E293F2E6E006D8E31 /* AuthorizationViewModel.swift */; };
DD6E48732A0E24D30025AD05 /* DownloadProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6E48722A0E24D30025AD05 /* DownloadProgressView.swift */; };
DD6E48762A0E2CD30025AD05 /* DownloadAPIManager+SharedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6E48752A0E2CD30025AD05 /* DownloadAPIManager+SharedInstance.swift */; };
DD887780293E33850065ED03 /* SampleErrorProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88777F293E33850065ED03 /* SampleErrorProcessor.swift */; };
DDD3AD1F2950E794006CB777 /* SampleAuthRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3AD1E2950E794006CB777 /* SampleAuthRouter.swift */; };
DDD3AD212951F527006CB777 /* SampleAuthorizationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3AD202951F527006CB777 /* SampleAuthorizationManager.swift */; };
Expand All @@ -45,12 +51,18 @@
23EA9CEF292FB70A00B8E418 /* SampleUserResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserResponse.swift; sourceTree = "<group>"; };
23EA9CF1292FB70A00B8E418 /* SampleUserAuthRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserAuthRequest.swift; sourceTree = "<group>"; };
23EA9CF2292FB70A00B8E418 /* SampleUserRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUserRequest.swift; sourceTree = "<group>"; };
58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = "<group>"; };
58C3E75D29B78ED3004FD1CD /* DownloadsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewModel.swift; sourceTree = "<group>"; };
58C3E76029B79259004FD1CD /* SampleDownloadRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleDownloadRouter.swift; sourceTree = "<group>"; };
58C3E76429B7D709004FD1CD /* DownloadProgressViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgressViewModel.swift; sourceTree = "<group>"; };
58E4E0EC2982D884000ACBC0 /* SampleAuthorizationStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleAuthorizationStorageManager.swift; sourceTree = "<group>"; };
58E4E0EE29843B42000ACBC0 /* NetworkingSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingSampleApp.swift; sourceTree = "<group>"; };
58E4E0F029850E86000ACBC0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
58FB80C6298521FF0031FC59 /* AuthorizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationView.swift; sourceTree = "<group>"; };
58FB80CD29895ABF0031FC59 /* TestData.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = TestData.xcassets; sourceTree = "<group>"; };
DD410D6E293F2E6E006D8E31 /* AuthorizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationViewModel.swift; sourceTree = "<group>"; };
DD6E48722A0E24D30025AD05 /* DownloadProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadProgressView.swift; sourceTree = "<group>"; };
DD6E48752A0E2CD30025AD05 /* DownloadAPIManager+SharedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DownloadAPIManager+SharedInstance.swift"; sourceTree = "<group>"; };
DD88777F293E33850065ED03 /* SampleErrorProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleErrorProcessor.swift; sourceTree = "<group>"; };
DDD3AD1E2950E794006CB777 /* SampleAuthRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleAuthRouter.swift; sourceTree = "<group>"; };
DDD3AD202951F527006CB777 /* SampleAuthorizationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleAuthorizationManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -91,6 +103,7 @@
23A575B325F8B9DA00617551 /* NetworkingSampleApp */ = {
isa = PBXGroup;
children = (
DD6E48742A0E2CC70025AD05 /* Extensions */,
58FB80CC29895A8D0031FC59 /* Resources */,
23EA9CE7292FB70A00B8E418 /* API */,
23A575ED25F8BF0E00617551 /* Scenes */,
Expand All @@ -113,6 +126,7 @@
23A575ED25F8BF0E00617551 /* Scenes */ = {
isa = PBXGroup;
children = (
58C3E75B29B78ED3004FD1CD /* Download */,
58FB80C5298521DA0031FC59 /* Authorization */,
);
path = Scenes;
Expand Down Expand Up @@ -153,6 +167,7 @@
isa = PBXGroup;
children = (
DDD3AD1E2950E794006CB777 /* SampleAuthRouter.swift */,
58C3E76029B79259004FD1CD /* SampleDownloadRouter.swift */,
23EA9CE9292FB70A00B8E418 /* SampleUserRouter.swift */,
);
path = Routers;
Expand All @@ -178,6 +193,17 @@
path = Requests;
sourceTree = "<group>";
};
58C3E75B29B78ED3004FD1CD /* Download */ = {
isa = PBXGroup;
children = (
DD6E48722A0E24D30025AD05 /* DownloadProgressView.swift */,
58C3E75C29B78ED3004FD1CD /* DownloadsView.swift */,
58C3E75D29B78ED3004FD1CD /* DownloadsViewModel.swift */,
58C3E76429B7D709004FD1CD /* DownloadProgressViewModel.swift */,
);
path = Download;
sourceTree = "<group>";
};
58FB80C5298521DA0031FC59 /* Authorization */ = {
isa = PBXGroup;
children = (
Expand All @@ -195,6 +221,14 @@
path = Resources;
sourceTree = "<group>";
};
DD6E48742A0E2CC70025AD05 /* Extensions */ = {
isa = PBXGroup;
children = (
DD6E48752A0E2CD30025AD05 /* DownloadAPIManager+SharedInstance.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -271,21 +305,27 @@
buildActionMask = 2147483647;
files = (
58E4E0EF29843B42000ACBC0 /* NetworkingSampleApp.swift in Sources */,
DD6E48762A0E2CD30025AD05 /* DownloadAPIManager+SharedInstance.swift in Sources */,
DDE8884529476AC300DD3BFF /* SampleRefreshTokenRequest.swift in Sources */,
DDD3AD212951F527006CB777 /* SampleAuthorizationManager.swift in Sources */,
23EA9CF7292FB70A00B8E418 /* SampleUserAuthResponse.swift in Sources */,
58E4E0ED2982D884000ACBC0 /* SampleAuthorizationStorageManager.swift in Sources */,
23EA9CF6292FB70A00B8E418 /* SampleAPIError.swift in Sources */,
58E4E0F129850E86000ACBC0 /* ContentView.swift in Sources */,
58C3E76529B7D709004FD1CD /* DownloadProgressViewModel.swift in Sources */,
23EA9CF9292FB70A00B8E418 /* SampleUserResponse.swift in Sources */,
DDD3AD1F2950E794006CB777 /* SampleAuthRouter.swift in Sources */,
DD887780293E33850065ED03 /* SampleErrorProcessor.swift in Sources */,
23EA9CFB292FB70A00B8E418 /* SampleUserRequest.swift in Sources */,
23EA9CFA292FB70A00B8E418 /* SampleUserAuthRequest.swift in Sources */,
58C3E76129B79259004FD1CD /* SampleDownloadRouter.swift in Sources */,
23EA9CF4292FB70A00B8E418 /* SampleUserRouter.swift in Sources */,
23EA9CF5292FB70A00B8E418 /* SampleAPIConstants.swift in Sources */,
58FB80C7298521FF0031FC59 /* AuthorizationView.swift in Sources */,
DD410D6F293F2E6E006D8E31 /* AuthorizationViewModel.swift in Sources */,
58C3E75F29B78EE8004FD1CD /* DownloadsViewModel.swift in Sources */,
58C3E75E29B78EE6004FD1CD /* DownloadsView.swift in Sources */,
DD6E48732A0E24D30025AD05 /* DownloadProgressView.swift in Sources */,
23EA9CF8292FB70A00B8E418 /* SampleUsersResponse.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// SampleDownloadRouter.swift
//
//
// Created by Matej Molnár on 07.03.2023.
//

import Foundation
import Networking

/// Implementation of sample API router
enum SampleDownloadRouter: Requestable {
case download(url: URL)

var baseURL: URL {
switch self {
case let .download(url):
return url
gajddo00 marked this conversation as resolved.
Show resolved Hide resolved
}
}

var path: String {
switch self {
case .download:
return ""
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ enum SampleAPIConstants {
static let authHost = "https://nonexistentmockauth.com/api"
static let validEmail = "eve.holt@reqres.in"
static let validPassword = "cityslicka"
static let videoUrl = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
}
13 changes: 8 additions & 5 deletions NetworkingSampleApp/NetworkingSampleApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@

import SwiftUI

enum NetworkingCase: String, Hashable, CaseIterable {
enum NetworkingFeature: String, Hashable, CaseIterable {
case authorization
case downloads
}

struct ContentView: View {
var body: some View {
NavigationStack {
List {
ForEach(NetworkingCase.allCases, id: \.self) { screen in
NavigationLink(screen.rawValue.capitalized, value: NetworkingCase.authorization)
ForEach(NetworkingFeature.allCases, id: \.self) { feature in
NavigationLink(feature.rawValue.capitalized, value: feature)
}
}
.navigationTitle("Examples")
.navigationDestination(for: NetworkingCase.self) { screen in
switch screen {
.navigationDestination(for: NetworkingFeature.self) { feature in
switch feature {
case .authorization:
AuthorizationView()
case .downloads:
DownloadsView()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// DownloadAPIManager+SharedInstance.swift
// NetworkingSampleApp
//
// Created by Dominika Gajdová on 12.05.2023.
//

import Networking

extension DownloadAPIManager {
static var shared: DownloadAPIManaging = {
var responseProcessors: [ResponseProcessing] = [
LoggingInterceptor.shared,
StatusCodeProcessor.shared
]
var errorProcessors: [ErrorProcessing] = [LoggingInterceptor.shared]

#if DEBUG
responseProcessors.append(EndpointRequestStorageProcessor.shared)
errorProcessors.append(EndpointRequestStorageProcessor.shared)
#endif

return DownloadAPIManager(
urlSessionConfiguration: .default,
requestAdapters: [
LoggingInterceptor.shared
],
responseProcessors: responseProcessors,
errorProcessors: errorProcessors
)
}()
}
5 changes: 5 additions & 0 deletions NetworkingSampleApp/NetworkingSampleApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// DownloadProgressView.swift
// NetworkingSampleApp
//
// Created by Matej Molnár on 07.03.2023.
//

import SwiftUI

struct DownloadProgressView: View {
@StateObject var viewModel: DownloadProgressViewModel

var body: some View {
VStack(alignment: .leading, spacing: 8) {
content
}
.task {
await viewModel.startObservingDownloadProgress()
}
.padding(10)
.background(
Color.white
.cornerRadius(15)
.shadow(radius: 10)
)
.padding(15)
}
}

// MARK: Components
private extension DownloadProgressView {
@ViewBuilder
var content: some View {
Text(viewModel.state.title)
.padding(.bottom, 8)

Text("Status: \(viewModel.state.statusTitle)")
Text("\(String(format: "%.1f", viewModel.state.percentCompleted))% of \(String(format: "%.1f", viewModel.state.totalMegaBytes))MB")

if let errorTitle = viewModel.state.errorTitle {
Text("Error: \(errorTitle)")
}

if let fileURL = viewModel.state.fileURL {
Text("FileURL: \(fileURL)")
}

downloadState
}

@ViewBuilder
var downloadState: some View {
if viewModel.state.status != .completed {
HStack {
Button {
viewModel.suspend()
} label: {
Text("Suspend")
}

Button {
viewModel.resume()
} label: {
Text("Resume")
}

Button {
viewModel.cancel()
} label: {
Text("Cancel")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// DownloadProgressViewModel.swift
// NetworkingSampleApp
//
// Created by Matej Molnár on 07.03.2023.
//

import Foundation
import Networking

@MainActor
final class DownloadProgressViewModel: ObservableObject {
private let task: URLSessionTask

@Published var state: DownloadProgressState = .init()

init(task: URLSessionTask) {
self.task = task
}

func startObservingDownloadProgress() async {
let stream = DownloadAPIManager.shared.progressStream(for: task)

for try await downloadState in stream {
var newState = DownloadProgressState()
newState.percentCompleted = downloadState.fractionCompleted * 100
newState.totalMegaBytes = Double(downloadState.totalBytes) / 1_000_000
newState.status = downloadState.taskState
newState.statusTitle = downloadState.taskState.title
newState.errorTitle = downloadState.error?.localizedDescription
newState.fileURL = downloadState.downloadedFileURL?.absoluteString
newState.title = task.currentRequest?.url?.absoluteString ?? "-"
state = newState
}
}

func suspend() {
task.suspend()
}

func resume() {
task.resume()
}

func cancel() {
task.cancel()
}
}

// MARK: Download state
struct DownloadProgressState {
var title: String = ""
var status: URLSessionTask.State = .running
var statusTitle: String = ""
var percentCompleted: Double = 0
var totalMegaBytes: Double = 0
var errorTitle: String?
var fileURL: String?
}

// MARK: URLSessionTask states
private extension URLSessionTask.State {
var title: String {
switch self {
case .canceling: return "cancelling"
case .completed: return "completed"
case .running: return "running"
case .suspended: return "suspended"
@unknown default: return ""
}
}
}
Loading