Advanced Networking with URLSession

Sep 15 2022 · Swift 5.6, iOS 15, Xcode 13.4.1

Part 1: Upload Data, Background Downloads & WebSockets

03. Upload Data

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 02. Install Vapor Next episode: 04. Background Downloads

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Notes: 03. Upload Data

URLSession - Apple Developer

Transcript: 03. Upload Data

So far you’ve download data via URLSession tasks and its asynchronous transfer methods. But what if you need to upload data to a server instead?

Uploads are handled through the upload task. These uploads are made using HTTP requests that require a request body by way of a POST or a PUT HTTP method.

When you create an upload task, you create a URLRequest and then add any additional headers describing the content of the actual upload.

One thing to note is that you can also perform upload tasks in the background. You’ll be learning more about background transfers in the next episode.

To demonstrate uploading data, open the Starter project for this episode, and build and run the code.

This project is the same one you’ve been working on, but reverts back to using the regular SongDownloader class as well as adds a rating view you can submit to a server, which will be data data you upload.

You’ll create a Vapor project to create a local web server that can receive the data you upload from your app.

Open a new Terminal window and type the following:

cd ~/desktop

This switches you to the desktop where you next create the Vapor project with the following command:

vapor new UploadServer

Type y when asked if you want to use Fluent. Then type 3 to select SQLite. And, finally, type n when asked if you want to use Leaf.

At this point you have a new Vapor project set up wth Fluent, SQLite, but not Leaf.

If you’re interested in knowing exactly what all of these do then check out the Vapor documentation or other resources on our site. We won’t be going over them in detail as this course’s focus is URLSession.

Type the following into your Terminal to switch into the directory for your new Vapor project:

cd UploadServer

Then the following to open your project in Xcode:

vapor xcode -y

Welcome to your Vapor project. You need to create a route for the upload request. So in the Project Navigator locate and open the routes.swift file.

Since you’ll upload JSON, start by creating a struct that will represent the uplaoded data. Add the following code:

struct MusicItemRating: Content {
  let id: String
  let artistName: String
  let trackName: String
  let rating: Int
}

Note how it implements the Content protocol as required by Vapor.

Now, in the routes method, add the following:

app.post("upload") { req -> HTTPResponseStatus in
  let item = try req.content.decode(MusicItemRating.self)
        
  print("ID: \(item.id)")
  print("Artist Name: \(item.artistName)")
  print("Track Name: \(item.trackName)")
  print("Rating: \(item.rating)")
        
  return .ok
}

Once the JSON us uploaded you convert it to a MusicItemRating object. From there, you simply print out its info to the console in order to visualize, and verify, that your is working. Finally, you return ok to indicate the request was successful.

Back in the iOS project, create a new file called RatingUploader.swift inside the Model folder.

Replace the import with the following:

import SwiftUI

Then, add the following code for the class itself:

class RatingUploader: ObservableObject {
  private let session: URLSession
  private let sessionConfiguration: URLSessionConfiguration
  
  init() {
    self.sessionConfiguration = URLSessionConfiguration.default
    self.session = URLSession(configuration: sessionConfiguration)
  }
  
  func submit(rating: Int, for musicItem: MusicItem) async throws {
    
  }
}

Similar to the song downloader, you create a couple of properties for URLSession and URLSessionConfiguration, and initialize them in the init method. You also declare a function that you’ll use to submit your rating.

The function takes two parameters. One for the rating value and one for the MusicItem to rate. This is an asynchronous function that can throw errors, so it’s marked accordingly.

Inside the function, add the following code:

guard let uploadURL = URL(string: "http://localhost:8080/upload") else {
  throw RatingUploadError.failedToCreateUploadURL
}

This checks that you can construct a valid URL to your local server or throws an error otherwise. Xcode will give you an error because you haven’t yet defined the error you’re throwing. For that, add this code within the class declaration:

enum RatingUploadError: Error {
  case failedToCreateUploadURL
}

Which declares an enum for your custom errors to throw.

Next, add the following code that creates the data you need to upload to the server based on a json property that you’ll soon create to hold your JSON data as a String:

guard let uploadData = json.data(using: .utf8) else {
  throw RatingUploadError.failedToCreateUploadData
}

And add the new case to your error enum:

case failedToCreateUploadData

Back in the upload method, between both guard statements, add the following code:

var request = URLRequest(url: uploadURL)
request.httpMethod = "Post"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

This code creates an object you haven’t seen before; URLRequest.

Whereas all the network operations you’ve executed have been done by passing a URL, for uploading data, where you need to change the HTTP method or set a body and headers, a URLRequest is needed in order to encapsulate that additional information.

After creating your request you set the HTTP method to POST, and the value of the Content-Type header field to application/json, in order to tell the server what data it’s receiving. Add the following after this code:

let json = """
{
  "id": \(musicItem.id),
  "artistName": \(musicItem.artistName),
  "trackName": \(musicItem.trackName),
  "rating": \(rating)
}
"""

This creates a small JSON string from your MusicItem that will server as the payload, or data, to upload to your server.

You’ll next do the actual upload operation. But before that, add an additional case to your errors enum for when the upload failes:

case invalidResponse

At the bottom of the method add this code next:

let (_, response) = try await session.upload(for: request, from: uploadData)

Similar to the async download method on URLSession, you use upload with a URLRequest and a Data object. You try and await the call, and store the results in properties, the first which you discard as it contains any data returned by the server that is not needed in this scenario.

To check whether the upload succeeded, add this code next:

guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200
else {
  throw RatingUploadError.invalidResponse
}

Same as before, you check for a response code of 200 or throw an error.

This wraps up your upload code. Open SongDetailView.swift in order to update the UI to allow for uploads to your server. Add two properties:

@ObservedObject private var uploader: RatingUploader = RatingUploader()

@MainActor @State private var showRatingSubmitFailedAlert: Bool = false

The first property creates a RatingUpload to use to perform the uploads, and the second property is a boolean that will show an alert to the user if the upload fails.

To do the upload when the Submit button is tapped, add this code:

private func submitRatingTapped() async {
  do {
    try await uploader.submit(rating: ratingView.rating, for: musicItem)

    ratingSubmitted = true
  } catch {
    showRatingSubmitFailedAlert = true
  }
}

This is an asynchronous method, to leverage concurrency, that attempts to use the uploader to submit your song’s rating.

If the request succeeds you set the ratingSubmitted property to true, otherwise you set showRatingSubmitFailedAlert to true. Next, for the actual alerts, add these modifiers before the sheet modifier:

.alert("Failed to submit your rating", isPresented: $showRatingSubmitFailedAlert) {
  Button(role: .cancel, action: {
    showRatingSubmitFailedAlert = false
  }, label: {
    Text("Dismiss")
  })
}
.alert("Rating submitted successfully", isPresented: $ratingSubmitted) {
  Button(role: .cancel, action: {
    ratingSubmitted = false
  }, label: {
    Text("Dismiss")
  })
}

Nothing too complex as far as SwiftUI goes, just a couple of alerts to give feedback to your users. Finally, update the VStack that contains your RatingView:

VStack(spacing: 16) {
  ratingView
    .disabled(ratingSubmitted)
            
  Button {
    Task {
      await submitRatingTapped()
    }
  } label: {
    Text("Submit")
  }
  .disabled(ratingSubmitted)
}

If the rating has already been submitted you disable the rating view and button. As for the button itself, you now call submitRatingTapped() within a task, so it’s performed asynchronously without blocking the UI.

Time to give everything a try. Build and run your iOS project, and tap the submit button.

You get an error, that’s right, the server isn’t running! Switch over to the Xcode project for the Vapor server and run it on the My Mac target. Then, back in your app, tap submit once again.

Hmmm, another error, but this time the Vapor project’s Xcode console gives us some information. “Unsupported media type”, which may indicate that our upload request or body was not formatted properly. Back in the Xcode project for your app, open RatingUploader.swift and look at how you construct the request.

Aha! The JSON string seems to have some minor errors. Update the snippet that constructs the JSON String with the following:

let json = “””
{
  “id”: \(musicItem.id),
  “artistName”: “\(musicItem.artistName)”,
  “trackName”: “\(musicItem.trackName)”,
  “rating”: \(rating)
}
“””

Run the app one more time and tap the Submit button. Success! The rating gets submitted, something you can verify by going to the server’s Xcode window and looking at the console.

Great, great work! You’ve gone from just downloading data to also being able to upload it.

URLSession’s asynchronous upload method is very easy and clean to call. The main difference from a download is how you used a URLRequest in order to provide some additional information about the upload request as well as the data to upload.

Great work once more.

In the next episode you’ll learn how to do background downloads, so users don’t have to keep the app open until everything finishes. I’ll see ya in a bit! :)