【问题标题】:add children comments to parent comment array将子评论添加到父评论数组
【发布时间】:2021-12-30 10:36:40
【问题描述】:

长话短说,我必须实现一个 comments 具有嵌套 cmets(1 级)的视图控制器,遵循以下结构:

  -- Comment
     -- reply to comment 
     -- reply to comment 
  -- Comment 
  -- Comment

我曾询问后端是否可以在内部提供一个可选的子 cmets (let children[Comment]?) 数组,因此,当我在集合视图单元格中删除父级时,我不必等待后端重新加载数据,从当前数据源中删除对象。

相反,后端提出了这个 json,如果 idroot_comment_id 相同,您可以在其中了解哪个是父级。如果不是,则所有后续的 cmets 都属于第一个对象。 ????

{
    "result": [
        {
            "id": "bedcab34-f6b7-44a9-ab81-6d443ada580e",
            "post_id": "03486c50-6a4a-48a3-a68e-374cf42686d8",
            "poster": {
                "id": "5b52c4ed-bd21-49fe-9439-8722e4223d50",
                "username": "foobar",
                "fullname": "foo bar",
                "avatar_url": null
            },
            "body": "Nice Post",
            "root_comment_id": "bedcab34-f6b7-44a9-ab81-6d443ada580e",
            "to_poster": null,
            "created_at": "2021-11-05T14:38:15.000Z"
        },
        {
            "id": "43fb2e48-2aae-4b01-bafa-456b927444d5",
            "post_id": "03486c50-6a4a-48a3-a68e-374cf42686d8",
            "poster": {
                "id": "5b52c4ed-bd21-49fe-9439-8722e4223d50",
                "username": "foobar",
                "fullname": "foo bar",
                "avatar_url": null
            },
            "body": "Yes I like it too",
            "root_comment_id": "bedcab34-f6b7-44a9-ab81-6d443ada580e",
            "to_poster": null,
            "created_at": "2021-11-05T14:38:46.000Z"
        },
        {
            "id": "11a4c5d6-9db8-47c1-a472-947d8d1ac81a",
            "post_id": "03486c50-6a4a-48a3-a68e-374cf42686d8",
            "poster": {
                "id": "5b52c4ed-bd21-49fe-9439-8722e4223d50",
                "username": "foobar",
                "fullname": "foo bar",
                "avatar_url": null
            },
            "body": "Awesome!",
            "root_comment_id": "bedcab34-f6b7-44a9-ab81-6d443ada580e",
            "to_poster": {
                "id": "788343e4-3695-44e5-bda4-9b0593b7a496",
                "username": "matthewzorpas",
                "fullname": "Matthew Zorpas",
                "avatar_url": null
            },
            "created_at": "2021-11-05T14:39:45.000Z"
        }
    ],
    "statusCode": 200
}

这对我来说真的没有意义,但由于他们不会改变它,我必须映射这个 json 并制作我自己的模型,看起来像这样:

struct CommentResult: Codable {
    let result: [Comment]
    let statusCode: Int
}

// MARK: - Result
struct Comment: Codable {
    let commentID, postID: String
    let poster: Poster
    let body: String?
    let rootCommentID: String
    let toPoster: Poster?
    let createdAt: Date
    let children:[Comment]? // I would like to add this and append children with a root_id == to the parent id

    enum CodingKeys: String, CodingKey {
        case commentID = "id"
        case postID = "post_id"
        case poster
        case body
        case rootCommentID = "root_comment_id"
        case toPoster = "to_poster"
        case createdAt = "created_at"
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.commentID = try container.decode(String.self, forKey: .commentID)
        self.postID = try container.decode(String.self, forKey: .postID)
        self.poster = try container.decode(Poster.self, forKey: .poster)
        self.toPoster = try container.decodeIfPresent(Poster.self, forKey: .toPoster)
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSX"
        let dateString = try container.decode(String.self, forKey: .createdAt)
        self.createdAt = dateFormatter.date(from: dateString) ?? Date()
        self.body = try container.decodeIfPresent(String.self, forKey: .body)
        self.rootCommentID = try container.decode(String.self, forKey: .rootCommentID)
    }
}

我知道我可能必须使用filter,但我一直在努力寻找正确的方法,而不会增加时间复杂度。

所以 json 看起来应该是这样的:

{
    "result": [
        {
            "id": "bedcab34-f6b7-44a9-ab81-6d443ada580e",
            "post_id": "03486c50-6a4a-48a3-a68e-374cf42686d8",
            "poster": {
                "id": "5b52c4ed-bd21-49fe-9439-8722e4223d50",
                "username": "foobar",
                "fullname": "foo bar",
                "avatar_url": null
            },
            "body": "Nice Post",
            "root_comment_id": "bedcab34-f6b7-44a9-ab81-6d443ada580e",
            "to_poster": null,
            "created_at": "2021-11-05T14:38:15.000Z",
            "children": [  {
                  "id": "43fb2e48-2aae-4b01-bafa-456b927444d5",
                  "post_id": "03486c50-6a4a-48a3-a68e-374cf42686d8",
                  "poster": {
                      "id": "5b52c4ed-bd21-49fe-9439-8722e4223d50",
                      "username": "foobar",
                      "fullname": "foo bar",
                      "avatar_url": null
                  },
                  "body": "Yes I like it too",
                  "root_comment_id": "bedcab34-f6b7-44a9-ab81-6d443ada580e",
                  "to_poster": null,
                  "created_at": "2021-11-05T14:38:46.000Z"
              },
              {
                  "id": "11a4c5d6-9db8-47c1-a472-947d8d1ac81a",
                  "post_id": "03486c50-6a4a-48a3-a68e-374cf42686d8",
                  "poster": {
                      "id": "5b52c4ed-bd21-49fe-9439-8722e4223d50",
                      "username": "foobar",
                      "fullname": "foo bar",
                      "avatar_url": null
                  },
                  "body": "Awesome!",
                  "root_comment_id": "bedcab34-f6b7-44a9-ab81-6d443ada580e",
                  "to_poster": {
                      "id": "788343e4-3695-44e5-bda4-9b0593b7a496",
                      "username": "matthewzorpas",
                      "fullname": "Matthew Zorpas",
                      "avatar_url": null
                  },
                  "created_at": "2021-11-05T14:39:45.000Z"
              }
            ]
        }
    ],
    "statusCode": 200
}

【问题讨论】:

  • 回复的深度可以达到多少?回复也能有回复吗?
  • 你好@Rob ....幸运的是只有一层;)

标签: ios arrays json swift


【解决方案1】:

如果您尝试实现自定义解码算法,您将度过一段非常艰难的时期。我会保留 cmets 并将 cmets 映射到 cmets 集群。在这个例子中,我添加了一些方便的方法和一个名为CommentCluster 的新类型。另请注意,为简洁起见,我已经缩短了 Comment 的定义。

struct Comment: Codable {
    let commentID: String
    let postID: String
    let body: String
    let rootCommentID: String
    let createdAt: Date
}

extension Comment: Equatable {
    static func == (lhs: Comment, rhs: Comment) -> Bool {
        lhs.commentID == rhs.commentID
    }
}

struct CommentCluster {
    let root: Comment
    var children: [Comment]
}

extension Comment {
    var isRoot: Bool { rootCommentID == commentID }
    func isChild(of parent: Comment) -> Bool {
        rootCommentID == parent.commentID
    }
}

这将有助于调试:

extension Comment: CustomStringConvertible {
    var description: String {
        "\(commentID)"
    }
}
extension CommentCluster: CustomStringConvertible {
    var description: String {
        "root=\(root), children=\(children)"
    }
}

在此示例中,我假设 cmets 已按原样解码。用解码后的 cmets 替换硬编码的 cmets。

class CommentChildrenTests: XCTestCase {

    let comments: [Comment] = [
        .init(commentID: "Comment - 0", postID: "Post 0", body: "Comment - 0", rootCommentID: "Comment - 0", createdAt: .now + 0.0),
        .init(commentID: "Comment - 1", postID: "Post 0", body: "Comment - 1", rootCommentID: "Comment - 1", createdAt: .now + 1.0),
        .init(commentID: "Comment - 2", postID: "Post 0", body: "Comment - 2", rootCommentID: "Comment - 2", createdAt: .now + 2.0),
        .init(commentID: "Comment - 3", postID: "Post 0", body: "Comment - 3", rootCommentID: "Comment - 3", createdAt: .now + 3.0),

        .init(commentID: "Comment - 2-0", postID: "Post 0", body: "Comment - 2-0", rootCommentID: "Comment - 2", createdAt: .now + 4.0),
        .init(commentID: "Comment - 2-1", postID: "Post 0", body: "Comment - 2-1", rootCommentID: "Comment - 2", createdAt: .now + 5.0),
        .init(commentID: "Comment - 3-0", postID: "Post 0", body: "Comment - 3-0", rootCommentID: "Comment - 3", createdAt: .now + 6.0),
        .init(commentID: "Comment - 3-1", postID: "Post 0", body: "Comment - 3-1", rootCommentID: "Comment - 3", createdAt: .now + 7.0),
    ]

    func testIt() throws {

        let comments = comments
            .shuffled()
            .sorted(by: { lhs, rhs in
                lhs.createdAt < rhs.createdAt
            })

        let rootComments = comments
            .filter { $0.isRoot }

        let clusters = rootComments
            .map { root -> CommentCluster in
                let children = comments
                    .filter { $0 != root && $0.isChild(of: root) }
                return CommentCluster(root: root, children: children)
            }

        print("=============")
        for cluster in clusters {
            print(cluster)
        }
        print("=============")
    }
}

正如您在示例中看到的那样,我正在洗牌和排序。您没有提到 JSON 中 cmets 的顺序。因此,您可能需要也可能不需要排序。我只是想确保按顺序列出 cmets 和回复。这可能很重要。

另外,我注意到您使用的是自定义 CodingKeys 和 DateFormatter。如果您正确设置了JSONDecoder 属性,则不需要这样做。在JSONDecoder 上查看keyDecodingStrategydateDecodingStrategy

如果您的团队对此类解决方案的性能不满意,请回击后端团队。他们应该以对您的移动应用有用的格式为您提供数据。

【讨论】:

  • 单元测试加 1。我肯定会把它推回后端。
  • 是的,我选择了这种方法。我必须对我的实现做最小的改变。可能有很多 cmet,就性能而言,它不会是完美的,但我无能为力!
【解决方案2】:

你是对的,JSON 应该是不同的格式,但这并不总是可能的,这就是 DTO 模式存在的原因。

在我的解决方案中,我们将使用一个符合后端结构的 CommentDTO 对象和另一个描述应用程序使用的对象的 Comment 对象。

第一步是创建 CommentDTO 结构:

// MARK: - CommentDTO
struct CommentDTO: Codable {
    let id, postID: String
    let poster: Poster
    let body, rootCommentID: String?
    let toPoster: Poster?
    let createdAt: String
    
    enum CodingKeys: String, CodingKey {
        case id
        case postID = "post_id"
        case poster, body
        case rootCommentID = "root_comment_id"
        case toPoster = "to_poster"
        case createdAt = "created_at"
    }
}

// MARK: - Poster
struct Poster: Codable {
    let id, username, fullname: String
    let avatarURL: String?

    enum CodingKeys: String, CodingKey {
        case id, username, fullname
        case avatarURL = "avatar_url"
    }
}

typealias CommentDTOS = [CommentDTO]

第二步是创建 Comment 结构,在这种情况下,我更喜欢使用 'Class' 而不是 'struct' 来添加对每个评论 Root 和 Children 的直接引用。如果您使用“struct”,则可以只引用 Children,因为 struct 不能具有递归包含它的存储属性。

class Comment {
    
    let id, postID : String
    let poster : Poster
    let body : String?
    let toPoster : Poster?
    let createdAt : String
    let rootComment : Comment?
    var childrenComments : [Comment]?
        
    init(commentDto : CommentDTO)
    {
        self.id = commentDto.id
        self.postID = commentDto.postID
        self.poster = commentDto.poster
        self.body = commentDto.body
        self.toPoster = commentDto.toPoster
        self.createdAt = commentDto.createdAt
        self.rootComment = nil
        self.childrenComments = nil
    }
    
    init(commentDto : CommentDTO, root : Comment)
    {
        self.id = commentDto.id
        self.postID = commentDto.postID
        self.poster = commentDto.poster
        self.body = commentDto.body
        self.toPoster = commentDto.toPoster
        self.createdAt = commentDto.createdAt
        self.rootComment = root
        self.childrenComments = nil
    }
    
    func addChild(comment : Comment)
    {
        if self.childrenComments == nil
        {
            self.childrenComments = []
        }
        
        self.childrenComments?.append(comment)
    }
}

算法真的很简单,像你说的那样使用过滤器。 现在我们有一个 CommentDTO 数组,我们将选择第一个 id 等于 rootCommentID 的评论,构造一个 CommentObject 并将其放入 Map 中,以便在 O(1) 时间内完成一些操作。 在我们只选择不是根的元素之后,我们会将它们添加到我们的结构中。

if let url = Bundle.main.url(forResource: "Comments", withExtension: "json") {
            do {
                //1 : Reading data from JSON and put in an Array of CommentDTO
                let data = try Data(contentsOf: url)
                let decoder = JSONDecoder()
                let commentDTOS = try decoder.decode(CommentDTOS.self, from: data)
                
                //2 : Create a Map that contain only Comment Rootes for doing some operation in O(1)
                var commentMap : [String : Comment] = [:]
                
                //3 : Using Filter for select only roots
                let roots = commentDTOS.filter { $0.id == $0.rootCommentID }
                //4 : Create a Root Comment and put it in a Map
                roots.forEach {
                    let comment = Comment(commentDto: $0)
                    commentMap[$0.id] = comment
                }
                
                //5 : Using Filter for select only children
                let children = commentDTOS.filter { $0.id != $0.rootCommentID }
                children.forEach {
                    //6 : Retrieve root from Map
                    let root = commentMap[$0.rootCommentID!]
                    if let root = root
                    {
                        //7 : Create comment and update Root Children list
                        let comment = Comment(commentDto: $0, root: root)
                        root.addChild(comment: comment)
                    }
                }
                
                let output = Array(commentMap.values) as [Comment]
            } catch {
                print("error:\(error)")
            }
        }

'output' 是一个仅包含根的 Comment 数组,您将通过它们的引用访问子项。

【讨论】:

  • Ciao Antonio...非常感谢...我同意您的观点,生活在“完美世界”中并不总是可能的! ;) +1 以获得您的详细答案。让我尝试实现它!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多