Skip to main content

18 - Json, Async

Json/Rest - Codable protocol

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

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.

// 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.

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

let urlString = "https://someApiUrl"

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

Decode the data

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

import Foundation

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


typealias ListItems = [ListItem]

View

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

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.

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

    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

    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

    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

Button("GET"){
Task {
await listItemApi.get()
}
}

Or use .task modifier

 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

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