iOS comes with some fantastic functionality for handling networking, and in particular the URLSession
class makes it surprisingly easy to send and receive data. If we combine that with Codable
to convert Swift objects to and from JSON, we can use a new URLRequest
struct to configure exactly how data should be sent, we can accomplish great things in about 20 lines of code.
First, let’s create a method we can call from our Place Order button – add this to CheckoutView
:
func placeOrder() async {
}
Just like when we were downloading data using URLSession
, uploading is also done asynchronously.
Now modify the Place Order button to this:
Button("Place Order", action: placeOrder)
.padding()
That code won’t work, and Swift will be fairly clear why: it calls an asynchronous function from a function that does not support concurrency. What it means is that our button expects to be able to run its action immediately, and doesn’t understand how to wait for something – even if we wrote await placeOrder()
it still wouldn’t work, because the button doesn’t want to wait.
Previously I mentioned that onAppear()
didn’t work with these asynchronous functions, and we needed to use the task()
modifier instead. That isn’t an option here because we’re executing an action rather than just attaching modifiers, but Swift provides an alternative: we can create a new task out of thin air, and just like the task()
modifier this will run any kind of asynchronous code we want.
In fact, all it takes is placing our await
call inside a task, like this:
Button("Place Order") {
Task {
await placeOrder()
}
}
And now we’re all set – that code will call placeOrder()
asynchronously just fine. Of course, that function doesn’t actually do anything just yet, so let’s fix that now.
Inside placeOrder()
we need to do three things:
order
object into some JSON data that can be sent.The first of those is straightforward, so let’s get it out of the way. We'll use JSONEncoder
to archive our order by adding this code to placeOrder()
:
guard let encoded = try? JSONEncoder().encode(order) else {
print("Failed to encode order")
return
}
That code won't work yet because the Order
class doesn't conform to the Codable
protocol. That's an easy change, though – modify its class definition to this:
class Order: Codable {
The second step means using a new type called URLRequest
, which is like a URL
except it gives us options to add extra information such as the type of request, user data, and more.
We need to attach the data in a very specific way so that the server can process it correctly, which means we need to provide two extra pieces of data beyond just our order:
So, the next code for placeOrder()
will be to create a URLRequest
object, then configure it to send JSON data using a HTTP POST request. We can then use that to upload our data using URLSession
, and handle whatever comes back.
Of course, the real question is where to send our request, and I don’t think you really want to set up your own web server in order to follow this tutorial. So, instead we’re going to use a really helpful website called https://reqres.in – it lets us send any data we want, and will automatically send it back. This is a great way of prototyping network code, because you’ll get real data back from whatever you send.
Add this code to placeOrder()
now:
let url = URL(string: "https://reqres.in/api/cupcakes")!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
That first line contains a force unwrap for the URL(string:)
initializer, which means “this returns an optional URL, but please force it to be non-optional.” Creating URLs from strings might fail because you inserted some gibberish, but here I hand-typed the URL so I can see it’s always going to be correct – there are no string interpolations in there that might cause problems.
At this point we’re all set to make our network request, which we’ll do using a new method called URLSession.shared.upload()
and the URL request we just made.
So, go ahead and add this to placeOrder()
:
do {
let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
// handle the result
} catch {
print("Checkout failed: \(error.localizedDescription)")
}
Now for the important work: we need to read the result of our request for times when everything has worked correctly. If something went wrong – perhaps because there was no internet connection – then our catch
block will be run, so we don’t have to worry about that here.
Because we’re using the ReqRes.in, we’ll actually get back the same order we sent, which means we can use JSONDecoder
to convert that back from JSON to an object.
To confirm everything worked correctly we’re going to show an alert containing some details of our order, but we’re going to use the decoded order we got back from ReqRes.in. Yes, this ought to be identical to the one we sent, so if it isn’t it means we made a mistake in our coding.
Showing an alert requires properties to store the message and whether it’s visible or not, so please add these two new properties to CheckoutView
now:
@State private var confirmationMessage = ""
@State private var showingConfirmation = false
We also need to attach an alert()
modifier to watch that Boolean, and show an alert as soon as it's true. Add this modifier below the navigation title modifiers in CheckoutView
:
.alert("Thank you!", isPresented: $showingConfirmation) {
Button("OK") { }
} message: {
Text(confirmationMessage)
}
And now we can finish off our networking code: we’ll decode the data that came back, use it to set our confirmation message property, then set showingConfirmation
to true so the alert appears. If the decoding fails – if the server sent back something that wasn’t an order for some reason – we’ll just print an error message.
Add this final code to placeOrder()
, replacing the // handle the result
comment:
let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
confirmationMessage = "Your order for \(decodedOrder.quantity)x \(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way!"
showingConfirmation = true
If you try running it now you should be able to select the exact cakes you want, enter your delivery information, then press Place Order to see an alert appear – it's all working nicely!
We're not quite done, though, because right now our networking has a small but invisible problem. To see what it is I want to introduce you to a tiny bit of debugging with Xcode: we're going to pause our app, so we can inspect a particular value.
First, click on the line number next to the `let url = URL…" line. A blue arrow should appear there, which is Xcode's way of saying we've placed a breakpoint there. This tells Xcode to pause execution when that line is reached, so we can poke around in all our data.
Now go ahead and run the app again, enter some shipping data, then place the order. All being well your app should pause, Xcode should come to the front, and that line of code should highlighted because it's about to be run.
All being well, you should see Xcode's debug console in the bottom right of the Xcode window – it's normally where all Apple's internal log messages appear, but right now it should say "(lldb)". LLDB is the name of Xcode's debugger, and we can run commands here to explore our data.
I'd like you to run this command there: p String(decoding: encoded, as: UTF8.self)
. That converts our encoded data back to a string, and prints it out. You should see it has lots of underscored variable names along with the observation registrar provided to us by the @Observable
macro.
Our code doesn't actually care about this, because we're sending all the properties up with the underscored names, the ReqRes.in server sends them back to us with the same names, and we decoded them back to the underscored properties. But when you're working with a real server these names matter – you need to send the actual names up, rather than the weird versions produced by the @Observable
macro.
This means we need to create some custom coding keys for the Order
class. This is rather tedious, particularly for classes like this one where we want to save and load quite a few properties, but it's the best way to ensure our networking is done properly.
So, open up the Order
class and add this nested enum there:
enum CodingKeys: String, CodingKey {
case _type = "type"
case _quantity = "quantity"
case _specialRequestEnabled = "specialRequestEnabled"
case _extraFrosting = "extraFrosting"
case _addSprinkles = "addSprinkles"
case _name = "name"
case _city = "city"
case _streetAddress = "streetAddress"
case _zip = "zip"
}
If you run the code again you should find you can run the p command again by pressing the up cursor key and return, and this time the data being sent and received is much cleaner.
With that final code in place our networking code is complete, and in fact our app is complete too.
We’re done! Well, I’m done – you still have some challenges to complete!
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's all new Paywall Editor allow you to remotely configure your paywall view without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.