Slide 1

Slide 1 text

Ұൠతͳ௨৴Ͱ΋࢖͑Δ όοΫάϥ΢ϯυURLSessionͷ׆༻ํ๏ iOSDC Japan 2023

Slide 2

Slide 2 text

ࣗݾ঺հ Li Jierong Ϧ έπΤΠ ▸ גࣜձࣾΏΊΈ ▸ iOSϦʔυΤϯδχΞ݉ςοΫϦʔυ ▸ ӬԕʹϦϦʔεͰ͖ͣݸਓ։ൃ ▸ @[email protected] DuckDuckGoGo

Slide 3

Slide 3 text

Ұൠతͳ௨৴Ͱ΋࢖͑ΔόοΫάϥ΢ϯυURLSessionͷ׆༻ํ๏ τϐοΫ ▸ όοΫάϥϯυURLSessionͱ͸ ▸ ϝϦοτͱ׆༻ํ๏ ▸ ࣮૷ͷ۩ମྫ ▸ ࢖༻্ͷ஫ҙࣄ߲

Slide 4

Slide 4 text

όοΫάϥ΢ϯυ URLSessionͱ͸

Slide 5

Slide 5 text

όοΫάϥ΢ϯυURLSessionͱ͸ URLSessionͱ͸ ▸ An object that coordinates a group of related, network data transfer tasks. ▸ ωοτϫʔΫ௨৴ʹ͓͍ͯෆՄܽͳ΋ͷɻ ▸ AlamofireͳͲͷωοτϫʔΫ௨৴ϥΠϒϥϦʔ͸͋͘·ͰURLSessionͷϥο ύʔɻ

Slide 6

Slide 6 text

όοΫάϥ΢ϯυURLSessionͱ͸ όοΫάϥ΢ϯυͱ͸ URLSessionConfigurationʹ͸ࡾͭͷछྨ͕͋Δɻ ▸ defaultɺephemeral ▸ ΞϓϦͱಉ͡ϓϩηεͰ࣮ߦ͞ΕΔɻ ▸ background ▸ ผϓϩηεͰ࣮ߦ͞ΕΔɻ

Slide 7

Slide 7 text

όοΫάϥ΢ϯυURLSessionͱ͸ όοΫάϥ΢ϯυͱ͸ Suspend Foreground runtime Suspend Background runtime System Background URLSession request URLSession response

Slide 8

Slide 8 text

όοΫάϥ΢ϯυURLSessionͱ͸ ओཁͳ༻్ σʔλྔ͕େ͖͍௨৴ʹ࢖༻͞ΕΔɻ ▸ ͕͔͔࣌ؒΔ௨৴ͩͱϢʔβʔʹϑΥΞάϥ΢ϯυͰ଴ͨͤΔΘ͚ʹ͸͍͔ͳ ͍ɻ ▸ URLSessionConfiguration.background(withIdentifier:)ͷυΩϡϝϯτʹݴٴ͞ Εͨ།Ұͷྫɿ ▸ Downloading Files in the Background

Slide 9

Slide 9 text

ϝϦοτͱ ׆༻ํ๏

Slide 10

Slide 10 text

ϝϦοτͱ׆༻ํ๏ ϝϦοτ ผϓϩηεͰ࣮ߦ͞ΕΔ͜ͱʹΑΓɺԼهͷ໰୊͕ճආͰ͖Δɻ ▸ iOSΞϓϦ͕όοΫάϥ΢ϯυʹ͋Δ࣌ʹɺURLSession͕ΞϓϦϓϩηεͱͱ΋ ʹҰ࣌ఀࢭ͞ΕΔɻ ▸ ϑΥΞάϥ΢ϯυʹ໭ͬͯ΋ɺλΠϜΞ΢τʹΑΔΤϥʔ͕ൃੜɻ ▸ ϝϞϦෆ଍ͳͲͷݪҼͰɺόοΫάϥ΢ϯυʹ͋ΔΞϓϦ͕iOSʹऴྃ͞Εͨ৔ ߹ɺதࢭ͞ΕΔɻ

Slide 11

Slide 11 text

ϝϦοτͱ׆༻ํ๏ ϝϦοτ γεςϜঢ়ଶΛ؂ࢹ͠ɺ࠷దͳߦಈΛͯ͘͠ΕΔɻ ▸ σϑΥϧτ ▸ ઀ଓঢ়گ ▸ ࣗಈϦτϥΠ ▸ isDiscretionary == true ▸ όοςϦʔ࢒ྔ ▸ ଳҬ෯ʢ௨৴࿏༰ྔʁʣ

Slide 12

Slide 12 text

ϝϦοτͱ׆༻ํ๏ ϝϦοτ Background TasksϑϨʔϜϫʔΫͱҧ͍ɺԼهͷ੍ݶ͕ͳ͍ɻ ▸ Capabilitiesͷొ࿥͕ඞਢɻ ▸ Ϣʔβʔͷઃఆ΍௿ిྗϞʔυʹΑΓɺڧ੍తʹແޮԽ͞ΕΔ͜ͱ͕͋Δɻ ▸ όοΫάϥ΢ϯυͰ࣮ߦՄೳͷ͕࣌ؒෆఆɻ

Slide 13

Slide 13 text

ϝϦοτͱ׆༻ํ๏ ׆༻ํ๏ ϑΥΞάϥ΢ϯυͰ଴ͭ࣌ؒͷ࡟ݮʹ໾ཱͭɻ ▸ ৘ใͷࣄલऔಘ ▸ iOSDC Japan 2021ɿWidgetKitͰྑ͍ମݧΛ࡞Δʹ͸ ▸ BGAppRefreshTaskͷ୅ସ

Slide 14

Slide 14 text

׆༻ํ๏ BGAppRefreshTaskͷ୅ସ Suspend Foreground runtime Background runtime System Request App refresh for soon URLSession response Suspend App refresh task URLSession request

Slide 15

Slide 15 text

׆༻ํ๏ BGAppRefreshTaskͷ୅ସ Suspend Foreground runtime Background runtime System Background URL Session request for soon URLSession response Suspend

Slide 16

Slide 16 text

ϝϦοτͱ׆༻ํ๏ ׆༻ํ๏ ϑΥΞάϥ΢ϯυͰ଴ͭ࣌ؒͷ࡟ݮʹ໾ཱͭɻ ▸ ৘ใͷࣄલऔಘ ▸ iOSDC Japan 2021ɿWidgetKitͰྑ͍ମݧΛ࡞Δʹ͸ ▸ BGAppRefreshTaskͷ୅ସ ▸ λΠϜΞ΢τ΍தࢭͷճආ ▸ beginBackgroundTask(withName:expirationHandler:)ͷԆ௕ ▸ BGAppRefreshTaskͷԆ௕

Slide 17

Slide 17 text

׆༻ํ๏ BGAppRefreshTaskͷԆ௕ Background runtime System Suspend App refresh task URLSession request URLSession response

Slide 18

Slide 18 text

׆༻ํ๏ BGAppRefreshTaskͷԆ௕ Background runtime System Suspend Suspend App refresh task URLSession request Background URLSession request Low runtime signal URLSession response

Slide 19

Slide 19 text

࣮૷ͷ۩ମྫ

Slide 20

Slide 20 text

࣮૷ͷ۩ମྫ όοΫάϥ΢ϯυURLSessionΛ׆༻ͨ͠ ͍͚Ͳɺcompletion handler෇͖ͷAPI͕࢖͑ ͣɺ࢖͍উख͕ѱ͍ɻ

Slide 21

Slide 21 text

࣮૷ͷ۩ମྫ DelegateͰ݁ՌΛड͚औΔ public final class BackgroundSessionTaskManager: NSObject { public typealias CompletionHandler = (Result<(Data, HTTPURLResponse), Error>) -> Void private var session: URLSession! private var tasks: [Int: TaskWrapper] = [:] public init(sessionIdentifier: String) { super.init() self.session = .init( configuration: .background(withIdentifier: sessionIdentifier), delegate: self, delegateQueue: nil ) } func makeTask(with request: URLRequest, completionHandler: CompletionHandler?) -> URLSessionDownloadTask { precondition(request.url?.scheme == "https") let task = session.downloadTask(with: request) tasks[task.taskIdentifier] = TaskWrapper(task: task, completionHandler: completionHandler) return task } }

Slide 22

Slide 22 text

࣮૷ͷ۩ମྫ DelegateͰ݁ՌΛड͚औΔ private struct TaskWrapper { let wrappedValue: URLSessionDownloadTask let completionHandler: CompletionHandler? var fileReadResult: Result? init( task: URLSessionDownloadTask, completionHandler: CompletionHandler? ) { self.wrappedValue = task self.completionHandler = completionHandler } }

Slide 23

Slide 23 text

࣮૷ͷ۩ମྫ DelegateͰ݁ՌΛड͚औΔ extension BackgroundSessionTaskManager: URLSessionDownloadDelegate { public func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) { let key = downloadTask.taskIdentifier guard var task = tasks[key] else { return } task.fileReadResult = .init { try Data(contentsOf: location) } tasks[key] = task } }

Slide 24

Slide 24 text

࣮૷ͷ۩ମྫ DelegateͰ݁ՌΛड͚औΔ extension BackgroundSessionTaskManager: URLSessionTaskDelegate { public func urlSession( _ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error? ) { guard let task = tasks.removeValue(forKey: task.taskIdentifier), let completionHandler = task.completionHandler else { return } do { if let error = error { throw error } else if let response = task.wrappedValue.response as? HTTPURLResponse { let data = try task.fileReadResult?.get() ?? .init() completionHandler(.success((data, response))) } else { throw BackgroundSessionTaskError.invalidResponse } } catch { completionHandler(.failure(error)) } } }

Slide 25

Slide 25 text

࣮૷ͷ۩ମྫ DelegateͰ݁ՌΛड͚औΔ let task = manager.makeTask(with: request) { result in // ... } task.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) task.resume()

Slide 26

Slide 26 text

࣮૷ͷ۩ମྫ ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ ▸ UIKitͷ৔߹ ▸ SwiftUIͷ৔߹ ▸ Widget extensionͷ৔߹

Slide 27

Slide 27 text

ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ UIKitͷ৔߹ public let manager = BackgroundSessionTaskManager(sessionIdentifier: "MySession") class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, handleEventsForBackgroundURLSession identifier: String ) async { await manager.waitUntilEventDeliveryFinished() } }

Slide 28

Slide 28 text

ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ SwiftUIͷ৔߹ public let manager = BackgroundSessionTaskManager(sessionIdentifier: "MySession") @main struct PlaygroundApp: App { var body: some Scene { WindowGroup { ContentView() } .backgroundTask(.urlSession) { _ in await manager.waitUntilEventDeliveryFinished() } } }

Slide 29

Slide 29 text

ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ Widget extensionͷ৔߹ struct MyWidget: Widget { let kind: String = "MyWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in WidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("My Widget") .description("This is an example widget.") .backgroundTask(.urlSession) { identifier in let manager = BackgroundSessionTaskManager(sessionIdentifier: identifier) await manager.waitUntilEventDeliveryFinished() } } }

Slide 30

Slide 30 text

ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ Widget extensionͷ৔߹ struct MyWidget: Widget { let kind: String = "MyWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in WidgetEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("My Widget") .description("This is an example widget.") .onBackgroundURLSessionEvents { identifier, completion in Task { let manager = BackgroundSessionTaskManager(sessionIdentifier: identifier) await manager.waitUntilEventDeliveryFinished() completion() } } } }

Slide 31

Slide 31 text

࣮૷ͷ۩ମྫ ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ extension BackgroundSessionTaskManager: URLSessionDelegate { public func urlSessionDidFinishEvents( forBackgroundURLSession session: URLSession ) { eventDeliveryCompletionHandler?() eventDeliveryCompletionHandler = nil } public func waitUntilEventDeliveryFinished() async { await withCheckedContinuation { eventDeliveryCompletionHandler = $0.resume } } }

Slide 32

Slide 32 text

࣮૷ͷ۩ମྫ ϝϞϦ্ʹอ͍࣋ͯ͠Δtasks͸Ξϓ Ϧ͕ऴྃ͞ΕͨΒফ͑ͳ͘ͳ͍ʁ

Slide 33

Slide 33 text

࣮૷ͷ۩ମྫ DelegateͰ݁ՌΛड͚औΔ extension BackgroundSessionTaskManager: URLSessionDownloadDelegate { public func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) { let key = downloadTask.taskIdentifier guard var task = tasks[key] else { return } task.fileReadResult = .init { try Data(contentsOf: location) } tasks[key] = task } }

Slide 34

Slide 34 text

࣮૷ͷ۩ମྫ DelegateͰ݁ՌΛड͚औΔ extension BackgroundSessionTaskManager: URLSessionDownloadDelegate { public func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) { let key = downloadTask.taskIdentifier var task = tasks[key, default: .init(task: downloadTask, completionHandler: nil)] task.fileReadResult = .init { try Data(contentsOf: location) } tasks[key] = task } }

Slide 35

Slide 35 text

࣮૷ͷ۩ମྫ ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ extension BackgroundSessionTaskManager: URLSessionTaskDelegate { public func urlSession( _ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error? ) { guard let task = tasks.removeValue(forKey: task.taskIdentifier), let completionHandler = task.completionHandler else { return } do { if let error = error { throw error } else if let response = task.wrappedValue.response as? HTTPURLResponse { let data = try task.fileReadResult?.get() ?? .init() completionHandler(.success((data, response))) } else { throw BackgroundSessionTaskError.invalidResponse } } catch { completionHandler(.failure(error)) } } }

Slide 36

Slide 36 text

࣮૷ͷ۩ମྫ ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ guard let task = tasks.removeValue(forKey: task.taskIdentifier), let completionHandler = task.completionHandler else { return }

Slide 37

Slide 37 text

࣮૷ͷ۩ମྫ ΞϓϦ͕Ұ࣌ఀࢭɾऴྃ͞Εͨ৔߹ let task = tasks.removeValue(forKey: task.taskIdentifier) ?? .init(task: task, completionHandler: nil) let completionHandler = task.completionHandler ?? { result in var userInfo = [TaskDidCompleteNotification.UserInfoKey: Any]() userInfo[.description] = task.wrappedValue.taskDescription userInfo[.result] = result NotificationCenter.default.post( name: TaskDidCompleteNotification.name, object: self, userInfo: userInfo ) }

Slide 38

Slide 38 text

࣮૷ͷ۩ମྫ BGAppRefreshTaskͷ֦ு .backgroundTask(.appRefresh("WEATHER_DATA")) { let (data, response) = try? await URLSession.shared.data(for: request) // ... }

Slide 39

Slide 39 text

࣮૷ͷ۩ମྫ BGAppRefreshTaskͷ֦ு .backgroundTask(.appRefresh("WEATHER_DATA")) { let request = URLRequest(url: WEATHER_URL) await withTaskCancellationHandler { let (data, response) = try? await URLSession.shared.data(for: request) // ... } onCancel: { manager.makeTask(with: request, completionHandler: nil) .resume() } }

Slide 40

Slide 40 text

࢖༻্ͷ஫ҙࣄ߲

Slide 41

Slide 41 text

࢖༻্ͷ஫ҙࣄ߲ ݶఆతʹ࢖༻ ผϓϩηεʹ࣮ߦ͞ΕΔͨΊɺΦʔόϔου͕ଘࡏɻ ▸ ओཁͳ༻్ ▸ σʔλྔ͕େ͖͍௨৴ ▸ ׆༻ํ๏ ▸ ৘ใͷࣄલऔಘ ▸ λΠϜΞ΢τ΍தࢭͷճආ

Slide 42

Slide 42 text

࢖༻্ͷ஫ҙࣄ߲ ಈ࡞֬ೝ ▸ exit(0)Ͱऴྃɻ ▸ task.resume()௚ޙͰ΋໰୊ͳ͍ɻ ▸ Proxyman΍CharlesͳͲͷπʔϧͰ௨৴Λ؍࡯ɻ ▸ Wait for the executable to be launchedɻ

Slide 43

Slide 43 text

࢖༻্ͷ஫ҙࣄ߲ ੍ݶ ▸ Delegate͕ඞਢɻ ▸ HTTP௨৴͔͠αϙʔτ͞Εͳ͍ɻ ▸ ϦμΠϨΫτ͸ৗʹڐՄ͞ΕΔɻ ▸ Upload taskΛ࢖͏৔߹ɺϑΝΠϧ͔ΒͷΞοϓϩʔυ͢Δඞཁ͕͋Δɻ

Slide 44

Slide 44 text

·ͱΊ ▸ ϑΝΠϧμ΢ϯϩʔυҎ֎ʹ΋όοΫάϥ΢ϯυURLSessionΛ׆༻Ͱ͖Δɻ ▸ ޻෉͢Ε͹ϑΥΞάϥ΢ϯυURLSessionͱಉ͡࢖͍উखʹͳΕΔɻ ▸ ผϓϩηεͰ࣮ߦ͞ΕΔͨΊɺบ͕͋Γɺ੍ݶ΋ଟ͍ɺཁ஫ҙɻ

Slide 45

Slide 45 text

ࢀߟ ▸ WWDC14: What’s New in Foundation Networking ▸ WWDC19: Advances in App Background Execution ▸ WWDC22: Efficiency awaits: Background tasks in SwiftUI ▸ Swift Package Manager: URLSessionHTTPClient ▸ URLSession: Common pitfalls with background download & upload tasks ▸ iOSDC Japan 2021ɿWidgetKitͰྑ͍ମݧΛ࡞Δʹ͸