目次
- スクロールビューで特定の位置までスクロール
- App Storeで購入可能なアイテムを表示
- App Storeのサブスクリプションを表示
- 写真がNSFWかどうかを分析
- データストレージSwiftDataを使用
- SwiftUIのビュースタイルのためにMetalシェーダーを使用
- マップにマーカーやその他のコンポーネントを追加
- SFシンボルの画像エフェクト(パルス、反転、バウンス、スケール、表示/非表示、トランジション)
- 回転のジェスチャー
- インスペクタビュー(右側に表示されるサイドバー)
- 新しい#previewプレビューブロック
- foregroundStyleスタイルの使用
- TipKitを使ったヒントの表示
- Swift Macro
はじめに
この記事の多くの部分(例えば、SwiftData、センシティブコンテンツ分析など)については、後ほどQiitaの記事でより詳しい解説を書く予定です。
※一般公開されている(一般に開示した情報)WWDC Keynoteの動画と公開Session/Documentationページだけを使ってこの記事を執筆しました。スクリーンショットはWWDCのセッション映像のものを使用しています。Xcodeのベータ版にアクセスできる場合は、自身でコードを実行されることをお勧めします。
ScrollView内で特定の位置までスクロールする
ScrollViewとその内部のスタック構造を使用している場合、
特定の行までビューをスクロールすることができます。
まず、.scrollTargetLayout()というビューモディファイアを、ScrollView内に主要な繰り返しコンテンツを含むレイアウトコンテナに追加します。これはVStack、LazyHStack、HStack、またはForEachなどになります。
import SwiftUI
struct ScrollViewToRow: View {
var body: some View {
VStack {
ScrollView {
ForEach(1..<30, id: \.self) { number in
// ...
}
+ .scrollTargetLayout()
}
}
}
}
この例では、1から30までのすべての数字をループするためにForEachを使用しています。したがって、このビューモディファイアをForEachに追加します。
次に、scrollPositionビューモディファイアを使用してスクロール位置をバインドします。このビューモディファイアはScrollViewに添付されます。
import SwiftUI
struct ScrollViewToRow: View {
+ @State private var scrollPosition: Int? = 0
var body: some View {
VStack {
ScrollView {
ForEach(1..<30, id: \.self) { number in
// ...
}
.scrollTargetLayout()
}
+ .scrollPosition(id: $scrollPosition)
}
}
}
オプションのInt型の@State変数を使用します。
新しい値を割り当てることでスクロール位置を更新することができます。コードをwithAnimationブロック内に配置すると、スクロールアニメーションが表示されます。
現在のスクロール位置を読み取ることもできます。
import SwiftUI
struct ScrollViewToRow: View {
@State private var scrollPosition: Int? = 0
var body: some View {
VStack {
Text("currently at \(scrollPosition ?? -1)")
Button("Scroll") {
withAnimation {
scrollPosition = 10
}
}
ScrollView {
ForEach(1..<30, id: \.self) { number in
HStack {
Text(verbatim: number.formatted())
Spacer()
}
.padding()
.foregroundStyle(.white)
.background {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.teal)
}
}
.scrollTargetLayout()
}
.scrollPosition(id: $scrollPosition)
}
.padding()
}
}
#Preview {
ScrollViewToRow()
}
App Storeのアプリ内購入製品を表示
このビューでは、1つの製品を表示しています。
複数の商品を紹介するスクロールビューや水平スタックなどを作成することができます。
この話題については後で詳しくQiitaの記事を書きます
App Storeから取得した製品IDを使用して、ProductView
を初期化できます。
大きな製品ビュー(上記のBox of Nutrition Pelletsのような)を表示するためには、ビューモディファイア .productViewStyle(.large)
を使用します。
ProductView(id: ids.nutritionPelletBox) {
BoxOfNutritionPelletsIcon()
}
.productViewStyle(.large)
ProductViewのコードブロック内では、製品のアイコン(たとえば、Image)を提供できます。
App Storeのすべてのサブスクリプションを表示
Appleから提供されるプレスタイルのビューを使用して、App Storeのサブスクリプションを表示することができます。
これを最も簡単に表示する方法は、SubscriptionStoreView(groupID: birdPassGroupID)を呼び出すだけで、非常にシンプルなビューが表示されます。
この話題については後で詳しくQiitaの記事を書きます
このビューはカスタマイズ可能で、カスタムヘッダー、背景、アイコンを表示できます。
まず、サブスクリプショングループIDを使用してSubscriptionStoreViewを初期化します。visibleRelationshipsでは、アップグレードのみを表示するか、すべてのオプション(ダウングレードオプションを含む)を表示するかを定義できます。
ブロック内では、PassMarketingContent(オプション)がヘッダーとして表示されます。ここでは、アプリのアイコン、サブスクリプショングループ名、サブスクリプションの利点を簡単に説明できます。
また、.subscriptionStoreControlIcon(オプション)ビューモディファイアを使用して、各サブスクリプションアイテムのアイコンを定義することもできます。
SubscriptionStoreView(
groupID: passGroupID,
visibleRelationships: showPremiumUpgrade ? .upgrade : .all
) {
PassMarketingContent(showPremiumUpgrade: showPremiumUpgrade)
#if !os(watchOS)
.containerBackground(for: .subscriptionStoreFullHeight) {
SkyBackground()
}
#endif
}
#if os(iOS)
.storeButton(.visible, for: .redeemCode)
#else
.frame(width: 400, height: 550)
#endif
.subscriptionStoreControlIcon { _, subscriptionInfo in
Group {
switch PassStatus(levelOfService: subscriptionInfo.groupLevel) {
case .premium:
Image(systemName: "bird")
case .family:
Image(systemName: "person.3.sequence")
default:
Image(systemName: "wallet.pass")
}
}
.foregroundStyle(.accent)
.symbolVariant(.fill)
}
#if !os(watchOS)
.backgroundStyle(.clear)
.subscriptionStoreButtonLabel(.multiline)
.subscriptionStorePickerItemBackground(.thinMaterial)
#endif
SensitiveContentAnalysisを使用して画像を解析
この話題については後で詳しくQiitaの記事を書きます
まず、センシティブコンテンツ(NSFW)アナライザーのエンタイトルメントをプロジェクトに追加する必要があります。
イメージアナライザーを初期化します:
let analyzer = SCSensitivityAnalyzer()
次に、アプリは、ユーザーがセンシティブコンテンツ分析機能をオンにしているかどうかをチェックします。
ユーザーは、システム設定(プライバシーセクション内)でこの機能をオンまたはオフにすることができます。この設定がオフの場合、画像の解析はできません。
switch analyzer.analysisPolicy {
case .simpleInterventions:
Label("Simple UI", systemImage: "checkmark")
Text("Blur the image, and show a button to show the content.")
case .descriptiveInterventions:
Label("Detailed UI", systemImage: "checkmark")
Text("Show a detailed description on how these types of images might affect the user.")
case .disabled:
Label("Not enabled", systemImage: "xmark")
Text("To analyze a photo, turn on the sensitive photo warning first in system settings.")
}
その後、CGImage
オブジェクトに変換することで、画像を解析することができます
func analyzePhoto(input: CGImage) {
Task {
let response = try await analyzer.analyzeImage(input)
self.isImageSensitive = response.isSensitive
}
}
以下は、フォトピッカーも含むSwiftUIのコードです。
import SwiftUI
import SensitiveContentAnalysis
import PhotosUI
struct ContentView: View {
let analyzer = SCSensitivityAnalyzer()
@State private var pickedPhotoToAnalyze: PhotosPickerItem?
@State private var analyzedImage: UIImage?
@State private var isImageSensitive: Bool?
var body: some View {
Form {
Section("Status") {
VStack(alignment: .leading) {
Text("Warning enabled in system settings")
.font(.headline)
switch analyzer.analysisPolicy {
case .simpleInterventions:
Label("Simple UI", systemImage: "checkmark")
Text("Blur the image, and show a button to show the content.")
case .descriptiveInterventions:
Label("Detailed UI", systemImage: "checkmark")
Text("Show a detailed description on how these types of images might affect the user.")
case .disabled:
Label("Not enabled", systemImage: "xmark")
Text("To analyze a photo, turn on the sensitive photo warning first in system settings.")
}
}
}
.id(analyzer.analysisPolicy.rawValue)
Section("Image analysis") {
PhotosPicker("Pick a photo to analyze", selection: $pickedPhotoToAnalyze)
.onChange(of: pickedPhotoToAnalyze) { newValue in
if let newValue {
handlePickedImageItem(newValue)
}
}
if let isImageSensitive {
if isImageSensitive {
Label("Oh no! What image did you pick?", systemImage: "eye.trianglebadge.exclamationmark")
.foregroundStyle(.red)
} else {
Label("It is OK!", systemImage: "checkmark")
.foregroundStyle(.green)
}
}
if let analyzedImage,
let isImageSensitive
{
if isImageSensitive {
Image(uiImage: analyzedImage)
.resizable()
.scaledToFit()
.frame(height: 230)
.blur(radius: 5.0, opaque: true)
} else {
Image(uiImage: analyzedImage)
.resizable()
.scaledToFit()
.frame(height: 230)
}
}
}
.disabled(analyzer.analysisPolicy == .disabled)
}
}
func analyzePhoto(input: CGImage) {
Task {
let response = try await analyzer.analyzeImage(input)
self.isImageSensitive = response.isSensitive
}
}
func handlePickedImageItem(_ newValue: PhotosPickerItem) {
self.isImageSensitive = nil
self.analyzedImage = nil
newValue.loadTransferable(type: Data.self) { result in
switch result {
case .success(let success):
if let success,
let imageObj = UIImage(data: success),
let cgImageObj = imageObj.cgImage
{
self.analyzedImage = imageObj
self.analyzePhoto(input: cgImageObj)
}
case .failure(let failure):
return
}
}
}
}
#Preview {
ContentView()
}
SwiftData
この話題については後で詳しくQiitaの記事を書きます
SwiftDataはCore Dataを基盤に構築されています(ただし、Core Dataと組み合わせて使用する場合は、ハッシュをチェックするための追加の手順が必要です)。
以下の53行のコードで、保存されるデータの構造を定義し、コンテキストを作成し、新しいレコードを追加するためのSwiftUIビュー、レコードを表示し、削除するためのビューを作成しています。
import SwiftUI
import SwiftData
@Model
class Record {
var id: UUID
var timestamp: Date
init(timestamp: Date) {
self.id = UUID()
self.timestamp = timestamp
}
}
struct SwiftDataDemo: View {
var body: some View {
RecordsList()
.modelContainer(for: [Record.self])
}
}
struct RecordsList: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \.timestamp, order: .forward, animation: .snappy)
var storedRecords: [Record]
var body: some View {
NavigationStack {
List {
ForEach(storedRecords) { record in
Text(record.timestamp, format: .dateTime)
}
.onDelete(perform: { indexSet in
indexSet.forEach({ modelContext.delete(storedRecords[$0]) })
})
}
.toolbar {
Button ("Add") {
let location = Record(timestamp: Date())
modelContext.insert(location)
}
}
}
}
}
#Preview {
SwiftDataDemo()
}
上記のコードにおいて、以下のコードでデータ構造を定義しています:
@Model
class Record {
var id: UUID
var timestamp: Date
init(timestamp: Date) {
self.id = UUID()
self.timestamp = timestamp
}
}
ビューにストレージをアタッチします。まだ存在しない場合は、新しいストレージが作成されます。
struct SwiftDataDemo: View {
var body: some View {
RecordsList()
.modelContainer(for: [Record.self])
}
}
上記のビューモディファイアには、さらにinitパラメータがあり、例えば、データを自動的に保存するかどうか、完了時にコードを実行するかどうかなどを制御することができます。
また、iCloudを有効にし、iCloudコンテナを作成することも可能です。データはiCloudに同期されます
データのクエリには @Query
を使用することができます
@Query(sort: \.timestamp, order: .forward, animation: .snappy)
var storedRecords: [Record]
レコードを削除するには、データベースを読む@Environmental変数を作成します。
@Environment(\.modelContext) private var modelContext
デフォルトでは、変更内容は自動的に保存されます。
ShaderLibraryを使用して、ビュースタイルにMetalを適用
まず、シェーダーの形状を定義する.metal定義ファイルを作成します。
// angledFill.metal
#include <metal_stdlib>
using namespace metal;
[[ stitchable ]] half4
angledFill(float2 position, float width, float angle, half4 color)
{
float pMagnitude = sqrt(position.x * position.x + position.y * position.y);
float pAngle = angle +
(position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x));
float rotatedX = pMagnitude * cos(pAngle);
float rotatedY = pMagnitude * sin(pAngle);
return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2;
}
次に、上記の.metal定義内の関数(angleFillと呼ばれる)を参照する新しいShaderシェーダーを作成します。
var stripes: Shader {
ShaderLibrary.angledFill(
.float(10),
.float(90),
.color(.blue)
)
}
これで、.foregroundStyle
ビューモディファイアを使って、テキストにShaderシェーダースタイルを適用できるようになりました。
Text("Furdinand").font(.system(size: 50).bold()).foregroundStyle(stripes) + Text(" is a good dog.").font(.system(size: 50).bold())
SwiftUIで異なるテキストコンポーネントを連結させることができます。
.foregroundStyleについて、
.foregroundStyleビューモディファイアを使用して、
SwiftUIビューに色またはMetalスタイルを設定することができます。
このビューモディファイアはiOS 15から使用可能ですが、iOS 17では入力パラメータとしてシェーダーを使用することができます。
>= iOS 15
iOS 15以降、.foregroundStyleを使用してビューの色やグラデーションを設定することができます。
Text("Furdinand")
.font(.system(size: 50).bold())
.foregroundStyle(.blue)
Text("Hello world")
.foregroundStyle(.linearGradient(colors: [.teal, .blue], startPoint: .top, endPoint: .bottom))
.foregroundColor`は非推奨 (will be deprecated) になるようです。
もしiOS 14以前のデバイス向けのアプリをターゲットにしている場合、カスタムのビューモディファイアを作成することができます。一方、iOS 15以降のデバイス向けのアプリをターゲットにしている場合は、新しいforegroundStyleビューモディファイアにコードを置き換えることができます。
カスタムのビューモディファイア: https://gist.github.com/mszpro/fb6bb8a95376402daf433220de222389
>= iOS 17
iOS 17では入力パラメータとしてシェーダーを使用することができます。
var stripes: Shader {
ShaderLibrary.angledFill(
.float(10),
.float(90),
.color(.blue)
)
}
Text("Furdinand")
.font(.system(size: 50).bold())
.foregroundStyle(stripes)
SwiftUIでMapKitのマップを制御
以下は、WWDCのセッション動画からのスクリーンショットです。
Markerや図形(円、折れ線、多角形)を簡単に適用することができます。
また、@Binding変数を使用することで、現在どのMarkerが選択されているかを確認することができます。
この話題については後で詳しくQiitaの記事を書きます
SFシンボルエフェクト
SwiftUIでSFシンボル画像に多くの視覚効果を加えることができる
パルス
シンボルの不透明度をアニメーションで表示します。
Image(systemName: "rectangle.inset.filled.and.person.filled")
.symbolEffect(.pulse)
.frame(maxWidth: .infinity)
.font(.system(size: 50))
.symbolRenderingMode(.multicolor)
リバーシング
シンボルをレイヤーごとにアニメーションさせることができます。例えば、WiFi接続のアニメーションを作成することができます。
Image(systemName: "wifi")
.symbolEffect(.variableColor.iterative.reversing)
.font(.system(size: 50))
.symbolRenderingMode(.multicolor)
バウンス(ハートビート効果)
画像のスケールをアニメーションで変化させます。画像を拡大し、縮小します。心臓の鼓動のようです。
Image(systemName: "arrow.down.circle")
.symbolEffect(.bounce, value: simulatedDownloadPercentage)
.font(.system(size: 50))
.symbolRenderingMode(.multicolor)
拡大・縮小
上記と同様に、bool値を使ってシンボルを拡大・縮小することもできます。以下のサンプルコードでは、simulatedDownloadPercentage
が偶数である場合に画像を拡大表示しています。
Image(systemName: "bubble.left.and.bubble.right.fill")
.symbolEffect(.scale.up, isActive: simulatedDownloadPercentage % 2 == 0)
.font(.system(size: 50))
.symbolRenderingMode(.multicolor)
表示・非表示
Image(systemName: "cloud.sun.rain.fill")
.symbolEffect(.disappear, isActive: simulatedDownloadPercentage % 2 == 0)
.font(.system(size: 50))
.symbolRenderingMode(.multicolor)
2つのシンボル間のトランジション効果
1つのシンボルから別のシンボルへの切り替え時に、トランジション効果を追加することができます。例えば、再生ボタンで、再生アイコンと一時停止アイコンを切り替えるような場合に使用できます。
VStack {
Image(systemName: runToggle ? "play.fill" : "pause.fill")
.contentTransition(.symbolEffect(.replace.downUp))
.font(.system(size: 50))
.symbolRenderingMode(.multicolor)
Button("Toggle status") {
runToggle.toggle()
}
}
回転ジェスチャー
回転ジェスチャーを認識することで、ビューの回転を監視することができます。
struct RotationTesting: View {
@State private var angleRotated = Angle(degrees: 0.0)
var body: some View {
Rectangle()
.frame(width: 200, height: 200, alignment: .center)
.rotationEffect(angleRotated)
.gesture(
RotateGesture()
.onChanged { value in
angleRotated = value.rotation
}
)
}
}
インスペクタービュー(右側のサイドバー)を表示
インスペクターは右側に表示され、通常、ユーザーがアイテムの詳細を表示するために使用されます。
public struct ContentView: View {
@State private var state = AppState()
@State private var presented = true
public var body: some View {
AnimalTable(state: $state)
.inspector(isPresented: $presented) {
AnimalInspectorForm(animal: $state.binding())
.inspectorColumnWidth(
min: 200, ideal: 300, max: 400)
.toolbar {
Spacer()
Button {
presented.toggle()
} label: {
Label("Toggle Inspector", systemImage: "info.circle")
}
}
}
}
}
簡単にプレビュー
SwiftUIのビューをプレビューするために#previewを記述することができます。
また、#preview内で変数を直接追加することもできます。
#Preview {
ContentView()
}
TipKitを使ったユーザーへのチップ表示について
TipKit
を使って、ユーザーにヒントを表示し、どこにどんな機能があるかを理解させることができます。
iOSベータ1では、ヒントを表示する際に問題があるかもしれませんが、
後でコードを更新します。
Macros
お読みいただき、ありがとうございました。
個人ウェブサイト https://MszPro.com
注
上記内容の一部は、Apple社のサンプルコードから引用しています。ライセンスは下記に添付しています:
Copyright © 2023 Apple Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.