初めに
WWDCの動画を視聴し、内容についてまとめていきます。
内容
- Meet Swift Async Algorithms
前提知識として、Concurrency
の AsyncSequence
を用いることで、非同期の値に対しても Sequence
のように for in
ループを使うことができました。(らしいです)
以下の例では公式ドキュメントで紹介されているカスタム Counter
を使用しています。AsyncSequence
を使用する場合は makeAsyncIterator()
の実装が必要で、AsyncIteratorProtocol
を使用する場合は next()
の実装が必要です。
for await i in Counter(howHigh: 10) {
print(i, terminator: " ")
}
// Prints: 1 2 3 4 5 6 7 8 9 10
また、map, filter, reduceなどの関数も提供されていました。
let stream = Counter(howHigh: 10)
.map { $0 % 2 == 0 ? "Even" : "Odd" }
2022年3月に Apple が swift-async-algorithms
の Beta 版をリリースしました。(2022年7月時点)
SPM でのダウンロードが可能で、AsyncSequence
を拡張した機能などが提供されています。
for try await (video, preview) in zip(videos, previews) {
try await upload(video, preview)
}
for try await message in merge(primaryAccount.messages, secondaryAccount.messages) {
displayPreview(message)
}
他にもたくさんの機能がありますが、インクリメンタルサーチや??秒ごとに送信などの実装が簡単にできるようになります。
インクリメンタルサーチの例
let channel = AsyncChannel<SearchResult>()
for await query in queryValue.debounce(for: .milliseconds(300)) {
let results = try await performSearch(query)
channel.send(results)
}
// 0.5秒ごとに送信する
let batches = outboundMessages.chunked(
by: .repeating(every: .milliseconds(500))
)
let encoder = JSONEncoder()
for await batch in batches {
let data = try encoder,encode(batch)
try await postToServer(data)
}
- Meet Swift Regex
ついにSwiftに正規表現がやってきました。文字列操作の幅が広がり、容易になるでしょう。
(競プロでも使えるかな?)
動画で紹介されていた例を紹介します。以下のような台帳から要素を一つずつ取り出したい場合を考えます。
let ledgers = """
KIND DATE INSITUTION AMOUNT
----------------------------------------------------------
CREDIT 03/02/2022 Payroll from employer $200.23
CREDIT 03/03/2022 Suspect A $2,000,000.00
DEBIT 06/03/2022 Doug's Dugout Dogs £57.33
"""
データの1行目だけに着目すると、以下のようなコードでデータ要素を取り出せます。
import RegexBuilder
import Foundation
let ledger = "CREDIT 03/02/2022 payroll from employer $200.23"
let fieldSeparator = /\s{2,}|\t/ // 2つ以上のスペース、もしくはタブ文字
let transactionMatcher = Regex {
Capture { /CREDIT|DEBIT/ }
fieldSeparator
Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: TimeZone(secondsFromGMT: 0)!)) }
fieldSeparator
Capture {
OneOrMore {
CharacterClass.any
}
}
fieldSeparator
Capture { One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) }
}
// transactionMatcher: Regex<(Substring, Substring, Date, Substring, Decimal)>
if let match = ledger.firstMatch(of: transactionMatcher) {
let (wholeMatch, kind, date, institution, amount) = match.output
print(wholeMatch) // CREDIT 03/02/2022 payroll from employer $200.23
print(kind) // CREDIT
print(date) // 2022-03-02 00:00:00 +0000
print(institution) // payroll from employer
print(amount) // 200.23
}
RegexBuilder
を import
することで SwiftUI
の ViewBuilder
のように書けます。
Capture { ... }
の部分が output
の型として定義されるといった感じです。(結構大変ですね...)
- Swift Regex: Beyond the basics
Regex
の初期化には3種類があります。
let input = "name: John Appleseed, user_id: 100"
// `user_id`を抽出する例
let regex1 = try Regex(#"user_id:\s*(\d+)"#)
let regex2 = /user_id:\s*(\d+)/
let regex3 = Regex {
"user_id:"
OneOrMore(.whitespace)
Capture(.localizedInteger(locale: .init(identifier: "en_US")))
}
他にもいろいろな関数があるヨ
let input = "name: John Appleseed, user_id: 100"
let regex = /user_id:\s*(\d+)/
input.firstMatch(of: regex) // Regex.Match<(Substring, Substring)>
input.wholeMatch(of: regex) // nil
input.prefixMatch(of: regex) // nil
input.starts(with: regex) // false
input.replacing(regex, with: "456") // "name: John Appleseed, 456"
input.trimmingPrefix(regex) // "name: John Appleseed, user_id: 100"
input.split(separator: /\s*,\s*/) // ["name: John Appleseed", "user_id: 100"]
また、Capture
の transform
クロージャを使うと Substring
以外の戻り値を返すことも可能です。
let input = "My answer is fuga."
enum MetaVars: String {
case hoge
case fuga
case piyo
}
let regex = Regex {
OneOrMore(.any)
TryCapture {
ChoiceOf {
"hoge"
"fuga"
"piyo"
}
} transform: {
MetaVars(rawValue: String($0))
}
Optionally(".")
}
if let match = try? regex.firstMatch(in: input) {
let (wholeMatch, metaVar) = match.output
print(wholeMatch) // My answer is fuga.
print(metaVar) // fuga
print(type(of: metaVar)) // MetaVars
}
- Embrace Swift generics
そろそろ記事を書くのも読むのも疲れてきたので筋トレでもしましょう。まずはトレーニング種目の定義から...
protocol Big3 {}
struct Squat: Big3 {}
struct Deadlift: Big3 {}
struct BenchPress: Big3 {}
Big3 は欠かせませんから、トレーニーを定義するなら既存の generics を使って以下の様になるでしょう。
struct Trainee {
func workout<Menu>(_ menu: Menu) where Menu: Big3 { ... }
// もしくは
func workout<Menu: Big3>(_ menu: Menu) { ... }
}
Opaque Result Type
を使うと、上の例がシンプルに書けます。
struct Trainee {
func workout(_ menu: some Big3) { ... }
}
次に、脳筋とそうない人のメニューを比較してみます。
let noukinMenus: [some Big3] = [Squat(), Squat(), Squat()]
let smartMenus: [any Big3] = [Squat(), Deadlift(), BenchPress()]
脳筋は(たぶん)スクワットしかやらないので [some Big3]
で問題ないですが、賢い人はさまざまなメニューを取り入れているため [any Big3]
とする必要があります。
some
は「どれかひとつ」、any
は「どれでも」というイメージです。そのため、some Big3
に複数の型を代入しようとするとエラーになります。
var menu: some Big3 = Squat()
menu = Deadlift() // エラー
let smartMenus: [some Big3] = [Squat(), Deadlift(), BenchPress()] // エラー
また、動画ではデフォルトでは some
を使いましょうという注意がありました。
- The SwiftUI cookbook for navigation
まずは Navigation
の遷移に関するセッションです。従来の SwiftUI
では NavigationLink
を使用して遷移先の画面を指定していました。この際、isActive
に Binding<Bool>
を渡すことで遷移イベントを発火させていました。
var body: some View {
NavigationLink(
"Details",
isActive: $item.showDetail
) { DetailView() }
}
iOS16 からの新しい NavigationLink
では、初期化時に path データを渡すことで画面遷移がしやすくなります。
@State private var path: [Recipe] = []
var body: some View {
NavigationStack(path: $path) {
List(Category.allCases) { category in
Section(category.localizedName) {
ForEach(dataModel.recipes(in: category)) { recipe in
NavigationLink(recipe.name, value: recipe)
}
}
}
.navigationTitle("Categories")
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetails(recipe: recipe)
}
}
}
以下のように path 変数を変更することで遷移できるようになります。
func showRecipeOfTheDay() {
path = [dataModel.recipeOfTheDay]
}
func popToRoot() {
path.removeAll()
}
- Compose custom layouts with SwiftUI
Case 1
まずは Grid layout
の登場です。下図のように、2つのラベルと ProgressView
といった3つの要素からなる3列のレイアウトを考えます。
VStack
と HStack
を組み合わせるてゴリゴリ頑張るのが従来の手法だと思いますが、iOS16 からは以下のようにレイアウトが組めるようになります。
@State private var pets: [Pet] = Pet.exampleData
var body: some View {
Grid(alignment: .leading) {
ForEach(pets) { pet in
GridRow {
Text(pet.type)
ProgressView(
value: Double(pet.votes),
total: Double(totalVotes)
)
Text("\(pet.votes)")
.gridColumnAlignment(.trailing)
}
}
}
}
Grid
と GridRow
が新しく使えるようになります。
よく見るとただ要素が並んでいるだけでなく、ペット名が左寄せになり、値ラベルが右寄せになっています。VStack
と HStack
では spacing
など細かい調整が必要になりそうなので、これは便利ですね!ちなみに、alignment
は何も指定しなければ .center
になります。
Case 2
次に、下図のような3つの要素を持ったレイアウトを考えます。最も大きい要素に合わせて、全て等しいサイズにしたいといったものです。中身が変化する場合は動的にサイズを変更する必要があるため、従来の SwiftUI
ではかなり難しい?というか無理じゃないかと思います。
iOS16 からは Layout
プロトコルを使うことでこのレイアウトを実現できます。
@State private var pets: [Pet] = Pet.exampleData
var body: some View {
EqualWidthHStack {
ForEach($pets) { $pet in
Button {
pet.votes += 1
} label: {
Text(pet.type)
.frame(maxWidth: .infinite)
}
.buttonStyle(.bordered)
}
}
}
struct EqualWidthHStack: Layout {
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Void
) -> CGSize {
/* 略 */
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Void
) {
/* 略 */
}
}
Layout の詳細な実装コードはこちら
struct EqualWidthHStack: Layout {
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Void
) -> CGSize {
let maxSize = maxSize(subviews: subviews)
let spacing = spacing(subviews: subviews)
let totalSpacing = spacing.reduce(0) { $0 + $1 }
return CGSize(
width: maxSize.width * CGFloat(subviews.count) + totalSpacing,
height: maxSize.height
)
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Void
) {
let maxSize = maxSize(subviews: subviews)
let spacing = spacing(subviews: subviews)
let sizeProposal = ProposedViewSize(
width: maxSize.width,
height: maxSize.height
)
var x = bounds.minX + maxSize.width / 2
for index in subviews.indices {
subviews[index].place(
at: CGPoint(x: x, y: bounds.midY),
anchor: .center,
proposal: sizeProposal
)
x += maxSize.width + spacing[index]
}
}
private func maxSize(subviews: Subviews) -> CGSize {
let subviewSizes = subview.map { $0.sizeThatFits(.unspecified) }
return subviewSizes.reduce(.zero) { currentMax, subviewSize in
CGSize(
width: max(currentMax.width, subviewSize.width),
height: max(currentMax.height, subviewSize.height)
)
}
}
private func spacing(subviews: Subviews) -> CGSize {
return subviews.indices.map { index in
guard index < subviews.count - 1 else { return 0 }
return subviews[index].spacing?.distance(
to: subviews[index + 1].spacing,
along: .horizontal
)
}
}
}
Layout
プロトコルには
-
sizeThatFits(proposal:subviews:cache:)
: ビューのサイズ計算 -
placeSubviews(in:proposal:subviews:cache:)
: 指定の位置にビューを配置
の2つの必須メソッドがあります。サイズ計算や座標計算など面倒なことは増えましたが、その分レイアウトの幅がぐんと広がりそうな予感がします。(ドキュメントはこちら)
- Use SwiftUI with UIKit
UIHostingController
に sizingOptions
プロパティが追加されました。
// Presenting UIHostingController as a popover
let heartRateView = HeartRateView()
let hostingController = UIHostingController(rootView: heartRateView)
//Enable automatic preferredContentSize updates on the hosting controller
hostingController.sizingOptions = .preferredContentSize
hostingController.modalPresentationStyle = .popover
self.present(hostingController, animated: true)
まだイマイチ理解できていませんが、SwiftUI
のビューが更新されたときに、自動でリサイズしてくれるようになるらしいです。
次は UIHostingConfiguration
です。ついに CollectionView
と TableView
のセルを SwiftUI
で書けるようになりました。(ドキュメントはこちら)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.contentConfiguration = UIHostingConfiguration {
CustomCell()
}
return cell
}
struct CustomCell: View {
var body: some View {
Text("custom")
}
}
今まで通り UIKit
の CollectionView
を使い、セルの部分のみを SwiftUI
に切り離せるようになります。
SwiftUI
でバックグラウンドからアプリを起動できるようになります。今回の例は毎日正午に天気情報を取得し、嵐だったら写真を撮るようにプッシュ通知を送るというもの。(なぜ嵐なのか)
従来の手法では、バックグラウンドでアプリを動かすためには AppDelegate
や SceneDelegate
に記述する必要がありましたが、これを SwiftUI
からできるようになります。
import SwiftUI
import BackgroundTasks
@main
struct StormyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.backgroundTask(.appRefresh("StormyNoon")) { // New!!
scheduleAppRefresh()
// 嵐だったらプッシュ通知を送信する
if await isStormy() {
await notifyForPhoto()
}
}
}
}
// 毎日正午にバックグラウンドでアプリを開くようにする
func scheduleAppRefresh() {
let today = Calendar.current.startOfDay(for: .now)
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)
let noonComponent = DateComponents(hour: 12)
let noon = Calendar.current.date(byAdding: noonComponent, to: tomorrow)
let request = BGAppRefreshTaskRequest(identifier: "StormyNoon")
request.earliestBeginDate = noon
try? BGTaskScheduler.shared.submit(request)
}
- What's new in WKWebView
最後は WKWebView
の新機能です。動画やキャンバスゲームをフルスクリーンで表示させたい場合、WKPreferences
の isElementFullscreenEnabled
で可能になりました。(iOS15.4からの機能らしい)
また、iOS16 からは fullscreenState
が追加されて、状態の変更を監視できるようになりました。
// Fullscreen API support
webView.configuration.preferences.isElementFullscreenEnabled = true
webView.loadHTMLString("""
<script>
button.addEventListener('click', () => {
canvas.webkitRequestFullscreen()
}, false);
</script>
...
""", baseURL: nil)
let observation = webView.observe(\.fullscreenState, options: [.new]) { object, change in
print("fullscreenState: \(object.fullscreenState)")
}
次は、キーワード検索です。Chrome の ⌘+F
でできるキーワード検索が WKWebView
内でもできるようになります。
webView.findInteractionEnabled = true
if let interaction = webView.findInteraction {
interaction.presentFindNavigator(showingReplace: false)
}
UIFindInteraction を利用し、presentFindNavigator
を呼ぶことで検索バーを表示することができます。
- What's new in Xcode
まずは Xcode14 のアップデート内容です。ここは簡潔にまとめます。
- 起動時間が30%速くなった🤩
- Preview のキャンバスがデフォルトでインタラクティブになり、すぐに触って動かせる
- initializer の補完(プロパティを1つずつ定義しなくて済む)
- ビルド時間が25%速くなった🤩
その他の変更としては、下画像のように initializer で複数の引数がある場合、必要なものだけを選んで残りはデフォルトのままで、ということができるようになります。
また、クラスや関数が長い場合にファイルの上部に追従して表示してくれるようになります。特定クラスのテストだけ動かしたい場合とかにいちいちファイルの先頭まで移動しなくて済むので地味にありがたい...
- Get to know Developer Mode
iOS16 と watchOS 9 の新しいモードとして、Developer Mode
というのが追加されます。
Developer Mode, introduced in iOS 16 and watchOS 9, protects people from inadvertently installing potentially harmful software on their devices, and reduces attack vectors exposed by developer-only functionality.
iOS 16とwatchOS 9で導入されたデベロッパーモードは、潜在的に有害なソフトウェアを不用意にデバイスにインストールすることから人々を保護し、開発者限定機能によってさらされる攻撃ベクトルを低減します。
上記の通り、Developer Mode
は開発用機能を攻撃から守るためのシステムです。主に、自分のデバイスでローカル開発(実機ビルド)する際に必要になります。
一方で、
- Test Flight からのダウンロード
- Enterprise(In-House) distribution
- App Store からのダウンロード
では必要ありません。
Developer Mode
は、「XcodeとiPhoneを接続 -> iPhoneで設定 -> Privacy & Security -> Developer Modeをオンにする」
という手順で有効化できます。
- What’s new in iPad app design
最後は iPadOS 16 での新機能の紹介です。
まずはツールバー。レイアウトが変わり、中央セクションの要素をカスタマイズできるようになりました!(画像奥がiPadOS 15、手前がiPadOS 16)
次は、検索と置換です。システムキーボードに検索と置換が内蔵されているため、特定の単語やフレーズを素早く検索できます!
続いて、Appナビゲーションです。「ブラウザスタイル」という新しいスタイルのナビゲーションが追加されます。
例えば、ファイル App では戻る・進むボタンでサイドバーの別の場所にあるフォルダー間を簡単にブラウジングできるようになります。
次は、選択メニューです。iPadOS 16では、バンドセレクションをしても自動で編集モードになることがなくなります。
選択した要素は、クリックすることでまとめて操作できます。
続いてサブメニューです。iPhone ではサブメニューは縦向きに開きます。スペースが限られていて、タップしなければいけないこともあり、本当に必要な場合にのみサブメニューを使用することが最適でした。
iPadOS 16 では、サブメニューはスペースがあれば横向きに展開されるようになります。
最後はテーブルです。今までのテーブルは単一の列だけでしたが、iPadOS 16 では複数列で情報を表示できるようになります。さらにヘッダーをタップすることでソートできるようになります。
終わりに
アップデートたくさんありましたね!(まだまだたくさんある...)
続きがあれば追記したいと思います!
ご指摘などあればmm