Подтвердить что ты не робот

Обработка ошибок в Swift

В моем приложении мне нужно загрузить JSON файл из Интернета. Я сделал класс ResourceService, у которого есть метод download, как показано ниже. Я использую эту услугу в услугах "более высокого уровня" своего приложения. Вы можете видеть, что во время загрузки много ошибок. Сервер может загореться и не сможет успешно реагировать на данный момент, во время перемещения временного файла может произойти что-то не так.

Теперь, вероятно, мало что может сделать пользователь, кроме как попробовать позже. Однако он, вероятно, хочет знать, что что-то не так и что загрузка или поведение методов "более высокого уровня" не могут быть успешными.

Меня как разработчика путают как этот пункт, потому что я не понимаю, как справляться с ошибками в Swift. У меня есть completionHandler, который принимает ошибку, если он есть, но я не знаю, какую ошибку я должен передать обратно вызывающему.

Мысли:

1) Если я передам объекты ошибки, которые я получаю из API NSFileManager или API NSURLSession, я бы подумал, что я "утечка" некоторой части метода download для вызывающих. И как бы вызывающий абонент знал, какие ошибки ожидать от ошибки? Это может быть и то и другое.

2) Если я должен поймать и обернуть те ошибки, которые могут произойти внутри метода download, как это будет выглядеть?

3) Как я могу работать с несколькими источниками ошибок внутри метода, и как выглядит код, вызывающий метод, который может вызывать/возвращать объекты NSError?

Если вы, как вызывающий, начинаете перехватывать ошибки, которые вы возвращаете, а затем записываете много кода, который отличает сообщения/действия, принятые на основе кода ошибки? Я вообще не получаю эту обработку ошибок и как это будет выглядеть, когда есть много вещей, которые могут пойти не так, как в одном методе.

func download(destinationUrl: NSURL, completionHandler: ((error: NSError?) -> Void)) {
    let request = NSURLRequest(URL: resourceUrl!)

    let task = downloadSession.downloadTaskWithRequest(request) {
        (url: NSURL?, response: NSURLResponse?, error: NSError?) in

        if error == nil {
            do {
                try self.fileManager.moveItemAtURL(url!, toURL: destinationUrl)
            } catch let e {
                print(e)
            }
        } else {
        }
    }.resume()
}
4b9b3361

Ответ 1

Прежде всего, это большой вопрос. Обработка ошибок - это конкретная задача, которая применяется к невероятному множеству ситуаций, когда кто-то знает, что влияет на состояние вашего приложения. Основная проблема заключается в том, что имеет смысл для вашего пользователя, приложения и разработчика.

Мне нравится это концептуально, как Цепочка ответчика используется для обработки событий. Подобно событию, пересекающему цепочку ответчиков, ошибка имеет возможность пузырить уровень абстракции вашего приложения. В зависимости от ошибки вы можете захотеть сделать несколько вещей, связанных с типом ошибки. Различные компоненты вашего приложения, возможно, должны знать об ошибке, возможно, ошибка, зависящая от состояния приложения, не требует действий.

Вы, как разработчик, в конечном счете знаете, где ошибки влияют на ваше приложение и как. Поэтому, учитывая, как мы выбираем, чтобы реализовать техническое решение.

Я бы предложил использовать Enumerations и Closures чтобы создать мое решение по обработке ошибок.

Вот надуманный пример ENUM. Как вы можете видеть, он представляет собой ядро ​​решения для обработки ошибок.

    public enum MyAppErrorCode {

    case NotStartedCode(Int, String)
    case ResponseOkCode
    case ServiceInProgressCode(Int, String)
    case ServiceCancelledCode(Int, String,  NSError)

    func handleCode(errorCode: MyAppErrorCode) {

        switch(errorCode) {
        case NotStartedCode(let code, let message):
            print("code: \(code)")
            print("message: \(message)")
        case ResponseOkCode:
            break

        case ServiceInProgressCode(let code, let message):
            print("code: \(code)")
            print("message: \(message)")
        case ServiceCancelledCode(let code, let message, let error):
            print("code: \(code)")
            print("message: \(message)")
            print("error: \(error.localizedDescription)")
        }
    }
}

Далее мы хотим определить наш завершающийHandler, который заменит ((error: NSError?) -> Void) закрытие, которое у вас есть в вашем методе загрузки.

((errorCode: MyAppErrorCode) -> Void)

Новая функция загрузки

func download(destinationUrl: NSURL, completionHandler: ((errorCode: MyAppErrorCode) -> Void)) {
    let request = NSURLRequest(URL: resourceUrl!)

    let task = downloadSession.downloadTaskWithRequest(request) {
        (url: NSURL?, response: NSURLResponse?, error: NSError?) in

        if error == nil {
            do {
                try self.fileManager.moveItemAtURL(url!, toURL: destinationUrl)
                completionHandler(errorCode: MyAppErrorCode.ResponseOkCode)

            } catch let e {
                print(e)
                completionHandler(errorCode: MyAppErrorCode.MoveItemFailedCode(170, "Text you would like to display to the user..", e))

            }


        } else {
            completionHandler(errorCode: MyAppErrorCode.DownloadFailedCode(404, "Text you would like to display to the user.."))

        }
    }.resume()
}

В закрытии, которое вы проходите, вы можете вызвать handleCode(errorCode: MyAppErrorCode) или любую другую функцию, определенную вами в ENUM.

Теперь у вас есть компоненты для определения собственного решения по обработке ошибок, которое легко адаптировать к вашему приложению, и которое вы можете использовать для сопоставления http-кодов и любых других сторонних кодов ошибок/ответов с чем-то значимым в вашем приложении. Вы также можете выбрать, полезно ли использовать пузырь NSError.


ИЗМЕНИТЬ

Вернемся к нашим ухищрениям.

Как мы имеем дело с взаимодействием с нашими контроллерами представлений? Мы можем выбрать централизованный механизм, как и сейчас, или мы могли бы обработать его в контроллере представления и сохранить область локальной. Для этого мы переместим логику с ENUM на контроллер представления и зададим очень специфические требования нашей задачи диспетчера представлений (в этом случае загрузка), вы также можете перенести ENUM в область просмотра контроллера. Мы добиваемся инкапсуляции, но очень легко закончим повторять наш код в другом месте проекта. В любом случае ваш контроллер просмотра должен будет что-то сделать с кодом ошибки/результата

Подход, который я предпочитаю, должен был дать диспетчеру просмотра возможность обработать конкретное поведение в обработчике завершения или передать его в наш ENUM для более общего поведения, такого как отправка уведомления о завершении загрузки, обновление приложения или просто выбросить AlertViewController с одним действием для "ОК".

Мы делаем это, добавляя методы к нашему контроллеру представления, которые могут быть переданы MyAppErrorCode ENUM и любые связанные переменные (URL, Request...) и добавлять любые переменные экземпляра для отслеживания нашей задачи, то есть другого URL-адреса, или количество попыток, прежде чем мы откажемся от попытки выполнить загрузку.

Вот возможный метод обработки загрузки на контроллере представления:

func didCompleteDownloadWithResult(resultCode: MyAppErrorCode, request: NSURLRequest, url: NSURL) {

    switch(resultCode) {
    case .ResponseOkCode:
        // Made up method as an example
        resultCode.postSuccessfulDownloadNotification(url, dictionary: ["request" : request])

    case .FailedDownloadCode(let code, let message, let error):

        if numberOfAttempts = maximumAttempts {
            // Made up method as an example
            finishedAttemptingDownload()

        } else {
             // Made up method as an example
            AttemptDownload(numberOfAttempts)
        }

    default:
        break

    }
}

Ответ 2

Короче говоря: да

... и затем напишите много кода, который отличает сообщений/действий, принятых на основе кода ошибки?

Большинство примеров кода оставляют только программиста о том, как вообще обрабатывать ошибки, но для того, чтобы сделать это правильно, ваш код обработки ошибок может быть больше, чем код для успешных ответов. Особенно, когда речь заходит об организации сетей и разбора json.

В одном из моих последних проектов (много сообщений о состоянии json-сервера) я применил следующий подход: я спросил себя: как приложение может реагировать на пользователя в случае ошибки (и перевести его как более удобный)?

  • игнорировать его
  • показать сообщение/предупреждение (возможно только одно)
  • Повторите попытку (как часто?)
  • заставить пользователя начать работу
  • предположим (т.е. ранее кэшированный ответ)

Для этого я создаю центральный класс ErrorHandler, у которого есть несколько перечислений для различных типов ошибок (например, перечисление NetworkResponseCode, ServerReturnCode, LocationStatusCode) и одно перечисление для разных ErrorDomains:

enum MyErrorDomain : String {
    // if request data has errors (i.e. json not valid)
    case NetworkRequestDomain   = "NetworkRequest"

    // if network response has error (i.e. offline or http status code != 200)
    case NetworkResponseDomain  = "NetworkResponse"

    // server return code in json: value of JSONxxx_JSON_PARAM_xxx_RETURN_CODE
    case ServerReturnDomain = "ServerReturnCode"

    // server return code in json: value of JSONxxxStatus_xxx_JSON_PARAM_xxx_STATUS_CODE
    case ServerStatusDomain = "ServerStatus"

    // if CLAuthorizationStatus
    case LocationStatusDomain = "LocationStatus"

    ....

}

Кроме того, существуют некоторые вспомогательные функции с именем createError. Эти методы выполняют некоторую проверку условия ошибки (т.е. Сетевые ошибки различны, если вы в автономном режиме или ответ сервера!= 200). Они короче, чем вы ожидали.

И чтобы собрать все вместе, есть функция, которая обрабатывает ошибку.

func handleError(error: NSError, msgType: String, shouldSuppressAlert: Bool = false){
...
}

Этот метод начался с оператора switch (и теперь он нуждается в некотором рефакторинге, поэтому я не буду показывать его, поскольку он все еще один). В этом заявлении реализованы все возможные реакции. Возможно, вам понадобится другой тип возврата, чтобы ваше состояние было корректным в приложении.

Извлеченные уроки:

  • Хотя я думал, что начал большие (разные перечисления, центральное оповещение пользователя), архитектура могла бы быть лучше (т.е. несколько классов, наследование,...).
  • Мне нужно было отслеживать предыдущие ошибки (поскольку некоторые из них отслеживаются), чтобы показывать только одно сообщение об ошибке пользователю → .
  • Есть веские причины скрывать ошибки.
  • Внутри карты errorObj.userInfo он выдает удобное сообщение об ошибке и техническую информацию об ошибке (которая отправляется поставщику отслеживания).
  • Мы ввели числовые коды ошибок (домен ошибок с префиксом буквы), которые согласованы между клиентом и сервером. Они также отображаются пользователю. Это действительно помогло отслеживать ошибки.
  • Я реализовал функцию handleSoftwareBug (которая почти такая же, как handleError, но гораздо меньше случаев). Он используется во многих других блоках, которые вы обычно не удосуживаетесь писать (поскольку вы думаете, что это состояние никогда не будет достигнуто). Удивительно, но это возможно.

        ErrorHandler.sharedInstance.handleSoftwareBug("SW bug? Unknown received error code string was code: \(code)")
    

Как это выглядит в коде: Есть много похожих сетевых запросов, где много кода выглядит примерно так:

func postAllXXX(completionHandler:(JSON!, NSError!) -> Void) -> RegisteringSessionTask {
    log.function()
    return postRegistered(jsonDict: self.jsonFactory.allXXX(),
        outgoingMsgType: JSONClientMessageToServerAllXXX,
        expectedIncomingUserDataType: JSONServerResponseAllXXX,
        completionHandler: {(json, error) in

            if error != nil {
                log.error("error: \(error.localizedDescription)")
                ErrorHandler.sharedInstance.handleError(error,
                    msgType: JSONServerResponseAllXXX, shouldSuppressAlert: true)
                dispatch_async(dispatch_get_main_queue(), {
                    completionHandler(json, error)
                })
                return
            }

            // handle request payload
            var returnList:[XXX] = []
            let xxxList = json[JSONServerResponse_PARAM_XXX][JSONServerResponse_PARAM_YYY].arrayValue
            .....
            dispatch_async(dispatch_get_main_queue(), {
                completionHandler(json, error)
            })
    })
}

В приведенном выше коде вы видите, что я вызываю completionHandler и даю этому вызывающему возможность настроить обработку ошибок. В большинстве случаев этот вызывающий абонент справляется с успехом.

Всякий раз, когда у меня возникла необходимость в повторных попытках, а также в других и не столь распространенных манипуляциях, я также делал это на стороне вызывающего абонента, т.е.

private func postXXXMessageInternal(completionHandler:(JSON!, NSError!) -> Void) -> NSURLSessionDataTask {
    log.function()
    return self.networkquery.postServerJsonEphemeral(url, jsonDict: self.jsonFactory.xxxMessage(),
        outgoingMsgType: JSONClientMessageToServerXXXMessage,
        expectedIncomingUserDataType: JSONServerResponseXXXMessage,
        completionHandler: {(json, error) in

            if error != nil {
                self.xxxMessageErrorWaitingCounter++
                log.error("error(\(self.xxxMessageErrorWaitingCounter)): \(error.localizedDescription)")
                if (something || somethingelse) &&
                    self.xxxMessageErrorWaitingCounter >= MAX_ERROR_XXX_MESSAGE_WAITING {
                        // reset app because of too many errors
                        xxx.currentState = AppState.yyy
                        ErrorHandler.sharedInstance.genericError(MAX_ERROR_XXX_MESSAGE_WAITING, shouldSuppressAlert: false)
                        dispatch_async(dispatch_get_main_queue(), {
                            completionHandler(json, nil)
                        })
                        self.xxxMessageErrorWaitingCounter = 0
                        return
                }

           // handle request payload
            if let msg = json[JSONServerResponse_PARAM_XXX][JSONServerResponse_PARAM_ZZZ].stringValue {
                .....
            }
            .....
            dispatch_async(dispatch_get_main_queue(), {
                completionHandler(json, error)
            })
    })
}

Вот еще один пример, когда пользователь вынужден повторить

        // user did not see a price. should have been fetched earlier (something is wrong), cancel any ongoing requests
        ErrorHandler.sharedInstance.handleSoftwareBug("potentially sw bug (or network to slow?): no payment there? user must retry")
        if let st = self.sessionTask {
            st.cancel()
            self.sessionTask = nil
        }
        // tell user
        ErrorHandler.sharedInstance.genericInfo(MESSAGE_XXX_PRICE_REQUIRED)
        // send him back
        xxx.currentState = AppState.zzz
        return

Ответ 3

Для любого запроса вы получаете либо код ошибки, либо код состояния http. Ошибка означает, что вашему приложению никогда не удалось правильно поговорить с сервером. http status code означает: ваше приложение говорило с сервером. Имейте в виду, что если вы заберете свой iPhone в ближайший Starbucks, "ваше приложение поговорило с сервером" не означает, что ваше приложение говорило с сервером, с которым он хотел поговорить ". Это может означать, что" вашему приложению удалось поговорить с сервером Starbucks, который просит вас войти в систему, и вы не знаете, как это сделать".

Я разделяю возможные ошибки на категории: "Это ошибка в моем коде". Это где вам нужно исправить свой код. "Что-то пошло не так, и пользователь может что-то сделать". Например, когда Wi-Fi отключен. "Что-то пошло не так, возможно, это работает позже". Вы можете попросить пользователя попробовать позже. "Что-то пошло не так, и пользователь ничего не может с этим поделать". Tough. "Я получил ответ от сервера, который я ожидал. Может быть, ошибка, может быть, нет, но то, что я знаю, как обращаться". Вы справляетесь с этим.

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

Когда вы показываете вещи пользователю, помните, что пользователю все равно. Пользователю: либо он работал, либо не работал. Если это не сработало, пользователь может решить проблему, если это проблема, которую они могут исправить, они могут попробовать позже или просто сложно. В корпоративном приложении у вас может быть сообщение "позвоните в справочную службу по адресу xxxxxx и скажите им yyyyyy".

И когда все не работает, не раздражайте пользователя, показывая ошибку после ошибки после ошибки. Если вы отправляете запросы, не говорите пользователю в десять раз, что сервер горит.

Есть вещи, которые вы просто не ожидаете ошибиться. Если вы загружаете файл, и вы не можете его разместить там, где он принадлежит, ну, это сложно. Это не должно произойти. Пользователь ничего не может с этим поделать. (Ну, может быть, они могут. Если память устройства заполнена, вы можете сказать пользователю). Кроме того, это та же категория, что "Что-то пошло не так, и пользователь ничего не может с этим поделать". Вы можете узнать как разработчик, в чем причина и исправить его, но если это происходит с приложением в руках пользователя, вы ничего не можете сделать.

Поскольку все такие запросы должны быть асинхронными, вы всегда будете передавать один или два блока обратного вызова на вызов, один для успеха и один для отказа. У меня большая часть обработки ошибок в коде загрузки, поэтому такие вещи, как запрос пользователя на включение Wi-Fi, происходят только один раз, и звонки могут даже повторяться автоматически, если такое условие ошибки фиксируется пользователем. Обратный вызов ошибки в основном используется для информирования приложения о том, что он не получит требуемые данные; иногда тот факт, что есть ошибка, является полезной информацией сам по себе.

Ответ 4

Для последовательной обработки ошибок я создаю свои собственные ошибки, представляющие либо ошибки, возвращаемые сеансом, либо коды состояния html, интерпретируемые как ошибки. Плюс две дополнительные ошибки "пользователь отменил" и "не разрешено взаимодействие с пользователем", если был задействован пользовательский интерфейс, и пользователь отменил операцию, или я хотел использовать какое-то взаимодействие с пользователем, но мне было запрещено. Последние две ошибки различны - эти ошибки никогда не будут сообщаться пользователю.

Ответ 5

Я бы обернул ошибки самостоятельно, но передал базовую ошибку как свойство в вашем классе ошибок (ala С# InnerException). Таким образом, вы предоставляете потребителям согласованный интерфейс, но при необходимости предоставляете информацию о более низкой степени ошибок. Однако основная причина, по которой я это сделаю, - это модульное тестирование. Это значительно облегчает издевательство вашего класса ResourceService и проверку путей кода для различных ошибок, которые могут произойти.

Мне не нравится мысль о передаче массива ошибок, так как это добавляет сложности для потребителя. Вместо этого я бы предоставил массив экземпляров InnerException. Если они являются экземплярами вашего собственного класса ошибок, у них потенциально будет свой собственный InnerException с основными ошибками. Однако это, вероятно, имеет смысл только в том случае, если вы выполняете свои собственные проверки, когда несколько ошибок могут иметь смысл. Ваш метод загрузки, вероятно, придется выручить после первой ошибки.