애플에서 발급하는 개발자 라이센스 종류와 차이점

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를 바꿔야하고, 이미 이전에 제출한 앱을 업데이트 한것이라면 제출한 다음에 크롬 변경하고 앱 정보에서 다시 변경해야한다.

Core Bluetooth Background Processing for iOS Apps

 

앱은 포그라운드와 백그라운드에서 다르게 동작되어야 한다. 왜냐하면 iOS 디바이스에서는 시스템 리소스의 제한이 있기 때문이다. 일반적인 블루투스의 작동방식은 앱이 백그라운드/Suspended 상태로 갔을 때 중앙장치나 주변장치 모두 disabled처리 되는 것이다. 

 

그 말은 즉 앱에 코어 블루투스 백그라운드 실행 모드를 살려서 앱을 깨우거나 할 수 있다는 것이다. 백그라운드 프로세싱 전부를 지원하지는 못하더라도 시스템이 여전히 어떤 중요한 이벤트가 일어났다고는 알려줄 수 있다.

 

만약 앱이 코어 블루투스 백그라운드 실행 모드를 지원한다면, 이 앱은 계속 실행시킬 수 없다. 어떤 지점에서 시스템은 앱을 종료해서 메모리 해제를 해줘서 현재 포그라운드에서 작동되는 앱을 실행가능하도록 해야하기 때문이다. 그래서 펜딩이라던지 활성화된 연결이 없어질수가 있다. iOS7 부터 코어 블루투스는 중앙장치와 주변장치의 상태를 저장하는 것을 지원한다. 그리고 앱이 실행되면 이 상태를 다시 저장한다. 이 기능을 활용해서 블루투스 디바이스를 활용한 롱텀 실행이 가능할 것이다.

 

Foreground-Only Apps

대부분의 iOS 앱들은 백그라운드 스테이트를 가면 suspended state로 변경된다. 특정 태스크를 백그라운드에 돌리겠다고 권한을 받지 않을 경우에 말이다. 서스펜디드 상태에서는 다시 포그라운드로 돌아오기 전까지는 블루투스 관련된 태스크를 진행할 수 없다. 혹은 어떤 블루투스 관련된 이벤트를 진행할 수 없다. 

 

중앙장치 사이드에서는 포그라운드 앱(즉, 백그라운드 실행모드를 선언하지 않은 앱)에서는 백그라운드나 서스펜드 상태에서 scan 처리또한 할 수 없다. 주변장치에서는 advertising 자체가 작동불가하며  그 어떤 중앙장치가 앱의 퍼블리쉬된 서비스의 characterisitc 값에 접근하려고 하면 error를 받는다. 

 

사용자 케이스에 따라 이러한 일반적 행동은 앱의 여러방향으로 영향을 줄 수 있다. 예를 들면 사용자가 현재 연결된 주변장치와 인터렉팅 하고싶을 때. 또는, 앱이 서스펜디드 스테이트로 갔을 때(유저가 다른 앱으로 전환해서). 만약 앱이 서스펜드 상태에서 주변장치와의 연결이 끊어진다해도 다시 앱을 켜는 순간까지 장치와의 연결이 끊어졌다는 사실을 알 수 다.

 

Take Advantage of Peripheral Connection Options

포그라운드에서만 작동되는 앱에서는 서스펜디드 스테이트에 갔을 때 블루투스 관련 이벤트들은 시스템에 의해 queue 되어지고, 앱이 다시 포그라운드 작업으로 갔을 때 실행이 된다. 코어블루투스는 유저에게 이벤트를 알릴 수 있는 방법을 제공하는데, 이를 이용해서 포그라운드로 이벤트들을 가져올건지 정하면 된다.

 

아래 CBCentralManager 클래스에 있는 주변장치 연결 관련 메소드들을 이용하면 된다. 

앱은 서스펜드 스테이트 상태인데, 주어진 주변장치와의 연결이 성공적으로 되었을 때 알림창을 준다.

 

앱이 서스펜드 스테이트 상태인데, 주변장치와의 연결이 끊어졌을 때 연결해제되었다는 알림창을 준다.

 

앱이 서스펜드 스테이트 상태인데 주변장치로부터 받은 모든 노티피케이션을 알림창으로 주고싶을 때이다.

 

 

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에 장치 특성에 따라 값을 저장해주면 이제 앱에서 블루투스관련된 태스크를 백그라운드로 도릴 수 있다. 백그라운드에 있더라도 주변장치를 스캔하거나 데이터를 가지고 상호작용할 수 있다. 그리고 시스템이 앱을 꺠운다. CBCentralManagerDelegate or CBPeripheralDelegate delegate 관련 메소드들이 발동될 때마다! 이를 통해서 중앙장치의 중요한 이벤트들이 핸들링 가능하다. 센트럴매니저의 상태값이 변경된다던지, characterisic value가 변동된다던지 하는 것들을 모두 포착할 수 있다. 

 

주의 사항으로는 백그라운드에서 스캔을 진행할 시에 스캔 옵션키로 정해놓은 디바이스만 찾아지는게 아니라 모든 디바이스가 다 찾아진다는 점이다. 또한, 중앙장치가 광고 패킷을 스캔하는 인터벌이 길어진다. 그래서 주변장치를 발견하는 시간이 길어진다. 

 

 

 

Use Background Execution Modes Wisely

백그라운드 실행모드를 현명하게 쓰는 것이 중요하다. 디바이스의 배터리 수명을 위해서 백그라운드에서 하는 작업은 최소화 하자. 그리고 아래와 같은 3가지 가이드라인을 따르자.

 

1. 앱은 세션 베이스여야 한다. 유저가 블루투스 이벤트를 언제 시작하고 끝마칠지에 대한 인터페이스를 제공해야한다.

2. 앱이 백그라운드에서 깨어있는 동안 10초 이내로 태스크가 완수되어야 한다. 다시 서스펜드 모드로 갈 수 있도록 가장 빠르게 테스크를 완수 해야한다. 백그라운드에서 너무 길게 실행되는 테스크가 있다면 앱이 kill될 것이다.

3. 앱은 원래 목적을 떠나 상관없는 과한 태스크를 진행하지 않도록 해야한다. 

 

 

Core Bluetooth Background Processing for iOS Apps

Core Bluetooth Background Processing for iOS Apps For iOS apps, it is crucial to know whether your app is running in the foreground or the background. An app must behave differently in the background than in the foreground, because system resources are mor

developer.apple.com

 

앱의 생명주기 참고

  • 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

 

https://github.com/firebase/firebase-ios-sdk/issues/6520

 

ARM64 Simulator support · Issue #6520 · firebase/firebase-ios-sdk

Step 0: Are you in the right place? For issues or feature requests related to the code in this repository file a Github issue. If this is a feature request please use the Feature Request template. ...

github.com

 

해결 방법

-------> 아래에 길게 과정이 있지만 결론을 보면 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 to Running 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 설치
  1. Xcode에서 File(파일) > Swift Packages(Swift 패키지) > Add Package Dependency(패키지 종속 항목 추가)…로 이동하여 Firebase 라이브러리를 설치합니다.
  2. 표시되는 메시지에서 Firebase GitHub 저장소를 선택합니다.
     
  3. https://github.com/firebase/firebase-ios-sdk.git

 

위 문제 모두 해결 심지어  Excluded Architectures에 arm64에 추가하는 것도 제외시킴

 

 
 
결론
 
Apple Silicon M1 CPU 사용자라면 그냥 cocoapod -> SPM으로 이전하는 것 추천..!!!!!!
 
 
문제 해결!!!!
 
 

CPU가 달라지니까 빌드할 때 여기저기 문제가 많이 생긴다. 킹받는다!

 

https://stackoverflow.com/questions/64901180/how-to-run-cocoapods-on-apple-silicon-m1

 

How to run CocoaPods on Apple Silicon (M1)

I have a Flutter project that I'm trying to run on iOS. It runs normally on my Intel-based Mac, but on my new Apple Silicon-based M1 Mac it fails to install pods. LoadError - dlsym(0x7f8926035eb0,

stackoverflow.com

 

애플의 정책 변경으로 인해 회원탈퇴 기능이 앱에 반드시 추가되어야 합니다. 원래 변경 기한은 올해 2022년 1월30일 까지였는데 연말로 연장되었다. 회원 탈퇴시 데이터베이스에서 정보를 지우기도 해야하지만 UserDefault에서도 데이터를 지워줘야한다.

 

UserDefault - get, set, remove

    

UserDefault란?

 

자신의 디바이스에 임시로 데이터값을 저장해놓은 공간

목적: 앱이 종료되어도 지정된 값으로 저장되어 있기 위해. 따로 삭제를 해야 초기화가 된다. 

예제: key-value pair로 값을 저장(set)하고, 필요할 때 get(key)를 하면 value를 사용할 수 있다. 

 

https://developer.apple.com/documentation/foundation/userdefaults

 

Apple Developer Documentation

 

developer.apple.com

 

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에 저장한 값을 가져와서 보여줄 수 있다. 

 

 

Default Object 삭제하기

 

  • removeObject 사용
UserDfaults.standard.removeObject(forKey: “userEmail”)

 

key값으로 삭제할 수 있다. 

 

**

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.

     

     */

 open func bool(forKey defaultName: String) -> Bool

 

사용예시

 

hasEmail = UserDefaults.standard.bool(forKey: “userEmailCheck)

 

 

**

     -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.

     */

    open func register(defaults registrationDictionary: [String : Any])

 

UIApplicationDelegate에서 사용하여 앱실행 가장 첫 시작점에서 코드

 

사용예시 

 

let dic = ["email_ud":"","pin_ud":"", "isPin":false, "isMember":false, "pw":""] as [String : Any]

    UserDefaults.standard.register(defaults: dic) //저장

 

    

삭제 여부 확인

 

마지막으로 삭제후에는 아래와 같이 데이터가 삭제됐는지 확인해준다.

print(UserDefaults.standard.string(forKey: USER_EMAIL))
print(UserDefaults.standard.bool(forKey: USER_PERMISSION_CHECK))

nil

false

가 나타나는데, bool 타입은 데이터를 삭제하면 false가 된다.

 

 

 

출처: 

https://developer.apple.com/documentation/foundation/userdefaults#1664798

https://qussk.github.io/2021/02/27/swift-UserDefault

 

 

 

 

 

 

 

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을 기준으로 내림차순으로 정렬해준다.

citiesRef
    .order(by: "state")
    .order(by: "population", descending: true)

 

직접 프로젝트에 구현한 코드는 아래와 같다.

컬렉션 => 도큐먼트=>컬렉션을 타고 들어가 나오는 모든 문서들 중에서 timeStamp필드가 존재하고, 이를 기준으로 내림차순으로 정렬해서 diaryArray에 담아주도록 구현했다.

 

        db.collection("Diaries")
            .document(userUid!)
            .collection("Diary")
            .order(by: "timeStamp", descending: true)
            .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()
                    }
                }
            }
    }

 

정렬전은 아래와 같이 다이어리가 날짜에 상관없이 무작위로 로드되는 걸 알 수있다.

정렬 전 데이터

 

 

위 코드 구현 후 날짜를 기준으로 내림차순 정렬이 되어 가장 최신으로 작성한 다이어리가 위쪽에 보이도록 변경되었다.

정렬 후 데이터

 

 

 

 

Cloud Firestore로 데이터 정렬 및 제한  |  Firebase Documentation

의견 보내기 Cloud Firestore로 데이터 정렬 및 제한 Cloud Firestore는 컬렉션에서 검색할 문서를 지정하는 강력한 쿼리 기능을 제공합니다. 데이터 가져오기에 설명된 대로 이러한 쿼리를 get() 또는 addS

firebase.google.com

 

위젯의 사이즈는 small, medium, large가 있습니다. extraLarge의 경우 아이패드에서만 되는 것으로 알고 있어요.

이 중에서 저는 small 사이즈만 2 타입의 디자인으로 만들고 싶어 아래와 같이 구현해보았습니다.

 

우선은 PrevieProvider 프로토콜을 따르는 struct에서 아래와 같이 코드를 구현해주면 됩니다. family에서 한 사이즈만 지정해주면 됩니다. 

struct cocoBabyWidget_Previews: PreviewProvider {
    static var previews: some View {
        cocoBabyWidgetEntryView(entry: SimpleEntry(date: Date(), babyNickName: "", dDay: "", dWeek: "", configuration: ConfigurationIntent()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

  .previewContext(WidgetPreviewContext(family: .systemSmall)) 

 

그리고 Widget 프로토콜을 따르는 struct에서 아래 소스코드를 추가해줍니다.

struct cocoBabyWidget: Widget {
    let kind: String = "cocoBabyWidget"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            cocoBabyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall])
    }
}

     .supportedFamilies([.systemSmall])

 

 

디폴트로 여러개 사이즈를 가진 위젯(제일 작은 크기 이외에는 unknwon"이라는 글자만 뜨게 했습니다.

디폴트로는 이렇게 3개의 사이즈가 나오는데요.

 

 

 

위와 같이 family에서 사이즈를 지정하면 원하는 사이즈만 위젯 추가시에 나타나는 것을 볼 수 있어요.

 

다음은 한가지 사이즈로 다양한 디자인, 혹은 포맷으로 구현하고 싶을 때가 있는데요. 이때 아래와 같이 구현하면 됩니다.

기본적으로 위젯을 2개 구현하고, 이 2개의 위젯을 메인에서 불러오는 개념인데요.

WidgetBundle 프로토콜을 통해서 구조체를 구현하고, Widget을 반환하는 인스턴스를 만들어 주면 됩니다.

 

첫번째 위젯, 두번째 위젯 둘다      .supportedFamilies([.systemSmall]) 로만 구현한 후에 WidgetBundle 을 이용하고, @main을 통해 가장 먼저 해당 구조체가 호출될 수 있도록 구현해줍니다.

@main
struct cocoBabyWidgetBundle: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        cocoBabyWidget()
        cocoBabySecondWidget()
    }
}

 

2가지 디자인 타입의 같은 사이즈의 위젯이 구현된 것을 볼 수 있습니다.

 

최종 버전은 아래와 같습니다! ㅎㅎ

얼른 마무리하고 업뎃버전 출시하고 싶네요.

 

개인 프로젝트 중에 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)
}

 

결과

viewDidLoad 호출 // viewDidLoad가 가장먼저 호출되지만,
API 호출 완료함 // getUser가 함수가 실행 완료되면,
다동 받아옴 // completion block 내부가 실행됨

위젯 개발 중 CustomFont를 사용할 일이 생겼다.

커스텀폰트를 사용하기 위해서는 아래와 같은 절차가 필요하다. 

 

1. 폰트를 Xcode프로젝트 폴더에 추가하고 Add Target을 해야한다. (기존 프로젝트에 추가해서 타겟만 위젯프로젝트 것 추가해도 됨)

 

 

본 프로젝트에 추가된 폰트를 위젯 프로젝트에도 사용하였다.
폰트 선택하고 우측에서 타겟만 WidgetExtension에 추가로 체크해주었다.

 

 

 

 

2. info.plist에 폰트를 추가해야한다.

 

=> 이때 주의할 점은 SwiftUI로 개발한 Widget프로젝트에도 info.plist가 따로 있으니 여기도 따로 추가 해줘야 한다는 것이다.

 

원래 프로젝트와 위젯의 info.plist가 각각 있는 것을 알 수 있다.

 

3. 소스코드

 Text("동동이")
                .font(Font.custom("S-CoreDream-5Medium", size: 40))

 

* 주의 *

위와 같이 구성해도 안될 경우 실제로 Font-Family와 Font명을 엑스코드에서 어떻게 인식하는지 아래 코드로 알아볼 수 있다.

 

  for family: String in UIFont.familyNames {
                        print(family)
                        for names : String in UIFont.fontNames(forFamilyName: family){
                            print("=== \(names)")
                        }
                    }

위 소스코드로 엑스코드에서 상요할 수 있는 폰트 명 모두를 알 수 있다.

 

콘솔에 찍힌 폰트명

콘솔에 찍힌 폰트명으로 입력해주면 된다.

 

결과화면

커스텀 폰트가 아래와 같이 반영된 것을 확인할 수 있다. ex)동동이

테이블뷰에서 선택한 Row의 파이어스토어 문서 documentID값 아는법

 

=> 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)

 

 

3) 생성이 성공하면 받아온 id값으로 필드값에 추가해준다. (새로운 다이어리를 작성하고 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값을 아는 것은 중요하고, 스택오버 플로우에도 이에 대한 질문이 많았다.

 

+ Recent posts