SwiftUI: A technology every iOS developer has badly been waiting for
This year at WWDC, a handful of major updates were brought to the Apple developer community. The newest operating systems for all platforms were released, along with some cool new libraries that will ease the developer’s everyday tasks.
SwiftUI is the coolest technology ever since the release of the Swift programming language, a real superhero in terms of UI development in iOS. Although the stable version will only be released in September, the team did a pretty good job writing this API so that it is available for all platforms, including baby iPadOS.
They literally got rid of some considerably large amount of code, enabling developers to successfully separate views from view controllers. In newly created projects, VCs aren’t even generated anymore. Also, in order to render UI, Storyboards are not required anymore. Through Xcode itself, developers can visualize previews of the interface they’re currently developing.
In this article, we will work our way through developing our first app using SwiftUI. Be aware of every step in order to follow the flow. Let’s get this started!
Requirements
For this project, you’ll need Xcode 11 in order to be able to use SwiftUI. Although Xcode Previews and many other shortcuts are available upon installing macOS Catalina too, you can complete the tutorial without it just fine.
Application use case & design
The main idea of this app is to “check” all the places you visited with a little description of what you liked the most. It will contain a list of the destinations, the details being accessed from the row itself. Also, you can add a new destination from a modal form accessible from a navbar button.
Implementation
We start by creating an iOS project in Xcode 11. “Use SwiftUI” checkbox is automatically set.
I just called it “FirstSwiftUIApp”.
Now, the project consists of the default generated SwiftUI View (ContentView.swift) and a new class called SceneDelegate, which is a part of the plain old AppDelegate that now configures application state and multi-window support.
Multi-window support is happening
Next time you create a new Xcode project you’ll see your AppDelegate has split in two: AppDelegate.swift and SceneDelegate.swift. This is a result of the new multi-window support that landed with iPadOS and effectively splits the work of the app delegate in two.
From iOS 13 onwards, your app delegate should:
Set up any data that you need for the duration of the app.
Respond to any events that focus on the app, such as a file being shared with you.
Register for external services, such as push notifications.
Configure your initial scenes.
In contrast, scene delegates are there to handle one instance of your app’s user interface. So, if the user has created two windows showing your app, you have two scenes, both backed by the same app delegate.
Keep in mind that these scenes are designed to work independently from each other. So, your application no longer moves to the background, but instead, individual scenes do — the user might move one to the background while keeping another open. — Paul Hudson, HackingWithSwift
That being said, let’s create the model of this application:
import Foundationstruct Destination: Hashable {let city: Stringlet country: String}
A list with default destinations will be presented as a @State variable (it is directly linked to the UI changes that can be made to the list and you cannot mutate it because it belongs to ‘Self’).
The following list will be presented to the UI. But how do we want the rows to look like? One option I really like looks like this:
The design of this cell is implemented in ContentCellView.swift:
import SwiftUIimport UIKitstruct ContentCellView : View {@State var destination : Destination@State var image: Image?var body: some View {NavigationButton(destination: ContentDetailView(destination: destination)) {HStack {if ((UIImage(named: destination.city)) != nil) {Image(destination.city).clipShape(Circle()).overlay(Circle().stroke(Color.white, lineWidth: 1)).shadow(radius: 10)} else {image!.frame(width: 120, height: 120).scaledToFit().clipShape(Circle()).overlay(Circle().stroke(Color.white, lineWidth: 1)).shadow(radius: 10)}Spacer().frame(width: 90, height: 20, alignment: .center).relativeWidth(20)VStack(alignment: .trailing) {Text(destination.city.capitalized).font(.headline).bold().color(.init(.sRGB, red: 102/255, green: 76/255, blue: 76/255, opacity: 1.0))Text(“Read all about it”).font(.subheadline).color(.init(.sRGB, red: 127/255, green: 102/255, blue: 102/255, opacity: 1.0))}}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)}}}
The main view structure will be an HStack containing an Image, a spacing structure, and a VStack containing the destination name and the identical-to-all subtitle ‘Read all about it’, with proper UI customizations (font, color, etc.).
In the code above, the variable ‘destination’ is used to populate the cell with existing and upcoming data, with the exception of the image of a new destination that would be retrieved from the Photo library. In that case, the variable ‘image’ will be used.
The NavigationButton presented in the top actually embeds the parent view, so this is what ‘body’ returns from this struct. When clicking it, the ContentDetailView.swift structure body var will be returned:
var body: some View {VStack(alignment: .center) {Image(destination.country+”s”).resizable().aspectRatio(contentMode: .fit).offset(y: -100).padding(.bottom, -70).padding(.top, 40)VStack(alignment: .center) {Text(“Welcome to \(destination.city.capitalized)!”).font(.title).color(.gray).frame(width: 360, height: 50)Spacer().frame(width: 20, height: 20)List {Text(“Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry’s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry’s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book”).lineLimit(nil)}.frame(minWidth: 0, idealWidth: UIScreen.main.bounds.size.width-20, maxWidth: UIScreen.main.bounds.size.width — 10, minHeight: 400, idealHeight: 400, maxHeight: 400, alignment: .center)}}}
The image presented at the top of the screen would be chosen from the already existing set of country flag images (you can add destinations only from pre-existing country values — for demo purposes). The description is filled with “Lorem ipsum” text because we only wanted to demonstrate the way in which we can currently set a scrolling TextField: by embedding it into a List and setting a nil line limit.
The list in the main screen is implemented as below:
var body: some View {VStack {NavigationView {List {ForEach(self.destinations.identified(by: \.self)) { destination inContentCellView(destination: destination, image: self.image)}}.navigationBarTitle(Text(“Europe’s Best”))
}}
The destinations array is used for building list rows, with ‘self’ being an identity criterion. This is achieved in code by conforming the model struct above to Hashable.
We want to be able to add a new destination into our checklist (the Add button in the top right corner), so we have to be sure key features are added.
This button will open a popup window (widely known as modal) that will enable users to add a new European city. The city name will be provided via a text field and the country that belongs to will be selected from a picker. We will use an image picker to add a picture from the Photo library.
You remember what a @State variable is. This view contains the states below:
@State var isPresented = false@State var image: Image? = nil@State var showImagePicker: Bool = false@State var canSave: Bool = false@State var selectedCountry = 3@State var textFieldString: String
- the first one is responsible for notifying if the modal is up or not
- the second one is responsible for knowing if an image was chosen or not
- the third one is responsible for notifying if the image picker is presented or not
- the fourth one is responsible for enabling/disabling the button that adds the new destination into the list
- the fifth one will store the country name selected from the picker
- the last one will store the introduced text in the modal text field, in order to be saved in a new Destination instance
In order to add an image picker to this project, we need to know some concepts regarding integrating UIKit into SwiftUI projects.
The main link between a SwiftUI view and the old UIViewController is the protocol ‘UIViewControllerRepresentable’. It is responsible for instantiating the UIViewController desired object via makeUIViewController(context:) method and also for updating it whenever changes at this level occur, through updateUIViewController(_:context:) method.
But, what is UIViewController good for if you can’t assign delegates, and eventually set data sources? That’s when Coordinator comes into action. It is an inner class of the struct that is the mediator between the SwiftUI View and the UIViewController that needs to conform to NSObject and the delegates and data sources of the VC. The implementation of ImagePicker.swift confirms what the documentation requires:
import SwiftUIstruct ImagePicker: UIViewControllerRepresentable {@Binding var isShown: Bool@Binding var image: Image?class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {@Binding var isShown: Bool@Binding var image: Image?init(isShown: Binding<Bool>, image: Binding<Image?>) {$isShown = isShown$image = image}func imagePickerController(_ picker: UIImagePickerController,didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImageimage = Image(uiImage: uiImage)isShown = false}func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {isShown = false}}func makeCoordinator() -> Coordinator {return Coordinator(isShown: $isShown, image: $image)}func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {let picker = UIImagePickerController()picker.delegate = context.coordinatorreturn picker}func updateUIViewController(_ uiViewController: UIImagePickerController,context: UIViewControllerRepresentableContext<ImagePicker>) {}}
Now, what does the @Binding annotation do? Well, let’s associate it with the reference parameters from a plain old function. That means if we modify the value of it from the function, the change will be reflected in the outer scope as well. That’s what bindings actually are, it emphasizes the need for other classes/objects to know the value of a member that they also possess and it is critical for the flow of the entire application. In our case, ‘isShown’ is related to the appearance of the modal (the main screen needs to know the status too) and ‘image’ is used to transfer the finally chosen picture from the modal to the main list.
The Coordinator then conforms to UIImagePickerControllerDelegate and implements the methods that pick the image and save it as an Image object and also notifies when the picker is canceled (to properly set the binding members). It should also implement the method that returns the coordinator, as seen above.
Getting back to ContentView.swift, we are going to implement the modal view as a variable to the already existing struct. This time, the main view will be a ZStack (not so deductible comparing to HStack and VStack), because we want the image picker to appear on top of the modal (by covering it). Below, we can see the modal body, the first element of the ZStack:
VStack(alignment: .leading) {FormLabelTextView(label: “Destination City”, placeholder: “Fill in the city name”, textString: $textFieldString)VStack(alignment: .leading) {Text(“Country”).font(.headline)Picker(selection: $selectedCountry, label: Text(“Country”)) {ForEach(0 ..< countries.count) {Text(self.countries[$0]).frame(width: 200, height: 30, alignment: .center)}}.padding(.all)}.padding(.horizontal, 15)VStack(alignment: .leading) {Text(“Cover Image”).font(.headline)Spacer().frame(width: 20, height: 20)HStack {image?.resizable().frame(width: 20, height: 20)Button(action: {self.showImagePicker.toggle()}, label: {Text(“Pick image”)}).disabled(showImagePicker)}}.padding(.horizontal, 15)
We should not forget the Save button that creates a new Destination struct instance and adds it to the list:
Button(action: {guard self.isModalValid() else {print(“Modal is not valid!”)return}print(“Save button pressed”)self.showImagePicker = falseself.isPresented = falseself.canSave = falselet destination = Destination(city: self.textFieldString, country: self.countries[self.selectedCountry].lowercased())self.destinations.append(destination)self.textFieldString = “”}) {HStack {Spacer()Text(“Save”).font(.headline).color(Color.red)Spacer()}}.padding(.vertical, 10.0).cornerRadius(4.0).padding(.horizontal, 50.0).disabled(canSave)
The modal should look like this:
To close it, you either drag it down or press the save button when all data is validated.
To present it, we need to add the following code at the end of the body var:
.navigationBarItems(trailing: Button(action: {self.isPresented = trueself.image = nil}, label: {Text(“Add”)})).presentation(isPresented ? Modal(modal, onDismiss: {self.isPresented.toggle()self.image = nil}) : nil)
The final product should look awesome and it is only consisting of a couple of files and lines of code!
However, it does contain some flaws:
- you have to do some movement in order to be able to open the modal again, after adding a new item to the list (I have heard several people complaining about this fact). The button completely shuts down, but a workaround would be to scroll to the bottom of the list or to enter the details of one of the items and then come back and try again.
- you cannot change the cover image you have selected after you entered values for all fields. Unfortunately, button touch events are linked to all controls’ handlers, so the buttons are actually all clicked at the same time. This way, when all fields are validated, the save button will close the modal and add a new item to the list, even though you decided to change the cover image.
This article briefly demonstrates some of the wide variety of UI controls and functionalities eased by the new framework. You can find this project on Github. Feel free to add some more features, or use it as a template to further improve your skills in developing engaging UIs.
Zipper Studios is a group of passionate engineers helping startups and well-established companies build their mobile products. Our clients are leaders in the fields of health and fitness, AI, and Machine Learning. We love to talk to likeminded people who want to innovate in the world of mobile so drop us a line here.