REST API macOS. Client Application.

В статье представлен вариант клиента для сервера приложения. В клиенте реализованы REST API и пользовательский интерфейс для работы с базой данных на сервере. Программа выполняет все операции CRUD.
Клиент написан в macOS 10.14.3, средствами XСode 10.1 и языка Swift 4.2. В проекте использована библиотека Alamofire и методика построения приложений предложенная Christina Moulton в ее книге iOS Apps with REST APIs
Пользовательский интерфейс и реализация REST API полностью разделены и находятся в разных классах и структурах. Вызовы функций REST инициируются из интерфейса пользователя. Формат запросов к серверу определяется их реализацией на сервере.
В прграмме использованы компоненты NSArrayController, NSTableView, NSImageView, NSTextField и другие. Пример клиента можно свободно загрузить
1. Интерфейс пользователя.
Вот так выглядит программа после старта.

В состав пользовательского интерфейса входят следующие элементы.
Таблицы с именами: Employe и Job - это объекты класса NSTableView. Каждая из таблиц подключена к соответствующему ей NSArrayController. Данные из NSArrayController отображаются в NSTableView. В объекты классов NSArrayController информация записывается в результате выполнения запросов к Web серверу. Таким образом, можно сказать, что информация в таблицах Employe и Job клиента - это текущая копия таблиц базы данных сервера.
Поля типа NSTextField, что находятся справа от таблиц предназначены для ввода данных оператором. Затем эти данные передаются в качестве параметров для запросов к серверу на добавления записей в базу данных.
Кнопки в составе таблиц с обозначением: «+», «-», … следует читать как: «Добавить», «Удалить», «Обновить», «Найти». При нажатии на любую из этих кнопок будет активирован соответствующий запрос к серверу приложения.
Элемент NSImageView, что находиться в верхней части приложения, предназначен для отображения фотографии выбранного в таблице Employe сотрудника.
Кнопка «Refresh All» запускает процесс считывания всех данных из таблиц Employe и Job на SQL сервере. Данные из этих таблиц передаются клиенту и отображаются в одноименных таблицах приложения.
2. Организация запросов к серверу.
Для дальнейшего чтения Вам понадобятся исходные тексты файлов WebServerController.cs и ViewController.swift.
По загрузке приложения, при выполнении функции viewDidLoad(), что находится в ViewController.swift, будут вызваны функции getEmployeAll() и getJobAll() которы сформируют GET запросы к серверу: https://localhost:5001/employe/all и https://localhost:5001/job/all. По этим запросам из таблиц на SQL сервере будут считаны все данные и переданы в клиент для отображения в таблицах Employe и Job. Запросы выполняются аналогично тем, что вызываются при нажатии на кнопку «Refresh All»
Каждая таблица NSTableView имеет кнопки для добавления, удаления, обновления и поиска данных. Нажатие на любую из этих кнопок приводит к формированию соответствующего запроса для сервера.
Например, при нажатии на кнопку «+» выполнятся POST запрос: https://localhost:5001/employe или https://localhost:5001/job/int в зависимости от таблицы, в которой нажата кнопка. Это запрос на добавление записи. По этому запросу в соответствующую таблицу на сервере будет добавлена новая запись. Данные для новой записи будут взяты из полей, что справа от каждой из таблиц. При добавлении записи в таблицу Job будет выполнена ее привязка к записи в таблице Employe. После операции добавления записи автоматически запускается операция на считывание всех данных из таблиц Employe и Job на SQL сервере, передача их в приложение клиент и отображение данных в таблицах NSTableView. Так двойные запросы здесь и далее синхронизируются по очередности.
При нажатии на кнопку «-» выполнятся DELETE запрос: https://localhost:5001/employe/int или https://localhost:5001/job/int в зависимости от таблицы в которой нажата кнопка. Это запрос на удаление записи. По этому запросу из соответствующей таблицы на сервере будет удалена указанная в запросе запись. Удаляемая запись должна быть выделена в таблице клиента. Далее запускается операция считывания данных из таблиц Employe и Job и передача их клиенту для отображения.
При нажатии на кнопку «Update» выполнятся PUT запрос: https://localhost:5001/employe/int или https://localhost:5001/job/int в зависимости от таблицы, в которой нажата кнопка. Это запрос на обновление записи. По этому запросу в соответствующей таблице на сервере будет обновлена указанная в запросе запись. Обновляемая запись должна быть выделена в таблице клиента и изменена. Далее запускается операция считывания данных из таблиц Employe и Job и передача их клиенту для отображения.
При нажатии на кнопку «Find» в таблице Employe выполнятся GET запрос: https://localhost:5001/?firstname=xxxx&secondname=xxxx&lastname=xxxx. Это запрос на поиск записи по указанным параметрам. Параметры должны быть введены в поля, что справа от таблицы. Указанная запись будет найдена в базе данных и передана в клиент для отображения.
Все ли разработанные на сервере запросы реализованы в клиенте? Нет. А что-же не реализовано? Например, одновременное добавление записей в обе таблицы или привязка записей в таблице Job к записям в Employe. При необходимости читатель может дописать их сам или обратиться ко мне.
В заключении, обратите внимание на изменения типа данных поля Photo при передаче с сервера в приложение клиент. Изначально, в C# это тип определен как byte[], затем Web сервер переводит это поле в тип base64, а в ViewController.swift он преобразуется для отображения в NSImageView в тип Data. Ну, а в базе данных SQL, что ожидаемо, это совершенно другой тип. :).
3. Исходный текст файла ViewController.swift.
Code

//  ViewController.swift

//  Test_osx

//

//  Created by Evgeny Veresov on 26.09.2018.

//  Copyright © 2018 Evgeny Veresov. All rights reserved.

//


import Cocoa

import Alamofire


// Перевод Date and Time в String

extension Date {

    var formatter: String{

        let dateFormatter        = DateFormatter()

        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

        let dateString           = dateFormatter.string(from:Date())

        return dateString

   }

}


// Имена таблиц

enum Table :String {

    case job        = "jobTable"

    case employe    = "employeTable"

}


// Расширение для включения кнопки редактирования 

extension ViewController:NSTextFieldDelegate {

    func controlTextDidEndEditing(_ obj: Notification) {

        if currentTable == Table.employe.rawValue {

            updateRecordEmployeOutlet.isEnabled = true

            return

        }

        if currentTable == Table.job.rawValue {

            updateRecordJobOutlet.isEnabled = true

            return

        }

        print("Ошибка при выборе таблицы обновления")

    }

}


class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {

    

    // Массив структур Employe

    @objc dynamic var myEmployeArray: [myEmploye]   = []

    // Массив структур Job

    @objc dynamic var myJobArray    : [myJob]       = []

    

    

    //Сылка на таблицу Job

    @IBOutlet var tableViewJob: NSTableView!

    // Ссылка на таблицу Employe

    @IBOutlet var tableViewEmploye: NSTableView!

    

    // Текущая таблица

    var currentTable:String = ""

    // Выбранная строка в таблице Job

    var selectRowJob:Int = -1

    // Выбранная строка в таблице Employe

    var selectRowEmploye:Int = -1

    

    // Признак первого запуска приложения

     var  flagLoadEndRefresh = true

    

    // Add Employe

    @IBOutlet var addRecordEmployeOutlet: NSButton!

    // Кнопка  Add Employe

    @IBAction func addRecordEmploye(_ sender: Any) {

        addEmployeTable(firstName: firstName.stringValue,

                        secondName: secondName.stringValue,

                        lastName: lastName.stringValue,

                        phone: phone.stringValue,

                        photo: image)

    }

    

    // Remove Employe

    @IBOutlet var removeRecordEmployeOutlet: NSButton!

    @IBAction func removeRecordEmploye(_ sender: Any) {

        // Если выбрана таблица Employe

        if currentTable == Table.employe.rawValue && selectRowEmploye != -1 {

            // Удалим запись

            deleteEmploye(numberRow: myEmployeArray[selectRowEmploye].employeId)

            tableViewJob.deselectRow(selectRowJob)

            tableViewEmploye.deselectRow(selectRowEmploye)

            //deleteRecOut.isEnabled = false

            removeRecordEmployeOutlet.isEnabled = false

            // Обнулим признаки

            emptyFlag()

            self.getEmployeAll()

            self.getJobAll()

        }

    }

    

    // Update Employe

    @IBOutlet var updateRecordEmployeOutlet: NSButton!

    @IBAction func updateRecordEmploye(_ sender: NSButton) {

        if currentTable == Table.employe.rawValue {

            updateEmploye(employeId:    myEmployeArray[selectRowEmploye].employeId,

                          firstName:    myEmployeArray[selectRowEmploye].firstName,

                          secondName:   myEmployeArray[selectRowEmploye].secondName,

                          lastName:     myEmployeArray[selectRowEmploye].lastName,

                          phone:        myEmployeArray[selectRowEmploye].phone,

                          photo:        image,

                          jobs:         nil)

            tableViewJob.deselectRow(selectRowJob)

            tableViewEmploye.deselectRow(selectRowEmploye)

            updateRecordEmployeOutlet.isEnabled = false

            removeRecordEmployeOutlet.isEnabled = false

        }

    }

    

    // Find in Employe

    @IBOutlet var findRecordEmployeOutlet: NSButton!

    @IBAction func findRecordEmploye(_ sender: Any) {

        if firstName.stringValue        != ""

            && secondName.stringValue   != ""

            && lastName.stringValue     != "" {

            // Очистим массив

            myEmployeArray.removeAll()

            // Сформируем параметры и вызовем функцию

            findEmployeParameter(param: "firstName="

                + firstName.stringValue

                + "&"

                + "secondName="

                + secondName.stringValue

                + "&"

                + "lastName="

                + lastName.stringValue)

            

            // Перезагрузим таблицу

            tableViewEmploye.reloadData()

        }

        else {

            print("Не заполнены поля для поиска ..")

        }

    }

    


    // Add Record Job

    @IBOutlet var addRecordJobOutlet: NSButton!

    @IBAction func addRecordJob(_ sender: NSButton) {

            addJobTable(

                titleJob: titleJobField.stringValue,

                dueDate: dueDataField.stringValue,

                isComplete: (isCompleteField != nil),

                assignedTo: assignedToField!.integerValue)

    }

    

    // Remove Job

    @IBOutlet var removeRecordJobOutlet: NSButton!

    @IBAction func removeRecordJob(_ sender: NSButton) {

        if currentTable == Table.job.rawValue && selectRowJob != -1 {

            // Удалим запись

            deleteJob(numberRow: myJobArray[selectRowJob].jobId)

            tableViewJob.deselectRow(selectRowJob)

            tableViewEmploye.deselectRow(selectRowEmploye)

            removeRecordJobOutlet.isEnabled = false

            // Обнулим признаки

            emptyFlag()

            self.getEmployeAll()

            self.getJobAll()

        }

    }

    

    // Update Job

    @IBOutlet var updateRecordJobOutlet: NSButton!

    @IBAction func updateRecordJob(_ sender: NSButton) {

        if currentTable == Table.job.rawValue

        {

            updateJob(titleJob:         myJobArray[selectRowJob].titleJob,

                      jobId:            myJobArray[selectRowJob].jobId,

                      dueDate:          myJobArray[selectRowJob].dueDate,

                      completedStatus:  Bool(myJobArray[selectRowJob].isComplete)!,

                      assignedTo:       myJobArray[selectRowJob].assignedTo)

            tableViewJob.deselectRow(selectRowJob)

            tableViewEmploye.deselectRow(selectRowEmploye)

            updateRecordJobOutlet.isEnabled = false

            removeRecordJobOutlet.isEnabled = false

        }

    }

    

    // Поля NSTextField на форме для таблицы Job

    @IBOutlet var titleJobField: NSTextField!

    @IBOutlet var dueDataField: NSTextField!

    @IBOutlet var isCompleteField: NSTextField!

    @IBOutlet var assignedToField: NSTextField!

    

    // Поля NSTextField на форме для таблицы Employe

    @IBOutlet var firstName: NSTextField!

    @IBOutlet var secondName: NSTextField!

    @IBOutlet var lastName: NSTextField!

    @IBOutlet var phone: NSTextField!

    @IBOutlet var photo: NSTextField!

    

    

    // Фотография сотрудника

    @IBOutlet var imageEmploye: NSImageView!

    

    //End Load

    override func viewDidLoad() {

        super.viewDidLoad()

        // Значение для Iscomplete

        isCompleteField.stringValue = "true"

        // Значение для date

        dueDataField.stringValue = Date().formatter

        

        // Выключим кнопки

        removeRecordEmployeOutlet.isEnabled = false

        updateRecordEmployeOutlet.isEnabled = false

        removeRecordJobOutlet.isEnabled = false

        updateRecordJobOutlet.isEnabled = false

        

        // Установим размер шрифта в заголовках таблиц

        tableViewJob.tableColumns.forEach { (column) in

            column.headerCell.attributedStringValue = NSAttributedString(string: column.title, attributes: [NSAttributedString.Key.font: NSFont.titleBarFont(ofSize: 13.5)])

        }

        tableViewEmploye.tableColumns.forEach { (column) in

            column.headerCell.attributedStringValue = NSAttributedString(string: column.title, attributes: [NSAttributedString.Key.font: NSFont.titleBarFont(ofSize: 13.5)])

        }

        

        // Считаем все записи таблицы Employe

        getEmployeAll()

        // Считаем все записи таблицы Job

        getJobAll()

    }

    

    

    // Кнопка сброса

    @IBAction func refreshAll(_ sender: Any) {

        removeRecordEmployeOutlet.isEnabled = false

        updateRecordEmployeOutlet.isEnabled = false

        

        removeRecordJobOutlet.isEnabled = false

        updateRecordJobOutlet.isEnabled = false

        

        // Установим фокус на FirstName

        self.view.window?.makeFirstResponder(self.firstName)

        // Обновим все

        clearField()

        self.getEmployeAll()

        self.getJobAll()

    }

    

    // Add record Job при нажатии Enter

    @IBAction func enterAssignTo(_ sender: NSTextField) {

        addJobTable(

            titleJob: titleJobField.stringValue,

            dueDate: dueDataField.stringValue,

            isComplete: (isCompleteField != nil),

            assignedTo: assignedToField!.integerValue)

        self.view.window?.makeFirstResponder(self.titleJobField)

    }

    

    

    // ******************* FUNCTIONS **********************

    // Click пользователя на строке таблицы Job, Employe

    func tableViewSelectionDidChange(_ notification: Notification) {

        let selectTable = notification.object as! NSTableView

        let identifier = selectTable.identifier

        // Job

        switch identifier?.rawValue {

        case Table.job.rawValue:

            if tableViewJob.selectedRow != -1 {

                selectRowJob = tableViewJob.selectedRow

                tableViewEmploye.deselectRow(selectRowEmploye)

                currentTable = (identifier?.rawValue)!

                removeRecordJobOutlet.isEnabled = true

            } else {

                currentTable = ""

            }

        //Employe

        case Table.employe.rawValue:

            if  tableViewEmploye.selectedRow != -1 {

                selectRowEmploye = tableViewEmploye.selectedRow

                tableViewJob.deselectRow(selectRowJob)

                currentTable = (identifier?.rawValue)!

                let charArray = myEmployeArray[selectRowEmploye].photo

                let data = NSData(base64Encoded: charArray, options: NSData.Base64DecodingOptions(rawValue: 0))

                imageEmploye.imageScaling = .scaleAxesIndependently

                imageEmploye.image = NSImage(data: data! as Data)

                removeRecordEmployeOutlet.isEnabled = true

            } else {

                currentTable = ""

            }

        default:

            currentTable = ""

            print("Ошибка при выборе таблицы!")

            break

        }

    }

    

    // Get запрос всех записей в таблице Employee

    func getEmployeAll() -> Void {

        Employe.employeByAll(all: "all") { result in

            // Ошибка при получении данных

            if let error = result.error {

                print("Ошибка GET запроса emplloye/all")

                print(error)

                return

            }

            // Если значение равно nil

            guard let employeArray = result.value else {

                print("GET запрос employe/all, result.value = nil")

                return

            }

            /*

            // Если все хорошо - распечатаем

            for item in employeArray {

                print(item.description())

            }

             */

            // Выведем в ArrayController

            self.copyArrayControllerEmploye(employe: employeArray)

            // Перезагрузим Employe

            self.tableViewEmploye.reloadData()

  

             // Выделим первую запись в таблице Employe

             if self.flagLoadEndRefresh == true {

                self.view.window?.makeFirstResponder(self.tableViewEmploye)

                self.tableViewEmploye.selectRowIndexes(NSIndexSet(index: 0) as IndexSet, byExtendingSelection: false)

                self.flagLoadEndRefresh = false

            }

            

        }

    }

    

    // Get запрос всех записей таблицы Job

    func getJobAll() -> Void  {

        Job.jobByAll(all: "all") { result in

            // Ошибка при получении данных

            if let error = result.error {

                print("Ошибка GET запроса job/all")

                print(error)

                return

            }

            // Если значение равно nil

            guard let jobArray = result.value else {

                print("GET запрос job/all, result.value = nil")

                return

            }

            /*

            // Если все хорошо - распечатаем

            for item in jobArray {

                print(item.description())

            }

             */

            // Выведем в таблицу Job

            self.copyArrayControllerJob(job: jobArray)

            self.tableViewJob.reloadData()

        }

    }

    

    // Добавление записи в таблицу Job

    func addJobTable (titleJob: String, dueDate:String, isComplete:Bool, assignedTo:Int) {

        // Создадим новую структуру Job

        guard let newJob = Job(titleJob: titleJob,

                               jobId: nil,

                               dueDate: dueDate,//T11:40:17.713187",

            completedStatus: isComplete,

            assignedTo: assignedTo)

            

            else{

                print("Error create newJob")

                return

        }

        newJob.save(linkEmploye:assignedToField.integerValue) { result in

            guard result.error == nil else {

                print("Error calling POST on Job")

                print(result.error!)

                return

            }

            guard let jobIdValue = result.value else {

                print("Error calling POST on Job. result is nil")

                return

            }

            // Выведем все ID Job привязанные к первой записи в Employe

            print(jobIdValue)

            self.getEmployeAll()

            self.getJobAll()

        }

    }

    

    // Добавление новой записи в таблицу Employee

    func addEmployeTable (firstName:String, secondName:String, lastName:String, phone:String, photo:String){

        // Создадим новую структуру Employe

        guard let newEmploye = Employe(employeId: nil, firstName: firstName, secondName: secondName, lastName: lastName, phone: phone, photo: image, jobs: nil )

            else {

                print("Error create newEmploye")

                return

        }

        newEmploye.save() { result in

            guard result.error == nil else {

                print("Error calling POST on Job")

                print(result.error!)

                return

            }

            guard let jobIdValue = result.value else {

                print("Error calling POST on Job. result is nil")

                return

            }

            // Все хорошо!

            self.getEmployeAll()

            print(jobIdValue)

        }

    }

    


    // Поиск в таблице Employee по параметрам

    func findEmployeParameter (param: String){

        Employe.employeByParam(parameter: param) { result in

            // Ошибка при получении данных

            if let _ = result.error {

                print("Ошибка GET запроса emplloye/parameter")

                return

            }

            // Если значение равно nil

            guard let employeArray = result.value else {

                print("GET запрос employe/parameter, result.value = nil")

                return

            }

            /*

            // Если все хорошо - распечатаем

            for item in employeArray {

                print(item.description())

            }

             */

            // Выведем в таблицу Employe

            self.copyArrayControllerEmploye(employe: employeArray)

            self.tableViewEmploye.reloadData()

        }

    }

    

    

    // Заполнение ArrayController для Employe

    func copyArrayControllerEmploye (employe:[Employe]){

        self.myEmployeArray.removeAll()

        if employe.count > 0 {

            for i in 0 ... employe.count - 1 {

                self.myEmployeArray.append(myEmploye())

      

                self.myEmployeArray[i].employeId    = employe[i].employeId!

                self.myEmployeArray[i].firstName    = employe[i].firstName

                self.myEmployeArray[i].secondName   = employe[i].secondName

                self.myEmployeArray[i].lastName     = employe[i].lastName

                self.myEmployeArray[i].phone        = employe[i].phone

                self.myEmployeArray[i].photo        = employe[i].photo

                let jobs = employe[i].jobs

                var strJobsId = ""

                for job in jobs! {

                    strJobsId = strJobsId + String(job.jobId!) + ";"

                }

                self.myEmployeArray[i].jobs         = strJobsId

            }

        }

    }

    

    // Заполнение  ArrayController для Job

    func copyArrayControllerJob (job:[Job]){

        self.myJobArray.removeAll()

        if job.count > 0 {

            for i in 0...job.count - 1 {

                self.myJobArray.append(myJob())

                

                self.myJobArray[i].titleJob     = job[i].titleJob

                self.myJobArray[i].jobId        = job[i].jobId!

                self.myJobArray[i].dueDate      = job[i].dueDate

                self.myJobArray[i].isComplete   = String(job[i].isComplete)

                self.myJobArray[i].assignedTo   = job[i].assignedTo!

            }

        }

    }

    

    

    // Обновление записи в таблице Employe

    func updateEmploye (employeId: Int?, firstName: String, secondName: String, lastName: String, phone: String, photo: String, jobs: [Job]?) {

        

        // Создадим новую структуру Employe

        guard let newEmploye = Employe(employeId: nil, firstName: firstName, secondName: secondName, lastName: lastName, phone: phone, photo: image, jobs: nil)

            else {

                print("Ошибка при создании Employe в PUT запросе")

                return

        }

        newEmploye.update(employeId: employeId!) { result in

            guard result.error == nil else {

                print("Ошибка при запросе обновлении Employe")

                print(result.error!)

                return

            }

            guard let updateId = result.value else {

                print("Ошибка Id в Employe")

                return

            }

            // Все хорошо!

            print(updateId)

            self.getEmployeAll()

            self.getJobAll()

        }

    }

    

    

    // Обновление записи в таблице Job

    func updateJob (titleJob: String, jobId : Int, dueDate:String, completedStatus:Bool, assignedTo:Int) {

        // Создадим новую структуру Job

        guard let newJob = Job(titleJob: titleJob, jobId: nil, dueDate: dueDate, completedStatus: completedStatus, assignedTo : assignedTo)

            else {

                print("Ошибка при создании Job в PUT запросе")

                return

        }

        newJob.update(jobId: jobId) { result in

            guard result.error == nil else {

                print("Ошибка при запросе обновлении Job")

                print(result.error!)

                return

            }

            guard let updateId = result.value else {

                print("Ошибка Id в Job")

                return

            }

            // Все хорошо!

            self.getEmployeAll()

            self.getJobAll()

            print(updateId)

        }

    }

 

    // Удаление записи в таблице Job

    func deleteJob(numberRow:Int){

        Alamofire.request(TodoRouter.deleteJob(numberRow))

            .responseJSON { response in

                guard response.result.error == nil else {

                    print("Ошибка при удалении записи Job..")

                    print(response.result.error!)

                    return

                }

                print("Delete Row \(numberRow) in Job OK!")

                // Перезагрузим таблицу job

                self.getJobAll()

        }

    }


    // Удаление записи в таблице Employe

    func deleteEmploye(numberRow:Int) -> Void {

        Alamofire.request(TodoRouter.deleteEmploye(numberRow))

            .responseJSON { response in

                guard response.result.error == nil else {

                    print("Ошибка при удалении записи Employe")

                    print(response.result.error!)

                    return

                }

                print("Delete row \(numberRow) in Employe OK!")

                self.imageEmploye.image = nil

                // Перезагрузим таблицы

                self.getEmployeAll()

                self.getJobAll()

        }

    }

    

    // Обнуления признаков при удалении Row

    func emptyFlag() {

        currentTable = ""

        selectRowJob = -1

        selectRowEmploye = -1

    }

    

    // Очистка полей ввода

    func clearField() {

        firstName.stringValue       = ""

        secondName.stringValue      = ""

        lastName.stringValue        = ""

        phone.stringValue           = ""

        

        titleJobField.stringValue   = ""

        assignedToField.stringValue = ""

    }

}



Если есть вопросы - спрашивайте не стесняйтесь.
Всего доброго, Евгений Вересов.
10.03.2019 года.