Communications from Go Backend to SwiftUI Frontend with JSON API

Let’s establish the communication between server API and client. We will deal with janitor work of programming. It consists of structuring values of data fields into groups, storing them long term and unpack them to work with.

Communications from Go Backend to SwiftUI Frontend with JSON API
Photo by Ferenc Almasi / Unsplash

This might seem overly complicated. So, why? In a real world applications not everyone should know about all the data, some calculations should be done on a server, some on a client, some data needs to be shared, other needs to be private.

This post will not limit itself to unwrapping JSON data in Swift and Go, but also discuss tools how to create a more complicated data structure. A founder might not have a team of engineers that each can focus on a single programming language. But it is not necessary to master a programming language with all its glory. It is fine if we can mange the parts we need and learn as we go.

This post will provide everything you need from planning the communication flow and get it going from backend to frontend. We will not deal with databases though.

  1. Define JSON Structure
  2. Create basic backend API server in Go
    1. Convert JSON Structure to Golang types
  3. Test JSON API
  4. Connect UI to Backend API
    1. Convert JSON Structure to Swift structs

We will use XCode 13.1, Swift 5 and iOS 15.1.

Define JSON Structure

The way we structure data needs to be consistent along database, backend, JSON API and the front end. Like using a software for UI mockups (Figma, Sketch), it is helpful to have tool to define data structures like jsoneditoronline.org. This will serve as a source of truth for the data structures. I use this because I can save structures. Later we will use quicktype to convert the JSON structure into structs in Swift and Go.

Create basic API server in Go

Install Go

Install Go from its website https://go.dev/doc/install.

Open the commandline and type go version to check if the installation worked.

$ go version
go version go1.18.2 darwin/amd64

I use Visual Studio Code with the Go extension as code editor and echo as a framework.

Create a new project by creating a folder and changing into the new directory. Then initialize the new app and fetch the echo framework from GitHub.

$ mkdir jsonserver && cd jsonserver
$ go mod init jsonserver
$ go get github.com/labstack/echo/v4

Hello World

Now write a short server in Go. It will display Hello World when called with a browser. In the next chapter we will output a JSON structure instead.

package main

import (
	"net/http"
	
	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

The crucial function is e.GET(). It says wenn we call the root URL / it will call a function that returns a string. We will create a separate function to do this next.

Save this file as server.go and from the terminal run $ go run server.go.

$ go run server.go
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.7.2
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:1323

Success! Open a browser with the URL 127.0.0.1:1323/and you should see Hello, World! in the browser window.

Convert JSON Structure to Golang types

First we create a sample file groceries.json where we will coding books.

{
"title" : "Weekend groceries"
}

{ } in JSON indicates a dictionary and not a function like in Go or Swift. A dictionary consists of key and value pairs. Later we can access the title by printing print(groceries.title). Parentheseses ""contains text or a string. Numbers on the other hand don’t need parentheses and are of type Int. These are basic types. We can also create our own types or structs with multiple fields.

We will create a struct to load this structure into an object in Go. After the import segment insert the following lines to declare how the structure of an object of type groceries looks like.

Note: The purpose here is to make programming easier. The code editor will suggest existing fields and give an error when a field doesn’t exist. There are other languages like python that are untyped or where types are optional. Working without types, classes, structs and objects is easier to learn, but more error prone later.

type Groceries struct {
	title 	string 	'json:"title"'
}

Title is the field name, string is the type and ‘json:”title"’ contains the corresponding json field, which usually is the same, but it doesn’t have to be.

Next we change the URL from root / to /api/v1/groceries using groups calling the function readJSON() that reads and returns the groceries.json file. Add the following in the main() function after initialising the echo e framework.

apiGroup := e.Group("/api")
v1 := v1.Group("/v1")

v1.GET("/groceries", func(c echo.Context) error {
		books := readJSON()

		return c.JSON(http.StatusOK, groceries)
		}
	)

Return JSON File

We still need to define the function readJSON().

func readJSON() book {
	file, err := ioutil.ReadFile("./groceries.json")
	
	if err != nil {
		log.Fatal(err)
	}

	b := book{}
	err json.Unmarshal(file, &b)

	if err != nil {
		log.Fatal(err)
	}
	return b
}

Visual Studio Code automatically adds required imports, but if it doesn’t here is the full listing with the added imports.

package main

import (
	"net/http"
	
	"encoding/json"
	"io/ioutil"
	"log"

	"github.com/labstack/echo/v4"
)

type Book struct {
	title 	string 	'json:"title"'
}

func readJSON() book {
	file, err := ioutil.ReadFile("./groceries.json")
	
	if err != nil {
		log.Fatal(err)
	}

	b := book{}
	err json.Unmarshal(file, &b)

	if err != nil {
		log.Fatal(err)
	}
	return b
}

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})

	apiGroup := e.Group("/api")
	v1 := v1.Group("/v1")

	v1.GET("/books", func(c echo.Context) error {
		books := readJSON()

		return c.JSON(http.StatusOK, books)
		}
	)
	e.Logger.Fatal(e.Start(":1323"))
}

We run this again with go run server.go and open a browser http://127.0.0.1:1323/api/v1/groceries.

Integrate an advanced JSON Model

If everything worked so far (and write a comment below if it doesn’t), we can move on to a more advanced data structure. The issue is that the JSON structure and the fields in the Go and Swift structs need to match. When fields are labelled wrong, are missing or have the wrong type the app crashes. Then it is important to have some kind of error message to tell you where to look for any issues.

Open JSONEditorOnline to create your own data structure or use the following grocery list.

{
  "list": {
       "title": "Dinner",
       "uid": "wq3413lkj"
  },
 "items": [{
    "name": "flour",
    "uid": "asd3lpr2j",
    "quantity": {
      "value": 500,
      "unit": "g"
    },
    "brand": "Natura",
    "expired": "12-12-2022",
    "image": "s3dasfsf"
     },
     
    {
    "name": "potatoes",
    "uid": "4h3q4erg",
    "quantity": {
      "value": 2,
      "unit": "kg" },
    "brand": "regio",
    "expired": "08-12-2022",
    "image": "s3/2343gvdv" }]
}

Also save this list as groceries.json in the Go folder. Before we will move to the front end, let me introduce a tool to test APIs. When APIs become more complex you don’t want to test each request by hand again and again.

Test JSON API

To test APIs I use Talend API Tester as a Chrome Browser extension, but there are others available.

Connect UI to Backend API

The backend now has the most basic functionality. You can expand it with user authentication, or attach other micro services. Now we will move to the front end . It will display the data encoded in JSON to the user. You will need XCode 13.1 or above, some functionality like async is not available in earlier versions and later versions don’t work on older macs.

Open Xcode to create a new project.

Create a new Folder Services and a SwiftUI File JSONService.swift.

Create a new struct JSONContentUI: View. To store the data we create a @StateObject and call it groceries. This will automatically update the UI when data is changed.

struct JSONContentUI: View {
@StateObject var groceries = GroceriesAPI()	
	var body: some View {
	
		List {
			ForEach(groceries.groceries, id: \.self) { grocery in
				HStack {
					Text(grocery.list.title)
			}
		}
		.onAppear {
			groceries.fetchJSON()
		}
	}

}

As you can see we use a class GroceriesAPI which has the function fechJSON(). Let’s implement this class next.

First we define the source URL. This might fail because of an invalid URL, so we need to guard it. We then open an URLSessionto obtain the JSON as a string and print it.

class GroceriesAPI: ObservableObject {
	@Published var groceries: [groceries] = []
	
	func fetchJSON() {
		guard let url = URL(string: "http://127.0.0.1:1323/api/v1/groceries") else { return }
		
		let task URLSession.shared.dataTask(with: url) { data, response, error in
				if let data = data, let string = String(data: data, encoding: .utf8) {
					print(string)
				}
	
		// Decode JSON data
	}

}

Convert JSON Structure to Swift structs

Then we add the structs to encode groceries. We paste the JSON structure on quicktype, name it Groceries and select Swift as a target language and copy the structs to the JSONService.swift file.

// MARK: - Welcome
struct Welcome: Codable {
    let list: List
    let items: [Item]
}

// MARK: - Item
struct Item: Codable {
    let name, uid: String
    let quantity: Quantity
    let brand, expired, image: String
}

// MARK: - Quantity
struct Quantity: Codable {
    let value: Int
    let unit: String
}

// MARK: - List
struct List: Codable {
    let title, uid: String
}

Decode JSON to struct

We then expand fetchJSON() to decode the JSON string.

let json = try JSONDecoder().decode([user].self, from: data)

To catch errors we wrap this in a do {} catch {} block. We then assign the encoded JSON groceries object to the groceries StateObject that is read by the UI.

do {
    let json = try JSONDecoder().decode([groceries].self, from: data)
    print(json[0])

    DispatchQueue.main.async {
        self.groceries = json
   	}
} catch {
	print("JSON Format does not comply with struct keys.")
}

Conclusion

So congratulations if you made it this far. There was a lot of stuff in here, I am writing these articles as I learn them myself, so please feel free to add any recommendations of issues you had while going through this.

Subscribe to sebastianroy.de

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe