SiriKit框架詳細解析(九) —— 構建Siri Shortcuts簡單示例(三)

weixin_33890499發表於2018-12-06

版本記錄

版本號 時間
V1.0 2018.12.06 星期四

前言

大家都知道隨著人工智慧的發展,會掀起來另外一個工業革命,而語音識別就是人工智慧的初始階段,但是每個公司做的都不一樣,涉及到一系列的語音的採集和演算法實現,蘋果的Siri就是業界語音識別的代表性的產品。接下來的幾篇我們就詳細解析一下SiriKit這個框架。感興趣的可以看下面幾篇文章。
1. SiriKit框架詳細解析(一)—— 基本概覽(一)
2. SiriKit框架詳細解析(二)—— 請求授權使用SiriKit和INPreferences類(一)
3. SiriKit框架詳細解析(三)—— 建立Intents App擴充套件(一)
4. SiriKit框架詳細解析(四)—— 構建程式碼以支援App擴充套件和將意圖排程到處理物件(一)
5. SiriKit框架詳細解析(五) —— 程式設計指南之Intents和Intents UI擴充套件(一)
6. SiriKit框架詳細解析(六) —— 程式設計指南之確認和處理請求、指定自定義詞彙表和介面(一)
7. SiriKit框架詳細解析(七) —— 構建Siri Shortcuts簡單示例(一)
8. SiriKit框架詳細解析(八) —— 構建Siri Shortcuts簡單示例(二)

原始碼

1. Swift

首先看一下程式碼結構

3691932-a4f3d76500a311c9.png

下面看一下原始碼

1. AppDelegate.swift
import UIKit
import IntentsUI
import ArticleKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  var nav: UINavigationController?
  
  let rootVC = ArticleFeedViewController()
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    nav = UINavigationController(rootViewController: rootVC)
    window?.rootViewController = nav
    window?.makeKeyAndVisible()
    
    ArticleManager.loadArticles()
    return true
  }
  
  func applicationWillResignActive(_ application: UIApplication) {
    ArticleManager.writeArticlesToDisk()
  }
  
  func applicationWillEnterForeground(_ application: UIApplication) {
    ArticleManager.loadArticles()
    rootVC.viewWillAppear(false)
  }
  
  func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
    if userActivityType == "com.razeware.NewArticle" {
      return true
    }
    
    return false
  }
  
  func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard userActivity.interaction == nil else  {
      ArticleManager.loadArticles()
      rootVC.viewWillAppear(false)
      return false
    }
    
    let vc = NewArticleViewController()
    nav?.pushViewController(vc, animated: false)
    return true
  }
}
2. ArticleFeedViewController.swift
import UIKit
import ArticleKit
import Intents
import CoreSpotlight
import MobileCoreServices

class ArticleFeedViewController: UIViewController {
  let tableView = UITableView(frame: .zero, style: .plain)
  let cellReuseIdentifier = "kArticleCellReuse"
  
  var articles: [Article] = []
  
  @objc func newArticleWasTapped() {
    let vc = NewArticleViewController()
    
    // Create and donate an activity-based Shortcut
    let activity = Article.newArticleShortcut(with: UIImage(named: "notePad.jpg"))
    vc.userActivity = activity
    activity.becomeCurrent()
    
    navigationController?.pushViewController(vc, animated: true)
  }
  
  // MARK: - Initialization
  
  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nil, bundle: nil)
    
    tableView.delegate = self
    tableView.dataSource = self
    tableView.register(ArticleTableViewCell.classForCoder(), forCellReuseIdentifier: cellReuseIdentifier)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

extension ArticleFeedViewController: UITableViewDelegate {
  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let vc = EditDraftViewController(article: articles[indexPath.row])
    navigationController?.pushViewController(vc, animated: true)
  }
  
  func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
  }
  
  func remove(article: Article, at indexPath: IndexPath) {
    ArticleManager.remove(article: article)
    articles = ArticleManager.allArticles()
    tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
    
    INInteraction.delete(with: article.title) { _ in
    }
  }
}
3. NewArticleViewController.swift
import UIKit
import IntentsUI
import ArticleKit

class NewArticleViewController: UIViewController {
  let titleTextField = UITextField()
  let contentsTextView = UITextView()
  let addShortcutButton = UIButton()
  
  @objc func saveWasTapped() {
    if let title = titleTextField.text, let content = contentsTextView.text {
      let article = Article(title: title, content: content, published: false)
      ArticleManager.add(article: article)
      
      // Donate publish intent
      article.donatePublishIntent()
      navigationController?.popViewController(animated: true)
    }
  }
  
  @objc func addNewArticleShortcutWasTapped() {
    // Open View Controller to Create New Shortcut
    let newArticleActivity = Article.newArticleShortcut(with: UIImage(named: "notePad.jpg"))
    let shortcut = INShortcut(userActivity: newArticleActivity)
    
    let vc = INUIAddVoiceShortcutViewController(shortcut: shortcut)
    vc.delegate = self
    
    present(vc, animated: true, completion: nil)
  }
}

extension NewArticleViewController: INUIAddVoiceShortcutViewControllerDelegate {
  func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController,
                                      didFinishWith voiceShortcut: INVoiceShortcut?,
                                      error: Error?) {
    dismiss(animated: true, completion: nil)
  }
  
  func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) {
    dismiss(animated: true, completion: nil)
  }
}
4. EditDraftViewController.swift
import UIKit
import ArticleKit

class EditDraftViewController: UIViewController {
  let titleTextField = UITextField()
  let contentsTextView = UITextView()
  
  var article: Article
  
  init(article: Article) {
    self.article = article
    super.init(nibName: nil, bundle: nil)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  @objc func saveWasTapped() {
    if let title = titleTextField.text, let content = contentsTextView.text {
      article = ArticleManager.update(article: article, title: title, content: content)
      
      navigationController?.popViewController(animated: true)
    }
  }
  
  @objc func publishWasTapped() {
    if let title = titleTextField.text, let content = contentsTextView.text {
      article = ArticleManager.update(article: article, title: title, content: content)
      ArticleManager.publish(article)
      navigationController?.popViewController(animated: true)
    } else {
      // Show alert
      
    }
  }
}
5. ArticleTableViewCell.swift
import UIKit
import ArticleKit

class ArticleTableViewCell: UITableViewCell {
  let titleLabel = UILabel()
  var publishStatusLabel = UILabel()
  
  var article: Article {
    didSet {
      initializeTitleLabel()
      article.published ? showArticleWasPublished() : showArticleIsDraft()
      setNeedsLayout()
    }
  }
  
  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    article = Article(title: "placeholder", content: "placeholder", published: false)
    publishStatusLabel = ArticleTableViewCell.statusLabel(title: "")
    
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    
    addSubview(titleLabel)
    addSubview(publishStatusLabel)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func layoutSubviews() {
    super.layoutSubviews()
    
    titleLabel.sizeToFit()
    titleLabel.bounds = CGRect(x: 0, y: 0, width: min(titleLabel.bounds.width, 150.0), height: titleLabel.bounds.height)
    titleLabel.center = CGPoint(x: titleLabel.bounds.width/2.0 + 16, y: bounds.height/2.0)
    
    publishStatusLabel.sizeToFit()
    publishStatusLabel.bounds = CGRect(x: 0, y: 0, width: publishStatusLabel.bounds.width + 30, height: publishStatusLabel.bounds.height + 8)
    publishStatusLabel.center = CGPoint(x: bounds.width - publishStatusLabel.bounds.width/2.0 - 16.0, y: bounds.height/2.0)
  }
  
  private func initializeTitleLabel() {
    titleLabel.text = article.title
    titleLabel.font = UIFont.systemFont(ofSize: 24.0)
  }
  
  class func statusLabel(title: String) -> UILabel {
    let label = UILabel()
    
    label.text = title
    label.layer.cornerRadius = 4.0
    label.layer.borderWidth = 1.0
    label.textAlignment = .center;
    return label
  }
  
  func showArticleWasPublished() {
    publishStatusLabel.text = "PUBLISHED"
    
    let green = UIColor(red: 0.0/255.0, green: 104.0/255.0, blue: 55.0/255.0, alpha: 1.0)
    
    publishStatusLabel.textColor = green
    publishStatusLabel.layer.borderColor = green.cgColor
  }
  
  func showArticleIsDraft() {
    publishStatusLabel.text = "DRAFT"
    
    let yellow = UIColor(red: 254.0/255.0, green: 223.0/255.0, blue: 0.0, alpha: 1.0)
    
    publishStatusLabel.textColor = yellow
    publishStatusLabel.layer.borderColor = yellow.cgColor
  }
  
  override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)
  }
}
6. Layouts.swift
import UIKit
import ArticleKit
import Intents

extension ArticleFeedViewController {
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    articles = ArticleManager.allArticles()
    
    tableView.reloadData()
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    navigationItem.title = "Articles"
    navigationItem.rightBarButtonItem = UIBarButtonItem(title: "New Article",
                                                        style: .plain,
                                                        target: self,
                                                        action: #selector(ArticleFeedViewController.newArticleWasTapped))
    tableView.allowsMultipleSelectionDuringEditing = false
    view.addSubview(tableView)
  }
  
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    tableView.frame = view.bounds
  }
}

extension ArticleFeedViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return articles.count
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath) as! ArticleTableViewCell
    cell.article = articles[indexPath.row]
    return cell
  }
  
  func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return 100.0
  }

  func tableView(_ tableView: UITableView,
                 commit editingStyle: UITableViewCell.EditingStyle,
                 forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
      let article = articles[indexPath.row]
      remove(article: article, at: indexPath)
      if articles.count == 0 {
        NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: [NSUserActivityPersistentIdentifier(kNewArticleActivityType)]) {
          print("Successfully deleted 'New Article' activity.")
        }
      }
    }
  }
}


extension NewArticleViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    
    // Shortcuts Button
    addShortcutButton.setTitle("Add Shortcut to Siri", for: .normal)
    addShortcutButton.addTarget(self, action: #selector(NewArticleViewController.addNewArticleShortcutWasTapped), for: .touchUpInside)
    addShortcutButton.setTitleColor(.blue, for: .normal)
    
    // Navbar
    navigationItem.title = "New Article"
    navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save Draft",
                                                        style: .plain,
                                                        target: self,
                                                        action: #selector(NewArticleViewController.saveWasTapped))
    
    // Text Fields
    titleTextField.placeholder = "Title"
    titleTextField.delegate = self
    
    contentsTextView.layer.cornerRadius = 4.0
    contentsTextView.layer.borderColor = UIColor.black.cgColor
    contentsTextView.layer.borderWidth = 1.0
    contentsTextView.delegate = self
    
    view.addSubview(addShortcutButton)
    view.addSubview(titleTextField)
    view.addSubview(contentsTextView)
  }
  
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    let navbarHeight: CGFloat = 44.0
    var topPadding: CGFloat = 20.0
    var bottomPadding: CGFloat = 20.0
    let paddingBetween: CGFloat = 20.0
    
    if let topInset = UIApplication.shared.keyWindow?.safeAreaInsets.top {
      topPadding += topInset
    }
    if let bottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom {
      bottomPadding += bottomInset
    }
    
    addShortcutButton.bounds = CGRect(x: 0, y: 0, width: view.bounds.width, height: 44.0)
    addShortcutButton.center = CGPoint(x: view.bounds.width/2.0, y: titleTextField.bounds.height/2.0 + topPadding + navbarHeight)
    
    titleTextField.bounds = CGRect(x: 0, y: 0, width: view.bounds.width - 32.0, height: 44.0)
    titleTextField.center = CGPoint(x: titleTextField.bounds.width/2.0 + 16.0, y: titleTextField.bounds.height/2.0 + addShortcutButton.center.y + addShortcutButton.bounds.height/2.0)
    
    let contentsTextViewYOrigin = titleTextField.bounds.height + titleTextField.frame.origin.y + 20.0
    let height = view.bounds.height - (titleTextField.center.y + titleTextField.bounds.height/2.0) - paddingBetween - bottomPadding
    contentsTextView.frame = CGRect(x: 16.0, y: contentsTextViewYOrigin, width: view.bounds.width - 32.0, height: height)
  }
  
  override func updateUserActivityState(_ activity: NSUserActivity) {
    guard let title = titleTextField.text, let content = contentsTextView.text else { return }
    
    activity.addUserInfoEntries(from: ["title": title, "content": content])
    
    super.updateUserActivityState(activity)
  }
}

// Since your user activity supports hand-off, updating its user info dictionary means you can easily continue writing your article on another device if you'd like to. Make sure to call needs save so updateUserActivityState(activity:) can be called periodically instead of at each change.

extension NewArticleViewController: UITextFieldDelegate {
  func textFieldDidEndEditing(_ textField: UITextField) {
    userActivity?.needsSave = true
  }
  
  func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    userActivity?.needsSave = true
    
    return true
  }
}

extension NewArticleViewController: UITextViewDelegate {
  func textViewDidChange(_ textView: UITextView) {
    userActivity?.needsSave = true
  }
}

extension EditDraftViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    
    // Navbar
    navigationItem.title = "Edit Draft"
    navigationItem.rightBarButtonItems = [UIBarButtonItem(title: "Publish",
                                                          style: .plain,
                                                          target: self,
                                                          action: #selector(EditDraftViewController.publishWasTapped)),
                                          UIBarButtonItem(title: "Save",
                                                          style: .plain,
                                                          target: self,
                                                          action: #selector(EditDraftViewController.saveWasTapped))]
    
    // Text Fields
    titleTextField.placeholder = "Title"
    titleTextField.text = article.title
    
    contentsTextView.layer.cornerRadius = 4.0
    contentsTextView.layer.borderColor = UIColor.black.cgColor
    contentsTextView.layer.borderWidth = 1.0
    contentsTextView.text = article.content
    
    view.addSubview(titleTextField)
    view.addSubview(contentsTextView)
  }
  
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    let navbarHeight: CGFloat = 44.0
    var topPadding: CGFloat = 20.0
    var bottomPadding: CGFloat = 20.0
    
    if let topInset = UIApplication.shared.keyWindow?.safeAreaInsets.top {
      topPadding += topInset
    }
    if let bottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom {
      bottomPadding += bottomInset
    }
    
    titleTextField.bounds = CGRect(x: 0, y: 0, width: view.bounds.width - 32.0, height: 44.0)
    titleTextField.center = CGPoint(x: titleTextField.bounds.width/2.0 + 16.0, y: titleTextField.bounds.height/2.0 + topPadding + navbarHeight)
    
    let contentsTextViewYOrigin = titleTextField.bounds.height + titleTextField.frame.origin.y + 20.0
    let height = view.bounds.height - navbarHeight - titleTextField.bounds.height - 20 - 20 - bottomPadding
    contentsTextView.frame = CGRect(x: 16.0, y: contentsTextViewYOrigin, width: view.bounds.width - 32.0, height: height)
  }
}
7. IntentHandler.swift
import Intents

class IntentHandler: INExtension {
  override func handler(for intent: INIntent) -> Any {
    return PostArticleIntentHandler()
  }
}
8. PostArticleIntentHandler.swift
import UIKit
import ArticleKit

class PostArticleIntentHandler: NSObject, PostArticleIntentHandling {
  func confirm(intent: PostArticleIntent, completion: @escaping (PostArticleIntentResponse) -> Void) {
    completion(PostArticleIntentResponse(code: PostArticleIntentResponseCode.ready,
                                         userActivity: nil))
  }
  
  func handle(intent: PostArticleIntent, completion: @escaping (PostArticleIntentResponse) -> Void) {
    guard
      let title = intent.article?.identifier,
      let article = ArticleManager.findArticle(with: title)
      else {
        completion(PostArticleIntentResponse.failure(failureReason: "Your article was not found."))
        return
    }
    guard !article.published else {
      completion(PostArticleIntentResponse.failure(failureReason: "This article has already been published."))
      return
    }
    
    ArticleManager.publish(article)
    completion(PostArticleIntentResponse.success(title: article.title, publishDate: article.formattedDate()))
  }
}
9. ArticleKit.h
#import <UIKit/UIKit.h>

//! Project version number for ArticleKit.
FOUNDATION_EXPORT double ArticleKitVersionNumber;

//! Project version string for ArticleKit.
FOUNDATION_EXPORT const unsigned char ArticleKitVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <ArticleKit/PublicHeader.h>
10. Article.swift
import UIKit
import Intents
import CoreSpotlight
import MobileCoreServices

public let kNewArticleActivityType = "com.razeware.NewArticle"

public class Article {
  public let title: String
  public let content: String
  public let published: Bool
  
  public static func newArticleShortcut(with thumbnail: UIImage?) -> NSUserActivity {
    let activity = NSUserActivity(activityType: kNewArticleActivityType)
    activity.persistentIdentifier = NSUserActivityPersistentIdentifier(kNewArticleActivityType)
    
    activity.isEligibleForSearch = true
    activity.isEligibleForPrediction = true
    
    let attributes = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
    
    // Title
    activity.title = "Write a new article"
    
    // Subtitle
    attributes.contentDescription = "Get those creative juices flowing!"
    
    // Thumbnail
    attributes.thumbnailData = thumbnail?.jpegData(compressionQuality: 1.0)
    
    // Suggested Phrase
    activity.suggestedInvocationPhrase = "Time to write!"
    
    activity.contentAttributeSet = attributes
    return activity
  }
  
  // Create an intent for publishing articles
  public func donatePublishIntent() {
    let intent = PostArticleIntent()
    intent.article = INObject(identifier: self.title, display: self.title)
    intent.publishDate = formattedDate()
    
    let interaction = INInteraction(intent: intent, response: nil)
    
    interaction.donate { error in
      if let error = error {
        print("Donating intent failed with error \(error)")
      }
    }
  }
  
  // MARK: - Init
  public init(title: String, content: String, published: Bool) {
    self.title = title
    self.content = content
    self.published = published
  }
  
  // MARK: - Helpers
  public func toData() -> Data? {
    let dict = ["title": title, "content": content, "published": published] as [String: Any]
    let data = try? NSKeyedArchiver.archivedData(withRootObject: dict, requiringSecureCoding: false)
    return data
  }
  
  public func formattedDate() -> String {
    let date = Date()
    let formatter = DateFormatter()
    
    formatter.dateFormat = "MM/dd/yyyy"
    let result = formatter.string(from: date)
    
    return result
  }
}
11. ArticleManager.swift
import UIKit

public class ArticleManager: NSObject {
  private static var articles: [Article] = []
  private static let groupIdentifier = "group.com.razeware.Writing"
  
  private static var articlesDir: String  {
    let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier)
    if groupURL == nil {
      let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
      return documentsDir.path + "/Articles"
    } else {
      return groupURL!.path + "/Articles"
    }
  }

  public static func update(article: Article, title: String, content: String) -> Article {
    let newArticle = Article(title: title, content: content, published: false)
    remove(article: article)
    add(article: newArticle)
    return newArticle
  }
  
  public static func findArticle(with title: String) -> Article? {
    loadArticles()
    return articles.first { $0.title == title }
  }
  
  public static func allArticles() -> [Article] {
    return articles
  }
  
  public static func publish(_ article: Article) {
    let publishedArticle = Article(title: article.title, content: article.content, published: true)
    
    ArticleManager.remove(article: article)
    ArticleManager.add(article: publishedArticle)
    ArticleManager.writeArticlesToDisk()
  }
  
  public static func writeArticlesToDisk() {
    do {
      if !FileManager.default.fileExists(atPath: articlesDir) {
        try FileManager.default.createDirectory(atPath: articlesDir, withIntermediateDirectories: false, attributes: nil)
      }
      
      // Delete all old articles
      let articlePaths = try FileManager.default.contentsOfDirectory(atPath: articlesDir)
      for articlePath in articlePaths  {
        let fullPath = articlesDir + "/\(articlePath)"
        try FileManager.default.removeItem(atPath: fullPath)
        print("Deleted \(articlePath)")
      }
    } catch let e {
      print(e)
    }
    
    for (i, article) in articles.enumerated() {
      let path = articlesDir + "/\(i + 1).article"
      let url = URL(fileURLWithPath: path)
      if let data = article.toData() {
        try? data.write(to: url)
        print("Wrote article \(article.title) published? \(article.published) to \(articlesDir)")
      }
    }
  }
  
  public static func loadArticles() {
    var savedArticles: [Article] = []
    
    do {
      if !FileManager.default.fileExists(atPath: articlesDir) {
        try FileManager.default.createDirectory(atPath: articlesDir, withIntermediateDirectories: false, attributes: nil)
      }
      
      let articlePaths = try FileManager.default.contentsOfDirectory(atPath: articlesDir)
      for articlePath in articlePaths  {
        let fullPath = articlesDir + "/\(articlePath)"
        if let articleData = FileManager.default.contents(atPath: fullPath),
          let articleDict = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(articleData) as? [String: Any],
          let title = articleDict["title"] as? String,
          let content = articleDict["content"] as? String,
          let published = articleDict["published"] as? Bool {
          let article = Article(title: title, content: content, published: published)
          savedArticles.append(article)
        }
      }
    } catch let e {
      print(e)
    }
    
    articles = savedArticles
  }
  
  public static func add(article: Article) {
    articles.append(article)
  }
  
  public static func remove(article articleToDelete: Article) {
    articles.removeAll { article -> Bool in
      article.title == articleToDelete.title && article.content == articleToDelete.content
    }
  }
}

後記

本篇主要介紹了構建Siri Shortcuts簡單示例,感興趣的給個贊或者關注~~~

3691932-f0389761822b79c1.png

相關文章