56548


初回起動の処理でアラートに文字を入力して" **OK** "を押したらUserDefaultsに入力した文字を保存する処理にしたいのですが (=
Thread 1: signal SIGABRT)
と致命的なエラーになります。ブレーキングポイントなどでエラーになる場所を突き止めたのですが、なぜそのコードがエラーになるのかその原因がわかりません。どういったコードに変更したら良いでしょうか。

↓TextFildに文字を入力  
[![初回起動アラート](https://i.stack.imgur.com/bAHwN.png)](https://i.stack.imgur.com/bAHwN.png)

↓入力してOKを押す  
[![OK](https://i.stack.imgur.com/ATCBi.png)](https://i.stack.imgur.com/ATCBi.png)

↓すると、スレッド1：シグナルSIGABRT //Thread 1: signal SIGABRT  
[![Thread 1: signal
SIGABRT](https://i.stack.imgur.com/HhRid.png)](https://i.stack.imgur.com/HhRid.png)

## コード

[code]

    import UIKit
    
    
    class ViewController: UIViewController {
        
        
        @IBOutlet weak var characterImage: UIImageView!
        @IBOutlet weak var timerlabel: UILabel!
        @IBOutlet weak var startButton: UIButton!
    
        let fastTitleKey = UserDefaultsController.shard.fastTitleKey
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
           SkillModal.shared.relode()
            SkillModal.shared.load()
            
            if let myTitle = SkillModal.shared.fastSkill(){
            self.title = myTitle.title
            } else {
                let alert = UIAlertController(title: "Title Name", message: "Please enter new name", preferredStyle: .alert)
                
                alert.addTextField(configurationHandler: nil)
              
                alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
                    if let text = alert.textFields?[0].text {
                        self.title = text
                       
                        SkillModal.shared.add(Skill(title: text, goalCount: 60 * 60 * 20))
                        
                       
                        SkillModal.shared.save() //ここで致命的なエラーになる
    
                    }
                }))
                alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
                present(alert, animated:  true, completion: nil)
            }
            
            startButton.layer.cornerRadius = 25.0
            
            
        }
        
        override func viewWillAppear(_ animated: Bool) {
            
            guard let skill = SkillModal.shared.currentSkill else {
                return
            }
            self.title = skill.title
            timerlabel.text = secondsToGoalTimerLabel(skill.goalCount)
    
            
        }
    
        }
    
[/code]

[code]

    import Foundation
    
    //「1つのセル」に捉われず、「1つのスキル」を表すデータがあればここに追加していく
    struct Skill {
        var title: String
        var goalCount: Int
    }
    class SkillModal{
        //アプリ全体で１インスタンスしかいらないならひたすらシングルトン！！
        static let shared = SkillModal()
       
        //このクラスはシングルトンとしてしかアクセスさせない
        private init() { }
    
     //配列としての`skills`を公開しておくと辻褄の合わない更新をしてしまう可能性があるのでprivateにする
        private var skills: [Skill] = []
        //`ViewController`側ではcurrentな要素を扱う
        private var currentIndex: Int? = nil
    
        //currentとなるindexを設定する
        func setCurrentIndex(_ newIndex: Int) {
            currentIndex = newIndex
        }
        
        //currentの要素を返す
        var currentSkill: Skill? {  //ここ分からない
            if let index = currentIndex,skills.indices.contains(index) {  //indicesて何？containsは指定された範囲内に含まれているか。ではindicesはコレクションを購読するのに有効なインデックス（昇順）とは？
                return skills[index]
            } else {
                return nil
            }
        }
    
        //一軒追加して、それをcurrentにする
        func addAndSetCurrent(_ newSkill: Skill) {
            currentIndex = skills.count
            skills.append(newSkill) //もしかしてこれと上はコードの順番が逆？？
        }
        
        func relode(){
            if let skillArray = UserDefaults.standard.array(forKey: "skillArray") as? [Skill] {
                skills = skillArray
            }
        }
        
        //一件追加
        func add(_ newSkill: Skill) {
            skills.append(newSkill)
        }
    
        //一件削除
        func remove(at index: Int) {
            currentIndex = nil //current -= 1じゃ無いの？？
            skills.remove(at: index)
        }
    
        //`index`けんめんを取り出す
     //`Skill`は値型なので、このメソッドの戻り値のプロパティを変更しても`skills`に反映されない
        func skill(at index: Int) -> Skill {
            return skills[index]
        }
        
        // ViewController の初回起動処理に使うメソッド
        func fastSkill() -> Skill? {
            
            if skills.indices.contains(0) {
                return skills[0]
            } else {
                return nil
            }
    
                }
        //プロパティを変更したい場合には、専用のメソッドを用意してそれを呼ぶ
        //`index`件目の`goalCount`を変更する
        func setGoalCount(_ newGoalCount: Int, at index: Int) {
            skills[index].goalCount = newGoalCount
        }
        
        //要素を返す
        var count: Int {
            return skills.count
        }
        
    /********問題のコード↓**********/    
        func save() {  //問題のコード
            
                UserDefaults.standard.set(skill, forKey: "skillArray")
            
            }
    /*********問題のコード↑********/
            func load() -> [Skill]? {
               
               let userDefaultsSkill = UserDefaults.standard.array(forKey: "skillArray")
                
                return userDefaultsSkill as? [Skill]
            }
        
    }
    
    
[/code]


CC BY-SA 4.0
2019-07-10T03:02:07.173
56537
2019-07-10T13:00:11.280
2020-06-17T08:14:45.997
-1
34541
question
swift
UserDefaultsでarray型を保存するときに（　Thread 1: signal SIGABRT ）という致命的なエラーになる。


`UserDefaults`と言うのはObjective-
Cのクラス(「本名」は`NSUserDefaults`)なのですが、そこに格納できるのはきわめて限られたデータ型のみとなります。(これらの型は「plist互換」と呼ばれることがあります。)

  * `NSNull`
  * `NSNumber`
  * `NSString`
  * `NSData`
  * `NSDate`
  * `NSArray`
  * `NSDictionary`

(`NSArray`や`NSDictionary`の場合、その要素型も上のいずれかでないといけません。)

以上の型は以下のようなSwiftの型とブリッジング機構により相互に変換されます。

  * `nil`
  * 全ての数値型、`Bool`
  * `String`
  * `Data`
  * `Date`
  * `Array`
  * `Dictionary`

保存しようとするデータの型が上のいずれでもない場合、あなたが経験されたような実行時エラーが発生します。

* * *

さて、この大前提を元に問題のコードを見てみましょう。

[code]

        func save() { //問題のコード
            UserDefaults.standard.set(skill, forKey: "skillArray")
        }
    
[/code]

あなたは`UserDefaults`にインスタンスメンバーの`skill`を保存しようとしています。この`skill`と言う「もの」はタイトルにあるような「array型」ではありません。あなたのコードでインスタンスメンバーの`skill`と言うのは、`skill`メソッド、つまりこちらを表しています。

[code]

        func skill(at index: Int) -> Skill {
            return skills[index]
        }
    
[/code]

他に`skill`と言う名前で表されるメンバーがない限り、単に`skill`と書くのと`skill(at:)`と書くのはどちらも同じことで、このメソッドを表しています。もちろんメソッドなんてものは上記の「`UserDefaults`に保存できるデータ型」ではありませんので、実行時エラーが発生します。

* * *

あなたの本来の意図としては、こう書きたかったのでしょう。

[code]

        func save() {
            UserDefaults.standard.set(skills, forKey: "skillArray")
            //                             ^
        }
    
[/code]

一字違いで大違いですが、ちゃんと`skills`と綴ると、それはこちら:

[code]

        private var skills: [Skill] = []
    
[/code]

`Skill`の配列を表すことになります。が、それでめでたしとはいきません。なぜなら`Skill`は、上記のplist互換型ではないからです。結局同じ実行時エラーが発生します。

* * *

では、今回のように **plist互換ではないものを`UserDefaults`に保存したい時には**どうするのかと言うと、何らかの方法で
**保存したいデータをplist互換な型のどれかに変換** してやります。現在のあなたのコードなら、最近Swiftに導入された
**`Codable`の仕組みを使う**のが一番簡単でしょう。

まずは、保存されるデータ、`Skill`型を`Codable`に適合させておきます。

[code]

    struct Skill: Codable {
        var title: String
        var goalCount: Int
    }
    
[/code]

Swiftが見えないところであれこれあなたの代わりにコードを書いてくれるので、理解しづらいのですが、こうしておくことで、`save()`をこんな風に書き換えることができます。

[code]

        func save() {
            do {
                //`Skill`型の配列である`skills`を`Data`型の`data`に変換
                let data = try JSONEncoder().encode(skills)
                //変換された`Data`型の値`data`は`UserDefaults`に保存できる
                UserDefaults.standard.set(data, forKey: "skillArray")
            } catch {
                print(error)
                //なんらかのエラー処理
                //...
            }
        }
    
[/code]

`JSONEncoder`と言うものを使って、最初に`skills`を`data`(型は`Data`型になります)に変換してから、その`data`を`UserDefaults`に保存します。`Data`はブリッジングにより`NSData`に変換され、無事に`UserDefaults`に保存することができます。

* * *

保存したデータを読み戻す時には、保存と逆のことをやってやります。  
(今のあなたの`load()`は、ちょっと想定と違う状態になっているんですが、こちらの思う`load()`で書かせてもらいます。一例だと思って見て下さい。)

[code]

        func load() {
            //`data`を`UserDefaults`から取り出して、
            if let data = UserDefaults.standard.data(forKey: "skillArray") {
                //あれば...
                do {
                    //`data`を`Skill`型の配列(`[Skill]`)に変換
                    skills = try JSONDecoder().decode([Skill].self, from: data)
                } catch {
                    print(error)
                    //なんらかのエラー処理
                    //...
                }
            } else {
                //なければ...
                print("No skillArray in UserDefaults")
                //UserDefaultsにデータがなかった時の処理
                //...
            }
        }
    
[/code]

* * *

ちなみに`UserDefaults`と言うのは、本来はアプリのユーザ設定程度の少量の情報を記憶しておくための仕組みです。お手軽にデータが保存できるため、ネットで見かけるサンプルコードでは多用されていますが、サンプルコード以上のちゃんとしたアプリを作る場合には、保存しようとしているデータが「ユーザ設定程度の少量の情報」かどうか、よーく考え直してみて下さい。


CC BY-SA 4.0
2019-07-10T13:00:11.280
56548
2019-07-10T13:00:11.280
13972
56537
answer