Photo by Michael Dziedzic on Unsplash
Difficulty: Beginner | Easy | Normal | Challenging
- This article skips over setting up a project, and gets rid of the storyboard and uses programmatic constraints as in this article
- Later in the article a real example is used, that is Decoding JSON and the internal workings of this are kind of skipped over
- If you are interested in testing, or a more in-depth explanation of JSON decoding you can look at this article
JSON: JavaScript Object Notation, a lightweight format for storing and transporting data Swift: An open source programming language for macOS, iOS, watchOS and tvOS
This Project is going to take data from an endpoint (the specific endpoint is one of those hosted at reqres) and displays the result in a UITextView
.
The end project looks something like the following:
So let us press on!
I've used [weak self] (the difference between weak and unowned is explained here )
Users Model The model is defined by the structure of the data from the endpoint. https://reqres.in/api/users?page=2 is
{"page":2,"per_page":6,"total":12,"total_pages":2,"data":[{"id":7,"email":"[email protected]","first_name":"Michael","last_name":"Lawson","avatar":"https://reqres.in/img/faces/7-image.jpg"},{"id":8,"email":"[email protected]","first_name":"Lindsay","last_name":"Ferguson","avatar":"https://reqres.in/img/faces/8-image.jpg"},{"id":9,"email":"[email protected]","first_name":"Tobias","last_name":"Funke","avatar":"https://reqres.in/img/faces/9-image.jpg"},{"id":10,"email":"[email protected]","first_name":"Byron","last_name":"Fields","avatar":"https://reqres.in/img/faces/10-image.jpg"},{"id":11,"email":"[email protected]","first_name":"George","last_name":"Edwards","avatar":"https://reqres.in/img/faces/11-image.jpg"},{"id":12,"email":"[email protected]","first_name":"Rachel","last_name":"Howell","avatar":"https://reqres.in/img/faces/12-image.jpg"}],"support":{"url":"https://reqres.in/#support-heading","text":"To keep ReqRes free, contributions towards server costs are appreciated!"}}
can be placed into http://json.parser.online.fr which gives us a clear view of the structure of the JSON. In a usual working situation you might well be given a clear API with documentation, but this online parser can still be useful.
Now usually we might expect that the properties in our mode struct
exactly mirror those of the endpoint. However in Swift we usually use Camel Case rather than snake case, and this is something will we cover when we decode the data - but in this case we can manually translate the Camel Case to Snake Case (for example per_page to perPage).
In order to decode the JSON using JSONDecoder()
the model must conform to Codable, and this applies to the struct
of the model (called User here) and any nested struct
.
Although in the video I decided NOT to use nested struct
instances for Users
, UserData
and Support
since the nested struct
instances are not referenced from outside the Struct
I feel that we are actually better to nest them.
struct Users: Codable {
let page: Int
let perPage: Int
let total: Int
let totalPages: Int
let data: [UserData]
let support: Support
struct UserData: Codable {
let id: Int
let email: String
let firstName: String
let lastName: String
let avatar: String
}
struct Support: Codable {
let url: String
let text: String
}
}
ViewController
The view controller will instantiate the ViewModel
through it's own initializer, requiring us to create the property and initializer
private let viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Most of the rest of the details in the class are about creating (the poorly named - sorry) UITextView
, where the action takes place mostly in viewDidLoad
.
private lazy var text: UITextView = .init(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
setupHierarchy()
setupComponents()
setupConstraints()
}
func setupHierarchy() {
view.addSubview(text)
text.translatesAutoresizingMaskIntoConstraints = false
}
func setupComponents() {
text.textAlignment = .center
text.isUserInteractionEnabled = false
text.text = "Placeholder"
}
func setupConstraints() {
NSLayoutConstraint.activate([
text.topAnchor.constraint(equalTo: view.topAnchor),
text.bottomAnchor.constraint(equalTo: view.bottomAnchor),
text.leadingAnchor.constraint(equalTo: view.leadingAnchor),
text.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
However the real action goes on in viewDidAppear
- now I know this loading will happen in front of the user (after all, the view will have appeared to the user before we even make the network call)
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.completion = {
let text = $0.data.map{ "\($0.firstName) \($0.lastName): \($0.email)" }.joined(separator: "\n")
DispatchQueue.main.async {
self.text.text = text
}
}
viewModel.download()
}
now we assign the completion hanlder in the view model (more to come of this later), which will return a Users
object. Now instead of explicitly defining a name for the object $0 is used as a shorthand for the first argument (and represents a the Users
array), and the second shorthand argument $0 represents each User in turn.
Now there is the full code in the repo at https://github.com/stevencurtis/SwiftCoding/tree/master/DecodeReqres, but here is the code reproduced:
SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
self.window = UIWindow(windowScene: windowScene)
let viewModel = ViewModel()
let viewController = ViewController(viewModel: viewModel)
let nav = UINavigationController(rootViewController: viewController)
self.window?.rootViewController = nav
self.window?.makeKeyAndVisible()
}
func sceneDidDisconnect(_ scene: UIScene) {
}
func sceneDidBecomeActive(_ scene: UIScene) {
}
func sceneWillResignActive(_ scene: UIScene) {
}
func sceneWillEnterForeground(_ scene: UIScene) {
}
func sceneDidEnterBackground(_ scene: UIScene) {
}
}
ViewController
class ViewController: UIViewController {
private let viewModel: ViewModel
private lazy var text: UITextView = .init(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
setupHierarchy()
setupComponents()
setupConstraints()
}
func setupHierarchy() {
view.addSubview(text)
text.translatesAutoresizingMaskIntoConstraints = false
}
func setupComponents() {
text.textAlignment = .center
text.isUserInteractionEnabled = false
text.text = "Placeholder"
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
viewModel.completion = {
let text = $0.data.map{ "\($0.firstName) \($0.lastName): \($0.email)" }.joined(separator: "\n")
DispatchQueue.main.async {
self.text.text = text
}
}
viewModel.download()
}
func setupConstraints() {
NSLayoutConstraint.activate([
text.topAnchor.constraint(equalTo: view.topAnchor),
text.bottomAnchor.constraint(equalTo: view.bottomAnchor),
text.leadingAnchor.constraint(equalTo: view.leadingAnchor),
text.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
override func loadView() {
let view = UIView()
view.backgroundColor = .red
self.view = view
}
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
ViewModel
class ViewModel {
var completion: ((Users) -> Void)?
func download() {
guard let url = URL(string: "https://reqres.in/api/users?page=2") else {return}
let task = URLSession.shared.dataTask(with: url, completionHandler: { [weak self] data, response, _ in
guard let data = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {return}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let decoded = try? decoder.decode(Users.self, from: data) {
self?.completion?(decoded)
}
})
task.resume()
}
}
Users
struct Users: Codable {
let page: Int
let perPage: Int
let total: Int
let totalPages: Int
let data: [UserData]
let support: Support
struct UserData: Codable {
let id: Int
let email: String
let firstName: String
let lastName: String
let avatar: String
}
struct Support: Codable {
let url: String
let text: String
}
}
Some people claim that mobile development is nothing but taking JSON strings and displaying them on screen. In that case, this article certainly has you covered.
In any case, this is a good start in processing the data that you would need to be able to do in order to create a worthwhile project in Swift.
I hope this is of help to you.
If you've any questions, comments or suggestions please hit me up on Twitter