권한 요청, 설정 이동을 추상화(protocol)를 이용하여 재사용 가능하게 사용하기

권한을 요구하는 작업을 수행할 때, 사용자에게 권한을 요청하는 알람을 뛰운다. 또, 사용자가 권한을 거절해 버리면, 설정으로 가서 권한을 부여하도록 Alert 창을 띄운다. 또한 HIG에서는 권한 요청이 필요한 타이밍에 권한을 요청하라고 하고 있다. 즉 시작하자마자 필요한 권한을 모조리 요구하지 말라는 말.

 

따라서 여러 화면마다 권한 요청을 해야한다면 많은 중복코드가 생길 것이다. 그래서 protocol을 이용하여 권한 요청/각 권한에 따른 설정 이동 Alert를 표시하는 기능을 채택하면 사용할 수 있게 구현해 보았다.

 

Alert를 화면에 띄우는 작업을 protocol의 extension을 이용해 구현했다.

protocol Alertable {}

extension Alertable where Self: UIViewController {
    func showAlert(title: String = "", message: String, defaultActionTitle: String, cancelActionTitle: String, defaultActionBlock: ((UIAlertAction) -> Void)? = nil) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let defaultAction = UIAlertAction(title: defaultActionTitle, style: .default, handler: defaultActionBlock)
        let cancelAction = UIAlertAction(title: cancelActionTitle, style: .cancel)
        alert.addAction(cancelAction)
        alert.addAction(defaultAction)
        self.present(alert, animated: true)
    }
}

 

 

그다음엔 권한을 관리하는 객체들을 실체화? 추상화? 관계로 묶는다. 정처기에서는 이런 할 수 있거나, 해야 하는 기능으로 묶는 걸 실체화 관계라 하던데... 일단은 권한을 크게 3가지로 나눌 수 있다.

enum AuthorizationStatus {
    case hasAuthorization
    case needAuthorization
    case notYet
}

protocol AuthorizationManager {
    func getAuthorizationStatus() -> AuthorizationStatus
    func requestAuthorization()
}
  • 권한이 있음
  • 권한이 필요함 (권한을 거부함)
  • 아직 권한 설정을 안 함

이렇게 크게 3가지로 나눌 수 있다. 3가지로 나눈 까닭은 사용자가 처음 권한을 요청하면, 그때서야 권한을 요청하는 Alert가 나오게 하기 위함이다. 여기서 권한을 거부하면, 설정으로 이동하는 Alert가 나오게 해야 하기에, 총 3가지로 권한을 나누었다.

 

이제 권한을 요청하는 기능을 만들 차례다. 해당 기능은 권한의 요청에 따라 어떤 작업을 수행할지를 결정하는 기능이다.

protocol AuthorizationAlertable: Alertable {
}

extension AuthorizationAlertable where Self: UIViewController {
    func checkAuthorization(authorizationManager: AuthorizationManager, title: String, message: String, completion: () -> Void) {
        switch authorizationManager.getAuthorizationStatus() {
        case .hasAuthorization:
            completion()
        case .needAuthorization:
            showAuthorizationAlert(title: title, message: message)
        case .notYet:
            authorizationManager.requestAuthorization()
        }
    }
    
    private func showAuthorizationAlert(title: String, message: String) {
        showAlert(title: title, message: message, defaultActionTitle: "설정", cancelActionTitle: "취소") { _ in
            guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
            if UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url)
            }
        }
    }
}

checkAuthorization을 통해, 인자값에 있는 권한을 관리하는 객체인 AuthorizationManager을 이용해 권한의 상태를 받은 후에 권한의 상태에 따라 수행하는 작업을 달리한다.

  • 권한이 있는 경우: completionBlock을 수행한다. 사용자가 권한이 있는 경우에 수행할 작업 (ex: 화면 이동 등등)
  • 권한이 없는 경우: showAuthorizationAlert를 통해 해당 앱의 설정으로 이동한다.
  • 권한을 아직 설정 안 한 경우: 기능을 수행할 때, 그때 권한 요청 Alert가 보이게 한다. HIG에서는 권한 요청을 필요한 타이밍에 요청하는 것을 추천하고 있다. 이에 따라, 권한 설정을 아직 안 한 경우 권한을 요청하도록 한다.

사용법

현재 위치를 요청하는 작업을 예시로 했다.

 

첫 번째로 위치를 관리하는 객체를 만들거나 extension으로 프로토콜을 채택하도록 한다. AuthorizationAlertable는 권한을 Alert로 보여주는 책임만 가지고 있을 뿐, 권한을 설정하고, 데이터를 가져오는 것은 AuthorizationManager가 가지고 있다. AuthorizationManager를 채택하고 있는 CoreLocationManager를 만든다. 

protocol CoreLocationManagerDelegate: AnyObject {
    func didChangeAuthorization(status: AuthorizationStatus)
    func getCurrentLocation(latitude: Double, longitude: Double)
}

class CoreLocationManager: NSObject, AuthorizationManager {
    
    let coreLocationManager = CLLocationManager()
    
    var delegate: CoreLocationManagerDelegate?
    
    override init() {
        super.init()
        coreLocationManager.delegate = self
    }
    
    func getAuthorizationStatus() -> AuthorizationStatus {
        switch coreLocationManager.authorizationStatus {
        case .authorizedAlways, .authorizedWhenInUse:
            return .hasAuthorization
        case .restricted, .denied:
            return .needAuthorization
        case .notDetermined:
            return .notYet
        default:
            fatalError()
        }
    }
    
    func requestAuthorization() {
        coreLocationManager.requestWhenInUseAuthorization()
    }
}

CLLocationManagerDelegate와 CoreLocationManagerDelegate를 이용해 ViewController에서 CoreLocationManagerDelegate를 채택하여 위도와 경도를 받을 수 있게 해 주었다. delegate의 didChangeAuthorization은 mapView에 현재 위치를 표시할 때, CLLocationManagerDelegate에서 실행되어야 한다고 해서 만들었다.

extension CoreLocationManager: CLLocationManagerDelegate {
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .authorizedWhenInUse:
            manager.startUpdatingLocation()
            delegate?.didChangeAuthorization(status: .hasAuthorization)
        default:
            break
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.first else { return }
        delegate?.getCurrentLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
    }
}

 객체를 만들었으면 이번에는 위치권한 Alert를 만들 차례다. 권한을 요구하는 작업이 필요하다면, AuthorizationAlertable을 채택하여 extension으로 AuthorizationAlertable의 checkAuthorization을 실행하기만 하면 된다.

위치 권한이 필요한 Alert이니 그에 알맞은 title과 설명을 넣어주고, 권한이 있을 경우 실행할 블록을 completion에 넣어 주자.

protocol LocationAlertable: AuthorizationAlertable {
    var locationManager: CoreLocationManager { get }
}

extension LocationAlertable where Self: UIViewController {
    func checkLocationAuthorization(completion: () -> Void) {
        checkAuthorization(authorizationManager: locationManager, title: "위치 권한이 필요합니다.", message: "설정에서 위치권환을 부여해주십시오.", completion: completion)
    }
}

드디어 마지막이다. ViewController에 LocationAlertable를 채택하고 원하는 곳에서 해당 작업을 실행만 하면 된다. 

import UIKit
import MapKit

class RunningStartViewController: UIViewController, LocationAlertable {
    var locationManager: CoreLocationManager
    
    @IBOutlet weak var mapView: MKMapView!
    @IBOutlet weak var goalStackView: UIStackView!
    @IBOutlet weak var startButton: UIButton!
    @IBOutlet weak var goalDistanceLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setButton()
        locationManager.delegate = self
    }
    
    init(locationManager: CoreLocationManager) {
        self.locationManager = locationManager
        super.init(nibName: "RunningStartViewController", bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setMapView() {
        mapView.delegate = self
        mapView.showsUserLocation = true
        mapView.setUserTrackingMode(.follow, animated: false)
    }
    
    func setButton() {
        startButton.cornerRadius = startButton.frame.height/2
    }
    
    @IBAction func tabStartButton(_ sender: Any) {
        checkLocationAuthorization {
            print("원하는 작업")
        }
    }
}

실행영상

 

 

권한 허용 안함
권한 허용