未經(jīng)授權(quán),禁止轉(zhuǎn)載
原文:http://m.itdecent.cn/p/01cda53e2fc8
轉(zhuǎn)眼間 WWDC 19 已經(jīng)過(guò)去1個(gè)多月了,這篇文章本應(yīng)該很早就寫的,但是有些代碼 beta1-beta4 一個(gè) beta 變一次 API,而且之前幾個(gè) beta 部分初始化方法還是以 __ 開頭的私有方法(無(wú)力吐槽),所以拖到現(xiàn)在 beta4 API 基本穩(wěn)定了才開始寫這篇文章。
目錄
- Gestures
- Presentations
- Search
- Menus
PDF(長(zhǎng)圖)
如果你已經(jīng)升到 iOS 13 你會(huì)發(fā)現(xiàn)當(dāng)你在 Safari 中截圖后有一個(gè) “整頁(yè)” 的功能,可以把當(dāng)前的 HTML 轉(zhuǎn)成 PDF 存到 “文件” 中。那么你可能會(huì)想了,這個(gè)新特性雨窩無(wú)瓜啊,我又不做瀏覽器的 App。其實(shí)我們可以把 scrollView 轉(zhuǎn)成 image,再把 image 轉(zhuǎn)成 PDF,這樣我們就可以把這個(gè) scrollView 做成一個(gè)長(zhǎng)圖了,我們先來(lái)看下效果。

怎么樣是不是挺不錯(cuò)的,接下來(lái)就讓我們來(lái)看看這是怎么實(shí)現(xiàn)的。
首先我們要在控制器中實(shí)現(xiàn) UIScreenshotServiceDelegate 代理,由于 iOS 13 項(xiàng)目結(jié)構(gòu)發(fā)生了變化,這里列出兩種設(shè)置代理的方式。
// iOS 13項(xiàng)目結(jié)構(gòu)
let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.windowScene?.screenshotService?.delegate = self
// iOS13之前項(xiàng)目結(jié)構(gòu)
UIApplication.shared.keyWindow?.windowScene?.screenshotService?.delegate = self
UIScreenshotServiceDelegate 代理只有一個(gè)方法,讓我們來(lái)實(shí)現(xiàn)它
func screenshotService(_ screenshotService: UIScreenshotService, generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) {
completionHandler(getScreenshotData(tableView), 0, CGRect.zero)
}
我們看一下這個(gè)回調(diào),第一個(gè)參數(shù)是 PDF 的 data 數(shù)據(jù),第二個(gè)參數(shù)是 PDF 頁(yè)面的索引,第三個(gè)參數(shù)是 PDF 中相對(duì)于當(dāng)前頁(yè)面的坐標(biāo)。getScreenshotData 是我自己寫的方法,方法里的邏輯是 scrollView → image → PDF → data,由于代碼不多,而且都能在網(wǎng)上找到,就不貼出來(lái)了。
說(shuō)一下思路,scrollView 轉(zhuǎn)成 image 的原理是 scrollView.frame = CGRect(origin: .zero, size: scrollView.contentSize)
注意:如果是 tableView 的話會(huì)導(dǎo)致所有 cell 都被加載出來(lái),如果當(dāng)前控制器是一個(gè)無(wú)限列表,請(qǐng)不要使用這個(gè)功能。
Gestures
雙指滑動(dòng)手勢(shì)
iOS 13 中 tableView 和 collectionView 都增加雙指滑動(dòng)編輯的功能,在短信和備忘錄中都使用這個(gè)功能,接下來(lái)我們來(lái)看下效果。

這個(gè)功能體驗(yàn)上也是很爽的,如果你的 App 中有相應(yīng)的場(chǎng)景,建議加上這個(gè)功能,下面讓我們一起來(lái)看看怎么實(shí)現(xiàn)這個(gè)效果。
首先設(shè)置 tableView.allowsMultipleSelectionDuringEditing = true 允許多選,然后實(shí)現(xiàn)兩個(gè)代理方法。
/// 是否允許多指選中
optional func tableView(_ tableView: UITableView, shouldBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath) -> Bool
///多指選中開始,這里可以做一些UI修改,比如修改導(dǎo)航欄上按鈕的文本
optional func tableView(_ tableView: UITableView, didBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath)
最后當(dāng)用戶選擇完,要做某些操作的時(shí)候,我們可以用 tableView.indexPathsForSelectedRows 獲取用戶選擇的 rows。
編輯手勢(shì)
- 復(fù)制:三指捏合
- 剪切:兩次三指捏合
- 粘貼:三指松開
- 撤銷:三指向左劃動(dòng)(或三指雙擊)
- 重做:三指向右劃動(dòng)
- 快捷菜單:三指單擊
iOS 13 增加了一些文本編輯的手勢(shì),這些手勢(shì)系統(tǒng)默認(rèn)會(huì)提供,如果我們想要禁用這些手勢(shì),需要重寫 editingInteractionConfiguration 屬性,代碼如下。
override var editingInteractionConfiguration: UIEditingInteractionConfiguration {
return .none
}
Presentations
iOS 13 下 present 的效果改成了這個(gè)樣子。

這樣帶來(lái)了新的交互方式,下拉就可以 dismiss 控制器,實(shí)測(cè)這是個(gè)很爽的功能,體驗(yàn)大幅度提升,但是對(duì)我們開發(fā)者來(lái)說(shuō)呢,帶來(lái)了一些坑,下面讓我們來(lái)看看吧。
首先 UIModalPresentationStyle 增加了一個(gè) automatic 屬性,在 iOS 13 下默認(rèn)就是這個(gè)屬性。系統(tǒng)會(huì)根據(jù)推出的控制器來(lái)選擇是 pageSheet 還是 fullScreen,比如當(dāng)我們用 UIImagePickerController 推出相機(jī)是 fullScreen,我們自己寫的控制器是 pageSheet。如果我們只想推出 fullScreen 的控制器也很簡(jiǎn)單,present 之前設(shè)置 vc.modalPresentationStyle = .fullScreen 就好了。
接下來(lái)說(shuō)一下 pageSheet 的坑是什么,我們先來(lái)看下 fullScreen 的調(diào)用順序。

再來(lái)看下
pageSheet 的調(diào)用順序。

當(dāng)A控制器 present B控制器,A控制器的 viewWillDisappear 和 viewDidDisappear 不會(huì)調(diào)用,當(dāng)B控制器 dismiss,A控制器的 viewWillAppear 和 viewDidAppear 也不會(huì)調(diào)用。也就是說(shuō)如果你有一些邏輯是放在這4個(gè)方法中的,要么把業(yè)務(wù)邏輯換個(gè)地方,要么設(shè)置 vc.modalPresentationStyle = .fullScreen。
另外,UIViewController 增加一個(gè)了屬性 isModalInPresentation,默認(rèn)為 false,當(dāng)該屬性為 false 時(shí),用戶下拉可以 dismiss 控制器,為 true 時(shí),下拉不可以 dismiss控制器。該屬性可以配合有編輯功能的控制器使用,讓我們來(lái)看下官方的 Demo

我們可以看到,未編輯內(nèi)容時(shí)下拉可以 dismiss,編輯了內(nèi)容后下拉不可以dismiss,同時(shí)彈出了一個(gè) alert 提示用戶要不要保存編輯過(guò)的內(nèi)容。詳細(xì)的代碼大家可以去 Demo 里看,這里就簡(jiǎn)單說(shuō)一下。
首先判斷用戶是否輸入,有輸入將 isModalInPresentation 改為 true。然后實(shí)現(xiàn) UIAdaptivePresentationControllerDelegate 代理的 presentationControllerDidAttemptToDismiss: 方法。這個(gè)方法會(huì)在 isModalInPresentation = true,且用戶嘗試下拉 dismiss 控制器時(shí)調(diào)用。最后在這個(gè)方法里彈出 alert 提示用戶是否保存編輯過(guò)的內(nèi)容即可。
Search
iOS 13 下 UISearchViewController 結(jié)構(gòu)如下。

我們先來(lái)說(shuō)下
UISearchBar 的變化,現(xiàn)在我們可以在 UISearchBar 中獲取到 UISearchTextField 了,可以修改 field 的顏色、字體等,代碼如下。
let field = searchController.searchBar.searchTextField
field.textColor = UIColor.label
field.font = UIFont.systemFont(ofSize: 20)
其次增加了 Token 功能,Token 可以被復(fù)制、粘貼和拖拽,Token 還具有以下特點(diǎn):
1.始終在普通文本前面;
2.可以被選中和刪除;
3.可以和普通文本一起被選中;

接下來(lái)我們看下如何創(chuàng)建
Token,我們有兩種創(chuàng)建 Token 的方式,代碼如下。
// 第一種方式,直接創(chuàng)建一個(gè) Token
let field = searchController.searchBar.searchTextField
field.insertToken(UISearchToken(icon: nil, text: "Token"), at: 0)
// 第二種方式,選擇一段文本,將其變成 Token,過(guò)程如圖
let field = searchController.searchBar.searchTextField
guard let selectedTextRange = field.selectedTextRange, !selectedTextRange.isEmpty else { return }
guard let selectedText = field.text(in: selectedTextRange) else { return } // "beach"
let token = UISearchToken(icon: nil, text: selectedText)
field.replaceTextualPortion(of: selectedTextRange, with: token, at: field.tokens.count)

此外,系統(tǒng)還提供
textualRange 屬性,來(lái)獲取普通文本的長(zhǎng)度。

最后介紹一下 showsSearchResultsController 屬性,該屬性可以控制是否展示搜索結(jié)果控制器。
Menus
還記得我在文章開頭說(shuō)有些 API 一個(gè) beta 改一次嘛...沒(méi)錯(cuò)就是它 UIMenu 每個(gè) beta 寫法都不一樣(吃棗藥丸)我們先來(lái)看下效果。

我們分析一下動(dòng)圖里的結(jié)構(gòu),如圖

我們可以看到 UIMenu 可以嵌套 UIAction 也可以再嵌套 UIMenu,下面讓我們一起來(lái)看看這是怎么實(shí)現(xiàn)的。
首先創(chuàng)建一個(gè) UIContextMenuInteraction 對(duì)象,將它加到對(duì)應(yīng)的 view 上。
let menuInteraction = UIContextMenuInteraction(delegate: self)
menuView.addInteraction(menuInteraction)
其次實(shí)現(xiàn) UIContextMenuInteractionDelegate 代理,配置 UIMenu。
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
// 需要展示的控制器
return ViewController2()
}) { (list) -> UIMenu? in
let editMenu = UIMenu(title: "Edit...", image: nil, identifier: nil, options: [], children: [
UIAction(title: "Copy", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
print("Copy")
}),
UIAction(title: "Duplicate", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
print("Duplicate")
})
])
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
UIAction(title: "Share", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
print("Share")
}),
editMenu,
UIAction(title: "Delete", image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off, handler: { (_) in
print("Delete")
})
])
}
}
代碼有點(diǎn)多,但是不難,我們一點(diǎn)點(diǎn)來(lái)分析,另外圖片相關(guān)的代碼我刪掉了,沒(méi)太多意義還會(huì)影響閱讀體驗(yàn)。
首先這個(gè)方法要求我們返回一個(gè) UIContextMenuConfiguration 對(duì)象,這個(gè)對(duì)象的初始化方法有3個(gè)參數(shù),第一個(gè)是 identifier,第二個(gè)是一個(gè)閉包,要求返回要展示的控制器,第三個(gè)也是個(gè)閉包,要求返回 UIMenu 對(duì)象。
UIMenu
接下來(lái)我們看下 UIMenu 創(chuàng)建的過(guò)程, 首先創(chuàng)建了 editMenu 也就是動(dòng)圖中第二欄,點(diǎn)擊之后會(huì)再?gòu)棾鰞蓚€(gè) UIAction,然后讓我們看看怎么創(chuàng)建 UIMenu。
init(title: String,
image: UIImage? = nil,
identifier: UIMenu.Identifier? = nil,
options: UIMenu.Options = [],
children: [UIMenuElement] = [])
這里我們主要說(shuō)下 options 參數(shù),UIMenu.Options 聲明如下。
public struct Options : OptionSet {
public init(rawValue: UInt)
/// Show children inline in parent, instead of hierarchically
public static var displayInline: UIMenu.Options { get }
/// Indicates whether the menu should be rendered with a destructive appearance in its parent
public static var destructive: UIMenu.Options { get }
}
options 參數(shù)是用于第二層 menu 的,我們可以看到動(dòng)圖中的 Delete 是紅色的,那是因?yàn)樗?UIAction 而且有對(duì)應(yīng)的屬性可以設(shè)置,那么如果我想把 Edit... 弄成成紅色就要設(shè)置 options = destructive。再說(shuō)下 displayInline,這個(gè)效果是把第二層 menu 放到第一層來(lái)展示,效果如下。

細(xì)心的小伙伴可能發(fā)現(xiàn),options 是一個(gè) OptionSet 意味著可以同時(shí)設(shè)置兩個(gè)屬性,那么設(shè)置兩個(gè)屬性會(huì)有什么效果呢,答案是:只有 displayInline 的效果,做成 OptionSet 應(yīng)該是為將來(lái)拓展用的,目前是沒(méi)什么用的。
UIAction
接下來(lái)我們來(lái)看看 UIAction 的初始化方法。
init(title: String,
image: UIImage? = nil,
identifier: UIAction.Identifier? = nil,
discoverabilityTitle: String? = nil,
attributes: UIMenuElement.Attributes = [],
state: UIMenuElement.State = .off,
handler: @escaping UIActionHandler)
前三個(gè)參數(shù)就不說(shuō)了,第四個(gè)參數(shù) discoverabilityTitle 這個(gè)參數(shù)我目前沒(méi)有研究出來(lái)是干嘛用的,如果有知道的小伙伴歡迎在評(píng)論區(qū)留言。
第五個(gè)參數(shù) attributes,我們先來(lái)看下聲明和效果圖。
public struct Attributes : OptionSet {
public init(rawValue: UInt)
public static var disabled: UIMenuElement.Attributes { get }
public static var destructive: UIMenuElement.Attributes { get }
public static var hidden: UIMenuElement.Attributes { get }
}

attributes 也是 OptionSet 可以多個(gè)一起用,但是這幾個(gè)組合都沒(méi)用。
第六個(gè)參數(shù) state,一樣先看聲明和效果圖。
public enum State : Int {
case off
case on
case mixed
}

state 可以和 attributes 搭配使用,on 和 mixed 的區(qū)別我目前沒(méi)找到,另外如果 UIAction 設(shè)置了圖片同時(shí)設(shè)置了 state = .on 則會(huì)把圖片覆蓋掉,只留下一個(gè)勾勾。第七個(gè)參數(shù)是個(gè)閉包,當(dāng)用戶點(diǎn)擊后會(huì)進(jìn)入回調(diào),處理相應(yīng)的邏輯即可。
最后我們把
UIAction 和 editMenu 一起放到一個(gè)新的 UIMenu 中就可以達(dá)到動(dòng)圖中的效果了。
以上是 iOS 13 部分新特性的介紹,如有錯(cuò)誤歡迎指出。
WWDC鏈接 Modernizing Your UI for iOS 13
如果你想知道 iOS 13 怎么適配夜間模式可以閱讀這篇文章。