Захранване на приложение Job Board с помощта на Strapi и SwiftUI

В този урок ще изградим приложение за табло за работа за iOS, използвайки SwiftUI и Strapi. Ще научим как да разширяваме колекциите на Strapi, да добавяме персонализирани маршрути и манипулатори на HTTP заявки (контролери). Ще използваме персонализирани функции на контролера, за да обработваме качвания на файлове, свързани с конкретен запис. Ще добавим функционалност на уеб сокети, за да позволим на кандидат и рекламодател да си изпращат съобщения в реално време. Ще използваме приставката за потребителски разрешения, за да удостоверим всяка връзка на сокет.

Контур

  • Въведение
  • Предпоставки
  • Сдвояване на SwiftUI със Strapi
  • Създаване на проект Strapi
  • Създаване на колекции
  • Разширяване на маршрути за събиране и контролери
  • Настройка и удостоверяване на сокет
  • Инициализиране на SwiftUI проект
  • Инсталиране на Socket IO Client
  • Настройка на мрежови услуги и модели
  • Поток на удостоверяване на SwiftUI
  • Заключение.

Въведение

Борсите за работа направиха своя дебют в интернет към края на 20-ти век по време на ранните етапи на интернет. Най-ранното табло за работа е разработено от компания на име NetStart Inc през 1995 г. Платформата позволява на работодателите да публикуват свободни работни места, а на търсещите работа да търсят и да кандидатстват за тях. Това беше доста впечатляващ уебсайт за времето си и може би смятан за пионер на онлайн борсите за работа. NetStart Inc се трансформира през годините и в момента е известен като CareerBuilder.

Таблата за работа се разраснаха от обикновени стари уебсайтове за създаване, четене, актуализиране и изтриване (CRUD) до уебсайтове с функции като предупреждения за работа чрез имейл и насочени известия, проследяване на приложения и анализ на автобиография. Освен това се появиха бордове за работа, които обслужват конкретни индустрии и професии. Някои дори могат да се считат за платформи за кариерни мрежи, където потребителите могат да си гарантират силните страни и умения.

В момента в интернет има много обяви за работа. Повечето организации и големи кооперации имат табло за работа на собствените си уебсайтове, където изброяват свободните позиции в своята работна сила. В този урок ще изграждаме приложение за табло за работа с помощта на SwiftUI. Приложението ще бъде приложение за iOS, което е силно зависимо от екземпляр на Strapi Server.

Предпоставки

  • Инсталация на NodeJS.
  • Познания за SwiftUI за начинаещи

Интегрирана среда за разработка, аз използвам VS Code, но вие сте свободни да използвате други.

  • Предварителните познания за Strapi са полезни, но не са задължителни — Научете основите на Strapi v4.
  • Основни познания за удостоверяване на уеб токени JSON.

Сдвояване на SwiftUI със Strapi

SwiftUI е набор от инструменти за потребителски интерфейс (UI), разработен от Apple през 2019 г. Той се основава на езика за програмиране „Swift“ на Apple. SwiftUI улеснява процеса на създаване на потребителски интерфейси, като предоставя декларативен програмен модел, който позволява на разработчиците да очертаят структурата и поведението на компонентите на потребителския интерфейс в кратък и лесен за четене синтаксис. Освен това SwiftUI поддържа адаптивни оформления на потребителския интерфейс, което позволява на разработчиците да се грижат за различни размери на дисплея, резолюции и ориентации. Това помага за рационализиране на процеса на разработка на софтуер за визуално привлекателни потребителски интерфейси на платформи на Apple.

Чрез сдвояването на SwiftUI със Strapi ние сме в състояние да създаваме приложения, които комуникират ефективно, тъй като SwiftUI има реактивни UI възможности, което означава, че всяка промяна на данните от нашия бекенд на Strapi ще бъде изобразена почти незабавно по ресурсно ефективен начин. В допълнение към това, Strapi е сравнително лесен за използване и може да се използва за създаване на потребителски интерфейси за програмиране на приложения (API) в рамките на минути. Когато се комбинират с предварително изградените UI компоненти на SwiftUI, данните могат бързо да бъдат извлечени и показани на всички устройства в екосистемата на Apple. Това намалява времето за разработка на софтуерен проект чрез намаляване на количеството код, необходим за настройка на напълно функционираща система.

И Strapi, и SwiftUI могат да се персонализират много, което ги прави наистина мощни инструменти за разработка на решения. Strapi може да бъде мащабиран, за да поеме голям потребителски трафик.

Настройване на Strapi

За да започнем, ще започнем, като настроим нашия екземпляр на сървър Strapi. Strapi е силно зависим от Node. Уверете се, че сте го инсталирали на вашата машина за разработка. Ще използваме командата npx, която е програма за изпълнение на пакети на възли, която ще изпълни скрипт и ще създаде нов проект на strapi в папка на проекта в текущата работна директория.

Отворете подканата на терминала или командния ред (cmd/terminal) и изпълнете следната команда, за да създадете скеле на сървъра Strapi.

npx create-strapi-app@latest backend --quickstart

Командата създава оголена система Strapi, извлича и инсталира необходимите зависимости от файла package.json, след което инициализира SQLite база данни. Други системи за управление на бази данни могат да се използват с помощта на следното ръководство. За да работи SQLite, може да се наложи да го инсталирате чрез следната връзка. След като всяка зависимост бъде инсталирана, вашият браузър по подразбиране ще се отвори и ще визуализира страницата за регистрация на администратор за Strapi. Попълнете всички задължителни полета, за да създадете своя администраторски акаунт, след което ще бъдете добре дошли на страницата по-долу.

Създаване на колекции

Ще използваме приставката за създаване на тип съдържание на Strapi, за да дефинираме схема, която ще опише структурата и правилата за организиране и валидиране на данни в базата данни. Той ще ръководи базата данни как искаме да се съхраняват нашите записи. Ще започнем със създаване на фирмена схема, схема на работа и схема на приложение. Схемата на приложението и схемата на компанията ще имат връзка с потребителската схема, която беше инициирана, след като направихме скеле на strapi сървъра. Потребителската схема съдържа необходимите полета, необходими за идентифициране на потребител на системата.

Фирмена колекция

Тази колекция ще се използва за подробности за компанията.

  1. Кликнете върху Content-type Builder под плъгини в страничната лента за навигация.
  2. Кликнете върху Създаване на нов тип колекция.
  3. Въведете Company за Показвано име и щракнете върху Продължи.
  4. Щракнете върху бутона Текстово поле.
  5. Въведете n``ame в полето Име.
  6. Повторете стъпките по-горе и добавете текстови полета за address, email, phone, bio и category в колекцията.
  7. Създайте медийно поле и го задайте на едно поле за тип медия. В полето за въвеждане на име въведете logo. Това поле ще се използва за запазване на логото на компанията. Типът лого ще бъде свързан с качвания с файлови разширения на изображения, т.е. .jpg и .png.
  8. Създайте поле за връзка и в името на полето на компанията въведете тип представител.
  9. Изберете User(from: users-permission) от падащото меню и се уверете, че опцията Company has one user е избрана, както е показано по-долу.

Полето за релация се използва за създаване на връзка между две таблици на база данни. В нашия случай ние свързваме всеки потребител с компания. Ще можем да получим всички подробности за потребителя от приставката user-permissions. Създадената от нас връзка гарантира, че всяка компания има само един потребител. Представителната колона в таблицата с котировки ще се използва за съхраняване на външния ключ, който е идентификаторът на потребителя. Така че всяка компания има един представител в приложението Job Board.

Щракнете върху бутона Запазване и изчакайте промените да бъдат приложени.

Колекция работа

Тази схема ще се използва за съхраняване на подробности за работата. Той ще бъде свързан с конкретна компания. Следвайте стъпките по-долу, за да настроите схемата.

  1. Кликнете върху Content-type Builder под плъгини в страничната лента за навигация.
  2. Кликнете върху Създаване на нов тип колекция.
  3. Въведете Job за Показвано име и щракнете върху Продължи.
  4. Създайте текстови полета за следните name, description, type, status и environment.
  5. Създайте поле за връзка и в полето за въвеждане на кандидатстване за работа въведете тип компания. Изберете фирма от падащото меню вдясно. Уверете се, че опцията company has many jobs е избрана, както е показано по-долу.

Връзката по-горе гарантира, че всяка работа е свързана с компания. Една компания може да има много работни места. Името на полето jobs ще създаде връзка, която ще бъде видима от колекцията на компанията.

Колекция от приложения

Тази схема ще отговаря за съхраняването на подробности за кандидатстването за работа, като автобиография на кандидата и статус на кандидатстване.

  1. Кликнете върху Content-type Builder под плъгини в страничната лента за навигация.
  2. Кликнете върху Създаване на нов тип колекция.
  3. Въведете Application за Показвано име и щракнете върху Продължи.
  4. Създайте текстово поле с име status.
  5. Добавете две релационни полета с име job, което се свързва с колекцията от задачи, и другото с име applicant, което се свързва с потребителската колекция.

Горната релация гарантира, че потребителят може да подаде толкова приложения, колкото иска. Всяка кандидатура е свързана с конкретна работа. Една работа може да има много приложения, както е показано по-долу.

  1. Създайте медийно поле с име cv, задайте го на единичен тип. Накрая под предварителната настройка се уверете, че само опцията files е маркирана, както е показано по-долу.

Колекция съобщения

Тази колекция ще отговаря за запазването на разговорите между представител на компанията и кандидат.

  1. Кликнете върху Content-type Builder под плъгини в страничната лента за навигация.
  2. Кликнете върху Създаване на нов тип колекция.
  3. Въведете Message за Показвано име и щракнете върху Продължи.
  4. Създайте JSON поле с име texts. Това поле съхранява всички съобщения между разговарящите страни.
  5. Създайте текстово поле с име room. Това поле ще се използва за съхраняване на конкатенация на низове на потребителските имена на страните, участващи в разговор. Нашата реализация ще гарантира, че само двама души могат да бъдат в една стая едновременно.

Разширяване на потребителската колекция

Когато създадохме проекта Strapi, беше генерирана схема, която ни позволява да идентифицираме различните потребители, които ще използват нашето приложение. Ще добавим още полета, които могат да се считат за KYC параметри за нашето приложение.

  1. Кликнете върху Content-type Builder под плъгини в страничната лента за навигация.
  2. Щракнете върху Потребител, която е последната колекция в списъка с типове колекции.
  3. В горния десен ъгъл щракнете върху Добавяне на друго поле.
  4. Кликнете върху бутона Текстово поле.
  5. Въведете first_name като име на полето. Не променяйте опцията за кратък текст по подразбиране.
  6. Повторете горната стъпка, за да добавите полета за last_name и phone_number.
  7. Създайте медийно поле от един тип. Наречете го profile. Това поле ще се използва за съхраняване на изображението на потребителския профил.
  8. Щракнете върху бутона Запазване и изчакайте промените да бъдат приложени.

Окончателната конфигурация на колекцията на потребителя трябва да има поле за връзка с приложението, което беше добавено автоматично, когато създадохме асоциация при настройване на колекцията приложения. Допълнителното поле за медиен файл ще се използва за съхраняване на изображението на потребителския профил.

Разширяване на функциите на колекцията

След създаване на колекции чрез администраторския интерфейс, Strapi върши отлична работа за създаване, четене, актуализиране и изтриване на функции, свързани с всяка колекция. Можем обаче да отменим функциите и да добавим собствена логика. Например можем да добавим известно валидиране, за да гарантираме, че само потребители, които имат компания, свързана с техния профил, могат да манипулират колекцията от работни места. В следващите стъпки ще внедрим такива валидации върху събирането на приложения, събирането на фирми и събирането на работни места. Ние също така ще заменим маршрутите за събиране по подразбиране и ще добавим нашите персонализирани маршрути и ще ги защитим с помощта на приставката за потребителски разрешения на Strapi.

работа

В тази колекция бихме искали да създадем специфичен маршрут, който ще позволи на компаниите да преглеждат работните места, които публикуват. За да приложите това, отворете файла job.js в директорията на контролерите (./src/api/job/controllers). Добавете кода по-долу във функционалния блок createCoreController.

//./src/api/jobs/controllers/job.js
    const { createCoreController } = require('@strapi/strapi').factories;
    module.exports = createCoreController('api::job.job', ({ strapi }) => ({
        async myJobs(ctx) {
            const company = await strapi.db.query('api::company.company').findOne({
                where: { representative: ctx.state.user.id },
                populate: {
                    jobs: {
                        populate: {
                            applications: {
                                populate: {
                                    cv: true,
                                    applicant: {
                                        select: ['username', 'first_name', 'last_name', 'phone_number', 'email', 'id']
                                    }
                                }
                            }
                        }
                    }
                }
            });
            ctx.body = company.jobs;
        },
        async create(ctx) {
            const company = await strapi.db.query('api::company.company').findOne({
                where: { representative: ctx.state.user.id }
            });
            if(company){
    
                    let job = await strapi.entityService.create('api::job.job', {
                        data: {
                            ...ctx.request.body.data,
                            company: company.id
                        }
                    });
    
                    ctx.body = job
        
            }else{
            
              ctx.body = {
              success: false,
              message: "Company not found!"};
              }
        },
        async delete(ctx) {
            const company = await strapi.db.query('api::company.company').findOne({
                where: { representative: ctx.state.user.id }
            });
            const target_job = await strapi.db.query('api::job.job').findOne({
                where: { company: company.id, id: ctx.request.params.id }
            });
            if (target_job != null) {
                const job = await strapi.entityService.delete('api::job.job', target_job.id);
                ctx.body = target_job
            } else {
                ctx.body = {
                    success: false
                }
            }
        }
    }));

Асинхронната функция, наречена myJobs, използва API на машината за заявки, за да намери компания, чийто представител е потребителят, записан във връзката. След това зареждаме предварително работните места на компаниите, заявленията за всяка работа и съответните подробности за кандидатите. Следващата функция, наречена create, е отмяна на функцията за създаване по подразбиране. Преди да бъде създадена работа, първо проверяваме дали текущият потребител е свързан с компания. Ако е така, създаваме работа, свързана с тази компания. Във функцията за изтриване извършваме същата проверка, преди да премахнем окончателно запис на работа.

Персонализирано маршрутизиране Тъй като добавихме функцията myJobs в контролера, трябва да я съпоставим с маршрут. Това ще даде възможност на функцията да се задейства, когато се направи HTTP заявка към маршрута. В директорията за работа, под папката routes създайте два файла, както е показано по-долу. Първият файл ще се използва за приставка в нашия персонализиран маршрут към рутера за събиране на задачи. Тъй като кодът се изпълнява последователно, нашият персонализиран маршрут ще бъде зареден първо, след това маршрутите по подразбиране след това.

Добавете кода по-долу във файла job-1.js. Кодът дефинира крайна точка на localhost:1337/api/jobs/mine. Крайната точка е GET HTTP заявка, която ще задейства функцията myJobs, която сме създали в контролера.

//job-1.js
    module.exports = {
        routes: [
                  {   
                      method: 'GET',
                      path: '/jobs/mine',
                      handler: 'job.myJobs'
                  }
        ]
    }

Файлът job-2.js ще съдържа функцията за рутер за събиране по подразбиране.

//job-2.js
    const { createCoreRouter } = require('@strapi/strapi').factories;
    module.exports = createCoreRouter('api::job.job');

Company

We are going to automatically link the user in the connection to a company. This is happen after signing up. We will use the entity service API to upload a file to the record being created. The file in the company context is the company’s logo. View the code below to see how it will be implemented.

//./src/api/company/controllers/company.js
    const { createCoreController } = require('@strapi/strapi').factories;
    module.exports = createCoreController('api::company.company', ({ strapi }) => ({
        async create(ctx) {
            const files = ctx.request.files;
            let company = await strapi.entityService.create('api::company.company', {
                data: {
                    ...ctx.request.body,
                    representative: ctx.state.user.id
                },
                files
            });
            ctx.body = company;
        },
        async myProfile(ctx) {
            const company = await strapi.db.query('api::company.company').findOne({
                where: { representative: ctx.state.user.id },
                populate: { logo: true },
            });
            ctx.body = company;
        }
    }));

Функцията myProfile е персонализирана функция, която ще зареди профила на компанията по специален маршрут. Той прави запитване към колекцията с подробности за текущия потребител, след което зарежда предварително логото на компанията. Функцията е съпоставена към GET HTTP заявка, както е показано по-долу. Крайната точка е localhost:1337/api/companies/me.

//./src/api/company/routes/company-1.js
    module.exports = {
        routes: [{ 
                method: 'GET',
                path: '/companies/me',
                handler: 'company.myProfile',
            }]
    }

Приложения

В тази колекция искаме да можем да обработваме възобновени качвания и да ги свързваме с текущия потребител. Също така искаме да започнем разговор, след като дадено приложение бъде актуализирано до статус „прието“ от представител на компанията. Ние също така ще внедрим персонализирана функция за контролер, която ще позволи на потребителя да зареди всички свои приложения. Функцията ще бъде наречена mine и ще използва API на машината за заявки, за да извлече всички приложения, свързани с текущия потребител. Той също така ще зареди предварително работата, за която кандидатства, файла, който е прикачен към приложението, името на компанията, която е създала работата, и нейното лого.

//./src/api/application/controllers/application.js
    const { createCoreController } = require('@strapi/strapi').factories;
    module.exports = createCoreController('api::application.application', ({ strapi }) => ({
        async create(ctx) {
            const files = ctx.request.files;
            let application = await strapi.entityService.create('api::application.application', {
                data: {
                    ...ctx.request.body,
                    applicant: ctx.state.user.id
                },
                files
            });
            ctx.body = application
        },
        async update(ctx) {
            const updated_application = await strapi.entityService.update('api::application.application', ctx.request.params.id, {
                data: ctx.request.body
            });
            if (ctx.request.body.status == "accepted") {
                let application = await strapi.entityService.create('api::message.message', {
                    data: {
                        room: `${ctx.state.user.username}_${ctx.request.body.job}_${ctx.request.body.applicant_username}`,
                        texts: [
                            {
                                source: 0,
                                text: "Application accepted",
                                id: Date.now()
                            }
                        ]
                    }
                });
            }
            ctx.body = updated_application;
        },
        async mine(ctx) {
            const applications = await strapi.db.query('api::application.application').findMany({
                where: { applicant: ctx.state.user.id },
                orderBy: { id: 'DESC' },
                populate: {
                    job: {
                        populate: {
                            company: {
                                populate: {
                                    logo: true
                                }
                            }
                        }
                    },
                    cv: true
                },
            });
            ctx.body = applications;
        }
    }));

Персонализираната функция с име mine е свързана с крайната точка localhost:1337/api/applications/mine чрез GET HTTP заявка. Както преди, персонализираните маршрути се зареждат първо, след това маршрутите за събиране по подразбиране.

//./src/api/application/routes/routes-1.js
    module.exports = {
        routes: [
            { 
                method: 'GET',
                path: '/applications/mine', 
                handler: 'application.mine',
            }  
        ]
    }

Можем да потвърдим дали нашите маршрути са били разпознати, като изпълним командата по-долу в командния ред (terminal/cmd).

npm run strapi routes:list
    # OR
    yarn strapi routes:list

Администраторският интерфейс за проверка дали нашите персонализирани маршрути са заредени. Ние ще гарантираме, че всички наши персонализирани маршрути са достъпни само от удостоверени потребители.

  1. В страничната лента за навигация щракнете върху бутона за настройки.
  2. Щракнете върху ролите в подменюто, което се появява, след което изберете роля с име „Удостоверени“
  3. Изберете една от колекциите, които отменихме, и се уверете, че квадратчетата за отметка, които съответстват на функциите на нашия контролер, са отметнати, както е показано по-долу.

Настройка на сокет и удостоверяване

Нашето приложение за борда за работа ще има функция за съобщения, която ще позволи на кандидат и представител на компания да комуникират. Тази функция ще разчита на сървърната библиотека socket.IO, която ще осигури управляван от събития двупосочен комуникационен модел, който ни позволява да изградим тази функция за съобщения в реално време. Използвайте командата по-долу, за да инсталирате библиотеката във файла package.json на нашия проект Strapi.

npm install socket.io
    # OR
    yarn add socket.io

Сокетът трябва да бъде създаден, преди сървърът да стартира. Ще го прикачим към адреса и номера на порта на нашето копие. Отворете ./src/index.js, файлът съдържа функции, които се изпълняват преди стартирането на приложението Strapi. Ще добавим нашия код във функционалния блок bootstrap. Няма да посочим обекта за споделяне на ресурси от различни източници (CORS), така че връзките да могат да се правят от всеки адрес и порт. Ако очаквате връзка от един известен източник, препоръчително е да добавите неговия адрес. Ограничаването на CORS адресите помага за предотвратяване на неоторизиран достъп до чувствителни данни и ресурси на сървъра. Само на разрешени домейни е разрешено да правят заявки от различни източници към сървъра.

В рамките на функционалния блок за стартиране ние директно извикваме и инициализираме инсталираната от нас библиотека socket.io. След това указваме типа заявка, която ще се използва от HTTP метода за транспортиране на „дълго запитване“. Преди да позволим на клиент да се свърже, първо проверяваме неговия токен за удостоверяване. Токенът съдържа идентификатора на потребителя като полезен товар. Следователно декодирането на токена ще изведе обект, който съдържа, което ще ни позволи да извлечем данните на потребителя от базата данни с помощта на API на entityService. Когато даден потребител бъде успешно извлечен от базата данни, ние запазваме неговите подробности за връзката на сокета, в противен случай връзката на сокета ще се провали.

//./src/index.js
    module.exports = {
      
    bootstrap({ strapi }) {
    let interval;
    let io = require('socket.io')(strapi.server.httpServer, {
          cors: {
            origin: "*",
            methods: ["GET", "POST"]
          }
    });
    
    io.use(async (socket, next) => {
    try {
    
      //Socket Authentication
      const result = await strapi.plugins['users-permissions'].services.jwt.verify(socket.handshake.auth.token);
      const user = await strapi.entityService.findOne('plugin::users-permissions.user', result.id, {
    fields: ['first_name', 'last_name', 'email', 'id', 'username', 'phone_number']
            });
            //Save the User to the socket connection
            socket.user = user;
            next();
          } catch (error) {
            console.log(error);
          }
    
        }).on('connection', function (socket) {
          if (interval) {
            clearInterval(interval);
          }
          interval = setInterval(async () => {
            try {
              const entries = await strapi.entityService.findMany('api::message.message', {
                filters: {
                  $or: [
                    {
                      room: {
                        $endsWith: `_${socket.user.username}`,
                      }
                    },
                    {
                      room: {
                        $startsWith: `${socket.user.username}_`,
                      }
                    },
                  ],
                },
                sort: { createdAt: 'DESC' },
                populate: { texts: true },
              });
    
              io.emit('messages', JSON.stringify({ "payload": entries })
              ); // This will emit the event to all connected sockets
    
    
            } catch (error) {
              console.log(error);
            }
    
          }, 2500);
    
          socket.on('send_message', async (sent_message) => {
    
            const message = await strapi.db.query('api::message.message').findOne({
              where: { room: sent_message.room },
              populate: { texts: true },
            });
    
    
            let new_text = message.texts;
            new_text.push(
              {
                "text": sent_message.text,
                "source": socket.user.id,
                "created": new Date().getTime(),
                "id": generateUUID()
              }
            )
    
            const entry = await strapi.db.query('api::message.message').update({
              where: { room: sent_message.room },
              data: {
                texts: new_text,
              },
            });
    
     io.to(sent_message.room).emit("room_messages", JSON.stringify({ message: entry }));
          });
    
          socket.on('join_room', async (sent_message) => {
            socket.join(sent_message.room);
            const entry = await strapi.db.query('api::message.message').findOne({
              where: { room: sent_message.room },
              populate: { texts: true },
            });
    
            io.to(sent_message.room).emit("room_messages", JSON.stringify({ message: entry }));
    
          });
    
          socket.on('exit_room', (message) => {
            socket.leave(message.room);
          });
    
          socket.on('disconnect', () => {
            clearInterval(interval);
            console.log('user disconnected');
          });
    
        });
        return strapi
      },
    };

В рамките на слушателя на събития за връзка дефинирахме набор от функции, които ще бъдат задействани, когато конкретни събития бъдат направени от клиента. Тъй като изграждаме чат в реално време, дефинирахме събитие, наречено „join_room“, което ще позволи на двама клиента да се присъединят и споделят стая, и събитието „send_message“, което ще отговаря за разрешаването на клиентите да изпращат съобщения до конкретна стая.

В рамките на същия функционален блок за събитие за свързване на сокет ние излъчваме всички налични съобщения след всеки 2,5 секунди, като използваме функцията setInterval. Ние използваме entityService API за филтриране на записите въз основа на потребителското име на клиента. Тъй като това събитие се излъчва на всяко устройство, филтрирането ни позволява да показваме само най-подходящите съобщения на клиента. В рамките на събитието за прекъсване на връзката, ние изчистваме интервала, за да предотвратим ненужното използване на ресурси и да освободим памет на екземпляра на сървъра.

Инициализиране на проекта SwiftUI

Трябва да имаме инсталиран Xcode на нашата машина за разработка, за да започнем да използваме SwiftUI. Тъй като SwiftUI е насочен само към платформата на Apple, трябва да използваме Mac. Това ще ни позволи да емулираме устройства като iPhone, iPad и Apple Watches. Можете да изтеглите Xcode от App Store на Mac или неговия уебсайт.

След като успешно го инсталирате и отворите, ще бъдете страхотни от екрана по-долу. Щракнете върху създаване на нов Xcode проект, изберете приложение за iOS и му дайте име на продукта JobBoard

Настройка на Socket IO

Ще инсталираме клиентския пакет swift на socket io в нашия проект за iOS. За да започнете, щракнете върху бутона за файл в лентата с инструменти на прозореца на Xcode. След това щракнете върху добавяне на нови пакети от изскачащия списък.

Пакетът, който ще инсталираме, е с отворен код и се поддържа активно от разработчиците в socket io. Ще използваме връзката към хранилището на GitHub на пакета, за да насочим Xcode къде трябва да извлече изходния код за пакета.

В лентата за търсене на модала поставете връзката към хранилището на github на socket io и натиснете enter, Xcode ще се опита да разреши адреса и файлът README.md ще бъде изобразен, след като бъде завършен. Щракнете върху добавяне на пакет в долния десен ъгъл на модала, за да завършите инсталирането на пакета.

Настройка на мрежови услуги и модели

Ще създадем класове, които ще се използват за правене на заявки за HTTP и сокет. Създайте папка с име Helpers и създайте следните файлове в нея NetworkService, AuthPersistor, NetworkModels и SocketService. Тези файлове ще съдържат файловото разширение .swift. По подразбиране Xcode не показва файловото разширение във файловата структура на проекта, но ще покаже логото на типа файл преди името на файла.

Поток на удостоверяване

В папката Helpers създаваме файл, който ще се използва за запазване на Jwt токена, който получаваме от strapi екземпляра по време на удостоверяване. Ще използваме функцията за ключодържател на iOS, за да се справим с постоянството. Keychain се използва главно за съхраняване на чувствителна информация като пароли и ключове за удостоверяване. Keychain е криптирана база данни, която се използва от устройства на Apple за съхраняване на пароли. Базата данни се заключва, когато устройството е заключено, и се отключва, когато устройството е отключено. Това ще гарантира, че токенът, който получихме, е безопасен и не може да бъде достъпен от неоторизирани лица.

Ще създадем клас, който ще съдържа функции, които ще ни помогнат да имаме достъп до Keychain. Ще го маркираме като окончателен, така че да не може да бъде отменен или модифициран. След това ще инициализираме статичен конструктор на клас, наречен стандартен. Ключовата дума static гарантира, че променливата принадлежи към типа, а не към конкретен екземпляр от този тип. Това означава, че всеки екземпляр на този клас ще споделя обекта, наречен стандарт, вместо всеки да дефинира свой собствен.

//Helpers/AuthPersistor.swift
    import Foundation
    import Combine
    
    final class KeychainHelper {
        
        static let standard = KeychainHelper()
        private init() {}
        
        func save(_ data: Data, service: String, account: String) {
    
            let query = [
                kSecValueData: data,
                kSecAttrService: service,
                kSecAttrAccount: account,
                kSecClass: kSecClassGenericPassword
            ] as CFDictionary
    
            // Add data in query to keychain
            let status = SecItemAdd(query, nil)
    
            if status == errSecDuplicateItem {
                // Item already exist, thus update it.
                let query = [
                    kSecAttrService: service,
                    kSecAttrAccount: account,
                    kSecClass: kSecClassGenericPassword,
                ] as CFDictionary
    
                let attributesToUpdate = [kSecValueData: data] as CFDictionary
    
                // Update existing item
                SecItemUpdate(query, attributesToUpdate)
            }
        }
        
        func read(service: String, account: String) -> Data? {
            
            let query = [
                kSecAttrService: service,
                kSecAttrAccount: account,
                kSecClass: kSecClassGenericPassword,
                kSecReturnData: true
            ] as CFDictionary
            var result: AnyObject?
            SecItemCopyMatching(query, &result)
            return (result as? Data)
        }
        
        func delete(service: String, account: String) {
            let query = [
                kSecAttrService: service,
                kSecAttrAccount: account,
                kSecClass: kSecClassGenericPassword,
                ] as CFDictionary
            // Delete item from keychain
            SecItemDelete(query)
        }
    }
    
    extension KeychainHelper {
        func save<T>(_ item: T, service: String, account: String) where T : Codable {        
            do {
                // Encode as JSON data and save in keychain
                let data = try JSONEncoder().encode(item)
                save(data, service: service, account: account)
            } catch {
                assertionFailure("Fail to encode item for keychain: \(error)")
            }
        }
        
    func read<T>(service: String, account: String, type: T.Type) -> T? where T : Codable {
            // Read item data from keychain
            guard let data = read(service: service, account: account) else {
                return nil
            }
            // Decode JSON data to object
            do {
                let item = try JSONDecoder().decode(type, from: data)
                return item
            } catch {
                assertionFailure("Fail to decode item for keychain: \(error)")
                return nil
            }
        }
    }

Класът съдържа три функции, а именно четене, изтриване и запазване. Всяка функция разчита на CFDictonaries за представяне на данни, които могат да се съхраняват в базата данни Keychain. CFDictonaries са двойки ключ-стойност, подобни на NSDictionary на swift. Функцията за запазване се използва за разстройване на елементи. Добавихме проверка за обработка на случаите, когато ключът вече съществува. Стойности, ако съществуващите ключове ще бъдат актуализирани.

След това разширихме функционалността на класа, за да се погрижим за JSON кодируеми и декодируеми обекти, използвайки ключовата дума extension. Ключовата дума добавя нова функционалност към съществуващи класове, структури, изброявания и типове протоколи. Използва се главно за разширяване на типове данни, до които нямаме пряк достъп. За да работи добавената функционалност безупречно, обектът, предаван на функциите за четене и запазване, трябва да отговаря на кодируемия протокол, който преобразува обектите във формат JSON, който може лесно да се съхранява в базата данни на Keychain. Добавихме изключение за обработка на грешки, което ще попречи на нашето приложение да се срине, ако предаваният обект не съответства на споменатия протокол.

Ще използваме модели на данни, за да структурираме данните, получени от strapi сървъра. В директорията на проекта създайте нова папка с име Models и създайте следните swift файлове: Job, JobApplication, Company, Message и User.

Файлът Job създава структура, която отговаря както на кодируемите, така и на идентифицируемите протоколи. Структурата с име job съдържа два инициализатора, единият от които ще се използва за декодиране на JSON данни от strapi за изграждане и екземпляр на структурата, използвайки извлечените данни. Инициализаторът съдържа JSON ключове за декодер, които ще бъдат използвани за извличане на дълбоко вградени JSON обекти.

//Models/Job.swift
    struct Job : Codable, Identifiable{
        
    var id : Int
    var name : String
    var description : String
    var company : Company? = nil
    var type : String //Contract/Long Term/Short Term/Internship/Consultancy
    var environment : String //Remote/Semi-remote/In-Office
    var status : String
        
    init(id: Int, name : String, description: String, type: String, environment: String, status: String){
     self.id = id
     self.name = name
     self.description = description
     self.type = type
     self.environment = environment
     self.status = status
    }
            
    private enum JobDataKeys: String, CodingKey {
     case id = "id",attributes = "attributes"
    enum AttributeKeys : String, CodingKey {
     case name = "name",
      description = "description",
      company = "company",
      type="type",
      environment = "environment", 
      status = "status"
                
     enum CompanyKey : String, CodingKey{
        case data = "data"
        enum CompanyDataKeys : String, CodingKey {
        case id = "id", attributes = "attributes"
        enum CompanyDataAttributesKeys : String, CodingKey{
        case address = "address",
            bio = "bio",
          category = "category",
            email = "email",
             name = "name", 
            phone = "phone",
             logo = "logo"
                            
     enum CompanyLogoKey : String, CodingKey{
     case data = "data"                    
     enum CompanyLogoAttributes : String, CodingKey {
     case attributes = "attributes"
     enum CompanyLogoAttributeKeys : String, CodingKey {
     case url = "url", formats = "formats"
     enum CompanyLogoAttributeFormatsKeys : String, CodingKey {
      case large = "large", 
      medium = "medium",
      small = "small", 
      thumbnail = "thumbnail", 
      url = "url"
      enum CompanyLogoFormartsLarge: String, CodingKey{case url = "url" }
      enum CompanyLogoFormartsThumbnail: String, CodingKey{case url = "url"}
      enum CompanyLogoFormartsSmall: String, CodingKey{case url = "url"}
      enum CompanyLogoFormartsMedium: String, CodingKey{case url = "url"}
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

Файлът Company.swift дефинира структурата, която определя как ще бъде структурирана информацията на компанията в нашата кандидатура за работа. Файлът по-долу показва как е дефинирана структурата. И компанията, и структурите на логото на компанията отговарят на кодируемите протоколи, което гарантира, че те могат да бъдат трансформирани към и от JSON формат.

//Company.swift
    import Foundation
    struct Company : Codable{
        var id: Int
        var name : String
        var phone : String
        var email : String
        var address : String
        var category : String //Tech/Pharma/Transport/NGO/Finance
        var bio: String
        var logo : CompanyLogo?
        init(id: Int, name: String, phone: String, email: String, address: String, category: String, bio: String, logo: CompanyLogo?) {
            self.id = id
            self.name = name
            self.phone = phone
            self.email = email
            self.address = address
            self.category = category
            self.bio = bio
            self.logo = logo
        }
    }
    
    struct CompanyLogo : Codable{
        var url : String
        var thumbnail : String
        var small : String
        var medium : String
        var large : String
        init(url: String, thumbnail: String, small: String, medium: String, large: String) {
            self.url = url
            self.thumbnail = thumbnail
            self.small = small
            self.medium = medium
            self.large = large
        }
    }

Потребителски модел

Този файл дефинира структурата, която ще се използва за представяне на информацията на нашия потребител. Потребителските данни могат да бъдат конвертирани към и от JSON, тъй като структурата съответства на протокола Codable. Файлът също така съдържа структура AuthState, която ще слуша промените в състоянията на удостоверяване, като използва системата PubSub на swift. Структурата също така използва създадения от нас клас KeyChainHelper. Когато даден потребител е удостоверен, неговите данни се запазват в базата данни на веригата ключове. Ние го съхраняваме там, защото jwt токенът е включен в потребителската структура.

//Models/User.swift
    import Foundation
    import Combine
    
    struct User: Codable{
        var username : String
        var id : Int
        var phone_number : String
        var email : String
        var first_name : String
        var last_name : String
        var token : String
        var profile : ProfileImage? = nil
        var role : Role? = nil
    }
    
    struct ProfileImage : Codable {
        var small : String = ""
        var medium : String = ""
        var large : String = ""
        var thumbnail : String = ""
        var url : String = ""
    }
    
    struct Role: Codable{
        var name : String
        var description: String
        private enum RoleKeys : String, CodingKey {
            case name = "name", description = "description"
        }
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: RoleKeys.self)
            self.name = try container.decode(String.self, forKey: .name)
            self.description = try container.decode(String.self, forKey: .description)
        }
    }
    
    struct AuthState{
        static let Authenticated = PassthroughSubject<Bool, Never>()
        static let Company = PassthroughSubject<Bool, Never>()
        static func IsAuthenticated() -> Bool {
        let user = KeychainHelper.standard.read( 
            service: "strapi_job_authentication_service", 
            account: "strapi_job_app", 
            type: User.self)
            NetworkService.current_user = user
            return user != nil
        }
    
      static func IsCompany() -> Bool {
      let company = KeychainHelper.standard.read( 
        service: "strapi_job_company_service",
        account: "strapi_job_app", 
        type: MyApplicationJobCompany.self)
        NetworkService.company = company
        return company != nil
        }
    }

Модели за реакция на мрежата

Тъй като SwiftUI е статично въведен език, трябва да декларираме променливи типове данни преди компилация. Също така трябва да анализираме данните от strapi сървъра в swiftUI типове данни. Ще създадем файл, който ще обработва JSON анализ след успешно мрежово повикване. Файлът ще съдържа структури, които могат да се използват от POST и GET заявка. Той също така съдържа структури за качване на файлове за различни типове файлове.

//NetworkModels.swift
    import Foundation
    import PhotosUI
    import PDFKit
    
    struct BulkJobServerResponse: Decodable {    
     var data : [Job]
     enum DataKeys: CodingKey {
      case data
     } 
     init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy: DataKeys.self)   
      self.data = try container.decode([Job].self, forKey: .data)
     }
    }
    
    struct AuthenticationResponse :  Codable{
        
     var user : User
     enum AuthResponseKeys: String, CodingKey {
      case jwt = "jwt", user = "user"
       enum UserDetailsKeys : String, CodingKey {
         case id = "id", username = "username", email = "email", first_name = "first_name", last_name = "last_name", phone_number = "phone_number"
       }
     }
        
     init(from decoder: Decoder) throws {
      let authReponseContainer = try decoder.container(keyedBy: AuthResponseKeys.self)
      let userDetailsContainer = try authReponseContainer.nestedContainer(keyedBy: AuthResponseKeys.UserDetailsKeys.self, forKey: .user)
      let id = try userDetailsContainer.decode(Int.self, forKey: .id)
      let phone_number = try userDetailsContainer.decode(String.self, forKey: .phone_number)
      let username = try userDetailsContainer.decode(String.self, forKey: .username)
      let first_name = try userDetailsContainer.decode(String.self, forKey: .first_name)
      let last_name = try userDetailsContainer.decode(String.self, forKey: .last_name)
      let email = try userDetailsContainer.decode(String.self, forKey: .email)
      let jwt = try authReponseContainer.decode(String.self, forKey: .jwt)
      self.user = User(username: username, id: id, phone_number: phone_number, email: email, first_name: first_name, last_name: last_name, token: jwt )
        }
    }
    
    struct UploadImage {
        let key: String
        let filename: String
        let data: Data
        let mimeType: String
        init?(withImage image: UIImage, forKey key: String) {
            self.key = key
            self.mimeType = "image/jpeg"
            self.filename = "imagefile.jpg"
            guard let data = image.jpegData(compressionQuality: 0.7) else { return nil }
            self.data = data
        }
    }
    
    struct UploadPDF {
        let key: String
        let filename: String
        let data: Data
        let mimeType: String
        init?(withPDF pdfdoc: PDFDocument, forKey key: String) {
            self.key = key
            self.mimeType = "application/pdf"
            self.filename = "document.pdf"
            self.data = pdfdoc.dataRepresentation()!
        }
    }

Удостоверяване

Има два типа потребители, нормален потребител и фирмен потребител. Фирмен потребител има фирмен профил, свързан с него. Те имат възможността да добавят нови работни места и да преглеждат кандидатурите за работни места, които са създали.

Когато типът потребител на фирмата е избран, ние незабавно създаваме компания и я свързваме с новосъздадения потребител. След това те могат да променят подробностите за компанията в раздела на профила, след като бъдат удостоверени.

//Register Function
    func register(first_name:String, last_name: String, username: String,email:String, phone:String, password: String, completion: @escaping (User?) -> ()  ){
            
    guard  let url = URL(string: "\(authentication_url)/local/register")  else {
        completion(nil)
        fatalError("Missing URL") 
    }
            
    var urlRequest = URLRequest(url: url)
    urlRequest.httpMethod = "POST"
    urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let parameters: [String: Any] = [
                "username": username,
                "password": password,
                "email" : email,
                "first_name": first_name,
                "last_name": last_name,
                "phone_number": phone
            ]
    do {
      // convert parameters to Data and assign dictionary to httpBody of request
      urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters)
      print(urlRequest)
    } catch let error {
      assertionFailure(error.localizedDescription)
      completion(nil)
      return
    }
            
    let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
    
    if let error = error {
     print("Request error: ", error)
     completion(nil)
     return
    }
     // ensure there is data returned
    guard let responseData = data else {
     assertionFailure("nil Data received from the server")
     completion(nil)
     return
    }
    
    do {
    
    let loaded_user = try JSONDecoder().decode(AuthenticationResponse.self, from: responseData)
    
    KeychainHelper.standard.save(loaded_user.user, service: "strapi_job_authentication_service",account: "strapi_job_app")
    
    NetworkService.current_user = loaded_user.user
    completion(loaded_user.user)
                    
    } catch let DecodingError.dataCorrupted(context) {
     print(context)
     completion(nil)
    } catch let DecodingError.keyNotFound(key, context) {
     print("Key '\(key)' not found:", context.debugDescription)
     print("codingPath:", context.codingPath)
     completion(nil)
    } catch let DecodingError.valueNotFound(value, context) {
     print("Value '\(value)' not found:", context.debugDescription)
     print("codingPath:", context.codingPath)
     completion(nil)
    } catch let DecodingError.typeMismatch(type, context)  {
     print("Type '\(type)' mismatch:", context.debugDescription)
     print("codingPath:", context.codingPath)
     completion(nil)
    } catch let error {
     assertionFailure(error.localizedDescription)
     completion(nil)
     }
     }
     dataTask.resume()
    }

Всички функции за извикване на мрежови заявки са дефинирани във файла NetworkService.swift. Всеки изглед инстанцира обекта на мрежовата услуга, след което извиква подходящата функция в рамките на .onAppear модификатора, който е метод на екземпляр на изглед, който използваме за извличане на данни, когато изглед е зареден на екрана на устройството.

Фирмен контекст

Тази част управлява всичко, свързано с една компания. Тя позволява на потребителя да актуализира фирмения си профил, да преглежда, одобрява и отхвърля кандидатури, да създава, преглежда, актуализира и изтрива обяви за работа.

За достъп до този контекст потребителят трябва да отиде до раздела с профили и той/тя ще види бутона за профил на компанията точно над бутона за излизане. Този бутон не е достъпен за потребители без компания, свързана с техния потребителски акаунт.

Фирмени работни места

Този раздел съдържа функционалности за извършване на CRUD действия върху колекцията от задачи, която сме създали. Тъй като сме заменили колекцията от задачи, нашият код проверява дали определена работа е свързана с компанията и потребителя. Това не позволява на неоторизирани хора да актуализират и изтриват работни места, които не са свързани с компания, която е харесана в техния потребителски профил.

Сокет комуникация

Трябва да използваме сокет връзката, която създадохме с помощта на Socket IO на strapi сървъра. По-долу е проста бърза дефиниция на клас от страна на клиента, която включва удостоверяване. Класът се използва за улесняване на съобщенията в реално време между кандидата и представителя на компанията. Той също така съдържа функция, която ни помага в различни функции на socket io, например слушане на предавания, присъединяване към стаи и излъчване на събития.

//SocketService.swift
    final class SocketService : ObservableObject{
        private var manager = SocketManager(socketURL: URL(string: "ws://127.0.0.1:1337")!, config: [ .compress])
        @Published var socket_messages : [SocketMessage] = []
        @Published var room : SocketMessage = SocketMessage(id: 0, room: "", texts: [])
        let socket : SocketIOClient
        init(){
            self.socket = manager.defaultSocket
            self.socket.on(clientEvent: .connect, callback: {data, ack in print("Connected") })
            self.socket.on("messages") {data, ack in
                guard let cur = data[0] as? String else { return }
                let jsonObjectData = cur.data(using: .utf8)!
                do {
                    let candidate  =  try JSONDecoder().decode(
                        MM.self,
                        from: jsonObjectData
                    )
                    self.socket_messages = candidate.payload
                } catch let DecodingError.dataCorrupted(context) {
                    print(context)
                    self.socket_messages = []
                } catch let DecodingError.keyNotFound(key, context) {
                    print("Key '\(key)' not found:", context.debugDescription)
                    print("codingPath:", context.codingPath)
                    self.socket_messages = []
                } catch let DecodingError.valueNotFound(value, context) {
                    print("Value '\(value)' not found:", context.debugDescription)
                    print("codingPath:", context.codingPath)
                    self.socket_messages = []
                } catch let DecodingError.typeMismatch(type, context)  {
                    print("Type '\(type)' mismatch:", context.debugDescription)
                    print("codingPath:", context.codingPath)
                    self.socket_messages = []
                } catch let error {
                    assertionFailure(error.localizedDescription)
                    self.socket_messages = []
                }            
            }
            self.socket.on("room_messages") {data, ack in
                guard let cur = data[0] as? String else { return }
                let jsonObjectData = cur.data(using: .utf8)!
                do {
                    let room_details  =  try JSONDecoder().decode(SocketMessage.self,from: jsonObjectData)
                    self.room = room_details
                } catch let DecodingError.dataCorrupted(context) {
                    print(context)
                    self.room = SocketMessage(id: 0, room: "", texts: [])
                } catch let DecodingError.keyNotFound(key, context) {
                    print("Key '\(key)' not found:", context.debugDescription)
                    print("codingPath:", context.codingPath)
                    self.room = SocketMessage(id: 0, room: "", texts: [])
                } catch let DecodingError.valueNotFound(value, context) {
                    print("Value '\(value)' not found:", context.debugDescription)
                    print("codingPath:", context.codingPath)
                    self.room = SocketMessage(id: 0, room: "", texts: [])
                } catch let DecodingError.typeMismatch(type, context)  {
                    print("Type '\(type)' mismatch:", context.debugDescription)
                    print("codingPath:", context.codingPath)
                    self.room = SocketMessage(id: 0, room: "", texts: [])
                } catch let error {
                    assertionFailure(error.localizedDescription)
                    self.room = SocketMessage(id: 0, room: "", texts: [])
                }
            }
            socket.connect(withPayload: ["token": NetworkService.current_user!.token])
        }
        
        func sendMesage(room_name: String, message : String) {
            self.socket.emit("send_message", ["room": room_name, "text": message])
        }
        
        func joinRoom(room_name: String){
            self.socket.emit("join_room", ["room": room_name])
        }
        
        func exitRoom(room_name: String){
            self.socket.emit("exit_room", ["room": room_name])
        }
    }

Списъкът със съобщения ще се обновява автоматично и другият потребител ще получава съобщения веднага щом представителят на компанията натисне изпращане. Разговор може да бъде иницииран само когато кандидатурата за работа е прегледана и приета от представителя на компанията. След това те могат да се свържат чрез нашата функция за съобщения.

Консумираме връзката на сокета в два изгледа, а именно MessageView.swift и MessageDetail.swift. Кодът по-долу показва как сме структурирали MessageView.swift. Тъй като класът SocketService отговаря на протокола [**ObservableObject**](https://developer.apple.com/documentation/combine/observableobject), трябва да използваме обвивката на свойството @StateObject всеки път, когато създаваме неговия екземпляр. Тъй като бяхме настроили сървъра да излъчва съобщения на всеки 2,5 секунди, потребителският интерфейс ще се актуализира, след като бъдат получени нови промени.

//MessageView.swift
    import SwiftUI
    import SocketIO
    
    struct MessageView: View {
        @StateObject var service = SocketService()
        static let tag: String? = "MessageView"
        var body: some View {
            NavigationView{
                List(){
                    ForEach(service.socket_messages){message in
                        NavigationLink(destination: MessageDetailView(
                            socketMessage: message,
                            socket: service
                        )) {
                            Text(message.receiver)
                        }
                    }
                }.navigationTitle("My Messages")
            }
        }
    }

работа

След като потребител влезе в системата, той е посрещнат от списък с налични работни места. След това могат да кликнат върху предпочитаната от тях обява за работа и да качат автобиографиите си. Веднага щом изгледът се зареди, ние правим GET api извикване до крайната точка localhost:1337/api/jobs, като използваме обекта NetworkService в модификатора на изглед onAppear. Тъй като вече бяхме влезли, обектът автоматично ще добави токена към заглавките на заявката.

//HomeView.swift
    import SwiftUI
    
    struct HomeView: View {
        private var network = NetworkService()
        static let tag: String? = "HomeView"
        @State private var jobs : [Job] = []
        var body: some View {
            NavigationView{
                List(jobs) { job in
                    NavigationLink {
                        DetailView(job: job)
                    } label: {
                        JobCard(job: job)
                    }
                }.onAppear{
                    network.listJobs{fetched_jobs in
                        jobs = fetched_jobs
                    }
                    network.loadMyCompanyProfile{company_profile in
                        if company_profile != nil{
                          KeychainHelper.standard.save(company_profile, service: "strapi_job_company_service", account: "strapi_job_app")
                            NetworkService.company = company_profile
                            AuthState.Company.send(true)
                        }
                    }
                    
                }
                .navigationTitle("Jobs")
            }}
    }
    
    struct HomeView_Previews: PreviewProvider {
        static var previews: some View {
            HomeView()
        }
    }

Тъй като бяхме задали променливата current_user като статична, докато дефинирахме класа NetworkService, всички екземпляри на класа ще имат jwt токена, който се съхранява в структурата current_user. Структурата се актуализира с помощта на pubs по време на удостоверяване и премахване на удостоверяване.

Заключение

В този урок разработихме приложение Job Board, поддържано от Strapi и SwiftUI. Разгледахме как да разширим функционалностите на колекциите по подразбиране. Добавихме персонализирани маршрути към колекциите strapi и прикачихме Socket IO към екземпляра на сървъра strapi, за да позволим връзки в реално време.

Можете да изтеглите изходния код от следните хранилища:

  1. SwiftUI Frontend
  2. Каишка Backend