News Tips & Tricks

Tutorial: Create a SwiftUI application in AppCode

In this tutorial, you’ll create a simple SwiftUI application that shows a list of iOS conferences. The application will consist of two views:

  • A list of conferences representing data from the local JSON file.
  • Details for each conference

SwiftUI application

Along the way, you’ll get familiar with the basic AppCode workflow and useful features that help writing code easier and faster.

Requirements:

  • AppCode 2019.3 or later
  • Xcode 11 or later

Sample project: iOSConferences

Step 1. Create a project

Projects created in AppCode are fully compatible with Xcode and use the same project model. After you create a project in AppCode, you can open and edit it in Xcode and vice versa, and all the data will be synchronized.

Launch AppCode and click New Project on the Welcome screen:

Start new project
If you have another project open in AppCode at the moment, select File | New Project from the main menu.

In the dialog that opens, you see a list of Xcode project templates. Select iOS | Application | Single View App and click Next:

Create project dialog

Fill in the following fields:

  • Product Name: your project name which will be also the name of your application. Type iOSConferences.
  • Organization Name: your or your company’s name.
  • Organization Identifier: your company’s identifier in reverse-DNS format, for example, com.mycompany:

Application details

Your project name and organization identifier together build a bundle identifier — an automatically generated string that will identify your application in the operating system.

If you need to change the project or organization name, bundle identifier, or other project settings, press ⌘; or select File | Project Settings from the main menu.

You can also change your project settings in Xcode. Just select File | Open Project in Xcode from the main menu and change the necessary settings there. The changes will appear in AppCode as soon as you apply them in Xcode.

Select Swift in the list of languages and SwiftUI in the User Interface field. Make sure that all checkboxes in the dialog are cleared as using tests or Core Data is outside the scope of this tutorial. Click Finish:

Create new project step
In the Finder window that opens, select a directory where your project will be located.

A new Swift project will be created and immediately opened in AppCode. In the left part of the AppCode window, you see the Project tool window. From here, you can navigate to necessary files, add and delete files and folders, exclude files from indexing or from Xcode project, add files to different targets, and so on.

To show and hide the Project tool window, press ⌘1. For more information, see Project tool window:

Full IDE window

Step 2. Enable interactive preview

To preview changes in SwiftUI layouts from AppCode, you can use the InjectionIII application.

Install and start InjectionIII

Download InjectionIII from AppStore or from GitHub (version 1.8 or later). Place the InjectionIII application file to the Application folder and start the application. Its icon will appear in the status bar:

Injection III in the status bar

Prepare the project for working with InjectionIII

In the AppDelegate.swift file, add the code for loading the InjectionIII bundle into the application(_:didFinishLaunchingWithOptions:) method:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    #if DEBUG
    var injectionBundlePath = "/Applications/InjectionIII.app/Contents/Resources"
    #if targetEnvironment(macCatalyst)
    injectionBundlePath = "\(injectionBundlePath)/macOSInjection.bundle"
    #elseif os(iOS)
    injectionBundlePath = "\(injectionBundlePath)/iOSInjection.bundle"
    #endif
    Bundle(path: injectionBundlePath)?.load()
    #endif
    return true
}

For more details, refer to the InjectionIII documentation.

In the ContentView.swift file, go to the ContentView_Previews structure and add the injected() method inside the #if DEBUG block. Also, change struct to class for ContentView_Previews:

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

    #if DEBUG
    @objc class func injected() {
        UIApplication.shared.windows.first?.rootViewController =
                UIHostingController(rootView: ContentView())
    }
    #endif
}

The injected() method reloads the view on code injection when the application is running in debug mode. Instead of reloading the view, you can have a design-time preview generated on code injection — the same way as Xcode 11 does. In this case, use the previews property of the current PreviewProvider:

@objc class func injected() {
    UIApplication.shared.windows.first?.rootViewController =
            UIHostingController(rootView: ContentView_Previews.previews)
}

Run the application with preview

Make sure that you have the Debug configuration selected for the current run/debug configuration. To do this, select Run | Edit Configurations from the main menu:

Run/debug configurations

To run the application, select a device or simulator on the toolbar and press ⌃R:

Run/debug configurations popup

To run your application on a real device connected to your Mac, you need to configure your project in Xcode first.

Then, press ⌃R. On the first run, you will be prompted to select the project directory in the Finder window that opens.

In case you need to change the project directory later, for example, if you’ve changed your project location or want to use InjectionIII with another project, select Open Project from the InjectionIII menu and choose a new directory in Finder.

Once InjectionIII is connected and the project directory is specified, you’ll see the following messages in the Run tool window:

Injection messages in Run toolwindow

Try to change some code in your view and see the changes on the simulator or device screen:

Try Injection

Changes that you make in your code appear in the preview on saving the file. AppCode saves changes automatically when you build and run an application, close files or projects, quit the IDE, and so on. Therefore, it may be useful to invoke autosave on a regular basis, let’s say, every second, so that you won’t need to trigger file saving manually. To do this, open the Preferences dialog ⌘, and go to Appearance & Behavior | System Settings | Synchronization. Select the Save files automatically if application is idle for … sec checkbox and set the value to 1.

Step 3. Create a list

In iOSConferences/ContentView.swift, rename ContentView to ConferenceList using the Rename refactoring: place the caret at ContentView, press ⇧F6, type the new name in the highlighted area, and press :

Rename refactoring

The file itself will be renamed as well. The same way, rename ContentView_Previews to ConferenceList_Previews.

Rebuild the project to have the interactive preview working after renaming the file by pressing ⌘F9. Instead of the Hello, World! text, add a list of two text elements:

  • Select the Text("Hello, World!") line by pressing ⌥↑ and add the List element containing Text("Conference1") instead:Extend selection
  • Add the second text element by duplicating ⌘D the first one:Duplicate line
  • You may notice that the code misses a space between List and {. Just press ⌥⌘L to reformat the code on the whole page according to the guidelines specified in the Preferences | Editor | Code Style | Swift:Reformat code

As a result, you will have the following code:

struct ConferenceList: View {
    var body: some View {
        List {
            Text("Conference1")
            Text("Conference2")
        }
    }
}

The application screen will look as follows:

Add SwiftUI list

Add a title to the list. To do this, wrap the list with the NavigationView element and then call the navigationBarTitle(_:) method for the list:

struct ConferenceList: View {
    var body: some View {
        NavigationView {
            List {
                Text("Conference1")
                Text("Conference2")
            }.navigationBarTitle("Conferences")
        }
    }
}
You can create custom live templates to quickly wrap your code with necessary constructs. See details in Create a surround live template.

Conferences list

Instead of two list items, let’s create one consisting of a title and subtitle. In the title, let’s display the conference name and in the subtitle — location. To do this, wrap the two Text elements in a VStack container, change their values, and apply the corresponding font styles to them:

VStack {
    Text("Conference").font(.headline)
    Text("Location").font(.subheadline)
}

Add element]

Align both Text elements left using the alignment parameter for the VStack container:

VStack(alignment: .leading) {
   // ...
}

Add element

To show documentation for the symbol at caret, press F1.

Step 4. Load data from JSON

Let’s make our list dynamic by loading data from the local conferencesData.json file.

Create a new group Resources where you will keep the JSON file:

  • Right-click the iOSConferences group in the Project tool window and select New | Group.
  • In the dialog that opens, type Resources.
  • Make sure the Create folder checkbox is selected and click OK:Create Resources group
    If you clear the Create folder checkbox, AppCode will create a group without adding the corresponding folder in the file system. See more in Create groups.

Add the downloaded JSON to the newly created group:

  • Right-click the Resources group and select Add | Files.
  • Select the conferencesData.json file on your Mac.
  • In the dialog that opens, leave the default settings and click OK:Add file

This file contains a list of iOS conferences. Each conference has a name, link to the official website, start and end dates, and location:

{
    "name": "SwiftLeeds",
    "link": "https://swiftleeds.co.uk/",
    "start": "2020-10-07",
    "end": "2020-10-08",
    "location": "🇬🇧 Leeds, UK"
}

Create a new Data.swift file where you will add a function for parsing the JSON data:

  • Create a new group called Model as described above.
  • Select this group, press ⌘N, and select Swift File
  • In the dialog that opens, type Data in the Name field and click OK:Add Swift file

In the Data.swift file, add a function for reading data from JSON:

func loadFile<T: Decodable>(_ filename: String) -> T {
    let data: Data

    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
            else {
        fatalError("Cannot find \(filename)")
    }
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Cannot load \(filename):\n\(error)")
    }
    do {
        let decoder = JSONDecoder()
        let format = DateFormatter()
        format.dateFormat = "yyyy-mm-dd"
        decoder.dateDecodingStrategy = .formatted(format)
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Cannot't parse \(filename): \(T.self):\n\(error)")
    }
}

Create a new class for handling the parsed JSON data:

  • Select the Model group.
  • Press ⌘N and select Swift Type.
  • In the dialog that opens, type Conference in the Name field and click OK:Add Conference
  • Declare conformance to the Codable and Identifiable protocols and add a set of properties corresponding to the JSON data:
    class Conference: Codable,Identifiable {</li></ul>
        var name: String
        var location: String
        var start: Date
        var end: Date?
        var link: String
    }

    In the Data.swift file, create a variable that will store the parsed data — an array of the Conference objects:

    let conferencesData: [Conference] = loadFile("conferencesData.json")
    You can use the let live template to introduce the variable: type let and press . Press to navigate between the template placeholders. To add the variable type, press ⌥⏎ and select Add explicit type:

    Live Template

    In the ConferenceList.swift file, pass conferencesData to the List initializer and replace the strings in the Text elements by the values from the loaded JSON:

    List(conferencesData) {conference in
        VStack(alignment: .leading) {
            Text(conference.name).font(.headline)
            Text(conference.location).font(.subheadline)
        }
    }.navigationBarTitle("Conferences")

    The list of conferences will be displayed:

    List of conferences

    Step 5. Add the details view

    The second view of our application will display the detailed data on each conference.

    Create a new SwiftUI file:

    • In the Project tool window, press ⌘N and select File from Xcode Template.
    • In the dialog that opens, select iOS | User Interface | SwiftUI View and click Next:Add SwiftUI file
    • On the next page, type the filename — ConferenceDetails, make sure that the iOSConferences group and location are selected, and click Finish.

    Instead the default Hello, World! text, add the conference location:

    struct ConferenceDetails: View {
        var body: some View {
            Text(conference.location)
        }
    }

    Since the conference variable is not defined in the current context, it is highlighted red. To define it directly from its usage, do the following:

    • Place the caret at conference and press ⌥⏎.
    • Select Create local variable ‘conference’ and press .
    • In the list that appears, select var.
    • To specify the type of the variable, press ⌥⏎ again, select Add explicit type, and start typing Conference.
    • Move the code line up by pressing ⌥⇧↑.
    • Press ⌥⌘L to fix the code formatting.

    As a result, your code will look as follows:

    struct ConferenceDetails: View {
        var conference: Conference
        var body: some View {
            Text(conference.location)
        }
    }

    Now, the conference parameter is missing in the ConferenceDetails() initializer within the ConferenceDetails_Preview structure. Place the caret at the highlighted code, press ⌥⏎, and select Apply Fix-it. The parameter name and placeholder for its value will appear. Pass Conference() as a parameter:

    struct ConferenceDetails_Previews: PreviewProvider {
        static var previews: some View {
            ConferenceDetails(conference: Conference())
        }
    }

    Conference() is highlighted red as the necessary initializer for the Conference class is missing. To add it, with the caret placed at the highlighted code, press ⌥⏎ and select Create initializer. An empty initializer will be added to the Conference class. Set initial values for all the properties there, for example:

    init() {
        name = "Conference Name"
        location = "Location"
        start = Date()
        end = Date()
        link = "https://www.google.com"
    }

    Duplicate ⌘D the Text(conference.location) line two times and wrap these three Text elements in a VStack container.

    You can create custom live templates to quickly wrap your code with necessary constructs. See details in Create a surround live template.
    struct ConferenceDetails: View {
        var conference: Conference
        var body: some View {
            VStack {
                Text(conference.location)
                Text(conference.location)
                Text(conference.location)
            }
        }
    }

    Enable the interactive preview for the ConferenceDetails by adding the injected() method for ConferenceDetails_Previews the same way you did for ConferenceList_Previews:

    class ConferenceDetails_Previews: PreviewProvider {
        static var previews: some View {
            ConferenceDetails(conference: Conference())
        }
    
        #if DEBUG
        @objc class func injected() {
            UIApplication.shared.windows.first?.rootViewController =
                    UIHostingController(rootView: ConferenceDetails(conference: Conference()))
        }
        #endif
    }

    Rebuild ⌘F9 the application to have the preview working after adding the new file. By now, leave the ConferenceDetails view as is. Later, you will display here the conference dates and link.

    Step 6. Set up navigation between the list and details

    In the ConferenceList.swift file, go to the ConferenceList structure and wrap the VStack container in a NavigationLink.

    NavigationLink {
        VStack(alignment: .leading) {
            Text(conference.name).font(.headline)
            Text(conference.location).font(.subheadline)
        }
    }

    Pass the ConferenceDetails view as the destination parameter for the NavigationLink:

    NavigationLink(destination: ConferenceDetails(conference: conference)) {
        VStack(alignment: .leading) {
            Text(conference.name).font(.headline)
            Text(conference.location).font(.subheadline)
         }
    }

    The list items are now clickable, and the details view opens on tapping each of them:

    List details

    Display the conference date

    In the ConferenceDetails view, the start and end dates should be displayed in format MMMM dd, yyyy - MMMM dd, yyyy.

    In the Data.swift file, add a new function dateToString as an extension for the Date class. This function converts Date to String and changes the date format:

    extension Date {
        func dateToString() -> String {
            let format = DateFormatter()
            format.dateFormat = "MMM dd, yyyy"
            format.string(from: self)
        }
    }

    The code above won’t be resolved as the function doesn’t return anything. Place the caret within the highlighted symbol and press ⌥⏎:

    Apply fix-it

    Press to apply the quick-fix. The return statement will be added:

    extension Date {
        func dateToString() -> String {
            let format = DateFormatter()
            format.dateFormat = "MMM dd, yyyy"
            return format.string(from: self)
        }
    }

    Create a method of the Conference class that will return the data of a conference in text format. Open Conference.swift and add the following method after the var link: String line:

    func textDates() -> String {
        var result = start.dateToString()
        if let end = self.end {
            result = "\(result) - \(end.dateToString())"
        }
        return result
    }

    In the ConferenceDetails view, replace the value of the second Text element with the conference date returned by the newly created textDates() method:

    VStack {
        Text(conference.location)
        Text(conference.textDates())
        Text(conference.location)
    }

    The dates will appear in the preview:

    Preview dates

    To make the conference link clickable, you need to add a button and pass a method for opening the URL as its action.

    In the ConferenceDetails.swift file, add a new structure named LinkButton that conforms to the View protocol and includes a Button with the Go to official website text:

    struct LinkButton: View {
        var body: some View {
            Button() {
                Text("Go to official website")
            }
        }
    }
    You can use the struct live template for writing the structure code faster: just type struct and press .

    Add an action for the Button — opening the conference link with the open(_:) method:

    struct LinkButton: View {
        var link = ""
        var body: some View {
            Button(action: {
                UIApplication.shared.open(URL(string: self.link)!)
            }) {
                Text("Go to official website")
            }
        }
    }

    Put the newly created LinkButton in the ConferenceDetails view and pass the conference link as a parameter for the LinkButton initializer:

    struct ConferenceDetails: View {
        var conference: Conference
        var body: some View {
            VStack {
                Text(conference.location)
                Text(conference.textDates())
                LinkButton(link: conference.link)
            }
        }
    }

    The link will appear in the preview:

    Link preview

    Step 8. Adjust the details view appearance

    In the ConferenceDetails view, place the content of the VStack container to the top left corner of the screen by adjusting its frame width, height, and alignment:

    struct ConferenceDetails: View {
          var conference: Conference
          var body: some View {
              VStack {
                  Text(conference.location)
                  Text(conference.textDates())
                  LinkButton(link: conference.link)
              }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
          }
    }

    Adjust details step 1

    Align the VStack content left by using the alignment parameter:

    VStack(alignment: .leading) {
    // ...
    }

    Adjust details step 2

    Add the default padding for the VStack container using the padding() method:

    VStack(alignment: .leading) {
        // ...
    }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
            .padding()
    

    Adjust details step 3

    Add the bottom padding for the elements within the VStack container:

    VStack(alignment: .leading) {
          Text(conference.location).padding(.bottom)
          Text(conference.textDates()).padding(.bottom)
          LinkButton(link: conference.link).padding(.bottom)
    }

    Adjust details step 4

    You can place multiple carets and type your code in several lines at the same time. To do this, place the caret where you want to start typing, press and hold ⇧⌥, and place the caret at the other lines:
    Multiple cursors

    Put the conference name in the view title by using the navigationBarTitle(_:) method for the VStack container:

    VStack(alignment: .leading) {
        // ...
    }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
            .padding()
            .navigationBarTitle(conference.name)
    
    

    This change won’t be available in the interactive preview. Rerun the application to check it:

    Adjust details - last step

    What’s next

    You can elaborate this application by making it load the data from the remote YAML file containing the up-to-date list of iOS/macOS conferences. For parsing the data, you can use the Yams library added to the project by means of the CocoaPods dependency manager. Stay tuned to learn it in action in our future tutorials!

    Your AppCode team
    JetBrains
    The Drive to Develop