Slather logo

Coverage for "UISplitViewController+Action.swift" : 100.00%

(81 of 81 relevant lines covered)

RouteComposer/Classes/Actions/UISplitViewController+Action.swift

1
//
2
// RouteComposer
3
// UISplitViewController+Action.swift
4
// https://github.com/ekazaev/route-composer
5
//
6
// Created by Eugene Kazaev in 2018-2022.
7
// Distributed under the MIT license.
8
//
9
// Become a sponsor:
10
// https://github.com/sponsors/ekazaev
11
//
12
13
import Foundation
14
import UIKit
15
16
// MARK: Actions for UISplitViewController
17
18
public extension ContainerViewController where Self: UISplitViewController {
19
20
    // MARK: Steps
21
22
    /// Presents a view controller as a master in the `UISplitViewController`
23
    static func setAsMaster() -> SplitViewControllerActions.SetAsMasterAction<Self> {
5x
24
        SplitViewControllerActions.SetAsMasterAction()
5x
25
    }
5x
26
27
    /// Presents a view controller as a detail in the `UISplitViewController`, *replacing* the previous detail.
28
    static func pushToDetails() -> SplitViewControllerActions.PushToDetailsAction<Self> {
9x
29
        SplitViewControllerActions.PushToDetailsAction()
9x
30
    }
9x
31
32
    /// Pushes a view controller *onto* the detail stack in the `UISplitViewController`. Requires the root detail view
33
    /// controller to be the `UINavigationController`
34
    static func pushOnToDetails() -> RouteComposer.SplitViewControllerActions.PushOnToDetailsAction<Self> {
9x
35
        SplitViewControllerActions.PushOnToDetailsAction()
9x
36
    }
9x
37
38
}
39
40
/// Actions for `UISplitViewController`
41
public enum SplitViewControllerActions {
42
43
    // MARK: Internal entities
44
45
    /// Presents a master view controller in the `UISplitViewController`
46
    public struct SetAsMasterAction<ViewController: UISplitViewController>: ContainerAction {
47
48
        // MARK: Methods
49
50
        /// Constructor
51
        init() {}
5x
52
53
        public func perform(embedding viewController: UIViewController, in childViewControllers: inout [UIViewController]) throws {
4x
54
            integrate(viewController: viewController, in: &childViewControllers)
4x
55
        }
4x
56
57
        public func perform(with viewController: UIViewController,
58
                            on splitViewController: ViewController,
59
                            animated: Bool,
60
                            completion: @escaping (_: RoutingResult) -> Void) {
1x
61
            integrate(viewController: viewController, in: &splitViewController.viewControllers)
1x
62
            completion(.success)
1x
63
        }
1x
64
65
        private func integrate(viewController: UIViewController, in childViewControllers: inout [UIViewController]) {
5x
66
            if childViewControllers.isEmpty {
5x
67
                childViewControllers.append(viewController)
3x
68
            } else {
5x
69
                childViewControllers[0] = viewController
2x
70
            }
5x
71
        }
5x
72
73
    }
74
75
    /// Presents a detail view controller in the `UISplitViewController`, *replacing* the previous detail.
76
    public struct PushToDetailsAction<ViewController: UISplitViewController>: ContainerAction {
77
78
        // MARK: Methods
79
80
        /// Constructor
81
        init() {}
9x
82
83
        public func perform(embedding viewController: UIViewController, in childViewControllers: inout [UIViewController]) throws {
4x
84
            guard !childViewControllers.isEmpty else {
4x
85
                throw RoutingError.compositionFailed(.init("Master view controller is not set in " +
1x
86
                        "UISplitViewController to present a detail view controller \(viewController)."))
1x
87
            }
3x
88
            childViewControllers.append(viewController)
3x
89
        }
3x
90
91
        public func perform(with viewController: UIViewController,
92
                            on splitViewController: ViewController,
93
                            animated: Bool,
94
                            completion: @escaping (_: RoutingResult) -> Void) {
5x
95
            guard !splitViewController.viewControllers.isEmpty else {
5x
96
                completion(.failure(RoutingError.compositionFailed(.init("Master view controller is not set in " +
1x
97
                        "\(splitViewController) to present a detail view controller \(viewController)."))))
1x
98
                return
1x
99
            }
4x
100
4x
101
            splitViewController.showDetailViewController(viewController, sender: nil)
4x
102
            completion(.success)
4x
103
        }
4x
104
    }
105
106
    /// Pushes a view controller *onto* the detail stack in the `UISplitViewController`, where the detail is a `UINavigationController`
107
    public struct PushOnToDetailsAction<ViewController: UISplitViewController>: ContainerAction {
108
109
        // MARK: Methods
110
111
        /// Constructor
112
        init() {}
9x
113
114
        public func perform(embedding viewController: UIViewController, in childViewControllers: inout [UIViewController]) throws {
5x
115
            guard !childViewControllers.isEmpty else {
5x
116
                throw RoutingError.compositionFailed(.init("Master view controller is not set in " +
1x
117
                        "UISplitViewController to push on a detail view controller \(viewController)."))
1x
118
            }
4x
119
            if childViewControllers.count > 1,
4x
120
               let navigationController = childViewControllers.last as? UINavigationController {
4x
121
                guard !(viewController is UINavigationController) else {
2x
122
                    throw RoutingError.compositionFailed(.init("The navigation controller is already set as a detail view controller root."))
1x
123
                }
1x
124
                navigationController.viewControllers = Array([navigationController.viewControllers, [viewController]].joined())
1x
125
            } else {
3x
126
                guard viewController is UINavigationController else {
2x
127
                    throw RoutingError.compositionFailed(.init("Action requires a `UINavigationController` to be set as a detail view controller root. " +
1x
128
                            "Got \(viewController) instead."))
1x
129
                }
1x
130
                childViewControllers.append(viewController)
1x
131
            }
2x
132
        }
2x
133
134
        public func perform(with viewController: UIViewController,
135
                            on splitController: ViewController,
136
                            animated: Bool,
137
                            completion: @escaping (_: RoutingResult) -> Void) {
4x
138
            guard !splitController.viewControllers.isEmpty else {
4x
139
                completion(.failure(RoutingError.compositionFailed(.init("Master view controller is not set in " +
1x
140
                        "\(splitController) to push on a detail view controller \(viewController)."))))
1x
141
                return
1x
142
            }
3x
143
3x
144
            guard let navigationController = (splitController.viewControllers.last as? UINavigationController) else {
3x
145
                completion(.failure(RoutingError.compositionFailed(.init("Detail navigation controller is not set in " +
1x
146
                        "\(splitController) to push on a detail view controller \(viewController)."))))
1x
147
                return
1x
148
            }
2x
149
            if navigationController.viewControllers.isEmpty {
2x
150
                navigationController.setViewControllers([viewController], animated: animated)
1x
151
            } else {
2x
152
                navigationController.pushViewController(viewController, animated: animated)
1x
153
            }
2x
154
            completion(.success)
2x
155
        }
2x
156
    }
157
158
}