Cloud Firestore iOS Codelab

1. 개요

목표

이 코드랩에서는 iOS의 Firestore 지원 레스토랑 추천 앱을 Swift로 빌드합니다. 다음 방법을 배우게 됩니다.

  1. iOS 앱에서 Firestore에 데이터 읽기 및 쓰기
  2. 실시간으로 Firestore 데이터의 변경 사항 듣기
  3. Firebase 인증 및 보안 규칙을 사용하여 Firestore 데이터 보호
  4. 복잡한 Firestore 쿼리 작성

전제 조건

이 코드랩을 시작하기 전에 다음을 설치했는지 확인하세요.

  • Xcode 버전 8.3(또는 그 이상)
  • CocoaPods 1.2.1(또는 그 이상)

2. Firebase 콘솔 프로젝트 생성

프로젝트에 Firebase 추가

  1. 로 이동 중포 기지 콘솔 .
  2. 새 프로젝트를 만들고 프로젝트 "경우 FireStore 아이폰 OS 코드 랩을"의 이름을 선택합니다.

3. 샘플 프로젝트 가져오기

코드 다운로드

복제에 의해 시작 샘플 프로젝트를 실행 pod update 프로젝트 디렉토리를 :

git clone https://github.com/firebase/friendlyeats-ios
cd friendlyeats-ios
pod update

열기 FriendlyEats.xcworkspace 엑스 코드 및 실행 (Cmd를 + R). 응용 프로그램은 제대로 컴파일하고 누락 된 이후 즉시 발사에 충돌한다 GoogleService-Info.plist 파일을. 다음 단계에서 수정하겠습니다.

Firebase 설정

에 따라 문서 새로운 경우 FireStore 프로젝트를 만들 수 있습니다. 프로젝트를했으면, 프로젝트의 다운로드 GoogleService-Info.plist 에서 파일 중포 기지 콘솔 엑스 코드 프로젝트의 루트에 드래그를. 프로젝트를 다시 실행하여 앱이 올바르게 구성되고 시작 시 더 이상 충돌하지 않는지 확인합니다. 로그인 후 아래 예시와 같은 빈 화면이 나타나야 합니다. 로그인할 수 없는 경우 인증 아래 Firebase 콘솔의 이메일/비밀번호 로그인 방법을 활성화했는지 확인하세요.

10a0671ce8f99704.png

4. Firestore에 데이터 쓰기

이 섹션에서는 앱 UI를 채울 수 있도록 Firestore에 일부 데이터를 작성합니다. 이것은을 통해 수동으로 수행 할 수 있습니다 중포 기지 콘솔 , 그러나 우리는 기본적인 경우 FireStore 기록을 보여 앱 자체에서 할 수 있습니다.

우리 앱의 주요 모델 객체는 레스토랑입니다. Firestore 데이터는 문서, 컬렉션 및 하위 컬렉션으로 분할됩니다. (이)라는 최상위 모음의 문서로 각 레스토랑 저장할 restaurants . 당신이 더 많은 경우 FireStore 데이터 모델에 대해 알아 보려면, 문서 및 컬렉션에 대해 읽어 문서 .

Firestore에 데이터를 추가하려면 먼저 레스토랑 컬렉션에 대한 참조를 가져와야 합니다. 에서 루프 내부에 다음을 추가 RestaurantsTableViewController.didTapPopulateButton(_:) 방법.

let collection = Firestore.firestore().collection("restaurants")

이제 컬렉션 참조가 있으므로 일부 데이터를 작성할 수 있습니다. 추가한 코드의 마지막 줄 바로 뒤에 다음을 추가합니다.

let collection = Firestore.firestore().collection("restaurants")

// ====== ADD THIS ======
let restaurant = Restaurant(
  name: name,
  category: category,
  city: city,
  price: price,
  ratingCount: 0,
  averageRating: 0
)

collection.addDocument(data: restaurant.dictionary)

위의 코드는 레스토랑 컬렉션에 새 문서를 추가합니다. 문서 데이터는 Restaurant 구조체에서 가져온 사전에서 가져옵니다.

거의 다 왔습니다. Firestore에 문서를 작성하기 전에 Firestore의 보안 규칙을 열고 데이터베이스의 어느 부분을 어떤 사용자가 쓸 수 있는지 설명해야 합니다. 지금은 인증된 사용자만 전체 데이터베이스를 읽고 쓸 수 있도록 허용합니다. 이것은 프로덕션 앱에 대해 너무 관대하지만, 앱 빌드 과정에서 실험하는 동안 인증 문제가 지속적으로 발생하지 않도록 충분히 완화된 것을 원합니다. 이 코드랩의 끝에서 보안 규칙을 강화하고 의도하지 않은 읽기 및 쓰기 가능성을 제한하는 방법에 대해 설명합니다.

에서 규칙 탭 중포 기지 콘솔의 다음과 같은 규칙을 추가 한 다음 게시를 클릭합니다.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

우리는 나중에 자세히 보안 규칙을 논의,하지만 당신은 서둘러 경우를 살펴 할게요 보안 규칙 문서를 .

에서 응용 프로그램 및 기호를 실행합니다. 그리고 레스토랑 문서의 일괄 처리를 생성합니다 왼쪽 상단에있는 "채우기"버튼을 눌러, 아직 응용 프로그램이 표시되지 않습니다 있지만.

그런 다음에 탐색 할 경우 FireStore 데이터 탭 중포 기지 콘솔한다. 이제 레스토랑 컬렉션에 새 항목이 표시됩니다.

스크린샷 2017-07-06 at 12.45.38 PM.png

축하합니다. iOS 앱에서 Firestore에 데이터를 작성했습니다! 다음 섹션에서는 Firestore에서 데이터를 검색하고 앱에 표시하는 방법을 배웁니다.

5. Firestore의 데이터 표시

이 섹션에서는 Firestore에서 데이터를 검색하고 앱에 표시하는 방법을 배웁니다. 두 가지 주요 단계는 쿼리 생성과 스냅샷 리스너 추가입니다. 이 수신기는 쿼리와 일치하는 모든 기존 데이터에 대한 알림을 받고 실시간으로 업데이트를 받습니다.

먼저 필터링되지 않은 기본 레스토랑 목록을 제공할 쿼리를 구성해 보겠습니다. 의 구현을 살펴보세요 RestaurantsTableViewController.baseQuery() :

return Firestore.firestore().collection("restaurants").limit(to: 50)

이 쿼리는 "restaurants"라는 최상위 컬렉션의 레스토랑을 최대 50개 검색합니다. 이제 쿼리가 있으므로 Firestore에서 앱으로 데이터를 로드하기 위해 스냅샷 리스너를 연결해야 합니다. 받는 다음 코드를 추가 RestaurantsTableViewController.observeQuery() 만 호출하면 방법을 stopObserving() .

listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
  guard let snapshot = snapshot else {
    print("Error fetching snapshot results: \(error!)")
    return
  }
  let models = snapshot.documents.map { (document) -> Restaurant in
    if let model = Restaurant(dictionary: document.data()) {
      return model
    } else {
      // Don't use fatalError here in a real app.
      fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
    }
  }
  self.restaurants = models
  self.documents = snapshot.documents

  if self.documents.count > 0 {
    self.tableView.backgroundView = nil
  } else {
    self.tableView.backgroundView = self.backgroundView
  }

  self.tableView.reloadData()
}

위의 코드는 Firestore에서 컬렉션을 다운로드하여 로컬로 배열에 저장합니다. addSnapshotListener(_:) 호출은 뷰 컨트롤러에게 데이터가 서버에 변경 때마다 업데이트됩니다 쿼리에 스냅 샷 리스너를 추가합니다. 업데이트를 자동으로 받으므로 수동으로 변경 사항을 푸시할 필요가 없습니다. 이 스냅샷 수신기는 서버 측 변경의 결과로 언제든지 호출될 수 있으므로 앱이 변경 사항을 처리할 수 있도록 하는 것이 중요합니다.

구조체 (참조 우리의 사전을 매핑 한 후 Restaurant.swift ) 데이터를 표시하는 데 몇보기 속성을 할당의 문제이다. 다음 줄을 추가 RestaurantTableViewCell.populate(restaurant:) 에서 RestaurantsTableViewController.swift .

nameLabel.text = restaurant.name
cityLabel.text = restaurant.city
categoryLabel.text = restaurant.category
starsView.rating = Int(restaurant.averageRating.rounded())
priceLabel.text = priceString(from: restaurant.price)

이 채우기 방법은 테이블 뷰 데이터 소스의에서 호출되는 tableView(_:cellForRowAtIndexPath:) 전에 각각의 테이블 뷰 셀 방식에서 값 형식의 컬렉션을 매핑을 처리합니다.

앱을 다시 실행하고 콘솔에서 이전에 본 레스토랑이 이제 시뮬레이터 또는 장치에 표시되는지 확인합니다. 이 섹션을 성공적으로 완료했다면 이제 앱이 Cloud Firestore로 데이터를 읽고 쓰는 것입니다!

2ca7f8c6052f7f79.png

6. 데이터 정렬 및 필터링

현재 우리 앱은 레스토랑 목록을 표시하지만 사용자가 필요에 따라 필터링할 수 있는 방법은 없습니다. 이 섹션에서는 Firestore의 고급 쿼리를 사용하여 필터링을 활성화합니다.

다음은 모든 딤섬 레스토랑을 가져오는 간단한 쿼리의 예입니다.

let filteredQuery = query.whereField("category", isEqualTo: "Dim Sum")

이름에서 알 수 있듯이 whereField(_:isEqualTo:) 방법은 우리의 쿼리 다운로드 필드를 우리가 설정 한 제한을 충족 컬렉션의 구성원을 만들 것입니다. 이 경우, 그것은 단지 레스토랑 다운로드 할 수 있습니다 category 있다 "Dim Sum" .

이 앱에서 사용자는 여러 필터를 연결하여 "샌프란시스코의 피자" 또는 "인기에 의해 주문된 로스앤젤레스의 해산물"과 같은 특정 쿼리를 생성할 수 있습니다.

열기 RestaurantsTableViewController.swift 와의 중간에 다음 코드 블록을 추가 query(withCategory:city:price:sortBy:) :

if let category = category, !category.isEmpty {
  filtered = filtered.whereField("category", isEqualTo: category)
}

if let city = city, !city.isEmpty {
  filtered = filtered.whereField("city", isEqualTo: city)
}

if let price = price {
  filtered = filtered.whereField("price", isEqualTo: price)
}

if let sortBy = sortBy, !sortBy.isEmpty {
  filtered = filtered.order(by: sortBy)
}

스 니핏은 상기 다수의 추가 whereFieldorder 사용자 입력에 기초하여 단일 화합물 쿼리 구축 조항. 이제 쿼리는 사용자의 요구 사항과 일치하는 레스토랑만 반환합니다.

프로젝트를 실행하고 가격, 도시 및 카테고리별로 필터링할 수 있는지 확인합니다(카테고리 및 도시 이름을 정확히 입력해야 함). 테스트하는 동안 로그에 다음과 같은 오류가 표시될 수 있습니다.

Error fetching snapshot results: Error Domain=io.grpc Code=9 
"The query requires an index. You can create it here: https://console.firebase.google.com/project/testapp-5d356/database/firestore/indexes?create_index=..." 
UserInfo={NSLocalizedDescription=The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_index=...}

Firestore에는 대부분의 복합 쿼리에 대한 색인이 필요하기 때문입니다. 쿼리에 색인을 요구하면 Firestore를 규모에 맞게 빠르게 유지할 수 있습니다. 오류 메시지에서 링크를 열면 자동으로 채워 올바른 매개 변수를 사용하여 중포 기지 콘솔에서 인덱스 생성 UI가 열립니다., 경우 FireStore의 인덱스에 대한 자세한 내용은 설명서를 참조하십시오 .

7. 트랜잭션에 데이터 쓰기

이 섹션에서는 사용자가 레스토랑에 리뷰를 제출할 수 있는 기능을 추가합니다. 지금까지 우리의 모든 글은 원자적이고 비교적 단순했습니다. 그 중 하나라도 오류가 발생하면 사용자에게 다시 시도하거나 자동으로 다시 시도하라는 메시지를 표시할 수 있습니다.

레스토랑에 등급을 추가하려면 여러 읽기 및 쓰기를 조정해야 합니다. 먼저 리뷰 자체를 제출한 다음 레스토랑의 평점 수와 평균 평점을 업데이트해야 합니다. 이 중 하나가 실패하고 다른 하나는 실패하면 데이터베이스의 한 부분에 있는 데이터가 다른 부분에 있는 데이터와 일치하지 않는 일관성 없는 상태가 됩니다.

다행히 Firestore는 단일 원자성 작업으로 여러 읽기 및 쓰기를 수행할 수 있는 트랜잭션 기능을 제공하여 데이터의 일관성을 유지합니다.

모든하자 선언 아래에 다음 코드를 추가 RestaurantDetailViewController.reviewController(_:didSubmitFormWithReview:) .

let firestore = Firestore.firestore()
firestore.runTransaction({ (transaction, errorPointer) -> Any? in

  // Read data from Firestore inside the transaction, so we don't accidentally
  // update using stale client data. Error if we're unable to read here.
  let restaurantSnapshot: DocumentSnapshot
  do {
    try restaurantSnapshot = transaction.getDocument(reference)
  } catch let error as NSError {
    errorPointer?.pointee = error
    return nil
  }

  // Error if the restaurant data in Firestore has somehow changed or is malformed.
  guard let data = restaurantSnapshot.data(),
        let restaurant = Restaurant(dictionary: data) else {

    let error = NSError(domain: "FireEatsErrorDomain", code: 0, userInfo: [
      NSLocalizedDescriptionKey: "Unable to write to restaurant at Firestore path: \(reference.path)"
    ])
    errorPointer?.pointee = error
    return nil
  }

  // Update the restaurant's rating and rating count and post the new review at the 
  // same time.
  let newAverage = (Float(restaurant.ratingCount) * restaurant.averageRating + Float(review.rating))
      / Float(restaurant.ratingCount + 1)

  transaction.setData(review.dictionary, forDocument: newReviewReference)
  transaction.updateData([
    "numRatings": restaurant.ratingCount + 1,
    "avgRating": newAverage
  ], forDocument: reference)
  return nil
}) { (object, error) in
  if let error = error {
    print(error)
  } else {
    // Pop the review controller on success
    if self.navigationController?.topViewController?.isKind(of: NewReviewViewController.self) ?? false {
      self.navigationController?.popViewController(animated: true)
    }
  }
}

업데이트 블록 내부에서 트랜잭션 개체를 사용하여 수행하는 모든 작업은 Firestore에서 단일 원자 업데이트로 처리됩니다. 서버에서 업데이트가 실패하면 Firestore가 자동으로 몇 번 다시 시도합니다. 이는 오류 조건이 반복적으로 발생하는 단일 오류일 가능성이 높다는 것을 의미합니다. 예를 들어 장치가 완전히 오프라인 상태이거나 사용자가 쓰기를 시도하는 경로에 쓸 권한이 없는 경우입니다.

8. 보안 규칙

앱 사용자는 데이터베이스의 모든 데이터를 읽고 쓸 수 없어야 합니다. 예를 들어 모든 사람이 레스토랑의 평가를 볼 수 있어야 하지만 인증된 사용자만 평가를 게시할 수 있어야 합니다. 클라이언트에서 좋은 코드를 작성하는 것만으로는 충분하지 않습니다. 백엔드에서 데이터 보안 모델을 지정해야 완전히 안전합니다. 이 섹션에서는 Firebase 보안 규칙을 사용하여 데이터를 보호하는 방법을 알아보겠습니다.

먼저 코드랩을 시작할 때 작성한 보안 규칙을 자세히 살펴보겠습니다. 에 중포 기지 콘솔 및 탐색을 열고 데이터베이스>는 경우 FireStore 탭의 규칙 .

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

request 규칙의 변수는 위의 모든 규칙에서 사용할 수있는 전역 변수이며, 조건 우리는 요청이 사용자가 아무것도 할 수 있도록 허용하기 전에 인증 보장을 추가했다. 이렇게 하면 인증되지 않은 사용자가 Firestore API를 사용하여 데이터를 무단으로 변경하는 것을 방지할 수 있습니다. 이것은 좋은 시작이지만 Firestore 규칙을 사용하여 훨씬 더 강력한 작업을 수행할 수 있습니다.

리뷰의 사용자 ID가 인증된 사용자의 ID와 일치해야 하도록 리뷰 쓰기를 제한합시다. 이렇게 하면 사용자가 서로를 가장하여 사기성 리뷰를 남길 수 없습니다. 보안 규칙을 다음으로 바꿉니다.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{any}/ratings/{rating} {
      // Users can only write ratings with their user ID
      allow read;
      allow write: if request.auth != null 
                   && request.auth.uid == request.resource.data.userId;
    }
  
    match /restaurants/{any} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

첫 번째 매치 문은 하위 컬렉션의 이름과 일치하는 ratings 에 속하는 모든 문서의 restaurants 모음. 은 allow write 조건은 다음 검토의 사용자 ID는 사용자의 일치하지 않는 경우 제출되는 모든 검토를 방지 할 수 있습니다. 두 번째 match 문을 사용하면 인증된 모든 사용자가 데이터베이스에 레스토랑을 읽고 쓸 수 있습니다.

이는 보안 규칙을 사용하여 이전에 앱에 작성한 묵시적 보증을 명시적으로 명시했기 때문에 리뷰에 매우 적합합니다. 즉, 사용자는 자신의 리뷰만 작성할 수 있습니다. 리뷰에 대한 편집 또는 삭제 기능을 추가하는 경우 이 똑같은 규칙 집합으로 인해 사용자가 다른 사용자의 리뷰도 수정하거나 삭제할 수 없습니다. 그러나 Firestore 규칙을 사용하여 전체 문서 자체가 아닌 문서 내의 개별 필드에 대한 쓰기를 제한할 수도 있습니다. 이를 사용하여 사용자가 레스토랑에 대한 평점, 평균 평점 및 평점 수만 업데이트할 수 있으므로 악의적인 사용자가 레스토랑 이름이나 위치를 변경할 가능성을 제거할 수 있습니다.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurants/{restaurant} {
      match /ratings/{rating} {
        allow read: if request.auth != null;
        allow write: if request.auth != null 
                     && request.auth.uid == request.resource.data.userId;
      }
    
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && request.resource.data.name == resource.data.name
                    && request.resource.data.city == resource.data.city
                    && request.resource.data.price == resource.data.price
                    && request.resource.data.category == resource.data.category;
    }
  }
}

여기에서 쓰기 권한을 생성 및 업데이트로 분할하여 허용해야 하는 작업에 대해 보다 구체적으로 설명할 수 있습니다. 모든 사용자는 코드랩 시작 시 만든 채우기 버튼의 기능을 유지하면서 데이터베이스에 레스토랑을 작성할 수 있지만 레스토랑이 작성되면 이름, 위치, 가격 및 카테고리를 변경할 수 없습니다. 더 구체적으로 말하면, 마지막 규칙은 데이터베이스에 이미 존재하는 필드와 동일한 이름, 도시, 가격 및 범주를 유지하기 위해 모든 레스토랑 업데이트 작업을 요구합니다.

보안 규칙을 수행 할 수있는 작업에 대한 자세한 내용은 살펴 걸릴 문서를 .

9. 결론

이 코드랩에서는 Firestore를 사용한 기본 및 고급 읽기 및 쓰기 방법과 보안 규칙으로 데이터 액세스를 보호하는 방법을 배웠습니다. 당신은에 전체 솔루션을 찾을 수 있습니다 codelab-complete 지점 .

Firestore에 대해 자세히 알아보려면 다음 리소스를 방문하세요.