[Swift] 64 メモアプリ製作 その16 データ暗号化実装 CryptoKit, キーチェーン

[Mac M2 Pro 12CPU, Ventura 13.6.1, iOS 17.1.2, Xcode 15.0]

データを暗号化してからCore Dataに保存するようにしました。暗号化キーはKeyChainに保存しています。

UIのコーディングでは毎度苦労させられますが、暗号化については情報は簡単に入手できて実装は比較的容易でした。

import CryptoKit
import Security
import Foundation

class CryptoManager {
    static let shared = CryptoManager()
    private let key: SymmetricKey
    
    private init() {
        if let keyData = KeychainManager.load(key: "myEncryptionKey") {
            // Keychainからキーを読み込む
            key = SymmetricKey(data: keyData)
        } else {
            // 新しいキーを生成してKeychainに保存する
            let newKey = SymmetricKey(size: .bits256)
            let newKeyData = newKey.withUnsafeBytes { Data($0) }
            let status = KeychainManager.save(key: "myEncryptionKey", data: newKeyData)
            if status == noErr {
                print("新しいキーをKeychainに保存しました。")
                key = newKey
            } else {
                fatalError("キーの保存に失敗しました。")
            }
        }
    }
    
    func encrypt(_ text: String) -> String? {
        guard let data = text.data(using: .utf8) else { return nil }
        do {
            let sealedBox = try AES.GCM.seal(data, using: key)
            return sealedBox.combined?.base64EncodedString()
        } catch {
            print("Encryption error: \(error)")
            return nil
        }
    }

    func decrypt(_ encryptedText: String) -> String? {
        guard let data = Data(base64Encoded: encryptedText),
              let sealedBox = try? AES.GCM.SealedBox(combined: data) else { return nil }
        do {
            let decryptedData = try AES.GCM.open(sealedBox, using: key)
            return String(data: decryptedData, encoding: .utf8)
        } catch {
            print("Decryption error: \(error)")
            return nil
        }
    }

}

class KeychainManager {
    static func save(key: String, data: Data) -> OSStatus {
        let query = [
            kSecClass as String       : kSecClassGenericPassword as String,
            kSecAttrAccount as String : key,
            kSecValueData as String   : data ] as [String : Any]

        SecItemDelete(query as CFDictionary) // 既存のアイテムを削除
        return SecItemAdd(query as CFDictionary, nil)
    }
    
    static func load(key: String) -> Data? {
        let query = [
            kSecClass as String       : kSecClassGenericPassword,
            kSecAttrAccount as String : key,
            kSecReturnData as String  : kCFBooleanTrue!,
            kSecMatchLimit as String  : kSecMatchLimitOne ] as [String : Any]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        if status == noErr {
            return item as? Data
        }
        return nil
    }
}

[Swift] 63 メモアプリ製作 その15 バックアップから復元機能実装

[Mac M2 Pro 12CPU, Ventura 13.6, iOS 17.1.2, Xcode 15.0]

バックアップ用テキストエディタ構造体 Draft2を作成し、失ったメモをクリップボードにコピーできるようにしていましたが、よりスムーズにするため復元ボタンを配置してワンタップで復元できるようにしました。

これでApple Watchでも復元したメモを見ることができます。

import SwiftUI

struct Draft2: View {
    @State var text = ""
    @FocusState var nameFieldIsFocused: Bool
    @Environment(\.managedObjectContext)var viewContext

    var body: some View {
        TextEditor(text:$text)
            .frame(minHeight: 0, maxHeight: .infinity)
            .focused($nameFieldIsFocused)
            .onAppear{
                DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+0.5){
                    nameFieldIsFocused = true
                }
            }
            .toolbar{
                ToolbarItem(placement:.navigationBarTrailing){
                    if(nameFieldIsFocused){
                        Button(action:{
                            addContent()
                        }){
                            Text("復元")
                            .font(.system(size: 20))
                        }
                    }
                }
            }
    }
    
    func addContent(){
        let newContent = Note(context:viewContext)

        let date = Date()
        newContent.creationDate = date
        newContent.content = text

        // Data Modelを更新
        viewContext.refreshAllObjects()
        
        do{
            try viewContext.save()
            
        }catch{
            fatalError("セーブ失敗")
        }
        nameFieldIsFocused = false
    }
}

[Swift] 62 メモアプリ製作 その14 バックアップ機能実装

[Mac M2 Pro 12CPU, Ventura 13.6, iOS 17.1.2, Xcode 15.0]

Apple Watchで誤ってメモを消してしまうという事故が数ヶ月内で2回発生しました。

対策としてバックアップ機能を新たに実装しました。

メモの保存時にバックアップ用Core Dataにも同じデータを追加するという仕様です。誤って消してしまってもiPhoneの方でバックアップを確認できます。

import SwiftUI

struct Draft: View {
    @State var text = ""
    @FocusState var nameFieldIsFocused: Bool
    @Environment(\.managedObjectContext)var viewContext
    var note: Note?
    var noteBackup: NoteBackup?

    var body: some View {
        TextEditor(text:$text)
            .frame(minHeight: 0, maxHeight: .infinity)
            .focused($nameFieldIsFocused)
            .onAppear{
                DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+0.5){
                    nameFieldIsFocused = true
                }
            }
            .toolbar{
                ToolbarItem(placement:.navigationBarTrailing){
                    if(nameFieldIsFocused){
                        Button(action:{
                            if note != nil {
                                updateContent(note: self.note!)
                            }else{
                                addContent()
                            }

                        }){
                            Text("更新")
                            .font(.system(size: 20))
                        }
                    }
                }
            }
    }

    func updateContent(note:Note){
        let date = Date()
        note.creationDate = date
        note.content=text
        
        let newContent2 = NoteBackup(context:viewContext)
        newContent2.creationDate = date
        newContent2.content = text

        // Data Modelを更新
        viewContext.refreshAllObjects()
        
        do{
            try viewContext.save()
        }catch{
            fatalError("セーブ失敗")
        }

        nameFieldIsFocused = false
    }

    func addContent(){
        let newContent = Note(context:viewContext)
        let newContent2 = NoteBackup(context:viewContext)

        let date = Date()
        newContent.creationDate = date
        newContent.content = text
        
        newContent2.creationDate = date
        newContent2.content = text

        // Data Modelを更新
        viewContext.refreshAllObjects()
        
        do{
            try viewContext.save()
            
        }catch{
            fatalError("セーブ失敗")
        }

        nameFieldIsFocused = false

    }

}

[Swift] 61 visionOSアプリ製作 その9 3Dモデルのカラー変更機能実装 PhysicallyBasedMaterial.BaseColor

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

ランチャーアプリvisionOS版を開発しています。

3Dモデルの各面および面取り領域のカラーを自由に変更できるようにしました。

予定の機能は全て実装完了となり、これで一区切りとします。

[Swift] 60 visionOSアプリ製作 その8 ModelEntityのカラー変更 RealityKit

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

usdzファイルを読み込んだModelEntityは後からコードでカラー変更できるようです。あくまでアプリ上で一時的に変わるだけで、元モデルの色は変わりません。

マテリアルのインデックス番号を指定して、3Dモデルの各面や面取り領域のカラーを変えることも可能でしょう。

Reality Composer Proの機能をコード化しているといったところでしょうか。

var body: some View {
    ZStack {
        RealityView { content in
            if let entity = try? await ModelEntity(named: "hexa") {
                // エンティティのモデルを取得
                if var modelComponent = entity.model {
                    for i in 0..<modelComponent.materials.count {
                        if var material = modelComponent.materials[i] as? PhysicallyBasedMaterial {
                            // 全てのマテリアルをグリーンにする
                            material.baseColor = PhysicallyBasedMaterial.BaseColor(tint: .green)
                            // マテリアルを更新
                            modelComponent.materials[i] = material
                        }
                    }
                    // エンティティのモデルコンポーネントを更新
                    entity.model = modelComponent
                }

[Swift] 59 visionOSアプリ製作 その7 スライダー、SwiftData、Componentの連携

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

スライダーで3Dモデルの回転速度を変えられるようにしました。

スライダーを動かして止めると同時に変数、SwiftData、Componentの値を更新する仕組みの構築に苦労しました。

回転速度を設定しても他の画面に遷移して設定画面に戻るとスライダーが初期値になっている(回転速度自体はそのまま)、スライダーが初期値から動かない(動かしても離すと戻る)など様々なトラブルに見舞われ、結局丸一日掛かってしまいました。

[Swift] 58 visionOSアプリ製作 その6 テキスト読み上げ / AVSpeechUtterance関連エラー

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

AVSpeechUtteranceなどを使ってテキスト読み上げをさせているのですが、空文字読み上げで簡単にクラッシュします。その場合はアプリ再起動で直ります。

今度は読み上げスピードを1.5に設定するとクラッシュしました。こちらは重症でシミュレータを再インストールして復活しました。

Apple Developer ForumsやStack Overflowではこのトラブルにコードで何とかしようと悪戦苦闘されていますが、そもそも環境が壊れているので、シミュレータであれば再インストールで解決する事例でした。iPhoneなど実機の場合は解決方法不明です。

import AVFoundation

let speechSynthesizer = AVSpeechSynthesizer()
let utterance: AVSpeechUtterance
var textToSpeak: String = "XXX"

utterance = AVSpeechUtterance(string: textToSpeak)
utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
utterance.rate = 1.5 // これを追加するとクラッシュした

speechSynthesizer.speak(utterance)
シミュレータ選択のManage Run Destinations…から上記画面でVision Proシミュレータを削除 / 再インストール

[Swift] 57 visionOSアプリ製作 その5 SwiftDataの特徴 

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

SwiftDataの特徴をようやくつかめてきたので、書き留めておきます。

SwiftDataは見た目は配列っぽいですが、中身はデータベースそのものです。配列として扱うと痛い目にあいます。

SwiftDataのデータモデルにデータを追加しても、配列の末尾に配置されるとは限りません。常に何らかのキーでソートしておく必要があります。そのため通し番号あるいは日時のプロパティは必須です。

import Foundation
import SwiftUI
import SwiftData

@Model
final class Face {
    var appNum: Int
    var appName: String
    var urlScheme: String
    var createDate: String

    init(appNum: Int, appName: String, urlScheme: String, createDate: String) {
        self.appNum = appNum
        self.appName = appName
        self.urlScheme = urlScheme
        self.createDate = createDate
    }
}
import SwiftUI
import RealityKitContent
import SwiftData

@main
struct TestApp: App {
    init(){
        RotateComponent.registerComponent()
        RotateSystem.registerSystem()
        }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [Face.self, AppColor.self])
        }
        .windowStyle(.volumetric)
        .defaultSize(width: 1.0, height: 1.2, depth: 0.3, in: .meters)

        ImmersiveSpace(id: "ImmersiveSpace") {
            ImmersiveView()
        }
        .immersionStyle(selection: .constant(.full), in: .full)
    }
}
import SwiftUI
import RealityKit
import RealityKitContent
import SwiftData

struct ContentView: View {
    // 常に生成日時で昇順ソート
    @Query(sort: \Face.createDate) public var faces: [Face]
private func updateApp() {
        var nums: [Int] = [1,2,3,4,5,6]
        
        let size = faces.count
        var index = size - 1
        print("size: \(size)")

        // 配列の末尾からfor文を回す(新しいデータのみ残すため)
        for face in faces.reversed() {
            print("index: \(index)")
            print("face.appNum: \(face.appNum)")
            print("face.appName: \(face.appName)")
            print("face.createDate: \(face.createDate)")
            
            let surNum = face.appNum
            
            if nums != [], nums.contains(surNum){
                nums.removeAll { $0 == surNum }
                print("\(surface.appNum)をnumsから削除しました")
                print("現在のnums: \(nums)")
                print("\(surface.appNum)の最初のsurfaceは残留しました")
                
            } else {
                context.delete(faces[index])
                try? context.save()
                print("index\(index)を削除しました")
            }
            index -= 1
            
            if index == -1 {
                print("faces整理後確認")
                var count = 0
                for face in faces{
                    print("faces整理後 count: \(count)")
                    print("face.appNum: \(face.appNum)")
                    print("face.appName: \(face.appName)")
                    print("face.urlScheme: \(face.urlScheme)")
                    print("face.createDate: \(face.createDate)")
                    
                    count += 1
                }
            }
        }
    }

[Swift] 56 visionOSアプリ製作 その4 Fatal error: failed to find a currently active container 対策 / visionOSシミュレータのリセット or アプリ削除

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

SwiftDataで使うモデルにプロパティを追加すると、シミュレータ操作時に以下のエラーが出るようになりました。

import Foundation
import SwiftUI
import SwiftData

@Model
final class Face {
    var appNum: Int
    var appName: String
    var urlScheme: String
    var createDate: String // 追加したプロパティ

    init(appNum: Int, appName: String, urlScheme: String, createDate: String) {
        self.appNum = appNum
        self.appName = appName
        self.urlScheme = urlScheme
        self.createDate = createDate
    }
}
Fatal error: failed to find a currently active container

Apple Developer Forumsでも問題になっていて、アプリを再インストールすると直るという結論に落ち着いていました。

visionOSシミュレータからアプリを削除する方法が分からなかったので、メニューからDevice – Erase All Content and Settings… を選択してリセットしました。

23/12/01追記
アプリのアイコン長押しで削除できることを思い出しました。

アイコン長押しで赤い削除ボタン出現

Apple Developer Forums

[Swift] 55 visionOSアプリ製作 その3 データ永続化 / SwiftData

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

visionOSで使える簡単なユーティリティアプリを製作しています。

iOS 17から利用可能になったSwiftDataを試しています。まだ基本的な操作しか修得していませんが、まずまずの感触です。

現段階で大方の機能は実装しました。あとは自動回転などお遊び機能を入れてみようかと思います。