Streaming is available in most browsers,
and in the Developer app.
-
Use Xcode for server-side development
Discover how you can create, build, and deploy a Swift server app alongside your pre-existing Xcode projects within the same workspace. We'll show you how to create your own local app and test endpoints using Xcode, and explore how you can structure and share code between server and client apps to ease your development process
Resources
Related Videos
WWDC23
WWDC22
WWDC21
WWDC20
WWDC19
-
Download
♪ ♪ Hello! My name is Tom, and I'm part of the Swift team at Apple. Today I'd like to share what it takes to extend an iOS application into the cloud. Many of our applications start out focusing on a single device, usually the iPhone. As usage grows, we find ourselves wanting to bring it to additional devices like the Mac, the watch, or other Apple platforms and devices. Xcode helps us organize and build our application for these platforms. We can share code using packages while embracing the unique aspects of each device in the platform specific application code. As systems continue to grow and evolve, applications often need to complement the client application with a server component. These server components enable the client application to extend their functionality into the cloud. For example, offload tasks that can be done in the background, offload tasks that are computational heavy, or tasks that require access to data that is not available on the device. Often, server components need to be built using different tools and methodologies from their client counterparts, creating duplication of effort and integration challenges. Using Swift for building server components help bridge this technology gap, providing a familiar environment across the stack. Let's see what building a server application in Swift looks like.
Server applications are modeled as Swift packages. The package defines an executable target that maps the application entry point. To make the application into a web application, we can add a dependency on a web framework that helps us structure our code and provides basic utilities like routing. In this example, we use the Vapor web framework, an open source community project popular for building web services.
As with other Swift based executables, the program's entry point is best modeled using the @main annotation. To integrate the web framework, we add the relevant bootstrap code to the main function. The Application type used in this example is provided by the Vapor web framework. With the basic bootstrapping in place, we can make our application do something useful. For example, let's add code to greet users making a request to the server. We use the web framework to define an HTTP endpoint and point it to a method that provides the greeting. Taking a step further, we add a second HTTP endpoint, this one handling an HTTP post request, and echoing the content of the request body back to the caller. Let's see this in action. Here we have our server application in Xcode. Since we're just getting started, we can run the server locally on our own machine to test things out. To run it locally, we pick the "MyServer" scheme that was generated for us by Xcode, use the “My Mac" as the destination, and hit "run".
Once the application has launched, we can use Xcode console to examine log messages emitted by the server. In this case, we can see that the server started and listening on the localhost address (127.0.0.1) port 8080. We can use this information to test our server. Let's switch over to the terminal, and make a request to the advertised server address. We use a utility called "curl" to make the request. Use our first endpoint.
And our second one. Pass in some data to echo.
Nice! Using the terminal sure was fun, but what we really want to know is how to call our server from an iOS app. Let's dig into that. Here is an example of a Swift data structure we can use to abstract the interaction with the server. We model the server APIs as async methods on our abstraction, because networking is inherently asynchronous. We use URLSession to make an asynchronous request then parse the server response and finally return it to the caller. In this case, the server response is a plain string, but in reality, it is likely to be more sophisticated. For example, the response may be encoded in JSON, in which case we can decode it using Swift's Codable system. Let's put this all together in Xcode. We are using an Xcode workspace to build and test the iOS and server applications side by side. We already have the iOS application server abstraction ready to go. Let's change the default SwiftUI ContentView to fetch the server greeting using the code we put together. First we create a state variable called serverGreeting.
Next, we bind the serverGreeting to the Text display.
Finally, we add a task to call the server API, and set the state.
With the code ready, we can run the application in the simulator. We pick the "MyApp" scheme, a simulator, and hit "run".
Oh, no! We got an error! Hmm, this seems to be some sort of a connection error. The address seems right, so we must have forgotten to start the local server. Let's switch back to Xcode, pick the server scheme, and run the server.
Now, let's restart our application, cross our fingers...
And whoo-hoo! It worked! To complete this part of the demo, let's deploy our application to the cloud. There are many cloud providers to choose from, including AWS, Google Cloud, Azure, Heroku, and many others. In this example, we will use Heroku. Heroku has a convenient git push to deploy system for small projects like this demo application. Let's switch over to the terminal to kick off a deployment. After setting up our account, and configuring our application with the Heroku service, we can git push our code to the Heroku remote.
And off it goes! Heroku uses a technology called buildpacks to compile the application remotely, then deploys the binary artifacts to an ephemeral host. Heroku swift buildpack was built by members of the Swift open source community, and it is available for all Swift on Server users. With our application deployed, we can test it using curl, as we have done with our local server. Let's test the first endpoint.
Copy the address here.
And our second one.
This time, we'll send a different payload.
Sweet, our application was successfully deployed! Before we continue, let's pause here and review the main takeaways from this part of the talk. If you're already using Swift to build iOS or macOS Applications, you could also be using it for developing the server side of the system. Xcode helps us develop and debug the different components of the system, both the client and the server, all in one Workspace. And finally, you have a choice of cloud providers for deploying Swift based server applications. Additional information about deploying to these cloud platforms can be found on the Swift Server documentation at swift.org. Now that we have seen a basic setup, let's look at a more real example– Food Truck! You've probably seen this application used in many of our sessions. Let's peek under the hood and see how data is managed. Hmm, looks like the donut list is hard coded. This means that users of the application may see a different menu of donuts from what is actually available. While this may be useful for a small Food Truck operation, one that can make any kind of donut on the spot, we want to build a donuts empire where the menu is centralized and the trucks are all about customer service. Let's design how our centralized Food Truck system may look like.
We are starting out with our iOS app, with its in-memory storage. To centralize the menu, we can extract the storage from the iOS app and move it to the server. This will allow all users of the app to share the same storage, and thus, the same donuts menu. Similar to the example in the first part of the talk, our server will expose an HTTP based API. The iOS app will use an abstraction for working with these APIs, then tie them back together to the presentation tier, in this example, SwiftUI. Our design is complete. Time to write some sweet code. You can follow along by downloading the Food Truck sample app from the developer resource kit. We start building our Server with an application skeleton, then define an HTTP endpoint for the "donuts" web API, and point it to the "listDonuts" method on our server abstraction. You may have noticed that the API returns a Response of type Donuts and that Response.Donuts conforms to a protocol called Content. The Content protocol is defined by the web framework and helps us encode the response as JSON on the wire. You may have also noticed that the API includes an array of a mysterious Model.Donut, which we have yet to define So here it is, our data model in all of its glory: Donut, Dough, Glaze, and Topping. One interesting point to make here is that we copied the definition of this model from our Food Truck iOS app, as we need the data models of the server and the client to roughly align. Another interesting point is the conformance to the Encodable protocol. This is required so that our server can encode the model objects as JSON over the wire. With the data model and basic APIs in place, we can expand our logic to include a storage abstraction. The storage will provide the Application with the list of available donuts. At this point, we should have a fully functional server. But wait! Our donuts menu is empty! Where should we get the our centralized menu from? Storage is always an interesting topic when designing server side applications. There are several strategies to choose from, depending on the use case. If the application data is static or changes very slowly and manually, files on disk may provide a good enough solution. For user-centric data or global datasets, iCloud provides a set of APIs that you can use directly from the iOS application, without deploying a dedicated server. When dealing with dynamic or transactional data, databases provide an excellent solution. There is a variety of database technologies available for server-side applications. Each technology is designed for specific performance, data consistency, and data modeling needs. Over the years, the Swift open source community developed database drivers that help interact natively with most databases technologies. Partial list includes Postgres, MySQL, MongoDB, Redis, DynamoDB, and many others. For the purposes of simplifying this demo, we will only demonstrate a static file storage strategy, but you can learn more about using databases on the Swift Server documentation at swift.org. Since we are using a static file storage strategy, we start off by creating a JSON file that captures the donut menu. After creating this file, we can make it accessible to the application using SwiftPM's resources support. With that in place, it is time to make our storage abstraction more sophisticated. Namely, we add a "load" method. This method finds the resource file path using SwiftPM's generated resource accessor, then uses FileManager APIs to load the content of the file into memory. Finally, we use JSONDecoder to decode the JSON content into the server application data model. One interesting change is that Storage is now defined as an actor. We chose to use an actor because Storage now has a mutable "donuts" variable, and which the "load" and "listDonuts" methods might access concurrently. Actors, which were first introduced in Swift 5.5, help us avoid data races and deal with shared mutable state in a safe but easy way. Prior to the introduction of actors, we would have needed to remember and add synchronization blocks when accessing mutable state using APIs such as Locks or Queues. With the storage updates done, we can tie it all together. We add a "bootstrap" method to our server abstraction and load the storage from there. Then we wire up the bootstrap to the executables entry point. Note that since storage is now an actor, we access it in an async context. Our server is ready. Let's switch over to the client side. We start by adding a Server abstraction that will help us encapsulate the server APIs. We use URLSession to make the HTTP request and a JSONDecoder to decode the server response and transform it from JSON into our iOS application model. At this point, we can remove the hard coded menu and replace it with an asynchronous fetch from the server. Finally, we make the call to the server from the ContentView load task. Time to test! This time, let's not forget to start the server. We'll select the "FoodTruckServer" scheme here. Hit run.
And with the application running, let's jump on the terminal and see that we can access the APIs.
Copy the address again.
This time, we're going to use a utility called jq to print the JSON output more nicely. This looks pretty good.
All right, time to test with our App.
Switch to Xcode. Pick the Food Truck scheme here. Simulator. And run it.
And there we have it, the three donuts from our centralized menu. We can cross reference that with what we see from the server. Let's switch back to the terminal. To make the comparison easy, we will use jq to query just the name of the donuts.
Deep space, Chocolate 2, Coffee Caramel– exactly what we expected. That was amazing! But we can do even better. As it stands, our server and client applications both have identical copies of the data model code. We can avoid repetition and make serialization safer, by sharing the model across the iOS and server applications. Let's review how to set this up at a high level. First, we create another package for a library named "Shared" and add it to the Xcode workspace. We can then move the data model code to the Shared package, add Shared as a dependency of the server application, and as a dependency of the iOS application, using the Target Frameworks and Libraries settings. At which point, we can refactor our client code to use the shared model and do the same to the server code.
Things look much nicer now. Before we conclude, here are some ideas of where we can take the application next. To take full advantage of the fact that we have a centralized server, we are likely to want and define APIs for adding, editing, or deleting donuts from the menu. This will require that we move our storage from a static file to a database. With a database in place, we can also implement buying and ordering APIs. Such APIs can help us monetize our donut business. They also provide a signal, which we can use to implement dynamic pricing, like sales and discounts for those donuts that are less popular. The opportunities are endless. To wrap up, in this session we have seen that Swift is a general purpose language, useful for both client and server applications, sharing code between the server and client applications can reduce boilerplate and make our system serialization safer, URLSession is a key tool for interacting with the server asynchronously, and finally, Xcode is a powerful development environment for the entire system. Thank you so much for watching, and enjoy the rest of the conference.
-
-
1:36 - Simple, server package manifest
// swift-tools-version: 5.7 import PackageDescription let package = Package( name: "MyServer", platforms: [.macOS("12.0")], products: [ .executable( name: "MyServer", targets: ["MyServer"]), ], dependencies: [ .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.0.0")), ], targets: [ .executableTarget( name: "MyServer", dependencies: [ .product(name: "Vapor", package: "vapor") ]), .testTarget( name: "MyServerTests", dependencies: ["MyServer"]), ] )
-
2:00 - Simple, server code
import Vapor @main public struct MyServer { public static func main() async throws { let webapp = Application() webapp.get("greet", use: Self.greet) webapp.post("echo", use: Self.echo) try webapp.run() } static func greet(request: Request) async throws -> String { return "Hello from Swift Server" } static func echo(request: Request) async throws -> String { if let body = request.body.string { return body } return "" } }
-
3:42 - Using curl to test the local server
curl http://127.0.0.1:8080/greet; echo curl http://127.0.0.1:8080/echo --data "Hello from WWDC 2022"; echo
-
4:10 - Simple, iOS app server abstraction
import Foundation struct MyServerClient { let baseURL = URL(string: "http://127.0.0.1:8080")! func greet() async throws -> String { let url = baseURL.appendingPathComponent("greet") let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url)) guard let responseBody = String(data: data, encoding: .utf8) else { throw Errors.invalidResponseEncoding } return responseBody } enum Errors: Error { case invalidResponseEncoding } }
-
5:00 - Simple, iOS app server call SwiftUI integration
import SwiftUI struct ContentView: View { @State var serverGreeting = "" var body: some View { Text(serverGreeting) .padding() .task { do { let myServerClient = MyServerClient() self.serverGreeting = try await myServerClient.greet() } catch { self.serverGreeting = String(describing: error) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
-
9:51 - Food truck, basic server
import Foundation import Vapor @main struct FoodTruckServerBootstrap { public static func main() async throws { // initialize the server let foodTruckServer = FoodTruckServer() // initialize the web framework and configure the http routes let webapp = Application() webapp.get("donuts", use: foodTruckServer.listDonuts) try webapp.run() } } struct FoodTruckServer { private let storage = Storage() func listDonuts(request: Request) async -> Response.Donuts { let donuts = self.storage.listDonuts() return Response.Donuts(donuts: donuts) } enum Response { struct Donuts: Content { var donuts: [Model.Donut] } } } struct Storage { var donuts = [Model.Donut]() func listDonuts() -> [Model.Donut] { return self.donuts } } enum Model { struct Donut: Codable { var id: Int var name: String var date: Date var dough: Dough var glaze: Glaze? var topping: Topping? } struct Dough: Codable { var name: String var description: String var flavors: FlavorProfile } struct Glaze: Codable { var name: String var description: String var flavors: FlavorProfile } struct Topping: Codable { var name: String var description: String var flavors: FlavorProfile } public struct FlavorProfile: Codable { var salty: Int? var sweet: Int? var bitter: Int? var sour: Int? var savory: Int? var spicy: Int? } }
-
12:18 - Food truck, server donuts menu
[ { "id": 0, "name": "Deep Space", "date": "2022-04-20T00:00:00Z", "dough": { "name": "Space Strawberry", "description": "The Space Strawberry plant grows its fruit as ready-to-pick donut dough.", "flavors": { "sweet": 3, "savory": 2 } }, "glaze": { "name": "Delta Quadrant Slice", "description": "Locally sourced, wormhole-to-table slice of the delta quadrant of the galaxy. Now with less hydrogen!", "flavors": { "salty": 1, "sour": 3, "spicy": 1 } }, "topping": { "name": "Rainbow Sprinkles", "description": "Cultivated from the many naturally occurring rainbows on various ocean planets.", "flavors": { "salty": 2, "sweet": 2, "sour": 1 } } }, { "id": 1, "name": "Chocolate II", "date": "2022-04-20T00:00:00Z", "dough": { "name": "Chocolate II", "description": "When Harold Chocolate II discovered this substance in 3028, it finally unlocked the ability of interstellar travel.", "flavors": { "salty": 1, "sweet": 3, "bitter": 1, "sour": -1, "savory": 1 } }, "glaze": { "name": "Chocolate II", "description": "A thin layer of melted Chocolate II, flash frozen to fit the standard Space Donut shape. Also useful for cleaning starship engines.", "flavors": { "salty": 1, "sweet": 2, "bitter": 1, "sour": -1, "savory": 2 } }, "topping": { "name": "Chocolate II", "description": "Particles of Chocolate II moulded into a sprinkle fashion. Do not feed to space whales.", "flavors": { "salty": 1, "sweet": 2, "bitter": 1, "sour": -1, "savory": 2 } } }, { "id": 2, "name": "Coffee Caramel", "date": "2022-04-20T00:00:00Z", "dough": { "name": "Hardened Coffee", "description": "Unlike other donut sellers, our coffee dough is simply a lot of coffee compressed into an ultra dense torus.", "flavors": { "sweet": -2, "bitter": 4, "sour": 2, "spicy": 1 } }, "glaze": { "name": "Caramel", "description": "Some good old fashioned Earth caramel.", "flavors": { "salty": 2, "sweet": 3, "sour": -1, "savory": 1 } }, "topping": { "name": "Nebula Bits", "description": "Scooped up by starships traveling through a sugar nebula.", "flavors": { "sweet": 4, "spicy": 1 } } } ]
-
12:23 - Food truck, server package manifest
// swift-tools-version: 5.7 import PackageDescription let package = Package( name: "FoodTruckServer", platforms: [.macOS("12.0")], products: [ .executable( name: "FoodTruckServer", targets: ["FoodTruckServer"]), ], dependencies: [ .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.0.0")), ], targets: [ .executableTarget( name: "FoodTruckServer", dependencies: [ .product(name: "Vapor", package: "vapor") ], resources: [ .copy("menu.json") ] ), .testTarget( name: "FoodTruckServerTests", dependencies: ["FoodTruckServer"]), ] )
-
12:30 - Food truck, server with integrated storage
import Foundation import Vapor @main struct FoodTruckServerBootstrap { public static func main() async throws { // initialize the server let foodTruckServer = FoodTruckServer() try await foodTruckServer.bootstrap() // initialize the web framework and configure the http routes let webapp = Application() webapp.get("donuts", use: foodTruckServer.listDonuts) try webapp.run() } } struct FoodTruckServer { private let storage = Storage() func bootstrap() async throws { try await self.storage.load() } func listDonuts(request: Request) async -> Response.Donuts { let donuts = await self.storage.listDonuts() return Response.Donuts(donuts: donuts) } enum Response { struct Donuts: Content { var donuts: [Model.Donut] } } } actor Storage { let jsonDecoder: JSONDecoder var donuts = [Model.Donut]() init() { self.jsonDecoder = JSONDecoder() self.jsonDecoder.dateDecodingStrategy = .iso8601 } func load() throws { guard let path = Bundle.module.path(forResource: "menu", ofType: "json") else { throw Errors.menuFileNotFound } guard let data = FileManager.default.contents(atPath: path) else { throw Errors.failedLoadingMenu } self.donuts = try self.jsonDecoder.decode([Model.Donut].self, from: data) } func listDonuts() -> [Model.Donut] { return self.donuts } enum Errors: Error { case menuFileNotFound case failedLoadingMenu } } enum Model { struct Donut: Codable { var id: Int var name: String var date: Date var dough: Dough var glaze: Glaze? var topping: Topping? } struct Dough: Codable { var name: String var description: String var flavors: FlavorProfile } struct Glaze: Codable { var name: String var description: String var flavors: FlavorProfile } struct Topping: Codable { var name: String var description: String var flavors: FlavorProfile } public struct FlavorProfile: Codable { var salty: Int? var sweet: Int? var bitter: Int? var sour: Int? var savory: Int? var spicy: Int? } }
-
14:42 - Using curl and jq to test the local server
curl http://127.0.0.1:8080/donuts | jq . curl http://127.0.0.1:8080/donuts | jq '.donuts[] .name'
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.