Tutorial: Use CocoaPods in your project

In this tutorial you’ll 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 used for the cocoaconferences.com website.

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

Requirements:

  • AppCode 2019.3 or later
  • Xcode 11 or later

Initial project: iOSConferences
Final project: iOSConferences

Step 1. Install CocoaPods

Download the iOSConferences project and open it in AppCode. Select Tools | CocoaPods | Select Ruby SDK from the main menu. Alternatively, in the Preferences dialog ⌘,, go to Tools | CocoaPods.

The CocoaPods menu item is grayed out while the project is being indexed.

In the Preferences dialog, click Add Ruby SDK, and specify the path to the Ruby SDK that will be used with CocoaPods, by default, /usr/bin/ruby:

Select interpretator

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:

Cocoapods list

If you are using the CDN specs repo (default for CocoaPods 1.8 and later), the pods summary and description is unavailable.

Step 2. Add the Yams pod to the project

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.

You can open the created Podfile by selecting Tools | CocoaPods | Edit Podfile from the main menu.

In the Podfile, add the Yams pod under the iOSConferences target:

project 'iOSConferences.xcodeproj'
target 'iOSConferences' do
  use_frameworks!
  pod 'Yams'
end

Code completion is available while you are editing the Podfile:
Pods completion

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-right corner of the editor. Alternatively, with the caret placed at pod 'Yams', press ⌥⏎, select Install, and press :

Install pods

When the library is installed, AppCode automatically reloads the project as a workspace.

Step 3. Load data from the remote YAML

In our application, the conference data model already exists — 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 don’t need to change anything in the current model.

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

Create a class for loading the data

Open the iOSConferences/Model/Data.swift file. Delete unnecessary code: the loadFile(_:) method and the conferencesData variable. Add a new class named ConferencesLoader and declare its conformance to ObservableObject:


public class ConferencesLoader: ObservableObject {

}

You can use the class live template for writing the class code faster: just type class and press .

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]()
}


Add the loadConferences() method that you’ll implement later:


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

    func loadConferences() {
    }
}

You can use the func live template for writing the method code faster: just type func and press .

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

In the loadConferences() method, call URLSession.shared.dataTaskPublisher(for:):


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

You can type just the first letters of the method name, and AppCode will suggest you appropriate options:
Camel Case

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.

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")

public class ConferencesLoader: ObservableObject {
    // ...
}


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 usage:

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

Create a YAML decoder

In the Data.swift file, import the Yams framework:


import Yams


Call the decode(_:from:) method for the created publisher and pass the [Conference] type and YAMLDecoder as the method parameters:

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


YAMLDecoder is highlighted red. Hover over the highlighted code to see the error message:

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


This means you need to create an extension for the YAMLDecoder which will conform to the TopLevelDecoder type. Import the Combine framework where the TopLevelDecoder type belongs to:

import Yams
import Combine


Add an extension of the YAMLDecoder type and declare its conformance to TopLevelDecoder:

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

extension YAMLDecoder: TopLevelDecoder {

}

public class ConferencesLoader: ObservableObject {
// ...
}


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

To open the protocol declaration, place the caret at its name and press ⌘B or select Go To | Declaration or Usages from the context menu.

To add the required property and method, you can use the corresponding code intention: 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


Again, place the caret at extension, 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:Decodable>(_ type: T.Type, from data: Input) throws -> T {
    try decode(type, from: String(data: data.data, encoding: .utf8)!)
}


As a result, the YAMLDecoder extension looks this way:

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

    public typealias Input = URLSession.DataTaskPublisher.Output
}

Load the data and handle errors

Get back to the loadConferences() method of the ConferencesLoader class and 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)
}

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

Use the eraseToAnyPublisher() method to erase the publisher’s actual type and convert it to AnyPublisher:


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


Add the completion parameter to the loadConferences() method’s declaration:

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


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 when receiving a value:

func loadConferences(completion: @escaping ([Conference]) -> Void) {
    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)
    })
}


Put the result of the URL session to a separate variable. With the caret placed inside the loadConferences() method, press ⌥⌘V, select the whole statement in the dropdown list that opens, and press . Name the variable as result, select the Declare with var and Specify type explicitly checkboxes, and press twice:

Extract variable

Move the variable declaration to the class level. With the caret placed at the variable line, press ⌥⏎ and select Split into declaration and assignment:

Split declaration and assignment

Add ? to the AnyCancellable type to make it optional and place the result variable declaration right below the @Published var conferences = [Conference]() line:


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


In the initializer where the loadConferences() method is called, pass the closure expression as the method’s parameter:

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

You can place the caret at the loadConferences() method’s call, press ⌥⏎, and click Apply Fix-it. AppCode will add the completion parameter and a placeholder for filling in its value.

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

Go to iOSConferences/ConferenceList.swift. 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 {
        // ...
    }
}


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) {
                // ...
        }
    }
}


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

Result
Your AppCode team
JetBrains
The Drive to Develop

Comments below can no longer be edited.

2 Responses to Tutorial: Use CocoaPods in your project

  1. Adrian says:

    June 3, 2020

    Unfortunately, using Cocoapods can really slow AppCode down – it seems to be all that extra code indexing. Here are some options for fixing that: https://luxmentis.org/appcode-is-slow-but-you-can-improve-it/

    • Stanislav Dombrovsky says:

      June 3, 2020

      Unfortunately, using Cocoapods can really slow AppCode down

      In fact, possible performance issues are not about the using Cocoapods itself, it’s more about adding the source code with complex dependencies, as you wrote in your article. It also could be a complex library subproject or anything that includes the source code we need to parse and cache. It’s also an SPM dependency, because it includes the source code and isn’t binary, so with SPM you will have the same situation (soon the initial SPM support will appear in 2020.2 EAP).

      You’re right that switching to binary form of frameworks can help reduce the amount of sources that needs to be processed by the IDE that automatically makes the overall peformance better, because with less symbols from dependencies you need less memory for the IDE, and IDE can process all the code assistance actions faster.

      Sad that you have such a feeling about the forum. As a person responsible for closing the AppCode forum (in fact, it’s not the company decision, it’s team decision, because our team is responisble for public resources related to AppCode), I’d like to say that we are still ready to investigate any of the performance issues you have in our public tracker. Our decision was simple: if we have a public tracker, it’s easier for us to sort out problems here, where we can link various issues to each other, find related problems, ask to attach the information needed for the investigation, easily notify users if need an additional information, track the version where particular issues appears (that is critically important for performance issues because issue that looks exactly the same, like “slow completion” can have totally different reasons in different AppCode versions, just becuase we have dozens of performance improvements in one of the versions), and much more. All of that wasn’t possible on our forum, so user wasn’t even able to see to which version this particular thread is related, that made a lot of users think that the issue just remains. It wasn’t even possible to attach the information needed, that caused us to redirect users to tracker just to be able to gather the information needed. Also, on the forum nobody see similar issues already created in tracker, and it’s strange to close the thread with the resolution like “it’s a known problem, see here”. We don’t think that’s a good approach, and that’s why we decided to leave the tracker only – and you still can submit any issue here, no matter if it’s formalized or not. We understand how hard is to understand where the problem is by yourself, and we are always ready to help.

Subscribe

Subscribe to product updates