Skip to content

18 - Json, Async

Json/Rest - Codable protocol

Codable - A type that can convert itself into and out of an external representation.

1
typealias Codable = Decodable & Encodable

Make your data types encodable and decodable for compatibility with external representations such as JSON.

The simplest way to make a type codable is to declare its properties using types that are already Codable. These types include standard library types like String, Int, and Double; and Foundation types like Date, Data, and URL. Any type whose properties are codable automatically conforms to Codable just by declaring that conformance.

1
2
3
4
5
6
7
// Landmark now supports the Codable methods init(from:) and encode(to:), 
// even though they aren't written as part of its declaration.
struct Landmark: Codable {
    var name: String
    var foundingYear: Int

}

Built-in types such as Array, Dictionary, and Optional also conform to Codable whenever they contain codable types.

Choose Properties to Encode and Decode Using Coding Keys

Codable types can declare a special nested enumeration named CodingKeys that conforms to the CodingKey protocol. When this enumeration is present, its cases serve as the authoritative list of properties that must be included when instances of a codable type are encoded or decoded.

If the keys used in your serialized data format don't match the property names from your data type, provide alternative keys by specifying String as the raw-value type for the CodingKeys enumeration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct Landmark: Codable {
    var name: String
    var foundingYear: Int
    var location: Coordinate
    var vantagePoints: [Coordinate]

    enum CodingKeys: String, CodingKey {
        case name = "title"
        case foundingYear = "founding_date"

        case location
        case vantagePoints
    }
}

Data

Load json from url - sync/blocking

1
2
3
4
5
6
7
let urlString = "https://someApiUrl"

if let url = URL(string: urlString) {
    if let data = try? Data(contentsOf: url) {
        // parse data
    }
}

Decode the data

1
2
3
4
5
6
7
func parse(json: Data) {
    let decoder = JSONDecoder()

    if let jsonData = try? decoder.decode(SomeDataClass.self, from: json) {
        items = jsonData
    }
}

Loading and parsing - Full Example

Data structure - Identifiable, Decodable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import Foundation

struct ListItem : Identifiable, Decodable {
    var id: UUID?
    var description: String
    var completed: Bool
}


typealias ListItems = [ListItem]

View

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
struct MainView: View {
    @State private var items: [ListItem] = []

    var body: some View {
        NavigationView {
            List {
                ForEach(items){ item in
                    NavigationLink {
                        Text(item.description)
                    } label: {
                        Text(item.description)
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing){
                    EditButton()
                }
            }
        }
        .onAppear {
            print("onAppear")
            loadData()
        }
        .onDisappear {
            print("onDisappear")
        }
    }

    private func deleteItems(offsets: IndexSet){
        withAnimation {
        }
    }

    private func loadData() {
        let urlString = "https://taltech.akaver.com/api/v1/listitems?apiKey=da933f9a-2471-4937-b69c-b276e42573eb"
        guard let url = URL(string: urlString) else { return }
        if let data = try? Data(contentsOf: url) {
            parseListItems(data)
        } else {
            print("Data contentsOf failed from \(urlString)")
        }
    }

    private func parseListItems(_ json: Data) {
        let decoder = JSONDecoder()
        if let jsonListItems = try? decoder.decode(ListItems.self, from: json) {
            items = jsonListItems
        } else {
            print("decoder.decode failed")
        }
    }
}

Example main takeaways

1
typealias ListItems = [ListItem]
  • typeAlias - allows to create new types on the go
  • guard - reverse of if let
  • ListItems.self - points out the class (same as in java - SomeClass.class)
  • .onAppear and .onDisappear closures - replaces viewDidAppear and viewDidDisappear
  • use init() for viewDidLoad - although it will be called even if view is never shown

ObservableObject, Publish, StateObject, ObservedObject

Create separate viewmodel. Has to conform to ObservableObject protocol. Use @Published attribute on properties whos updates should update UI.

When creating ObservableObject object use StateObject. When receiving already created object, use ObservedObject.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import Foundation
import Combine

class ListItemApi : ObservableObject {
    @Published var items :ListItems = []

    func loadData( ){
        let urlString = "https://taltech.akaver.com/api/v1/listitems?apiKey=da933f9a-2471-4937-b69c-b276e42573eb"
        guard let url = URL(string: urlString) else { return }

        URLSession.shared.dataTask(with: url) { data, response, error in
            DispatchQueue.main.async {
                self.items = try! JSONDecoder().decode(ListItems.self, from: data!)
            }
        }.resume()
    }
}

Use URLSession.shared.dataTask to do get request on background thread. Command is executed with .resume().

Change Published variables on main thread with DispatchQueue.

Rest API requests using URLSession

json print

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    func printJson(_ data: Data){
        do {
            let jsonObject = try JSONSerialization.jsonObject(with: data)
            guard let prettyJsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted) else {
                print("Error: Cannot convert JSON object to Pretty JSON data")
                return
            }
            guard let prettyPrintedJson = String(data: prettyJsonData, encoding: .utf8) else {
                print("Error: Could print JSON in String")
                return
            }

            print(prettyPrintedJson)
        } catch {
            print("Error: Trying to convert JSON data to string")
            return
        }
    }

GET

No data to send, receives data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    func get() async {
        let urlString = "https://taltech.akaver.com/api/v1/listitems?apiKey=da933f9a-2471-4937-b69c-b276e42573eb"
        guard let url = URL(string: urlString) else { return }

        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "GET"

        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            printJson(data)
            print(response)
        }
        catch {
            print("Error")
        }

    }

POST

Data to send, receives data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    func post(listItem: ListItem ) async {
       let urlString = "https://taltech.akaver.com/api/v1/listitems?apiKey=da933f9a-2471-4937-b69c-b276e42573eb"
       guard let url = URL(string: urlString) else { return }

       guard let encoded = try? JSONEncoder().encode(listItem) else {
           print("Failed to encode listItem")
           return
       }

       var request = URLRequest(url: url)
       request.setValue("application/json", forHTTPHeaderField: "Content-Type")
       request.httpMethod = "POST"

       do {
           let (data, response) = try await URLSession.shared.upload(for: request, from: encoded)
               printJson(data)
               print(response)
       } catch {
           print("Error")
       }
   }

PUT - similar to post

DELETE - similar to get

Calling async from UI

Either create Task context

1
2
3
4
5
Button("GET"){
    Task {
        await listItemApi.get()
    }
}

Or use .task modifier

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 var body: some View {
    List {
      ...
    }
    .task {
      await listItemApi.get()
    }
    .refreshable {
        await listItemApi.loadData()
    }
}

List supports also .refreshable - pull down to execute the async

If you're using a separate view model, make sure to mark async functions that update Published as @MainActor to ensure property updates get executed on the main actor.

Timer

Triggering repeated code execution with Timer.scheduledTimer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation

class TimerClass {
    var timer: Timer = Timer()
    var secondsElapsed = 0

    init() {
        start()
    }

    func start(){
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            self.secondsElapsed += 1
            print(self.secondsElapsed)
        }
    }

    func stop() {
        timer.invalidate()
    }
}

Reading

https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html