ios/swift Firebase database 이중 배열 읽기

3일 내내 firebase 데이터를 이렇게도 받아보고 저렇게도 받아보다..

스트레스 받아서 결국엔 갈아엎고 아예 색다른 버젼으로 재구성했습니다.

다시는 이렇게 삽질하고 싶지 않아서 글로 남깁니다.

 

받을 데이터의 구조

저는 firebase 내의 서비스인 realtimeDB 를 사용하고 있습니다.

이번 글에서 다룰 DB를 json형태로 나타내보겠습니다.

{
  "places" : {
    "autoid2" : {
      "address_korean" : "개포로 321길 32",
      "address_latitude" : 23.31,
      "address_longtitude" : 334.23,
      "category" : "japanese",
      "image_address" : "yeonsushi_image.jpg",
      "place_name" : "개포동 연스시",
      "rating" : 3.2,
      "user_comments" : {
        "autocomment1" : {
          "comment" : "굿굿",
          "comment_image_address" : "yeonsushi_image.jpg",
          "comment_rating" : 3.2,
          "user_id" : "joockim"
        }
      }
    },
    "place" : {
      "address_korean" : "개포로 213길 42",
      "address_latitude" : 123.232,
      "address_longtitude" : 34.12,
      "category" : "korean",
      "image_address" : "savor_image.jpg",
      "place_name" : "개포동 사보르",
      "rating" : 3.2,
      "user_comments" : {
        "comment1" : {
          "comment" : "맛있어요",
          "comment_image_address" : "savor_image.jpg",
          "comment_rating" : 3.2,
          "user_id" : "kchoi"
        },
        "user_comment" : {
          "comment" : "맛없어요",
          "comment_image_address" : "savor_image.jpg",
          "comment_rating" : 3.2,
          "user_id" : "sujilee"
        }
      }
    }
  }
}

 

전체 데이터 읽기

전체 데이터는 DataSnapshot이라는 자료형으로 들어오는데, 다음과 같이 받을 수 있습니다. 여기까지는 공식문서만 잘 따라가도 무난히 처리하셨을 거라 생각합니다.

 

참고로 아래의 self.ref는 다음과 같이 선언돼 있습니다:

var ref: DatabaseReference!

    override func viewDidLoad() {
    ...
    testGetPlaceInfo()
  } 

    func testGetPlaceInfo() {
        self.ref.child("places").observeSingleEvent(of: .value, andPreviousSiblingKeyWith: {
          (snapshot, error) in
            let places = snapshot.value as? [String:AnyObject] ?? [:]
            // print(type(of: places)) // Dictionary<String, AnyObj>
            self.placesArray = Array(places)

            DispatchQueue.main.async {
                self.placesTableView.reloadData()
            }
        })
    }

위에서 self.placesArray에 Array(places) 로 저장하는 것을 기억해주세요.

참고로 self.placesArray는 var placesArray: Array<(key: String, value: AnyObject)>? 이렇게 선언돼 있습니다.

 

위에서 places에 snapshot.value를 [String:AnyObject] 형태로 받고 있는데, 이것을 출력해보면 다음과 같습니다.

참고로 그 type(of:places)는 주석에 나와있듯이 Dictionary<String, ObjectAny> 입니다.

 

// print(places)
["place": {
    "address_korean" = "\Uac1c\Ud3ec\Ub85c 213\Uae38 42";
    "address_latitude" = "123.232";
    "address_longtitude" = "34.12";
    category = korean;
    "image_address" = "savor_image.jpg";
    "place_name" = "\Uac1c\Ud3ec\Ub3d9 \Uc0ac\Ubcf4\Ub974";
    rating = "3.2";
    "user_comments" =     {
        comment1 =         {
            comment = "\Ub9db\Uc788\Uc5b4\Uc694";
            "comment_image_address" = "savor_image.jpg";
            "comment_rating" = "3.2";
            "user_id" = kchoi;
        };
        "user_comment" =         {
            comment = "\Ub9db\Uc5c6\Uc5b4\Uc694";
            "comment_image_address" = "savor_image.jpg";
            "comment_rating" = "3.2";
            "user_id" = sujilee;
        };
    };
}, "autoid2": {
    "address_korean" = "\Uac1c\Ud3ec\Ub85c 321\Uae38 32";
    "address_latitude" = "23.31";
    "address_longtitude" = "334.23";
    category = japanese;
    "image_address" = "yeonsushi_image.jpg";
    "place_name" = "\Uac1c\Ud3ec\Ub3d9 \Uc5f0\Uc2a4\Uc2dc";
    rating = "3.2";
    "user_comments" =     {
        autocomment1 =         {
            comment = "\Uad7f\Uad7f";
            "comment_image_address" = "yeonsushi_image.jpg";
            "comment_rating" = "3.2";
            "user_id" = joockim;
        };
    };
}]

 

여기서 알아볼 수 없는 \Uac1c\Ud3ec 같은 것들은 Encoding하지 않고 DB에 올려서 그렇게 된 것입니다. 그러니까 DB에 올릴 때에는 utf8 형식으로 Encoding한 뒤 올리고, 받을 때에도 decoding한 뒤 내려 받으면 정상적으로 출력됩니다.

 

tableView에서 각각의 place 정보를 읽어내기

저는 place 배열에서 place 하나의 정보를 얻어내고 싶었습니다. 이것 때문에 place를 NSDictionary형으로도 받아보고 그냥 Dictionary 형으로 받아보는 등 수십 가지 방식을 동원했지만 결국 안돼서 상당히 스트레스를 받았습니다..

그러나 결국 파훼법(?)을 찾아냈고 그 결과를 공유하려고 합니다.

아래를 잘 봐주세요

 

먼저 tableViewDataSource 및 Delegate를 상속했을 때 필수적으로 써야 하는 cellForRowAt 함수입니다.

해당 함수에서 저는 custom하게 만든 tableViewCell인 PlaceTableViewCell을 cell 이라는 변수에 일단 담았습니다.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        guard let cell: PlaceTableViewCell = tableView.dequeueReusableCell(withIdentifier: self.placesTableViewCellIdentifier, for: indexPath) as? PlaceTableViewCell
        else { return UITableViewCell() }

  ...

        return (cell)
}

 

그리고 나서 아까 저장해두었던 var placesArray: Array<(key: String, value: AnyObject)>? 을 활용해보도록 하겠습니다.

먼저 placesDataArray가 안정적으로 추출되는지 확인합니다.

func tableView(... cellForRowAt ...) {
  ...
              guard let placesDataArray: Array<(key: String, value: AnyObject)> = self.placesArray
        else {
            print("getting self.placesArray error")
            return UITableViewCell()
        }
  ...
  return (cell)
}

 

그 다음으로 indexPath.row를 이용해서 indexPath.row 번째 cell의 데이터를 채워보도록 하겠습니다.

아래 코드에서 for - in 구문은 placesDataArray의 index 번째 정보를

참고로 아래에서 storageImageReference는 Firebase Storage에 있는 이미지를 다운받기 위한 변수입니다.

    func tableView(... cellForRowAt ...) {
  ...
  //    아래 storageImageReference 변수를 사용하려면 viewController에    
  // let storage = Storage.storage() 를 추가해주세요.
        let storageImagesReference = self.storage.reference(withPath: "images")

        for index in 0..<placesDataArray.count {
            if index == indexPath.row {
                let place = placesDataArray[index]

                guard let addressLabelText = place.value["address_korean"]
                        as? String
                else {return UITableViewCell()}
                guard let placeNameLabelText = place.value["place_name"]
                        as? String
                else {return UITableViewCell()}
                guard let categoryLabelText = place.value["category"]
                        as? String
                else {return UITableViewCell()}
                guard let ratingLabelRawValue = place.value["rating"]
                        as? Double
                else {return UITableViewCell()}
                guard let imageAddress = place.value["image_address"]
                        as? String
                else {return UITableViewCell()}
                let imageReference = storageImagesReference.child("\(imageAddress)")
                print(imageAddress)
                imageReference.getData(maxSize: 10 * 1024 * 1024, completion: {
                    (data, error) in
                    if let error = error {
                        print(error)
                    }
                    else {
                        let image = UIImage(data: data!)
                        cell.placeImageView?.image = image
                        cell.addressLabel?.text = addressLabelText
                        cell.placeNameLabel?.text = placeNameLabelText
                        cell.categoryLabel?.text = categoryLabelText
                        cell.ratingLabel?.text = String(ratingLabelRawValue)
                    }
                })

            }
        }  
  return (cell)
}

 

이미지를 다운받는 로직도 포함시켜서 넣었는데, 조금 tmi였나요..?

아무튼 요지는 let place = placesDataArray[index] 를 통해 데이터를 받고 있다는 것입니다.

place를 출력해보면 다음과 같이 나옵니다.

 

(key: "place", value: {
    "address_korean" = "\Uac1c\Ud3ec\Ub85c 213\Uae38 42";
    "address_latitude" = "123.232";
    "address_longtitude" = "34.12";
    category = korean;
    "image_address" = "savor_image.jpg";
    "place_name" = "\Uac1c\Ud3ec\Ub3d9 \Uc0ac\Ubcf4\Ub974";
    rating = "3.2";
    "user_comments" =     {
        comment1 =         {
            comment = "\Ub9db\Uc788\Uc5b4\Uc694";
            "comment_image_address" = "savor_image.jpg";
            "comment_rating" = "3.2";
            "user_id" = kchoi;
        };
        "user_comment" =         {
            comment = "\Ub9db\Uc5c6\Uc5b4\Uc694";
            "comment_image_address" = "savor_image.jpg";
            "comment_rating" = "3.2";
            "user_id" = sujilee;
        };
    };
})

 

그리고 place는 Dictionary형태이기 때문에, place["key_name"] 에서 key_name 부분에 키 값을 적어줌으로써 원하는 key의 value를 얻어낼 수 있습니다.

아래 함수의

guard let addressLabelText = place.value["address_korean"]
                        as? String
else {return UITableViewCell()}

부분을 예로 들 수 있습니다.

 

아래는 cellForRowAt 함수의 전체 코드입니다.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let storageImagesReference = self.storage.reference(withPath: "images")

        guard let cell: PlaceTableViewCell = tableView.dequeueReusableCell(withIdentifier: self.placesTableViewCellIdentifier, for: indexPath) as? PlaceTableViewCell
        else { return UITableViewCell() }

        guard let placesDataArray: Array<(key: String, value: AnyObject)> = self.placesArray
        else {
            print("getting self.placesArray error")
            return UITableViewCell()
        }

        for index in 0..<placesDataArray.count {
            if index == indexPath.row {
                let place = placesDataArray[index]
                print(place)
                guard let addressLabelText = place.value["address_korean"]
                        as? String
                else {return UITableViewCell()}
                guard let placeNameLabelText = place.value["place_name"]
                        as? String
                else {return UITableViewCell()}
                guard let categoryLabelText = place.value["category"]
                        as? String
                else {return UITableViewCell()}
                guard let ratingLabelRawValue = place.value["rating"]
                        as? Double
                else {return UITableViewCell()}
                guard let imageAddress = place.value["image_address"]
                        as? String
                else {return UITableViewCell()}
                let imageReference = storageImagesReference.child("\(imageAddress)")
                print(imageAddress)
                imageReference.getData(maxSize: 10 * 1024 * 1024, completion: {
                    (data, error) in
                    if let error = error {
                        print(error)
                    }
                    else {
                        let image = UIImage(data: data!)
                        cell.placeImageView?.image = image
                        cell.addressLabel?.text = addressLabelText
                        cell.placeNameLabel?.text = placeNameLabelText
                        cell.categoryLabel?.text = categoryLabelText
                        cell.ratingLabel?.text = String(ratingLabelRawValue)
                    }
                })

            }
        }
        return (cell)
    }

 

여기서 깜빡한 게 하나 있군요.

tableView에는 하나 더 구현해주어야 하는 메서드가 있죠?

아래와 같이 numOfRowsInSection 함수를 구현해주도록 합시다.

 

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let places = self.placesArray
        else {
            print ("error on getting placesArray")
            return 0
        }
        return places.count
    }

 

배열 속 배열의 값을 얻어내기

위에서 place를 얻어온 원리를 이용해 각각의 place별로 저장돼 있는 user_comments 배열 정보를 얻어와 보겠습니다.

어떤 셀을 누르면, 해당 셀의 정보를 다음 viewController에 넘길 예정입니다.

여기서 정보에는 comments 배열도 포함합니다.

 

먼저 didSelectRowAt 함수를 구현해주도록 합시다.

어떤 셀을 클릭했을 때 일어나는 이벤트를 관리하는 메서드입니다.

위에서 설명했듯이 places에 대한 정보를 담고 있는 self.placesArray 를 안전하게 추출합시다.

또, 현재 클릭한 cell의 정보도 얻어야 하니 cell도 추출해주도록 합니다.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    guard let placesDataArray: Array<(key: String, value: AnyObject)> = self.placesArray
    else {
            print("getting self.placesArray error on didselectRowAt")
            return
    }

    guard let cell: PlaceTableViewCell = self.placesTableView.cellForRow(at: indexPath) 
          as? PlaceTableViewCell
    else {return}
  ...
}

 

제 데이터는 간단히 표현하자면 다음과 같은 구조를 띄고 있었습니다.

[places: [
    {place1: {
        image_address_korean:"~",
        ...,
        user_comments: [{
            comment1: {
                ...
            }
        },
        {
            comment2: {
                ...
            }
        }
        ]
    }, place2: {
        ...
    }}
]]

 

갑자기 이걸 왜 보여주냐구요?

어떤 place 하나에 대해 그 안의 comment는 전부 user_comments라는 이름의 키를 가지는 데이터 뭉치가 가지고 있다는 걸 얘끼하기 위해서입니다.

이를 알아두면 아래 코드를 이해하기 편할 것 같았거든요.

다음 코드도 살펴봅시다.

 

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    ...
          for index in 0..<placesDataArray.count {
            if index == indexPath.row {
                let place = placesDataArray[index]
//                print(place.key) // place의 key를 얻어올 수 있습니다.

              // 아래에서 places 밑의 place.key 라는 key를 가진 객체 밑의 /user_comments를 얻어옵니다.
                self.ref.child("places/\(place.key)/user_comments").observeSingleEvent(of: .value, andPreviousSiblingKeyWith: { 
                  (snapshot, error) in

                  // commentsInfo는 snapshot.value, 그러니까 place 하나의 정보를 갖습니다.
                    let commentsInfo = snapshot.value as? [String:AnyObject] ?? [:]
                  // Array로 변환해서 다음 viewController에 저장할 얘정입니다.
                    let commentsInfoArray = Array(commentsInfo)
                    print(type(of: placeInfoArray)) //Array<(key: String, value: AnyObject)>
                  // placeInfoArray의 타입이 위와 같기 때문에
                  // nextViewController에도 위와 같은 필드를 선언해줍시다.
                  // 저는 nextViewController에 
                  // commentsArray: Array<(key: String, value: AnyObject)>?
                  // 라고 선언했습니다.
                    let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
                  // 아래는 nextViewController를 navigationController 방식으로 띄우는 코드입니다.
                    if let nextViewController = storyBoard.instantiateViewController(identifier: "DetailPlaceViewController") as? DetailPlaceViewController {
                        DispatchQueue.main.async {
                            nextViewController.commentsArray = commentsInfoArray
                            nextViewController.addressText = cell.addressLabel?.text
                            nextViewController.placeNameText =
                                cell.placeNameLabel?.text
                            nextViewController.ratingText = cell.ratingLabel?.text
                            nextViewController.categoryText = cell.categoryLabel?.text
                            nextViewController.placeImage = cell.placeImageView?.image
                            self.navigationController?.pushViewController(nextViewController, animated: true)
                        }
                    }
                    else {
                        print("storyBoard instantiating failure")
                        return
                    }

                })
            }
        }

    }

}

 

여기까지 잘 따라하셨다면, 다음으로 넘어가봅시다.

nextViewController를 담당하는 DetailPlaceViewController는 다음의 필드를 가지고 있습니다.

참고로만 알아둡시다.

 

      var addressText: String?
    var placeNameText: String?
    var ratingText: String?
    var categoryText: String?
    var placeImage: UIImage?
    var commentsArray: Array<(key: String, value: AnyObject)>?



    @IBOutlet weak var placeNameLabel: UILabel!
    @IBOutlet weak var categoryLabel: UILabel!
    @IBOutlet weak var addressLabel: UILabel!
    @IBOutlet weak var ratingLabel: UILabel!
    @IBOutlet weak var placeImageView: UIImageView!
    @IBOutlet weak var commentsTableView: UITableView!
    let tableViewCellIdentifier: String = "commentTableViewCell"

    let storage = Storage.storage()

 

DetailViewController에서도 tableView를 이용해 comment를 하니씩 출력할 예정입니다.

어떻게 출력하는지 여러분들에게 전체 코드를 보여주면서 글을 마치도록 하겠습니다.

for - in 구문을 잘 살펴봐주세요.

참고로 굳이 guard-let을 두 번씩 써준 이유는 comment를 commentsArray[index] 로 받을 때

Optional(Optional<Any>))와 같이 받아져서 그렇습니다.

 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let storageImagesReference = self.storage.reference(withPath: "images")

        guard let cell: CommentTableViewCell = self.commentsTableView.dequeueReusableCell(withIdentifier: self.tableViewCellIdentifier) as? CommentTableViewCell
        else {return UITableViewCell()}

        guard let commentsArray = self.commentsArray
        else {
            print("getting self.commentsArray error")
            return UITableViewCell()

        }
        for index in 0..<commentsArray.count {
            if index == indexPath.row {
                let comment = commentsArray[index]
                print("comment Info")
                print(comment)
                print(comment.value)
                guard let commentUserId = comment.value["user_id"]
                else {
                    print ("getting comment.value['user_id'] error")
                    return UITableViewCell()
                }
                guard let commentComment = comment.value["comment"]
                else {
                    print("getting comment.value['comment'] error")
                    return UITableViewCell()
                }
                guard let commentRating = comment.value["comment_rating"]
                else {
                    print("getting comment.value['comment_rating'] error")
                    return UITableViewCell()
                }
                guard let commentImageAddress = comment.value["comment_image_address"] as? String
                else {
                    print("getting comment.value['comment_image_address'] error")
                    return UITableViewCell()
                }
                guard let commentUserIdText = commentUserId as? String
                else {
                    print("unwrapping error")
                    return UITableViewCell()
                }
                guard let commentRatingDouble = commentRating as? Double
                else {
                    print("unwrapping error on commentRating to commentRatingText")
                    return UITableViewCell()
                }
                guard let commentCommentText = commentComment as? String
                else {
                    print("unwrapping error")
                    return UITableViewCell()
                }
                let imageReference = storageImagesReference.child("\(commentImageAddress)")
                imageReference.getData(maxSize: 10 * 1024 * 1024, completion: {
                    (data, error) in
                    if let error = error {
                        print(error)
                    }
                    else {
                        let image = UIImage(data: data!)
                        cell.commentImageView?.image = image
                        cell.userIdLabel?.text = commentUserIdText
                        cell.commentLabel?.text = commentCommentText
                        cell.ratingLabel?.text = String(commentRatingDouble)
                    }
                })


            }
        }

        return cell
    }

 

numberOfRowsInSection

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        guard let commentsArray = self.commentsArray
        else {
            print("getting self.commentsArray error on numOfRowsInSection")
            return 0
        }
        return (commentsArray.count)
    }

 

아직 완성본은 아니라 깔끔하게 보이진 않겠지만, 어떤 식으로 흘러가는지 보여주는 데에는 문제가 없다고 생각합니다.

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기
// custom