iOS13 & WWDC2019

1,015 阅读11分钟

iOS13-NSHipster

苹果今年在WWDC上宣布了很多内容。在会议期间以及随后的日子里,我们很多人发呆,到处走动,试图从头脑中恢复“#hashtag-mindblown’d”。但是现在几个月后,在所有宣布的消息发布之后(嗯,几乎所有消息),这些完全相同的基调宣布现在引起了截然不同的反应:

iOS13暗黑模式 ? Already covered it.

SwiftUI? 参考WWDC2019

AR增强现实? 当苹果发布AR眼睛,叫醒我们

尽管大部分注意力都集中在上述功能上,但对iOS 13的其余部分却没有给予足够的报道—这真是令人遗憾,因为此版本在新功能和满意度方面是最令人兴奋的改善现有功能。

因此,为了纪念上周发布的iOS 13,我们来看看一些现在可以在您的应用中使用的晦涩(大部分未记录)的API。我们从iOS 13发行说明API差异中找出了最好的东西,现在向您展示。

从iOS 13开始,您可以执行以下一些我们喜欢的操作:

生成丰富的URLs表示

iOS 13中的新增功能LinkPresentation框架提供了一种便捷的内置方法来复制您在消息中看到的丰富的URL预览。 如果您的应用程序具有任何类型的聊天或消息传递功能,那么您一定要检查一下。

丰富的URL预览至少可以追溯到00年代,因为语义网络先驱者使用了微格式,并且Digg和Reddit的早期先驱使用khtml2png来生成网页的缩略图,因此历史丰富。 随着社交媒体和用户生成内容的兴起,快进到2010年,当时Facebook创建了OpenGraph协议,以允许网络发布者自定义其页面在Newsfeed上的外观。

如今,大多数网站都在其站点上可靠地带有OpenGraph 标记,这些标记可为社交网络,搜索引擎以及其他任何链接流量的内容提供其内容的摘要。 例如,如果您对此网页进行“查看源代码”,就会看到以下内容:

<meta property="og:site_name" content="NSHipster" />
<meta property="og:image" content="https://nshipster.com/logo.png" />
<meta property="og:type" content="article" />
<meta property="og:title" content="iOS 13" />
<meta property="og:url" content="https://nshipster.com/ios-13/" />
<meta property="og:description" content="To mark last week's release of iOS 13, we're taking a look at some obscure (largely undocumented) APIs that you can now use in your apps." />

如果您想在应用程序中使用此信息,则现在可以使用LinkPresentation框架的LPMetadataProvider类来获取元数据,并可以选择构造表示形式:

import LinkPresentation

let metadataProvider = LPMetadataProvider()
let url = URL(string: "https://nshipster.com/ios-13/")!

metadataProvider.startFetchingMetadata(for: url) { [weak self] metadata, error in
    guard let metadata = metadata else { return }

    let linkView = LPLinkView(metadata: metadata)
    self?.view.addSubview(linkView)
}

设置适当的约束后(可能会调用sizeToFit()),您将获得以下内容,用户可以点击以下内容预览链接的网页:

In the startFetchingMetadata(for:) completion handler, you can inspect and mutate the metadata retrieved from the server. You might take this opportunity to add default images / videos for pages that don’t have them, translate text into one of the user’s preferred languages, or censor explicit text and media.

或者,如果您已经拥有应用程序内的元数据,或者无法或不希望远程获取元数据,则可以直接构造LPLinkMetadata:

let metadata = LPLinkMetadata()
metadata.url = url
metadata.title = "iOS 13"
metadata.iconProvider = ...

let linkView = LPLinkView(metadata: metadata)

执行设备上的语音识别: Speech Recognition

SFSpeechRecognizer 在iOS 13中进行了重大升级-最著名的是它对设备上语音识别的新增支持。

以前,转录需要连接互联网,并且每天最多只能有1分钟的时间限制。 但是现在,您可以完全在设备上和离线进行语音识别,而没有任何限制。 唯一需要注意的是,脱机转录不如通过服务器连接获得的那样好,并且仅适用于某些语言。

要确定离线转录是否可用于用户的语言环境,请检查SFSpeechRecognizer属性supportsOnDeviceRecognition。 在发布时,支持的语言列表如下:

English United States (en-US) Canada (en-CA)
Great Britain (en-GB) India (en-IN)
Chinese Mandarin (zh-cmn) Cantonese (zh-yue)
Spanish United States (es-US) Mexico (es-MX) Spain (es-ES)
Italian Italy (it-IT)
Portuguese Brazil (pt-BR)
Russian Russia (ru-RU)
Turkish Turkey (tr-TR)
根据iOS 13发行说明:**supportsOnDeviceRecognition**属性在首次访问时始终返回false。 几秒钟后,再次访问它会返回正确的值。

但这还不是iOS 13中语音识别的全部功能! SFSpeechRecognizer现在可以提供信息,包括讲话速度和平均暂停持续时间,以及语音分析功能,例如抖动(音高变化)和微光(振幅变化)。

import Speech

guard SFSpeechRecognizer.authorizationStatus() == .authorized,
    let recognizer = SFSpeechRecognizer()
else {
    fatalError()
}

let url: URL = ...
let request = SFSpeechURLRecognitionRequest(url: url)

recognizer.recognitionTask(with: request) { (result, error) in
    guard let result = result else { return }

    for segment in result.bestTranscription.segments {
        guard let voiceAnalytics = segment.voiceAnalytics else { continue }

        let pitch = voiceAnalytics.pitch.acousticFeatureValuePerFrame
        let voicing = voiceAnalytics.voicing.acousticFeatureValuePerFrame
        let jitter = voiceAnalytics.jitter.acousticFeatureValuePerFrame
        let shimmer = voiceAnalytics.shimmer.acousticFeatureValuePerFrame
    }
}

您的应用程序可能会使用有关音高,发声和其他功能的信息(可能与CoreML配合使用),以区分说话者或确定说话者音调的亚文本。

发送和接收Web Socket 信息

说到“the Foundation URL Loading System”,我们已经对许多年来一直是我们最希望的东西提供了本地支持:web sockets。

多亏了iOS 13中新的 URLSessionWebSocketTask 类,您现在可以像发送HTTP请求一样轻松,可靠地将实时通信合并到您的应用程序中,而无需任何第三方库或框架,如下方示例:

let url = URL(string: "wss://...")!
let webSocketTask = URLSession.shared.webSocketTask(with: url)
webSocketTask.resume()

// Send one message
let message: URLSessionWebSocketTask.Message = .string("Hello, world!")
webSocketTask.send(message) { error in
    ...
}

// Receive one message
webSocketTask.receive { result in
    guard case let .success(message) = result else { return }
    ...
}

// Eventually...
webSocketTask.cancel(with: .goingAway, reason: nil)

要对Web套接字(包括客户端和服务器支持)进行较低级别的控制,请查看Network framework

Do More With Maps

MapKit是Apple SDK的另一个组成部分,在WWDC中年复一年地表现出色。 在我们的日常工作中,影响最大的往往往往是细微的接触。

例如,iOS 13中新的 MKMapView.CameraBoundary API使将地图的视口限制到特定区域变得更加容易,而无需完全锁定它。 示例如下:

let region = MKCoordinateRegion(center: mapView.center,
                                        latitudinalMeters: 1000,
                                        longitudinalMeters: 1000)
mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: region)

借助新的 MKPointOfInterestFilter API,您现在可以自定义地图视图的外观,以仅显示某些类型的兴趣点(而以前这是一个all-or-nothing proposition)。 示例如下:

let filter = MKPointOfInterestFilter(including: [.cafe])
mapView.pointOfInterestFilter = filter // only show cafés

最后,借助 MKGeoJSONDecoder,我们现在有了一种内置的方式,可以从Web服务和其他数据源中提取**GeoJSON 类型**。

示例:

let decoder = MKGeoJSONDecoder()

if let url = URL(string: "..."),
    let data = try? Data(contentsOfURL: url),
    let geoJSONObjects = try? decoder.decode(data) {

    for case let overlay as MKOverlay in geoJSONObjects {
        mapView.addOverlay(overlay)
    }
}

Keep Promises in JavaScript

如果您喜欢我们有关 JavaScriptCore的文章,您会很高兴知道JSValue对象现在本身就支持promise。

对于未初始化的对象:在JavaScript中,Promise是一个对象,代表异步操作的最终完成(或拒绝)及其结果值。 承诺是现代JS开发的支柱-也许在fetch API中最为明显。

iOS 13中JavaScriptCore的另一个新增功能是对符号的支持(不,不是那些符号)。

有关 init(newSymbolFromDescription:in :) 的更多信息,请参考文档,只是猜测如何使用它。

Respond to Objective-C Associated Objects (?)

不知所措,我们决定看看今年的Objective-C是否有新内容,并且很惊讶地发现关于 objc_setHook_setAssociatedObject 的信息。 同样,除了声明外,我们没有什么其他需要做的事情,但是看起来您现在可以将一个块配置为在设置关联对象时执行。 对于那些仍然沉迷于Objective-C运行时的人来说,这听起来似乎很方便。

Tame Activity Items (?)

关于缺少文档的主题:UIActivityItemsConfiguration 似乎是用于在新的iOS 13共享表中管理操作的引人注目的选项,但我们真的不知道从哪里开始。

遗憾的是,我们没有足够的信息来利用这一点。

Format Lists and Relative Times

如前一篇文章*Formatter*所述,iOS 13为基础带来了两个新的格式化程序:ListFormatterRelativeDateTimeFormatter

不必为此担心,但是它们仍然没有文档记录,因此,如果您想了解更多信息,建议您从7月开始查看该文章。 或者,如果您很着急,这里有一个简单的示例,演示如何将它们一起使用:

import Foundation

let relativeDateTimeFormatter = RelativeDateTimeFormatter()
relativeDateTimeFormatter.dateTimeStyle = .named

let listFormatter = ListFormatter()
listFormatter.string(from: [
    relativeDateTimeFormatter.localizedString(from: DateComponents(day: -1)),
    relativeDateTimeFormatter.localizedString(from: DateComponents(day: 0)),
    relativeDateTimeFormatter.localizedString(from: DateComponents(day: 1))
]) // "yesterday, today, and tomorrow"

Track the Progress of Enqueued Operations

从iOS 13开始,OperationQueue 现在具有 progress属性

当然,(NS)Progress对象并不是最直接或最方便的处理对象(我们一直有意写一篇有关它们的文章),但是它们具有完整且经过深思熟虑的API,甚至 应用程序框架中的一些便捷插槽。

例如,检查连接一个UIProgressView以便通过其observedProgress属性显示操作队列的实时更新进度有多么容易:

import UIKit

fileprivate class DownloadOperation: Operation { ... }

class ViewController: UIViewController {
    private let operationQueue = {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
    }()

    @IBOutlet private var progressView: UIProgressView!

    @IBAction private func startDownloading(_ sender: Any) {
        operationQueue.cancelAllOperations()
        progressView.observedProgress = operationQueue.progress

        for url in [...] {
            let operation = DownloadOperation(url: url)
            operationQueue.addOperation(operation)
        }
    }
}

还值得一提的还有13种其他API

例如schedule(after:interval:tolerance:options:_:)(可以很方便地将OperationQueue引入新的Combine框架)和 addBarrierBlock()

就像Dispatch barrier blocks一样工作(尽管没有文档,这是任何人的猜测)。

轻松管理后台任务:Background Tasks

通常,将类别定义应用程序与竞争对手区分开来的一件事是,他们使用后台任务来确保应用程序在下次进入前台时完全同步和更新。

iOS 7是第一个提供用于调度后台任务的官方API的版本(尽管在此之前开发人员已经采用了各种创造性的方法)。 但是在随后的几年中,从iOS应用程序功能和复杂性的增加,到对应用程序性能,效率和隐私的日益重视,多种因素催生了对更全面解决方案的需求。

该解决方案通过新的BackgroundTasks framework进入了iOS 13。

正如今年WWDC会议“Advances in App Background Execution”所述,该框架区分了两大类后台任务:

  • 应用刷新任务:短期任务,可以使应用全天保持最新状态
  • 后台处理任务:执行可延期维护任务的长期任务

WWDC会话和随附的示例代码项目在解释如何将这两项都集成到您的应用程序方面做得很好。 但是,如果您需要它的要点,这是一个小示例,该应用计划从Web服务器安排定期刷新:

import UIKit
import BackgroundTasks

fileprivate let backgroundTaskIdentifier = "com.nshipster.example.task.refresh"

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    lazy var backgroundURLSession = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.nshipster.url-session.background")
        configuration.discretionary = true
        configuration.timeoutIntervalForRequest = 30

        return URLSession(configuration: configuration, delegate: ..., delegateQueue: ...)
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
       ...

        BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }

        return true
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        scheduleAppRefresh()
    }

    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier)
        request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 10)

        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print("Couldn't schedule app refresh: \(error)")
        }
    }

    func handleAppRefresh(task: BGAppRefreshTask) {
        scheduleAppRefresh()

        let url: URL = ...
        var dataTask = backgroundURLSession.dataTask(with: url) { (data, response, error) in
            ...
            let success = (200..<300).contains(response?.statusCode)
            task.setTaskCompleted(success: success)
        }

        task.expirationHandler = {
            dataTask.cancel()
        }

        dataTask.resume()
    }

    ...
}

现在,iOS 13中已弃用了以前进行后台更新的方法,即UIApplication.setMinimumBackgroundFetchInterval(_ :)和UIApplicationDelegate.application(_:performFetchWithCompletionHandler :)。

Annotate Text Content Types for Better Accessibility

您知道听到某些人读出URL会令人沮丧吗? (“eɪʧti ti pi ˈkoʊlənslæʃslæʃˈdʌbəlju ˈdʌbəljudʌbəljudɑt”。)这就是VoiceOver试图阅读某些内容而又不了解其阅读内容的感觉。

iOS 13承诺通过新的accessibilityTextualContext属性和UIAccessibilityTextAttributeContext NSAttributedString属性键可以大大改善这种情况。 只要有可能,请确保使用最能描述所显示文本类型的常量注释视图和属性字符串:

  • UIAccessibilityTextualContextConsole
  • UIAccessibilityTextualContextFileSystem
  • UIAccessibilityTextualContextMessaging
  • UIAccessibilityTextualContextNarrative
  • UIAccessibilityTextualContextSourceCode
  • UIAccessibilityTextualContextSpreadsheet
  • UIAccessibilityTextualContextWordProcessing

For more information, see the WWDC 2019 session “Creating an Accessible Reading Experience”.

Remove Implicitly Unwrapped Optionals from View Controllers Initialized from Storyboards

SwiftUI可能已预告了Storyboard的终结,但这并不意味着事情不会并且直到/那天到来之前不会继续变得更好。

在使用Storyboard进行iOS项目时,Swift纯粹主义者最讨厌的反模式之一就是视图控制器初始化。由于Interface Builder的“准备进行数据存储”方法与Swift的对象初始化规则之间存在阻抗不匹配,我们经常不得不在使所有属性都是非私有,可变和(隐式解包)可选属性之间还是完全在故事板之前进行选择。

Xcode 11和iOS 13允许这些范例通过新的@IBSegueAction属性和一些新的UIStoryboard类方法来协调它们的差异:

首先,可以将**@IBSegueAction**属性应用到视图控制器方法声明中,以将其自身指定为负责创建Segue的目标视图控制器的API(即,prepare(for:sender :)方法中segue参数的destinationViewController属性)。 示例:

@IBSegueAction
func makeProfileViewController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ProfileViewController? {
    ProfileViewController(
        coder: coder,
        name: self.selectedName,
        avatarImageURL: self.selectedAvatarImageURL
    )
}

Second, the UIStoryboard class methods instantiateInitialViewController(creator:) and instantiateViewController(identifier:creator:) offer a convenient block-based customization point for instantiating a Storyboard’s view controllers.

import UIKit

struct Person { ... }

class ProfileViewController: UIViewController {
    let name: String
    let avatarImageURL: URL?

    init?(coder: NSCoder, name: String, avatarImageURL: URL?) {
        self.name = name
        self.avatarImageURL = avatarImageURL

        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

let storyboard = UIStoryboard(name: "ProfileViewController", bundle: nil)
storyboard.instantiateInitialViewController(creator: { decoder in
    ProfileViewController(
        coder: decoder,
        name: "Johnny Appleseed",
        avatarImageURL: nil
    )
})

Together with the new UIKit Scene APIs, iOS 13 gives us a lot to work with as we wait for SwiftUI to mature and stabilize.

这样做是为了弥补您可能错过的iOS 13功能。 但是请放心-我们计划在以后的NSHipster文章中介绍更多新的API。