

# 前提・実現したいこと

Swift3.0でTableViewのセルをタップして **別のStoryboardにあるViewController** に画面遷移させたいのですが，  
画面遷移させる以下のメソッド

[code]

        present(viewController, animated: true, completion: nil)
    
[/code]

がTableViewクラスで実装できません．  
このような場合はTableViewControllerを使うしか方法はないのでしょうか？  
ご回答いただけると助かります。

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

[code]

        Use of unresolved identifier 'present'
    
[/code]

# 該当のソースコード

[code]

        /*
         * FolderTableView.swift
         */
        import UIKit
    
        class FolderTableView: NSObject, UITableViewDelegate, UITableViewDataSource {
    
            // 途中省略
    
            func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
                let storyboard: UIStoryboard = UIStoryboard(name: "Task", bundle: nil)
    
                let taskListViewController =  storyboard.instantiateViewController(withIdentifier: "Task")
    
                self.present(taskListViewController, animated: true, completion: nil)
                 ↑このメソッドで画面遷移させたい
            }
    
        }
    
        /*
         * FolderListViewController.swift
         */
        import UIKit
        import RealmSwift
    
        class FolderListViewController: UIViewController {
    
            @IBOutlet weak var folderTableView: UITableView!
    
            let folderTable = FolderTableView()
    
            override func viewDidLoad() {
                super.viewDidLoad()
    
                folderTableView.delegate = folderTable
                folderTableView.dataSource = folderTable
    
            }
    
            // 省略    
    
        }
    
[/code]

# 試したこと

・TableViewを呼び出しているFolderListViewController内でpresentメソッドを実装  
→セルをタップした時にfolderTableView.delegate = folderTableの行で以下のエラーが発生

[code]

    fatal error: unexpectedly found nil while unwrapping an Optional value
    
[/code]

・presentメソッド用のViewControllerクラスを作成し，FolderTableViewでメソッドを呼び出す  
→セルをタップした時に以下のエラーが発生

[code]

    Warning: Attempt to present <skillup7.TaskListViewController: 0x7f98eb423650> on <skillup7.TestViewController: 0x7f98eb522230> whose view is not in the window hierarchy!
    
[/code]

↓↓用意したViewControllerクラス↓↓

[code]

    /*
     * TestViewController.swift
     */
    import UIKit
    
    class TestViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Do any additional setup after loading the view.
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
        func send(viewController: UIViewController) {
            self.present(viewController, animated: true, completion: nil)
        }
    
    }
    
[/code]

# 補足情報(言語/FW/ツール等のバージョンなど)

言語：Swift 3.0


CC BY-SA 3.0
2017-07-06T16:08:24.963
36160
2017-08-06T12:46:43.070
2017-08-06T12:46:43.070
76
23465
question
swift ios swift3
画面遷移はViewControllerからじゃないとできない？


**_画面遷移はViewControllerからじゃないとできない？_**  
→Yes

**_このような場合はTableViewControllerを使うしか方法はないのでしょうか？_**  
(`UITableViewController`の意味なら)→No  
(`UITableView`を表示しているViewControllerの意味なら)→Yes

**_・TableViewを呼び出しているFolderListViewController内でpresentメソッドを実装_**

現在提示されているコードには、 **FolderListViewController内でpresentメソッドを実装**
している部分がないのでなんとも言えません。

**・presentメソッド用のViewControllerクラスを作成し，FolderTableViewでメソッドを呼び出す**

これは必ず失敗します。`present`メソッドを呼び出すViewControllerクラスのインスタンスは、必ず「現在画面表示を担当しているもの」でないといけません。たとえ同一クラスであっても、画面表示を担当しているインスタンスとは別のインスタンスを作成してしまうと失敗します。

* * *

上記のように試したことの中身にはよくわからない点もあるのですが、要は`present`メソッドは「現在画面表示を担当しているViewControllerインスタンス」から呼ばないといけないので、そのためのプロパティを`FolderTableView`に付け加えると言うのが手っ取り早いでしょう。

なお、コメントに示したようにViewではないクラスに`...View`と言うクラス名をつけておくのはあまりにも気持ち悪いので、以下のコード例では勝手に`FolderTable`にリネームしてあります。

[code]

    /*
     * FolderTable.swift
     */
    import UIKit
    
    class FolderTable: NSObject, UITableViewDelegate, UITableViewDataSource {
    
        ///現在画面表示を担当しているViewControllerインスタンスを保持しておくプロパティ
        weak var owner: UIViewController?
    
        // 途中省略
    
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
            let storyboard: UIStoryboard = UIStoryboard(name: "Task", bundle: nil)
    
            let taskListViewController =  storyboard.instantiateViewController(withIdentifier: "Task")
    
            //現在画面表示を担当しているViewControllerインスタンスに対して`present(_:animated:completion:)`メソッドを呼ぶ
            owner?.present(taskListViewController, animated: true, completion: nil)
        }
    }
    
    /*
     * FolderListViewController.swift
     */
    import UIKit
    import RealmSwift
    
    class FolderListViewController: UIViewController {
    
        @IBOutlet weak var folderTableView: UITableView!
    
        let folderTable = FolderTable()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            //`folderTable`に「現在画面表示を担当しているViewControllerインスタンス」(=== self)を設定しておく
            folderTable.owner = self
    
            folderTableView.delegate = folderTable
            folderTableView.dataSource = folderTable
    
        }
    
        // 省略
    
    }
    
[/code]

* * *

私的には、そんなに苦労してまで`UITableViewDelegate`,
`UITableViewDataSource`を別クラスに実装するのは、メリットとデメリットを比べてデメリットの方がはるかに多いように思われます。iOSプログラミングでは、ViewControllerが肥大しがちなので、それを避けたかったのかもしれませんが、Swiftではextensionが非常に使いやすくなっていますから、その方がはるかにコードはスッキリすると思うのですが。

[code]

    class FolderListViewController: UIViewController {
    
        @IBOutlet weak var folderTableView: UITableView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            folderTableView.delegate = self
            folderTableView.dataSource = self
    
        }
    
        // 省略
    
    }
    
    extension FolderListViewController: UITableViewDelegate, UITableViewDataSource {
    
        // 途中省略
    
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
            let storyboard: UIStoryboard = UIStoryboard(name: "Task", bundle: nil)
    
            let taskListViewController =  storyboard.instantiateViewController(withIdentifier: "Task")
    
            self.present(taskListViewController, animated: true, completion: nil)
        }
    
    }
    
[/code]

extensionにはプロパティなどは宣言できませんが、`UITableViewDelegate`,
`UITableViewDataSource`を実装しやすく構成したモデルクラスをしっかり設計して別クラスにすることで、大きな問題にはならないだろうと思います。


CC BY-SA 3.0
2017-07-06T21:11:26.630
36164
2017-07-06T21:11:26.630
13972
36160
answer


present(_:animated:completion:) 関数は
[UIViewController](https://developer.apple.com/documentation/uikit/uiviewcontroller)のメソッドなので、呼び出せるのは
**UIViewController もしくは UIViewControllerを継承するクラスのみ** となります。

「Use of unresolved identifier
‘present’」というエラーメッセージは、FolderTableViewクラスに「present」と名のつくメソッドが実装されていないことを意味します。存在しないものを呼び出しているので、エラーとなるわけです。

present(_:animated:completion:) 関数を使用できないクラス内での処理をきっかけに
present関数を実行したい場合には、カスタムデリゲートを作成すると便利です。例えば、このような新しいプロトコルを作成します。

[code]

    import UIKit
    
    protocol FolderTableViewTransitionDelegate {
      func goToTaskListScene()
    }
    
[/code]

そして、遷移実行のきっかけをつくるクラスに上記デリゲートのプロパティを作成し、実際に遷移実行のトリガーとなるメソッド内で、デリゲートのメソッドを呼びます。

[code]

    class FolderTableView: NSObject, UITableViewDelegate, UITableViewDataSource {
    
      // デリゲートプロトコルを参照するプロパティ（オプショナルにし、ここでは値を入れない）
      var delegate: FolderTableViewTransitionDelegate?
    
      // テーブルビューのセル選択時に呼ばれる UITableViewDelegate メソッド
      func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
        // カスタムデリゲートメソッドを呼ぶ
        delegate?.goToTaskListScene()
      }
    
      //以下略
    }
    
[/code]

次に、遷移処理を担うコントローラクラスを、先ほど作成したプロトコルに準拠させます。そして、デリゲートプロパティに自身を設定します。さらに、実際に遷移処理を行うメソッドも実装します。

[code]

    // プロトコル名を追加し、このクラスを準拠させる
    class FolderListViewController: UIViewController, FolderTableViewTransitionDelegate {
    
      @IBOutlet weak var folderTableView: UITableView!
    
      private let folderTable = FolderTableView()
    
    
      //MARK:- UIViewController メソッド
    
      override func viewDidLoad() {
        super.viewDidLoad()
    
        // デリゲートプロパティにこのクラスを設定
        folderTable.delegate = self
    
        folderTableView.delegate = folderTable
        folderTableView.dataSource = folderTable
      }
    
    
      //MARK:- FolderTableViewTransitionDelegate メソッド
    
      // 遷移を実行するメソッド
      func goToTaskListScene() {
    
        let storyboard: UIStoryboard = UIStoryboard(name: “Task”, bundle: nil)
        let taskListViewController =  storyboard.instantiateViewController(withIdentifier: “Task”)
        self.present(taskListViewController, animated: true, completion: nil)
      }
    
    }
    
[/code]

以上のようにプロトコルを使用しデリゲート機能を実装することで、UIViewController もしくは UIViewController
を継承するクラス以外からでも、present(_:animated:completion:)メソッドによるモーダル遷移を実現できます。

present関数を使用するためだけに新たな
UIViewControllerのサブクラスを作る必要はありません。今回の場合は、既存のクラスを使用するほうが効率的ではないでしょうか。

といっても、他の回答やコメントにある皆さんのご指摘通り、今回のコードには根本的な問題があります。Appleはアプリ開発のプログラミングに、
**MVC（Model-View-Controller）デザインパターン** を推奨しています。今回のコードは
MVCの形に整理されていない節があり、クラス名なども推奨される **命名規則**
から外れているようです。MVCやSwiftの命名規則を理解してコードを書くと、エラーが減らせるはずです。また、より効率的かつ適切なサブクラスを作成できるようにもなるはずです。

なお、「fatal error: unexpectedly found nil while unwrapping an Optional
value」エラーの発生は今回の場合、アウトレットが正常にリンクされていないことが原因と思われます。まずは、Connections Inspector
等でアウトレットの接続状態を確認し、コードにあるアウトレットの folderTableViewと
storyboardのテーブルビューを正常につないでから、新しいコードを試すことをおすすめします。


CC BY-SA 3.0
2017-07-06T22:05:09.127
36166
2017-07-06T22:05:09.127
9833
36160
answer