3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ページ送り出来る詳細画面

Last updated at Posted at 2019-05-02

ページ送り出来る詳細画面の実装方法のまとめです
詳細画面でページ送りしたら一覧画面のcellの位置も同期させるように実装します

前置き

一覧のAPIと詳細のAPIで別れているような場合は実現が難しい気がします
詳細画面は一覧のAPIから大まかな表示が出来るようにして、詳細画面で必要な追加情報は個別のAPIを呼んで表示させるような設計が望ましいでしょう

なお今回は記載しませんが、詳細画面のインタラクティブなトランジション(dismiss)を実装しておくと使いやすくなります

実装

詳細画面を管理するPageViewControllerを実装します

一覧画面のリストに更新があった時(Paginateなど)、更新メソッド(updatePlaylist)を呼び最新のデータを反映させます
詳細画面をページ送りで遷移させた時、delegateで一覧画面に通知することで一覧画面のcellの位置を同期させることが出来ます

細かい実装は省略します(コメント参照)

Sample.swift
// 画面に表示するデータ
public struct Sample {
    public let ~~~
}
SampleNavigatorViewController.swift

internal protocol SampleNavigatorDelegate: class {
    /// PageViewControllerの遷移が完了した時に呼ばれる
    func transitionedToSample(at index: Int)
}

// 詳細画面を管理するPageViewController
class SampleNavigatorViewController: UIPageViewController {

    fileprivate weak var navigatorDelegate: SampleNavigatorDelegate?
    fileprivate let pageDataSource: SampleNavigatorPagesDataSource!

    internal static func configuredWith(
        sample: Sample,
        initialPlaylist: [Sample]? = nil,
        navigatorDelegate: SampleNavigatorDelegate?) -> SampleNavigatorViewController {
        let vc = SampleNavigatorViewController(
            initialSample: sample,
            initialPlaylist: initialPlaylist,
            navigatorDelegate: navigatorDelegate
        )
        vc.setViewControllers(
            [.init()],
            direction: .forward,
            animated: true,
            completion: nil
        )
        return vc
    }
    
    private init(initialSample: Sample,
                 initialPlaylist: [Sample]?,
                 navigatorDelegate: SampleNavigatorDelegate?) {
        
        self.pageDataSource = SampleNavigatorPagesDataSource(initialPlaylist: initialPlaylist,
                                                             initialSample: initialSample)
        self.navigatorDelegate = navigatorDelegate

        super.init(transitionStyle: .scroll,
                   navigationOrientation: .horizontal,
                   options:[UIPageViewController.OptionsKey.interPageSpacing: 6])
    }
    
    internal required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.dataSource = self.pageDataSource
        self.delegate = self
        self.view.backgroundColor = .white

        self.setInitialPagerViewController()
    }

    /// 呼び出し元のViewControllerはリストが更新される度にこのメソッドを呼ぶ必要がある
    internal func updatePlaylist(_ playlist: [Sample]) {
        self.pageDataSource.updatePlaylist(playlist)
    }

    fileprivate func setInitialPagerViewController() {
        guard let navController = self.pageDataSource.initialController() else { return }
        self.setViewControllers([navController], direction: .forward, animated: false, completion: nil)
    }
}

extension SampleNavigatorViewController: UIPageViewControllerDelegate {
    
    internal func pageViewController(_ pageViewController: UIPageViewController,
                                     didFinishAnimating finished: Bool,
                                     previousViewControllers: [UIViewController],
                                     transitionCompleted completed: Bool) {
        // `completed` がtrueなら、
        // SampleNavigatorDelegateの transitionedToSample(at:) を呼び、
        // pageViewController(_:willTransitionTo:) で取得した `newIndex` を渡すように実装する
    }
    
    internal func pageViewController(
        _ pageViewController: UIPageViewController,
        willTransitionTo pendingViewControllers: [UIViewController]) {
        
        guard let nav = pendingViewControllers.first as? UINavigationController else { return }
        let newIndex = self.pageDataSource.indexFor(controller: nav)

        // `newIndex` を変数に保持するように実装する
    }
}

DataSourceクラスでページ(詳細画面)の取得・作成・削除を行います
pageViewController(_:viewControllerBefore)pageViewController(_:viewControllerAfter)で前後の画面を作成します

SampleNavigatorPagesDataSource
import UIKit

internal final class SampleNavigatorPagesDataSource: NSObject, UIPageViewControllerDataSource {
    
    fileprivate let initialSample: Sample
    fileprivate var playlist: [Sample] = []
    fileprivate var viewControllers: [UIViewController?] = []

    init(initialPlaylist: [Sample]?, initialSample: Sample) {
        self.initialSample = initialSample
        self.playlist = initialPlaylist ?? [initialSample]
        
        super.init()
        
        self.padControllers(toLength: self.playlist.count)
    }
    
    internal func updatePlaylist(_ playlist: [Sample]) {
        self.playlist = playlist
    }
    
    internal func initialController() -> UIViewController? {
        return self.playlist.index(of: self.initialSample).flatMap(self.controllerFor(index:))
    }
    
    internal func initialDetailController() -> SampleDetailViewController? {
        return self.playlist.index(of: self.initialSample).flatMap(self.sampleDetailControllerFor(index:))
    }
    
    internal func controllerFor(index: Int) -> UIViewController? {
        guard index >= 0 && index < self.playlist.count else { return nil }
        
        let sample = self.playlist[index]
        self.padControllers(toLength: index)
        
        self.viewControllers[index] = self.viewControllers[index]
            ?? self.createViewController(forSample: sample)
        return self.viewControllers[index]
    }
    
    internal func sampleDetailControllerFor(index: Int) -> SampleDetailViewController? {
        return self.controllerFor(index: index)
            .flatMap { $0 as? UINavigationController }
            .flatMap { $0.viewControllers.first as? SampleDetailViewController }
    }
    
    internal func indexFor(controller: UIViewController) -> Int? {
        return self.viewControllers.index { $0 == controller }
    }
    
    internal func sampleFor(controller: UIViewController) -> Sample? {
        return self.indexFor(controller: controller).map { self.playlist[$0] }
    }
    
    internal func pageViewController(_ pageViewController: UIPageViewController,
                                     viewControllerBefore viewController: UIViewController) -> UIViewController? {
        
        guard let pageIdx = self.viewControllers.index(where: { $0 == viewController }) else {
            fatalError("Couldn't find \(viewController) in \(self.viewControllers)")
        }
        
        let previousPageIdx = pageIdx - 1
        guard previousPageIdx >= 0 else {
            return nil
        }
        
        let sample = self.playlist[previousPageIdx]
        self.padControllers(toLength: previousPageIdx)
        self.viewControllers[previousPageIdx] = self.viewControllers[previousPageIdx]
            ?? self.createViewController(forSample: sample)
        
        self.clearViewControllersFarAway(fromIndex: previousPageIdx)
        
        return self.viewControllers[previousPageIdx]
    }
    
    internal func pageViewController(_ pageViewController: UIPageViewController,
                                     viewControllerAfter viewController: UIViewController) -> UIViewController? {
     
        guard let pageIdx = self.viewControllers.index(where: { $0 == viewController }) else {
            fatalError("Couldn't find \(viewController) in \(self.viewControllers)")
        }
        
        let nextPageIdx = pageIdx + 1
        guard nextPageIdx < self.playlist.count else {
            return nil
        }
        
        let sample = self.playlist[nextPageIdx]
        self.padControllers(toLength: nextPageIdx)
        self.viewControllers[nextPageIdx] = self.viewControllers[nextPageIdx]
            ?? self.createViewController(forSample: sample)
        
        self.clearViewControllersFarAway(fromIndex: nextPageIdx)
        
        return self.viewControllers[nextPageIdx]
    }
    
    fileprivate func createViewController(forSample: Sample) -> UIViewController {
        return UINavigationController(
            rootViewController: SampleDetailViewController.configuredWith(
                sample: forSample
            )
        )
    }
    
    fileprivate func clearViewControllersFarAway(fromIndex index: Int) {
        // 前後のページ以外は必要ないので削除する
        self.viewControllers.indices
            .filter { abs($0 - index) >= 3 }
            .forEach { idx in
                self.viewControllers[idx] = nil
        }
    }
    
    fileprivate func padControllers(toLength length: Int) {
        guard self.viewControllers.count <= length else { return }
        
        (self.viewControllers.count...length).forEach { _ in
            self.viewControllers.append(nil)
        }
    }
}

一覧画面
こちらも細かい実装は省略します(コメント参照)

class SamplesViewController: UITableViewController {
    // viewDidLoadなどの実装

    // tableView(_:didSelectRowAt)で呼ぶように実装する
    // tapしたSampleとリスト全体のSample配列を渡す
    private func goTo(sample: Sample, initialPlaylist: [Sample]) {
        let vc = SampleNavigatorViewController.configuredWith(sample: sample,
                                                              initialPlaylist: initialPlaylist,
                                                              navigatorDelegate: self)
        self.present(vc, animated: true, completion: nil)
    }

    // 一覧画面のリストに更新があった時(Paginateなど)、このメソッドを呼ぶように実装する
    private func updateSamplePlaylist(_ playlist: [Sample]) {
        guard let navigator = self.presentedViewController as? SampleNavigatorViewController else { return }
        navigator.updatePlaylist(playlist)
    }
}

extension SamplesViewController: SampleNavigatorDelegate {
    func transitionedToSample(at index: Int) {
        self.tableView.scrollToRow(at: self.dataSource.indexPath(forSampleRow: index),
                                   at: .top,
                                   animated: false)
    }
}
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?