系統推送的集成(二十二) —— 關於本地通知的詳細解析(二) 版本記錄 前言 源碼 後記

版本記錄

版本號 時間
V1.0 2021.05.24 星期一

前言

我們做APP很多時候都需要推送功能,以直播爲例,如果你關注的主播開播了,那麼就需要向關注這個主播的人發送開播通知,提醒用戶去看播,這個只是一個小的方面,具體應用根據公司的業務邏輯而定。前面已經花了很多篇幅介紹了極光推送,其實極光推送無非就是將我們客戶端和服務端做的很多東西封裝了一下,節省了我們很多處理邏輯和流程,這一篇開始,我們就利用系統的原生推送類結合工程實踐說一下系統推送的集成,希望我的講解能讓大家很清楚的理解它。感興趣的可以看上面幾篇。
1. 系統推送的集成(一) —— 基本集成流程(一)
2. 系統推送的集成(二) —— 推送遇到的幾個坑之BadDeviceToken問題(一)
3. 系統推送的集成(三) —— 本地和遠程通知編程指南之你的App的通知 - 本地和遠程通知概覽(一)
4. 系統推送的集成(四) —— 本地和遠程通知編程指南之你的App的通知 - 管理您的應用程序的通知支持(二)
5. 系統推送的集成(五) —— 本地和遠程通知編程指南之你的App的通知 - 調度和處理本地通知(三)
6. 系統推送的集成(六) —— 本地和遠程通知編程指南之你的App的通知 - 配置遠程通知支持(四)
7. 系統推送的集成(七) —— 本地和遠程通知編程指南之你的App的通知 - 修改和顯示通知(五)
8. 系統推送的集成(八) —— 本地和遠程通知編程指南之蘋果推送通知服務APNs - APNs概覽(一)
9. 系統推送的集成(九) —— 本地和遠程通知編程指南之蘋果推送通知服務APNs - 創建遠程通知Payload(二)
10. 系統推送的集成(十) —— 本地和遠程通知編程指南之蘋果推送通知服務APNs - 與APNs通信(三)
11. 系統推送的集成(十一) —— 本地和遠程通知編程指南之蘋果推送通知服務APNs - Payload Key參考(四)
12. 系統推送的集成(十二) —— 本地和遠程通知編程指南之Legacy信息 - 二進制Provider API(一)
13. 系統推送的集成(十三) —— 本地和遠程通知編程指南之Legacy信息 - Legacy通知格式(二)
14. 系統推送的集成(十四) —— 發送和處理推送通知流程詳解(一)
15. 系統推送的集成(十五) —— 發送和處理推送通知流程詳解(二)
16. 系統推送的集成(十六) —— 自定義遠程通知(一)
17. 系統推送的集成(十七) —— APNs從工程配置到自定義通知UI全流程解析(一)
18. 系統推送的集成(十八) —— APNs從工程配置到自定義通知UI全流程解析(二)
19. 系統推送的集成(十九) —— APNs配置接收和處理的簡單入門(一)
20. 系統推送的集成(二十) —— APNs配置接收和處理的簡單入門(二)
21. 系統推送的集成(二十一) —— 關於本地通知的詳細解析(一)

源碼

1. Swift

首先看下工程組織結構

下面就是源碼啦

1. TaskManager.swift
import Foundation

class TaskManager: ObservableObject {
  static let shared = TaskManager()
  let taskPersistenceManager = TaskPersistenceManager()

  @Published var tasks: [Task] = []

  init() {
    loadTasks()
  }

  func save(task: Task) {
    tasks.append(task)
    DispatchQueue.global().async {
      self.taskPersistenceManager.save(tasks: self.tasks)
    }
    if task.reminderEnabled {
      NotificationManager.shared.scheduleNotification(task: task)
    }
  }

  func loadTasks() {
    self.tasks = taskPersistenceManager.loadTasks()
  }

  func addNewTask(_ taskName: String, _ reminder: Reminder?) {
    if let reminder = reminder {
      save(task: Task(name: taskName, reminderEnabled: true, reminder: reminder))
    } else {
      save(task: Task(name: taskName, reminderEnabled: false, reminder: Reminder()))
    }
  }

  func remove(task: Task) {
    tasks.removeAll {
      $0.id == task.id
    }
    DispatchQueue.global().async {
      self.taskPersistenceManager.save(tasks: self.tasks)
    }
    if task.reminderEnabled {
      NotificationManager.shared.removeScheduledNotification(task: task)
    }
  }

  func markTaskComplete(task: Task) {
    if let row = tasks.firstIndex(where: { $0.id == task.id }) {
      var updatedTask = task
      updatedTask.completed = true
      tasks[row] = updatedTask
    }
  }
}
2. NotificationManager.swift
import Foundation
import UserNotifications
import CoreLocation

enum NotificationManagerConstants {
  static let timeBasedNotificationThreadId =
    "TimeBasedNotificationThreadId"
  static let calendarBasedNotificationThreadId =
    "CalendarBasedNotificationThreadId"
  static let locationBasedNotificationThreadId =
    "LocationBasedNotificationThreadId"
}

class NotificationManager: ObservableObject {
  static let shared = NotificationManager()
  @Published var settings: UNNotificationSettings?

  func requestAuthorization(completion: @escaping  (Bool) -> Void) {
    UNUserNotificationCenter.current()
      .requestAuthorization(options: [.alert, .sound, .badge]) { granted, _  in
        self.fetchNotificationSettings()
        completion(granted)
      }
  }

  func fetchNotificationSettings() {
    // 1
    UNUserNotificationCenter.current().getNotificationSettings { settings in
      // 2
      DispatchQueue.main.async {
        self.settings = settings
      }
    }
  }

  func removeScheduledNotification(task: Task) {
    UNUserNotificationCenter.current()
      .removePendingNotificationRequests(withIdentifiers: [task.id])
  }

  // 1
  func scheduleNotification(task: Task) {
    // 2
    let content = UNMutableNotificationContent()
    content.title = task.name
    content.body = "Gentle reminder for your task!"
    content.categoryIdentifier = "OrganizerPlusCategory"
    let taskData = try? JSONEncoder().encode(task)
    if let taskData = taskData {
      content.userInfo = ["Task": taskData]
    }

    // 3
    var trigger: UNNotificationTrigger?
    switch task.reminder.reminderType {
    case .time:
      if let timeInterval = task.reminder.timeInterval {
        trigger = UNTimeIntervalNotificationTrigger(
          timeInterval: timeInterval,
          repeats: task.reminder.repeats)
      }
      content.threadIdentifier =
        NotificationManagerConstants.timeBasedNotificationThreadId
    case .calendar:
      if let date = task.reminder.date {
        trigger = UNCalendarNotificationTrigger(
          dateMatching: Calendar.current.dateComponents(
            [.day, .month, .year, .hour, .minute],
            from: date),
          repeats: task.reminder.repeats)
      }
      content.threadIdentifier =
        NotificationManagerConstants.calendarBasedNotificationThreadId
    case .location:
      // 1
      guard CLLocationManager().authorizationStatus == .authorizedWhenInUse else {
        return
      }
      // 2
      if let location = task.reminder.location {
        // 3
        let center = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
        let region = CLCircularRegion(center: center, radius: location.radius, identifier: task.id)
        trigger = UNLocationNotificationTrigger(region: region, repeats: task.reminder.repeats)
      }
      content.threadIdentifier =
        NotificationManagerConstants.locationBasedNotificationThreadId
    }

    // 4
    if let trigger = trigger {
      let request = UNNotificationRequest(
        identifier: task.id,
        content: content,
        trigger: trigger)
      // 5
      UNUserNotificationCenter.current().add(request) { error in
        if let error = error {
          print(error)
        }
      }
    }
  }
}
3. TaskPersistenceManager.swift
import Foundation

class TaskPersistenceManager {
  enum FileConstants {
    static let tasksFileName = "tasks.json"
  }

  func save(tasks: [Task]) {
    do {
      let documentsDirectory = getDocumentsDirectory()
      let storageURL = documentsDirectory.appendingPathComponent(FileConstants.tasksFileName)
      let tasksData = try JSONEncoder().encode(tasks)
      do {
        try tasksData.write(to: storageURL)
      } catch {
        print("Couldn't write to File Storage")
      }
    } catch {
      print("Couldn't encode tasks data")
    }
  }

  func loadTasks() -> [Task] {
    let documentsDirectory = getDocumentsDirectory()
    let storageURL = documentsDirectory.appendingPathComponent(FileConstants.tasksFileName)
    guard
      let taskData = try? Data(contentsOf: storageURL),
      let tasks = try? JSONDecoder().decode([Task].self, from: taskData)
    else {
      return []
    }

    return tasks
  }

  func getDocumentsDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return paths[0]
  }
}
4. LocationManager.swift
import CoreLocation

class LocationManager: NSObject, ObservableObject {
  var locationManager = CLLocationManager()
  @Published var authorized = false

  override init() {
    super.init()
    locationManager.delegate = self
    if locationManager.authorizationStatus == .authorizedWhenInUse {
      authorized = true
      locationManager.startMonitoringSignificantLocationChanges()
    }
  }

  func requestAuthorization() {
    locationManager.requestWhenInUseAuthorization()
  }
}

// MARK: - CLLocationManagerDelegate
extension LocationManager: CLLocationManagerDelegate {
  func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    if locationManager.authorizationStatus == .authorizedWhenInUse ||
      locationManager.authorizationStatus == .authorizedAlways {
      authorized = true
    } else {
      authorized = false
    }
  }
}`
5. TaskListView.swift
import SwiftUI

struct TaskListView: View {
  @ObservedObject var taskManager = TaskManager.shared
  @State var showNotificationSettingsUI = false

  var body: some View {
    ZStack {
      VStack {
        HStack {
          Spacer()
          Text("Organizer Plus")
            .font(.title)
            .foregroundColor(.pink)
          Spacer()
          Button(
            action: {
              // 1
              NotificationManager.shared.requestAuthorization { granted in
                // 2
                if granted {
                  showNotificationSettingsUI = true
                }
              }
            },
            label: {
              Image(systemName: "bell")
                .font(.title)
                .accentColor(.pink)
            })
            .padding(.trailing)
            .sheet(isPresented: $showNotificationSettingsUI) {
              NotificationSettingsView()
            }
        }
        .padding()
        if taskManager.tasks.isEmpty {
          Spacer()
          Text("No Tasks!")
            .foregroundColor(.pink)
            .font(.title3)
          Spacer()
        } else {
          List(taskManager.tasks) { task in
            TaskCell(task: task)
          }
          .padding()
        }
      }
      AddTaskView()
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    TaskListView()
  }
}

struct TaskCell: View {
  var task: Task

  var body: some View {
    HStack {
      Button(
        action: {
          TaskManager.shared.markTaskComplete(task: task)
          DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            TaskManager.shared.remove(task: task)
          }
        }, label: {
          Image(systemName: task.completed ? "checkmark.circle.fill" : "circle")
            .resizable()
            .frame(width: 20, height: 20)
            .accentColor(.pink)
        })
      if task.completed {
        Text(task.name)
          .strikethrough()
          .foregroundColor(.pink)
      } else {
        Text(task.name)
          .foregroundColor(.pink)
      }
    }
  }
}

struct AddTaskView: View {
  @State var showCreateTaskView = false

  var body: some View {
    VStack {
      Spacer()
      HStack {
        Spacer()
        Button(
          action: {
            showCreateTaskView = true
          }, label: {
            Text("+")
              .font(.largeTitle)
              .multilineTextAlignment(.center)
              .frame(width: 30, height: 30)
              .foregroundColor(Color.white)
              .padding()
          })
          .background(Color.pink)
          .cornerRadius(40)
          .padding()
          .sheet(isPresented: $showCreateTaskView) {
            CreateTaskView()
          }
      }
      .padding(.bottom)
    }
  }
}
6. CreateTaskView.swift
import SwiftUI
import MapKit

struct CreateTaskView: View {
  @State var taskName: String = ""
  @State var reminderEnabled = false
  @State var selectedTrigger = ReminderType.time
  @State var timeDurationIndex: Int = 0
  @State private var dateTrigger = Date()
  @State private var shouldRepeat = false
  @State private var latitude: String = ""
  @State private var longitude: String = ""
  @State private var radius: String = ""
  @Environment(\.presentationMode) var presentationMode

  let triggers = ["Time", "Calendar", "Location"]
  let timeDurations: [Int] = Array(1...59)
  var body: some View {
    NavigationView {
      Form {
        Section {
          HStack {
            Spacer()
            Text("Add Task")
              .font(.title)
              .padding()
            Spacer()
            Button("Save") {
              TaskManager.shared.addNewTask(taskName, makeReminder())
              presentationMode.wrappedValue.dismiss()
            }
            .disabled(taskName.isEmpty ? true : false)
            .padding()
          }
          VStack {
            TextField("Enter name for the task", text: $taskName)
              .padding(.vertical)
            Toggle(isOn: $reminderEnabled) {
              Text("Add Reminder")
            }
            .padding(.vertical)

            if reminderEnabled {
              ReminderView(
                selectedTrigger: $selectedTrigger,
                timeDurationIndex: $timeDurationIndex,
                triggerDate: $dateTrigger,
                shouldRepeat: $shouldRepeat,
                latitude: $latitude,
                longitude: $longitude,
                radius: $radius)
                .navigationBarHidden(true)
                .navigationTitle("")
            }
            Spacer()
          }
          .padding()
        }
      }
      .navigationBarTitle("")
      .navigationBarHidden(true)
    }
  }

  func makeReminder() -> Reminder? {
    guard reminderEnabled else {
      return nil
    }
    var reminder = Reminder()
    reminder.reminderType = selectedTrigger
    switch selectedTrigger {
    case .time:
      reminder.timeInterval = TimeInterval(timeDurations[timeDurationIndex] * 60)
    case .calendar:
      reminder.date = dateTrigger
    case .location:
      if let latitude = Double(latitude),
        let longitude = Double(longitude),
        let radius = Double(radius) {
        reminder.location = LocationReminder(
          latitude: latitude,
          longitude: longitude,
          radius: radius)
      }
    }
    reminder.repeats = shouldRepeat
    return reminder
  }
}

struct CreateTaskView_Previews: PreviewProvider {
  static var previews: some View {
    CreateTaskView()
  }
}

struct ReminderView: View {
  @Binding var selectedTrigger: ReminderType
  @Binding var timeDurationIndex: Int
  @Binding var triggerDate: Date
  @Binding var shouldRepeat: Bool
  @Binding var latitude: String
  @Binding var longitude: String
  @Binding var radius: String
  @StateObject var locationManager = LocationManager()

  var body: some View {
    VStack {
      Picker("Notification Trigger", selection: $selectedTrigger) {
        Text("Time").tag(ReminderType.time)
        Text("Date").tag(ReminderType.calendar)
        Text("Location").tag(ReminderType.location)
      }
      .pickerStyle(SegmentedPickerStyle())
      .padding(.vertical)
      if selectedTrigger == ReminderType.time {
        Picker("Time Interval", selection: $timeDurationIndex) {
          ForEach(1 ..< 59) { i in
            if i == 1 {
              Text("\(i) minute").tag(i)
            } else {
              Text("\(i) minutes").tag(i)
            }
          }
          .navigationBarHidden(true)
          .padding(.vertical)
        }
      } else if selectedTrigger == ReminderType.calendar {
        DatePicker("Please enter a date", selection: $triggerDate)
          .labelsHidden()
          .padding(.vertical)
      } else {
        VStack {
          if !locationManager.authorized {
            Button(
              action: {
                locationManager.requestAuthorization()
              },
              label: {
                Text("Request Location Authorization")
              })
          } else {
            TextField("Enter Latitude", text: $latitude)
            TextField("Enter Longitude", text: $longitude)
            TextField("Enter Radius", text: $radius)
          }
        }
        .padding(.vertical)
      }
      Toggle(isOn: $shouldRepeat) {
        Text("Repeat Notification")
      }
    }
  }
}
7. NotificationSettingsView.swift
import SwiftUI

struct NotificationSettingsView: View {
  @ObservedObject var notificationManager = NotificationManager.shared

  var body: some View {
    VStack {
      Form {
        Section {
          HStack {
            Spacer()
            Text("Notification Settings")
              .font(.title2)
            Spacer()
          }
        }
        Section {
          SettingRowView(
            setting: "Authorization Status",
            enabled: notificationManager.settings?.authorizationStatus == UNAuthorizationStatus.authorized)
          SettingRowView(
            setting: "Show in Notification Center",
            enabled: notificationManager.settings?.notificationCenterSetting == .enabled)
          SettingRowView(
            setting: "Sound Enabled?",
            enabled: notificationManager.settings?.soundSetting == .enabled)
          SettingRowView(
            setting: "Badges Enabled?",
            enabled: notificationManager.settings?.badgeSetting == .enabled)
          SettingRowView(
            setting: "Alerts Enabled?",
            enabled: notificationManager.settings?.alertSetting == .enabled)
          SettingRowView(
            setting: "Show on lock screen?",
            enabled: notificationManager.settings?.lockScreenSetting == .enabled)
          SettingRowView(
            setting: "Alert banners?",
            enabled: notificationManager.settings?.alertStyle == .banner)
          SettingRowView(
            setting: "Critical Alerts?",
            enabled: notificationManager.settings?.criticalAlertSetting == .enabled)
          SettingRowView(
            setting: "Siri Announcement?",
            enabled: notificationManager.settings?.announcementSetting == .enabled)
        }
      }
    }
  }
}

struct NotificationSettingsView_Previews: PreviewProvider {
  static var previews: some View {
    NotificationSettingsView()
  }
}

struct SettingRowView: View {
  var setting: String
  var enabled: Bool
  var body: some View {
    HStack {
      Text(setting)
      Spacer()
      if enabled {
        Image(systemName: "checkmark")
          .foregroundColor(.green)
      } else {
        Image(systemName: "xmark")
          .foregroundColor(.red)
      }
    }
    .padding()
  }
}
8. Task.swift
import Foundation

struct Task: Identifiable, Codable {
  var id = UUID().uuidString
  var name: String
  var completed = false
  var reminderEnabled = false
  var reminder: Reminder
}

enum ReminderType: Int, CaseIterable, Identifiable, Codable {
  case time
  case calendar
  case location
  var id: Int { self.rawValue }
}

struct Reminder: Codable {
  var timeInterval: TimeInterval?
  var date: Date?
  var location: LocationReminder?
  var reminderType: ReminderType = .time
  var repeats = false
}

struct LocationReminder: Codable {
  var latitude: Double
  var longitude: Double
  var radius: Double
}
9. AppMain.swift
import SwiftUI

@main
struct AppMain: App {
  @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

  var body: some Scene {
    WindowGroup {
      TaskListView()
    }
  }
}`
10. AppDelegate.swift
import UIKit

class AppDelegate: NSObject, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    configureUserNotifications()
    return true
  }
}

// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void
  ) {
    completionHandler(.banner)
  }

  private func configureUserNotifications() {
    UNUserNotificationCenter.current().delegate = self
    // 1
    let dismissAction = UNNotificationAction(
      identifier: "dismiss",
      title: "Dismiss",
      options: []
    )
    let markAsDone = UNNotificationAction(
      identifier: "markAsDone",
      title: "Mark As Done",
      options: []
    )
    // 2
    let category = UNNotificationCategory(
      identifier: "OrganizerPlusCategory",
      actions: [dismissAction, markAsDone],
      intentIdentifiers: [],
      options: []
    )
    // 3
    UNUserNotificationCenter.current().setNotificationCategories([category])
  }

  // 1
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
  ) {
    // 2
    if response.actionIdentifier == "markAsDone" {
      let userInfo = response.notification.request.content.userInfo
      if let taskData = userInfo["Task"] as? Data {
        if let task = try? JSONDecoder().decode(Task.self, from: taskData) {
          // 3
          TaskManager.shared.remove(task: task)
        }
      }
    }
    completionHandler()
  }
}

後記

本篇主要講述了關於本地通知的詳細解析,感興趣的給個贊或者關注~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章