SiriKit框架詳細解析(九) —— 構建Siri Shortcuts簡單示例(三)
版本記錄
版本號 | 時間 |
---|---|
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](https://i.iter01.com/images/77639d993ec3d385c7d6ff27147729e59bbbe202c4b461a9ca44ebe22f44d33c.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](https://i.iter01.com/images/2863c89616bfd6acfc17da450a86da270f7140607b63680297548d54e3a7d152.png)
相關文章
- iOS12 Siri Shortcuts詳解iOS
- iOS 12 中的 Siri Shortcuts 簡介iOS
- CoreLocation框架詳細解析框架
- Hadoop Yarn框架詳細解析HadoopYarn框架
- Qt構建與解析Json示例QTJSON
- 工作流Activiti框架中表單的使用!詳細解析內建表單和外接表單的渲染框架
- SSM三大框架整合詳細教程SSM框架
- github的詳細使用,非常簡單!Github
- 工作流引擎Activiti使用進階!詳細解析工作流框架中高階功能的使用示例框架
- 構建最簡單陣列陣列
- Hadoop 學習系列(三)之 YARN 詳細解析HadoopYarn
- 資料結構知識框架【超詳細】資料結構框架
- RxLifecycle詳細解析
- Metal框架詳細解析(十九) —— 工具、分析和除錯之GPU活動監視器(三)框架除錯GPU
- 用Keras框架構建一個簡單的卷積神經網路!Keras框架架構卷積神經網路
- Linux Centos 7安裝MongoDB(簡單!詳細!)LinuxCentOSMongoDB
- startViewTransition的簡單示例View
- 構建框架框架
- MVC 三層架構案例詳細講解MVC架構
- 微服務架構技術棧:程式設計師必須掌握的微服務架構框架詳細解析微服務架構程式設計師框架
- Semaphore最詳細解析
- 使用React構建精簡版本掘金(三)React
- SSM三大框架整合詳細教程(Spring+SpringMVC+MyBatis)SSM框架SpringMVCMyBatis
- Mybatis原始碼簡單解讀----構建MyBatis原始碼
- WGCNA構建基因共表達網路詳細教程GC
- Zookeeper詳細使用解析!分散式架構中的協調服務框架最佳選型實踐分散式架構框架
- C# superSocket簡單示例C#
- 構建自己的簡單微服務架構(開源)微服務架構
- Android UI——SpannableString詳細解析AndroidUI
- scala模式匹配詳細解析模式
- MySQL:排序(filesort)詳細解析MySql排序
- 淘寶詳情api介面獲取的方式及簡單示例API
- 利用BT構建簡單的CDN網路
- 用 Java 構建簡單的規則引擎Java
- 超詳細解析微服務架構,寫得太好了!微服務架構
- 三分鐘帶你解析Hive中的json字串(詳細!)HiveJSON字串
- Redis學習記錄三:資料型別(常用:詳細解析)Redis資料型別
- UINavigationBar簡單解析UINavigation