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.
Along the way, you'll get familiar with the basic AppCode workflow and useful features and learn how to enable the interactive preview in AppCode by means of the InjectionIII application.
Watch our video tutorial and follow the step-by-step instructions below:
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:
If you have another project open in AppCode at the moment, select
from the main menu.In the dialog that opens, you see a list of Xcode project templates. Select Next:
and clickFill 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
.
Your project name and organization identifier together build a bundle identifier — an automatically generated string that will identify your application in the operating system.
Select Swift in the list of languages and SwiftUI in the 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.
Starting with Xcode 12, you can select the application life cycle in the corresponding field: UIKit App Delegate or SwiftUI App. Choose your preferable option and click Finish.
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.
Step 2. Enable interactive preview
To preview changes in SwiftUI layouts from AppCode, you can use the InjectionIII application.
1. Install and start InjectionIII
2. Prepare the project for working with InjectionIII
Add the
-Xlinker -interposable
flag in the Other Linker Flags section of the project settings ⌃ ⌥ ⇧ S:If you selected the UIKit App Delegate life cycle when creating the application, go to the AppDelegate.swift file and 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 }If you selected the SwiftUI App life cycle, modify the iOSConferencesApp.swift file:
import SwiftUI import UIKit class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { #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 } } @main struct iOSConferencesApp: App { // Add this line @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } }For more details, refer to the InjectionIII documentation.
In the ContentView.swift file, go to the
ContentView_Previews
structure and add theinjected()
method inside the#if DEBUG
block. Also, changestruct
toclass
forContentView_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 thepreviews
property of the current PreviewProvider:@objc class func injected() { UIApplication.shared.windows.first?.rootViewController = UIHostingController(rootView: ContentView_Previews.previews) }
3. Run the application with preview
Make sure that you have the Debug configuration selected for the current run/debug configuration. To do this, select from the main menu.
Run the application ⇧ F10. To do this, select a device or simulator on the toolbar:
Then, press ⇧ F10 or click .
On the first run, you will be prompted to select the project directory in the Finder window that opens.
Once InjectionIII is connected and the project directory is specified, you'll see the following messages in the Run tool window:
Try to change some code in your view and see the changes on the simulator or device screen:
Step 3. Create a list
In iOSConferences/ContentView.swift, rename
ContentView
toConferenceList
using the Rename refactoring: place the caret atContentView
, press ⇧ F6, type the new name in the highlighted area, and press ⏎:The file itself will be renamed as well.
The same way, rename
ContentView_Previews
toConferenceList_Previews
.Rebuild the project to have the interactive preview working after renaming the file: press ⌃ F9 or click on the toolbar.
Instead of the
Hello, World!
text, add a list of two text elements:Select the
Text("Hello, World!")
line by pressing ⌃ W and add theList
element containingText("Conference1")
instead:Add the second text element by duplicating ⌃ D the first one:
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 :
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 a title to the list. To do this, wrap the list with the
NavigationView
element and then call thenavigationBarTitle(_:)
method for the list:struct ConferenceList: View { var body: some View { NavigationView { List { Text("Conference1") Text("Conference2") }.navigationBarTitle("Conferences") } } }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 aVStack
container, change their values, and apply the corresponding font styles to them:VStack { Text("Conference").font(.headline) Text("Location").font(.subheadline) }Align both
Text
elements left using thealignment
parameter for theVStack
container:VStack(alignment: .leading) { // ... }
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 .In the dialog that opens, type Resources.
Make sure the Create folder checkbox is selected and click OK:
Add the downloaded JSON to the newly created group:
Right-click the
Resources
group and select .Select the conferencesData.json file on your Mac.
In the dialog that opens, leave the default settings and click OK:
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:
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:
Declare conformance to the
Codable
andIdentifiable
protocols and add a set of properties corresponding to the JSON data:class Conference: Codable,Identifiable { 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")In the ConferenceList.swift file, pass
conferencesData
to theList
initializer and replace the strings in theText
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:
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 Next:
and clickOn 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 theConferenceDetails()
initializer within theConferenceDetails_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. PassConference()
as a parameter:struct ConferenceDetails_Previews: PreviewProvider { static var previews: some View { ConferenceDetails(conference: Conference()) } }Conference()
is highlighted red as the necessary initializer for theConference
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 theConference
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 threeText
elements in aVStack
container.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 theinjected()
method forConferenceDetails_Previews
the same way you did forConferenceList_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 theConferenceList
structure and wrap theVStack
container in aNavigationLink
.NavigationLink { VStack(alignment: .leading) { Text(conference.name).font(.headline) Text(conference.location).font(.subheadline) } }Pass the
ConferenceDetails
view as thedestination
parameter for theNavigationLink
: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:
Step 7. Display the conference date and link
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 theDate
class. This function convertsDate
toString
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 ⌥ ⏎:
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 date of a conference in text format. Open Conference.swift and add the following method after thevar 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 secondText
element with the conference date returned by the newly createdtextDates()
method:VStack { Text(conference.location) Text(conference.textDates()) Text(conference.location) }The dates will appear in the preview:
Display the conference link
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 theView
protocol and includes aButton
with theGo to official website
text:struct LinkButton: View { var body: some View { Button() { Text("Go to official website") } } }Add an action for the
Button
— opening the conference link with theopen(_:)
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 theConferenceDetails
view and pass the conference link as a parameter for theLinkButton
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:
Step 8. Adjust the details view appearance
In the
ConferenceDetails
view, place the content of theVStack
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) } }Align the
VStack
content left by using thealignment
parameter:VStack(alignment: .leading) { // ... }Add the default padding for the
VStack
container using thepadding()
method:VStack(alignment: .leading) { // ... }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) .padding()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) }Put the conference name in the view title by using the
navigationBarTitle(_:)
method for theVStack
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:
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. See more in the Use CocoaPods in your project tutorial.