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.
Step 1. Install CocoaPods
Download the iOSConferences project and open it in AppCode.
Select Preference dialog ⌃ ⌥ S, go to .
from the main menu. Alternatively, in theIn 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:
Click the Install CocoaPods button.
After the CocoaPods gem is installed, the list of pods is displayed on the Preferences dialog:
page of theStep 2. Add the Yams pod to the project
From the main menu, select .xcodeproj file and opened in the editor.
. The Podfile will be created in the same directory with theIn the Podfile, add the
Yams
pod under theiOSConferences
target:project 'iOSConferences.xcodeproj' target 'iOSConferences' do use_frameworks! pod 'Yams' endAfter you have added the
pod 'Yams'
code line, AppCode notifies you that the Podfile contains pods not installed yet. To install theYams
pod, click the Install pods link in the top-right corner of the editor. Alternatively, with the caret placed atpod 'Yams'
, press ⌥ ⏎, select Install, and press ⏎.
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
.
1. Create a class for loading the data
Open the iOSConferences/Model/Data.swift file.
Delete unnecessary code: the
loadFile(_:)
method and theconferencesData
variable.Add a new class named
ConferencesLoader
and declare its conformance toObservableObject
:public class ConferencesLoader: ObservableObject { }In the new class, add a
@Published
propertyconferences
that stores an array of theConference
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() { } }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() { } }
2. Create a publisher
In the
loadConferences()
method, call URLSession.shared.dataTaskPublisher(for:):func loadConferences() { URLSession.shared.dataTaskPublisher(for: url) }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 theurl
variable usage:func loadConferences() { URLSession.shared.dataTaskPublisher(for: url!) }
3. Create a YAML decoder
In the Data.swift file, import the Yams framework:
import YamsCall the decode(_:from:) method for the created publisher and pass the
[Conference]
type andYAMLDecoder
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 CombineAdd an extension of the
YAMLDecoder
type and declare its conformance toTopLevelDecoder
: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 theInput
property and the decode(_:from:) method.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 theInput
typealias:extension YAMLDecoder: TopLevelDecoder { public typealias Input = type }Specify the
URLSession.DataTaskPublisher.Output
data type here:public typealias Input = URLSession.DataTaskPublisher.OutputAgain, place the caret at
extension
, press ⌥ ⏎, select Do you want to add protocol stubs?, and click ⏎. This time, AppCode adds thedecode(_: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:
4. Load the data and handle errors
Get back to the
loadConferences()
method of theConferencesLoader
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) }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 theloadConferences()
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 thereceiveValue
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 asresult
, select the Declare with var and Specify type explicitly checkboxes, and press ⏎ twice:Move the variable declaration to the class level. With the caret placed at the variable line, press ⌥ ⏎ and select Split into declaration and assignment:
Add
?
to theAnyCancellable
type to make it optional and place theresult
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 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 theConferencesLoader
class:struct ConferenceList: View { @ObservedObject var conferenceLoader = ConferencesLoader() var body: some View { // ... } }Pass the list of the loaded conferences (
conferenceLoader.conferences
) to theList
initializer:struct ConferenceList: View { @ObservedObject var conferenceLoader = ConferencesLoader() var body: some View { NavigationView { List(conferenceLoader.conferences) { // ... } } }Run ⇧ F10 the application. Now the list consists of all conferences available in the remote YAML file: