-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #43 from strvcom/feat/upload
Data and file upload support
- Loading branch information
Showing
25 changed files
with
1,812 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
NetworkingSampleApp/NetworkingSampleApp/API/Routers/SampleUploadRouter.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// | ||
// SampleUploadRouter.swift | ||
// NetworkingSampleApp | ||
// | ||
// Created by Tony Ngo on 12.06.2023. | ||
// | ||
|
||
import Foundation | ||
import Networking | ||
|
||
enum SampleUploadRouter: Requestable { | ||
case image | ||
case file(URL) | ||
case multipart(boundary: String) | ||
|
||
var baseURL: URL { | ||
URL(string: SampleAPIConstants.uploadHost)! | ||
} | ||
|
||
var headers: [String: String]? { | ||
switch self { | ||
case .image: | ||
return ["Content-Type": "image/png"] | ||
case let .file(url): | ||
return ["Content-Type": url.mimeType] | ||
case let .multipart(boundary): | ||
return ["Content-Type": "multipart/form-data; boundary=\(boundary)"] | ||
} | ||
} | ||
|
||
var path: String { | ||
"/post" | ||
} | ||
|
||
var method: HTTPMethod { | ||
.post | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import Foundation | |
enum SampleAPIConstants { | ||
static let userHost = "https://reqres.in/api" | ||
static let authHost = "https://nonexistentmockauth.com/api" | ||
static let uploadHost = "https://httpbin.org" | ||
static let validEmail = "[email protected]" | ||
static let validPassword = "cityslicka" | ||
static let videoUrl = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
NetworkingSampleApp/NetworkingSampleApp/Extensions/ByteCountFormatter+Convenience.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// | ||
// ByteCountFormatter+Convenience.swift | ||
// NetworkingSampleApp | ||
// | ||
// Created by Tony Ngo on 30.06.2023. | ||
// | ||
|
||
import Foundation | ||
|
||
extension ByteCountFormatter { | ||
static let megaBytesFormatter: ByteCountFormatter = { | ||
let formatter = ByteCountFormatter() | ||
formatter.allowedUnits = [.useMB] | ||
formatter.countStyle = .file | ||
return formatter | ||
}() | ||
} |
68 changes: 68 additions & 0 deletions
68
NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/FormUploadsViewModel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// | ||
// FormUploadsViewModel.swift | ||
// NetworkingSampleApp | ||
// | ||
// Created by Tony Ngo on 19.06.2023. | ||
// | ||
|
||
import Foundation | ||
import Networking | ||
import OSLog | ||
|
||
@MainActor | ||
final class FormUploadsViewModel: ObservableObject { | ||
@Published var username = "" | ||
@Published var fileUrl: URL? | ||
@Published var isErrorAlertPresented = false | ||
@Published private(set) var error: Error? | ||
@Published private(set) var uploadItemViewModels: [UploadItemViewModel] = [] | ||
|
||
var selectedFileName: String { | ||
let fileSize = Int64(fileUrl?.fileSize ?? 0) | ||
var fileName = fileUrl?.lastPathComponent ?? "" | ||
let formattedFileSize = ByteCountFormatter.megaBytesFormatter.string(fromByteCount: fileSize) | ||
if fileSize > 0 { fileName += "\n\(formattedFileSize)" } | ||
return fileName | ||
} | ||
|
||
private let uploadService: UploadService | ||
|
||
init(uploadService: UploadService = .init()) { | ||
self.uploadService = uploadService | ||
} | ||
} | ||
|
||
extension FormUploadsViewModel { | ||
func uploadForm() { | ||
Task { | ||
do { | ||
let multipartFormData = try createMultipartFormData() | ||
let uploadItem = try await uploadService.uploadFormData(multipartFormData) | ||
|
||
uploadItemViewModels.append(UploadItemViewModel( | ||
item: uploadItem, | ||
uploadService: uploadService | ||
)) | ||
|
||
username = "" | ||
fileUrl = nil | ||
} catch { | ||
os_log("❌ FormUploadsViewModel failed to upload form with error: \(error.localizedDescription)") | ||
self.error = error | ||
self.isErrorAlertPresented = true | ||
} | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Prepare multipartForm data | ||
private extension FormUploadsViewModel { | ||
func createMultipartFormData() throws -> MultipartFormData { | ||
let multipartFormData = MultipartFormData() | ||
multipartFormData.append(Data(username.utf8), name: "username-textfield") | ||
if let fileUrl { | ||
try multipartFormData.append(from: fileUrl, name: "attachment") | ||
} | ||
return multipartFormData | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/UploadItem.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// | ||
// UploadItem.swift | ||
// NetworkingSampleApp | ||
// | ||
// Created by Tony Ngo on 12.06.2023. | ||
// | ||
|
||
import Foundation | ||
|
||
struct UploadItem: Hashable { | ||
let id: String | ||
let fileName: String | ||
} |
75 changes: 75 additions & 0 deletions
75
NetworkingSampleApp/NetworkingSampleApp/Scenes/Upload/UploadItemView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// | ||
// UploadItemView.swift | ||
// NetworkingSampleApp | ||
// | ||
// Created by Tony Ngo on 12.06.2023. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct UploadItemView: View { | ||
@ObservedObject var viewModel: UploadItemViewModel | ||
|
||
var body: some View { | ||
VStack(alignment: .leading) { | ||
HStack { | ||
HStack { | ||
Text(viewModel.fileName) | ||
.font(.subheadline) | ||
Text(viewModel.stateTitle) | ||
.font(.footnote) | ||
.foregroundColor(.gray) | ||
} | ||
|
||
Spacer() | ||
|
||
if !viewModel.isCancelled && !viewModel.isRetryable && !viewModel.isCompleted { | ||
HStack { | ||
button( | ||
symbol: viewModel.isPaused ? "play" : "pause", | ||
color: .blue, | ||
action: { viewModel.isPaused ? viewModel.resume() : viewModel.pause() } | ||
) | ||
|
||
button( | ||
symbol: "x", | ||
color: .red, | ||
action: { viewModel.cancel() } | ||
) | ||
} | ||
} else if viewModel.isRetryable { | ||
button( | ||
symbol: "repeat", | ||
color: .blue, | ||
action: { viewModel.retry() } | ||
) | ||
} | ||
} | ||
|
||
if !viewModel.isCancelled && !viewModel.isRetryable { | ||
ProgressView(value: viewModel.progress, total: viewModel.totalProgress) | ||
.progressViewStyle(.linear) | ||
} | ||
} | ||
.animation(.easeOut(duration: 0.3), value: viewModel.progress) | ||
.padding(.vertical, 8) | ||
.task { await viewModel.observeProgress() } | ||
} | ||
} | ||
|
||
private extension UploadItemView { | ||
func button(symbol: String, color: Color, action: @escaping () -> Void) -> some View { | ||
Button( | ||
action: action, | ||
label: { | ||
Image(systemName: symbol) | ||
.symbolVariant(.circle.fill) | ||
.font(.title2) | ||
.symbolRenderingMode(.hierarchical) | ||
.foregroundStyle(color) | ||
} | ||
) | ||
.buttonStyle(.plain) | ||
.contentShape(Circle()) | ||
} | ||
} |
Oops, something went wrong.