カテゴリー: プログラム

  • SwiftData @Modelを使ってList項目の追加・移動・削除を実装してみました

    SwiftDataの@Modelを使って、Listの項目を移動させる処理を作ってみました。@Modelはクラスに適用することで、アプリを終了させてもプロパティの値を保持させることができます。ただしアプリを削除したらプロパティの値は消滅します。

    まず、@Modelを適用させるクラスです。

    import Foundation
    import SwiftData
    
    @Model
    final class Item: Identifiable {
        var name: String
        //チェックボックス表示切り替え用
        var check: Bool
        //移動処理時にソートをかける対象
        var sortNumber: Int
        //移動処理をかけるときにIdentifiableプロトコルを準拠させるのに必要
        var id = UUID()
            
        init(name: String, sortNumber: Int) {
            self.name = name
            self.check = false
            self.sortNumber = sortNumber
        }
    }

    Itemの配列をリストとして表示し項目の追加、移動、削除の処理を可能にしたViewがこちらです。

    import SwiftUI
    import SwiftData
    
    struct ItemListView: View {
        //@Model class Item を modelContextとしてしようできるようにします
        //これでmodelContextの insert delete が利用できます
        @Environment(\.modelContext) var modelContext
        //Listのエディットモードを切り替える変数 editModeを設定
        @Environment(\.editMode) var editMode
        //ItemのsortNumberプロパティを並び替えの対象としてItemの配列をnumbersとして設定
        @Query(sort: \Item.sortNumber) private var numbers: [Item]
        @State var shoppingItem: String = ""
        @State var modeName: String = "追加削除モード開始"
    
        var body: some View {
            VStack {
                if editMode?.wrappedValue.isEditing == true {
                    HStack{
                        //買い物リストのアプリにしているので↓
                        TextField("ここに買い物するものを入力してください", text: $shoppingItem)
                            .font(.system(size: 30))
                            .background(Color.yellow)
                        Button{
                            let i = numbers.count
                            modelContext.insert(Item(name: shoppingItem, sortNumber: i))
                            shoppingItem = ""
                        } label: {
                            Text("追加")
                                .font(.system(size: 24))
                        }
                    }
                    .padding()
                }
                //[Item]のnumbersをListで表示
                List {
                    ForEach(numbers) { number in
                        HStack {
                            //チェックボックスの表示を切り替えると表示文字の大きさと色を変更
                            if number.check == false {
                                Text(number.name)
                                    .font(.system(size: 24, weight: .bold, design: .rounded))
                                    .foregroundColor(.black)
                            } else {
                                Text(number.name)
                                    .font(.system(size: 18))
                                    .foregroundColor(.gray)
                            }
                            Spacer()
                            //チェックボックスの表示を切り替え
                            if number.check == false {
                                Image(systemName: "square")
                                    .scaledToFill()
                                    .onTapGesture {
                                        number.check = true
                                    }
                            } else {
                                Image(systemName: "checkmark.square")
                                    .scaledToFill()
                                    .onTapGesture {
                                        number.check = false
                                    }
                            }
                        }
                    }
                    //移動時の処理
                    .onMove{ fromOffSet, newOffset in
                        var fromIndex: Int = 0
                        
                        //fromOffSetの配列オブジェクトから並び替え用の数値を取得
                        //これが移動対象の並び替え用の数値
                        for itm in fromOffSet {
                            let obj = numbers[itm]
                            fromIndex = obj.sortNumber
                        }
                        
                        var toIndex = 0
                        
                        if newOffset < numbers.count - 1 {
                            toIndex = numbers[newOffset].sortNumber
                        } else {
                            toIndex = numbers[numbers.count - 1].sortNumber + 1
                        }
                        
                        //移動対象と移動先の関係で条件付けし、移動後の並び替え用数値を変化させます
                        //これにより移動処理後に項目の並び替えが行えます
                        if fromIndex > toIndex {
                            numbers[fromIndex].sortNumber = toIndex
                            var toi = 0
                            if toIndex > 1 {
                                toi = toIndex
                            }
                            for i in toi..<fromIndex {
                                numbers[i].sortNumber += 1
                            }
                        } else if fromIndex < toIndex {
                            numbers[fromIndex].sortNumber = toIndex
                            let fromi = fromIndex
                            for i in fromi..<toIndex {
                                numbers[i].sortNumber -= 1
                            }
                        }
                    }
                    //削除時の処理
                    .onDelete{ indexSet in
                        //
                        for item in indexSet {
                            let obj = numbers[item]
                            modelContext.delete(obj)
                            //削除処理後並び替え用の数値を変化させます
                            //これにより移動処理後に項目の並び替えが行えます
                            //項目追加時に配列の個数を元に並び替え用数値を設定しているのでここで全体の数値を整理しています
                            if obj.sortNumber < numbers.count - 1 {
                                for i in obj.sortNumber..<numbers.count {
                                    numbers[i].sortNumber -= 1
                                }
                            }
                        }
                    }
                    .listRowBackground(Color.yellow)
                }
                .scrollContentBackground(.hidden)
                .background(.clear)
    
                Button {
    
                    if editMode?.wrappedValue.isEditing == true {
                        editMode?.wrappedValue = .inactive
                        modeName = "追加削除モード開始"
                    } else {
                        editMode?.wrappedValue = .active
                        modeName = "追加削除モード終わり"
                    }
    
                }label: {
                    Text(modeName)
                        .padding()
                        .font(.system(size: 24))
                        .foregroundColor(.black)
                        .background(Color.green)
                        .cornerRadius(10)
                }
            }
            .padding()
        }
    }
    
  • XCodeアプリarchiveでiconのエラー

    簡単なアプリができたので、試しにTestFlightに登録してみようと、XCodeでarchiveをかけて登録しようとしたら、iconの透過ができないかアルファを含んでるとかなんとかで登録できませんでした。結局ファイル書き出し時にPNG形式にして、アルファのチェックを外した物をアイコンにしたらすんなり通りました。assetsに登録時に教えてほしいけど、Apple Developer Programに登録してなくてもアプリを作れるから、その辺はルーズにしてあるのかも・・・。以後気を付けます。

  • SwiftUIの@Observableと@Environmentについて調べてみました

    SwiftUIの@Observableをクラスに適用すると、そのクラスを@EnvironmentでViewに導入すると、そのクラスのプロパティに変更を加えた時その変更を随時Viewに適用してくれます。

    下記コードはTestObservableクラスに@Observableを適用し、ContentViewに@Environmentでそのクラスを導入したものです。

    旧姓、新姓のボタンをクリックすると、クラスのnameプロパティを変化させます。その変化に対応しクラスのnameプロパティをセットしたTextの表示が変化します。

    PreviewもViewにenvironmentでクラスを導入することで、Previewも変化します。

    import SwiftUI
    
    struct ContentView: View {
        @Environment(TestObservable.self) var testObservable
        
        var body: some View {
            Text(testObservable.name)
            Button {
                testObservable.changeName(name: "旧姓")
            } label: {
                Text("旧姓")
            }
            Button {
                testObservable.refreshName()
            } label: {
                Text("新姓")
            }
        }
    }
    
    @Observable
    class TestObservable {
        var name: String
        
        init() {
            self.name = "新姓"
        }
        
        func refreshName() {
            self.name = "新姓"
        }
        
        func changeName(name: String) {
            self.name = name
        }
    }
    
    #Preview {
        ContentView()
            .environment(TestObservable())
    }
    

    @mainからの呼び出しは下記の感じです。

    import SwiftUI
    
    @main
    struct SwiftUILearnApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environment(TestObservable())
            }
        }
    }
    

  • CloudKit iCloudにサインインできない

    CloudKitを学習しようとサンプルプログラムをダウンロードしたり、参考になるサイトをまねてみたりして、実行させるとなぜか

    「iCloudにサインインしてからにして」

    みたいなメッセージが出てしまう。iPhoneでも、macでもiCloudにサインインして、driveを有効にしているのに、なぜなのか。途方に暮れてしまいました。こういう時はいつも、Apple Developer Ducumentationサイトを確認することにしています。CloudKitを一つ一つ見ていくと、Share Recordsにサンプルプログラムがありました。今まで見つけられなかったサンプルプログラムです。こういう時は、宝物でも見つけた気分になります。

    早速ダウンロードして、実行して見ます。

    「iCloudにサインインしてからにして」

    みたいなメッセージがまた出てしまう。

    今度は、ひとつひとつコードを見ていきます。

    「iCloudのcontainer identifierが”iCloud.<bundle identifier>”でなければcontainer作る時は

    CKContainer(identifier: <your container ID>)

    を使え!!」(もとは英語です。結構意訳してます。)

    いままで見なかったコメントでした。早速変更して・・・。

    今度はエラー

    「CloudKitShare.entitlementsがどうたらこうたら」

    ちょっとイラっときて、ファイルからCloudKitShare.entitlementsを消してあげました。再度Signing & CapabilitiesでiCloudのContainersを設定して・・・。CloudKitShare.entitlementsはできません。

    「エラーが消えてる。!」

    ダメもとで、早速実行。

    「うごいた。」

    結局、コメントをよく読むと、Bundle Identifierの頭に「iCloud.」をつけたContainerであれば、

    let container = CKContainer.defualt()

    Bundle Identifierの頭に「iCloud.」をつけたContainerでなければ、

    let container = CKContainer(identifier: <your container ID>)

    にする。ただし、<your container ID>の頭は iCloud. で始まらなければいけないということでした。

  • Swiftのasyncとawaitについて調べてみました

    asyncはasynchronousの略のようです。訳は、非同期の、同じ速度で進まない、同時に起こらないといったようなことです。asyncは実行の途中で一時停止できる特別な種類の関数またはメソッドを示します。また、async letのように組み合わせて使うと、他の処理を先行させることもできます。awaitは途中で一時停止の可能性があるポイントを明示するためのマークのようです。例えば、

    async let 非同期1 = await 遅延関数1(1)
    
    //順番に実行されます
    print("after1")
    
    //順番に実行されます
    let 普通2 = 関数1(2)
    
    //順番に実行されます
    print("after2")
    
    //同一関数は一度に一つしか実行されません
    async let 非同期3 = await 遅延関数1(3)
    
    //順番に実行されます
    print("after3")
    
    //遅延関数1とは異なる関数なので遅延関数1が終わるより先に実行されます
    async let 非同期4 = await 遅延関数2(4)
    
    //順番に実行されます
    print("after4")
    
    //順番に実行されます
    let 普通4 = 関数1(5)
    
    //順番に実行されます
    print("after5")
    
    /*
    after1
    Check Number: 2
    after2
    after3
    after4
    Check Number: 5
    Check Number: 4
    after5
    Check Number: 1
    Check Number: 3
    */
    
    
    func 遅延関数1(_ check: Int) async -> String {
        for _ in 1...1000000 {
        }
        print("Check Number: \(check)")
        return ("遅延1-\(check)")
    }
    func 遅延関数2(_ check: Int) async -> String {
        for _ in 1...100000 {
        }
        print("Check Number: \(check)")
        return ("遅延2-\(check)")
    }
    
    
    func 関数1(_ check: Int) -> String {
        print("Check Number: \(check)")
        return ("普通\(check)")
    }

    こんな感じです。asyncと明示した関数、メソッド、変数を参照するときはawaitをつけないとコンパイラーエラーになります。async letを使いawaitをつけいなとコンパイラーに警告されますが、実行されます。しかし、awaitをつけておけば、どこでasyncの関数やメソッドを参照しているかが、後でみてわかりやすくなるので、asyncとawaitはセットで使うことにしておいた方がよさそうです。

  • Swiftのenumについて調べてみました

    enumは列挙型と呼ばれます。

    enum Seasons {
        case Spring
        case Summer
        case Autumn
        case Winter
    }

    のように主にcaseを用いて定義されます。それから、switch式とよく組み合わせて使います。

    var myFavorite = Seasons.Spring
    
    switch myFavorite {
        case .Spring:
            print("どことなく気持ちが浮かれますよね")
        case .Summer:
            print("何かしなくちゃって気がせきますね")
        case .Autumn:
            print("旅に出たくなりますよね")
        case .Winter:
            print("一人が落ち着きますよね")
    }
    
    //どことなく気持ちが浮かれますよね

    CaseIterableプロトコルに準拠させるとcaseを配列のように扱えます。但しcaseをassociated Valueとするとエラーになります。

    enum Seasons: CaseIterable {
        case Spring
        case Summer
        case Autumn
        case Winter
    }

    これはOKですが、caseをassociated Valueにして見ると

    enum Seasons: CaseIterable {
        case Spring(month: String)
        case Summer(month: String)
        case Autumn(month: String)
        case Winter(month: String)
    }

    これはエラーになります。しかし、associated ValueでもallCaseを使いたいケースもあります。そんな時はallCaseを具体的な値の配列で定義してあげれば使えます。例えば

    enum E干支時刻: CaseIterable {
    	//associated Value
    	case eto(kanji: String, yomi: String, startJi: Int, shouJi: Int, endJi: Int)
    
    	//CaseIterableプロトコルに準拠させる為
    	//allCasesをassociated Valueを具体化して定義します。
    	static var allCases: [E干支時刻] {
    		return [.eto(kanji: "子", yomi: "ね", startJi: 23, shouJi: 0, endJi: 1),
    			.eto(kanji: "丑", yomi: "うし", startJi: 1, shouJi: 2, endJi: 3),
    			.eto(kanji: "寅", yomi: "とら", startJi: 3, shouJi: 4, endJi: 5),
    			.eto(kanji: "卯", yomi: "う", startJi: 5, shouJi: 6, endJi: 7),
    			.eto(kanji: "辰", yomi: "たつ", startJi: 7, shouJi: 8, endJi: 9),
    			.eto(kanji: "巳", yomi: "み", startJi: 9, shouJi: 10, endJi: 11),
    			.eto(kanji: "午", yomi: "うま", startJi: 11, shouJi: 12, endJi: 13),
    			.eto(kanji: "未", yomi: "ひつじ", startJi: 13, shouJi: 14, endJi: 15),
    			.eto(kanji: "申", yomi: "さる", startJi: 15, shouJi: 16, endJi: 17),
    			.eto(kanji: "酉", yomi: "とり", startJi: 17, shouJi: 18, endJi: 19),
    			.eto(kanji: "戌", yomi: "いぬ", startJi: 19, shouJi: 20, endJi: 21),
    			.eto(kanji: "亥", yomi: "い", startJi: 21, shouJi: 22, endJi: 23)]
    	}
    }

    こんな感じです。これをforEachで回してみます。

    E干支時刻.allCases.forEach { e干支時刻 in
    	switch e干支時刻 {
    	case .eto(let kanji, let yomi, let startJi, let shouJi, let endJi):
    			print("\(kanji)(\(yomi))の刻は \(startJi)時から\(endJi)時です。正\(kanji)は\(shouJi)時です。")
    	}
    }
    /*
    子(ね)の刻は 23時から1時です。正子は0時です。
    丑(うし)の刻は 1時から3時です。正丑は2時です。
    寅(とら)の刻は 3時から5時です。正寅は4時です。
    卯(う)の刻は 5時から7時です。正卯は6時です。
    辰(たつ)の刻は 7時から9時です。正辰は8時です。
    巳(み)の刻は 9時から11時です。正巳は10時です。
    午(うま)の刻は 11時から13時です。正午は12時です。
    未(ひつじ)の刻は 13時から15時です。正未は14時です。
    申(さる)の刻は 15時から17時です。正申は16時です。
    酉(とり)の刻は 17時から19時です。正酉は18時です。
    戌(いぬ)の刻は 19時から21時です。正戌は20時です。
    亥(い)の刻は 21時から23時です。正亥は22時です。
    */

    こんな感じになります。

  • Swiftのprotocolについて調べてみました

    protocolは、特定のタスクや機能に適したメソッド、プロパティ、その他の要件の設計図を定義します。とのことです。

    設計図の定義ということですが、プロパティ名は作れても、値は設定できません。メソッド名は作れても処理を書くことはできません。値の設定や処理を書くことは、protocolに準拠したclass、struct、enumでしかできません。

    また、継承はclassしかできません。protocolの場合は準拠というようです。

    共通の要件を設定しようとしたとき、struct、enumではprotocolを使うしかありません。

    ところで、classでは値の設定や、処理を書くことができるので、どの継承部分の設定や、処理を引き継いでいるか管理するのに大変そうです。

    「何代前のご先祖様はこんなことがこんだけできたけど、お前はできないんだったら、変更しておけよ!」

    って叱られそうです。

    その点、protocolは準拠させるときに具体的に要件を設定すればいいので、

    「出来そうなことを書いとくから、後はどんなふうにどれだけできるかはその都度設定してね!」

    みたいな軽い感じがします。

    さらに、protocolは型として認識されるので、funcの引数に設定できます。例えば、

    protocol PersonProtocol {
    	var name: String {get set}
    }
    
    //protocol PersonProtocol に準拠したprotocol AdultPersonProtocolを定義します
    protocol AdultPersonProtocol: PersonProtocol {
    	var job: String {get set}
    }
    
    //protocol PersonProtocol に準拠したstruct PersonAを定義します
    struct PersonA: PersonProtocol {
    	var name: String
    	init(){
    		name = ""
    	}
    }
    
    //protocol AdultPersonProtocolに準拠したstruct PersonBを定義します
    struct PersonB: AdultPersonProtocol {
    	var name: String
    	var job: String
    	init(){
    		name = ""
    		job = ""
    	}
    }
    
    //struct PersonAを実装したpersonAを定義します
    var personA = PersonA()
    personA.name = "子供店長"
    
    //struct PersonBを実装したpersonBを定義します
    var personB = PersonB()
    personB.name = "大人店長"
    personB.job = "サラリーマン"
    
    //ここでprotocol PersonProtocolをfuncの引数personの型に設定します
    func printName(_ person: PersonProtocol) {
    	//protocol PersonProtocolのプロパティnameに設定された値を表示します
    	print(person.name)
    }
    
    printName(personA)
    //子供店長
    printName(personB)
    //大人店長  

    こんな感じです。protocolのAdultPersonProtocolはPersonProtocolに準拠しているので、PersonProtocol型でもあります。ですから、この場合の引数として有効です。

  • Swiftのclassとstructについて調べてみました

    結論としては、特にこだわりが無ければ、structを使う方がよいようです。

    classの継承を使いたいときや、Object-Cと連携させたいときはclassを使わないといけないようです。

    参照渡しを利用したいとき。つまり、

    struct TestStruct {
    	var sampleStruct: String 
    	init(){
    		sampleStruct = ""
    	}
    }
    
    class TestClass {
    	var sampleClass: String 
    	init(){
    		sampleClass = ""
    	}
    }
    
    var testStruct = TestStruct()
    var testClass = TestClass()
    
    testStruct.sampleStruct = "Hello"
    testClass.sampleClass = "Hello"
    
    var ts1 = testStruct
    var ts2 = testStruct
    var tc1 = testClass
    var tc2 = testClass
    
    ts1.sampleStruct = "なんでやねん"
    tc1.sampleClass = "なんでやねん"
    
    print("structは値渡しなのでts2.sampleStructは\(ts2.sampleStruct)のままです。")
    //structは値渡しなのでts2.sampleStructはHelloのままです。
    print("classは参照渡しなのでtc2.sampleClassは\(tc2.sampleClass)に変わります。")
    //classは参照渡しなのでtc2.sampleClassはなんでやねんに変わります。
    

    上記のように、変数testStruct、testClassに実装し、プロパティtestStruct.sampleStruct、testClass.sampleClassに”Hello”を代入後、変数ts1、ts2に変数testStruct、tc1、tc2にtestClassを代入し、プロパティts1.sampleStruct、tc1.sampleClassを”なんでやねん”に変更した時、

    structの場合、ts2.sampleStructは”Hello”のままですが、

    classの場合、tc2.sampleClassは”なんでやねん”に変わります。

    ということで、プロパティの値を変更したら、他のも変更させたいときはclassを使うようです。

  • SwiftUIのアプリはどこから始まるのか調べてみました

    XCodeで作成するプロジェクトでは@mai attributeがあるstructから始まります。

    import SwiftUI
    
    @main
    struct MyApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    

    iOSを対象にしたSwiftUI Interfaceではこんな感じです。下記のファイルと合わせると、地球マークの下に「こんにちは!」と表示されたアプリが出来上がりです。

    import SwiftUI
    
    
    struct ContentView: View {
        var body: some View {
            VStack {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                Text("こんにちは!")
            }
            .padding()
        }
    }
    
    
    #Preview {
        ContentView()
    }

    こんな感じです。

  • PowerApps PDFビューアーを触ってみました

    PowerAppsには、PDFを表示する機能があります。

    はじめはPowerAutomateで下記の関数を使い、帰ってきた、バイナリデータをSet関数で変数に代入し、その変数をPDFViewerのDocumentプロパティにセットし、PDFを表示させていたのですが。

    dataUri(base64ToBinary(body('ステップ名')?['$Content']))

    PowerAppsだけでも、データでデータの追加 コネクタでBoxを追加すると、Box.GetFileContent(“Box.File.Id“)が使えるようになり、Set関数で変数に代入し、その変数をPDFViewerのDocumentプロパティにセットすれば表示できますした。こちらの方が簡単なので、今はこの方法を使っています。