版本记录
版本号 | 时间 |
---|---|
V1.0 | 2022.11.06 星期日 |
前言
Background Modes
我们在程序中总会用到,包括语音、定位更新、后台任务以及远程通知等,这个模块我们就一起来学习下。感兴趣的可以看下面几篇文章。
1. Background Modes详细解析(一) —— 几种Mode使用示例(一)
源码
1. Swift
首先看下工程组织架构
下面就是源码了
1. AppMain.swift
import SwiftUI
@main
struct AppMain: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
2. AppDelegate.swift
import UIKit
import BackgroundTasks
class AppDelegate: UIResponder, UIApplicationDelegate {
static var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .long
return formatter
}()
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: AppConstants.backgroundTaskIdentifier,
using: nil) { task in
self.refresh()
task.setTaskCompleted(success: true)
self.scheduleAppRefresh()
}
scheduleAppRefresh()
return true
}
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: AppConstants.backgroundTaskIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: 1 * 60)
do {
try BGTaskScheduler.shared.submit(request)
print("background refresh scheduled")
} catch {
print("Couldn't schedule app refresh \(error.localizedDescription)")
}
}
func refresh() {
// to simulate a refresh, just update the last refresh date to current date/time
let formattedDate = Self.dateFormatter.string(from: Date())
UserDefaults.standard.set(formattedDate, forKey: UserDefaultsKeys.lastRefreshDateKey)
print("refresh occurred")
}
}
3. AppConstants.swift
import Foundation
enum AppConstants {
static let backgroundTaskIdentifier = "com.mycompany.myapp.task.refresh"
}
enum UserDefaultsKeys {
static let lastRefreshDateKey = "lastRefreshDate"
}
4. AudioModel.swift
import SwiftUI
import AVFoundation
import Combine
extension AudioView {
class Model: ObservableObject {
@Published var time: TimeInterval = 0
@Published var item: AVPlayerItem?
@Published var isPlaying = false
var itemTitle: String {
if let asset = item?.asset as? AVURLAsset {
return asset.url.lastPathComponent
} else {
return "-"
}
}
private var itemObserver: AnyCancellable?
private var timeObserver: Any?
private let player: AVQueuePlayer
init() {
do {
try AVAudioSession
.sharedInstance()
.setCategory(.playback, mode: .default)
} catch {
print("Failed to set audio session category. Error: \(error)")
}
self.player = AVQueuePlayer(items: Self.songs)
player.actionAtItemEnd = .advance
itemObserver = player.publisher(for: \.currentItem).sink { [weak self] newItem in
self?.item = newItem
}
timeObserver = player.addPeriodicTimeObserver(
forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
queue: nil) { [weak self] time in
self?.time = time.seconds
}
}
func playPauseAudio() {
isPlaying.toggle()
if isPlaying {
player.play()
} else {
player.pause()
}
}
static var songs: [AVPlayerItem] = {
// find the mp3 song files in the bundle and return player item for each
let songNames = ["FeelinGood", "IronBacon", "WhatYouWant"]
return songNames.map {
guard let url = Bundle.main.url(forResource: $0, withExtension: "mp3") else {
return nil
}
return AVPlayerItem(url: url)
}
.compactMap { $0 }
}()
}
}
5. LocationModel.swift
import Combine
import CoreLocation
import MapKit
extension LocationView {
class Model: NSObject, CLLocationManagerDelegate, ObservableObject {
@Published var isLocationTrackingEnabled = false
@Published var location: CLLocation?
@Published var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
@Published var pins: [PinLocation] = []
let mgr: CLLocationManager
override init() {
mgr = CLLocationManager()
mgr.desiredAccuracy = kCLLocationAccuracyBest
mgr.requestAlwaysAuthorization()
mgr.allowsBackgroundLocationUpdates = true
super.init()
mgr.delegate = self
}
func enable() {
mgr.startUpdatingLocation()
}
func disable() {
mgr.stopUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let currentLocation = locations.first {
print(currentLocation)
location = currentLocation
appendPin(location: currentLocation)
updateRegion(location: currentLocation)
}
}
func appendPin(location: CLLocation) {
pins.append(PinLocation(coordinate: location.coordinate))
}
func updateRegion(location: CLLocation) {
region = MKCoordinateRegion(
center: location.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.0015, longitudeDelta: 0.0015))
}
func startStopLocationTracking() {
isLocationTrackingEnabled.toggle()
if isLocationTrackingEnabled {
enable()
} else {
disable()
}
}
}
struct PinLocation: Identifiable {
let id = UUID()
var coordinate: CLLocationCoordinate2D
}
}
6. CompleteTaskModel.swift
import Combine
import SwiftUI
extension CompleteTaskView {
class Model: ObservableObject {
@Published var isTaskExecuting = false
@Published var resultsMessage = initialMessage
static let initialMessage = "Fibonacci Computations"
static let maxValue = NSDecimalNumber(mantissa: 1, exponent: 40, isNegative: false)
var previous = NSDecimalNumber.one
var current = NSDecimalNumber.one
var position: UInt = 1
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
var updateTimer: Timer?
func beginPauseTask() {
isTaskExecuting.toggle()
if isTaskExecuting {
resetCalculation()
updateTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
self?.calculateNextNumber()
}
} else {
updateTimer?.invalidate()
updateTimer = nil
endBackgroundTaskIfActive()
resultsMessage = Self.initialMessage
}
}
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
print("iOS has signaled time has expired")
self?.endBackgroundTaskIfActive()
}
}
func endBackgroundTaskIfActive() {
let isBackgroundTaskActive = backgroundTask != .invalid
if isBackgroundTaskActive {
print("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
}
func resetCalculation() {
previous = .one
current = .one
position = 1
}
func calculateNextNumber() {
let result = current.adding(previous)
if result.compare(Self.maxValue) == .orderedAscending {
previous = current
current = result
position += 1
} else {
// This is just too much.... Start over.
resetCalculation()
}
resultsMessage = "Position \(self.position) = \(self.current)"
switch UIApplication.shared.applicationState {
case .background:
let timeRemaining = UIApplication.shared.backgroundTimeRemaining
if timeRemaining < Double.greatestFiniteMagnitude {
let secondsRemaining = String(format: "%.1f seconds remaining", timeRemaining)
print("App is backgrounded - \(resultsMessage) - \(secondsRemaining)")
}
default:
break
}
}
func onChangeOfScenePhase(_ newPhase: ScenePhase) {
switch newPhase {
case .background:
let isTimerRunning = updateTimer != nil
let isTaskUnregistered = backgroundTask == .invalid
if isTimerRunning && isTaskUnregistered {
registerBackgroundTask()
}
case .active:
endBackgroundTaskIfActive()
default:
break
}
}
}
}
7. ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
AudioView()
.tabItem {
VStack {
Image(systemName: "music.note")
Text("Audio")
}
}
.tag(0)
LocationView()
.tabItem {
VStack {
Image(systemName: "mappin")
Text("Location")
}
}
.tag(1)
CompleteTaskView()
.tabItem {
VStack {
Image(systemName: "platter.filled.bottom.and.arrow.down.iphone")
Text("Completion")
}
}
.tag(2)
RefreshView()
.tabItem {
VStack {
Image(systemName: "arrow.clockwise.circle")
Text("Refresh")
}
}
.tag(3)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
8. AudioView.swift
import SwiftUI
import AVFoundation
struct AudioView: View {
@StateObject var model = Model()
var body: some View {
VStack(alignment: .center, spacing: 20) {
Text("Audio Player")
.font(.largeTitle)
.fontWeight(.bold)
.padding(EdgeInsets(top: 50, leading: 50, bottom: 0, trailing: 50))
Spacer()
Button(
action: model.playPauseAudio) {
VStack {
Text(model.isPlaying ? "Pause" : "Play")
.padding()
Image(systemName: model.isPlaying ? "pause" : "play")
}
}
.font(.title)
.padding()
Text("Now Playing: \(model.itemTitle)")
.font(.title2)
Text(Self.formatter.string(from: model.time) ?? "")
.font(.title2)
Spacer()
Spacer()
}
}
static let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = .pad
return formatter
}()
}
struct AudioView_Previews: PreviewProvider {
static var previews: some View {
AudioView()
}
}
9. LocationView.swift
import SwiftUI
import CoreLocation
import MapKit
struct LocationView: View {
@StateObject var model = Model()
var body: some View {
VStack(alignment: .center, spacing: 20) {
Text("Location Tracker")
.font(.largeTitle)
.fontWeight(.bold)
.padding(EdgeInsets(top: 50, leading: 50, bottom: 0, trailing: 50))
Button(
action: { model.startStopLocationTracking() },
label: {
VStack {
Image(systemName: model.isLocationTrackingEnabled ? "stop" : "location")
Text(model.isLocationTrackingEnabled ? "Stop" : "Start")
}
})
.font(.title)
.padding(EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10))
Map(coordinateRegion: $model.region, annotationItems: model.pins) { pin in
MapPin(coordinate: pin.coordinate, tint: .red)
}
.padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20))
}
}
}
struct LocationView_Previews: PreviewProvider {
static var previews: some View {
LocationView()
}
}
10. CompleteTaskView.swift
import SwiftUI
struct CompleteTaskView: View {
@Environment(\.scenePhase) var scenePhase
@StateObject var model = Model()
var body: some View {
VStack(alignment: .center, spacing: 20) {
Text("Task Completion")
.font(.largeTitle)
.fontWeight(.bold)
.padding(EdgeInsets(top: 50, leading: 50, bottom: 0, trailing: 50))
Spacer()
Button(
action: { model.beginPauseTask() },
label: {
VStack {
Text(model.isTaskExecuting ? "Stop Task" : "Begin Task")
.padding()
Image(systemName: model.isTaskExecuting ? "stop" : "play")
}
})
.font(.title)
.padding()
Text(model.resultsMessage)
.font(.title2)
Spacer()
Spacer()
}
.onChange(of: scenePhase) { newPhase in
model.onChangeOfScenePhase(newPhase)
}
}
}
struct FinishTaskView_Previews: PreviewProvider {
static var previews: some View {
CompleteTaskView()
}
}
11. RefreshView.swift
import SwiftUI
import BackgroundTasks
struct RefreshView: View {
@AppStorage(UserDefaultsKeys.lastRefreshDateKey) var lastRefresh = "Never"
@Environment(\.scenePhase) var scenePhase
var body: some View {
VStack(alignment: .center, spacing: 20) {
Text("Background Refresh")
.font(.largeTitle)
.fontWeight(.bold)
.padding(EdgeInsets(top: 50, leading: 50, bottom: 0, trailing: 50))
Spacer()
Text("Refresh last performed:")
.multilineTextAlignment(.center)
.font(.title)
.onChange(of: scenePhase) { scenePhase in
if scenePhase == .background {
print("moved to background")
}
}
.padding()
Text(lastRefresh)
.font(.title2)
Spacer()
Spacer()
}
}
}
struct FetchView_Previews: PreviewProvider {
static var previews: some View {
RefreshView()
}
}
后记
本篇主要讲述了
Background Modes
几种Mode
使用示例,感兴趣的给个赞或者关注~~~