Apple Developer Program과 Apple Developer Enterprise Program이 있다. 먼저, Apple Developer Program은 배포에 필요한 도구에 대한 접근 권한을 제공하는데, 이 안에 두 가지 Entity Type이 존재하고 먼저, individual 개인이 있어서 앱이 개발자 개인 이름으로 등록되는 것이고, 조직 Entity Type은 법인의 이름으로 등록된다.
두 번째 Enterprise Program은 대규모 조직 내부에서 사용하는 전용 앱을 배포할 수 있도록 하고, MDM 솔루션을 통해 직원들에게 비공개 배포해야하는 경우 해당 멤버십을 사용한다.
두 라이센스의 가장 큰 차이점은, Apple Developer Program의 경우 App Store에 공개 앱을 배포 하고 Apple Developer Enterprise Program의 경우 조직 내부에서만 사용하는 전용 앱을 배포할 시 에 사용한 다는 것이다.
앱스토어 배포 순서
1. CSR 인증서 요청 발급 받아야함 -> 맥에서 키체인 화면 띄워서 받고 이를 Certificates, IDs & Profile 메뉴를 선택하고 Certificate 인증서 생성하는데 사용해서 만든다.
2. 같은 Certificates, IDs & Profile에서 Identifier 등록해야 하는데 이때 다. Identifier를 추가하는데, 이때 배포할 앱의 Bundle ID를 입력해주어야 한다.
3. Provisioning Profile(프로비저닝 프로파일)을 생성해야한다. 같은 메뉴에서 Profile 로 들어가 Register a New Provisioning Profile을 진행한다. Distribution에 AppStore를 누르고 앞서 생성한 App ID와 Certificate을 선택하여 진행합니다. 프로비저닝 프로파일은 다운받아 놓는다.
4. Xcode에서 Automatically managing signing을 체크하면 자동으로 프로비저닝 파일이 연결된다.
5. 아카이브
6. Appstore Connect에서 빌드 파일 올라오면 스크린샷 올리고, 현지화 필요하면 하고 제출!
(스크린샷 디바이스 사이즈별로 만들고 현지화하고, 이렇게 되면 스크린샷 현지화도 필요하고, 프로모션 텍스트도 적고, 앱 설명에 약관동의 개인정보 등 해야할 일이 아주아주 많다는 것)
앱 배포시 Primary Language를 바꾸고 싶을 때는?
크롬 Language를 바꿔야하고, 이미 이전에 제출한 앱을 업데이트 한것이라면 제출한 다음에 크롬 변경하고 앱 정보에서 다시 변경해야한다.
앱은 포그라운드와 백그라운드에서 다르게 동작되어야 한다. 왜냐하면 iOS 디바이스에서는 시스템 리소스의 제한이 있기 때문이다. 일반적인 블루투스의 작동방식은 앱이 백그라운드/Suspended 상태로 갔을 때 중앙장치나 주변장치 모두 disabled처리 되는 것이다.
그 말은 즉 앱에 코어 블루투스 백그라운드 실행 모드를 살려서 앱을 깨우거나 할 수 있다는 것이다. 백그라운드 프로세싱 전부를 지원하지는 못하더라도 시스템이 여전히 어떤 중요한 이벤트가 일어났다고는 알려줄 수 있다.
만약 앱이 코어 블루투스 백그라운드 실행 모드를 지원한다면, 이 앱은 계속 실행시킬 수 없다. 어떤 지점에서 시스템은 앱을 종료해서 메모리 해제를 해줘서 현재 포그라운드에서 작동되는 앱을 실행가능하도록 해야하기 때문이다. 그래서 펜딩이라던지 활성화된 연결이 없어질수가 있다. iOS7 부터 코어 블루투스는 중앙장치와 주변장치의 상태를 저장하는 것을 지원한다. 그리고 앱이 실행되면 이 상태를 다시 저장한다. 이 기능을 활용해서 블루투스 디바이스를 활용한 롱텀 실행이 가능할 것이다.
Foreground-Only Apps
대부분의 iOS 앱들은 백그라운드 스테이트를 가면 suspended state로 변경된다. 특정 태스크를 백그라운드에 돌리겠다고 권한을 받지 않을 경우에 말이다. 서스펜디드 상태에서는 다시 포그라운드로 돌아오기 전까지는 블루투스 관련된 태스크를 진행할 수 없다. 혹은 어떤 블루투스 관련된 이벤트를 진행할 수 없다.
중앙장치 사이드에서는 포그라운드 앱(즉, 백그라운드 실행모드를 선언하지 않은 앱)에서는 백그라운드나 서스펜드 상태에서 scan 처리또한 할 수 없다. 주변장치에서는 advertising 자체가 작동불가하며 그 어떤 중앙장치가 앱의 퍼블리쉬된 서비스의 characterisitc 값에 접근하려고 하면 error를 받는다.
사용자 케이스에 따라 이러한 일반적 행동은 앱의 여러방향으로 영향을 줄 수 있다. 예를 들면 사용자가 현재 연결된 주변장치와 인터렉팅 하고싶을 때. 또는, 앱이 서스펜디드 스테이트로 갔을 때(유저가 다른 앱으로 전환해서). 만약 앱이 서스펜드 상태에서 주변장치와의 연결이 끊어진다해도 다시 앱을 켜는 순간까지 장치와의 연결이 끊어졌다는 사실을 알 수 다.
Take Advantage of Peripheral Connection Options
포그라운드에서만 작동되는 앱에서는 서스펜디드 스테이트에 갔을 때 블루투스 관련 이벤트들은 시스템에 의해 queue 되어지고, 앱이 다시 포그라운드 작업으로 갔을 때 실행이 된다. 코어블루투스는 유저에게 이벤트를 알릴 수 있는 방법을 제공하는데, 이를 이용해서 포그라운드로 이벤트들을 가져올건지 정하면 된다.
아래 CBCentralManager 클래스에 있는 주변장치 연결 관련 메소드들을 이용하면 된다.
CBConnectPeripheralOptionNotifyOnConnectionKey—Include this key if you want the system to display an alert for a given peripheral if the app is suspended when a successful connection is made.
앱은 서스펜드 스테이트 상태인데, 주어진 주변장치와의 연결이 성공적으로 되었을 때 알림창을 준다.
CBConnectPeripheralOptionNotifyOnDisconnectionKey—Include this key if you want the system to display a disconnection alert for a given peripheral if the app is suspended at the time of the disconnection.
앱이 서스펜드 스테이트 상태인데, 주변장치와의 연결이 끊어졌을 때 연결해제되었다는 알림창을 준다.
CBConnectPeripheralOptionNotifyOnNotificationKey—Include this key if you want the system to display an alert for all notifications received from a given peripheral if the app is suspended at the time.
앱이 서스펜드 스테이트 상태인데 주변장치로부터 받은 모든 노티피케이션을 알림창으로 주고싶을 때이다.
Core Bluetooth Background Execution Modes
만약 백그라운드도 블루투스 관련 테스크들이 동작하도록 하려면?
1. info.plist에 정의한다.
이때 앱은 서스펜디드 상태에서 일어나 블루투스 관련된 이벤트들을 처리할 수 있도록 해준다. 이러한 지원은 심박수 모니터와 같은 앱, 일정한 간격으로 데이터를 전달하는 BLE 디바이스와 소통할 때 중요하다.
나의 앱이 central의 장치를 하는지 peripheral의 역할을 하는지에 따라 info.plist에 정의하는 값은 달라진다.
bluetooth-central—The app communicates with Bluetooth low energy peripherals using the Core Bluetooth framework.
bluetooth-peripheral—The app shares data using the Core Bluetooth framework.
하지만 백그라운드로 돌리더라도 제약사항이 너무 많다.
- 시스템의 상황에 따라서 앱이 백그라운드에 실행이 되지 않을수도 있고,, 복잡한 수행은 할 수 없고 간단한 통신값 저장 정도할 수 있으며 10초 이내에 종료되는 작업이여야 한다.
The bluetooth-central Background Execution Mode
위와 같이 info.plist에 장치 특성에 따라 값을 저장해주면 이제 앱에서 블루투스관련된 태스크를 백그라운드로 도릴 수 있다. 백그라운드에 있더라도 주변장치를 스캔하거나 데이터를 가지고 상호작용할 수 있다. 그리고 시스템이 앱을 꺠운다. CBCentralManagerDelegateorCBPeripheralDelegatedelegate 관련 메소드들이 발동될 때마다! 이를 통해서 중앙장치의 중요한 이벤트들이 핸들링 가능하다. 센트럴매니저의 상태값이 변경된다던지, characterisic value가 변동된다던지 하는 것들을 모두 포착할 수 있다.
주의 사항으로는 백그라운드에서 스캔을 진행할 시에 스캔 옵션키로 정해놓은 디바이스만 찾아지는게 아니라 모든 디바이스가 다 찾아진다는 점이다. 또한, 중앙장치가 광고 패킷을 스캔하는 인터벌이 길어진다. 그래서 주변장치를 발견하는 시간이 길어진다.
Use Background Execution Modes Wisely
백그라운드 실행모드를 현명하게 쓰는 것이 중요하다. 디바이스의 배터리 수명을 위해서 백그라운드에서 하는 작업은 최소화 하자. 그리고 아래와 같은 3가지 가이드라인을 따르자.
1. 앱은 세션 베이스여야 한다. 유저가 블루투스 이벤트를 언제 시작하고 끝마칠지에 대한 인터페이스를 제공해야한다.
2. 앱이 백그라운드에서 깨어있는 동안 10초 이내로 태스크가 완수되어야 한다. 다시 서스펜드 모드로 갈 수 있도록 가장 빠르게 테스크를 완수 해야한다. 백그라운드에서 너무 길게 실행되는 테스크가 있다면 앱이 kill될 것이다.
Background : 앱 사용중에 다른 앱을 실행하거나 홈 화면으로 나갔을 때 상태입니다. 백그라운드에서 동작하는 코드를 추가하면 suspended 상태로 넘어가지 않고 백그라운드 상태를 유지하게 됩니다. 처음부터 background 상태로 실행되는 앱은 inactive 대신 background 상태로 진입합니다. 음악을 실행하고 홈 화면으로 나가도 음악이 나오는 상태가 이 경우에 해당됩니다.
Suspended : 앱이 background 상태에서 추가적인 작업을 하지 않으면 곧바로 suspended 상태로 진입합니다. 앱을 다시 실행할 경우 빠른 실행을 위해 메모리에만 올라가 있습니다. 메모리가 부족한 상황이 되면 iOS는 suspended 상태에 있는 앱들을 메모리에서 해제시켜서 메모리를 확보합니다.
ld: in ......./Pods/FirebaseAnalytics/Frameworks/FIRAnalyticsConnector.framework/FIRAnalyticsConnector(FIRAnalyticsConnector_8094107d82c527bf23f93e98c9db96d1.o), building for iOS Simulator, but linking in object file built for iOS, file '...../Pods/FirebaseAnalytics/Frameworks/FIRAnalyticsConnector.framework/FIRAnalyticsConnector' for architecture arm64
-------> 아래에 길게 과정이 있지만 결론을 보면 cocoapod을 SPM으로 마이그레이션 하는것 추천
1) 커맨드+쉬프트+K -> Clean Build하고 리 빌드시 -> 해결 X
2) Excluded Architectures에 arm64를 넣어 빌드시 제외한다. -> 해결 O
이때 해줘야할 일
a. 빌드 클린 -> 리빌드
이유추측: 시뮬레이터는 모바일창처럼 나타나지만 사실은 노트북의 CPU로 돌아간다. 그렇기에 새로운 CPU인 M1에서 시뮬레이터를 돌릴 때는 인텔 기반의 cpu에서 빌드되는 arm64로 컴파일할 수 없다. 그래서 오류가 나는게 아닐까?
-> 이후 FirebaseAuth가 없다는 에러가 추가 발생했다.
/Users/dahaekim/Documents/GitHub/cocobaby_2022/cocoBaby_2021/Controller/LoginViewController.swift:11:8: No such module 'FirebaseAuth'
cocoapod문제인데
Are you using Apple M1? I had this issue as well and after some research, I find that it might be something to do with Rosetta. You can refer toRunning CocoaPods on Apple Silicon (M1).
I managed to solve this issue on my MacBook Air M1 by typing this in the terminal:
apple silicon M1을 사용해서 그런 것이다.
아래 커맨드 입력
sudo arch -x86_64 gem install ffi
위 커맨드 입력 후 아래로 다시 pod installd이 필요하다
arch -x86_64 pod install
그제서야 pod install 성공
하지만 여전히 no such module Firebase가 발생했다.
이왕 이렇게 된거 cocoapod 말고 SPM 사용하는 것으로 마음을 바꿈! ㅠ
cocoapod에서 swift package manager로 전환
1) 터미널에서 pod deintegrate 실행
2)xcworkspace 삭제
3)podfile / podfile.lock 삭제
4)SPM 설치
Xcode에서File(파일) > Swift Packages(Swift 패키지) > Add Package Dependency(패키지 종속 항목 추가)…로 이동하여 Firebase 라이브러리를 설치합니다.
표시되는 메시지에서 Firebase GitHub 저장소를 선택합니다.
https://github.com/firebase/firebase-ios-sdk.git
위 문제 모두 해결 심지어 Excluded Architectures에 arm64에 추가하는 것도 제외시킴
결론
Apple Silicon M1 CPU 사용자라면 그냥 cocoapod -> SPM으로 이전하는 것 추천..!!!!!!
An interface to the user’s defaults database, where you store key-value pairs persistently across launches of your app.
유저의 디폴트 데이터베이스 인터페이스이다. 키-벨류 쌍으로 지속적으로 저장하는 앱을 실행시키는 과정에서
Apps store these preferences by assigning values to a set of parameters in a user’s defaults database. The parameters are referred to as defaults because they’re commonly used to determine an app’s default state at startup or the way it acts by default.
런타임시에 객체를 사용해 사용자의 기본데이터베이스에서 앱이 사용하는 기본 값을읽고, 기본값이 필요할 때마다 사용자의 기본 데이터베이스를 열지 않도록 캐시를 함. 저장은 단일장치에 로컬로 저장이되고, 백업 및 복원을 위해 유지가 됨. 사용자의 연결된 장치에서 기본 설정 및 기타 데이터를 동기화 하려면 NSUbiquitousKeyValueStore를 사용하면 됨.
Default Object 저장하기
setValue 사용
userDefaults.standard.setValue(email, forKey: “userEmail”)
let userInfo = UserDefault.standard
userInfo.set(email, forKey: “userEmail”)
value: 저장 값(String, Int, Bool, Date 등 됨)
key: 저장값을 부를 때 사용하는 key(무조건 String 써야함)
Default Object 불러오기
value 사용
key로 부르면 됨
let userInfoEmail = UserDefaults.standard.value(forKey: “userEmail”)
let userInfoEmail = userInfo.value(forKey: “userEmail”) as? String
emailLabel.text = userInfoEmail
로그인 화면에서 사용자가 이메일 자동저장에 체크를 하고 앱을 종료했다면?
다음 앱을 켤 때도 이메일 값은 보이도록 해야함. 이를 위해서 위 코드로 userDefault에 저장한 값을 가져와서 보여줄 수 있다.
Bool 값의 경우 setValue를 통해 false로 바꿔주며 분기처리할 때 사용할 수 있다. ex) 자동로그인의 경우 로그아웃을 하면 없애준다.
회원탈퇴, 로그아웃등의 이유로 데이터를 제거 해야할 때 사용된다.
추가 관련 개념
synchronize()
Default데이터베이스에서 펜딩중인 비동기 업데이트를 기다리고 반환함. => 불필요하며 사용하지 말라고함
didChangeNotification
유저디폴트 값이 변화하면 스레드에 올라옴.키-값 관찰을하여 모든 로컬 디폴트 데이터베이스에 업데이트가 발생하면 노티를 받을수 있습니다.
Default Object 불러올 때 관련 함수
/**
-boolForKey: is equivalent to -objectForKey:, except that it converts the returned value to a BOOL. If the value is an NSNumber, NO will be returned if the value is 0, YES otherwise. If the value is an NSString, values of "YES" or "1" will return YES, and values of "NO", "0", or any other string will return NO. If the value is absent or can't be converted to a BOOL, NO will be returned.
-registerDefaults: adds the registrationDictionary to the last item in every search list. This means that after NSUserDefaults has looked for a value in every other valid location, it will look in registered defaults, making them useful as a "fallback" value. Registered defaults are never stored between runs of an application, and are visible only to the application that registers them.
Default values from Defaults Configuration Files will automatically be registered.
AWS Amplify Sign In/Sign Out API에 Combine 을 제공해줘 사용해보기 위해 Combine 스터디를 시작했다. 그런데 하다보니 비동기 개념을 한번 더 잡고 가야할 것 같아서 결국 비동기 처리 관련해서 내용이 더 많다. Combine은 추후에 더 스터디할 예정이다.
Combine
애플에서 만든 RxSwift와 비슷한 프레임워크로 iOS 13.0+ 부터 사용 가능
Customize handling of asynchronous events by combining event-processing opetators
처리 시간이 긴작업(비동기 이벤트)을 처리함
비동기 이벤트 처리를 위한 것
비동기처리의 사용이유는 시간절약
완료 이벤트 인지 방법
기존의 비동기 프로그래밍 방식은 아래의 예시가 있음
Delegate, Closure callback, GCD, Notification center
비동기는 왜 필요함?
우선 스레드부터 알아야하는데, 스레드는 Task를 받는 일을 함. 그 Task중에는 단순연산, 네트워킹, Print 등의 작업이 예시로 있을수가 있음. 많은 스레드 중에 메인스레드는 UI를 그리는 일을 담당한다. 근데 코드 작성할 때 별도의 처리를 안했다면 메인스레드가 다 모든 처리를 하고 있었을 거라는 사실…! 뚜둥
메인 스레드에 몰린 작업들을 다른 스레드에서도 동시에 작업하도록 하는 것이 동시성 프로그래밍임.
애플에서 이를 위한 API를 만들어 놓았기 때문에 우리는 Queue에만 넣으면 됨. GCD가 우리가 큐에 보낸 작업을 스레드에 적절히 분배해준다는 것임. 이 GCD의 이름이 Dispatch Queue인 것임. 그래서 우리가 Dispatch Queue에 작업을 추가하면 GCD가 작업에 맞도록 Thread를 자동으로 생성해서 실행하고 Task가 끝나면 스레드에서 제거를 한다.
GCD?
디스패치큐: iOS 동시성 프로그래밍을 돕기 위헤 제공하는 queue
Global: 디스패치큐의 종류
Async: 비동기
DispatchQueue.global().async {
TASK 작업의 한 단위
// 클로저 내의 { } 하나의 작업 단위이기 때문에 그 안의 동작들은 순차적으로 처리될 수 있음
// 하지만 이 블럭자체는 비동기로 task를 큐에 보내는 단위임
}
Operation?
Operation에서 사용하는 큐의 이름은 Opération Queue. 얘도 사실는 내부적으로 GCD위에서 동작함
동시에 실행할 수 있는 동작의 최대 수 지정
동작 일시 중지 및 취소
기능은 좀 더 많지만디스패치큐 보다 구현이 조금 더 복잡.
어쨋든 정리를 하면 GCD나 Operation이 Task를 받아 스레드에 분배하는 것임
그렇다면 GCD나Operation이 작업을 받으면 스레드에 작업을 나눠줄때 하나의 스레드에 몰아줄 수가 있고, 여러 개의 스레드에 나눠줄 수가 있다. 이 작업을 Serial 직렬 큐인지, Concurrent 큐인지 명시해서 결정할 수 있다.
시리얼 Serial 큐 -> 한 개의 스레드에 몰아 넣기, 메인 스레드에서 분산처리 시킨 작업을 다른 한개의~~~스레드에서 처리하는 큐
동시 큐 Concurrent -> 메인스레드에서 분산처리 시킨 작업을 다른 여러개의 스레드에서 처리하는 큐. 몇 개의 스레드로 분산시킬지는 시스템이 알아서 결정함
그럼 언제 어느시점에 얘네들을 써야함?
Serial은 모든 작업들이 그 전 작업이 끝나길 기다렸다가 하나씩 실행되기 때문에 task의 시작과 종류에 대한 순서 예측이 가능함
Concurrent는 큐에 담긴 작업들을 여러개의 스레드로 분배함. 큐의 특성상 FIFO이지만 끝나는 순서는 알 수 없음. 제일 늦게들어와도 실행시간이 가장 짧으면 가장 먼저 끝날 수 있는 것임. 예를들면 cell에서 이미지를 로드할 때 어떤 데이터가 먼저 들어오는지보다 빠르면 좋기 때문에 concurrent 큐를 사용하고, 순서가 중요한 작업들을 처리해야하면 늦게 들어온것이 먼저 끝나는 것을 방지하기위해 Serial Queue를 사용해야함!
Sync/Async는 작업을 보내는 시점에 기다릴지 말지를 정함
Concurrent/Serial은 큐에 보내진 작업들을 여러개의 스레드로 할지 한개의 스레드로 할지 정하는 것. Concurrent로 하면 작업들이 순서에 상관없이 실행됨.
다이어리 앱 개발 중 테이블뷰에 데이터를 뿌려줄 때, 가장 최근 날짜 순으로 데이터를 가져오기 위해 정렬이 필요하다. 파이어스토어에 데이터를 생성할 때 TimeStamp순으로 순차적으로 쌓이나?싶었지만 그런거 없이 무작위로 데이터가 생성되었다. 그래서 데이터를 get해올 때 내가 만든 데이터에 timeStamp를 찍어주고 그 기준으로 정렬하라는 쿼리를 같이 써줘야한다.
공식문서에 보면 아래와 같이 샘플코드가 있다. 이때 by에 들어가는 string은 해당 string에 부합하는 특정 필드가 있는지도 필터링해준다. 아래로 예를 들면 state와, population필드가 존재하는 데이터여야하며, 존재하지 않은 데이터는 제외된다. descending에 true를 해줬기에 population을 기준으로 내림차순으로 정렬해준다.
개인 프로젝트 중에 SceneDelegate에서 API 통신 후에 값을 계산하고 계산된 값을 MainTabBarController -> MainViewController 순으로 넘겨주는 코드가 있다. 그런데 API 통신 및 계산이 완료되지 않은 시점에 MainTabBarController, MainViewController가 호출이 되어서 넘겨준 값을 viewDidLoad에서 가져올 수가 없게 되었다. 이때 필요한게 비동기 처리다.
결과적으로 SceneDelegate에서 API 호출 및 리턴값으로 계산한 함수 실행이 끝난 시점에 MainTabBarViewController와 MainViewController를 부를 수 있도록 비동기처리를 해줘야 하는 것이다.
동기
메인스레드에서 쭉 실행이 되는데,
중간에 API를 받아올 때 딜레이가 걸리기 때문에 그 뒤 처리를 아무것도 못하는 문제가 생긴다.
비동기
야, 메인스레드 너는 너 갈길 가라
나는 API호출 내 갈길 간다
메인스레드에서 쭉 실행되며 갈 때, 중간에 API가 호출되고 응답이 올 때,
메인스레드는 API 통신과 관계 없이 계속 진행이 되고있고,
API 호출은 완료되는 시점에 따로 처리해주는 것
요즘 앱은 대부분 서버와 통신을 하기 때문에 비동기 기술이 중요하다.
코드 예제
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad 호출")
getUser{ name in
// getUser가 완료되어서 doneCallUser하면,
// 넘긴 파라미터 name 데이터를 받아올 수 있고
// 이 코드가 실행됨
print("\(name) 받아옴")
}
}
func getUser(doneCallUser: @escaping(String) -> ()) {
let userName: String
// API호출 코드
userName = API 호출 완료후 받은 데이터
prnt("API 호출 완료함")
doneCallUser(userName)
}
=> Diary Struct에 documentID값 필드를 추가해서 문서를 생성할 때 documentID값을 필드에 추가해준다. 추가시(addDocuemt)에 자동으로 생선된 documentID값이 있는데 성공적으로 추가 됐을 경우 필드에 해당값을 추가하면 된다.
이를 데이터 조회(get)해올 때 받아올때 같이 받아와서 배열에 넣어주면 didSelectRow에서 선택한 indexPath.row를 통해 배열의 한 element의 필드값으로 이전에 생성하며 미리 저장해두고, get해온 documentID값을 불러와서 수정하면된다. 소스코드를 보며 이해할 수 있도록 프로젝트 하며 작성한 코드를 공유해본다.
먼저, 1) 데이터 받아오는 모델 구조에 documentID필드가 필요하니 추가하고
struct Diary {
var content: String
var timeStamp: Date
var emoji: String
var documentId: String
// Key(Strin(g)-Value(Any)
var dictionary: [String:Any] {
return [
"content": content,
"timeStamp": timeStamp,
"emoji": emoji,
"documentId": documentId
]
}
}
가져오기 위해선 문서가 먼저 생성되어있어야 하기 때문에 새로운 다이어리 글 작성할 때 2) addDocument함수로 먼저 난수로 documentID값이 생성되어있어야 한다. (새로운 다이어리를 작성하고 Save버튼을 눌렀을 때 실행되는 함수)
self.ref = self.db
.collection("Diaries").document(self.userUid!)
.collection("Diary").addDocument(data: newDiary.dictionary) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(self.ref!.documentID)")
self.db.collection("Diaries").document(self.userUid!)
.collection("Diary").document(self.ref!.documentID).updateData([
"documentId": self.ref!.documentID
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document ID successfully added!")
}
}
}
필드값에 실제 난수로 생성된 documentId가 성공적으로 추가되는 것을 알 수 있다.
4) 이를 get데이터해올때 가져온다. document객체에서 아이디값을 물고있기 때문에 쉽게 가져올 수 있다.
func loadDiaryData() {
db.collection("Diaries").document(userUid!)
.collection("Diary").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
if let stamp = document.data()["timeStamp"] as? Timestamp {
self.diaryArray.append(Diary(content: document.data()["content"] as! String, timeStamp: stamp.dateValue() as! Date, emoji: document.data()["emoji"] as! String, documentId: document.data()["documentId"] as! String))
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
5) 이렇게 불러와진 데이터를 diaryArray에 넣고, didSelectRow했을 때 해당행의 documentID값을 통해 문서에 접근하여 데이터를 변경하거나 삭제할 수 있다.
그리고 주의점 setData를 할 경우에 아예 새로 써지기 때문에 기존에 추가된 필드도 사라진다.
6) updateData함수를 이용해서 전체 덮어쓰기가 아닌 일부필드만 업데이트 하도록 한다.
self.db.collection("Diaries").document(self.userUid!)
.collection("Diary").document(diary.documentId).updateData([
"content": note
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully modified!")
}
}
수정 전 데이터
수정 후 데이터
데이터가 정상적으로 업데이트 되는 것을 확인할 수 있다.
핵심은 다이어리 구조체에 documentId를 추가하고, 문서가 성공적으로 생성됐을 때 파이어스토어의 documentId 값으로 문서를 타고들어가 구조체 필드에 성공한 ref!.documentID 값을 업데이트 시키는 것이다. 그리고 viewDidLoad()에서 이렇게 만들어진 데이터들을 배열에 가져와 담고, indexPath.Row를 통해 선택한 배열의 indexPath.Row번째 값의 documentID값을 가져와서 이용하면된다.
선택한 배열의 요소, 즉 테이블뷰 셀의 documentID값을 알아야 수정된 문서 데이터를 다시 update쳐서 반영할 수 있기 때문에 documentID값을 아는 것은 중요하고, 스택오버 플로우에도 이에 대한 질문이 많았다.