28 - 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