質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.34%

[SWIFT]設定画面からカスタムセルの色を変更したい。

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 205

kee4234

score 2

前提・実現したいこと

設定画面からアラートをクリックしてユーザーデフォルトに値を保存しつつカスタムセルのViewの色を変更したい。

発生している問題・エラーメッセージ

UIView Unexpectedly found nil while implicitly unwrapping an Optional value

カスタムセルのソースコード(必要部分抜粋)

import UIKit

class StickyCell: UITableViewCell {

    @IBOutlet weak var stickyBubble: UIView!
    @IBOutlet weak var label: UILabel!

    let defaults = UserDefaults.standard

    override func awakeFromNib() {
        super.awakeFromNib()

        let cellColor = defaults.color(forKey: "CellColor")

        if (cellColor != nil){
            stickyBubble.backgroundColor = cellColor
        }
    }
}

メイン画面のソースコード

import UIKit
import CoreData
import GrowingTextView

class ViewController: UIViewController,UITableViewDelegate,UIGestureRecognizerDelegate {

    @IBOutlet weak var inputToolbar: UIView!
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var textView: GrowingTextView!
    @IBOutlet weak var textViewBottomConstraint: NSLayoutConstraint!

    var itemArray = [Item]()
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

    let defaults = UserDefaults.standard

    override func viewDidLoad() {
        super.viewDidLoad()

        // *** Customize GrowingTextView ***
        textView.layer.cornerRadius = 4.0
        tableView.contentInset = UIEdgeInsets(top: 15,left: 0,bottom: 15,right: 0)

        print(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask))

        textView.delegate = self
        tableView.dataSource = self
        tableView.delegate = self

        tableView.register(UINib(nibName: "StickyCell", bundle: nil), forCellReuseIdentifier: "ReusableCell")

        loadItems()

        // ロングプレス
        let longPressGesture =
            UILongPressGestureRecognizer(target: self,
                                         action: #selector(ViewController.longPress(_:)))

        longPressGesture.delegate = self
        self.view.addGestureRecognizer(longPressGesture)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)


    }

    // Long Press イベント
    @objc func longPress(_ sender: UILongPressGestureRecognizer){
        let point = sender.location(in: tableView)
        let indexPath = tableView.indexPathForRow(at: point)
        if sender.state == .began {
            let alert: UIAlertController = UIAlertController(title: "メッセージの削除", message: "削除してもいいですか?", preferredStyle:  UIAlertController.Style.alert)

            let defaultAction: UIAlertAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler:{
                (action: UIAlertAction!) -> Void in
                print(indexPath!.row)
                self.context.delete(self.itemArray[indexPath!.row])
                self.itemArray.remove(at: indexPath!.row)

                self.saveItems()
            })

            let cancelAction: UIAlertAction = UIAlertAction(title: "キャンセル", style: UIAlertAction.Style.cancel, handler:{
                (action: UIAlertAction!) -> Void in
                print("Cancel")
            })

            alert.addAction(cancelAction)
            alert.addAction(defaultAction)

            present(alert, animated: true, completion: nil)
        }
    }

    @IBAction func addButton(_ sender: UIButton) {
        loadMessages()
    }

    func textFieldShouldReturn(_ textView: UITextField) -> Bool {
        loadMessages()

        return false
    }


    func loadMessages() {
        if textView.text == "" {
            print("error")
        } else {

            let newItem = Item(context: context)
            newItem.text = textView.text!

            itemArray.append(newItem)

            textView.resignFirstResponder()

            saveItems()
        }
        DispatchQueue.main.async {
            self.textView.text = ""
            let indexPath = IndexPath(row: self.itemArray.count - 1, section: 0)
            self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
        }
    }

    func saveItems() {

        do{
            try context.save()
        } catch {
            print("Error saving context \(error)")
        }

        tableView.reloadData()

    }

    func loadItems() {
        let request : NSFetchRequest<Item> = Item.fetchRequest()
        do {
            itemArray = try context.fetch(request)
        } catch {
            print("Error fetching data from context \(error)")
        }
    }

    @IBAction func settingButtonAction(_ sender: UIBarButtonItem) {
        performSegue(withIdentifier: "goSetting", sender: nil)
    }

    func cellUpdate(){

        itemArray.removeAll()
        print("remove")

        loadItems()

        DispatchQueue.main.async {
            self.tableView.reloadData()
        }

    }

}

extension ViewController: UITableViewDataSource{
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return itemArray.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableCell", for: indexPath) as! StickyCell
        cell.label.text = itemArray[indexPath.row].text
        return cell
    }
}

extension UserDefaults {

    func color(forKey key: String) -> UIColor? {

        guard let colorData = data(forKey: key) else { return nil }

        do {
            return try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: colorData)
        } catch let error {
            print("color error \(error.localizedDescription)")
            return nil
        }

    }

    func set(_ value: UIColor?, forKey key: String) {

        guard let color = value else { return }
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: false)
            set(data, forKey: key)
        } catch let error {
            print("error color key data not saved \(error.localizedDescription)")
        }

    }

}

extension ViewController: GrowingTextViewDelegate {

    // *** Call layoutIfNeeded on superview for animation when changing height ***

    func textViewDidChangeHeight(_ textView: GrowingTextView, height: CGFloat) {
        UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.7, options: [.curveLinear], animations: { () -> Void in
            self.view.layoutIfNeeded()
        }, completion: nil)
    }
}

設定画面のソースコード(必要部分抜粋)

import UIKit

class SettingTableViewController: UITableViewController {

    let defaults = UserDefaults.standard

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let alert: UIAlertController = UIAlertController(title: "セルカラー", message: "", preferredStyle:  UIAlertController.Style.alert)

            let defaultAction_1: UIAlertAction = UIAlertAction(title: "標準", style: UIAlertAction.Style.default, handler:{
                (action: UIAlertAction!) -> Void in
                self.defaults.set(originalGreen, forKey: "CellColor")
                let cellColor = self.defaults.color(forKey: "CellColor")

                let stickyCell = StickyCell()
                stickyCell.stickyBubble.backgroundColor = cellColor

            })

            alert.addAction(defaultAction_1)

            present(alert, animated: true, completion: nil)
    }
}

問題となっているコード

let stickyCell = StickyCell()
stickyCell.stickyBubble.backgroundColor = cellColor

試したこと(設定画面アラートクリックアクション)

アラートクリックで色を変更しようとしましたが上記のエラーでクラッシュしてしまいます。
カスタムセル上でstickyCell.stickyBubble.backgroundColorをプリントした際はnilではない。

アプリを再起動した際にはセルの色が変更されているのでユーザーデフォルトへの保存までは成功しております。

ユーザーデファルトに値は保存されているのでメインの画面に戻りセルをリロードして色を変更しようと試しましたが新しいセルを追加した際は色が変更されるが既存のセルは色が変更されておらずうまくいきませんでした。

補足情報

SWIFT初心者でいたらないこともあると思いますが何卒よろしくお願い申し上げます。

実現したいイメージ

最初の画面
イメージ説明

設定画面
イメージ説明

セルカラーをタップ後カラー変更アラート表示
イメージ説明

カラー変更アラートのカラーを選択でセルの色を変更
イメージ説明

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

0

    let stickyCell = StickyCell()
    stickyCell.stickyBubble.backgroundColor = cellColor

ここで UITableViewCell のインスタンスを作成していますが、class StickyCell: UITableViewCell の内部で

    @IBOutlet weak var stickyBubble: UIView!
    @IBOutlet weak var label: UILabel!

と、styckyBubble は @IBOutlet 、つまり StoryBoard 経由でインスタンス化した時に値が入るようになっています。

したがって、直接インスタンス化したのであれば、当然これらの値は nil となっていまいますので、別の方法を考える必要があります。

最終的に実現したいことがいまいち把握できていないのですが、セルをタップしてアラートを出した後、最終的にどのような状態にしたいのかもう少し明確にしていただければ、解決法が出るかもしれません。

修正案(追記)

awakeFromNib()でセル背景の色を設定しても、それは xib を登録する時にしか呼び出されないと思いますので、tableView でセルを生成する時に色を変更してはどうでしょうか。

条件には書かれていませんが、スクリーンショットから、画面遷移は Navigation Controller で行っていると仮定します。

たとえば、

//  StickyCell.swift
import UIKit

class StickyCell: UITableViewCell {
    @IBOutlet weak var stickyBubble: UIView!
    @IBOutlet weak var label: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
}

とし、

//  SettingTableViewController.swift
import UIKit

class SettingTableViewController: UITableViewController {

    let defaults = UserDefaults.standard

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // 中略
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let alert: UIAlertController = UIAlertController(title: "セルカラー", message: "", preferredStyle:  .actionSheet)

        let defaultAction1: UIAlertAction = UIAlertAction(title: "オレンジ", style: UIAlertAction.Style.default, handler:{
            (action: UIAlertAction!) -> Void in
            self.defaults.set(.orange, forKey: "CellColor")
        })

        let defaultAction2: UIAlertAction = UIAlertAction(title: "グリーン", style: UIAlertAction.Style.default, handler:{
            (action: UIAlertAction!) -> Void in
            self.defaults.set(.green, forKey: "CellColor")
        })

        alert.addAction(defaultAction1)
        alert.addAction(defaultAction2)

        present(alert, animated: true, completion: nil)
    }
    // 後略
}

のように、UIAlert で色変更する部分も UserDefaults に保存するだけにします。

ViewController では、

// ViewController.swift
import UIKit
import GrowingTextView

class ViewController: UIViewController, UITableViewDelegate {
    // 中略
    // セル背景色
    var cellColor: UIColor?

    // 追加
    // ビューが表示される直前に UserDefaults から色を読み込み、変数にセットし、セルをリロードする
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        cellColor = defaults.color(forKey: "CellColor")
        tableView.reloadData()
    }
    // 中略
}
extension ViewController: UITableViewDataSource{
    // 中略
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableCell", for: indexPath) as! StickyCell
        cell.label.text = itemArray[indexPath.row].text
        // 追加
        // セルを表示する時に背景色をセットする
        cell.stickyBubble.backgroundColor = cellColor
        return cell
    }
}

コメントにもあるように、ViewWillAppear(_:) でセルの背景色を読み込み、tableView(_:cellForRowAt:)でセルを生成する時に背景色をセットします。

こんな感じかと思います。 

注意しなければいけないのは、iOS13 以降の設定で、かつもしNavigationController を使わずに画面遷移し、表示が Pop Over だった場合には、元の画面に戻った時にViewWillAppear(_:)が呼び出されません。

なので、そのような場合には Full Screen で遷移させるか、あるいは適切な関連記事を読んでいただいて、元の画面に戻ったときの処理を設定していただければと思います。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/09/03 16:49

    文字の制限で設定画面のコードは一部省略しておりますがメインの画面のソースコード載せさせていただいたのですがこちらでいかがでしょうか?
    お手数おかけいたしますが何卒よろしくお願いいたします。

    キャンセル

  • 2020/09/03 18:16

    変更案を回答本文に追記しましたので、ご確認いただけますでしょうか。

    キャンセル

  • 2020/09/03 21:24

    TsukubaDepot様
    いただいた内容でコードを修正したころ希望の動きに対応することが出来ました。
    いろいろ試してみて自分では解決出来ず諦めかけていたので本当に助かりました。
    これでまた開発が進めることができます。
    ありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.34%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る