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

4,463 阅读9分钟

未经授权,禁止转载

原文:juejin.cn/post/684490…

转眼间 WWDC 19 已经过去1个多月了,这篇文章本应该很早就写的,但是有些代码 beta1-beta4 一个 beta 变一次 API,而且之前几个 beta 部分初始化方法还是以 __ 开头的私有方法(无力吐槽),所以拖到现在 beta4 API 基本稳定了才开始写这篇文章。

PDF(长图)

如果你已经升到 iOS 13 你会发现当你在 Safari 中截图后有一个 “整页” 的功能,可以把当前的 HTML 转成 PDF 存到 “文件” 中。那么你可能会想了,这个新特性雨窝无瓜啊,我又不做浏览器的 App。其实我们可以把 scrollView 转成 image,再把 image 转成 PDF,这样我们就可以把这个 scrollView 做成一个长图了,我们先来看下效果。

注.我将pdf转成了jpeg

怎么样是不是挺不错的,接下来就让我们来看看这是怎么实现的。

首先我们要在控制器中实现 UIScreenshotServiceDelegate 代理,由于 iOS 13 项目结构发生了变化,这里列出两种设置代理的方式。

// iOS 13项目结构
let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
scene?.window?.windowScene?.screenshotService?.delegate = self

// iOS 13之前项目结构
UIApplication.shared.keyWindow?.windowScene?.screenshotService?.delegate = self

UIScreenshotServiceDelegate 代理只有一个方法,让我们来实现它

func screenshotService(_ screenshotService: UIScreenshotService, generatePDFRepresentationWithCompletion completionHandler: @escaping (Data?, Int, CGRect) -> Void) {
    completionHandler(getScreenshotData(tableView), 0, CGRect.zero)
}

我们看一下这个回调,第一个参数是 PDF 的 data 数据,第二个参数是 PDF 页面的索引,第三个参数是 PDF 中相对于当前页面的坐标。getScreenshotData 是我自己写的方法,方法里的逻辑是 scrollView → image → PDF → data,由于代码不多,而且都能在网上找到,就不贴出来了。

说一下思路,scrollView 转成 image 的原理是 scrollView.frame = CGRect(origin: .zero, size: scrollView.contentSize)

注意: 如果是 tableView 的话会导致所有 cell 都被加载出来,如果当前控制器是一个无限列表,请不要使用这个功能。

Gestures

双指滑动手势

iOS 13 中 tableViewcollectionView 都增加双指滑动编辑的功能,在短信和备忘录中都使用这个功能,接下来我们来看下效果。

这个功能体验上也是很爽的,如果你的 App 中有相应的场景,建议加上这个功能,下面让我们一起来看看怎么实现这个效果。

首先设置 tableView.allowsMultipleSelectionDuringEditing = true 允许多选,然后实现两个代理方法。

/// 是否允许多指选中
optional func tableView(_ tableView: UITableView, shouldBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath) -> Bool

///多指选中开始,这里可以做一些UI修改,比如修改导航栏上按钮的文本
optional func tableView(_ tableView: UITableView, didBeginMultipleSelectionInteractionAtIndexPath indexPath: IndexPath) 

最后当用户选择完,要做某些操作的时候,我们可以用 tableView.indexPathsForSelectedRows 获取用户选择的 rows。

编辑手势

  • 复制:三指捏合
  • 剪切:两次三指捏合
  • 粘贴:三指松开
  • 撤销:三指向左划动(或三指双击)
  • 重做:三指向右划动
  • 快捷菜单:三指单击

iOS 13 增加了一些文本编辑的手势,这些手势系统默认会提供,如果我们想要禁用这些手势,需要重写 editingInteractionConfiguration 属性,代码如下。

override var editingInteractionConfiguration: UIEditingInteractionConfiguration {
    return .none
}

Presentations

iOS 13 下 present 的效果改成了这个样子。

这样带来了新的交互方式,下拉就可以 dismiss 控制器,实测这是个很爽的功能,体验大幅度提升,但是对我们开发者来说呢,带来了一些坑,下面让我们来看看吧。

首先 UIModalPresentationStyle 增加了一个 automatic 属性,在 iOS 13 下默认就是这个属性。系统会根据推出的控制器来选择是 pageSheet 还是 fullScreen,比如当我们用 UIImagePickerController 推出相机是 fullScreen,我们自己写的控制器是 pageSheet。如果我们只想推出 fullScreen 的控制器也很简单,present 之前设置 vc.modalPresentationStyle = .fullScreen 就好了。

接下来说一下 pageSheet 的坑是什么,我们先来看下 fullScreen 的调用顺序。

fullScreen
再来看下 pageSheet 的调用顺序。

pageSheet

当A控制器 present B控制器,A控制器的 viewWillDisappearviewDidDisappear 不会调用,当B控制器 dismiss,A控制器的 viewWillAppearviewDidAppear 也不会调用。也就是说如果你有一些逻辑是放在这4个方法中的,要么把业务逻辑换个地方,要么设置 vc.modalPresentationStyle = .fullScreen

另外,UIViewController 增加一个了属性 isModalInPresentation,默认为 false,当该属性为 false 时,用户下拉可以 dismiss 控制器,为 true 时,下拉不可以 dismiss控制器。该属性可以配合有编辑功能的控制器使用,让我们来看下官方的 Demo

我们可以看到,未编辑内容时下拉可以 dismiss,编辑了内容后下拉不可以dismiss,同时弹出了一个 alert 提示用户要不要保存编辑过的内容。详细的代码大家可以去 Demo 里看,这里就简单说一下。

首先判断用户是否输入,有输入将 isModalInPresentation 改为 true。然后实现 UIAdaptivePresentationControllerDelegate 代理的 presentationControllerDidAttemptToDismiss: 方法。这个方法会在 isModalInPresentation = true,且用户尝试下拉 dismiss 控制器时调用。最后在这个方法里弹出 alert 提示用户是否保存编辑过的内容即可。

Search

iOS 13 下 UISearchViewController 结构如下。

我们先来说下 UISearchBar 的变化,现在我们可以在 UISearchBar 中获取到 UISearchTextField 了,可以修改 field 的颜色、字体等,代码如下。

let field = searchController.searchBar.searchTextField
field.textColor = UIColor.label
field.font = UIFont.systemFont(ofSize: 20)

其次增加了 Token 功能,Token 可以被复制、粘贴和拖拽,Token 还具有以下特点:

  1. 始终在普通文本前面;
  2. 可以被选中和删除;
  3. 可以和普通文本一起被选中;

接下来我们看下如何创建 Token,我们有两种创建 Token 的方式,代码如下。

// 第一种方式,直接创建一个 Token
let field = searchController.searchBar.searchTextField
field.insertToken(UISearchToken(icon: nil, text: "Token"), at: 0)

// 第二种方式,选择一段文本,将其变成 Token,过程如图
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)

此外,系统还提供 textualRange 属性,来获取普通文本的长度。

最后介绍一下 showsSearchResultsController 属性,该属性可以控制是否展示搜索结果控制器。

Menus

还记得我在文章开头说有些 API 一个 beta 改一次嘛...没错就是它 UIMenu 每个 beta 写法都不一样(吃枣药丸)我们先来看下效果。

我们分析一下动图里的结构,如图

我们可以看到 UIMenu 可以嵌套 UIAction 也可以再嵌套 UIMenu,下面让我们一起来看看这是怎么实现的。 首先创建一个 UIContextMenuInteraction 对象,将它加到对应的 view 上。

let menuInteraction = UIContextMenuInteraction(delegate: self)
menuView.addInteraction(menuInteraction)

其次实现 UIContextMenuInteractionDelegate 代理,配置 UIMenu

let menuInteraction = UIContextMenuInteraction(delegate: self)
menuView.addInteraction(menuInteraction)

其次实现 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")
            })
        ])
    }
}

代码有点多,但是不难,我们一点点来分析,另外图片相关的代码我删掉了,没太多意义还会影响阅读体验。

首先这个方法要求我们返回一个 UIContextMenuConfiguration 对象,这个对象的初始化方法有3个参数,第一个是 identifier,第二个是一个闭包,要求返回要展示的控制器,第三个也是个闭包,要求返回 UIMenu 对象。

UIMenu

接下来我们看下 UIMenu 创建的过程, 首先创建了 editMenu 也就是动图中第二栏,点击之后会再弹出两个 UIAction,然后让我们看看怎么创建 UIMenu

init(title: String, 
     image: UIImage? = nil, 
     identifier: UIMenu.Identifier? = nil, 
     options: UIMenu.Options = [], 
     children: [UIMenuElement] = [])

这里我们主要说下 options 参数,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 参数是用于第二层 menu 的,我们可以看到动图中的 Delete 是红色的,那是因为它是 UIAction 而且有对应的属性可以设置,那么如果我想把 Edit... 弄成成红色就要设置 options = destructive。再说下 displayInline,这个效果是把第二层 menu 放到第一层来展示,效果如下。

细心的小伙伴可能发现,options 是一个 OptionSet 意味着可以同时设置两个属性,那么设置两个属性会有什么效果呢,答案是:只有 displayInline 的效果,做成 OptionSet 应该是为将来拓展用的,目前是没什么用的。

UIAction

接下来我们来看看 UIAction 的初始化方法。

init(title: String, 
     image: UIImage? = nil, 
     identifier: UIAction.Identifier? = nil, 
     discoverabilityTitle: String? = nil, 
     attributes: UIMenuElement.Attributes = [], 
     state: UIMenuElement.State = .off, 
     handler: @escaping UIActionHandler)

前三个参数就不说了,第四个参数 discoverabilityTitle 这个参数我目前没有研究出来是干嘛用的,如果有知道的小伙伴欢迎在评论区留言。

第五个参数 attributes,我们先来看下声明和效果图。

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 可以多个一起用,但是这几个组合都没用。

第六个参数 state,一样先看声明和效果图。

public enum State : Int {
    case off
    case on
    case mixed
}

state 可以和 attributes 搭配使用,onmixed 的区别我目前没找到,另外如果 UIAction 设置了图片同时设置了 state = .on 则会把图片覆盖掉,只留下一个勾勾。

第七个参数是个闭包,当用户点击后会进入回调,处理相应的逻辑即可。

最后我们把 UIAction 和 editMenu 一起放到一个新的 UIMenu 中就可以达到动图中的效果了。


以上是 iOS 13 部分新特性的介绍,如有错误欢迎指出。

WWDC链接 Modernizing Your UI for iOS 13

如果你想知道 iOS 13 怎么适配夜间模式可以阅读这篇文章