#swift #asynchronous #recursion #network-programming #closures
#swift #асинхронный #рекурсия #сетевое программирование #замыкания
Вопрос:
Я пишу сетевой клиент для Hacker News. Я использую их официальный API.
У меня возникли проблемы с модификацией моего сетевого клиента для работы со структурами вместо классов для комментариев к истории. Он отлично работает с классами, особенно с синхронным рекурсивным замыканием.
Это моя модель данных.
class Comment: Item {
var replies: [Comment?]?
let id: Int
let isDeleted: Bool?
let parent: Int
let repliesIDs: [Int]?
let text: String?
let time: Date
let type: ItemType
let username: String?
enum CodingKeys: String, CodingKey {
case isDeleted = "deleted"
case id
case parent
case repliesIDs = "kids"
case text
case time
case type
case username = "by"
}
}
Это пример моего сетевого клиента.
class NetworkClient {
// ...
// Top Level Comments
func fetchComments(for story: Story, completionHandler: @escaping ([Comment]) -> Void) {
var comments = [Comment?](repeating: nil, count: story.comments!.count)
for (commentIndex, topLevelCommentID) in story.comments!.enumerated() {
let topLevelCommentURL = URL(string: "https://hacker-news.firebaseio.com/v0/item/(topLevelCommentID).json")!
dispatchGroup.enter()
URLSession.shared.dataTask(with: topLevelCommentURL) { (data, urlResponse, error) in
guard let data = data else {
print("Invalid top level comment data.")
return
}
do {
let comment = try self.jsonDecoder.decode(Comment.self, from: data)
comments[commentIndex] = comment
if comment.repliesIDs != nil {
self.fetchReplies(for: comment) { replies in
comment.replies = replies
}
}
self.dispatchGroup.leave()
} catch {
print("There was a problem decoding top level comment JSON.")
print(error)
print(error.localizedDescription)
}
}.resume()
}
dispatchGroup.notify(queue: .global(qos: .userInitiated)) {
completionHandler(comments.compactMap { $0 })
}
}
// Recursive method
private func fetchReplies(for comment: Comment, completionHandler: @escaping ([Comment?]) -> Void) {
var replies = [Comment?](repeating: nil, count: comment.repliesIDs!.count)
for (replyIndex, replyID) in comment.repliesIDs!.enumerated() {
let replyURL = URL(string: "https://hacker-news.firebaseio.com/v0/item/(replyID).json")!
dispatchGroup.enter()
URLSession.shared.dataTask(with: replyURL) { (data, _, _) in
guard let data = data else { return }
do {
let reply = try self.jsonDecoder.decode(Comment.self, from: data)
replies[replyIndex] = reply
if reply.repliesIDs != nil {
self.fetchReplies(for: reply) { replies in
reply.replies = replies
}
}
self.dispatchGroup.leave()
} catch {
print(error)
}
}.resume()
}
dispatchGroup.notify(queue: .global(qos: .userInitiated)) {
completionHandler(replies)
}
}
}
Вы вызываете сетевой клиент следующим образом для получения дерева комментариев для конкретной истории.
var comments = [Comment]()
let networkClient = NetworkClient()
networkClient.fetchStories(from: selectedStory) { commentTree in
// ...
comments = commentTree
// ...
}
Переключение модели данных класса комментариев на struct не очень хорошо работает с асинхронной рекурсией замыкания. Это отлично работает с классами, потому что на классы ссылаются, тогда как структуры копируются, и это вызывает некоторые проблемы.
Как я могу адаптировать свой сетевой клиент для работы со структурами? И есть ли способ переписать мои методы в один метод вместо двух? Один метод предназначен для комментариев верхнего уровня (root), в то время как другой — рекурсия для каждого ответа на комментарий верхнего уровня (root).
Ответ №1:
Рассмотрим этот блок кода
let reply = try self.jsonDecoder.decode(Comment.self, from: data)
replies[replyIndex] = reply
if reply.repliesIDs != nil {
self.fetchReplies(for: reply) { replies in
reply.replies = replies
}
}
Если Comment
это была структура, это приведет к выборке reply
, добавлению ее копии в replies
массив, а затем к fetchReplies
изменению оригинала reply
(который вы должны были изменить на let
, чтобы var
эта строка даже компилировалась), а не копии в массиве.
Итак, вы можете захотеть сослаться replies[replyIndex]
на свое fetchReplies
закрытие, например:
let reply = try self.jsonDecoder.decode(Comment.self, from: data)
replies[replyIndex] = reply
if reply.repliesIDs != nil {
self.fetchReplies(for: reply) { replies in
replies[replyIndex].replies = replies
}
}
Кстати,
- группа отправки не должна быть свойством, а скорее должна быть локальной переменной (тем более, что вы, похоже, вызываете этот метод рекурсивно!);
- у вас есть несколько путей выполнения, при которых вы не покидаете группу (если
data
былnil
или еслиreply.repliesIDs
былnil
или если синтаксический анализ JSON не удался); и - у вас есть пути выполнения, в которых вы преждевременно покидаете группу (если
reply.repliesIDs
неnil
было, вы должны переместитьleave()
вызов в это закрытие обработчика завершения).
Я не проверял это, но я бы предложил что-то вроде:
private func fetchReplies(for comment: Comment, completionHandler: @escaping ([Comment?]) -> Void) {
var replies = [Comment?](repeating: nil, count: comment.repliesIDs!.count)
let group = DispatchGroup() // local var
for (replyIndex, replyID) in comment.repliesIDs!.enumerated() {
let replyURL = URL(string: "https://hacker-news.firebaseio.com/v0/item/(replyID).json")!
group.enter()
URLSession.shared.dataTask(with: replyURL) { data, _, _ in
guard let data = data else {
group.leave() // leave on failure, too
return
}
do {
let reply = try self.jsonDecoder.decode(Comment.self, from: data)
replies[replyIndex] = reply
if reply.repliesIDs != nil {
self.fetchReplies(for: reply) { replies in
replies[replyIndex].replies = replies
group.leave() // if reply.replieIDs was not nil, we must not `leave` until this is done
}
} else {
group.leave() // leave if reply.repliesIDs was nil
}
} catch {
group.leave() // leave on failure, too
print(error)
}
}.resume()
}
dispatchGroup.notify(queue: .main) { // do this on main to avoid synchronization headaches
completionHandler(replies)
}
}
Комментарии:
1. Спасибо за ваш ответ! Я пробовал ваши предложения, но это не сработало. Я напечатал значение до и после для этой одной строки
replies[replyIndex].replies = replies
. До печатиnil
и после печатиOptional([nil])
2. Я также попробовал принудительное разворачивание.
replies[replyIndex]!.replies = replies
Изначально это было такreplies[replyIndex]?.replies = replies
3. Да, вам придется его развернуть.
4. Кстати, все проблемы с диспетчерской группой
fetchReplies
, на которые я указал,fetchComments
также применимы.5. Большое спасибо, Роб! Я настроил другие части своего приложения, и оно работает так, как ожидалось.