관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. Observable로 Wrapping하여 권한 요청) 사진 권한, 카메라 권한  본문

iOS 응용 (swift)

[iOS - swift] 2. Observable로 Wrapping하여 권한 요청) 사진 권한, 카메라 권한 

jake-kim 2022. 1. 5. 23:25

1. Observable로 Wrapping하여 권한 요청) 위치 권한, 실시간 위치 정보 획득

2. Observable로 Wrapping하여 권한 요청) 사진 권한, 카메라 권한

3. Observable로 Wrapping하여 권한 요청) 마이크 권한, ATT(App Tracking Transparency) 권한

4. Observable로 Wrapping하여 권한 요청) RxSwift의 concat을 이용하여 순서대로 권한 요청 방법

Observable로 wrapping 작업 핵심

  • 기존에 Observable 형태를 리턴해주는 작업이면, RxSwift의 생성자 연산자 중에 deferred 연산자 사용하여 wrapping
  • 기존에 Observable 형태가 아니고 클로저 형태로 값을 받는 경우, create 연산자 사용

필요한 framework 준비

  • framework
pod 'RxSwift'
pod 'RxCocoa'
  • 사진 시스템 접근: Privacy - Photo Library Usage Description

observable로 Wrapping

  • Observable로 Wrapping 방법
    • 기존에 Observable 형태를 리턴해주는 작업이면, RxSwift의 생성자 연산자 중에 deferred 연산자 사용하여 wrapping
    • 기존에 Observable 형태가 아니고 클로저 형태로 값을 받는 경우, create 연산자 사용

ex) deferred 연산자 사용하여 위치 권한 wrapping

func requestLocation() -> Observable<CLAuthorizationStatus> {
  return Observable<CLAuthorizationStatus>
    .deferred { [weak self] in
      guard let ss = self else { return .empty() }
      ss.locationManager.requestWhenInUseAuthorization()
      return ss.locationManager.rx.didChangeAuthorization
        .map { $1 }
        .filter { $0 != .notDetermined }
        .do(onNext: { _ in ss.locationManager.startUpdatingLocation() })
        .take(1)
    }
}​
  • Photo 권한 요청은 기존에 Observable 형태를 리턴해주는 모듈이 없으므로 create연산자를 사용
    • async 작업이 끝나고 completion 클로저에서 observable.onNext()하여 방출하고
    • create 연산자 사용 시 주의사항은 Disposables.create()를 completion 클로저 블록 안에서 사용하지 않는 점
import RxSwift
import RxCocoa
import Photos

class PhotoPermissionManager {
  static let shared = PhotoPermissionManager()
  private init() {}
  
  func requestPhoto() -> Observable<PHAuthorizationStatus> {
    Observable<PHAuthorizationStatus>.create { observable in
      PHPhotoLibrary.requestAuthorization { auth in
        DispatchQueue.main.async {
          observable.onNext(auth)
          observable.onCompleted()
        }
      }
      return Disposables.create()
    }
  }
}
  • 사용하는 쪽
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
  
  private let disposeBag = DisposeBag()

  @IBAction func didTapPhotoPermissionButton(_ sender: Any) {
    PhotoPermissionManager.shared.requestPhoto()
      .bind { print($0) }
      .disposed(by: self.disposeBag)
  }
}

카메라 권한 요청

  • info.plist에 Privacy - Camera Usage Description 추가

  • create 연산자를 이용하여 wrapping
import RxSwift
import RxCocoa
import AVFoundation

class CameraPermissionManager {
  static let shared = CameraPermissionManager()
  
  func requestCamera(mediaType: AVMediaType = .video) -> Observable<Bool> {
    Observable<Bool>.create { observable in
      AVCaptureDevice.requestAccess(for: mediaType) { isGranted in
        DispatchQueue.main.async {
          observable.onNext(isGranted)
          observable.onCompleted()
        }
      }
      return Disposables.create()
    }
  }
}

* 전체 코드: https://github.com/JK0369/ExPhotoPermission

7 Comments
  • 프로필사진 ggasoon2 2022.08.18 13:27 신고 좋은 글 감사합니다 선생님.
    질문 하나만 하겠습니다.
    Rx로 image picker 후 선택한 이미지를 bind 시켜주려고합니다.
    그래서 공식문서 Rxswift의 imagePicker를 참고하여, 선생님의 권한 확인 뒤에 붙여 작성하였습니다.

    galleryButton.rx.tap
    .flatMap{PhotoPermissionManager.shared.requestPhoto()}
    .filter({ status in
    return status.rawValue == 3
    })
    .flatMapLatest { [weak self] _ in
    return UIImagePickerController.rx.createWithParent(self) { picker in
    picker.sourceType = .photoLibrary
    picker.allowsEditing = false
    }
    .flatMap {
    $0.rx.didFinishPickingMediaWithInfo
    }
    .take(1)
    }
    .map { info in
    return info[.originalImage] as? UIImage
    }
    .bind(to: imageView.rx.image)
    .disposed(by: disposeBag)

    이런식으로 구성하였는데
    Main Thread Checker: UI API called on a background thread: -[UIImagePickerController init]
    PID: 74369, TID: 1114158, Thread name: (none), Queue name: com.apple.photos.accessCallbacks, QoS: 0
    이런 에러가 뜹니다..
    혹시 원인을 알 수 있을까요?
    rx image picker는 https://github.com/ReactiveX/RxSwift/tree/main/RxExample/RxExample/Examples/ImagePicker 이부분 입니다.
    혹시 아신다면 알려주시면 감사드리겠습니다 ㅠ
  • 프로필사진 jake-kim 2022.08.18 13:47 신고 안녕하세요,

    UI관련 작업은 MainThread에서 동작해야하지만,
    지금은 background thread에서 동작하는거 같아서 에러가 뜨는거 같네요.
    observe(on: MainScheduler.asyncInstance)를 추가해서 해보시면 해결 될거같네요 :)

    galleryButton.rx.tap .flatMap{PhotoPermissionManager.shared.requestPhoto()}
    .observe(on: MainScheduler.asyncInstance) // <- 추가
    .flatMapLatest { [weak self] _ in
    return UIImagePickerController.rx.createWithParent(self) { picker in
    picker.sourceType = .photoLibrary
    picker.allowsEditing = false
    }
    .flatMap {
    $0.rx.didFinishPickingMediaWithInfo
    }
    .take(1)
    }
    .map { info in
    return info[.originalImage] as? UIImage
    }
    .bind(to: imageView.rx.image)
    .disposed(by: disposeBag)
  • 프로필사진 ggasoon2 2022.08.18 14:11 신고 감사합니다 선생님 ㅠㅠ 잘동작합니다.. 정말 감사합니다.

    너무 잘 알려주셨는데 이해가 가지 않는부분이 있습니다.

    메인 스레드에서 돌리는 코드는 .observe(on: MainScheduler.instance) 이거라고 생각했는데
    .observe(on: MainScheduler.asyncInstance) 이걸로 작성하신것과

    코드순서를 이렇게 작성하게되면
    .observe(on: MainScheduler.asyncInstance)
    .flatMap{PhotoPermissionManager.shared.requestPhoto()}
    동일하게 에러가 나는데, 이유가 궁금합니다.

    혹시 알려주신다면 감사드리겠습니다..ㅠ
  • 프로필사진 jake-kim 2022.08.18 19:47 신고 오류가 나는 곳은, 코드 순서를 바꾼 부분의 전체 코드를 보여주시면 확인해보겠습니다.

    ----

    .observe(on: MainScheduler.instance)와
    .observe(on: MainScheduler.asyncInstance)
    이것의 차이는 sync와 async입니다.

    sync는 작업의 우선순위를 높여서 지금 메인 스레드에서 처리하는 작업을 멈추고 이 작업을 먼저 하는 것이고,
    async는 작업의 우선순위는 낮지만, 지금 메인 스레드에서 처리하는 작업을 멈추지 않고 처리하는 것으로 이해하시면 될거같아요.
    (sync, async 에 대한 개념은 이 포스팅 글에 잘 설명되어 있습니다. https://ios-development.tistory.com/1082)

    기능상은 큰 차이가 없어서, 아무거나 쓰셔도 좋을거같아요.
  • 프로필사진 ggasoon2 2022.08.18 21:50 신고 동시성프로그래밍 관련하여 알려주셔서 감사드립니다.

    오류가 나는곳은,

    observe(on: MainScheduler.asyncInstance)이 부분이

    galleryButton.rx.tap
    .observe(on: MainScheduler.asyncInstance) // <- 여기에 추가될경우 동일한 에러가 발생하더라구요..
    .flatMap{PhotoPermissionManager.shared.requestPhoto()}
    .flatMapLatest { [weak self] _ in
    return UIImagePickerController.rx.createWithParent(self) { picker in
    picker.sourceType = .photoLibrary
    picker.allowsEditing = false
    }
    .flatMap {
    $0.rx.didFinishPickingMediaWithInfo
    }
    .take(1)
    }
    .map { info in
    return info[.originalImage] as? UIImage
    }
    .bind(to: imageView.rx.image)
    .disposed(by: disposeBag)

    PhotoPermissionManager.shared.requestPhoto() 다음에 들어갈때 어떤 차이가 있는지 잘 모르겠습니다.
    알려주시면 정말 감사드리겠습니다..!!
  • 프로필사진 jake-kim 2022.08.18 22:37 신고 코드를 직접 확인해보니, 어디가 문제인지 알겠네요.

    우선 알려드린것중에 정정할부분이 있어요.
    observe(on: MainScheduler.asyncInstance)을 추가한다고해서 flatMap{} 클로저 내부가 main thread에서 동작하지 않아요. observe(on:)은 onNext, onCompleted, onError로 이벤트를 받는 곳에서만 적용되어서요.

    이때 위 문제점으로 돌아가서, 아래처럼 했을때는 잘 동작했지만,
    galleryButton.rx.tap .flatMap{PhotoPermissionManager.shared.requestPhoto()}
    .observe(on: MainScheduler.asyncInstance) // <- 추가

    아래처럼하면 런타임 에러가 나고있죠?
    .observe(on: MainScheduler.asyncInstance)
    .flatMap{PhotoPermissionManager.shared.requestPhoto()}

    원래는 둘 다 런타임에러가 나야 정상입니다. (flatmap 내부는 지금 메인스레드가 아니므로)
    우선 에러가 나는 원리는, RxSwift에서 DEBUG모드일때만 메인 스레드를 사용해야하는 곳에서 메인스레드를 사용하지 않으면 내부적으로 런타임에러를 발생시키고 있어요.
    위에서 둘 다 런타임에러가 발생해야했지만 하나만 발생한 이유는 RxSwift내부적으로 MainThread 스케줄링이 변경되면서 나머지 한 곳을 체크하지 못했기 때문이에요.

    그걸 알 수 있는 이유는 지금 아래 코드에서 Thread.current를 출력해보시면 항상 main스레드에서 동작하고 있지 않는걸 볼 수 있어요.
    PHPhotoLibrary.requestAuthorization {
    observable.onNext($0)
    observable.onCompleted()
    }

    결론은 기존 코드를 변경하면 observeOn없이도 해결할수있을 겁니다.
    class PhotoPermissionManager {
    func requestPhoto() -> Observable<PHAuthorizationStatus> {
    Observable<PHAuthorizationStatus>.create { observable in
    PHPhotoLibrary.requestAuthorization { auth in
    DispatchQueue.main.async { // <- 메인 스레드 추가
    observable.onNext(auth)
    observable.onCompleted()
    }
    }
    return Disposables.create()
    }
    }
    }

    꼼꼼하게 읽어주셔서 감사합니다:) 이해하기 쉽게 글에 있는 코드와 깃에도 업데이트 해놓았어요.
  • 프로필사진 ggasoon2 2022.08.19 10:56 신고 친절하게 설명해주셔서 정말 감사드립니다
댓글쓰기 폼