版本记录
版本号 | 时间 |
---|---|
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中的内容
下面就是源码了
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全流程解析,感兴趣的给个赞或者关注~~~