美文网首页
系统推送的集成(十八) —— APNs从工程配置到自定义通知UI

系统推送的集成(十八) —— APNs从工程配置到自定义通知UI

作者: 刀客传奇 | 来源:发表于2020-05-16 19:06 被阅读0次

    版本记录

    版本号 时间
    V1.0 2020.05.16 星期六

    前言

    我们做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全流程解析(一)

    源码

    1. Swift

    首先看下工程组织结构

    接着就是看sb中的内容

    Main.storyboard MainInterface.storyboard

    下面就是源码了

    1. AppDelegate.swift
    
    import UIKit
    import SafariServices
    import UserNotifications
    import CoreData
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
      var window: UIWindow?
      
      func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UINavigationBar.appearance().barTintColor = .themeGreenColor
        UINavigationBar.appearance().tintColor = .white
        UITabBar.appearance().barTintColor = .themeGreenColor
        UITabBar.appearance().tintColor = .white
        
        registerForPushNotifications()
        
        return true
      }
      
      func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
      ) {
        let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
        let token = tokenParts.joined()
        print("Device Token: \(token)")
      }
      
      func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
      ) {
        print("Failed to register: \(error)")
      }
      
      func application(
        _ application: UIApplication,
        didReceiveRemoteNotification userInfo: [AnyHashable: Any],
        fetchCompletionHandler completionHandler:
        @escaping (UIBackgroundFetchResult) -> Void
      ) {
        guard let aps = userInfo["aps"] as? [String: AnyObject] else {
          completionHandler(.failed)
          return
        }
        
        if aps["content-available"] as? Int == 1 {
          let podcastStore = PodcastStore.sharedStore
          podcastStore.refreshItems { didLoadNewItems in
            completionHandler(didLoadNewItems ? .newData : .noData)
          }
        }
      }
      
      func registerForPushNotifications() {
        UNUserNotificationCenter.current()
          .requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
            guard let self = self else { return }
            print("Permission granted: \(granted)")
            
            guard granted else { return }
            
            let viewAction = UNNotificationAction(
              identifier: Identifiers.viewAction, title: "View",
              options: [.foreground])
            
            let newsCategory = UNNotificationCategory(
              identifier: Identifiers.newsCategory, actions: [viewAction],
              intentIdentifiers: [], options: [])
            
            UNUserNotificationCenter.current()
              .setNotificationCategories([newsCategory])
            
            self.getNotificationSettings()
        }
      }
      
      func getNotificationSettings() {
        UNUserNotificationCenter.current().getNotificationSettings { settings in
          print("Notification settings: \(settings)")
          guard settings.authorizationStatus == .authorized else { return }
          DispatchQueue.main.async {
            UIApplication.shared.registerForRemoteNotifications()
          }
        }
      }
      
      
      // MARK: UISceneSession Lifecycle
      
      func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
      }
    }
    
    2. SceneDelegate.swift
    
    import UIKit
    import UserNotifications
    import SafariServices
    
    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
      var window: UIWindow?
      
      func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        UNUserNotificationCenter.current().delegate = self
      }
      
      func sceneWillEnterForeground(_ scene: UIScene) {
        NotificationCenter.default.post(name: Notification.Name.appEnteringForeground, object: nil)
      }
    }
    
    extension SceneDelegate: UNUserNotificationCenterDelegate {
      func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
      ) {
        let userInfo = response.notification.request.content.userInfo
        
        if let aps = userInfo["aps"] as? [String: AnyObject],
          let category = aps["category"] as? String,
          category == "new_podcast_available",
          let link = userInfo["podcast-link"] as? String {
          
          if let podcast = CoreDataManager.shared.fetchPodcast(byLinkIdentifier: link) {
            guard
              let navController = window?.rootViewController as? UINavigationController,
              let podcastFeedVC = navController.viewControllers[0] as? PodcastFeedTableViewController
              else {
                preconditionFailure("Invalid tab configuration")
            }
            
            let podcastItem = podcast.toPodcastItem()
            podcastFeedVC.loadPodcastDetail(for: podcastItem)
          }
        }
        
        completionHandler()
      }
    }
    
    3. PodcastFeedTableViewController.swift
    
    import UIKit
    
    class PodcastFeedTableViewController: UITableViewController {
      let podcastStore = PodcastStore.sharedStore
      
      override var prefersStatusBarHidden: Bool {
        return true
      }
      
      override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 75
        
        if let patternImage = UIImage(named: "pattern-grey") {
          let backgroundView = UIView()
          backgroundView.backgroundColor = UIColor(patternImage: patternImage)
          tableView.backgroundView = backgroundView
        }
        
        podcastStore.refreshItems { didLoadNewItems in
          if didLoadNewItems {
            DispatchQueue.main.async {
              self.tableView.reloadData()
            }
          }
        }
        
        NotificationCenter.default.addObserver(
          forName: Notification.Name.appEnteringForeground,
          object: nil,
          queue: nil) { _ in
            self.podcastStore.reloadCachedData()
            self.tableView.reloadData()
        }
      }
      
      override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        PodcastStore.sharedStore.reloadCachedData()
        tableView.reloadData()
      }
      
      @IBSegueAction
      func createPodcastItemViewController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> PodcastItemViewController? {
        guard let indexPath = tableView.indexPathsForSelectedRows?.first else {
          return nil
        }
        
        let podcastItem = podcast(for: indexPath)
        return PodcastItemViewController(coder: coder, podcastItem: podcastItem)
      }
      
      private func podcast(for indexPath: IndexPath) -> PodcastItem {
        return podcastStore.items[indexPath.row]
      }
      
      func loadPodcastDetail(for podcast: PodcastItem) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let detailVC = storyboard.instantiateViewController(identifier: "PodcastItemViewController") { coder in
          return PodcastItemViewController(coder: coder, podcastItem: podcast)
        }
        
        navigationController?.pushViewController(detailVC, animated: true)
      }
    }
    
    // MARK: - UITableViewDataSource, UITableViewDelegate
    extension PodcastFeedTableViewController {
      override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return podcastStore.items.count
      }
      
      override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "PodcastItemCell", for: indexPath)
        
        if let podcastCell = cell as? PodcastItemCell {
          let podcastItem = podcast(for: indexPath)
          podcastCell.update(with: podcastItem)
        }
        
        return cell
      }
    }
    
    4. PodcastItemViewController.swift
    
    import UIKit
    import AVKit
    
    class PodcastItemViewController: UIViewController {
      let artworkURLString = "https://koenig-media.raywenderlich.com/uploads/2019/04/Podcast-icon-2019-1400x1400.png"
      
      @IBOutlet weak var titleLabel: UILabel!
      @IBOutlet weak var playerContainerView: UIView!
      @IBOutlet weak var podcastDetailTextView: UITextView!
      @IBOutlet weak var favoriteButton: UIButton!
      
      var playerViewController: AVPlayerViewController!
      var podcastItem: PodcastItem
      
      required init?(coder: NSCoder) {
        fatalError("init(coder:) not implemented")
      }
      
      required init?(coder: NSCoder, podcastItem: PodcastItem) {
        self.podcastItem = podcastItem
        super.init(coder: coder)
      }
      
      override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        setupNotificationObservers()
        refreshData()
        
        titleLabel.text = podcastItem.title
        let htmlStringData = podcastItem.detail.data(using: .utf8)!
        let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
          NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html
        ]
        
        if let attributedHTMLString = try? NSMutableAttributedString(
          data: htmlStringData,
          options: options,
          documentAttributes: nil) {
          podcastDetailTextView.attributedText = attributedHTMLString
        }
        
        updateFavoriteUI()
        
        guard let url = URL(string: artworkURLString) else {
          fatalError("Invalid URL")
        }
        
        downloadImage(for: url)
        
        let player = AVPlayer(url: podcastItem.streamingURL)
        playerViewController.player = player
        playerViewController.player?.play()
      }
      
      override func viewWillDisappear(_ animated: Bool) {
        playerViewController.player?.pause()
        super.viewWillDisappear(animated)
      }
      
      override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "playerEmbed",
          let playerVC = segue.destination as? AVPlayerViewController {
          playerViewController = playerVC
        }
      }
      
      @IBAction func favoriteButtonTapped(_ sender: UIButton) {
        let favoriteSetting = podcastItem.isFavorite ? false : true
        CoreDataManager.shared.updatePodcaseFavoriteSetting(podcastItem.link, isFavorite: favoriteSetting)
        podcastItem.isFavorite = favoriteSetting
        
        updateFavoriteUI()
      }
      
      private func downloadImage(for url: URL) {
        ImageDownloader.shared.downloadImage(forURL: url) { [weak self] result in
          guard
            let self = self,
            let image = try? result.get()
            else {
              return
          }
          
          DispatchQueue.main.async {
            let imageView = UIImageView(image: image)
            imageView.contentMode = .scaleAspectFit
            guard let contentOverlayView = self.playerViewController.contentOverlayView else {
              return
            }
            
            contentOverlayView.addSubview(imageView)
            imageView.frame = contentOverlayView.bounds
          }
        }
      }
      
      private func setupNotificationObservers() {
        NotificationCenter.default.addObserver(
          forName: Notification.Name.appEnteringForeground,
          object: nil,
          queue: nil
        ) { _ in
          self.refreshData()
          self.updateFavoriteUI()
        }
      }
      
      private func updateFavoriteUI() {
        let symbolString = podcastItem.isFavorite ? "star.fill" : "star"
        favoriteButton.setImage(UIImage(systemName: symbolString), for: .normal)
      }
      
      private func refreshData() {
        let refreshedItem = PodcastStore.sharedStore.reloadData(for: podcastItem)
        self.podcastItem = refreshedItem
      }
    }
    
    5. PodcastItemCell.swift
    
    import UIKit
    
    class PodcastItemCell: UITableViewCell {
      func update(with newsItem: PodcastItem) {
        textLabel?.text = newsItem.title
        detailTextLabel?.text = DateParser.displayString(for: newsItem.publishedDate)
      }
    }
    
    6. CoreDataManager.swift
    
    import Foundation
    import CoreData
    
    public class CoreDataManager {
      public static let shared = CoreDataManager()
      
      private let diskManager = DiskCacheManager()
      private let container: NSPersistentContainer
      
      private init() {
        let persistentContainer = NSPersistentContainer(name: "Wendercast")
        let storeURL = diskManager.databaseURL
        let storeDescription = NSPersistentStoreDescription(url: storeURL)
        persistentContainer.persistentStoreDescriptions = [storeDescription]
        
        persistentContainer.loadPersistentStores { description, error in
          if let error = error {
            preconditionFailure("Unable to configure persistent container: \(error)")
          }
        }
        
        container = persistentContainer
      }
      
      func fetchPodcastItems() -> [PodcastItem] {
        let managedObjectContext = container.viewContext
        let fetchRequest: NSFetchRequest<Podcast> = Podcast.fetchRequest()
        fetchRequest.sortDescriptors = [
          NSSortDescriptor(key: "publishedDate", ascending: false)
        ]
        
        do {
          let results: [Podcast] = try managedObjectContext.fetch(fetchRequest)
          let finalResults = results.map { podcast in
            podcast.toPodcastItem()
          }
          
          return finalResults
        } catch {
          print("Podcast fetch error: \(error)")
          return []
        }
      }
      
      public func savePodcastItems(_ podcastItems: [PodcastItem]) {
        var newPodcasts: [Podcast] = []
        for podcastItem in podcastItems {
          
          let podcastFetchResult = fetchPodcast(byLinkIdentifier: podcastItem.link)
          
          if podcastFetchResult == nil {
            let podcast = Podcast(context: container.viewContext)
            podcast.title = podcastItem.title
            podcast.link = podcastItem.link
            podcast.publishedDate = podcastItem.publishedDate
            podcast.streamingURL = podcastItem.streamingURL
            podcast.isFavorite = podcastItem.isFavorite
            podcast.detail = podcastItem.detail
            
            newPodcasts.append(podcast)
          }
        }
        
        saveContext()
      }
      
      func fetchPodcast(byLinkIdentifier linkIdentifier: String) -> Podcast? {
        let fetchRequest: NSFetchRequest<Podcast> = Podcast.fetchRequest()
        let predicate = NSPredicate(format: "link == %@", linkIdentifier)
        fetchRequest.predicate = predicate
        fetchRequest.fetchLimit = 1
        
        let result = try? container.viewContext.fetch(fetchRequest)
        return result?.first
      }
      
      func updatePodcaseFavoriteSetting(_ podcastLink: String, isFavorite: Bool) {
        guard let podcast = fetchPodcast(byLinkIdentifier: podcastLink) else {
          return
        }
        
        podcast.isFavorite = isFavorite
        
        saveContext()
      }
      
      func saveContext() {
        do {
          try container.viewContext.save()
        } catch {
          preconditionFailure("Unable to save context: \(error)")
        }
      }
    }
    
    7. PushIdentifiers.swift
    
    import Foundation
    
    enum Identifiers {
      static let viewAction = "VIEW_IDENTIFIER"
      static let newsCategory = "NEWS_CATEGORY"
    }
    
    8. Podcast.swift
    
    import Foundation
    import CoreData
    
    @objc(Podcast)
    public class Podcast: NSManagedObject {
      public func toPodcastItem() -> PodcastItem {
        guard
          let title = title,
          let publishedDate = publishedDate,
          let link = link,
          let streamingURL = streamingURL,
          let detail = detail
          else {
            preconditionFailure("Invalid podcast item")
        }
        
        return PodcastItem(
          title: title,
          publishedDate: publishedDate,
          link: link,
          streamingURL: streamingURL,
          isFavorite: isFavorite,
          detail: detail
        )
      }
    }
    
    9. PodcastItem.swift
    
    import Foundation
    
    public struct PodcastItem: Codable {
      let title: String
      let publishedDate: Date
      let link: String
      let streamingURL: URL
      var isFavorite = false
      let detail: String
    }
    
    10. PodcastStore.swift
    
    import Foundation
    
    class PodcastStore {
      static let sharedStore = PodcastStore()
      
      let podcastCacheLoader = PodcastCacheLoader()
      var items: [PodcastItem] = []
      
      init() {
        items = CoreDataManager.shared.fetchPodcastItems()
      }
      
      func refreshItems(_ completion: @escaping (_ didLoadNewItems: Bool) -> Void) {
        PodcastFeedLoader.loadFeed { [weak self] items in
          guard let self = self else {
            completion(false)
            return
          }
          
          let didLoadNewItems = items.count > self.items.count
          self.items = items
          self.podcastCacheLoader.savePodcastItems(items)
          completion(didLoadNewItems)
        }
      }
      
      func reloadCachedData() {
        items = CoreDataManager.shared.fetchPodcastItems()
      }
      
      func reloadData(for podcastItem: PodcastItem) -> PodcastItem {
        let refreshedItem = CoreDataManager.shared.fetchPodcast(byLinkIdentifier: podcastItem.link)
        
        guard let item = refreshedItem else {
          return podcastItem
        }
        
        return item.toPodcastItem()
      }
    }
    
    11. PodcastFeedLoader.swift
    
    import Foundation
    
    struct PodcastFeedLoader {
      static let feedURL = "https://www.raywenderlich.com/category/podcast/feed"
      
      static func loadFeed(_ completion: @escaping ([PodcastItem]) -> Void) {
        guard let url = URL(string: feedURL) else { return }
        
        let task = URLSession.shared.dataTask(with: url) { data, _, _ in
          guard let data = data else { return }
          
          let xmlIndexer = SWXMLHash.config { config in
            config.shouldProcessNamespaces = true
          }.parse(data)
          
          let items = xmlIndexer["rss"]["channel"]["item"]
          
          let feedItems = items.compactMap { (indexer: XMLIndexer) -> PodcastItem? in
            if
              let dateString = indexer["pubDate"].element?.text,
              let date = DateParser.dateWithPodcastDateString(dateString),
              let title = indexer["title"].element?.text,
              let link = indexer["link"].element?.text,
              let streamURLString = indexer["enclosure"].element?.attribute(by: "url")?.text,
              let streamURL = URL(string: streamURLString),
              let detail = indexer["description"].element?.text {
              return PodcastItem(
                title: title,
                publishedDate: date,
                link: link,
                streamingURL: streamURL,
                isFavorite: false,
                detail: detail
              )
            }
            
            return nil
          }
          
          completion(feedItems)
        }
        
        task.resume()
      }
    }
    
    12. SWXMLHash.swift
    
    import Foundation
    
    let rootElementName = "SWXMLHash_Root_Element"
    
    /// Parser options
    public class SWXMLHashOptions {
      internal init() {}
      
      /// determines whether to parse the XML with lazy parsing or not
      public var shouldProcessLazily = false
      
      /// determines whether to parse XML namespaces or not (forwards to
      /// `XMLParser.shouldProcessNamespaces`)
      public var shouldProcessNamespaces = false
    }
    
    /// Simple XML parser
    public class SWXMLHash {
      let options: SWXMLHashOptions
      
      private init(_ options: SWXMLHashOptions = SWXMLHashOptions()) {
        self.options = options
      }
      
      /**
       Method to configure how parsing works.
       
       - parameters:
       - configAction: a block that passes in an `SWXMLHashOptions` object with
       options to be set
       - returns: an `SWXMLHash` instance
       */
      class public func config(_ configAction: (SWXMLHashOptions) -> Void) -> SWXMLHash {
        let opts = SWXMLHashOptions()
        configAction(opts)
        return SWXMLHash(opts)
      }
      
      /**
       Begins parsing the passed in XML string.
       
       - parameters:
       - xml: an XML string. __Note__ that this is not a URL but a
       string containing XML.
       - returns: an `XMLIndexer` instance that can be iterated over
       */
      public func parse(_ xml: String) -> XMLIndexer {
        return parse(xml.data(using: String.Encoding.utf8)!)
      }
      
      /**
       Begins parsing the passed in XML string.
       
       - parameters:
       - data: a `Data` instance containing XML
       - returns: an `XMLIndexer` instance that can be iterated over
       */
      public func parse(_ data: Data) -> XMLIndexer {
        let parser: SimpleXmlParser = options.shouldProcessLazily
          ? LazyXMLParser(options)
          : FullXMLParser(options)
        return parser.parse(data)
      }
      
      /**
       Method to parse XML passed in as a string.
       
       - parameter xml: The XML to be parsed
       - returns: An XMLIndexer instance that is used to look up elements in the XML
       */
      class public func parse(_ xml: String) -> XMLIndexer {
        return SWXMLHash().parse(xml)
      }
      
      /**
       Method to parse XML passed in as a Data instance.
       
       - parameter data: The XML to be parsed
       - returns: An XMLIndexer instance that is used to look up elements in the XML
       */
      class public func parse(_ data: Data) -> XMLIndexer {
        return SWXMLHash().parse(data)
      }
      
      /**
       Method to lazily parse XML passed in as a string.
       
       - parameter xml: The XML to be parsed
       - returns: An XMLIndexer instance that is used to look up elements in the XML
       */
      class public func lazy(_ xml: String) -> XMLIndexer {
        return config { conf in conf.shouldProcessLazily = true }.parse(xml)
      }
      
      /**
       Method to lazily parse XML passed in as a Data instance.
       
       - parameter data: The XML to be parsed
       - returns: An XMLIndexer instance that is used to look up elements in the XML
       */
      class public func lazy(_ data: Data) -> XMLIndexer {
        return config { conf in conf.shouldProcessLazily = true }.parse(data)
      }
    }
    
    struct Stack<T> {
      var items = [T]()
      mutating func push(_ item: T) {
        items.append(item)
      }
      mutating func pop() -> T {
        return items.removeLast()
      }
      mutating func drop() {
        let _ = pop()
      }
      mutating func removeAll() {
        items.removeAll(keepingCapacity: false)
      }
      func top() -> T {
        return items[items.count - 1]
      }
    }
    
    protocol SimpleXmlParser {
      init(_ options: SWXMLHashOptions)
      func parse(_ data: Data) -> XMLIndexer
    }
    
    #if os(Linux)
    
    extension XMLParserDelegate {
      
      func parserDidStartDocument(_ parser: Foundation.XMLParser) { }
      func parserDidEndDocument(_ parser: Foundation.XMLParser) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  foundNotationDeclarationWithName name: String,
                  publicID: String?,
                  systemID: String?) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  foundUnparsedEntityDeclarationWithName name: String,
                  publicID: String?,
                  systemID: String?,
                  notationName: String?) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  foundAttributeDeclarationWithName attributeName: String,
                  forElement elementName: String,
                  type: String?,
                  defaultValue: String?) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  foundElementDeclarationWithName elementName: String,
                  model: String) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  foundInternalEntityDeclarationWithName name: String,
                  value: String?) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  foundExternalEntityDeclarationWithName name: String,
                  publicID: String?,
                  systemID: String?) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  didStartElement elementName: String,
                  namespaceURI: String?,
                  qualifiedName qName: String?,
                  attributes attributeDict: [String : String]) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  didEndElement elementName: String,
                  namespaceURI: String?,
                  qualifiedName qName: String?) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  didStartMappingPrefix prefix: String,
                  toURI namespaceURI: String) { }
      
      func parser(_ parser: Foundation.XMLParser, didEndMappingPrefix prefix: String) { }
      
      func parser(_ parser: Foundation.XMLParser, foundCharacters string: String) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  foundIgnorableWhitespace whitespaceString: String) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  foundProcessingInstructionWithTarget target: String,
                  data: String?) { }
      
      func parser(_ parser: Foundation.XMLParser, foundComment comment: String) { }
      
      func parser(_ parser: Foundation.XMLParser, foundCDATA CDATABlock: Data) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  resolveExternalEntityName name: String,
                  systemID: String?) -> Data? { return nil }
      
      func parser(_ parser: Foundation.XMLParser, parseErrorOccurred parseError: NSError) { }
      
      func parser(_ parser: Foundation.XMLParser,
                  validationErrorOccurred validationError: NSError) { }
    }
    
    #endif
    
    /// The implementation of XMLParserDelegate and where the lazy parsing actually happens.
    class LazyXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate {
      required init(_ options: SWXMLHashOptions) {
        self.options = options
        super.init()
      }
      
      var root = XMLElement(name: rootElementName)
      var parentStack = Stack<XMLElement>()
      var elementStack = Stack<String>()
      
      var data: Data?
      var ops: [IndexOp] = []
      let options: SWXMLHashOptions
      
      func parse(_ data: Data) -> XMLIndexer {
        self.data = data
        return XMLIndexer(self)
      }
      
      func startParsing(_ ops: [IndexOp]) {
        // clear any prior runs of parse... expected that this won't be necessary,
        // but you never know
        parentStack.removeAll()
        root = XMLElement(name: rootElementName)
        parentStack.push(root)
        
        self.ops = ops
        let parser = Foundation.XMLParser(data: data!)
        parser.shouldProcessNamespaces = options.shouldProcessNamespaces
        parser.delegate = self
        _ = parser.parse()
      }
      
      func parser(_ parser: Foundation.XMLParser,
                  didStartElement elementName: String,
                  namespaceURI: String?,
                  qualifiedName qName: String?,
                  attributes attributeDict: [String: String]) {
        
        elementStack.push(elementName)
        
        if !onMatch() {
          return
        }
        #if os(Linux)
        let attributeNSDict = NSDictionary(
          objects: attributeDict.values.flatMap({ $0 as? AnyObject }),
          forKeys: attributeDict.keys.map({ NSString(string: $0) as NSObject })
        )
        let currentNode = parentStack.top().addElement(elementName, withAttributes: attributeNSDict)
        #else
        let currentNode = parentStack
          .top()
          .addElement(elementName, withAttributes: attributeDict as NSDictionary)
        #endif
        parentStack.push(currentNode)
      }
      
      func parser(_ parser: Foundation.XMLParser, foundCharacters string: String) {
        if !onMatch() {
          return
        }
        
        let current = parentStack.top()
        
        current.addText(string)
      }
      
      func parser(_ parser: Foundation.XMLParser,
                  didEndElement elementName: String,
                  namespaceURI: String?,
                  qualifiedName qName: String?) {
        
        let match = onMatch()
        
        elementStack.drop()
        
        if match {
          parentStack.drop()
        }
      }
      
      func onMatch() -> Bool {
        // we typically want to compare against the elementStack to see if it matches ops, *but*
        // if we're on the first element, we'll instead compare the other direction.
        if elementStack.items.count > ops.count {
          return elementStack.items.starts(with: ops.map { $0.key })
        } else {
          return ops.map { $0.key }.starts(with: elementStack.items)
        }
      }
    }
    
    /// The implementation of XMLParserDelegate and where the parsing actually happens.
    class FullXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate {
      required init(_ options: SWXMLHashOptions) {
        self.options = options
        super.init()
      }
      
      var root = XMLElement(name: rootElementName)
      var parentStack = Stack<XMLElement>()
      let options: SWXMLHashOptions
      
      func parse(_ data: Data) -> XMLIndexer {
        // clear any prior runs of parse... expected that this won't be necessary,
        // but you never know
        parentStack.removeAll()
        
        parentStack.push(root)
        
        let parser = Foundation.XMLParser(data: data)
        parser.shouldProcessNamespaces = options.shouldProcessNamespaces
        parser.delegate = self
        _ = parser.parse()
        
        return XMLIndexer(root)
      }
      
      func parser(_ parser: Foundation.XMLParser,
                  didStartElement elementName: String,
                  namespaceURI: String?,
                  qualifiedName qName: String?,
                  attributes attributeDict: [String: String]) {
        #if os(Linux)
        let attributeNSDict = NSDictionary(
          objects: attributeDict.values.flatMap({ $0 as? AnyObject }),
          forKeys: attributeDict.keys.map({ NSString(string: $0) as NSObject })
        )
        let currentNode = parentStack.top().addElement(elementName, withAttributes: attributeNSDict)
        #else
        let currentNode = parentStack
          .top()
          .addElement(elementName, withAttributes: attributeDict as NSDictionary)
        #endif
        parentStack.push(currentNode)
      }
      
      func parser(_ parser: Foundation.XMLParser, foundCharacters string: String) {
        let current = parentStack.top()
        
        current.addText(string)
      }
      
      func parser(_ parser: Foundation.XMLParser,
                  didEndElement elementName: String,
                  namespaceURI: String?,
                  qualifiedName qName: String?) {
        
        parentStack.drop()
      }
    }
    
    /// Represents an indexed operation against a lazily parsed `XMLIndexer`
    public class IndexOp {
      var index: Int
      let key: String
      
      init(_ key: String) {
        self.key = key
        self.index = -1
      }
      
      func toString() -> String {
        if index >= 0 {
          return key + " " + index.description
        }
        
        return key
      }
    }
    
    /// Represents a collection of `IndexOp` instances. Provides a means of iterating them
    /// to find a match in a lazily parsed `XMLIndexer` instance.
    public class IndexOps {
      var ops: [IndexOp] = []
      
      let parser: LazyXMLParser
      
      init(parser: LazyXMLParser) {
        self.parser = parser
      }
      
      func findElements() -> XMLIndexer {
        parser.startParsing(ops)
        let indexer = XMLIndexer(parser.root)
        var childIndex = indexer
        for op in ops {
          childIndex = childIndex[op.key]
          if op.index >= 0 {
            childIndex = childIndex[op.index]
          }
        }
        ops.removeAll(keepingCapacity: false)
        return childIndex
      }
      
      func stringify() -> String {
        var s = ""
        for op in ops {
          s += "[" + op.toString() + "]"
        }
        return s
      }
    }
    
    /// Error type that is thrown when an indexing or parsing operation fails.
    public enum IndexingError: Error {
      case Attribute(attr: String)
      case AttributeValue(attr: String, value: String)
      case Key(key: String)
      case Index(idx: Int)
      case Init(instance: AnyObject)
      case Error
    }
    
    /// Returned from SWXMLHash, allows easy element lookup into XML data.
    public enum XMLIndexer: Sequence {
      case element(XMLElement)
      case list([XMLElement])
      case stream(IndexOps)
      case xmlError(IndexingError)
      
      /// The underlying XMLElement at the currently indexed level of XML.
      public var element: XMLElement? {
        switch self {
        case .element(let elem):
          return elem
        case .stream(let ops):
          let list = ops.findElements()
          return list.element
        default:
          return nil
        }
      }
      
      /// All elements at the currently indexed level
      public var all: [XMLIndexer] {
        switch self {
        case .list(let list):
          var xmlList = [XMLIndexer]()
          for elem in list {
            xmlList.append(XMLIndexer(elem))
          }
          return xmlList
        case .element(let elem):
          return [XMLIndexer(elem)]
        case .stream(let ops):
          let list = ops.findElements()
          return list.all
        default:
          return []
        }
      }
      
      /// All child elements from the currently indexed level
      public var children: [XMLIndexer] {
        var list = [XMLIndexer]()
        for elem in all.map({ $0.element! }).compactMap({ $0 }) {
          for elem in elem.xmlChildren {
            list.append(XMLIndexer(elem))
          }
        }
        return list
      }
      
      /**
       Allows for element lookup by matching attribute values.
       
       - parameters:
       - attr: should the name of the attribute to match on
       - value: should be the value of the attribute to match on
       - throws: an XMLIndexer.xmlError if an element with the specified attribute isn't found
       - returns: instance of XMLIndexer
       */
      public func withAttr(_ attr: String, _ value: String) throws -> XMLIndexer {
        switch self {
        case .stream(let opStream):
          let match = opStream.findElements()
          return try match.withAttr(attr, value)
        case .list(let list):
          if let elem = list.filter({$0.attribute(by: attr)?.text == value}).first {
            return .element(elem)
          }
          throw IndexingError.AttributeValue(attr: attr, value: value)
        case .element(let elem):
          if elem.attribute(by: attr)?.text == value {
            return .element(elem)
          }
          throw IndexingError.AttributeValue(attr: attr, value: value)
        default:
          throw IndexingError.Attribute(attr: attr)
        }
      }
      
      /**
       Initializes the XMLIndexer
       
       - parameter _: should be an instance of XMLElement, but supports other values for error handling
       - throws: an Error if the object passed in isn't an XMLElement or LaxyXMLParser
       */
      public init(_ rawObject: AnyObject) throws {
        switch rawObject {
        case let value as XMLElement:
          self = .element(value)
        case let value as LazyXMLParser:
          self = .stream(IndexOps(parser: value))
        default:
          throw IndexingError.Init(instance: rawObject)
        }
      }
      
      /**
       Initializes the XMLIndexer
       
       - parameter _: an instance of XMLElement
       */
      public init(_ elem: XMLElement) {
        self = .element(elem)
      }
      
      init(_ stream: LazyXMLParser) {
        self = .stream(IndexOps(parser: stream))
      }
      
      /**
       Find an XML element at the current level by element name
       
       - parameter key: The element name to index by
       - returns: instance of XMLIndexer to match the element (or elements) found by key
       - throws: Throws an XMLIndexingError.Key if no element was found
       */
      public func byKey(_ key: String) throws -> XMLIndexer {
        switch self {
        case .stream(let opStream):
          let op = IndexOp(key)
          opStream.ops.append(op)
          return .stream(opStream)
        case .element(let elem):
          let match = elem.xmlChildren.filter({ $0.name == key })
          if !match.isEmpty {
            if match.count == 1 {
              return .element(match[0])
            } else {
              return .list(match)
            }
          }
          fallthrough
        default:
          throw IndexingError.Key(key: key)
        }
      }
      
      /**
       Find an XML element at the current level by element name
       
       - parameter key: The element name to index by
       - returns: instance of XMLIndexer to match the element (or elements) found by
       */
      public subscript(key: String) -> XMLIndexer {
        do {
          return try self.byKey(key)
        } catch let error as IndexingError {
          return .xmlError(error)
        } catch {
          return .xmlError(IndexingError.Key(key: key))
        }
      }
      
      /**
       Find an XML element by index within a list of XML Elements at the current level
       
       - parameter index: The 0-based index to index by
       - throws: XMLIndexer.xmlError if the index isn't found
       - returns: instance of XMLIndexer to match the element (or elements) found by index
       */
      public func byIndex(_ index: Int) throws -> XMLIndexer {
        switch self {
        case .stream(let opStream):
          opStream.ops[opStream.ops.count - 1].index = index
          return .stream(opStream)
        case .list(let list):
          if index <= list.count {
            return .element(list[index])
          }
          return .xmlError(IndexingError.Index(idx: index))
        case .element(let elem):
          if index == 0 {
            return .element(elem)
          }
          fallthrough
        default:
          return .xmlError(IndexingError.Index(idx: index))
        }
      }
      
      /**
       Find an XML element by index
       
       - parameter index: The 0-based index to index by
       - returns: instance of XMLIndexer to match the element (or elements) found by index
       */
      public subscript(index: Int) -> XMLIndexer {
        do {
          return try byIndex(index)
        } catch let error as IndexingError {
          return .xmlError(error)
        } catch {
          return .xmlError(IndexingError.Index(idx: index))
        }
      }
      
      typealias GeneratorType = XMLIndexer
      
      /**
       Method to iterate (for-in) over the `all` collection
       
       - returns: an array of `XMLIndexer` instances
       */
      public func makeIterator() -> IndexingIterator<[XMLIndexer]> {
        return all.makeIterator()
      }
    }
    
    /// XMLIndexer extensions
    /*
     extension XMLIndexer: Boolean {
     /// True if a valid XMLIndexer, false if an error type
     public var boolValue: Bool {
     switch self {
     case .xmlError:
     return false
     default:
     return true
     }
     }
     }
     */
    
    extension XMLIndexer: CustomStringConvertible {
      /// The XML representation of the XMLIndexer at the current level
      public var description: String {
        switch self {
        case .list(let list):
          return list.map { $0.description }.joined(separator: "")
        case .element(let elem):
          if elem.name == rootElementName {
            return elem.children.map { $0.description }.joined(separator: "")
          }
          
          return elem.description
        default:
          return ""
        }
      }
    }
    
    extension IndexingError: CustomStringConvertible {
      /// The description for the `IndexingError`.
      public var description: String {
        switch self {
        case .Attribute(let attr):
          return "XML Attribute Error: Missing attribute [\"\(attr)\"]"
        case .AttributeValue(let attr, let value):
          return "XML Attribute Error: Missing attribute [\"\(attr)\"] with value [\"\(value)\"]"
        case .Key(let key):
          return "XML Element Error: Incorrect key [\"\(key)\"]"
        case .Index(let index):
          return "XML Element Error: Incorrect index [\"\(index)\"]"
        case .Init(let instance):
          return "XML Indexer Error: initialization with Object [\"\(instance)\"]"
        case .Error:
          return "Unknown Error"
        }
      }
    }
    
    /// Models content for an XML doc, whether it is text or XML
    public protocol XMLContent: CustomStringConvertible { }
    
    /// Models a text element
    public class TextElement: XMLContent {
      /// The underlying text value
      public let text: String
      init(text: String) {
        self.text = text
      }
    }
    
    public struct XMLAttribute {
      public let name: String
      public let text: String
      init(name: String, text: String) {
        self.name = name
        self.text = text
      }
    }
    
    /// Models an XML element, including name, text and attributes
    public class XMLElement: XMLContent {
      /// The name of the element
      public let name: String
      
      // swiftlint:disable line_length
      /// The attributes of the element
      @available(*, deprecated, message: "See `allAttributes` instead, which introduces the XMLAttribute type over a simple String type")
      public var attributes: [String:String] {
        var attrMap = [String: String]()
        for (name, attr) in allAttributes {
          attrMap[name] = attr.text
        }
        return attrMap
      }
      // swiftlint:enable line_length
      
      /// All attributes
      public var allAttributes = [String: XMLAttribute]()
      
      public func attribute(by name: String) -> XMLAttribute? {
        return allAttributes[name]
      }
      
      /// The inner text of the element, if it exists
      public var text: String? {
        return children
          .map({ $0 as? TextElement })
          .compactMap({ $0 })
          .reduce("", { $0 + $1.text })
      }
      
      /// All child elements (text or XML)
      public var children = [XMLContent]()
      var count: Int = 0
      var index: Int
      
      var xmlChildren: [XMLElement] {
        return children.map { $0 as? XMLElement }.compactMap { $0 }
      }
      
      /**
       Initialize an XMLElement instance
       
       - parameters:
       - name: The name of the element to be initialized
       - index: The index of the element to be initialized
       */
      init(name: String, index: Int = 0) {
        self.name = name
        self.index = index
      }
      
      /**
       Adds a new XMLElement underneath this instance of XMLElement
       
       - parameters:
       - name: The name of the new element to be added
       - withAttributes: The attributes dictionary for the element being added
       - returns: The XMLElement that has now been added
       */
      func addElement(_ name: String, withAttributes attributes: NSDictionary) -> XMLElement {
        let element = XMLElement(name: name, index: count)
        count += 1
        
        children.append(element)
        
        for (keyAny, valueAny) in attributes {
          if let key = keyAny as? String,
            let value = valueAny as? String {
            element.allAttributes[key] = XMLAttribute(name: key, text: value)
          }
        }
        
        return element
      }
      
      func addText(_ text: String) {
        let elem = TextElement(text: text)
        
        children.append(elem)
      }
    }
    
    extension TextElement: CustomStringConvertible {
      /// The text value for a `TextElement` instance.
      public var description: String {
        return text
      }
    }
    
    extension XMLAttribute: CustomStringConvertible {
      /// The textual representation of an `XMLAttribute` instance.
      public var description: String {
        return "\(name)=\"\(text)\""
      }
    }
    
    extension XMLElement: CustomStringConvertible {
      /// The tag, attributes and content for a `XMLElement` instance (<elem id="foo">content</elem>)
      public var description: String {
        var attributesString = allAttributes.map { $0.1.description }.joined(separator: " ")
        if !attributesString.isEmpty {
          attributesString = " " + attributesString
        }
        
        if !children.isEmpty {
          var xmlReturn = [String]()
          xmlReturn.append("<\(name)\(attributesString)>")
          for child in children {
            xmlReturn.append(child.description)
          }
          xmlReturn.append("</\(name)>")
          return xmlReturn.joined(separator: "")
        }
        
        if text != nil {
          return "<\(name)\(attributesString)>\(text!)</\(name)>"
        } else {
          return "<\(name)\(attributesString)/>"
        }
      }
    }
    
    // Workaround for "'XMLElement' is ambiguous for type lookup in this context" error on macOS.
    //
    // On macOS, `XMLElement` is defined in Foundation.
    // So, the code referencing `XMLElement` generates above error.
    // Following code allow to using `SWXMLhash.XMLElement` in client codes.
    extension SWXMLHash {
      public typealias XMLElement = SWXMLHashXMLElement
    }
    
    public  typealias SWXMLHashXMLElement = XMLElement
    
    13. UIColor+Theme.swift
    
    import UIKit
    
    extension UIColor {
      static var themeGreenColor: UIColor {
        return UIColor(red: 0.0, green: 104/255.0, blue: 55/255.0, alpha: 1)
      }
    }
    
    14. DateParser.swift
    
    import Foundation
    
    struct DateParser {
      static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US")
        return formatter
      }()
      
      //Wed, 04 Nov 2015 21:00:14 +0000
      static func dateWithPodcastDateString(_ dateString: String) -> Date? {
        dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
        return dateFormatter.date(from: dateString)
      }
      
      static func displayString(for date: Date) -> String {
        dateFormatter.dateFormat = "HH:mm MMMM dd, yyyy"
        return dateFormatter.string(from: date)
      }
    }
    
    15. PodcastCacheLoader.swift
    
    import Foundation
    
    public class PodcastCacheLoader {
      let cacheManager = DiskCacheManager()
      
      public init() {}
      
      public func savePodcastItems(_ podcastItems: [PodcastItem]) {
        CoreDataManager.shared.savePodcastItems(podcastItems)
      }
    }
    
    16. DiskCacheManager.swift
    
    import Foundation
    
    class DiskCacheManager {
      let groupIdentifier = "<#group identifier here#>"
      let databaseName = "Wendercast.sqlite"
      
      var groupDirectoryLocation: URL {
        guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) else {
          preconditionFailure("Invalid group configuration")
        }
        
        return containerURL
      }
      
      var databaseURL: URL {
        return groupDirectoryLocation.appendingPathComponent(databaseName)
      }
    }
    
    17. Notification+Name.swift
    
    import Foundation
    
    extension Notification.Name {
      static let appEnteringForeground = Notification.Name("com.app.foregrouns")
    }
    
    18. ImageDownloader.swift
    
    import UIKit
    
    public class ImageDownloader {
      public static let shared = ImageDownloader()
      
      private init () { }
      
      public func downloadImage(forURL url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
          if let error = error {
            completion(.failure(error))
            return
          }
          
          guard let data = data else {
            completion(.failure(DownloadError.emptyData))
            return
          }
          
          guard let image = UIImage(data: data) else {
            completion(.failure(DownloadError.invalidImage))
            return
          }
          
          completion(.success(image))
        }
        
        task.resume()
      }
    }
    
    19. NetworkError.swift
    
    import Foundation
    
    public enum DownloadError: Error {
      case emptyData
      case invalidImage
    }
    

    后记

    本篇主要讲述了APNs从工程配置到自定义通知UI全流程解析,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

          本文标题:系统推送的集成(十八) —— APNs从工程配置到自定义通知UI

          本文链接:https://www.haomeiwen.com/subject/osjrohtx.html