カテゴリー: IT

  • 初めてのアプリ登録

    Apple Developer Program に登録して、1年以上紆余曲折しながら、初めてApp Store Connectにアプリの審査申請をしてみました。Apple Developer Program に登録したての頃は軽い気持ちで取り組んでました。ところが、いろいろ情報を得ていくうちに今までの知識ではとても間に合わないことに気づき、休日を利用し基本的なところから、まず学習していきました。そのかいあってか簡単なアプリを作ることができたので、ここは試しにアプリの登録をしようとこれまた軽い気持ちで、審査申請に挑戦しました。まずアプリをどうやって審査申請のまな板にのせるかがわからず、ネットを調べたら、XCodeのメニューproduct の archiveでできるとのこと、さっそくやってみました。そして、登録申請のボタンがあるのでとりあえず、クリックすると、メッセージの山です。登録上必要事項が記入されていないと、メッセージが出て、審査申請が完了しません。何度もメッセージに従って必要事項を記入して、根負けしそうになりながらも、何度も審査申請をクリックすると、やっと、審査中になりました。そして、2日目に配信準備完了になりました。同時に何通か来ていたメールに、配信への手順の記載があり、それに従い必要項目を設定し配信できました。AppStoreでの表示はその翌日に可能となりました。

    申請にはプライバシポリシーの保存先URLと、サポートページのURLを記載する必要がありました。このサイトの一部を使用することで対応できました。このサイトはそんなことは全く想定しておらず勢いで作成していたのですが、この時ばかりはこのサイトを運営していてよかった思いました。

  • 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()
        }
    }
    
  • TestFlightアプリ登録App Reviewに関する情報で電話番号がはじかれる

    TestFlightアプリ登録時 App Reviewに関する情報の電話番号の入力で0から始まる電話番号を入れたら、無効扱いされました。国際電話番号表記にしないといけなかったらしく、はじめの0を日本の国際電話番号+81 に変えたらすんなり受け入れたもらえました。改めて、グローバル感につつかれた感じです。

  • 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. で始まらなければいけないということでした。

  • CloudKit Console レコードタイプが削除できない

    CloudKitを試そうと色々触っているうちにレコードタイプが消せなくなりました。出てくるダイアログを見ると、

    “invalid attempt to delete a record type which is active in a production”

    とメッセージが出て消せません。どうやら、

    「本番コンテナでアクティブなレコード タイプを削除しようとする試みは無効です。」

    といった意味なようです。本番コンテナー?いつした?どこでした?疑問の嵐が巻き起こりです。それから、https://developer.apple.com/documentation/cloudkitを調べてみました。次のようなことがわかりました。私の悪い癖で、説明書とかを読みながらさわればよさそうなものを、適当に触ってつまずいてから、なんでだろうと調べ出すのは悪い癖なんですが、治りません。でも、安心してください。壊れて困るようなものを触る時は必ず説明書等を読みます。

    さて、どうしたら本番環境になるのかですが、Apple Developer Programに登録されていることを前提にして進めます。

    まず、CloudKit DatabaseへはAccount – Apple Developerからも入れます。

    Xcodeからも入れますが、その説明は割愛します。

    Account – Apple Developerのサイトの下記

    赤四角マークのCloudKitをクリック

    赤四角マークのCloudKit Databaseをクリックして、データベースの管理画面に入れます。

    CloudKit Databaseを開くと、左下に下の表示があります。(赤四角や青四角はありません。)

    赤四角のDeploy Schema Changes を実行すると下のような表示が出ます。Schemaの変更をするとここに表示され、右下のDeployボタンをクリックすると、変更したSchemaがDevelopment(開発環境)からProduction(本番環境)へ保存されます。

    そして、

    ただし、レコードタイプは開発環境から本番環境に保存されますが、レコードは開発環境から本番環境に保存されません。

    また、青四角のReset Environmentを実行すると、すべての開発環境のレコードタイプおよびレコードは削除されます。本番環境のレコードタイプおよびレコードは削除されません。本番環境で編集する必要があるようです。本番環境にアプリを移行していなければ、この操作は必要なさそうです。

    開発環境と本番環境の切り替えですが、コンテナ名の右に下の表示があります。そこで開発環境と本番環境を替えらえます。

    上の表示をクリックすると、下の表示が出ますので、そこで環境を選べます。

  • 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型でもあります。ですから、この場合の引数として有効です。