-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathFluidStackController.swift
More file actions
1168 lines (879 loc) · 33.6 KB
/
Copy pathFluidStackController.swift
File metadata and controls
1168 lines (879 loc) · 33.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import SwiftUI
import UIKit
import ResultBuilderKit
import FluidPortal
/// Actions that comes from ``FluidStackController``
public enum FluidStackAction {
/// on started push operation in the stack
/// dispatches after viewDidLoad, added in hierarchy, before transitioning
case willPush
/// Potentially it won't be emmited after ``FluidStackAction/willPush``
case didPush
/// on started pop operation
case willPop
/// Potentially it won't be emmited after ``FluidStackAction/willPop``
case didPop
/// will become currenty top view controller on the stack.
/// that happens on push and pop higher view controller.
case willBecomeTop
}
/// A struct that configures how to display in ``FluidStackController``
public struct FluidStackContentConfiguration {
public enum ContentType {
/// Allows background view offloads.
case opaque
/// Leaves background view controller in the hierarchy.
case overlay
}
/// Specifies whether ``FluidStackController`` updates status bar appearance when displaying its target view controller.
public var capturesStatusBarAppearance: Bool = true
public var contentType: ContentType = .opaque
}
/// A container view controller that manages view controller and view as child view controllers.
/// It provides transitions when adding and removing.
///
/// You may create subclass of this to make a first view.
///
/// Passing an identifier on initializing, make it could be found in hierarchy.
/// Use ``UIViewController/fluidStackController(with: )`` to find.
open class FluidStackController: UIViewController {
public enum Action {
case onChanged(viewControllers: [UIViewController])
}
// MARK: - Properties
public private(set) var path: FluidStackPath = .init()
/// A closure that receives ``Action``
public final var stackActionHandler: (Action) -> Void = { _ in }
/// A configuration
public let stackConfiguration: Configuration
/// an string value that identifies the instance of ``FluidStackController``.
public var stackIdentifier: Identifier?
/// A content view that stays in back
public let contentView: UIView
/// The view controller at the top of the stack.
public var topViewController: UIViewController? {
return topItem?.viewController
}
/// An array of view controllers currently managed.
/// Might be different with ``UIViewController.children``.
public var stackingViewControllers: [UIViewController] {
stackingItems.map { $0.viewController }
}
private var topItem: StackingPlatterView? {
stackingItems.last
}
private(set) var stackingItems: [StackingPlatterView] = [] {
didSet {
if stackingItems != oldValue {
let components = stackingItems.map { item -> FluidStackPath.Component in
if let identifiable = item.viewController as? (any FluidIdentifiableViewController) {
return .identifiable(.init(identifiable))
} else {
return .volatile(.init(objectIdentifier: .init(item.viewController), ref: item.viewController))
}
}
self.path = .init(components: components)
// TODO: Update with animation
UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1) {
self.setNeedsStatusBarAppearanceUpdate()
}
.startAnimation()
stackingViewControllersDidChange(stackingViewControllers)
stackActionHandler(.onChanged(viewControllers: stackingViewControllers))
}
}
}
private var state: State = .init()
private let __rootView: UIView?
open override var childForScreenEdgesDeferringSystemGestures: UIViewController? {
return stackingItems.last?.viewController
}
open override var childForHomeIndicatorAutoHidden: UIViewController? {
return stackingItems.last?.viewController
}
open override var childForStatusBarStyle: UIViewController? {
return stackingItems.last {
$0.viewController.fluidStackContentConfiguration.capturesStatusBarAppearance == true
}?.viewController
}
open override var childForStatusBarHidden: UIViewController? {
return stackingItems.last {
$0.viewController.fluidStackContentConfiguration.capturesStatusBarAppearance == true
}?.viewController
}
open override func loadView() {
if let __rootView = __rootView {
view = __rootView
} else {
super.loadView()
}
}
// MARK: - Initializers
/// Creates an instance
/// - Parameters:
/// - identifier: ``Identifier-swift.struct`` to find the instance in hierarchy.
/// - view: a view that used in ``loadView()``
/// - contentView: a view that displays as first view in hierarchy of ``UIViewController/view``
/// - stackConfiguration: ``Configuration-swift.struct``
/// - rootViewController: Adds as a first content
public init(
identifier: Identifier? = nil,
view: UIView? = nil,
contentView: UIView? = nil,
configuration: Configuration = .init(),
rootViewController: UIViewController? = nil
) {
self.stackIdentifier = identifier
self.__rootView = view
self.contentView = contentView ?? .init()
self.stackConfiguration = configuration
super.init(nibName: nil, bundle: nil)
if let rootViewController = rootViewController {
addContentViewController(rootViewController, transition: .disabled)
}
}
@available(*, unavailable)
public required init?(
coder: NSCoder
) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Functions
open func stackingViewControllersDidChange(_ viewControllers: [UIViewController]) {
}
public func stackingDescription() -> String {
let body = stackingItems.map { item in
"- isLoaded: \(item.isLoaded ? "✅" : "⬜️"), \(item.viewController.debugDescription)"
}
.joined(separator: "\n")
return """
🪜 Stacking: \(stackingItems.count), \(self.debugDescription)
\(body)
"""
}
// MARK: - ViewController
/**
Make sure call super method when you create override.
*/
open override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "FluidStack.\(stackIdentifier?.rawValue ?? "unnamed")"
view.addSubview(contentView)
contentView.frame = view.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
// MARK: - Add or Remove view controllers
/**
Removes the view controller displayed on most top.
*/
public func removeLastViewController(
transition: AnyRemovingTransition?,
completion: @MainActor @escaping (RemovingTransitionContext.CompletionEvent) -> Void = { _ in }
) {
assert(Thread.isMainThread)
guard let wrapperView = stackingItems.last else {
Log.error(.stack, "The last view controller was not found to remove")
return
}
removeViewController(
wrapperView.viewController,
transition: transition,
completion: completion
)
}
/**
Add a view controller to display.
This is a primitive operation to add view controller to display.
``UIViewController/fluidPush`` runs this method internally.
- Parameters:
- transition:
a transition for adding. if view controller is type of ``TransitionViewController``, uses this transition instead of TransitionViewController's transition.
You may set ``.disabled`` to disable animation
*/
public func addContentViewController(
_ viewControllerToAdd: UIViewController,
transition: AnyAddingTransition?,
afterViewDidLoad: @escaping @MainActor () -> Void = {},
completion: (@MainActor (AddingTransitionContext.CompletionEvent) -> Void)? = nil
) {
/**
possible to enter while previous adding operation.
adding -> removing(interruption) -> adding(interruption) -> dipslay(completed)
*/
assert(Thread.isMainThread)
// Construct view controller chain
if viewControllerToAdd.parent != self {
addChild(viewControllerToAdd)
viewControllerToAdd.view.resetToVisible()
viewControllerToAdd.didMove(toParent: self)
} else {
// case of adding while removing
// TODO: might something needed
}
// set a context if not set
if viewControllerToAdd.fluidStackContext == nil {
let context = FluidStackContext(
fluidStackController: self,
targetViewController: viewControllerToAdd
)
// set context
viewControllerToAdd.fluidStackContext = context
}
/// Save current first-responder from the current displaying view controller.
/// To restore it when back to this view controller as the top - ``FluidStackController/StackingPlatterView/restoreResponderState()``
topItem?.saveResponderState()
// Trigger `viewDidLoad` explicitly.
viewControllerToAdd.loadViewIfNeeded()
afterViewDidLoad()
let platterView: StackingPlatterView = {
if let currentPlatterView = viewControllerToAdd.view.superview as? StackingPlatterView {
// reuse
return currentPlatterView
} else {
// create new one
let newPlatterView = StackingPlatterView(
viewController: viewControllerToAdd,
frame: self.view.bounds
)
return newPlatterView
}
}()
view.addSubview(platterView)
platterView.makeViewControllerFirstResponder()
// propagate after `viewDidLoad`
viewControllerToAdd.propagateStackAction(.willPush)
viewControllerToAdd.propagateStackAction(.willBecomeTop)
// take before modifying.
let currentTop = stackingItems.last
// Adds the view controller at the latest position.
do {
var modified = stackingItems
modified.removeAll { $0.viewController == viewControllerToAdd }
modified.append(platterView)
stackingItems = modified
}
assert(viewControllerToAdd.view.superview != nil)
assert(viewControllerToAdd.view.superview is StackingPlatterView)
let newTransitionContext = AddingTransitionContext(
contentView: platterView,
fromViewController: currentTop?.viewController,
toViewController: viewControllerToAdd,
onAnimationCompleted: { [weak self, weak platterView] context in
// MARK: Handling after animation
assert(Thread.isMainThread)
guard let self = self, let platterView = platterView else { return }
defer {
platterView.removeTransitionContext(expect: context)
if self.state.latestTransitionContext == context {
// handling offload
if self.stackConfiguration.isOffloadViewsEnabled {
self.updateOffloadingItems()
}
}
}
guard context.isInvalidated == false else {
Log.debug(.stack, "\(context) was invalidated, skips adding")
return
}
context.transitionSucceeded()
platterView.viewController.propagateStackAction(.didPush)
}
)
// To run offloading after latest adding transition.
// it won't run if got invalidation by started removing-transition.
state.latestTransitionContext = newTransitionContext
newTransitionContext.addCompletionEventHandler { event in
completion?(event)
}
platterView.swapTransitionContext(newTransitionContext)
// Start transition after invalidated current transition.
do {
// Turns off touch through to prevent the user attempt to start another adding-transition.
// `Flexible` means the user can dispatch cancel in the current transition.
platterView.isTouchThroughEnabled = false
if let transition = transition {
transition.startTransition(context: newTransitionContext)
} else if let transitionViewController = viewControllerToAdd as? FluidTransitionViewController
{
transitionViewController.startAddingTransition(
context: newTransitionContext
)
} else {
AnyAddingTransition.disabled.startTransition(context: newTransitionContext)
}
}
}
/**
Add a view to display with wrapping internal view controller.
- Parameters:
- transition: You may set ``.disabled`` to disable transition animation.
*/
public func addContentView(
_ view: UIView,
transition: AnyAddingTransition?,
completion: (@MainActor (AddingTransitionContext.CompletionEvent) -> Void)? = nil
) {
assert(Thread.isMainThread)
let viewController = ContentWrapperViewController(view: view)
addContentViewController(viewController, transition: transition, completion: completion)
}
/**
Starts removing transaction for interaction.
Make sure to complete the transition with the context.
*/
public func startRemovingForInteraction(
_ viewControllerToRemove: UIViewController,
completion: (@MainActor (RemovingTransitionContext.CompletionEvent) -> Void)? = nil
) -> RemovingTransitionContext {
// Handles stackConfiguration
if stackConfiguration.retainsRootViewController,
viewControllerToRemove == stackingItems.first?.viewController
{
Log.error(
.stack,
"the stacking will broke. Attempted to remove the view controller which displaying as root view controller. but the stackConfiguration requires to retains the root view controller."
)
}
guard let viewToRemove = stackingItems.first(where: { $0.viewController == viewControllerToRemove }) else {
preconditionFailure("Not found wrapper view to manage \(viewControllerToRemove)")
}
return _startRemoving(viewToRemove, completion: completion)
}
/**
Starts removing transaction.
Make sure to complete the transition with the context.
*/
private func _startRemoving(
_ platterView: StackingPlatterView,
completion: (@MainActor (RemovingTransitionContext.CompletionEvent) -> Void)? = nil
) -> RemovingTransitionContext {
// Ensure it's managed
guard
let index = stackingItems.firstIndex(of: platterView)
else {
Log.error(.stack, "\(platterView.viewController) was not found to remove")
fatalError()
}
// finds a view controller that will be displayed next.
let backView: StackingPlatterView? = {
let target = index.advanced(by: -1)
if stackingItems.indices.contains(target) {
return stackingItems[target]
} else {
return nil
}
}()
platterView.viewController.propagateStackAction(.willPop)
backView?.viewController.propagateStackAction(.willBecomeTop)
let newTransitionContext = RemovingTransitionContext(
contentView: platterView,
fromViewController: platterView.viewController,
toViewController: backView?.viewController,
onAnimationCompleted: { [weak self, weak platterView] context in
guard let self, let platterView else { return }
defer {
platterView.removeTransitionContext(expect: context)
if self.state.latestTransitionContext == context {
// handling offload
if self.stackConfiguration.isOffloadViewsEnabled {
self.updateOffloadingItems()
}
}
}
guard context.isInvalidated == false else {
Log.debug(.stack, "\(context) was invalidated, skips removing")
return
}
/**
Completion of transition, cleaning up
*/
let viewControllerToRemove = platterView.viewController
self.stackingItems.removeAll { $0.viewController == viewControllerToRemove }
viewControllerToRemove.fluidStackContext = nil
viewControllerToRemove.willMove(toParent: nil)
viewControllerToRemove.removeFromParent()
platterView.removeFromSuperview()
context.transitionSucceeded()
platterView.viewController.propagateStackAction(.didPop)
self.topItem?.restoreResponderState()
},
onRequestedDisplayOnTop: { [weak self, weak platterView] source in
guard let self = self, let viewToRemove = platterView else {
assertionFailure("FluidStackController has been already deallocated.")
return .init(run: {})
}
return self.addPortalView(for: source, on: viewToRemove)
}
)
// To run offloading after latest adding transition.
// it won't run if got invalidation by started removing-transition.
state.latestTransitionContext = newTransitionContext
newTransitionContext.addCompletionEventHandler { event in
completion?(event)
}
// To enable through to make back view controller can be interactive.
// Consequently, the user can start another transition.
platterView.isTouchThroughEnabled = true
// set before update offloading
platterView.swapTransitionContext(newTransitionContext)
// handling offload
if self.stackConfiguration.isOffloadViewsEnabled {
updateOffloadingItems(displayItem: backView ?? platterView)
}
return newTransitionContext
}
public func canRemove(viewController: UIViewController) -> Bool {
if stackConfiguration.retainsRootViewController,
viewController == stackingItems.first?.viewController
{
return false
}
return true
}
/**
Removes given view controller with transition.
Switches to batch removing if there are multiple view controllers on top of the given view controller.
*/
public func removeViewController(
_ viewControllerToRemove: UIViewController,
transition: AnyRemovingTransition?,
transitionForBatch: @autoclosure @escaping () -> AnyBatchRemovingTransition? = .crossDissolve,
completion: (@MainActor (RemovingTransitionContext.CompletionEvent) -> Void)? = nil
) {
// Handles configuration
guard canRemove(viewController: viewControllerToRemove) else {
Log.error(
.stack,
"Attempted to remove the view controller which displaying as root view controller. but the configuration requires to retains the root view controller."
)
return
}
guard let viewToRemove = stackingItems.first(where: { $0.viewController == viewControllerToRemove }) else {
assertionFailure("Not found wrapper view to manage \(viewControllerToRemove)")
return
}
if stackingItems.last?.viewController != viewControllerToRemove {
// Removes view controllers with batch
let transition = transitionForBatch()
Log.debug(
.stack,
"The removing view controller is not displaying on top. it's behind of the other view controllers. Switches to batch-removing using transition: \(transition as Any)"
)
removeAllViewController(
from: viewToRemove.viewController,
transition: transition,
completion: { event in
switch event {
case .succeeded:
completion?(.succeeded)
case .interrupted:
completion?(.interrupted)
}
}
)
return
}
// Removes view controller
let transitionContext = _startRemoving(viewToRemove, completion: completion)
if let transition = transition {
transition.startTransition(context: transitionContext)
} else if let transitionViewController = viewToRemove.viewController
as? FluidTransitionViewController
{
transitionViewController.startRemovingTransition(context: transitionContext)
} else {
transitionContext.notifyAnimationCompleted()
}
}
/**
Removes all view controllers which are displaying
*/
public func removeAllViewController(
transition: AnyBatchRemovingTransition?,
completion: (@MainActor (BatchRemovingTransitionContext.CompletionEvent) -> Void)? = nil
) {
if stackConfiguration.retainsRootViewController {
guard let target = stackingItems.dropFirst().first else { return }
removeAllViewController(from: target.viewController, transition: transition, completion: completion)
} else {
guard let target = stackingItems.first else { return }
removeAllViewController(from: target.viewController, transition: transition, completion: completion)
}
}
/**
Removes all view controllers which displaying on top of the given view controller.
- FIXME
- Supports re-entrant operation - adding-transition. It's undefined behavior to get adding while removing.
- Parameters:
- from:
- transition:
*/
private func removeAllViewController(
from viewController: UIViewController,
transition: AnyBatchRemovingTransition?,
completion: (@MainActor (BatchRemovingTransitionContext.CompletionEvent) -> Void)? = nil
) {
Log.debug(.stack, "Remove \(viewController) from \(stackingItems)")
assert(Thread.isMainThread)
let targetStackingItems = stackingItems
guard let index = targetStackingItems.firstIndex(where: { $0.viewController == viewController }) else {
Log.error(.stack, "\(viewController) was not found to remove")
return
}
let targetTopItem = targetStackingItems[0..<(index)].last
let itemsToRemove = Array(
targetStackingItems[
index...stackingItems.indices.last!
]
)
assert(itemsToRemove.count > 0)
let transition: AnyBatchRemovingTransition = transition ?? .disabled
let newTransitionContext = BatchRemovingTransitionContext(
contentView: itemsToRemove.first!,
fromViewControllers: itemsToRemove.map(\.viewController),
toViewController: targetTopItem?.viewController,
onCompleted: { [weak self] context in
assert(Thread.isMainThread)
guard let self = self else {
return
}
/**
Completion of transition, cleaning up
*/
for itemToRemove in itemsToRemove {
itemToRemove.removeTransitionContext(expect: context)
itemToRemove.viewController.willMove(toParent: nil)
itemToRemove.removeFromSuperview()
itemToRemove.viewController.removeFromParent()
itemToRemove.viewController.fluidStackContext = nil
self.stackingItems.removeAll { instance in
(instance as StackingPlatterView) == (itemToRemove as StackingPlatterView)
}
}
context.transitionSucceeded()
self.topItem?.restoreResponderState()
}
)
newTransitionContext.addCompletionEventHandler { event in
completion?(event)
}
for itemToRemove in itemsToRemove {
itemToRemove.swapTransitionContext(newTransitionContext)
}
if let targetTopItem = targetTopItem {
updateOffloadingItems(displayItem: targetTopItem)
}
transition.startTransition(context: newTransitionContext)
}
// MARK: - Accessing displaying view controllers
public func viewController(before viewController: UIViewController) -> UIViewController? {
let stackingViewControllers = stackingViewControllers
guard let index = stackingViewControllers.firstIndex(of: viewController) else {
return nil
}
let targetIndex = stackingViewControllers.index(before: index)
guard stackingViewControllers.indices.contains(targetIndex) else {
return nil
}
return stackingViewControllers[targetIndex]
}
public func viewController(after viewController: UIViewController) -> UIViewController? {
let stackingViewControllers = stackingViewControllers
guard let index = stackingViewControllers.firstIndex(of: viewController) else {
return nil
}
let targetIndex = stackingViewControllers.index(after: index)
guard stackingViewControllers.indices.contains(targetIndex) else {
return nil
}
return stackingViewControllers[targetIndex]
}
// MARK: - Others
private func addPortalView(
for source: DisplaySource,
on targetView: StackingPlatterView
) -> DisplayingOnTopSubscription {
assert(Thread.isMainThread)
let portalView = PortalView(source: source)
portalView.frame = targetView.bounds
portalView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
portalView.matchesPosition = true
portalView.hidesSourceLayer = true
portalView.matchesTransform = true
portalView.matchesOpacity = true
targetView.addSubview(portalView)
return .init {
portalView.removeFromSuperview()
}
}
/// Convinience method
private func updateOffloadingItems() {
let items = stackingItems
guard let last = items.last else {
// seems no there to update offloading.
return
}
updateOffloadingItems(displayItem: last)
}
/**
[primitive]
Offloads view controllers which do not need to display from their wrapper view.
- Parameters:
- displayItem: a item that supposed to be visible on top.
- TODO: Write Test
*/
private func updateOffloadingItems(displayItem: StackingPlatterView) {
let items = stackingItems
var order: [(StackingPlatterView, Bool)] = []
var offloads: Bool = false
// if current performing in behined given display item
var isInBehindDisplayItem: Bool = false
// performs from most top view
// complex 🤷🏻♂️ my bad
for item in items.reversed() {
if isInBehindDisplayItem {
if offloads {
order.append((item, true))
} else {
order.append((item, false))
if item.isTransitioning == false {
switch item.viewController.fluidStackContentConfiguration.contentType {
case .opaque:
offloads = true
case .overlay:
break
}
} else {
offloads = false
}
}
} else {
order.append((item, false))
if item.isTransitioning == false {
switch item.viewController.fluidStackContentConfiguration.contentType {
case .opaque:
offloads = true
case .overlay:
offloads = false
}
} else {
offloads = false
}
isInBehindDisplayItem = item == displayItem
}
}
Log.debug(.stack, "Update offload \(displayItem)")
for (item, offloads) in order {
if offloads {
item.offloadViewController()
} else {
item.loadViewController()
}
}
Log.debug(.stack, self.stackingDescription())
}
}
/**
Extended type of ``FluidStackController`` for working on modal-presentation.
To create stacking context on modal-presentation.
It dismisses itself automatically when the stacking items is empty.
```swift
let stack = PresentationFluidStackController()
stack.display(on: self)
let content = ContentViewController(color: .neonRandom())
stack.fluidPush(content.fluidWrapped(configuration: .defaultNavigation), target: .current, relation: .modality)
```
*/
open class PresentationFluidStackController: FluidStackController {
public override init(
identifier: Identifier? = nil,
view: UIView? = nil,
contentView: UIView? = nil,
configuration: Configuration = .init(retainsRootViewController: false),
rootViewController: UIViewController? = nil
) {
super.init(
identifier: identifier,
view: view,
contentView: contentView,
configuration: configuration,
rootViewController: rootViewController
)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
/// Displays this view controller as modal-presentation on the given view controller.
/// - Parameter viewController: A target view controller to dispach presentation operation.
public func display(on viewController: UIViewController) {
viewController.present(self, animated: false)
CATransaction.flush()
}
open override func stackingViewControllersDidChange(_ viewControllers: [UIViewController]) {
if viewControllers.isEmpty {
dismiss(animated: false)
}
}
}
extension FluidStackController {
open override var debugDescription: String {
Fluid.renderOnelineDescription(subject: self) { s in
[
("stackIdentifier", stackIdentifier?.rawValue ?? "null"),
]
}
}
}
// MARK: - Nested types
extension FluidStackController {
public struct DisplayingOnTopSubscription {
private let _run: () -> Void
init(run: @escaping () -> Void) {
self._run = run
}
public func dispose() {
_run()
}
}
/// A wrapper object that stores an string value that identifies a instance of ``FluidStackController``.
public struct Identifier: Hashable {
public let rawValue: String
public init(_ rawValue: String) {
self.rawValue = rawValue
}
}
public struct Configuration {
/// Keeps hodling a root view controller.
public var retainsRootViewController: Bool
/// Offloads background view controllers from hierarchy.
public var isOffloadViewsEnabled: Bool
/// Whether prevents `forwading-pop`
/// `forwarding-pop` is forwarding pop-operation to the parent stack if there are no items to pop in the current stack.
public var preventsFowardingPop: Bool
public init(
retainsRootViewController: Bool = true,