アルパカノフン

【SwiftUI】非同期処理

概要

非同期処理とは
処理を待たずに他の処理を並行して進められる仕組みのこと

使い方

・非同期処理を行う箇所をTaskで囲む
・非同期で行うメソッドには先頭にawaitをつける

Button("非同期処理開始") {
    Task {  // Taskで囲む
        try? await Task.sleep(nanoseconds: 2_000_000_000) // 2秒待機(awaitをつける)
        print("2秒経過")  // Task内なので2秒後に実行される
    }
    print("すぐ実行")  // Task外なので待たずにすぐ実行される
}

非同期メソッド

【呼び出し】

Button("非同期処理開始") {
    Task {
        await hiDouki()
     }
}

【非同期メソッド】
asyncをつける

// 非同期処理
func hiDouki() async {
    try? await Task.sleep(nanoseconds: 2_000_000_000) // 2秒待機
    print("2秒経過")
}

サンプル


・同期処理は処理を待たないと他の処理(Stepper)を行えない
・非同期処理は処理完了を待たずに他の処理を行える
・Sleepは同期のwait
・Task.sleepは非同期のwait

import SwiftUI

struct ContentView: View {
    @State private var count = 0
    @State private var alertFlg = false

    var body: some View {
        Form {
            Button("同期処理開始") {
                douki()
            }

            Button("非同期処理開始") {
                Task {
                    await hiDouki()
                }
            }
            Stepper("カウント:\(count)", value: $count)
            
        }
        .alert("2秒経過", isPresented: $alertFlg) {
            Button("OK") {
            }
        }
    }

    // 同期処理
    func douki() {
        sleep(2) // 2秒待機
        alertFlg = true
    }

    // 非同期処理
    func hiDouki() async {
        try? await Task.sleep(nanoseconds: 2_000_000_000) // 2秒待機
        alertFlg = true
    }
}

#Preview {
    ContentView()
}

【SwiftUI】日付

現在時刻の取得(Date型)

・Date()で現在時刻を取得する
・デフォルトではUTC協定世界時で表示される(日本時間でない)
・.description(with: .current)を使用すとデバイスの現在のロケール(言語・地域設定)の時刻で表示される

// 2025/03/15 09:00:00 に実行した例
print(Date())  // 2025-03-15 00:00:00 +0000
print(Date().description(with: .current))  // Saturday, March 15, 2025 at 9:00:00 Japan Standard Time

フォーマットを変換(String型)

var nowTime = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd(E) \nHH:mm:ss"

var now = dateFormatter.string(from: nowTime)

print(now)  // 2025/03/15(Sat) 09:07:24

日本語表示

var nowTime = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd(E) \nHH:mm:ss"
dateFormatter.locale = Locale(identifier: "ja_jp")

var now = dateFormatter.string(from: nowTime)

print(now)  // 2025/03/15(土) 09:07:24
指定子 説明 例 (2025年3月15日 14:05:30 の場合)
yyyy 西暦(4桁) 2025
yy 西暦(下2桁) 25
MM 月(2桁) 03
M 月(1桁, 2桁) 3
dd 日(2桁) 15
d 日(1桁, 2桁) 15
EEEE 曜日(フル) Saturday
E 曜日(短縮) Sat
a 午前/午後(AM/PM PM
HH 時(24時間制, 2桁) 14
H 時(24時間制, 1桁, 2桁) 14
hh 時(12時間制, 2桁) 02
h 時(12時間制, 1桁, 2桁) 2
mm 分(2桁) 05
m 分(1桁, 2桁) 5
ss 秒(2桁) 30
s 秒(1桁, 2桁) 30
SSS ミリ秒(3桁) 123

年を取得(Int型)

let intYear = calendar.component(.year, from: Date())

時刻を指定(Date型)

let day = Date()
let start = Calendar.current.startOfDay(for: day) // 指定した日付の0:00:0
let end = Calendar.current.date(bySettingHour: 23, minute: 59, second: 1, of: day) // 指定した日付の23:59:1

【SwiftUI】値をアプリ全体で共有する:@EnvironmentObject

初期値ありの場合


import SwiftUI

class Item: ObservableObject {
    @Published var id = UUID()
    @Published var name = "初期値"
}

struct ContentView: View {
    @State private var showFlg = false
    @EnvironmentObject var item: Item

    var body: some View {
        Form {
            Text("入力値:\(item.name)")
            InputView()
        }

    }
}

struct InputView: View {
    @EnvironmentObject var item: Item

    var body: some View {
        TextField("", text:$item.name)
    }
}

#Preview {
    ContentView()
        .environmentObject(Item())
}

DemoApp.swift

import SwiftUI

@main
struct DemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(Item())
        }
    }
}
ポイント1:クラス

・ObservableObjectをつける→ データが変更されたときに SwiftUI のビューを自動的に更新
・@Publishedをつける→ 値の変更検知対象とする(ObservableObjectと合わせて使用)

class Item: ObservableObject {
    @Published var id = UUID()
    @Published var name = "初期値"
}
ポイント2:使用

・@EnvironmentObjectで宣言する→ 使用できるようになる
・item.name→ 参照する方法

struct ContentView: View {
    @State private var showFlg = false
    @EnvironmentObject var item: Item

    var body: some View {
        Form {
            Text("入力値:\(item.name)")
            InputView()
        }

    }
}
struct InputView: View {
    @EnvironmentObject var item: Item

    var body: some View {
        TextField("", text:$item.name)
    }
}
ポイント3:プレビュー

・.environmentObject(Item())→ 最上位のビューで定義必要

#Preview {
    ContentView()
        .environmentObject(Item())
}
@main
struct Demo5App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(Item())
        }
    }
}

初期値なしの場合

import SwiftUI

class Item: ObservableObject {
    @Published var id: UUID
    @Published var name: String

    init(name: String){
        self.id = UUID()
        self.name = name
    }
}

struct ContentView: View {
    @State private var showFlg = false
    @EnvironmentObject var item: Item

    var body: some View {
        Form {
            Text("入力値:\(item.name)")
            InputView()
        }

    }
}

struct InputView: View {
    @EnvironmentObject var item: Item

    var body: some View {
        TextField("", text:$item.name)
    }
}

#Preview {
    ContentView()
        .environmentObject(Item(name: "初期値"))
}

複数ある場合

#Preview {
    ContentView()
        .environmentObject(Item(name: "初期値"))
        .environmentObject(Item2(name: "初期値"))
}

【SwiftUI】ビューコントローラー(TabView)

TabView

パターン1


iPhoneの場合、タブは5つまで(6個以上だと5つ目がMoreとなり内包される)
iPadの場合、タブは8つまで

タブで画面を切り替える例
切り替えるビューを作成しておく
タブビューは切り替え対象のビューに記載しない(今回はContentView)

struct ContentView: View {
    // タブの選択項目を保持する
    @State var selectionTab = 1
    
    var body: some View {
        TabView(selection: $selectionTab) {
            
            PageOneView()   // Viewファイル①
                .tabItem {
                    // タブの文字とアイコンを設定
                    Label("Page1", systemImage: "1.circle")
                }
                .tag(1)
                .badge(999) // バッチの設定(数値)
            
            PageTwoView()   // Viewファイル②
                .tabItem {
                    // タブの文字とアイコンを設定
                    Label("Page2", systemImage: "2.circle")
                }
                .tag(2)
                .badge("new") // バッチの設定(文字列)
        }
    }
}
パターン2

struct ContentView: View {
    // タブの選択項目を保持する
    @State var selectionTab = 1
    
    var body: some View {
        TabView(selection: $selectionTab) {
            PageOneView()
                .tag(1)

            PageTwoView()
                .tag(2)
        } // TabView ここまで
        .tabViewStyle(.page)
    }
}

【Xcode】アプリの設定

環境

Xcode16.1
Swift:6

ダークモードを無効化

①InfoにAppearanceを追加

Valueに「Light」を入力

画面の向きを固定

「Supported interface orientations」で管理している
iPhoneiPadで個別に設定したい場合は
「Supported interface orientations(iPhone)」、「Supported interface orientations(iPad)」で設定

縦固定にしたい場合は、Portrait(bottom home button)のみを設定する

Portrait(bottom home button) 縦画面にすることができる
Portrait(top home button) 画面を上下逆さにすることができる
Landscape(left home button) バイスの左を下にして横画面にすることができる
Landscape(right home button) バイスの右を横にして縦画面にすることができる

※向きを指定した場合、下記の警告が表示されるが「Requires full screen」にチェックを入れると解消する
「all interface orientations must be supported unless the app requires full screen」

対応するデバイスを指定

「PROJECT」と「TARGETS」の値は基本的に合わせておく
※設定の優先度は「TARGETS」>「PROJECT」

①「TARGETS」>「General」タブの「Supported Destinations」
※「TARGETS」>「Build Settings」タブの「Targeted Device Families」にも同じ項目があるが同じ内容を表示しているだけ

②「PROJECT」>「Build Settings」タブの「Targeted Device Families」

対応するiOSのバージョンを指定

・①「TARGETS」>「General」タブの「minimum Deployments」
指定したiOSのバージョンより低い場合、インストール不可になる
「TARGETS」>「Build Settings」タブの「iOS Deployment Target」にも同じ項目があるが同じ内容を表示しているだけ

②「PROJECT」>「Build Settings」タブの「iOS Deployment Target」


Swiftのバージョンを指定

「TARGETS」>「Build Settings」タブの「Swift Compiler - Language」

アプリの情報を設定

「TARGETS」>「General」タブの「Identity」

App Category App Storeでの分類
Display Name アプリのホーム画面に表示される名前
Bundle Identifier 一意になる名前
Version ユーザーに見えるアプリの公開バージョン。新しいバージョンをリリースする際は 過去のバージョンより大きくする必要がある
Build ビルド番号。App Store Connect で審査に提出するたびに増やす必要あり

VersionとBuildは「TARGETS」>「Build Settings」タブの「Versioning」にも同じ項目があるが同じ内容を表示しているだけ

【Xcode】実機シュミレーター

Apple IDの登録

Xcode>Settingsを押す

②Accountsの+を押す

Apple IDを選択してContinueを押す

Apple IDを入力してNextボタンを押す(次画面でパスワードも入力)

⑤AppleIDが登録されることを確認

⑥プロジェクトのTeamを選択する

iPhoneの設定

デベロッパーモードの設定
設定>プライバシーとセキュリティ>デベロッパーモードをONにする
※再起動が必要
②アプリの許可
設定>一般>VPNとデバイス管理>デベロッパAPPのアカウントをタップ>
メールアドレスをタップ>ポップアップの「信頼」をタップ

アプリをiPhoneに転送

iPhoneMacに接続
②シュミレータでiPhoneを選択

③Runを実行

④パスワードを入力

【SwiftUI】リレーション:SwiftData

リレーション

データベースのリレーションとは、データベース内のテーブル間の関連性のことを指す。

関連 説明
1 : 1
(1対1)
あるテーブルのレコードが、別のテーブルのレコードと1つだけ関連する場合
1 : N
(1対多)
あるテーブルの1つのレコードが、別のテーブルの複数のレコードと関連する場合
N : N
(多対多)
あるテーブルの複数のレコードが、別のテーブルの複数のレコードと関連する場合

SwiftData でリレーションを定義すると、関連があるプロパティに自動的に値が入る。
Delete Rule を定義することで削除時にリレーションがあるモデルを nil にしたり、データを削除する振る舞いも定義可能。

1 : 1(1対1)
import SwiftData

@Model
class User {
    var name: String
    @Relationship(deleteRule: .cascade) var profile: Profile? // 1対1の関係

    init(name: String, profile: Profile? = nil) {
        self.name = name
        self.profile = profile
    }
}

@Model
class Profile {
    var age: Int
    var user: User?

    init(age: Int, user: User? = nil) {
        self.age = age
        self.user = user
    }
}
1 : N(1対多)

1(タスク):N(カテゴリ)

import SwiftData

@Model
class Category {
    var name: String
    @Relationship(deleteRule: .cascade) var tasks: [Task] = [] // 1対多の関係

    init(name: String) {
        self.name = name
    }
}

@Model
class Task {
    var title: String
    var category: Category?  // 逆参照用(多対1の関係)

    init(title: String, category: Category? = nil) {
        self.title = title
        self.category = category
    }
}
N : N(多対多)
import SwiftData

@Model
class Student {
    var name: String
    @Relationship(deleteRule: .nullify) var courses: [Course] = [] // 多対多の関係

    init(name: String) {
        self.name = name
    }
}

@Model
class Course {
    var title: String
    @Relationship(deleteRule: .nullify) var students: [Student] = [] // 多対多の関係

    init(title: String) {
        self.title = title
    }
}

削除ルール(deleteRule)

deleteRule の値 説明
.cascade 親オブジェクトが削除されたとき、関連する子オブジェクトも削除される。
.nullify 親オブジェクトが削除されたとき、子オブジェクトの関連を解除(nil にする)。
.deny 親オブジェクトに関連する子オブジェクトがある場合、削除を禁止する。
.noAction 何もしない(制約なし)。

サンプル

・1(タグ)対 多(イベント)
・イベントはタグの情報をもつ(持たなくてもよい)
・タグを変更、削除するとイベントのタグ情報も連動する
【ポイント】
・イベントクラスはtagを持つにする
・タグクラスはイベントクラス(配列)を持つようにする

import SwiftUI
import SwiftData


@Model
// イベントのクラス
class Event {
    var eventId: UUID
    var eventName: String
    var tag: Tag?

    init(eventName: String, tag: Tag? = nil) {
        self.eventId = UUID()
        self.eventName = eventName
        self.tag = tag
    }
}

@Model
// タグのクラス
class Tag {
    var tagId: UUID
    var tagName: String
    var order: Int
    var event: [Event] = []

    init(tagName: String, order:Int) {
        self.tagId = UUID()
        self.tagName = tagName
        self.order = order
    }
}

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
    // SwiftData
    @Query var events: [Event]
    @Query(sort: [SortDescriptor(\Tag.order)]) var tags: [Tag]

    // 入力の値
    @State private var newEventName: String = ""
    @State private var newTagName: String = ""
    @State private var changeTagName = ""

    // インデックス
    @State private var selectedTagIndex: Int?
    @State private var changeTagIndex: Int = 0

    // フラグ
    @State private var alertDispayFlg = false

    var body: some View {
        Form{
            // ・イベント入力フォーム
            HStack{
                TextField("イベント", text: $newEventName)
                Button("登録"){
                    if selectedTagIndex != nil {
                        let tag = tags[selectedTagIndex!]
                        let newEvent = Event(eventName: newEventName, tag: tag)
                        modelContext.insert(newEvent)

                    } else {
                        let newEvent = Event(eventName: newEventName)
                        modelContext.insert(newEvent)
                    }
                }
                .disabled(newEventName.isEmpty)
            }
            // ・タグ入力フォーム
            HStack{
                TextField("タグ", text: $newTagName)
                Button("登録"){
                    let maxOrder = tags.isEmpty ? 0 :(tags.map { $0.order }.max() ?? 0) + 1
                    let newTag = Tag(tagName: newTagName, order: maxOrder)
                    modelContext.insert(newTag)

                }
                .disabled(newTagName.isEmpty)
            }

            // ・選択中のタグ表示
            Section("選択中のタグ") {
                if selectedTagIndex != nil {
                    Text(tags[selectedTagIndex!].tagName)
                }
            }
        }

        // ・イベントの一覧表示
        List(events) { event in
            HStack {

                Text(event.eventName)
                Spacer()
                if let name = event.tag?.tagName {
                    Text(name)
                } else {
                    Text("タグなし")
                }
                Button("削除"){
                    modelContext.delete(event)
                    selectedTagIndex = nil
                }
                .buttonStyle(.plain)
            }
        }

        // ・タグの一覧表示
        List {
            ForEach (tags) { tag in
                HStack{
                    Text(tag.tagName)
                        .onTapGesture {
                            if let index = tags.firstIndex(of: tag) {
                                selectedTagIndex = index
                            }
                        }
                    Spacer()
                    Button("変更"){
                        if let index = tags.firstIndex(of: tag) {
                            changeTagIndex = index
                        }
                        changeTagName = ""

                        alertDispayFlg = true
                        
                    }
                    .alert(tags[changeTagIndex].tagName, isPresented: $alertDispayFlg) {
                        TextField(tags[changeTagIndex].tagName, text: $changeTagName)

                        Button("変更") {
                            tags[changeTagIndex].tagName = changeTagName
                            try? modelContext.save()
                            changeTagName = ""
                        }
                        .disabled(changeTagName.isEmpty)

                        Button("キャンセル", role: .cancel){
                            changeTagName = ""
                        }

                    }
                    .buttonStyle(.plain)

                    Button("削除"){
                        modelContext.delete(tag)
                        selectedTagIndex = nil
                        changeTagIndex = 0
                    }
                    .buttonStyle(.plain)
                }
            }
        }
    }
}

#Preview {
    ContentView()
        .modelContainer(for: [Event.self, Tag.self], inMemory: true)
}