AppCode 2019.3 Help

Use CocoaPods in your project

In this tutorial we will elaborate the iOSConferences application (see Create a SwiftUI application in AppCode) by making it load the up-to-date list of iOS/macOS conferences from the remote YAML file.

To parse the YAML file, we will use the Yams library which will be added into the project by means of the CocoaPods dependency manager.

Step 1. Install CocoaPods

  1. Clone or download the iOSConferences project and open it in AppCode.

  2. Select Tools | CocoaPods | Select Ruby SDK from the main menu.
  3. In the Preferences dialog that opens, click Add Ruby SDK, and specify the path to the Ruby SDK that will be used with CocoaPods, by default, /usr/bin/ruby:

    Add Ruby SDK

  4. Click the Install CocoaPods button.

After the CocoaPods gem is installed, the list of pods is displayed on the Tools | CocoaPods page of the Preferences dialog:

List of pods

Step 2. Add the Yams pod to the project

  1. From the main menu, select Tools | CocoaPods | Create CocoaPods Podfile. The Podfile will be created in the same directory with the .xcodeproj file and opened in the editor.
  2. In the Podfile, add the Yams pod under the iOSConferences target:
    project 'iOSConferences.xcodeproj' target 'iOSConferences' do use_frameworks! pod 'Yams' end
  3. After you have added the pod 'Yams' code line, AppCode notifies you that the Podfile contains pods not installed yet. To install the Yams pod, click the Install pods link in the top-left corner of the editor window. Alternatively, with the caret placed at pod 'Yams', press ⌥⏎, select Install, and press .

    Install pods

When the library is installed, you can find its files under the Pods/Pods/Yams group in the Project tool window:

The installed pod

Step 3. Load data from the remote YAML

In our application, we already have the conference data model — iOSConferences/Model/Conference.swift. It contains a set of properties corresponding to the data stored in the conferencesData.json file located in iOSConferences/Resources. The remote YAML file contains the same-name attributes, so you do not need to change anything in the current model.

However, we will need to change the current code used for loading and parsing the data. For handling the results of the URL session, we will use the Combine framework. For parsing the data we will use a dedicated YAMLDecoder.

Create a class for loading the data

The new class should conform to ObservableObject — a protocol from the Combine framework — and contain the following:

  • A @Published property that emits all changes in the conferences data so that the updated data appears the view.

  • A method for loading the connferences data that creates a publisher for wrapping a URL session data task.

  • An initializer.

  1. Open the iOSConferences/Model/Data.swift file.

  2. Delete unnecessary code: the loadFile(_:) method and the conferencesData variable.

  3. Add a new class named ConferencesLoader and declare its conformance to ObservableObject:
    public class ConferencesLoader: ObservableObject { }
  4. In the new class, add a @Published property conferences that stores an array of the Conference objects:

    public class ConferencesLoader: ObservableObject { @Published var conferences = [Conference]() }

  5. Add the loadConferences() method that we will implement later.
    public class ConferencesLoader: ObservableObject { @Published var conferences = [Conference]() func loadConferences() { } }
  6. Add an initializer for the class: while the caret is inside the class code, click ⌘N, select Initializer, and choose Select none in the dialog that opens. Add the loadConferences() method to the initializer:

    public class ConferencesLoader: ObservableObject { @Published var conferences = [Conference]() public init() { loadConferences() } func loadConferences() { } }

Create a publisher

  1. In the loadConferences() method, call URLSession.shared.dataTaskPublisher(for:):
    func loadConferences() { URLSession.shared.dataTaskPublisher(for: url) }
  2. With the caret placed at url, press ⌥⏎ and select Create global variable 'url'. This intention action will let you introduce the global variable directly from its usage.

  3. Set the link to the remote YAML file as the variable's value:

    let url = URL(string: "https://raw.githubusercontent.com/Lascorbe/CocoaConferences/master/_data/conferences.yml")

  4. You will see that the url parameter is highlighted red — press ⌥⏎ to check available options for fixing it. Select Force-unwrap using '!'..., which will add the ! character after the url variable:

    func loadConferences() { URLSession.shared.dataTaskPublisher(for: url!) }

Create a YAML decoder

We will decode the loaded data calling the decode(_:from:) method for the created publisher.

  1. Pass the [Conference] type and YAMLDecoder as the method parameters:

    func loadConferences() { URLSession.shared.dataTaskPublisher(for: url!) .decode(type: [Conference].self, decoder: YAMLDecoder()) }

  2. As YAMLDecoder belongs to the Yams framework which is not imported yet, it is highlighted red. Add the corresponding import:

    import Yams

  3. After the import is added, YAMLDecoder still remains highlighted. Hover over the highlighted code to see the error message:

    Argument type 'YAMLDecoder' does not conform to expected type 'TopLevelDecoder'

  4. Add an extension of the YAMLDecoder type that conforms to the TopLevelDecoder protocol:

    extension YAMLDecoder: TopLevelDecoder { }

  5. TopLevelDecoder belongs to the Combine framework which is not imported yet and, thus, the code is highlighted. Add the import:

    import Combine

  6. The TopLevelDecoder protocol requires the conforming types to provide the Input property and the decode(-:from:) method.

    To add the required property and method, do the following:

    • Place the caret at extension, press ⌥⏎, select Do you want to add protocol stubs?, and click . This will add a code stub for the Input typealias:

      extension YAMLDecoder: TopLevelDecoder { public typealias Input = type }

    • Specify the URLSession.DataTaskPublisher.Output data type here:

      public typealias Input = URLSession.DataTaskPublisher.Output

    • Place the caret at extension again, press ⌥⏎, select Do you want to add protocol stubs?, and click . This time, AppCode adds the decode(-:from:) method stub code:

      public func decode <T>(_ type: T.Type, from: URLSession.DataTaskPublisher.Output) throws -> T where T : Decodable { <#code#> }

    • Add the method's implementation:

      public func decode<T>(_ type: T.Type, from data: Input) throws -> T where T : Decodable { try decode(type, from: String(data: data.data, encoding: .utf8)!) }

    • You can also declare this method in a more succinct way:

      public func decode<T:Decodable>(_ type: T.Type, from data: Input) throws -> T { try decode(type, from: String(data: data.data, encoding: .utf8)!) }

Load the data and handle errors

  1. In the receive(on:) method, specify a scheduler on which the current publisher is going to receive elements:
    func loadConferences() { URLSession.shared.dataTaskPublisher(for: url!) .decode(type: [Conference].self, decoder: YAMLDecoder()) .receive(on: RunLoop.main) }
  2. Use the eraseToAnyPublisher() method to erase the publisher's actual type and convert it to AnyPublishers:

    func loadConferences() { URLSession.shared.dataTaskPublisher(for: url!) .decode(type: [Conference].self, decoder: YAMLDecoder()) .receive(on: RunLoop.main) .eraseToAnyPublisher() }

  3. With the sink(receiveCompletion:receiveValue:) method, attach a subscriber to the publisher. In the receiveCompletion parameter, pass a closure to execute on completion — here you can specify how to handle the errors. In the receiveValue parameter, pass a closure to execute on receipt of a value.

    func loadConferences() { URLSession.shared.dataTaskPublisher(for: url!) .decode(type: [Conference].self, decoder: YAMLDecoder()) .receive(on: RunLoop.main) .eraseToAnyPublisher() .sink(receiveCompletion: { completion in switch completion { case .finished: break case .failure(let error): print(error.localizedDescription) } }, receiveValue: { conferences in completion(conferences) }) }
  4. Add the completion argument to the loadConferences method's declaration:

    func loadConferences(completion: @escaping (([Conference]) -> Void)) { // ... }

  5. In the initializer where the loadConferences method is called, pass the closure expression as the method's arguments:
    public init() { loadConferences(completion: { conferences in self.conferences = conferences }) }
  6. You can also simplify the method's call by using the trailing closure syntax:

    public init() { loadConferences() { conferences in self.conferences = conferences } }

Step 4. Pass data to the view

  1. Go to iOSConferences/ConferenceList.swift.

  2. Inside the ConferenceList view, add an @ObservedObject property wrapper with an instance of the ConferencesLoader class:

    struct ConferenceList: View { @ObservedObject var conferenceLoader = ConferencesLoader() var body: some View { // ... } }

  3. Pass the list of the loaded conferences (conferenceLoader.conferences) to the List initializer:

    struct ConferenceList: View { @ObservedObject var conferenceLoader = ConferencesLoader() var body: some View { NavigationView { List(conferenceLoader.conferences) { // ... } } }

  4. Run ⌃R the application. Now the list consists of all conferences available in the remote YAML file:

    The list of conferences loaded from the remote YAML file

Last modified: 6 February 2020