56459


画面遷移する前は表示されているのですが画面遷移から戻ると初期状態に戻ってしまうのはなぜですか。どうすれば望む処理になるでしょうか。  
[![画像の説明をここに入力](https://i.stack.imgur.com/cCzDS.png)](https://i.stack.imgur.com/cCzDS.png)  
[![画像の説明をここに入力](https://i.stack.imgur.com/3cpcj.png)](https://i.stack.imgur.com/3cpcj.png)  
[![画像の説明をここに入力](https://i.stack.imgur.com/MkQ3u.png)](https://i.stack.imgur.com/MkQ3u.png)  
[![画像の説明をここに入力](https://i.stack.imgur.com/9pGim.png)](https://i.stack.imgur.com/9pGim.png)

[code]

    import UIKit
    
    class TableViewController: UITableViewController {
    
        @IBOutlet var myTableView: UITableView!
    
        var array : Array<String> = ["\(UserDefaults.standard.object(forKey: UserDefaultsController.shard.fastTitleKey + "0") as! String)"]
    
        let arrayNameKey = "arrayNameKey"
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
    
        }
    
    
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return array.count
        }
    
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "customTableViewCell", for: indexPath) as! costomViewCell
            if let skillNameArray = (UserDefaults.standard.array(forKey: self.arrayNameKey) as? Array<String>) {
    
                cell.skillName.text = "\(skillNameArray[indexPath.row])"
    
            } else {
                cell.skillName.text = array[indexPath.row]
    
            }
    
    
    
            cell.goalCountLabel.text = secondsToGoalTimerLabel(UserDefaults.standard.integer(forKey: UserDefaultsController.shard.goalCountNumberKey))
    
            return cell
        }
    
    
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
            let storyboard : UIStoryboard = self.storyboard!
            let nextView =
            storyboard.instantiateViewController(withIdentifier: "ViewController")
            let navi = UINavigationController(rootViewController: nextView)
    
            UserDefaults.standard.set(indexPath.row, forKey: UserDefaultsController.shard.currentNumberKey)
    
            if let arr = UserDefaults.standard.array(forKey: self.arrayNameKey) {
    
               let arrConversion = arr as! [String]
    
                UserDefaults.standard.set(arrConversion[indexPath.row], forKey: UserDefaultsController.shard.titleCurrentNumberKey)
            }
    
    
            present(navi, animated: true, completion: nil)
    
        }
    
        override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == .delete {
                array.remove(at: indexPath.row)
                tableView.deleteRows(at: [indexPath], with: .fade)
                UserDefaults.standard.set(self.array, forKey: self.arrayNameKey)
            }
        }
    
    
        @IBAction func addSkillButton(_ sender: UIBarButtonItem) {
    
            var alertTextFeld: UITextField?
            let alert = UIAlertController(title: "Skill Name", message: "Enter new name", preferredStyle: UIAlertController.Style.alert)
            alert.addTextField { (textField: UITextField!) in
                alertTextFeld = textField
            }
            alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
                if let text = alertTextFeld?.text {
                    self.array.append(text)
                    UserDefaults.standard.set(self.array, forKey: self.arrayNameKey)
                    self.myTableView.reloadData()
                }
            }))
    
           self.present(alert, animated: true, completion: nil)
        }
    
        @IBAction func secret(_ sender: UIBarButtonItem) {
        }
    
        func saveDate() {
    
        }
    
    
    }
    
    
[/code]

[code]

    import UIKit
    
    class costomViewCell: UITableViewCell {
    
        @IBOutlet weak var skillName: UILabel!
        @IBOutlet weak var goalCountLabel: UILabel!
    
    
        override func awakeFromNib() {
            super.awakeFromNib()
            // Initialization code
        }
    
        override func setSelected(_ selected: Bool, animated: Bool) {
            super.setSelected(selected, animated: animated)
    
            // Configure the view for the selected state
        }
    
    }
    
[/code]

[code]

    import UIKit
    
    class UserDefaultsController {
        static let shard = UserDefaultsController()
    
        let currentNumberKey = "currentNumberKey"
        let fastTitleKey = "fastTitleKey"
        let goalCountKey = "goalCountKey"
        let userCurrentNumberKey = String(UserDefaults.standard.integer(forKey: "currentNumberKey"))
        let titleCurrentNumberKey = "fastTitleKey" + String(UserDefaults.standard.integer(forKey: "currentNumberKey"))
        let goalCountNumberKey = "goalCountKey" + String(UserDefaults.standard.integer(forKey: "currentNumberKey"))
    
        init() {
    
        }
    
[/code]

↓UIViewCountroller

[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()
    
    
            if let myTitle = UserDefaults.standard.string(forKey:  UserDefaultsController.shard.titleCurrentNumberKey) {
            self.title = myTitle
            } 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
                        UserDefaults.standard.set(0, forKey: UserDefaultsController.shard.currentNumberKey)
                        UserDefaults.standard.set(text, forKey:  UserDefaultsController.shard.titleCurrentNumberKey)
    
                    }
                }))
                alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
                present(alert, animated:  true, completion: nil)
            }
    
            startButton.layer.cornerRadius = 25.0
    
            guard let obj = UserDefaults.standard.object(forKey:  UserDefaultsController.shard.goalCountNumberKey) else {
                return
            }
    
            let goalString = "\(obj)"
            timerlabel.text = secondsToGoalTimerLabel(Int(goalString)!)
    
    
        }
    
        override func viewWillAppear(_ animated: Bool) {
    
            guard let obj = UserDefaults.standard.object(forKey: UserDefaultsController.shard.goalCountNumberKey) else {
                return
            }
    
            let goalString = "\(obj)"
            timerlabel.text = secondsToGoalTimerLabel(Int(goalString)!)
        }
    
        }
    
    
    
    
    
    
    
[/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 
            skills.remove(at: index)
        }
    
        //`index`けんめんを取り出す
     //`Skill`は値型なので、このメソッドの戻り値のプロパティを変更しても`skills`に反映されない
        func skill(at index: Int) -> Skill {
            return skills[index]
        }
        //プロパティを変更したい場合には、専用のメソッドを用意してそれを呼ぶ
        //`index`件目の`goalCount`を変更する
        func setGoalCount(_ newGoalCount: Int, at index: Int) {
            skills[index].goalCount = newGoalCount
        }
    
        //要素を返す
        var count: Int {
            return skills.count
        } //なぜここは func count() -> Int { return skills.count }ではなく計算型puropatyなのですか？？
    
    
        func save() {
    
                UserDefaults.standard.set(skill, forKey: "skillArray")
    
            }
    
            func load() -> [Skill] {
    
               let userDefaultsSkill = UserDefaults.standard.array(forKey: "skillArray")
    
                return userDefaultsSkill as! [Skill]
            }
    
    }
    
    
[/code]

[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.skill(at: 0) {
            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 UIKit
    
    class TableViewController: UITableViewController {
    
        @IBOutlet var myTableView: UITableView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            SkillModal.shared.relode() // It is this!!
        }
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return SkillModal.shared.count 
        }
    
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "customTableViewCell", for: indexPath) as! costomViewCell
            let skill = SkillModal.shared.skill(at: indexPath.row)
            cell.skillName.text = skill.title
            cell.goalCountLabel.text = secondsToGoalTimerLabel(skill.goalCount)
    
            return cell
        }
    
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
            SkillModal.shared.setCurrentIndex(indexPath.row)
    
            self.navigationController?.popViewController(animated: true)
        }
    
        override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == .delete {  //後で変えて遊んでみる
                tableView.deleteRows(at: [indexPath], with: .fade)
            }
            SkillModal.shared.remove(at: indexPath.row)
        }
    
    
        @IBAction func addSkillButton(_ sender: UIBarButtonItem) {
    
            var alertTextFeld: UITextField?
            let alert = UIAlertController(title: "Skill Name", message: "Enter new name", preferredStyle: UIAlertController.Style.alert) 
            alert.addTextField { (textField: UITextField!) in
                alertTextFeld = textField
            }
            alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
                if let text = alertTextFeld?.text {
                    let indexPath = IndexPath(row: SkillModal.shared.count, section: 0)
                    SkillModal.shared.add(Skill(title: text, goalCount: 60 * 60 * 20)) 
                    self.tableView.insertRows(at: [indexPath], with: .automatic) 
                    SkillModal.shared.save()
                }
            }))
            self.present(alert, animated: true, completion: nil)
        }
    
        @IBAction func secret(_ sender: UIBarButtonItem) {
        }
    
    
    }
    
    
[/code]


CC BY-SA 4.0
2019-07-05T14:27:32.587
56425
2019-07-08T03:19:01.317
2019-07-07T13:37:53.607
34541
34541
question
swift
TableViewのcellに保存されている配列が表示されないのはなぜか。


# 「戻らない」アプリは絶対に作ってはいけない

まずは、iOSアプリとして致命的な『「画面遷移から戻る」処理が行われていない』について、確認しておきましょう。

iOSは`present(_:animated:completion:)`で表示されるような画面遷移や`UINavigationController`内部の画面遷移(セグエを使った場合、iOS内部でどちらか同様のメソッドが呼ばれる)では、表示されるview
controllerをスタック上に管理しています。

(`UINavigationController`が絡むと内部の構造はもっと複雑になりますが、簡略化しています。)

> ①(アプリの起動時の状態)  
>  ↓  
>  ②`ViewController`  
>  ↓ (「スキル」をタップ)  
>  ③`TableViewController`

この状態で`TableViewController`の行を選択すると、本当はこうならなくては行けません。

> ①(アプリの起動時の状態)  
>  ↓  
>  ②`ViewController`(選択された行の内容を表示)

しかし、あなたの現在のコードだと、こうなります。

> ①(アプリの起動時の状態)  
>  ↓  
>  ②`ViewController`  
>  ↓ (「スキル」をタップ)  
>  ③`TableViewController`  
>  ↓ (どこかの行を選択)  
>  ④`ViewController` (選択された行の内容を表示しているが、②とは別のインスタンス)

今のあなたの画面遷移構造のままアプリを使い続けていると、こんな感じに。

> ①(アプリの起動時の状態)  
>  ↓  
>  ②`ViewController`  
>  ↓ (「スキル」をタップ)  
>  ③`TableViewController`  
>  ↓ (どこかの行を選択)  
>  ④`ViewController`  
>  ↓ (「スキル」をタップ)  
>  ⑤`TableViewController`(③とは別インスタンス)  
>  ↓ (どこかの行を選択)  
>  ⑥`ViewController` (②④とは別のインスタンス)  
>  ↓ (「スキル」をタップ)  
>  ⑦`TableViewController`(③⑤とは別のインスタンス)  
>  ↓ (どこかの行を選択)  
>  ⑧`ViewController` (②④⑥とは別のインスタンス)  
>  ↓ (「スキル」をタップ)  
>  ⑨`TableViewController`(③⑤⑦とは別のインスタンス)  
>  ↓ (どこかの行を選択)  
>  ⑩`ViewController` (②④⑥⑧とは別のインスタンス)  
>  :

「インスタンス」と言うものは、それなりのメモリを消費するものですが、あなたのアプリを使い続けていると、view
controllerの大群がメモリを消費しきって、やがてメモリ落ち(先に他の破綻が訪れる可能性もありますが)でクラッシュすることになります。同時動作中の他のアプリやシステムにも影響を与えるでしょう。

と言うわけで、iOSでは、

**「戻らない」アプリは絶対に作ってはいけない**

と言うことになります。これは「推奨」だとか「良い習慣」のレベルではなく、「絶対にやってはいけない」の一つです。

解決方法としては、行選択のメソッドでは「選択した行のデータを表示できるような処理をした上で元の画面に戻る」ようにすれば良いことになります。そこら辺は、次に示す「保存・読み込みの方法」に絡めて一緒に見直しましょう。

* * *

# 保存・読み込みの方法に問題がある

正直`UserDefaults`を多用しすぎな上、その使い方がうまいとは言えないので、コードが大変分かりにくいものになっています。

特定のview controllerだけでなく、アプリケーション全体から使われる可能性のあるデータは、各view
controllerでバラバラに変数を宣言して細かい単位でやりとり(相手が`UserDefaults`かどうかは置いといて)するのでなく、独立したデータモデルとして管理し、各view
controllerからはデータモデルに用意されたメソッドを使ってデータ操作を行います。

SkillModel.swift

[code]

    import Foundation
    
    // あなたの`TableViewController`の1つのセルに対するデータ構造
    //「1つのセル」に捉われず、「1つのスキル」を表すデータがあればここに追加していく
    struct Skill {
        var title: String
        var goalCount: Int
    }
    
    /// `TableViewController`の`array`のような内容を特定view controllerによらず保持できるようにする
    class SkillModel {
        //アプリ全体で1インスタンスしか要らないなら、ひたすらシングルトン
        static let shared = SkillModel()
    
        //このクラスはシングルトンとしてしかアクセスさせない
        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) {
                return skills[index]
            } else {
                return nil
            }
        }
    
        /// 1件追加して、それをcurrentにする
        func addAndSetCurrent(_ newSkill: Skill) {
            currentIndex = skills.count
            skills.append(newSkill)
        }
    
        /// 1件追加
        func add(_ newSkill: Skill) {
            skills.append(newSkill)
        }
    
        /// 1件削除
        func remove(at index: Int) {
            currentIndex = nil
            skills.remove(at: index)
        }
    
        /// `index`件目を取り出す
        //`Skill`は値型なので、このメソッドの戻り値のプロパティを変更しても`skills`には反映されない
        func skill(at index: Int) -> Skill {
            return skills[index]
        }
    
        //プロパティを変更したい場合には、専用のメソッドを用意してそれを呼ぶ
        /// `index`件目の`goalCount`を更新する
        func setGoalCount(_ newGoalCount: Int, at index: Int) {
            skills[index].goalCount = newGoalCount
        }
    
        /// 要素数を返す
        var count: Int {
            return skills.count
        }
    
        func save() {
            //...
        }
    
        func load() {
            //...
        }
    }
    
[/code]

`save()`や`load()`なんかの処理を省略していますが、全体としてそれほど難解なものではないと思います。`shared`なんてプロパティを用意してシングルトンとして使えるようにしてある理由はもうお分かりでしょう。

`TableViewController`の中で`array`を1個宣言すればよかったのに比べると、何か面倒なことをしているように見えますが、`UserDefaults`を多用しているせいでコードが複雑化している今のあなたの`TableViewController`はこんな感じですっきりさせることが出来ます。

[code]

    import UIKit
    
    class TableViewController: UITableViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
        }
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return SkillModel.shared.count
        }
    
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "customTableViewCell", for: indexPath) as! CustomViewCell
    
            let skill = SkillModel.shared.skill(at: indexPath.row)
            cell.skillName.text = skill.title
            cell.goalCountLabel.text = secondsToGoalTimerLabel(skill.goalCount)
    
            return cell
        }
    
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            //### 行が選択されたら、その行の内容が表示されるような処理をして、「元の画面に戻る」
            SkillModel.shared.setCurrentIndex(indexPath.row)
            self.navigationController?.popViewController(animated: true)
        }
    
        override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == .delete {
                tableView.deleteRows(at: [indexPath], with: .fade)
                //↑↓辻褄を合わせる
                SkillModel.shared.remove(at: indexPath.row)
            }
        }
    
        @IBAction func addSkillButton(_ sender: UIBarButtonItem) {
    
            var alertTextFeld: UITextField?
            let alert = UIAlertController(title: "Skill Name", message: "Enter new name", preferredStyle: UIAlertController.Style.alert)
            alert.addTextField { (textField: UITextField!) in
                alertTextFeld = textField
            }
            alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
                if let text = alertTextFeld?.text {
                    let indexPath = IndexPath(row: SkillModel.shared.count, section: 0)
                    SkillModel.shared.add(Skill(title: text, goalCount: 0))
                    self.tableView.insertRows(at: [indexPath], with: .automatic)
                }
            }))
    
            self.present(alert, animated: true, completion: nil)
        }
    
        @IBAction func secret(_ sender: UIBarButtonItem) {
        }
    
        func saveDate() {
        }
    }
    
[/code]

`ViewController`の方もやはりスッキリさせることが出来ます。

[code]

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var characterImage: UIImageView!
        @IBOutlet weak var timerlabel: UILabel!
        @IBOutlet weak var startButton: UIButton!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            //最初に呼び出される`ViewController`の中でデータモデルの初期化を行う
            SkillModel.shared.load()
    
            startButton.layer.cornerRadius = 25.0
        }
    
        override func viewWillAppear(_ animated: Bool) {
            guard let skill = SkillModel.shared.currentSkill else {
                return
            }
            self.title = skill.title
            timerlabel.text = secondsToGoalTimerLabel(skill.goalCount)
        }
    }
    
[/code]

* * *

いくつかの問題はそのままにしてあります。

  * アプリを終了したあとの次の起動では常に空っぽの画面からスタートする

(要はデータが保存されていない。)

  * 追加されるスキルの時間が常に`00:00`になってしまう

  * 他にもこまごまとした問題があるかも…

1点目については`load()`と`save()`を適切に実装して、適切なタイミングで`save()`を呼んでやる必要があります。

2点目については、あなたがどのような動作にしたいのかが読めなかったので、わざと0にしてしまったのですが、どの部分を変えれば良いかはお分りいただけるのでは無いかと思います。

とりあえずお試しいただいた上で、何かあればまたお知らせください。

* * *

**追記**

* * *

**_自分で考えてstructにコードに組み込むことはできないです。_**

`struct`は一般的に「構造体」と訳されることが多いようですが、現在のSwiftでは非常に多用されるものです。 **よくできた** table
viewのSwift用サンプルでは必ず使用していると言っても過言ではありません。(残念ながら古いObjective-
C用のコードの文法をSwiftになおしただけとか、複数の配列を超絶努力でなんとか使っている、なんてあまりよくないサンプルもネットでの検索では上位に見つかります。良いサンプルを選べるようにしていって下さい。)

例えば、table
viewにタイトルと時間以外にアイコンかなにかの画像を表示したいと思ったら、(cellに`UIImageView`なんかを追加するだけでなく)モデルの側にも画像の情報を追加します。

[code]

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

何かcellとモデル側が常に二重化されて面倒なように見えますが、モデル側がアプリが扱いたい情報をモデル化したもの、cellの方はそれを表示するときのためだけのもの、と考えて下さい。

まずは、良いサンプルをたくさん見ましょう。Apple的には今は強いstruct推しなので、Apple的設計思想をちゃんと理解している方の作ったサンプルコードには必ずstructが使われているといっても過言ではありません。

* * *

**indices** と **contains**

indicesと言う単語はindexの複数形です。(わからない単語があれば、辞書を引く癖をつけましょう。)配列のアクセスで`array[i]`なんて書くときの`i`のことをインデックス(index)と呼ぶことがあるのはご存知でしょう。また要素10個の配列にインデックス`10`を指定すると実行時エラーになるのもご存知だと思います。この場合、「10は有効なインデックス」では無いわけです。

`indices`と言うのは有効なインデックス全体を表す何か(配列の場合は範囲型)を返してくれます。

`array.indices.contains(i)`が成立するときには「`i`は有効なインデックスに含まれている」、つまり「`array[i]`と言う添字アクセスが実行時エラーにならないことが保証される」わけです。

* * *

**一件追加して、それをcurrentにする**

もちろんこう書いても構いません。

[code]

        func addAndSetCurrent(_ newSkill: Skill) {
            skills.append(newSkill)
            currentIndex = skills.count - 1 //<- 最後の要素のインデックス
        }
    
[/code]

ただひっくり返して書いたのは、『「配列の添字が0で始まるということを考慮」すると順番に書くときに余計な`-
1`が必要になるんでそれを節約したかった』と言うより、「配列の添字が0で始まるということを考慮」すると、「次に追加される要素のインデックスは(追加する前の)`配列.count`で表される」と言うのが自然に頭に染み付いてしまっているからです。

同じ考え方は、ここにも使われています。

[code]

                if let text = alertTextFeld?.text {
                    let indexPath = IndexPath(row: SkillModel.shared.count, section: 0) //<-要素を追加する前の`count`が、追加される要素のインデックス
                    SkillModel.shared.add(Skill(title: text, goalCount: 0))
                    self.tableView.insertRows(at: [indexPath], with: .automatic)
                }
    
[/code]

* * *

**_一件削除なら`currentIndex -= 1`ではいけない?_**

配列の要素を指しているインデックスを追加・削除などがある時に整合性を保つのは意外と大変です。`currentIndex`が削除される要素より前にあるなら`-=
1`なんてしちゃいけませんよね?

また`currentIndex ==
index`、つまり`currentIndex`で指されている要素自体が削除対象の場合、`currentIndex`はどんな値にすれば良いでしょう?
指された要素がなくなったんだから`nil`? 次の要素を指したいから`+= 1`? でも削除しちゃうんだから、`-=
1`も要るんで±で元の値のままでしょうか?
そうすると、配列の最後の要素を削除しちゃったら、`currentIndex`が配列の範囲外ってことになりませんか…。

コードで書くならこんな感じになります。

[code]

        func remove(at index: Int) {
            if let currentIndex = currentIndex {
                if currentIndex == index {
                    //currentが削除される要素そのもの
                    self.currentIndex = nil
                } else if currentIndex > index {
                    //currenが削除される要素より後ろ
                    self.currentIndex = currentIndex - 1
                }
            }
            skills.remove(at: index)
        }
    
[/code]

か、

[code]

        func remove(at index: Int) {
            if let currentIndex = currentIndex {
                if currentIndex == index {
                    //currentが削除される要素そのもの
                    if currentIndex == skills.count - 1 {
                        //currentが最後の要素だったら
                        self.currentIndex = nil
                    }
                } else if currentIndex > index {
                    //currenが削除される要素より後ろ
                    self.currentIndex = currentIndex - 1
                }
            }
            skills.remove(at: index)
        }
    
[/code]

この回答のコードでの`currentIndex`の役割は「Skill1個だけを表示するときの、その1個がどれか」覚えておくためだけのものです。そう言う画面に遷移する時には、必ずどれかを選択し直すので、頑張って管理してもあまり意味はないのですっぱり諦める、と言う方針で作ってあります。

例えば、「追加や削除を繰り返した後、どのセルも選択せずに元の画面に戻る」なんてことがあるのなら、面倒でもちゃんと管理しないといけません。ただ単純に`currentIndex
-= 1`ではダメですね。

* * *

**_Initializer for conditional binding must have Optional type, not 'Skill'_**

文字通りの意味ですね。この回答のコードで`currentSkill`は`Skill?`型、つまり`Optional<Skill>`型として宣言されていますから、`guard
let skill = SkillModel.shared.currentSkill else
{`のような条件付きバインディングが意味を持つんですが、`skill(at:)`は`Skill`型(Optionalじゃない)を返すと宣言されています。

したがって、`if let myTitle = SkillModal.shared.skill(at: 0)
{`と言う書き方は意味を持ちません。`SkillModal.shared.skill(at:
0)`は「クラッシュするか非nilの値を返すか」と言う操作になります。

あなたの元のコードで、その部分の条件判断が本当は何を判断したいのかまで理解できなかったのでそのままにしたのですが、

「どんな条件の時に`self.title =
〜`を実行したいのか」「どんな条件の時にalertを表示したいのか」をよく考えて、意味のある条件文にしてみて下さい。

* * *

**_計算型propety?_**

引数なしのメソッドでも、計算型プロパティでも同じことが実現できる時に、どっちを使うべきかと言うのは、綺麗な線引きは難しい問題だと思います。

私が計算型プロパティを使う基準は、

  * 重い処理を行わないこと
  * Swiftの標準ライブラリなんかでもプロパティになっているものと類似の機能を提供している

辺りでしょうか。ご指摘の箇所は、

  * 配列の`count`と同様の機能を提供している
  * 内部的にも配列の`count`をそのまま返すだけなんで軽い処理

と言う条件に合致するから、と言うところですね。

どの程度の処理を「重い」と判定するのかは、アプリ全体のいろんな処理を眺めてバランスをとりながら決める、と言ったところでしょう。


CC BY-SA 4.0
2019-07-06T17:49:01.533
56459
2019-07-08T03:19:01.317
2019-07-08T03:19:01.317
13972
13972
56425
answer