iOS Background

iOS 앱은 백그라운드 상태로 진입시 기본적으로 모든 기능이 멈춘다. 예를 들어 주기적인 타이머를 돌려서 데이터를 주기적으로 서버와 주고 받는 일이 앱이 실행 상태에서는 당연히 잘 되지만 백그라운드 상태로 진입하면 되질 않는다.
앱이 백그라운드로 들어가면 얼마동안은 잘 실행되는것 처럼 보이지만 곧 멈춤 상태로 진입한다.

그래서 앱이 백그라운드에 진입해도 최소한의 필요한 작업을 위해서 백그라운드 작업을 등록하고 요청해야 한다.
Target > Sign & Capabilities > Background modes 에서 “Background fetch”, “Background Processing” 두가지를 체크한다. info.plist 파일에 BGTaskSchedulerPermittedIdentifiers 항목을 추가하고 아래에 예로 든 identifier를 추가한다.

예를 들어서 아래처럼 identifier를 지정하고 앱내에서 사용한다.
Background fetch identifier : “com.daymore.background.fetch”
Background processing identifier : “com.daymore.background.processing”

Background fetch 와 processing 의 차이점은 가벼운 작업이라면 fetch, 좀 더 무거운 작업이라면 processing 에서 진행한다고 애플 문서에 나와 있지만 명확한 구분이 없다.

경험상(이렇지 않을 수도 있다) fetch만 등록하면 시스템에서 잘 호출해 주지만 processing만 등록한 경우 시스템에서 거의 호출해 주지 않아서 백그라운드 작업 거의 이루어 지지 않아 2개 모두 등록했다. 2개 모두 등록하니까 주기적으로 잘 호출되었다. 백그라운드 작업은 시스템이 호출해 주는데 개발자가 호출 주기를 10분으로 설정하더라도 언제 얼마만큼 호출될지 알 수 없다.

예시로 AppDelegate.swift에 작성했다. 어디든 작성 가능하다.
가장 짜증나는 상황은 앱 백그라운드 상태에서 어떠한 데이터를 서버에 보내야 하는 상황이었다.
앱 백그라운드 상황에서는 Data 형을 직접 보낼 수 없고 파일만 업로드 가능하고 클로저 형식의 전송 완료 처리는 안되서 delegate 함수 구현으로 해야한다. 그렇지 않으면 크래시 발생하거나 동작하지 않는다.
이를 구현하기 위해 Custom Background Session을 만들어야 한다. 구글에서 검색하여 참고해서 만들었다.

// AppDelegate.swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.daymore.background.fetch", using: nil) { task in
        self.handleBackground(task)
        self.scheduleNext("com.daymore.background.fetch")
    }
    BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.daymore.background.processing", using: nil) { task in
        self.handleBackground(task)
        self.scheduleNext("com.daymore.background.processing")
    }
}

func handleBackground(task: BGTask) {
    // work something
    sendToServer(["event":"1", "data":"test"])
    task.setTaskCompleted(success: true)
}

func scheduleNext(identifier: String) {
    let req = BGAppRefreshTaskRequest(identifier: identifier)
    req.earliestBeginDate = Date(timeIntervalSinceNow: 60*10)
    try? BGTaskScheduler.shared.submit(req)
}

func applicationDidEnterBackground(_ application: UIApplication) {
    scheduleNext("com.daymore.background.fetch")
    scheduleNext("com.daymore.background.processing")
}

func sendToServer(_ dic: [String:Any]) {
    guard let httpBody = try? JSONSerialization.data(withJSONObject: dic, options: []) else { return }
        
    let url = URL(string: "https://daymore.com/to/upload")
    var req = URLRequest(url: U"https:")
    req.httpMethod = "POST"
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.setValue("authorization_value", forHTTPHeaderField: "Authorization")
        
    let identifier = UUID().uuidString
    let file = URL(fileURLWithPath: NSTemporaryDirectory().appending(identifier))
    try? httpBody.write(to: file)
        
    let task = BackgroundSession.shared.session.uploadTask(with: req, fromFile: file)
    BackgroundSession.shared.begin(taskId: task.taskIdentifier, dataId: identifier)
    task.resume()
}

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    BackgroundSession.shared.saveCompletionHandler = completionHandler
}
// BackgroundSession.swift

class BackgroundSession: NSObject {
    static let shared = BackgroundSession()
    static let identifier = "com.daymore.background.session"
    
    var session: URLSession!
    var tasks = [Int:String]()
    
    var saveCompletionHandler: (() -> Void)?
    
    private override init() {
        super.init()
        let conf = URLSessionConfiguration.background(withIdentifier: BackgroundSession.identifier)
        session = URLSession(configuration: conf, delegate: self, delegateQueue: nil)
    }
    
    func begin(taskId: Int, dataId: String) {
        tasks[taskId] = dataId
    }
    
    func end(taskId: Int) {
        tasks.removeValue(forKey: taskId)
    }
}

extension BackgroundSession: URLSessionDelegate {
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        DispatchQueue.main.async {
            self.saveCompletionHandler?()
            self.saveCompletionHandler = nil
        }
    }
}

extension BackgroundSession: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let e = error {
            print("background upload error \(e)")
            return
        }
        
        if let dataId = tasks[task.taskIdentifier] {
            let file = URL(fileURLWithPath: NSTemporaryDirectory().appending(dataId))
            try? FileManager.default.removeItem(at: file)
        }
        
        end(taskId: task.taskIdentifier)
    }
}

답글 남기기

이메일 주소는 공개되지 않습니다.