If you've been developing for iOS, you're no doubt familiar with Storyboard, Apple's visual tool for developing the user interfaces of iOS applications. And depending on how early you got started in iOS development, you might even be familiar with Interface Builder and XIBs. Last year, Apple announced SwiftUI at the WWDC 2019. With SwiftUI, Apple aims to update the iOS development experience to the modern world.

Each iteration of the tools makes it much easier and more efficient for developers to create their apps, but with each innovative tool that comes along, developers have to start learning all over again. In this article, I aim to introduce you to SwiftUI, and like all my articles, I hope to get you jumpstarted in the shortest amount of time. Let's get started!

What Is SwiftUI

SwiftUI is a declarative programming framework for developing user interfaces for iOS and macOS applications. To see how it compares to existing framework, let's see how user interfaces are built before SwiftUI was introduced.

Prior to SwiftUI, most developers used UIKit and Storyboard (which is still supported by Apple in the current version of Xcode (version 11.3.1)). Using UIKit and Storyboard, developers drag and drop View controls onto View Controllers and connect them to outlets and actions on the View Controller classes. This model of building UIs is known as Model View Controller (MVC) and creates a clean separation between the UI and business logic.

The following shows a simple implementation in Storyboard. Here, a Button and a TextField view have been added to the View Controller in Storyboard and an outlet and an action have been created to connect to them:

import UIKit
class ViewController: UIViewController { 
   
  @IBOutlet weak var txtText: UITextField!    
  @IBAction func btnClicked(_ sender: Any) {
  }

To lay out the views, you use auto-layout to position both views in the middle of the screen (both horizontally and vertically). To customize the look-and-feel of the TextField, you can code it in the viewDidLoad() method:

override func viewDidLoad() {
    super.viewDidLoad()
        
    txtText.font = UIFont(name: "AppleSDGothicNeo-Bold", size: 20)
    txtText.layer.cornerRadius = 8.0;
    txtText.layer.masksToBounds = true
    txtText.layer.borderColor = UIColor.lightGray.cgColor
    txtText.layer.borderWidth = 2.0
    txtText.textAlignment = NSTextAlignment.center
    txtText.addConstraint(txtText.heightAnchor.constraint(equalToConstant: 50))
}

When the button is tapped, the IBAction named bnClicked (a delegate method) is fired. Using the IBOutlet named txtText, you reference the UITextField object and set its text property to a string:

@IBAction func btnClicked(_ sender: Any) {
    txtText.text = "Hello, UIKit!"
}

View as a Function of State

SwiftUI, on the other hand, is a state-driven, declarative framework. In SwiftUI, all the above could be implemented with the following statements:

import SwiftUI
struct ContentView: View {
  @State private var text = ""
  var body: some View {
    VStack {
      Button(action: { self.text = "Hello, SwiftUI!" }) {
        Text("Button").padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))
      }
      TextField("", text: $text)        
        .multilineTextAlignment(TextAlignment.center)        
        .padding(15)
        .frame(maxWidth: .infinity,  alignment: .center)
        .foregroundColor(Color.black)    
        .font(.custom("AppleSDGothicNeo-Bold", size: 20.0))
        .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray,lineWidth: 2))
        .padding(.leading ,10)
        .padding(.trailing ,10)
    }
  }
}

Here, the UI is created declaratively using code, and there's no need for Storyboard in this example. Layouts are now also specified declaratively using code (the VStack in this example stacks all the views vertically). Delegates are now replaced with closures.

More importantly, views are now a function of state: The text displayed by the TextField view is now bound to the state variable text. When the button is tapped, you change the value of the text state variable, which automatically updates the text displayed in the TextField view.

To alter the behaviour of each view, you use modifiers, which are basically functions that you apply to a view or another view modifier, thereby producing a different version of the original view. Examples of modifiers in this example are padding(), overlay(), font(), and so on.

Figure 1 shows the how the UI looks in SwiftUI when the button is clicked.

Figure 1: The button and text field in SwiftUI
Figure 1: The button and text field in SwiftUI

Getting the Tools

To start developing using SwiftUI, you need the following:

  • Xcode version 11 or later
  • A deployment target (simulator or real device) of iOS 13 or later
  • macOS Mojave (10.14) or later (if you are running macOS Mojave, you can still use SwiftUI but you won't be able to use live preview and design canvas features; full features are only available in macOS Catalina (10.15) and later)

Hello, SwiftUI

Once you've installed Xcode, I know you're very eager to try out SwiftUI. So let's have a dive in into SwiftUI and see how it works first-hand.

Launch Xcode. Click on the “Create a new Xcode project” to create a new project (see Figure 2).

Figure 2: Launch Xcode and create a new project
Figure 2: Launch Xcode and create a new project

Select Single View App and click Next (see Figure 3).

Figure 3: Create a new Single View App project
Figure 3: Create a new Single View App project

Name the project HelloSwiftUI and select the various options, as shown in Figure 4. For the User Interface option, ensure that SwiftUI is selected. Click Next and save the project to a location on your Mac.

Figure 4: Name the project
Figure 4: Name the project

You should see the project created for you (see Figure 5). The ContentView.swift file contains the user interface for your application's main screen.

Figure 5: The ContentView.swift file contains the main UI for your app.
Figure 5: The ContentView.swift file contains the main UI for your app.

Automatic Previewing of Your UI Using the Canvas

By default, you should see the Inspector window on the right side of the Xcode window. For building your UI using SwiftUI, you usually don't need the Inspector window, so you can dismiss it to gain more screen estate for previewing your UI using the Canvas. To dismiss the Inspector window, click on the button on the top right corner of Xcode (see Figure 6).

Figure 6: Dismissing the Inspector window
Figure 6: Dismissing the Inspector window

With the Inspector window dismissed, you should now see the Canvas on the right-side of Xcode (Figure 7). The Canvas lets you preview the UI of your application without needing to run the application on the iPhone Simulator or real device.

In Xcode, if you don't see the Canvas, you can bring it up again through the Editor > Canvas menu. To preview your UI, click the Resume button on the Canvas. You should now be able to see the preview.

Figure 7: Viewing the Canvas on Xcode
Figure 7: Viewing the Canvas on Xcode

You can click on the Resume button to start the preview (see Figure 8).

If you don't see the Resume button when trying to preview your SwiftUI UI, make sure you are running macOS Catalina (10.15) or later.

Figure 8: Previewing your UI on the Canvas
Figure 8: Previewing your UI on the Canvas

Let's now modify the ContentView.swift file with the code that you've seen earlier (see Figure 9).

Figure 9: Modifying the ContentView.swift file
Figure 9: Modifying the ContentView.swift file

You may notice that the automatic preview has paused. This sometimes happens when the file you're previewing has some changes that caused the containing module to be rebuilt. When that happens, click the Restore button and you should now see the preview again (see Figure 10).

Figure 10: Restoring the preview shows the UI again
Figure 10: Restoring the preview shows the UI again

If you now change the color of the Text view (within the Button view) to red, you should see the changes appear automatically reflected in the preview:

Button(action: { self.text = "Hello, SwiftUI!" }) {
    Text("Button").padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)).foregroundColor(.red)
}

Note that the automatic update feature of Preview doesn't always work. There are times where you have to click Try Again button to rebuild the preview (see Figure 11).

Figure 11: If the preview fails, click Try Again
Figure 11: If the preview fails, click Try Again

Live Preview

If you recall, the code changes the text on the TextField when the button is clicked (or tapped on a real device). However, if you try clicking on the button on the preview canvas, you can observe that there's no reaction. This is because the preview canvas only allows previewing of your UI and doesn't run your application. To run the application, you need to click on the Live Preview button (see Figure 12).

Figure 12: Turn on Live Preview to test your application explicitly running it
Figure 12: Turn on Live Preview to test your application explicitly running it

Once the Live Preview mode is turned on, the background of the simulator turns dark (see left of Figure 13). You can now click on the button and the text on the TextField will be updated (see right of Figure 13).

Figure 13: Turning on Live Preview allows you to test your application without explicitly running it
Figure 13: Turning on Live Preview allows you to test your application without explicitly running it

Generating Different Previews

Notice this block of code at the bottom of ContentView.swift?

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The ContentView_Previews struct conforms to the PreviewProvider protocol. This protocol produces view previews in Xcode so that you can preview your user interface created in SwiftUI without needing to explicitly run the application on the iOS Simulator or real devices. Essentially, it controls what you see on the Preview canvas. As an example, if you want to preview how your UI will look on an older iPhone 8 device, you can modify the ContentView_Previews struct as follows (see also Figure 14):

ContentView().previewDevice("iPhone 8")
Figure 14: Previewing the UI on an older iPhone 8
Figure 14: Previewing the UI on an older iPhone 8

Creating a News Reader Application

The best way to learn a new framework or tool is to actually create an application using it. In this section, you will build a news reader application to download the news headlines, display them in a List view, and allow the user to tap on a particular news item to read more about the news.

Examining the Structure of the News Headline Feed

For this example, you'll use the free service provided by News API (https://newsapi.org). This is a JSON-based API that provides you with breaking news headlines and allows you to search for articles from over 30,000 news sources and blogs. To register for your own API key, go to https://newsapi.org/register.

For your project, you'll retrieve all the top business headlines in the US. The URL looks like this: https://newsapi.org/v2/top-headlines?country=us&apiKey=<API_Key>

The news headline API returns a JSON string containing the details of the news headlines. You can paste the URL onto a Web browser and obtain the content. Once the JSON content is displayed on your browser, copy and paste it into a JSON validator website, such as http://jsonlint.com, and you'll have a good idea of the structure of the JSON content. Figure 15 shows the structure of a sample of the JSON content:

Figure 15: The structure of the JSON content displayed using jsonlint.com
Figure 15: The structure of the JSON content displayed using jsonlint.com

Observe that the value of the articles key is an array of items each containing the details of each article. In each article, you want to retrieve the following details:

  • title: the title of the news item
  • url: the link containing the details of the news item
  • description: a synopsis of the news item
  • urlToImage: the link containing the image for the article

You're now ready to start coding. Using Xcode, create a Single View App project and name it NewsReader.

To extract the items from JSON into structures in Swift, create the following structs in ContentView.swift:

import SwiftUI
struct Result: Codable {
    var articles: [Article]
}
struct Article: Codable {
    var url: String
    var title: String
    var description: String?
    var urlToImage: String?
}
struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

By conforming to the Codable protocol, the Result and Article structs are now able to map Swift Objects to JSON data, and vice versa.

Fetching the JSON String

Before you see how to fetch the news headlines from the Web, let's define a state variable named articles. This state variable stores all the decoded JSON content and you'll also use it to bind to your List view for display:

struct ContentView: View {
    private let url = "https://newsapi.org/v2/top-headlines?country=us&amp;apiKey=<API_KEY>"
    @State private var articles = [Article]() 
    var body: some View {
        Text("Hello, World!")
    }
}

To fetch the news headlines, you shall define the fetchData() function as shown in Listing 1.

Listing 1: Defining the fetchData() function

struct ContentView: View {
  private let url = "https://newsapi.org/v2/top-headlines?country=us&amp;apiKey=<API_KEY>"

  @State private var articles = [Article]()
    
  func fetchData() {
    guard let url = URL(string: url) else {
      print("URL is not valid")
      return
    }
    
    let request = URLRequest(url: url)
    URLSession.shared.dataTask(with: request) {
      data, response, error in
      if let data = data {  // data is Optional, so you need to unwrap it
        if let decodedResult = try?
          JSONDecoder().decode(Result.self, from: data) {
            // decoding is successful
            DispatchQueue.main.async {
              // assign the decoded articles to the state variable
              self.articles = decodedResult.articles
            }
            return
        }
      }
      print("Error: \(error?.localizedDescription ?? "Unknown error")")
    }.resume()
  }

  var body: some View {
    Text("Hello, World!")
  }
}

You use the dataTask() method of the URLSession.shared object instance to fetch the news headlines. Once the JSON content is downloaded, you use the JSONDecoder() object's decode() function to convert the JSON content into the Result struct that you've defined earlier. Once the conversion is done, you assign the result to the articles state variable.

Defining the View

You can now define the view. Use a List view to display the list of articles:

var body: some View {
    List(articles, id: \.url) { item in
        VStack(alignment: .leading) {
            Text(item.title).font(.headline)
            Text(item.description ?? "").font(.footnote)
        }
    }.onAppear(perform: fetchData)
}

The List view is bound to the articles state variable, and for each row in the List view you use a VStack view to display the title and description of each article. The onAppear() modifier to the List view specifies that the fetchData() function be called when the List view first appears.

For your reference, the content of the ContentView.swift file is shown in Listing 2.

Listing 2: The code in ContentView.swift

import SwiftUI

struct Result: Codable {
  var articles: [Article]
}

struct Article: Codable {
  var url: String
  var title: String
  var description: String?
  var urlToImage: String?
}

struct ContentView: View {
  private let url = "https://newsapi.org/v2/top-headlines?country=us&amp;apiKey=<API_KEY&>"

  @State private var articles = [Article]()
    
  func fetchData() {
    guard let url = URL(string: url) else {
      print("URL is not valid")
      return
    }
    let request = URLRequest(url: url)
    URLSession.shared.dataTask(with: request) {
      data, response, error in
      if let data = data {  // data is Optional, so you need to unwrap it
        if let decodedResult = try?
          JSONDecoder().decode(Result.self, from: data) {
            // decoding is successful
            DispatchQueue.main.async {
              // assign the decoded articles to the state variable
              self.articles = decodedResult.articles
            }
            return
          }
        }
        print("Error: \(error?.localizedDescription ?? "Unknown error")")
      }.resume()
  }

  var body: some View {
    List(articles, id: \.url) { item in
      VStack(alignment: .leading) {
        Text(item.title).font(.headline)
        Text(item.description ?? "").font(.footnote)
      }
    }.onAppear(perform: fetchData)
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

Figure 16 shows how the app looks when you run the Live Preview on Xcode.

Figure 16: Displaying the news headlines using the List view
Figure 16: Displaying the news headlines using the List view

Displaying Images Remotely

So far, the news headlines are displayed nicely using the List view. However, it would be much nicer if you were able to display an image for each news headlines. As the saying goes, a picture is worth a thousand words.

In SwiftUI, you can display images using the Image view. However, one key problem with the Image view is that it's only capable of displaying local images. That is, images that are bundled locally with the application. If you want to display an image that's located on the Web, you're out of luck.

One way to fix this is to create your own Image view to load images remotely. But there are already solutions developed by others, so you can just make use of one of them. For this purpose, you'll use the URLImage view located at https://github.com/dmytro-anokhin/url-image.

To make use of the URLImage view, you need to add its package to your project. You can do so by going to Xcode and selecting File > Swift Packages > Add Package Dependency... Enter this URL: https://github.com/dmytro-anokhin/url-image (see Figure 17).

Figure 17: Adding a package to your Xcode project
Figure 17: Adding a package to your Xcode project

Click Next in the current page as well as the next page. Finally, click Finish. The package will now be added to the project.

With the URLImage package added to the project, add the following statements in bold to the ContentView.swift file, as shown in Listing 3.

Listing 3: Adding the statements to load remote images using the URLImage view

import SwiftUI
import URLImage

struct Result: Codable {
  var articles: [Article]
}

struct Article: Codable {
  var url: String
  var title: String
  var description: String?
  var urlToImage: String?
}

struct ContentView: View {
  private let url = "https://newsapi.org/v2/top-headlines?country=us&amp;apiKey=<API_KEY>"

  @State private var articles = [Article]()
    
  func fetchData() {
    ...
  }

  var body: some View {
    List(articles, id: \.url) { item in
      HStack(alignment: .top) {
        URLImage((( URL(string:item.urlToImage ?? "https://picsum.photos/100") ?? nil
        )!),
          delay: 0.25,
          processors:
            [Resize(size: CGSize(width: 100.0, height: 100.0), scale: UIScreen.main.scale)],
          content: {
            $0.image
            .resizable()
            .aspectRatio(contentMode:.fit)
            .clipped()
          }
        ).frame(width: 100.0, height: 100.0)
      }

      VStack(alignment: .leading) {
        Text(item.title).font(.headline)
        Text(item.description ?? "").font(.footnote)
      }
    }.onAppear(perform: fetchData)
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

The bold statements add the URLImage view (of size 100x100) to each row in the List view. You need to check whether each news headline contains an image (through the urlToImage property). If no image is available, make use of a sample image provided by this site: https://picsum.photos/. The URL: https://picsum.photos/100 indicates to the site to return an image of size 100x100 pixels.

Figure 18 shows the image displayed next to each news headline.

Figure 18: Displaying image next to each news headline
Figure 18: Displaying image next to each news headline

Wrapping the List View in a NavigationView

Now that you have managed to populate the List view with the various news headlines, you can wrap the List view in a NavigationView:

var body: some View {
    NavigationView {
        List(articles, id: \.url) { 
            item in ...
        }.onAppear(perform: fetchData).navigationBarTitle("News Headlines")
    }
} 

The NavigationView is a view for presenting a stack of views representing a visible path in a navigation hierarchy. Figure 19 shows the List view displayed within a NavigationView with the navigation bar title set.

Figure 19: Displaying the navigation bar title
Figure 19: Displaying the navigation bar title

You can make the text in the navigation bar title smaller by setting its display mode to inline:

.navigationBarTitle("News Headlines", displayMode: .inline)

Figur 20 shows reduced font size of the navigation bar title.

Figure 20: Reducing the font size of the navigation bar title
Figure 20: Reducing the font size of the navigation bar title

Creating the Details Page

When the user taps on a row in the List view, you should display the content of the news in another page. The details of the news could be obtained through its url property of the Article struct.

To display the news using its URL, you need to use a Web browser. In the current version of SwiftUI, not all the views are implemented yet, and this includes the implementation of the Web browser, which is available in the existing WebKit framework and known as the WebView. What you need to do now is make use of the WebView in your SwiftUI application.

To do so, let's add a new SwiftUI View file to the project and name it as NewsView.swift. Add the following statements in bold to the NewsView.swift file, as shown in Listing 4.

Listing 4: Adding a new file named NewsView.swift to the project

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
  let request: URLRequest
      
  func makeUIView(context: Context) -> WKWebView  {
      return WKWebView()
  }
    
  func updateUIView(_ uiView: WKWebView,  context: Context) {
    uiView.load(request)
  }
}

struct NewsView: View {
  let url: String

  var body: some View {
    WebView(request: URLRequest(url: URL(string:url)!))
  }
}

struct NewsView_Previews: PreviewProvider {
  static var previews: some View {
    NewsView(url: "https://codemag.com/Magazine")
  }
}

The UIViewRepresentable protocol allows you to create and manage a UIView object in your SwiftUI application. By conforming to this protocol, you need to implement the following methods:

  • makeUIView: creates the view object
  • updateUIView: updates the state of the view object

Once the WebView is created, you can use it by passing in the URL of the page to load. For the preview, you load the CODE Magazine home page, as shown in Figure 21.

Figure 21: Previewing the detail page
Figure 21: Previewing the detail page

Once the details page is created, you're ready to link it with the ContentView. Add the following statements in bold to the ContentView.swift file, as shown in Listing 5.

Listing 5: Linking the ContentView to NewsView

  var body: some View {
    List(articles, id: \.url) { 
        item in NavigationLink(destination: NewsView(url:item.url)
      ) {
        HStack(alignment: .top) {
          URLImage((( URL(string:item.urlToImage ?? "https://picsum.photos/100") ?? nil
          )!),
            delay: 0.25,
            processors:
              [Resize(size: CGSize(width: 100.0, height: 100.0), scale: UIScreen.main.scale)],
            content: {
              $0.image
              .resizable()
              .aspectRatio(contentMode:.fit)
              .clipped()
            }
          ).frame(width: 100.0, height: 100.0)
        }

        VStack(alignment: .leading) {
          Text(item.title).font(.headline)
          Text(item.description ?? "").font(.footnote)
        }
      }
    }.onAppear(perform: fetchData)
  }
}

Figure 22 shows how the application works. Tap on a row in the List view and the details will be loaded on the details page.

Figure 22: Tapping on a news item displays the news in more detail
Figure 22: Tapping on a news item displays the news in more detail

Want to display the navigation bar title back to large text? Well, you can set it as follows in the ContentView.swift file:

var body: some View {
    NavigationView {
        List(articles, id: \.url) { item in  
            NavigationLink(destination: NewsView(url:item.url)) {
                ...
            }.onAppear(perform: fetchData).navigationBarTitle("News Headlines")
        }
    }

However, you'll soon realize that when you navigate to the details page, there's a large empty space below the navigation bar in the details page (see Figure 23).

Figure 23: The detail page with an empty space below the navigation bar
Figure 23: The detail page with an empty space below the navigation bar

To resolve this, you need to set the navigation bar title in the NewsView.swift to display in inline mode:

struct NewsView: View {
  let url: String
  var body: some View {
    WebView(request: URLRequest(url: URL(string:url)!))
      .navigationBarTitle("News Details", displayMode: .inline)
    }
}

Figure 24 shows the details view now showing the content without the empty space. In addition, the navigation bar also displays a title.

Figure 24: The empty space below the navigation bar is gone
Figure 24: The empty space below the navigation bar is gone
.navigationBarTitle("", displayMode: .inline)

Summary

I hope this article has given you a good idea of the power of SwiftUI. Although SwiftUI is still in its early days, by the time you read this, you shouldn't be too far off from the next update of SwiftUI. By then, SwiftUI will have ported more of the UIKit views and you should be able to develop your applications entirely using SwiftUI.