iOS13-新特性(PDF/Search/Menus)

未經(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)定了才開始寫這篇文章。

目錄

  • PDF
  • 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)看下效果。

注.我將pdf轉(zhuǎn)成了jpeg

怎么樣是不是挺不錯(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 中 tableViewcollectionView 都增加雙指滑動(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)用順序。

fullScreen

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

pageSheet

當(dāng)A控制器 present B控制器,A控制器的 viewWillDisappearviewDidDisappear 不會(huì)調(diào)用,當(dāng)B控制器 dismiss,A控制器的 viewWillAppearviewDidAppear 也不會(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 搭配使用,onmixed 的區(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 怎么適配夜間模式可以閱讀這篇文章。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 掌握 UIScrollView的常見屬性 UIScrollView的常用代理方法 UIScrollView的縮放 ...
    JonesCxy閱讀 2,880評(píng)論 1 12
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,685評(píng)論 1 32
  • 【感想】:自己就存在認(rèn)知偏差這種觀念。在沒(méi)有參加媛創(chuàng)讀書會(huì)之前,由于很少讀書,自己的世界觀存在一定的偏差。每天面對(duì)...
    西風(fēng)瘦馬_25c2閱讀 430評(píng)論 0 0
  • 最近導(dǎo)師出差一周,我和脫了韁的野馬一樣開心又緊張,一邊在宿舍聽歌一邊處理屎一樣的數(shù)據(jù),一邊在影院看狂暴巨獸一邊擔(dān)心...
    二爺周記閱讀 1,700評(píng)論 10 2
  • 做攻略、定旅館、買車票, READY,GO! 網(wǎng)購(gòu)的是周六上午6:30福州到廈門的動(dòng)車票,5:20就睡不著了,起床...
    懶懶的無(wú)尾熊閱讀 634評(píng)論 0 5

友情鏈接更多精彩內(nèi)容