iOS development. Best practices

iOS development. Best practices

Всем привет. В этой статье я попытаюсь дать небольшие советы по написанию кода для начинающих iOS-девелоперов. Я не буду особо углубляться, так как это займет много времени (если честно, то мне просто лень). Суть этой статьи: помочь в создании хороших, стабильных приложений; продемонстрировать, как делать нужно, а как не стоит; выработать у вас привычку писать хороший, понятный код.

UI

Стили

Начнем со стилей, который приходят к нам от дизайнера. Не будете же вы каждый раз вспоминать, какой rgb-код нужно использовать для той или иной кнопочки? Пишем extension для UIColor и UIFont:

extension UIColor {
    class func blueGradientColor() -> UIColor {
        return UIColor(red: 65.0 / 255.0, green: 196.0 / 255.0, blue: 254.0 / 255.0, alpha: 1.0)
    }
    class func orangeGradient() -> UIColor {
        return UIColor(red: 246.0 / 255.0, green: 117.0 / 255.0, blue: 68.0 / 255.0, alpha: 1.0)
    }
    class func orangeGradient30opacity() -> UIColor {
        return UIColor(red: 246.0 / 255.0, green: 117.0 / 255.0, blue: 68.0 / 255.0, alpha: 0.3)
    }
…
extension UIFont {
    class func L36() -> UIFont {
        return UIFont.systemFont(ofSize: 36.0, weight: UIFontWeightLight)
    }
    class func M26() -> UIFont {
        return UIFont.systemFont(ofSize: 26.0, weight: UIFontWeightMedium)
    }
    class func L26() -> UIFont {
        return UIFont.systemFont(ofSize: 26.0, weight: UIFontWeightLight)
    }
    class func M20() -> UIFont {
        return UIFont.systemFont(ofSize: 20.0, weight: UIFontWeightMedium)
    }

Использовать также легко:

titleLbl.font = UIFont.R18()
titleLbl.textColor = UIColor.blueGreyColor()

Если потом вам скажут, что тут нужно использовать другой цвет/стиль, вы делаете изменение в одном файле, а не разыскиваете по всему проекту те места, где его применяли.

UIAppearance

Идем дальше. Предположим, что нужно, чтобы все title у navigationBar были в одном стиле. Можно, конечно, прописывать один и тот же код во всех view controllers. Но было бы лучше использовать такой подход, как в css: мы заранее задаем стили а потом система сама их использует. Метод appearance():

let barBtn = UIBarButtonItem.appearance()
    barBtn.setTitleTextAttributes([NSForegroundColorAttributeName : UIColor.white(), NSFontAttributeName : UIFont.L17()], for: UIControlState()) 

Метод appearance() есть у всех UIKit-элементов. Вот костомный gradient, к которому также применим этот метод:

let navBar = CRGradientNavigationBar.appearance()
        let colors = [
            UIColor.orangeGradient(),
            UIColor.pinkGradient()
        ]
        navBar.setBarTintGradientColors(colors)
        navBar.isTranslucent = false
        navBar.titleTextAttributes = [NSForegroundColorAttributeName : UIColor.white(),
NSFontAttributeName : UIFont.M17()] 

Сгруппируйте подобные штуки и поместите в один метод — назовем его interfaceAppearance() — и поставьте его куда-нибудь в application: didFinishLaunchingWithOptions.

VCManager. sharedInstance

На последнем проекте я столкнулся с проблемой частого вызова разных контроллеров из самых «неподходящих» мест :). Например, есть у меня такой контролер demoVC, к свойствам которого я очень часто обращаюсь. Вроде бы не сложно написать две строчки кода, но не писать же мне их постоянно. Вдруг поменяется архитектура приложения? Создаем сингелтон:

class VCManager: NSObject {
 
    fileprivate override init() {}
    static let sharedInstance = VCManager()
    override func copy() -> Any {
        fatalError("You are not allowed to use copy method on singleton!")
    }

И добавляем в него те методы, которые, по логике, он должен реализовывать:

func demoVC() -> DemoVC {
        let navVC = tabBarVC.viewControllers?[2] as! UINavigationController
        return navVC.viewControllers.first as! DemoVC
    }
 
func logout() {
        if let tabBarVC = tabBarVC {
            let vc = tabBarVC.storyboard?.instantiateViewController(withIdentifier: "WelcomeVC")
            UIApplication.shared.keyWindow?.rootViewController = vc  
            //socket close
            SocketManager.sharedInstance.logoutFromWebSocket()
 
            //remove purshases
            KeychainManager.sharedInstance.removeAll()
        }
    }  

NSLayoutConstraint vs SnapKit

Еще один момент по UI — это создание NSLayoutConstraint. Вот пример использование этого «кошмара» в коде:

private func setupConstraints() {
 
        self.backImView.translatesAutoresizingMaskIntoConstraints = false
        self.textLbl.translatesAutoresizingMaskIntoConstraints = false
        self.titleLbl.translatesAutoresizingMaskIntoConstraints = false
        self.imageView?.translatesAutoresizingMaskIntoConstraints = false
        self.button?.translatesAutoresizingMaskIntoConstraints = false
        self.customView?.translatesAutoresizingMaskIntoConstraints = false
 
        let backImLeading = NSLayoutConstraint(item: self.backImView,
                                               attribute: .Leading,
                                               relatedBy: .Equal,
                                               toItem: self,
                                               attribute: .Leading,
                                               multiplier: 1.0,
                                               constant: 15.0)
 
        let backImTop = NSLayoutConstraint(item: self.backImView,
                                           attribute: .Top,
                                           relatedBy: .Equal,
                                           toItem: self,
                                           attribute: .TopMargin,
                                           multiplier: 1.0,
                                           constant: 20.0)

А вот как выглядит код с использованием SnapKit:

//constraints
  imageView.snp_makeConstraints(closure: { (make) in
          make.left.equalTo(customView).offset(15.0)
          make.top.equalTo(customView).offset(15.0)
          make.size.equalTo(CGSize(width: 90.0, height: 57.0))
  })
 
  whereLbl.snp_makeConstraints(closure: { (make) in
          make.left.equalTo(imageView.snp_right).offset(10.0)
          make.top.equalTo(imageView)
          make.right.equalTo(customView).offset(-15.0)
  })

Заходите на официальный сайт, читайте доку, пишите лаконичный и понятный код, который легко можно потом поправить.

Architecture

Dependency managers

Cocoapods, Carthage или стандартный Package Manager? Предположим, что нужно использовать сторонние решения, которые, в свою очередь, требует еще каких-то зависимостей. Конечно, можно серфить по гиту, искать зависимости, копировать себе в проект, потом узнавать, что записали не ту версию, что вам нужно, и начинать процесс с начала. Но зачем? Можно ведь использовать один из трех выше названных менеджеров, которые сами все сделают, и в случае обновления также автоматически обновят зависимости.

В своей работе я отдаю предпочтение Cocoapods. Во-первых, их используют Google, а это уже о многом говорит. Во-вторых, использовать cocoapods очень легко, и все сторонние фреймворки юзают этот менеджер.

Инструкция по Cocapods произрастает тут.

Важное замечание по Cocoapods. Предположим, вы используете в проекте swift 2.3 и еще не перешли на swift 3.0. Используете, например, Gloss, который к моменту написания статьи уже обновился до версии, поддерживающей swift 3.0. Как же быть? Если прописать в Podfile pod ‘Gloss', полезут ошибки. Лезем на github, ищем Gloss, смотрим, есть ли у них branch, который поддерживает swift 2.3. Прописываем в Cocoapods следующее:

pod 'Gloss', :git => 'https://github.com/hkellaway/Gloss', :branch => ‘swift_2.3'

Что говорит о следующем: использовать Gloss, поддерживающий swift 2.3 :)

Или просто напишете, до какой версии обновлять Gloss:

pod 'Gloss', '< 1.0'

Правда, в данном случае все новые фичи вам будут уже недоступны :(

Кто-то хотел почитать? Вот, почитать про Package Manager и про Carthage.

Gloss, JSON mapping

Проект располагается здесь. Предназначен он для мапинга JSON`а и создания моделек.

Пример использования:

let dict = obj as! [AnyHashable:Any]
ChallengeModel(json: dict as! Gloss.JSON)!

Тут мы создаем объект ChallengeModel из объекта [AnyHashable:Any].

Так выглядит сам класс модели (кстати, также можно использовать и структуру value type:

class ChallengeModel: Decodable {
    var author: AuthorModel?
    var categories: [CategoriesModel]?
    var created_at: UInt?
    var id: String?
    var opponent: AuthorModel?
    var questions: [QuestionModel]?
 
    required init?(json: JSON) {
        author = "author" <~~ json
        categories = "categories" <~~ json
        created_at = "created_at" <~~ json
        id = "id" <~~ json
        opponent = "opponent" <~~ json
        questions = "questions" <~~ json
…   

Подчиняем протоколы Decodable, реализуем init?(json: JSON), и все! На выходе у нас уже готовая к использованию моделька. Не надо использовать NSJSONSerializer и тому подобных вещей, Gloss делает это за вас :)

Также я очень часто использую SwiftyJSON. Можете почитать и разобраться, в чем его преимущества по сравнению со стандартным подходом.

Moya

В чистом виде (без functional programming и mvvm) я не использовал, но все же советую попробовать Moya. Это абстрактный «слой», в котором можно хранить все ваши endpoints.

Создаем enum, называем как нам угодно :). Пример использования:

import Moya
 
enum Endpoints {
    case authorization(client_id: String, client_secret: String, grant_type: String, fb_access_token: String, refresh_token: String?, device_type: String, device_id: String)
    case updateRefreshToken(client_id: String, client_secret: String, grant_type: String, refresh_token: String, device_type: String, device_id: String)
    case updateUserName(name: String)
    case updatePhoto(photo: UIImage)
    case getCategories()
    case startQuestions(categories: [String], search_type: String)

Пишем для него extension:

extension Endpoints: TargetType {
 
    var baseURL: URL { return URL(string: “https://master.you_base.url.com”)! }
 
    var path: String {
        switch self {
        case .authorization, .updateRefreshToken: return/api/some_endpoint”
        case .updateUserName: return "/api/some_endpoint"
        case .updatePhoto: return "/api/some_endpoint"
        case .getCategories: return "/api/some_endpoint"
…
 
var method: Moya.Method {
        switch self {
        case .authorization,
            .updateRefreshToken,
            .getIsFriend: return .GET
 
        case .updatePhoto,
            .startQuestions,
            .postPurchases: return .POST
 
var parameters: [String: Any]? {
        switch self {
        case .getUserFriends(let facebook_friends, let page):
            return ["page":page, "facebook_friends":facebook_friends]
 
        case .getAnswerStatistics(let categories):
            return [“categories":categories]
…
 
//for unit test
var sampleData: Data {
        return "".data(using: .utf8)!
    }
 
    var task: Task {
        switch self {
        case .updatePhoto(let photo):
            let imData = UIImagePNGRepresentation(photo)
            let multipartFormData = Moya.MultipartFormData.init(provider:
Moya.MultipartFormData.FormDataProvider.data(imData!),
                                                                name: "photo",
                                                                fileName: "file.webp",
                                                                mimeType: "image/png")
            return Task.upload(UploadType.multipart([multipartFormData]))
 
        default: return Task.request
        }
    }
… 

И каждый раз, когда нужно обратиться к серверу, во viewModel-классе вызываем соответствующую функцию:

func getCategories() -> Observable<[CategoriesModel]?> {
        return ProviderManager.sharedInstance.jsonProvider
            .request(Endpoints.getCategories())
            .debug()
            .mapJSON()
            .map { (obj) -> [CategoriesModel]? in
                print(obj)
 
                var categories = [CategoriesModel]()
 
                for item in (items as! NSArray) {
                    categories.append(CategoriesModel(json: item as! Gloss.JSON)!)
                }
 
                return categories
    }
}  

Endpoints.getCategories() и есть enum, определенный выше.

В этом примере я использую ProviderManager. Это сингелтон, у которого есть два поля:

defaultProvider = RxMoyaProvider<Endpoints>()
jsonProvider = RxMoyaProvider<Endpoints>(endpointClosure: closure)

Первое используется без параметров, второе наоборот — имеет их слишком много, чтобы тут показывать. Но суть в том, что на выходе получите уже распаршенную модель и те данные, которые вам нужны. И этот подход у меня сохраняется на протяжении всей работы над этим проектом. Почему ProviderManager сингелтон? ARC удалит объект, если на него нет ссылок.

«Свой» DataManager

Если ваше приложение использует всего несколько endpoint, можете воспользоваться иным подходом. Пишем класс, например, DataManager, в него пишем методы такого типа:

class func getProfileUserId(userId: Int, handler: UserProfileHandler) {
 
            let url = "\(baseURL)/user/profile/\(userId)"
 
            NetworkManager.requestWith(url, reqMethod: .GET, dataToSend: ["userId":userId],
uploadImage: nil, handler: { (json, error) in
 
                print(json)
 
                if json != nil && error == nil {

Тут NetworkManager — это private class, который в моем случае использует Alamofire, но мог бы использовать что-то еще. Суть в том, что если что-то ваc не устроило, можете поменять реализацию класса, но оставить нетронутым интерфейс!

private class NetworkManager {
 
    class func requestWith(url: String,
                           reqMethod: Alamofire.Method,
                           dataToSend: [String:AnyObject]?,
                           uploadImage: UIImage?,
                           handler: SuccessHandler) {
 
        var header: [String:String]?
        if DataManager.getToken().characters.count > 0 {
            let langId = NSLocale.currentLocale().objectForKey(NSLocaleLanguageCode) as! String
            let countryId = NSLocale.currentLocale().objectForKey(NSLocaleCountryCode) as! String
            let language = "\(langId)-\(countryId)"
            header = ["Authorization":"Bearer \(DataManager.getToken())", "AcceptLanguage":language]
        } else {
            header = nil
        }
 
        Alamofire.request(reqMethod, url, parameters: dataToSend, headers: header).responseJSON
{ response in
            switch response.result {
            case .Success(let JSON):
                let jsonS = SwiftyJSON.JSON(JSON)
                handler(json: jsonS, error: nil)
 
            case .Failure(let error): handler(json: nil, error: error)
            }
        }
    }
}  

Паттерн «фасад»

В конце этого раздела хотел рассказать о паттерне «фасад» и как применять его на практике. Предположим, что сервер возвращает time stamp (seconds since Jan 01 1970.(UTC)), а отображать надо время в следующем формате: если дата сегодня — часы:минуты, если вчера — просто писать Yesterday, если позже — отображать число.

Не будем же мы каждый раз писать это в контроллере? Создаем отдельный класс, скажем, DateManager, и добавляем в него методы, необходимые для работы:

func gmtTimeInSeconds(date: Date) -> UInt {
        return UInt(date.timeIntervalSince1970)
    }
 
func convertSecondsToDate(seconds: UInt) -> String {
        let date = Date(timeIntervalSince1970: TimeInterval(seconds))
        let formatter = DateFormatter()
        formatter.timeZone = NSTimeZone.local
 
        if NSCalendar.current.isDateInToday(date) {
            formatter.dateFormat = "HH:mm"
            return formatter.string(from: date)
 
        } else if NSCalendar.current.isDateInYesterday(date) {
            return "Yesterday"
 
        } else {
            formatter.dateFormat = "MM"
            let monthIndex = formatter.string(from: date)
            let monthName = formatter.standaloneMonthSymbols[Int(monthIndex)!]
            formatter.dateFormat = "dd"
            return "\(monthName) \(formatter.string(from: date))"
        }
    }
 
func convertTimeIntervalInMinutesSeconds(time: UInt) -> String {
    let minutes = Int(time / 60)
    let seconds = (time % 60)
 
    if seconds < 10 {
        return "\(minutes):0\(seconds)"
    }
    return "\(minutes):\(seconds)"
} 

Класс этот может быть как сингелтоном, так и простым, в последнем случае лучше использовать классовые методы (class func) или каждый раз создавать экземпляр этого класса.

Далее рассмотрим работу с сокетами. Для этой цели я использую уже проверенный фреймворк SocketRocket от Facebook. Опять же пишем фасад, и тут уже точно нужно использовать сингелтон (yeaaaaaah!):

import Foundation
import SocketRocket
 
class SocketManager: NSObject {
 
    fileprivate override init() {}
    static let sharedInstance = SocketManager()
    override func copy() -> Any {
        fatalError("You are not allowed to use copy method on singleton!")
    }
 
    //------------------------------
    private var webSocket: SRWebSocket?
 
    var connectionWasOpened = true
    var lastUserId = UserDefaults.standard.string(forKey: Constants.myIdKey.rawValue)
 
    func openNewConnection(userId: String) {
        if webSocket != nil {
            webSocket?.close()
            webSocket?.delegate = nil
            webSocket = nil
        }
        lastUserId = userId
        let urlStr = “ws://your_url/\(lastUserId ?? "")"
        webSocket = SRWebSocket(url: URL(string: urlStr))
        webSocket?.delegate = self
        webSocket?.open()
}
 
func closeConnection() {
    webSocket?.close()
}
 
func logoutFromWebSocket() {
    webSocket?.close()
    webSocket?.delegate = nil
    webSocket = nil
}
 
func reconnect() {
    if lastUserId != nil &&
        (webSocket?.readyState != SRReadyState.CONNECTING) &&
        (webSocket?.readyState != SRReadyState.OPEN) {
 
        openNewConnection(userId: lastUserId!)
        }
    }
 
    deinit {
        logoutFromWebSocket()
    }
}
 
extension SocketManager: SRWebSocketDelegate {
    func webSocketDidOpen(_ webSocket: SRWebSocket!) {
        webSocket?.sendPing(nil)
        connectionWasOpened = true
    }
 
    func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Swift.Error!) {
        if error != nil {
            print("SOCKET ERROR: \(error.localizedDescription)")
        }
        if connectionWasOpened == true {
            connectionWasOpened = false
            reconnect()
        } else {
            DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: {
                self.reconnect()
            })
        }
    }
 
    func webSocket(_ webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) {
        print("SOCKET ERROR: \(reason)")
        reconnect()
    }
 
    func webSocket(_ webSocket: SRWebSocket!, didReceivePong pongPayload: Data!) {
        print("PONG PAYLOAD: \(pongPayload.description)")
        if webSocket.readyState == SRReadyState.OPEN {
            webSocket.sendPing(nil)
        }
    }
 
    func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) {
        print("MESSAGE: \(message)")
 
        EventManager.sharedInstance.performEvent(data: message)
    }
} 

Можете смело (или не очень) использовать этот класс в своих проектах. Небольшой процент (50-95%) закидываете на счет Stfalcon и работаете себе в удовольствие :). Шутка, все закидывайте на мой счет.

SecureNSUserDefaults

Еще один важный момент. Мы все любим использовать UserDefaults. Простой в использовании и надежный класс. Вроде бы, но если пользователь будет использовать jailbreaked device, то сможет легко получить доступ к данным. Так что позаботьтесь о том, чтобы данные шифровались:

#Keychain
pod 'SecureNSUserDefaults', '~> 1.0.1'
pod ‘CocoaSecurity'

Это extension для UserDefaults, которое шифрует ваши данные. Так что не стоит хранить важные данные в UserDefaults, он только для настроек.

No internet connection

Пропал интернет. И что, скажем пользователю об этом или пускай догадается сам, когда упадет приложение? Допустим, что сказать все-таки надо или хотя бы предупредить :). Используем стороннее решение под названием UHBConnectivityManager (доступно на Cocoapods). В AppDelegate, applicaiton: didFinishLaunchingWithOptions: пишем следующее:

//internet connection
        UHBConnectivityManager.shared().registerCallBack({ [weak self] (status:
ConnectivityManagerConnectionStatus) in
            if status == ConnectivityManagerConnectionStatusConnected {
                print("Internet connected")
                SocketManager.sharedInstance.reconnect()
 
                self?.alertVC.dismiss(animated: true, completion: nil)
 
            } else {
                print("No connection")
                SocketManager.sharedInstance.closeConnection()
 
                VCManager.sharedInstance.topViewController()?.present((self?.alertVC)!, animated: true, completion: nil)
             }
        }, forIdentifier: self.memoryAddress()) 

alertVC — это property:

fileprivate var alertVC: UIAlertController = {
        let vc = UIAlertController(title: "Warning", message: "No internet connection",
preferredStyle: .alert)
        let action = UIAlertAction(title: "Ok", style: .default, handler: nil)
        vc.addAction(action)
        return vc
    }() 

Напоследок, хотел бы поделиться опытом по работе с UIWebView. На последнем проекте пришлось создавать контролер, который активно использует UIWebView. Это Explanation Screen, показывающий результат пройденного игроком теста. Если ответили неправильно, рисуется красная кнопочка с ответом, показывается правильных ответ и объяснение. Можно было бы, конечно, использовать UILabel и UIButton, и поместить все это на UIScrollView ячейки, в которой включена пагинация (вопросов больше, чем один). Так изначально и было сделано, но (опять это но!!!) в коде нужно было использовать MathML (для показа математических формул), а UILabel не может его отображать правильно. Тем более, что UIWebView уже имеет встроенный UIScrollView, так что рассчитывать высоту для каждого элемента не пришлось :).

Показываю кусок кода для ячейки:

 let pathToCss = Bundle.main.path(forResource: "styles", ofType: "css")
 
        cell.webView.loadHTMLString(HtmlHelper.fontExplanation(questionTitle: questionTitle,
                                                               question: question,
                                                               wrongTitle: item!.is_correct! == false ? "YOUR ANSWER —
WRONG" : "",
                                                               wrongAnswer: item!.is_correct! == false ? wrongText : nil,
                                                               correctTitle: item!.is_correct! == false ? "CORRECT
ANSWER" : "YOUR ANSWER - CORRECT",
                                                               correctAnswer: correctText,
                                                               explanationTitle: "EXPLANATION",
                                                               explanation: explanationText,
                                                               isCorrect: item!.is_correct!),
                                     baseURL: URL(string: pathToCss!)) 

Интересным является pathToCss — в проект я также добавил css-стили и использовал путь к ним, как baseURL. Метод класса HtmlHelper fontExplanation:

class func fontExplanation(questionTitle: String,
                               question: String,
                               wrongTitle: String?,
                               wrongAnswer: String?,
                               correctTitle: String,
                               correctAnswer: String,
                               explanationTitle: String,
                               explanation: String,
                               isCorrect: Bool) -> String {
 
            return "<!doctype html> <html lang=\"en\"> <head> <meta charset=\"UTF-8\"> <meta
name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximumscale=1.0, minimum-scale=1.0\"> <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"> <title>Document</title> <link rel=\"stylesheet\" href=\"styles.css\"> </head> <body> <p class=\"answer-text\">\(question)</p> <div class=\"label\">\(correctTitle)</div> <div class=\"answer answer_right\">\(correctAnswer)</div> <div class=\"label\">\(explanationTitle)</div> <p
class=\"answer-text\">\(explanation)</p> </div> </body> </html>"

Вот так вот, используя небольшие знания по верстке (которых у меня 0, спасибо Руслану ;)) и UIWebView, можно отформатировать любой текст, даже MathML, и представить это все в очень красивой форме.

Swift

Ну, как вы уже, видимо, поняли из название и моего кода, Swift — это тот язык, что я использую везде в своей работе. Раньше использовал Objective-C, но время его прошло, и остался он лишь для поддержки legacy-проектов. Хотя я видел программеров, которые до сих пор пишут на obj-c, но это отдельная история и не мне их судить. Ведь конечный результат — это двоичный код для машины, и как ты его написал, если он работает, уже мало кого волнует. Но вот поддержка таких проектов — крайне сложное занятие, и никто за это не хочет браться. Если языки С и С++ будут оставаться всегда (так как, по сути, лежат в ядре любого языка), то obj-c может полностью исчезнуть, поскольку swift уже его обогнал по всем показателям :)

С тех пор, как Apple сделали swift open source, его уже используют в других областях. Например, пишут на нем под raspberry pi 3! Мне сложно судить что и как у них там обстоит, так как я не работаю в этой сфере, но пробовал нечто другое, а именно Vapor.

Vapor — это framework для backend, написанный полностью на swift. Полезный ресурс и как установить — читайте тут. Официальная документация: вот она. Я лишь покажу небольшой пример, чтобы вам стало понятно, хотя бы приблизительно, как с этим работать.

После установки и создания проекта запустите из консоли команду vapor xcode. Vapor создаст для вас xcode-проект, который можно использовать для «поднятия» сервера локально на вашей машине.

import Vapor
 
let drop = Droplet()
 
drop.get("hello") { request in
    if let name = request.data["name"]?.string {
        return try drop.view.make("hello", ["name": name])
    }
 
    return try drop.view.make("hello", ["name": "no name"])
}
 
drop.run()

Жмем cmd + B, далее запускаем проект. В консоли будет написано:

No command supplied, defaulting to serve...
No preparations.
Server 'default' starting at 0.0.0.0:8080

Открываем Safari (или что вы там используете как браузер). Вводим в адресную строку 0.0.0.0:8080 или localhost:8080. Видим 404 page not found. Дописываем в адресной строке:

https://localhost:8080/hello?name=enter_you_name

Видим результат в браузере:

Разумеется, если у вас есть глубокие познания в backend-разработке, вы можете написать что-то действительно хорошее :). Я лишь хотел показать, насколько swift развился за последние несколько лет, оставаясь при этом довольно простым и понятным языком.