├── .gitignore
├── .swiftpm
└── xcode
│ └── xcshareddata
│ └── xcschemes
│ └── Navidux.xcscheme
├── Example
├── Example.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Example.xcscheme
└── Example
│ ├── App
│ ├── AppDelegate.swift
│ └── SceneDelegate.swift
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Info.plist
│ ├── Navidux+Ext
│ ├── NaviduxScreen+Extension.swift
│ ├── NaviduxScreenAssembler.swift
│ └── NaviduxScreenFactory.swift
│ └── Screens
│ ├── ButtonView.swift
│ ├── FirstContentView.swift
│ ├── SecondContentView.swift
│ ├── ThirdContentTableViewCell.swift
│ └── ThirdContentViewController.swift
├── Package.swift
├── README.md
├── Sources
└── Navidux
│ ├── NaviduxScreen.swift
│ ├── Navigation.swift
│ ├── NavigationController.swift
│ ├── NavigationCoordinator.swift
│ ├── NavigationCoordinatorProxy.swift
│ ├── Router.swift
│ ├── ScreenAssembler.swift
│ ├── ScreenFactory.swift
│ ├── alert
│ ├── AlertConfiguration.swift
│ ├── AlertFactory.swift
│ └── AlertScreen.swift
│ ├── extensions
│ ├── CGFloat+Extension.swift
│ ├── CGPoint+Extension.swift
│ ├── ExtendableEnum.swift
│ ├── Storyboard.swift
│ ├── UIImage+Extension.swift
│ └── UIPanGestureRecognizer+Extension.swift
│ ├── implemention
│ ├── NavigationControllerImpl.swift
│ ├── NavigationReducer.swift
│ ├── NavigationRouter.swift
│ ├── NavigationStore.swift
│ └── Payload.swift
│ ├── resources
│ └── Media.xcassets
│ │ ├── Contents.json
│ │ └── PullbarIcon.imageset
│ │ ├── Contents.json
│ │ └── PullBarIcon.pdf
│ └── screens
│ ├── ActivityViewController.swift
│ ├── BottomSheet
│ ├── BSPresentationController.swift
│ ├── BSScrollableViews
│ │ ├── BSCollectionView.swift
│ │ ├── BSScrollView.swift
│ │ └── BSTableView.swift
│ ├── BSTransitionDriver.swift
│ ├── BSTransitioningDelegate.swift
│ ├── CoverVerticalDismissAnimatedTransitioning.swift
│ ├── CoverVerticalPresentAnimatedTransitioning.swift
│ └── PullBar.swift
│ ├── HostingController.swift
│ ├── NavigationScreen.swift
│ ├── ScreenConfig.swift
│ └── ViewController.swift
├── Tests
└── NaviduxTests
│ ├── Fixtures
│ ├── NaviduxFixture.swift
│ ├── NaviduxScreenFixture.swift
│ ├── NavigationControllerStub.swift
│ ├── PayloadStub.swift
│ └── ScreenAssemblerStub.swift
│ ├── NavigationControllerTests.swift
│ └── NavigationCoordinatorTests.swift
└── readme
├── Navidux_scheme.png
└── Roadmap.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecs.plist
10 | .netrc
11 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/Navidux.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
68 |
74 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 4F45EBC72AFB701C00A92472 /* ThirdContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F45EBC62AFB701C00A92472 /* ThirdContentViewController.swift */; };
11 | 4F45EBC92AFB8AD000A92472 /* ThirdContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F45EBC82AFB8AD000A92472 /* ThirdContentTableViewCell.swift */; };
12 | E239A3BE2915756C00A03EB6 /* FirstContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E239A3BD2915756C00A03EB6 /* FirstContentView.swift */; };
13 | E239A3C02915756D00A03EB6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E239A3BF2915756D00A03EB6 /* Assets.xcassets */; };
14 | E239A3CC291578A500A03EB6 /* Navidux in Frameworks */ = {isa = PBXBuildFile; productRef = E239A3CB291578A500A03EB6 /* Navidux */; };
15 | E239A3D1291579FB00A03EB6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E239A3D0291579FB00A03EB6 /* AppDelegate.swift */; };
16 | E239A3D329157A0E00A03EB6 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E239A3D229157A0E00A03EB6 /* SceneDelegate.swift */; };
17 | E239A3D729157B6F00A03EB6 /* NaviduxScreen+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E239A3D629157B6F00A03EB6 /* NaviduxScreen+Extension.swift */; };
18 | E239A3D929157B8600A03EB6 /* NaviduxScreenAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E239A3D829157B8600A03EB6 /* NaviduxScreenAssembler.swift */; };
19 | E239A3DB29157B9300A03EB6 /* NaviduxScreenFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E239A3DA29157B9300A03EB6 /* NaviduxScreenFactory.swift */; };
20 | E239A3E02916E14800A03EB6 /* SecondContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E239A3DF2916E14800A03EB6 /* SecondContentView.swift */; };
21 | E239A3E22916E38300A03EB6 /* ButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E239A3E12916E38300A03EB6 /* ButtonView.swift */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXFileReference section */
25 | 4F45EBC62AFB701C00A92472 /* ThirdContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdContentViewController.swift; sourceTree = ""; };
26 | 4F45EBC82AFB8AD000A92472 /* ThirdContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdContentTableViewCell.swift; sourceTree = ""; };
27 | E239A3B82915756C00A03EB6 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
28 | E239A3BD2915756C00A03EB6 /* FirstContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstContentView.swift; sourceTree = ""; };
29 | E239A3BF2915756D00A03EB6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
30 | E239A3D0291579FB00A03EB6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
31 | E239A3D229157A0E00A03EB6 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
32 | E239A3D629157B6F00A03EB6 /* NaviduxScreen+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NaviduxScreen+Extension.swift"; sourceTree = ""; };
33 | E239A3D829157B8600A03EB6 /* NaviduxScreenAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NaviduxScreenAssembler.swift; sourceTree = ""; };
34 | E239A3DA29157B9300A03EB6 /* NaviduxScreenFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NaviduxScreenFactory.swift; sourceTree = ""; };
35 | E239A3DE2916DE4700A03EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
36 | E239A3DF2916E14800A03EB6 /* SecondContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondContentView.swift; sourceTree = ""; };
37 | E239A3E12916E38300A03EB6 /* ButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonView.swift; sourceTree = ""; };
38 | E2D206252AB32DB30066EE5D /* navidux_fork */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = navidux_fork; path = ..; sourceTree = ""; };
39 | /* End PBXFileReference section */
40 |
41 | /* Begin PBXFrameworksBuildPhase section */
42 | E239A3B52915756C00A03EB6 /* Frameworks */ = {
43 | isa = PBXFrameworksBuildPhase;
44 | buildActionMask = 2147483647;
45 | files = (
46 | E239A3CC291578A500A03EB6 /* Navidux in Frameworks */,
47 | );
48 | runOnlyForDeploymentPostprocessing = 0;
49 | };
50 | /* End PBXFrameworksBuildPhase section */
51 |
52 | /* Begin PBXGroup section */
53 | E239A3AF2915756C00A03EB6 = {
54 | isa = PBXGroup;
55 | children = (
56 | E2D206242AB32DB30066EE5D /* Packages */,
57 | E239A3BA2915756C00A03EB6 /* Example */,
58 | E239A3B92915756C00A03EB6 /* Products */,
59 | E239A3CA291578A500A03EB6 /* Frameworks */,
60 | );
61 | sourceTree = "";
62 | };
63 | E239A3B92915756C00A03EB6 /* Products */ = {
64 | isa = PBXGroup;
65 | children = (
66 | E239A3B82915756C00A03EB6 /* Example.app */,
67 | );
68 | name = Products;
69 | sourceTree = "";
70 | };
71 | E239A3BA2915756C00A03EB6 /* Example */ = {
72 | isa = PBXGroup;
73 | children = (
74 | E239A3DE2916DE4700A03EB6 /* Info.plist */,
75 | E239A3D529157A8100A03EB6 /* Navidux+Ext */,
76 | E239A3CF291578FD00A03EB6 /* Screens */,
77 | E239A3CE291578F100A03EB6 /* App */,
78 | E239A3BF2915756D00A03EB6 /* Assets.xcassets */,
79 | );
80 | path = Example;
81 | sourceTree = "";
82 | };
83 | E239A3CA291578A500A03EB6 /* Frameworks */ = {
84 | isa = PBXGroup;
85 | children = (
86 | );
87 | name = Frameworks;
88 | sourceTree = "";
89 | };
90 | E239A3CE291578F100A03EB6 /* App */ = {
91 | isa = PBXGroup;
92 | children = (
93 | E239A3D0291579FB00A03EB6 /* AppDelegate.swift */,
94 | E239A3D229157A0E00A03EB6 /* SceneDelegate.swift */,
95 | );
96 | path = App;
97 | sourceTree = "";
98 | };
99 | E239A3CF291578FD00A03EB6 /* Screens */ = {
100 | isa = PBXGroup;
101 | children = (
102 | E239A3BD2915756C00A03EB6 /* FirstContentView.swift */,
103 | E239A3DF2916E14800A03EB6 /* SecondContentView.swift */,
104 | E239A3E12916E38300A03EB6 /* ButtonView.swift */,
105 | 4F45EBC62AFB701C00A92472 /* ThirdContentViewController.swift */,
106 | 4F45EBC82AFB8AD000A92472 /* ThirdContentTableViewCell.swift */,
107 | );
108 | path = Screens;
109 | sourceTree = "";
110 | };
111 | E239A3D529157A8100A03EB6 /* Navidux+Ext */ = {
112 | isa = PBXGroup;
113 | children = (
114 | E239A3D629157B6F00A03EB6 /* NaviduxScreen+Extension.swift */,
115 | E239A3D829157B8600A03EB6 /* NaviduxScreenAssembler.swift */,
116 | E239A3DA29157B9300A03EB6 /* NaviduxScreenFactory.swift */,
117 | );
118 | path = "Navidux+Ext";
119 | sourceTree = "";
120 | };
121 | E2D206242AB32DB30066EE5D /* Packages */ = {
122 | isa = PBXGroup;
123 | children = (
124 | E2D206252AB32DB30066EE5D /* navidux_fork */,
125 | );
126 | name = Packages;
127 | sourceTree = "";
128 | };
129 | /* End PBXGroup section */
130 |
131 | /* Begin PBXNativeTarget section */
132 | E239A3B72915756C00A03EB6 /* Example */ = {
133 | isa = PBXNativeTarget;
134 | buildConfigurationList = E239A3C62915756D00A03EB6 /* Build configuration list for PBXNativeTarget "Example" */;
135 | buildPhases = (
136 | E239A3B42915756C00A03EB6 /* Sources */,
137 | E239A3B52915756C00A03EB6 /* Frameworks */,
138 | E239A3B62915756C00A03EB6 /* Resources */,
139 | );
140 | buildRules = (
141 | );
142 | dependencies = (
143 | );
144 | name = Example;
145 | packageProductDependencies = (
146 | E239A3CB291578A500A03EB6 /* Navidux */,
147 | );
148 | productName = Example;
149 | productReference = E239A3B82915756C00A03EB6 /* Example.app */;
150 | productType = "com.apple.product-type.application";
151 | };
152 | /* End PBXNativeTarget section */
153 |
154 | /* Begin PBXProject section */
155 | E239A3B02915756C00A03EB6 /* Project object */ = {
156 | isa = PBXProject;
157 | attributes = {
158 | BuildIndependentTargetsInParallel = 1;
159 | LastSwiftUpdateCheck = 1400;
160 | LastUpgradeCheck = 1400;
161 | TargetAttributes = {
162 | E239A3B72915756C00A03EB6 = {
163 | CreatedOnToolsVersion = 14.0;
164 | };
165 | };
166 | };
167 | buildConfigurationList = E239A3B32915756C00A03EB6 /* Build configuration list for PBXProject "Example" */;
168 | compatibilityVersion = "Xcode 14.0";
169 | developmentRegion = en;
170 | hasScannedForEncodings = 0;
171 | knownRegions = (
172 | en,
173 | Base,
174 | );
175 | mainGroup = E239A3AF2915756C00A03EB6;
176 | packageReferences = (
177 | );
178 | productRefGroup = E239A3B92915756C00A03EB6 /* Products */;
179 | projectDirPath = "";
180 | projectRoot = "";
181 | targets = (
182 | E239A3B72915756C00A03EB6 /* Example */,
183 | );
184 | };
185 | /* End PBXProject section */
186 |
187 | /* Begin PBXResourcesBuildPhase section */
188 | E239A3B62915756C00A03EB6 /* Resources */ = {
189 | isa = PBXResourcesBuildPhase;
190 | buildActionMask = 2147483647;
191 | files = (
192 | E239A3C02915756D00A03EB6 /* Assets.xcassets in Resources */,
193 | );
194 | runOnlyForDeploymentPostprocessing = 0;
195 | };
196 | /* End PBXResourcesBuildPhase section */
197 |
198 | /* Begin PBXSourcesBuildPhase section */
199 | E239A3B42915756C00A03EB6 /* Sources */ = {
200 | isa = PBXSourcesBuildPhase;
201 | buildActionMask = 2147483647;
202 | files = (
203 | 4F45EBC72AFB701C00A92472 /* ThirdContentViewController.swift in Sources */,
204 | 4F45EBC92AFB8AD000A92472 /* ThirdContentTableViewCell.swift in Sources */,
205 | E239A3E02916E14800A03EB6 /* SecondContentView.swift in Sources */,
206 | E239A3D1291579FB00A03EB6 /* AppDelegate.swift in Sources */,
207 | E239A3E22916E38300A03EB6 /* ButtonView.swift in Sources */,
208 | E239A3BE2915756C00A03EB6 /* FirstContentView.swift in Sources */,
209 | E239A3DB29157B9300A03EB6 /* NaviduxScreenFactory.swift in Sources */,
210 | E239A3D929157B8600A03EB6 /* NaviduxScreenAssembler.swift in Sources */,
211 | E239A3D329157A0E00A03EB6 /* SceneDelegate.swift in Sources */,
212 | E239A3D729157B6F00A03EB6 /* NaviduxScreen+Extension.swift in Sources */,
213 | );
214 | runOnlyForDeploymentPostprocessing = 0;
215 | };
216 | /* End PBXSourcesBuildPhase section */
217 |
218 | /* Begin XCBuildConfiguration section */
219 | E239A3C42915756D00A03EB6 /* Debug */ = {
220 | isa = XCBuildConfiguration;
221 | buildSettings = {
222 | ALWAYS_SEARCH_USER_PATHS = NO;
223 | CLANG_ANALYZER_NONNULL = YES;
224 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
225 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
226 | CLANG_ENABLE_MODULES = YES;
227 | CLANG_ENABLE_OBJC_ARC = YES;
228 | CLANG_ENABLE_OBJC_WEAK = YES;
229 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
230 | CLANG_WARN_BOOL_CONVERSION = YES;
231 | CLANG_WARN_COMMA = YES;
232 | CLANG_WARN_CONSTANT_CONVERSION = YES;
233 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
236 | CLANG_WARN_EMPTY_BODY = YES;
237 | CLANG_WARN_ENUM_CONVERSION = YES;
238 | CLANG_WARN_INFINITE_RECURSION = YES;
239 | CLANG_WARN_INT_CONVERSION = YES;
240 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
241 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
244 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
245 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
246 | CLANG_WARN_STRICT_PROTOTYPES = YES;
247 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
248 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
249 | CLANG_WARN_UNREACHABLE_CODE = YES;
250 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
251 | COPY_PHASE_STRIP = NO;
252 | DEBUG_INFORMATION_FORMAT = dwarf;
253 | ENABLE_STRICT_OBJC_MSGSEND = YES;
254 | ENABLE_TESTABILITY = YES;
255 | GCC_C_LANGUAGE_STANDARD = gnu11;
256 | GCC_DYNAMIC_NO_PIC = NO;
257 | GCC_NO_COMMON_BLOCKS = YES;
258 | GCC_OPTIMIZATION_LEVEL = 0;
259 | GCC_PREPROCESSOR_DEFINITIONS = (
260 | "DEBUG=1",
261 | "$(inherited)",
262 | );
263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
265 | GCC_WARN_UNDECLARED_SELECTOR = YES;
266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
267 | GCC_WARN_UNUSED_FUNCTION = YES;
268 | GCC_WARN_UNUSED_VARIABLE = YES;
269 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
270 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
271 | MTL_FAST_MATH = YES;
272 | ONLY_ACTIVE_ARCH = YES;
273 | SDKROOT = iphoneos;
274 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
275 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
276 | };
277 | name = Debug;
278 | };
279 | E239A3C52915756D00A03EB6 /* Release */ = {
280 | isa = XCBuildConfiguration;
281 | buildSettings = {
282 | ALWAYS_SEARCH_USER_PATHS = NO;
283 | CLANG_ANALYZER_NONNULL = YES;
284 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
285 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
286 | CLANG_ENABLE_MODULES = YES;
287 | CLANG_ENABLE_OBJC_ARC = YES;
288 | CLANG_ENABLE_OBJC_WEAK = YES;
289 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
290 | CLANG_WARN_BOOL_CONVERSION = YES;
291 | CLANG_WARN_COMMA = YES;
292 | CLANG_WARN_CONSTANT_CONVERSION = YES;
293 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
294 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
295 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
296 | CLANG_WARN_EMPTY_BODY = YES;
297 | CLANG_WARN_ENUM_CONVERSION = YES;
298 | CLANG_WARN_INFINITE_RECURSION = YES;
299 | CLANG_WARN_INT_CONVERSION = YES;
300 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
301 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
302 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
303 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
304 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
305 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
306 | CLANG_WARN_STRICT_PROTOTYPES = YES;
307 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
308 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
309 | CLANG_WARN_UNREACHABLE_CODE = YES;
310 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
311 | COPY_PHASE_STRIP = NO;
312 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
313 | ENABLE_NS_ASSERTIONS = NO;
314 | ENABLE_STRICT_OBJC_MSGSEND = YES;
315 | GCC_C_LANGUAGE_STANDARD = gnu11;
316 | GCC_NO_COMMON_BLOCKS = YES;
317 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
318 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
319 | GCC_WARN_UNDECLARED_SELECTOR = YES;
320 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
321 | GCC_WARN_UNUSED_FUNCTION = YES;
322 | GCC_WARN_UNUSED_VARIABLE = YES;
323 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
324 | MTL_ENABLE_DEBUG_INFO = NO;
325 | MTL_FAST_MATH = YES;
326 | SDKROOT = iphoneos;
327 | SWIFT_COMPILATION_MODE = wholemodule;
328 | SWIFT_OPTIMIZATION_LEVEL = "-O";
329 | VALIDATE_PRODUCT = YES;
330 | };
331 | name = Release;
332 | };
333 | E239A3C72915756D00A03EB6 /* Debug */ = {
334 | isa = XCBuildConfiguration;
335 | buildSettings = {
336 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
337 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
338 | CODE_SIGN_STYLE = Automatic;
339 | CURRENT_PROJECT_VERSION = 1;
340 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
341 | DEVELOPMENT_TEAM = 7898RN388J;
342 | ENABLE_PREVIEWS = YES;
343 | GENERATE_INFOPLIST_FILE = YES;
344 | INFOPLIST_FILE = Example/Info.plist;
345 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
346 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
347 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
348 | LD_RUNPATH_SEARCH_PATHS = (
349 | "$(inherited)",
350 | "@executable_path/Frameworks",
351 | );
352 | MARKETING_VERSION = 1.0;
353 | PRODUCT_BUNDLE_IDENTIFIER = evseev.com.Example;
354 | PRODUCT_NAME = "$(TARGET_NAME)";
355 | SWIFT_EMIT_LOC_STRINGS = YES;
356 | SWIFT_VERSION = 5.0;
357 | TARGETED_DEVICE_FAMILY = "1,2";
358 | };
359 | name = Debug;
360 | };
361 | E239A3C82915756D00A03EB6 /* Release */ = {
362 | isa = XCBuildConfiguration;
363 | buildSettings = {
364 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
365 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
366 | CODE_SIGN_STYLE = Automatic;
367 | CURRENT_PROJECT_VERSION = 1;
368 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
369 | DEVELOPMENT_TEAM = 7898RN388J;
370 | ENABLE_PREVIEWS = YES;
371 | GENERATE_INFOPLIST_FILE = YES;
372 | INFOPLIST_FILE = Example/Info.plist;
373 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
374 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
375 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
376 | LD_RUNPATH_SEARCH_PATHS = (
377 | "$(inherited)",
378 | "@executable_path/Frameworks",
379 | );
380 | MARKETING_VERSION = 1.0;
381 | PRODUCT_BUNDLE_IDENTIFIER = evseev.com.Example;
382 | PRODUCT_NAME = "$(TARGET_NAME)";
383 | SWIFT_EMIT_LOC_STRINGS = YES;
384 | SWIFT_VERSION = 5.0;
385 | TARGETED_DEVICE_FAMILY = "1,2";
386 | };
387 | name = Release;
388 | };
389 | /* End XCBuildConfiguration section */
390 |
391 | /* Begin XCConfigurationList section */
392 | E239A3B32915756C00A03EB6 /* Build configuration list for PBXProject "Example" */ = {
393 | isa = XCConfigurationList;
394 | buildConfigurations = (
395 | E239A3C42915756D00A03EB6 /* Debug */,
396 | E239A3C52915756D00A03EB6 /* Release */,
397 | );
398 | defaultConfigurationIsVisible = 0;
399 | defaultConfigurationName = Release;
400 | };
401 | E239A3C62915756D00A03EB6 /* Build configuration list for PBXNativeTarget "Example" */ = {
402 | isa = XCConfigurationList;
403 | buildConfigurations = (
404 | E239A3C72915756D00A03EB6 /* Debug */,
405 | E239A3C82915756D00A03EB6 /* Release */,
406 | );
407 | defaultConfigurationIsVisible = 0;
408 | defaultConfigurationName = Release;
409 | };
410 | /* End XCConfigurationList section */
411 |
412 | /* Begin XCSwiftPackageProductDependency section */
413 | E239A3CB291578A500A03EB6 /* Navidux */ = {
414 | isa = XCSwiftPackageProductDependency;
415 | productName = Navidux;
416 | };
417 | /* End XCSwiftPackageProductDependency section */
418 | };
419 | rootObject = E239A3B02915756C00A03EB6 /* Project object */;
420 | }
421 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Example/Example/App/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @main
4 | class AppDelegate: UIResponder, UIApplicationDelegate {
5 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
6 | return true
7 | }
8 |
9 | // MARK: UISceneSession Lifecycle
10 |
11 | func application(
12 | _ application: UIApplication,
13 | configurationForConnecting connectingSceneSession: UISceneSession,
14 | options: UIScene.ConnectionOptions
15 | ) -> UISceneConfiguration {
16 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Example/Example/App/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | import Navidux
2 | import UIKit
3 |
4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
5 |
6 | var window: UIWindow?
7 |
8 | func scene(
9 | _ scene: UIScene,
10 | willConnectTo session: UISceneSession,
11 | options connectionOptions: UIScene.ConnectionOptions
12 | ) {
13 | guard let windowScene = (scene as? UIWindowScene) else { return }
14 |
15 | let window = UIWindow(windowScene: windowScene)
16 |
17 | let navigation = NavigationControllerImpl { controller in
18 | controller.view.backgroundColor = .green
19 | controller.navigationBar.isTranslucent = false
20 | controller.navigationBar.backgroundColor = .green
21 | controller.navigationBar.shadowImage = .init()
22 | controller.navigationBar.barTintColor = .green
23 | controller.navigationBar.tintColor = .black
24 | controller.navigationBar.titleTextAttributes = [
25 | NSAttributedString.Key.foregroundColor: UIColor.black
26 | ]
27 | }
28 | let screenFactory = NaviduxScreenFactory()
29 | let alertFactory = AlertFactoryImpl()
30 | let navigationCoordinatorProxy = NavigationCoordinatorProxy()
31 | let screenAssembler = NaviduxScreenAssembler(
32 | screenFactory: screenFactory,
33 | alertFactory: alertFactory,
34 | screenCoordinator: navigationCoordinatorProxy
35 | )
36 | let navigationCoordinator = NavigationCoordinator(
37 | navigation,
38 | screenAssembler: screenAssembler
39 | )
40 | navigationCoordinatorProxy.subject = navigationCoordinator
41 | navigationCoordinatorProxy.route(
42 | with: .push(
43 | .firstScreen,
44 | ScreenConfig(navigationTitle: "First screen", isNeedSetBackButton: false),
45 | .fullscreen
46 | )
47 | )
48 |
49 | window.rootViewController = navigation
50 | self.window = window
51 | window.makeKeyAndVisible()
52 |
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Example/Example/Navidux+Ext/NaviduxScreen+Extension.swift:
--------------------------------------------------------------------------------
1 | import Navidux
2 |
3 | extension NaviduxScreen {
4 | public static let firstScreen = NaviduxScreen(
5 | screenClass: HostingController.self
6 | )
7 |
8 | public static let secondScreen = NaviduxScreen(
9 | screenClass: HostingController.self
10 | )
11 |
12 | public static let thirdScreen = NaviduxScreen(
13 | screenClass: ThirdContentViewController.self
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/Example/Example/Navidux+Ext/NaviduxScreenAssembler.swift:
--------------------------------------------------------------------------------
1 | import Navidux
2 |
3 | public final class NaviduxScreenAssembler: Navidux.ScreenAssembler {
4 | private var screenFactory: any Navidux.ScreenFactory
5 | private var alertFactory: Navidux.AlertFactory
6 | private var screenCoordinator: Navidux.NavigationCoordinatorProxy?
7 |
8 | public init(
9 | screenFactory: Navidux.ScreenFactory,
10 | alertFactory: Navidux.AlertFactory,
11 | screenCoordinator: Navidux.NavigationCoordinatorProxy?
12 | ) {
13 | self.screenFactory = screenFactory
14 | self.alertFactory = alertFactory
15 | self.screenCoordinator = screenCoordinator
16 | }
17 |
18 | public func assemblyScreen(screenType: NaviduxScreen, config: ScreenConfig) -> any NavigationScreen {
19 | switch screenType {
20 | case .firstScreen:
21 | return screenFactory.firstScreenFactory(screenCoordinator?.subject, config)
22 | case .secondScreen:
23 | return screenFactory.secondScreenFactory(screenCoordinator?.subject, config)
24 | case .thirdScreen:
25 | return screenFactory.thirdScreenFactory(screenCoordinator?.subject, config)
26 | default:
27 | return ViewController(navigation: nil)
28 | }
29 | }
30 |
31 | public func assemblyScreen(components: ScreenAsseblyComponents) -> any NavigationScreen {
32 | assemblyScreen(screenType: components.screenType, config: components.config)
33 | }
34 |
35 | public func assemblyAlert(configuration: AlertConfiguration) -> AlertScreen {
36 | alertFactory.createAlert(configuration: configuration)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Example/Example/Navidux+Ext/NaviduxScreenFactory.swift:
--------------------------------------------------------------------------------
1 | import Navidux
2 |
3 | extension Navidux.ScreenFactory {
4 | var firstScreenFactory: (NavigationCoordinator?, ScreenConfig) -> any NavigationScreen {
5 | { coordinator, screenConfig in
6 | let viewContent = FirstContentView(navigation: coordinator)
7 | let viewController = HostingController(
8 | title: screenConfig.navigationTitle,
9 | isNeedBackButton: false,
10 | tag: "FirstContentView",
11 | navigation: coordinator,
12 | content: viewContent
13 | )
14 |
15 | return viewController
16 | }
17 | }
18 |
19 | var secondScreenFactory: (NavigationCoordinator?, ScreenConfig) -> any NavigationScreen {
20 | { coordinator, screenConfig in
21 | let viewContent = SecondContentView(navigation: coordinator)
22 | let viewController = HostingController(
23 | title: screenConfig.navigationTitle,
24 | isNeedBackButton: true,
25 | tag: "SecondContentView",
26 | navigation: coordinator,
27 | content: viewContent
28 | )
29 |
30 | return viewController
31 | }
32 | }
33 |
34 | var thirdScreenFactory: (NavigationCoordinator?, ScreenConfig) -> any NavigationScreen {
35 | { coordinator, screenConfig in
36 | ThirdContentViewController()
37 | }
38 | }
39 | }
40 |
41 | final class NaviduxScreenFactory: Navidux.ScreenFactory {
42 | init() {}
43 | }
44 |
--------------------------------------------------------------------------------
/Example/Example/Screens/ButtonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonView.swift
3 | // Example
4 | //
5 | // Created by Александр Евсеев on 06.11.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ButtonView: View {
11 | let action: () -> Void
12 | let title: String
13 |
14 | var body: some View {
15 | Button(
16 | action: action) {
17 | HStack {
18 | Text(title)
19 | Spacer()
20 | Image(systemName: "chevron.right")
21 | }
22 | }
23 | .padding(.horizontal, 32)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Example/Example/Screens/FirstContentView.swift:
--------------------------------------------------------------------------------
1 | import Navidux
2 | import SwiftUI
3 |
4 | struct FirstContentView: View {
5 | let navigation: NavigationCoordinator?
6 |
7 | var body: some View {
8 | VStack(spacing: 8) {
9 | Image(systemName: "globe")
10 | .imageScale(.large)
11 | .foregroundColor(.accentColor)
12 | Text("Hello, world!")
13 | ButtonView(
14 | action: { [weak navigation] in
15 | navigation?.route(with:
16 | .push(
17 | .secondScreen,
18 | ScreenConfig(navigationTitle: "Second Screen"),
19 | .fullscreen
20 | )
21 | )
22 | },
23 | title: "Open fullscreen second screen"
24 | )
25 | .padding(.top, 40)
26 | }
27 | .padding()
28 | .navigationBarBackButtonHidden()
29 | }
30 | }
31 |
32 | struct FirstContentView_Previews: PreviewProvider {
33 | static var previews: some View {
34 | FirstContentView(navigation: nil)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Example/Example/Screens/SecondContentView.swift:
--------------------------------------------------------------------------------
1 | import Navidux
2 | import SwiftUI
3 |
4 | struct SecondContentView: View {
5 | let navigation: NavigationCoordinator?
6 |
7 | var body: some View {
8 | VStack(spacing: 8) {
9 | Image(systemName: "globe")
10 | .imageScale(.large)
11 | .foregroundColor(.accentColor)
12 | Text("Hello, world!")
13 | ButtonView(
14 | action: { [weak navigation] in
15 | navigation?.route(with: .pop(nil))
16 | },
17 | title: "Back"
18 | )
19 | .padding(.top, 40)
20 | ButtonView(
21 | action: { [weak navigation] in
22 | navigation?.route(with: .push(
23 | .thirdScreen,
24 | ScreenConfig(),
25 | .bottomSheet(.auto)
26 | ))
27 | },
28 | title: "Present bottom sheet - auto"
29 | )
30 | ButtonView(
31 | action: { [weak navigation] in
32 | navigation?.route(with: .push(
33 | .thirdScreen,
34 | ScreenConfig(),
35 | .bottomSheet(.fixed(120))
36 | ))
37 | },
38 | title: "Present bottom sheet - fixed height"
39 | )
40 | ButtonView(
41 | action: { [weak navigation] in
42 | navigation?.route(with: .push(
43 | .thirdScreen,
44 | ScreenConfig(),
45 | .bottomSheet(.fullScreen)
46 | ))
47 | },
48 | title: "Present bottom sheet - full screen"
49 | )
50 | ButtonView(
51 | action: { [weak navigation] in
52 | navigation?.route(with: .push(
53 | .thirdScreen,
54 | ScreenConfig(),
55 | .bottomSheet(.halfScreen)
56 | ))
57 | },
58 | title: "Present bottom sheet - half screen"
59 | )
60 | }
61 | .padding()
62 | .navigationBarBackButtonHidden()
63 | }
64 | }
65 |
66 | struct SecondContentView_Previews: PreviewProvider {
67 | static var previews: some View {
68 | SecondContentView(navigation: nil)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Example/Example/Screens/ThirdContentTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThirdContentTableViewCell.swift
3 | // Example
4 | //
5 | // Created by Oleg Krasnov on 08/11/2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct ThirdContentCellModel {
11 | let image: UIImage
12 | let title: String
13 | }
14 |
15 | final class ThirdContentTableViewCell: UITableViewCell {
16 |
17 | private lazy var iconImageView = UIImageView()
18 | private lazy var label = UILabel()
19 |
20 | override init(
21 | style: UITableViewCell.CellStyle,
22 | reuseIdentifier: String?
23 | ) {
24 | super.init(style: style, reuseIdentifier: reuseIdentifier)
25 | setup()
26 | }
27 |
28 | required init?(coder _: NSCoder) {
29 | fatalError("init(coder:) has not been implemented")
30 | }
31 |
32 | func configureWith(image: UIImage, title: String) {
33 | imageView?.image = image
34 | label.text = title
35 | }
36 |
37 | private func setup() {
38 | backgroundColor = .white
39 | [iconImageView, label].forEach {
40 | $0.translatesAutoresizingMaskIntoConstraints = false
41 | contentView.addSubview($0)
42 | }
43 |
44 | NSLayoutConstraint.activate([
45 | iconImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
46 | iconImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
47 | iconImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
48 | iconImageView.heightAnchor.constraint(equalToConstant: 32),
49 | iconImageView.widthAnchor.constraint(equalToConstant: 32),
50 |
51 | label.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 8),
52 | label.topAnchor.constraint(equalTo: contentView.topAnchor),
53 | label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
54 | label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
55 | label.heightAnchor.constraint(equalToConstant: 64)
56 | ])
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Example/Example/Screens/ThirdContentViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThirdContentViewController.swift
3 | // Example
4 | //
5 | // Created by Oleg Krasnov on 08/11/2023.
6 | //
7 |
8 | import Navidux
9 | import UIKit
10 |
11 | final class ThirdContentViewController: ViewController {
12 |
13 | private let dataSource: [ThirdContentCellModel] = [
14 | .init(image: .actions, title: "Actions"),
15 | .init(image: .add, title: "Add"),
16 | .init(image: .checkmark, title: "Checkmark"),
17 | .init(image: .remove, title: "Remove"),
18 | .init(image: .strokedCheckmark, title: "StrokedCheckmark"),
19 |
20 | .init(image: .actions, title: "Actions"),
21 | .init(image: .add, title: "Add"),
22 | .init(image: .checkmark, title: "Checkmark"),
23 | .init(image: .remove, title: "Remove"),
24 | .init(image: .strokedCheckmark, title: "StrokedCheckmark"),
25 |
26 | .init(image: .actions, title: "Actions"),
27 | .init(image: .add, title: "Add"),
28 | .init(image: .checkmark, title: "Checkmark"),
29 | .init(image: .remove, title: "Remove"),
30 | .init(image: .strokedCheckmark, title: "StrokedCheckmark")
31 | ]
32 |
33 | private lazy var pullBar = PullBar()
34 |
35 | private lazy var tableView: BSTableView = {
36 | let tableView = BSTableView(frame: .zero, style: .plain)
37 | tableView.delegate = self
38 | tableView.dataSource = self
39 | tableView.backgroundColor = .white
40 | tableView.register(
41 | ThirdContentTableViewCell.self,
42 | forCellReuseIdentifier: String(
43 | describing: ThirdContentTableViewCell.self
44 | )
45 | )
46 | return tableView
47 | }()
48 |
49 | init() {
50 | super.init()
51 | }
52 |
53 | required init?(coder: NSCoder) { nil }
54 |
55 | override func viewDidLoad() {
56 | super.viewDidLoad()
57 | [pullBar ,tableView].forEach {
58 | $0.translatesAutoresizingMaskIntoConstraints = false
59 | view.addSubview($0)
60 | }
61 |
62 | NSLayoutConstraint.activate([
63 | pullBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
64 | pullBar.topAnchor.constraint(equalTo: view.topAnchor),
65 | pullBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
66 | pullBar.heightAnchor.constraint(equalToConstant: 36),
67 |
68 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
69 | tableView.topAnchor.constraint(equalTo: pullBar.bottomAnchor),
70 | tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
71 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
72 | ])
73 | }
74 |
75 | }
76 |
77 | extension ThirdContentViewController: UITableViewDataSource {
78 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
79 | dataSource.count
80 | }
81 |
82 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
83 | guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ThirdContentTableViewCell.self), for: indexPath) as? ThirdContentTableViewCell
84 | else {
85 | return UITableViewCell()
86 | }
87 | cell.configureWith(
88 | image: dataSource[indexPath.row].image,
89 | title: dataSource[indexPath.row].title
90 | )
91 | return cell
92 | }
93 | }
94 |
95 | extension ThirdContentViewController: UITableViewDelegate {
96 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
97 | 40
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | //swift-tools-version: 5.7
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "Navidux",
7 | platforms: [.iOS(.v13)],
8 | products: [
9 | .library(
10 | name: "Navidux",
11 | targets: ["Navidux"]
12 | ),
13 | ],
14 | dependencies: [
15 | ],
16 | targets: [
17 | .target(
18 | name: "Navidux",
19 | dependencies: [],
20 | resources: [.process("resources/Media.xcassets")]
21 | ),
22 | .testTarget(
23 | name: "NaviduxTests",
24 | dependencies: ["Navidux"]
25 | ),
26 | ]
27 | )
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Navidux
2 |
3 | [](https://swift.org/package-manager)
4 | [](https://developer.apple.com/swift)
5 |
6 | Navidux is easy and simple module to build your navigation without thinking about complicated routes in factories.
7 |
8 | ## Table of Contents
9 | - [Navidux](#navidux)
10 | - [Table of Contents](#contents)
11 | - [About Navidux](#about-navidux)
12 | - [Requirements](#requirements)
13 | - [Installation](#installation)
14 | - [Preparation](#preparation)
15 | - [Usage](#usage)
16 | - [Initialisation phase](#initialisation-phase)
17 | - [Using phase](#using-phase)
18 | - [Roadmap](#roadmap)
19 |
20 | ## About Navidux
21 | We create this package with router to facilitate routing duties and improve reading in complicated projects. Analyzing previous project give us idea of creating independently navigation. It's did not depend on your project and may use in different combination and variations. Of course it can uses with Storyboard, UIKit and SwiftUI screens.
22 | The goals we want to achieve:
23 | - Easy using and create routes in screen modules.
24 | - Use with most architectual approaches (MVVM, MVC, VIPER, MVI, MVP etc).
25 | - Not complicated logic of library engine.
26 |
27 | 
28 |
29 | NavigationCoordinator core consists from:
30 | - Reducer function - ``actionReducer(action:)``;
31 | - ScreenAssembler - that implements in navigation depended module;
32 | - State - contains important properties for better functionality;
33 | - NavigationController - component that directry implements routing and hold some important information, like Screen Stack.
34 | Most core components have documentation on the spot with some examples.
35 |
36 | ## Requirements
37 | - iOS 13.0+
38 |
39 | ## Installation
40 | Swift Package Manager
41 |
42 | Swift Package Manager is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
43 |
44 | Xcode 11+ is required to build Navidux using Swift Package Manager.
45 | To integrate Navidux into your Xcode project using Swift Package Manager, add it to the dependencies value of your Package.swift:
46 | ``` swift
47 | dependencies: [
48 | .package(url: "https://github.com/RedMadRobot/navidux.git")
49 | ]
50 | ```
51 |
52 | ### Preparation
53 | Next you have to extends 2 object and implement 1 to use Navidux at maximum. Some were in your APP module extends:
54 | ``` swift
55 | extension NaviduxScreen {
56 | static let newScreen = NaviduxScreen(
57 | description: "someDescription",
58 | screenClass: YourScreenTypeInheritedFromUIViewController.self
59 | )
60 | }
61 | ```
62 | ``` swift
63 | extension Navidux.ScreenFactory {
64 | public var someScreenFactory: (NavigationCoordinator?, ScreenConfig) -> any NavigationScreen {
65 | { coordinator, config
66 | return MyViewControllerConformedNavigationScreen()
67 | }
68 | }
69 | ```
70 |
71 | And implement Navidux.ScreenAssembler protocol.
72 |
73 | ## Usage
74 | ### Initialisation phase
75 | To set Navidux as initial navigation controller. You need do installation and preparation phases. After you may initialise NavigationCoordinator like example below.
76 | ``` swift
77 | let navigationController = NavigationControllerImpl()
78 | let screenFactory: ScreenFactory = NaviduxScreenFactory()
79 | let alertFactory: AlertFactory = AlertFactoryImpl()
80 | let navigationCoordinatorProxy = NavigationCoordinatorProxy()
81 | let screenAssembler = NaviduxScreenAssembler(
82 | screenFactory: screenFactory,
83 | alertFactory: alertFactory,
84 | screenCoordinator: navigationCoordinatorProxy
85 | )
86 |
87 | let navigationCoordinator = NavigationCoordinator(
88 | navigationController,
89 | screenAssembler: screenAssembler
90 | )
91 | navigationCoordinatorProxy.subject = navigationCoordinator
92 | navigationCoordinator.actionReducer(
93 | action: .push(.firstScreen, .init(navigationTitle: ""), .fullscreen)
94 | )
95 |
96 | window?.rootViewController = navigationController
97 | ```
98 |
99 | ### Using phase
100 | For example in your UIViewController you may call NavigationCoordinator and ask it for action:
101 | ``` swift
102 | navigation?.actionReducer(
103 | action: .push(.nextScreen, .init(navigationTitle: "My title"), .fullscreen)
104 | )
105 | ```
106 |
107 | ## Roadmap
108 | 
109 |
--------------------------------------------------------------------------------
/Sources/Navidux/NaviduxScreen.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// NaviduxScreen its alternative to simple Enumeration with possibility to extension.
4 | /// if you want to add case with new screen you have to:
5 | /// ``` swift
6 | /// extension NaviduxScreen {
7 | /// static let newScreen = NaviduxScreen(
8 | /// screenClass: YourScreenTypeInheritedFromUIViewController.self
9 | /// )
10 | /// }
11 | /// ```
12 | /// - Note: You can place extension in another module.
13 | public class NaviduxScreen: ExtendableEnum {
14 | public var asScreenClass: UIViewController.Type
15 |
16 | public init(screenClass: UIViewController.Type) {
17 | self.asScreenClass = screenClass
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Navidux/Navigation.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 |
4 | public enum Navigation: Equatable {
5 | /// PresentationStyle uses to choose correct form of pushing your screen.
6 | /// - Note:
7 | /// + fullscren - its normal presentation like `UINavigationController.push(...)` without additional settings.
8 | /// + modal - its modal presentation like `UINavigationController.present(...)` without additional settings.
9 | /// + bottomSheet - its modal presentation of the screen from bottom part with simple animation.
10 | /// + custom - its fully customisable animation for `UINavigationController.push(...)` with your own parameters. *WORK IN PROGRESS*
11 | public enum PresentationStyle {
12 | case fullscreen
13 | case modal
14 | case bottomSheet(BottomSheetSize)
15 | case custom(UIViewControllerTransitioningDelegate)
16 | }
17 |
18 | public enum BottomSheetSize {
19 | case fixed(CGFloat)
20 | case halfScreen
21 | case fullScreen
22 | case auto
23 | }
24 |
25 | public enum RestructActionAnimation {
26 | case forward
27 | case backward
28 | }
29 |
30 | public enum Action {
31 | case push(NaviduxScreen, ScreenConfig, PresentationStyle)
32 | case pop(NullablePayload)
33 | case popUntil(NaviduxScreen, NullablePayload)
34 | case restruct(screens: [NavigationRestructable], animationType: RestructActionAnimation)
35 | case replaceCertain(NaviduxScreen, ScreenConfig, RestructActionAnimation)
36 | case showAlert(AlertConfiguration)
37 | }
38 | }
39 |
40 | public protocol NavigationRestructable {}
41 |
--------------------------------------------------------------------------------
/Sources/Navidux/NavigationController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// NavigationController its abstract object uses for navigation. Its a closest wrapper to UINavigation controller or alternative.
4 | public protocol NavigationController {
5 | /// - **screens**: return some representation of screen from UINavigationController.viewController. Difference in track screen in navigation stack.
6 | var screens: [any NavigationScreen] { get set }
7 | /// - **topScreen**: return screen from top of the
8 | var topScreen: (any NavigationScreen)? { get }
9 |
10 | /// - **addToStack**: method uses to add **NavigationScreen** into screen property.
11 | func addToStack(screen: any NavigationScreen)
12 | /// - **removeLastFromStack**: method uses to remove last **NavigationScreen** from screen property.
13 | func removeLastFromStack()
14 | /// - **removeTillFromStack**: method uses to remove last n **NavigationScreen** from screen property before condition met.
15 | func removeTillFromStack(screen: any NavigationScreen)
16 | /// - **rebuildNavStack**: method uses to replace all **NavigationScreen** from screen property with new ones.
17 | func rebuildNavStack(with screens: [any NavigationScreen])
18 |
19 | // MARK: UINavigationController properties and methods
20 |
21 | /// - **topViewController**: (UINavigationController compatibility) The top view controller on the stack.
22 | var topViewController: UIViewController? { get }
23 | /// - **viewControllers**: (UINavigationController compatibility) The current view controller stack.
24 | var viewControllers: [UIViewController] { get set }
25 |
26 | /// - **pushViewController(viewController:, animated:)**: (UINavigationController compatibility) Uses a horizontal slide transition. Has no effect if the view controller is already in the stack.
27 | func pushViewController(_ viewController: UIViewController, animated: Bool)
28 | /// - **popViewController(animated:)**: (UINavigationController compatibility) Returns the popped controller.
29 | @discardableResult
30 | func popViewController(animated: Bool) -> UIViewController?
31 | /// - **popToViewController(viewController:, animated:)**: (UINavigationController compatibility) Pops view controllers until the one specified is on top. Returns the popped controllers.
32 | @discardableResult
33 | func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]?
34 | /// - **present(viewControllerToPresent:, animated:, completion:)**: (UINavigationController compatibility) Presents a view controller modally..
35 | func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
36 | /// - **dismiss(animated:, completion:)**:(UINavigationController compatibility) Dismisses the view controller that was presented modally by the view controller.
37 | func dismiss(animated flag: Bool, completion: (() -> Void)?)
38 | /// - **setViewControllers(viewController:, animated:)**:(UINavigationController compatibility) If animated is YES, then simulate a push or pop depending on whether the new top view controller was previously in the stack.
39 | func setViewControllers(_ viewControllers: [UIViewController], animated: Bool)
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/Navidux/NavigationCoordinator.swift:
--------------------------------------------------------------------------------
1 | public final class NavigationCoordinator: Router {
2 | var navigationController: NavigationController
3 | var screenAssembler: ScreenAssembler
4 | public var state: NavigationStore
5 | let bottomSheetTransitioningDelegate = BSTransitioningDelegate()
6 |
7 | public init(
8 | _ controller: NavigationController,
9 | screenAssembler: some ScreenAssembler,
10 | state: NavigationStore = NavigationStore()
11 | ) {
12 | navigationController = controller
13 | self.screenAssembler = screenAssembler
14 | self.state = state
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/Navidux/NavigationCoordinatorProxy.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SafariServices
3 |
4 | ///Proxy give possibility to create `ScreenAssembler` that nessasary for `NavigationCoordinator`.
5 | ///Usage: after declaration `NavigationCoordinatorProxy` you can create `ScreenAssembler`.
6 | ///Next step to create real `Router`. And last stage is set subject property as real `Router`.
7 | /// - Example:
8 | ///``` swift
9 | ///let navigationController = NavigationControllerImpl()
10 | ///let screenFactory = NaviduxScreenFactory()
11 | ///let alertFactory = AlertFactoryImpl()
12 | ///let navigationCoordinatorProxy = NavigationCoordinatorProxy()
13 | ///let screenAssembler = NaviduxScreenAssembler(
14 | /// screenFactory: screenFactory,
15 | /// alertFactory: alertFactory,
16 | /// screenCoordinator: navigationCoordinatorProxy
17 | ///)
18 | ///
19 | ///let navigationCoordinator = NavigationCoordinator(
20 | /// navigationController,
21 | /// screenAssembler: screenAssembler
22 | ///)
23 | ///navigationCoordinatorProxy.subject = navigationCoordinator
24 | ///```
25 | public final class NavigationCoordinatorProxy: Router {
26 | public var subject: NavigationCoordinator!
27 |
28 | public init(subject: NavigationCoordinator? = nil) {
29 | self.subject = subject
30 | }
31 |
32 | public func route(with action: Navigation.Action) {
33 | subject.route(with: action)
34 | }
35 |
36 | public func findCertain(controller: NaviduxScreen) -> (any NavigationScreen)? {
37 | subject.findCertain(controller: controller)
38 | }
39 |
40 | public func findFirstCertain(controller: NaviduxScreen) -> (any NavigationScreen)? {
41 | subject.findFirstCertain(controller: controller)
42 | }
43 |
44 | public func presentSFSafaryViewController(url: URL, delegate: SFSafariViewControllerDelegate?) {
45 | subject.presentSFSafaryViewController(url: url, delegate: delegate)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/Navidux/Router.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SafariServices
3 |
4 | /// Main Object that used in navigation. Have many different types of presentation and inner methods to manipulate navigation stack.
5 | /// Work on states with associated values.
6 | /// - Note use **route(with:)** function with some action to change state of navigation stack. It's trigger inner function to change navigation screen.
7 | public protocol Router: AnyObject {
8 | func route(with action: Navigation.Action)
9 | func findCertain(controller: NaviduxScreen) -> (any NavigationScreen)?
10 | func findFirstCertain(controller: NaviduxScreen) -> (any NavigationScreen)?
11 | func presentSFSafaryViewController(url: URL, delegate: SFSafariViewControllerDelegate?)
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Navidux/ScreenAssembler.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SafariServices
3 |
4 | public protocol ScreenAssembler {
5 | func assemblyScreen(components: ScreenAsseblyComponents) -> any NavigationScreen
6 | func assemblyAlert(configuration: AlertConfiguration) -> AlertScreen
7 | func assemblySFSafaryViewController(url: URL, delegate: SFSafariViewControllerDelegate?) -> SFSafariViewController
8 | }
9 |
10 | extension ScreenAssembler {
11 | public func assemblySFSafaryViewController(url: URL, delegate: SFSafariViewControllerDelegate?) -> SFSafariViewController {
12 | let controller = SFSafariViewController(url: url)
13 | controller.delegate = delegate
14 | return controller
15 | }
16 | }
17 |
18 | public struct ScreenAsseblyComponents: NavigationRestructable {
19 | public let screenType: NaviduxScreen
20 | public let config: ScreenConfig
21 |
22 | public init(screenType: NaviduxScreen, config: ScreenConfig) {
23 | self.screenType = screenType
24 | self.config = config
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Navidux/ScreenFactory.swift:
--------------------------------------------------------------------------------
1 | /// ScreenFactory uses as part of `ScreenAssembly` module and helps you to create `NavigationScreen` in `assmblyScreen` function.
2 | /// You can extend protocol with `(Dependencies) -> (Router, Configuration) -> Resulted VC` function and call them in assembler
3 | /// on call.
4 | /// - Example:
5 | /// ``` swift
6 | /// import Navidux
7 | ///
8 | /// extension Navidux.ScreenFactory {
9 | /// public var someScreenFactory: (Coordinator?, ScreenConfig) -> any NavigationScreen {
10 | /// { router, config
11 | /// return MyViewControllerConformedNavigationScreen()
12 | /// }
13 | /// }
14 | /// ```
15 | public protocol ScreenFactory {}
16 |
--------------------------------------------------------------------------------
/Sources/Navidux/alert/AlertConfiguration.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public struct AlertConfiguration: Equatable {
4 | public struct ButtonAction: Equatable {
5 | let title: String
6 | let style: ButtonStyle
7 | let action: () -> Void
8 |
9 | public static func == (lhs: Self, rhs: Self) -> Bool {
10 | lhs.title == rhs.title && lhs.style == rhs.style
11 | }
12 | }
13 |
14 | public enum ButtonStyle: Equatable {
15 | case `default`
16 | case cancel
17 | case destructive
18 | }
19 |
20 | public enum PresentationStyle: Equatable {
21 | case actionSheet
22 | case center
23 | }
24 |
25 | let title: String
26 | let message: String
27 | let style: PresentationStyle
28 | let actions: [ButtonAction]
29 | }
30 |
31 | extension AlertConfiguration.ButtonStyle {
32 | var asUIAlertActionStyle: UIAlertAction.Style {
33 | switch self {
34 | case .cancel:
35 | return .cancel
36 | case .destructive:
37 | return .destructive
38 | case .default:
39 | return .default
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/Navidux/alert/AlertFactory.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol AlertFactory {
4 | func createAlert(configuration: AlertConfiguration) -> AlertScreen
5 | }
6 |
7 | public final class AlertFactoryImpl: AlertFactory {
8 | public func createAlert(configuration : AlertConfiguration) -> AlertScreen {
9 | AlertScreen(configuration: configuration)
10 | }
11 |
12 | public init() {}
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Navidux/alert/AlertScreen.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public final class AlertScreen {
4 | let configuration: AlertConfiguration
5 |
6 | public init(configuration: AlertConfiguration) {
7 | self.configuration = configuration
8 | }
9 |
10 | func generateAlert(dismissedCallback: @escaping () -> Void) -> UIAlertController {
11 | let alert = UIAlertController(
12 | title: configuration.title,
13 | message: configuration.message,
14 | preferredStyle: configuration.style == .center ? .alert : .actionSheet
15 | )
16 | configuration.actions.map { config in
17 | UIAlertAction(
18 | title: config.title,
19 | style: config.style.asUIAlertActionStyle,
20 | handler: { _ in
21 | dismissedCallback()
22 | config.action()
23 | }
24 | )
25 | }.forEach { alert.addAction($0) }
26 | return alert
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Navidux/extensions/CGFloat+Extension.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension CGFloat {
4 | func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGFloat {
5 | let multiplier = 1 / (1 - decelerationRate.rawValue) / 1_000
6 | return self * multiplier
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/Navidux/extensions/CGPoint+Extension.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension CGPoint {
4 | func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint {
5 | CGPoint(x: x.projectedOffset(decelerationRate: decelerationRate),
6 | y: y.projectedOffset(decelerationRate: decelerationRate))
7 | }
8 |
9 | static func + (left: CGPoint, right: CGPoint) -> CGPoint {
10 | CGPoint(x: left.x + right.x,
11 | y: left.y + right.y)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Navidux/extensions/ExtendableEnum.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol ExtendableEnum: AnyObject, Hashable { }
4 |
5 | extension ExtendableEnum {
6 | public func hash(into hasher: inout Hasher) {
7 | hasher.combine(ObjectIdentifier(self))
8 | }
9 | }
10 |
11 | public func ==(lhs: T, rhs: T) -> Bool {
12 | return lhs === rhs
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Navidux/extensions/Storyboard.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | protocol Storyboarded {
4 | static func instantiate() -> Self
5 | }
6 |
7 | extension Storyboarded where Self: UIViewController {
8 | static func instantiate(storyboardName: String) -> Self {
9 | let className = NSStringFromClass(self).components(separatedBy: ".").last
10 | let storyboard = UIStoryboard(name: storyboardName, bundle: Bundle.main)
11 | return storyboard.instantiateViewController(withIdentifier: className!) as! Self
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/Navidux/extensions/UIImage+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+Extension.swift
3 | //
4 | //
5 | // Created by Oleg Krasnov on 09/11/2023.
6 | //
7 |
8 | import UIKit
9 |
10 | public extension UIImage {
11 | static let pullBarIcon = UIImage(named: "PullBarIcon", in: .module, with: nil)
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Navidux/extensions/UIPanGestureRecognizer+Extension.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIPanGestureRecognizer {
4 | func incrementToBottom(maxTranslation: CGFloat) -> CGFloat {
5 | let translation = self.translation(in: view).y
6 | setTranslation(.zero, in: nil)
7 |
8 | let percentIncrement = translation / maxTranslation
9 | return percentIncrement
10 | }
11 |
12 | // На основе смещения и его скорости рассчитываем, хотел ли пользователь закрыть экран.
13 | func isProjectedToDownHalf(maxTranslation: CGFloat, percentComplete: CGFloat) -> Bool {
14 | let velocityOffset = velocity(in: view).projectedOffset(decelerationRate: .normal)
15 | let verticalTranslation = maxTranslation * percentComplete
16 | let translation = CGPoint(x: 0, y: verticalTranslation) + velocityOffset
17 |
18 | let isPresentationCompleted = translation.y > maxTranslation / 2
19 | return isPresentationCompleted
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Navidux/implemention/NavigationControllerImpl.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public final class NavigationControllerImpl: UINavigationController, NavigationController {
4 |
5 | // MARK: - Private properties
6 |
7 | private var navbarConfiguration: (UINavigationController) -> Void = { controller in
8 | controller.view.backgroundColor = .white
9 | controller.navigationBar.isTranslucent = false
10 | controller.navigationBar.backgroundColor = .white
11 | controller.navigationBar.shadowImage = .init()
12 | controller.navigationBar.barTintColor = .white
13 | controller.navigationBar.tintColor = .black
14 | controller.navigationBar.titleTextAttributes = [
15 | NSAttributedString.Key.foregroundColor: UIColor.black
16 | ]
17 | }
18 |
19 | // MARK: - Public properties
20 |
21 | public var screens: [any NavigationScreen] = [] {
22 | didSet { debugPrint("screenStack: \(screens.map { $0.tag })") }
23 | }
24 |
25 | public var topScreen: (any NavigationScreen)? {
26 | screens.last
27 | }
28 |
29 | // MARK: - Init
30 |
31 | public init(
32 | navbarConfiguration: ((UINavigationController) -> Void)? = nil
33 | ) {
34 | if let navbarConfiguration {
35 | self.navbarConfiguration = navbarConfiguration
36 | }
37 | super.init(nibName: nil, bundle: nil)
38 | }
39 |
40 | @available(*, deprecated, message: "use init() instead.")
41 | required init?(coder aDecoder: NSCoder) {
42 | super.init(coder: aDecoder)
43 | }
44 |
45 | // MARK: - Lifecycle
46 |
47 | public override func viewDidLoad() {
48 | super.viewDidLoad()
49 | configureAppearance()
50 | }
51 |
52 | // MARK: - Public methods
53 |
54 | public func addToStack(screen: any NavigationScreen) {
55 | screens.append(screen)
56 | }
57 |
58 | public func removeLastFromStack() {
59 | guard screens.count != 1 else {
60 | print("Navigation Coordinator can't pop last screen")
61 | return
62 | }
63 | screens.removeLast()
64 | }
65 |
66 | public func removeTillFromStack(screen: any NavigationScreen) {
67 | if let idx = screens.lastIndex(where: { $0 == screen }) {
68 | screens.removeLast(screens.count - (idx + 1))
69 | }
70 | }
71 |
72 | public func rebuildNavStack(with screens: [any NavigationScreen]) {
73 | self.screens = screens
74 | }
75 |
76 | public override var childForStatusBarStyle: UIViewController? {
77 | self.topViewController
78 | }
79 |
80 | // MARK: - Private methods
81 |
82 | public func configureAppearance() {
83 | navbarConfiguration(self)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/Navidux/implemention/NavigationReducer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SafariServices
3 |
4 | extension NavigationCoordinator {
5 |
6 | // MARK: - Public methods
7 |
8 | public func route(with action: Navigation.Action) {
9 | switch action {
10 | case let .push(screen, config, presentationStyle):
11 | var controller: any NavigationScreen
12 | controller = screenAssembler.assemblyScreen(
13 | components: ScreenAsseblyComponents(
14 | screenType: screen,
15 | config: config
16 | )
17 | )
18 | pushNew(screen: controller, style: presentationStyle, animated: true)
19 |
20 | case let .pop(payload):
21 | popLast(animated: true)
22 | if let topScreen = navigationController.topScreen {
23 | topScreen.gotUpdatedData(payload)
24 | topScreen.output(payload)
25 | }
26 |
27 | case let .popUntil(screen, payload):
28 | let certainController = findCertain(controller: screen, in: navigationController.screens)
29 | if let vc = certainController {
30 | popTo(screen: vc, animated: true)
31 | if let topScreen = navigationController.topScreen {
32 | topScreen.gotUpdatedData(payload)
33 | topScreen.output(payload)
34 | }
35 | }
36 |
37 | case let .restruct(screens, animationType):
38 | let controllers: [any NavigationScreen] = screens.compactMap { screen in
39 | switch screen {
40 | case let components as ScreenAsseblyComponents:
41 | return screenAssembler.assemblyScreen(components: components)
42 | case let navigationScreen as any NavigationScreen:
43 | return navigationScreen
44 | default:
45 | return nil
46 | }
47 | }
48 | restruct(with: controllers, animated: true, animationType: animationType)
49 |
50 | case let .replaceCertain(screen, config, animationType):
51 | var controllers = navigationController.viewControllers.compactMap { $0 as? any NavigationScreen }
52 | let newController = screenAssembler.assemblyScreen(
53 | components: ScreenAsseblyComponents(
54 | screenType: screen,
55 | config: config
56 | )
57 | )
58 | if controllers.last != nil {
59 | controllers[controllers.count - 1] = newController
60 | } else {
61 | controllers = [newController]
62 | }
63 | restruct(with: controllers, animated: true, animationType: animationType)
64 |
65 | case let .showAlert(configuration):
66 | let assembledAlert = screenAssembler.assemblyAlert(configuration: configuration)
67 | showAlert(alert: assembledAlert)
68 | }
69 | }
70 |
71 | public func findCertain(controller: NaviduxScreen) -> (any NavigationScreen)? {
72 | findCertain(controller: controller, in: navigationController.screens)
73 | }
74 |
75 | public func findFirstCertain(controller: NaviduxScreen) -> (any NavigationScreen)? {
76 | findFirstCertain(controller: controller, in: navigationController.screens)
77 | }
78 |
79 | public func presentSFSafaryViewController(url: URL, delegate: SFSafariViewControllerDelegate?) {
80 | let controller = screenAssembler.assemblySFSafaryViewController(url: url, delegate: delegate)
81 | presentSFSafaryViewController(controller, animated: true)
82 | }
83 |
84 | // MARK: - Private methods
85 |
86 | private func findCertain(
87 | controller: NaviduxScreen,
88 | in stack: [any NavigationScreen]
89 | ) -> (any NavigationScreen)? {
90 | return stack.last(where: { [controller] in
91 | $0.isKind(of: controller.asScreenClass)
92 | })
93 | }
94 |
95 | private func findFirstCertain(
96 | controller: NaviduxScreen,
97 | in stack: [any NavigationScreen]
98 | ) -> (any NavigationScreen)? {
99 | return stack.first(where: { [controller] in
100 | $0.isKind(of: controller.asScreenClass)
101 | })
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Sources/Navidux/implemention/NavigationRouter.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SafariServices
3 |
4 | extension NavigationCoordinator {
5 |
6 | // MARK: - Public methods
7 |
8 | func pushNew(screen: any NavigationScreen, style: Navigation.PresentationStyle, animated: Bool) {
9 | switch style {
10 | case .fullscreen:
11 | screen.navigationCallback = { [weak self, weak screen] in
12 | self?.controllerDismissed(screenTag: screen?.tag)
13 | }
14 | navigationController.pushViewController(screen, animated: animated)
15 |
16 | case .modal:
17 | screen.isModal = true
18 | screen.navigationCallback = { [weak self, weak screen] in
19 | self?.modalControllerDismissed(screenTag: screen?.tag)
20 | }
21 | if state.hasOverlay {
22 | navigationController.topScreen?.present(screen, animated: animated, completion: nil)
23 | } else {
24 | navigationController.present(screen, animated: animated, completion: nil)
25 | }
26 | state.hasOverlay = true
27 |
28 | // TODO: - Допилить SheetViewController
29 | case let .bottomSheet(size):
30 | screen.isModal = true
31 | screen.navigationCallback = { [weak self, weak screen] in
32 | self?.modalControllerDismissed(screenTag: screen?.tag)
33 | }
34 | state.hasOverlay = true
35 |
36 | switch size {
37 | case .auto:
38 | bottomSheetTransitioningDelegate.sheetSize = .auto
39 | screen.transitioningDelegate = bottomSheetTransitioningDelegate
40 | screen.modalPresentationStyle = .custom
41 | case .fixed(let height):
42 | bottomSheetTransitioningDelegate.sheetSize = .fixed(height)
43 | screen.transitioningDelegate = bottomSheetTransitioningDelegate
44 | screen.modalPresentationStyle = .custom
45 | case .halfScreen:
46 | bottomSheetTransitioningDelegate.sheetSize = .halfScreen
47 | screen.transitioningDelegate = bottomSheetTransitioningDelegate
48 | screen.modalPresentationStyle = .custom
49 | case .fullScreen:
50 | screen.modalPresentationStyle = .formSheet
51 | }
52 |
53 | navigationController.present(screen, animated: true, completion: nil)
54 |
55 | // TODO: - Реализовать
56 | case let .custom(delegate):
57 | break
58 | }
59 | navigationController.addToStack(screen: screen)
60 | }
61 |
62 | func showAlert(alert: AlertScreen) {
63 | guard !state.isAlertShow else { return }
64 | state.isAlertShow = true
65 | navigationController.present(
66 | alert.generateAlert(
67 | dismissedCallback: { [weak self] in
68 | self?.alertControllerDismissed()
69 | }
70 | ),
71 | animated: true,
72 | completion: nil
73 | )
74 | }
75 |
76 | func popLast(animated: Bool) {
77 | if state.hasOverlay {
78 | state.hasOverlay = false
79 | navigationController.dismiss(animated: animated, completion: nil)
80 | } else {
81 | navigationController.popViewController(animated: animated)
82 | }
83 |
84 | navigationController.removeLastFromStack()
85 | }
86 |
87 | func popTo(screen: any NavigationScreen, animated: Bool) {
88 | if state.hasOverlay {
89 | state.hasOverlay = false
90 | navigationController.dismiss(animated: animated, completion: nil)
91 | }
92 | navigationController.popToViewController(screen, animated: animated)
93 | navigationController.removeTillFromStack(screen: screen)
94 | }
95 |
96 | func restruct(
97 | with screens: [any NavigationScreen],
98 | animated: Bool,
99 | animationType: Navigation.RestructActionAnimation
100 | ) {
101 | guard navigationController.topViewController != screens.last else {
102 | navigationController.viewControllers = screens
103 | return
104 | }
105 |
106 | switch animationType {
107 | case .forward:
108 | updateStackWithForwardAnimation(
109 | screens: screens,
110 | navigationController: &navigationController,
111 | store: &state,
112 | animated: animated
113 | )
114 | case .backward:
115 | updateStackWithBackwardAnimation(
116 | screens: screens,
117 | navigationController: &navigationController,
118 | store: &state,
119 | animated: animated
120 | )
121 | }
122 |
123 | navigationController.rebuildNavStack(with: screens)
124 | }
125 |
126 | func presentSFSafaryViewController(_ controller: SFSafariViewController, animated: Bool) {
127 | if state.hasOverlay {
128 | navigationController.topScreen?.present(controller, animated: animated, completion: nil)
129 | } else {
130 | navigationController.present(controller, animated: animated, completion: nil)
131 | }
132 | state.hasOverlay = true
133 | }
134 |
135 | // MARK: - Private methods
136 |
137 | private func updateStackWithBackwardAnimation(
138 | screens: [any NavigationScreen],
139 | navigationController: inout NavigationController,
140 | store: inout NavigationStore,
141 | animated: Bool
142 | ) {
143 | var newStack = (screens.map { $0 as UIViewController })
144 | if !store.hasOverlay {
145 | newStack += [navigationController.topViewController].compactMap { $0 }
146 | }
147 | navigationController.viewControllers = newStack
148 |
149 | if navigationController.topScreen?.isModal ?? false {
150 | store.hasOverlay = false
151 | navigationController.dismiss(animated: animated, completion: nil)
152 | } else {
153 | navigationController.popViewController(animated: animated)
154 | }
155 | }
156 |
157 | private func updateStackWithForwardAnimation(
158 | screens: [any NavigationScreen],
159 | navigationController: inout NavigationController,
160 | store: inout NavigationStore,
161 | animated: Bool
162 | ) {
163 | let newStack = screens.map { $0 as UIViewController }
164 | navigationController.setViewControllers(newStack, animated: animated)
165 | }
166 |
167 | private func checkEquality(lhs: [any NavigationScreen], rhs: [any NavigationScreen]) -> Bool {
168 | guard lhs.count == rhs.count else { return false }
169 |
170 | var result: Bool = true
171 |
172 | for (leftElement, rightElement) in zip(lhs, rhs) {
173 | if leftElement.tag != rightElement.tag {
174 | result = false
175 | break
176 | }
177 | }
178 |
179 | return result
180 | }
181 |
182 | private func modalControllerDismissed(screenTag: String?) {
183 | guard let screenTag = screenTag else { return }
184 |
185 | let topScreen = navigationController.topScreen
186 | if state.hasOverlay,
187 | topScreen?.isModal ?? false,
188 | topScreen?.tag == screenTag {
189 | navigationController.removeLastFromStack()
190 | if !(navigationController.topScreen?.isModal ?? false) {
191 | state.hasOverlay = false
192 | }
193 | navigationController.topScreen?.gotUpdatedData(topScreen?.dataToSendFromModal)
194 | }
195 | }
196 |
197 | private func alertControllerDismissed() {
198 | state.isAlertShow = false
199 | }
200 |
201 | private func controllerDismissed(screenTag: String?) {
202 | guard let screenTag = screenTag else { return }
203 |
204 | let topScreen = navigationController.topScreen
205 | if !(topScreen?.isModal ?? true) && topScreen?.tag == screenTag {
206 | navigationController.removeLastFromStack()
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/Sources/Navidux/implemention/NavigationStore.swift:
--------------------------------------------------------------------------------
1 | public struct NavigationStore {
2 | public var isAlertShow: Bool = false
3 | public var hasOverlay: Bool = false
4 |
5 | public init(isAlertShow: Bool = false, hasOverlay: Bool = false) {
6 | self.isAlertShow = isAlertShow
7 | self.hasOverlay = hasOverlay
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/Navidux/implemention/Payload.swift:
--------------------------------------------------------------------------------
1 | public typealias NullablePayload = (any Payload)?
2 |
3 | /// Payload uses to transfer some data from screen to screen.
4 | /// In default implementation on push new screen `Payload` added to `ScreenConfig` objet.
5 | /// You have access to data in `ScreenAssemble` object.
6 | /// Another case with access, you have on `.pop` and `.popUntil` actions.
7 | /// This payload return as parameter in default implementation of `gotUpdatedData(_:)` or `output: ((NullablePayload) -> Void)` in `NavigationScreen` protocol.
8 | /// If you want access data at this access point you have to override `gotUpdatedData(_:)` function in your inherited ViewController or use `output: ((NullablePayload) -> Void)`.
9 | public protocol Payload: Hashable {
10 | associatedtype T: Hashable
11 | var data: T { get }
12 | }
13 |
14 | /// It's shortcut for Payload.
15 | public struct VoidPayload: Payload {
16 | public var data = false
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Navidux/resources/Media.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/Navidux/resources/Media.xcassets/PullbarIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "PullBarIcon.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/Navidux/resources/Media.xcassets/PullbarIcon.imageset/PullBarIcon.pdf:
--------------------------------------------------------------------------------
1 | %PDF-1.7
2 |
3 | 1 0 obj
4 | << /ExtGState << /E1 << /ca 0.200000 >> >> >>
5 | endobj
6 |
7 | 2 0 obj
8 | << /Length 3 0 R >>
9 | stream
10 | /DeviceRGB CS
11 | /DeviceRGB cs
12 | q
13 | /E1 gs
14 | 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
15 | 0.000000 0.000000 0.000000 scn
16 | 0.000000 2.000000 m
17 | 0.000000 3.104569 0.895431 4.000000 2.000000 4.000000 c
18 | 46.000000 4.000000 l
19 | 47.104568 4.000000 48.000000 3.104569 48.000000 2.000000 c
20 | 48.000000 2.000000 l
21 | 48.000000 0.895431 47.104568 0.000000 46.000000 0.000000 c
22 | 2.000001 0.000000 l
23 | 0.895431 0.000000 0.000000 0.895431 0.000000 2.000000 c
24 | 0.000000 2.000000 l
25 | h
26 | f
27 | n
28 | Q
29 |
30 | endstream
31 | endobj
32 |
33 | 3 0 obj
34 | 466
35 | endobj
36 |
37 | 4 0 obj
38 | << /Annots []
39 | /Type /Page
40 | /MediaBox [ 0.000000 0.000000 48.000000 4.000000 ]
41 | /Resources 1 0 R
42 | /Contents 2 0 R
43 | /Parent 5 0 R
44 | >>
45 | endobj
46 |
47 | 5 0 obj
48 | << /Kids [ 4 0 R ]
49 | /Count 1
50 | /Type /Pages
51 | >>
52 | endobj
53 |
54 | 6 0 obj
55 | << /Pages 5 0 R
56 | /Type /Catalog
57 | >>
58 | endobj
59 |
60 | xref
61 | 0 7
62 | 0000000000 65535 f
63 | 0000000010 00000 n
64 | 0000000074 00000 n
65 | 0000000596 00000 n
66 | 0000000618 00000 n
67 | 0000000790 00000 n
68 | 0000000864 00000 n
69 | trailer
70 | << /ID [ (some) (id) ]
71 | /Root 6 0 R
72 | /Size 7
73 | >>
74 | startxref
75 | 923
76 | %%EOF
--------------------------------------------------------------------------------
/Sources/Navidux/screens/ActivityViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityViewController.swift
3 | // Navidux
4 | //
5 | // Created by Stanislav Anatskii on 20.12.2022.
6 | // Copyright © 2022 red_mad_robot. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public final class ActivityViewController: UIActivityViewController, NavigationScreen {
12 |
13 | // MARK: - Public properties
14 |
15 | public var tag: String
16 | public var isModal: Bool = true
17 | public var navigationCallback: (() -> Void)?
18 | public var onBackCallback: () -> Void
19 | public var dataToSendFromModal: NullablePayload
20 | public var output: ((NullablePayload) -> Void)
21 |
22 | // MARK: - Init
23 |
24 | public init(
25 | activityItems: [Any],
26 | applicationActivities: [UIActivity]?,
27 | tag: String = UUID().uuidString,
28 | output: @escaping (NullablePayload) -> Void
29 | ) {
30 | onBackCallback = { }
31 | self.output = output
32 | self.tag = tag
33 | self.dataToSendFromModal = nil
34 |
35 | super.init(
36 | activityItems: activityItems,
37 | applicationActivities: applicationActivities
38 | )
39 | }
40 |
41 | // MARK: - Lifecycle
42 |
43 | public override func viewWillDisappear(_ animated: Bool) {
44 | super.viewWillDisappear(animated)
45 | navigationCallback?()
46 | }
47 |
48 | // MARK: - Public methods
49 |
50 | public func gotUpdatedData(_ payload: NullablePayload) {}
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/BSPresentationController.swift:
--------------------------------------------------------------------------------
1 | // https://habr.com/ru/companies/koshelek/articles/703260/
2 | import UIKit
3 |
4 | final class BSPresentationController: UIPresentationController {
5 |
6 | public var sheetSize: Navigation.BottomSheetSize = .auto
7 |
8 | private lazy var dimmView: UIView = {
9 | let view = UIView()
10 | view.backgroundColor = UIColor.black.withAlphaComponent(0.75)
11 | view.addGestureRecognizer(tapRecognizer)
12 | return view
13 | }()
14 |
15 | private lazy var tapRecognizer: UITapGestureRecognizer = {
16 | let recognizer = UITapGestureRecognizer(
17 | target: self,
18 | action: #selector(handleTap)
19 | )
20 | return recognizer
21 | }()
22 |
23 | override var shouldPresentInFullscreen: Bool {
24 | false
25 | }
26 |
27 | // Этот метод UIKit вызовет перед стартом презентации.
28 | // Расположит презентуемый контроллер в containerView presentation controller’а.
29 | override func presentationTransitionWillBegin() {
30 | super.presentationTransitionWillBegin()
31 |
32 | guard let containerView = containerView,
33 | let presentedView = presentedView
34 | else { return }
35 |
36 | [dimmView, presentedView].forEach {
37 | $0.translatesAutoresizingMaskIntoConstraints = false
38 | containerView.addSubview($0)
39 | }
40 |
41 | dimmView.alpha = 0
42 | performAlongsideTransitionIfPossible {
43 | self.dimmView.alpha = 1
44 | }
45 |
46 | var constraints: [NSLayoutConstraint] = [
47 | presentedView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
48 | presentedView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
49 | presentedView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
50 |
51 | dimmView.topAnchor.constraint(equalTo: containerView.topAnchor),
52 | dimmView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
53 | dimmView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
54 | dimmView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
55 | ]
56 |
57 | switch sheetSize {
58 | case .fixed(let height):
59 | constraints.append(
60 | presentedView.heightAnchor.constraint(equalToConstant: height)
61 | )
62 | case .halfScreen:
63 | constraints.append(
64 | presentedView.heightAnchor.constraint(
65 | lessThanOrEqualTo: containerView.heightAnchor,
66 | constant: -(containerView.bounds.height / 2)
67 | )
68 | )
69 | case .auto:
70 | constraints.append(
71 | presentedView.heightAnchor.constraint(
72 | lessThanOrEqualTo: containerView.heightAnchor,
73 | constant: -containerView.safeAreaInsets.top
74 | )
75 | )
76 | case .fullScreen:
77 | break
78 | }
79 |
80 | NSLayoutConstraint.activate(constraints)
81 | }
82 |
83 | // Удаляем subviews из контейнера, если транзишен был прерван.
84 | override func presentationTransitionDidEnd(_ completed: Bool) {
85 | if !completed {
86 | dimmView.removeFromSuperview()
87 | presentedView?.removeFromSuperview()
88 | }
89 | }
90 |
91 | override func dismissalTransitionWillBegin() {
92 | super.dismissalTransitionWillBegin()
93 | performAlongsideTransitionIfPossible {
94 | self.dimmView.alpha = 0
95 | }
96 | }
97 |
98 | private func performAlongsideTransitionIfPossible(_ animation: @escaping () -> Void ) {
99 | guard let coordinator = presentedViewController.transitionCoordinator else {
100 | animation()
101 | return
102 | }
103 |
104 | coordinator.animate { _ in
105 | animation()
106 | }
107 | }
108 |
109 | @objc
110 | private func handleTap(_ sender: UITapGestureRecognizer) {
111 | presentingViewController.dismiss(animated: true)
112 | }
113 | }
114 |
115 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/BSScrollableViews/BSCollectionView.swift:
--------------------------------------------------------------------------------
1 | // https://habr.com/ru/companies/koshelek/articles/703260/
2 | import UIKit
3 |
4 | open class BSCollectionView: UICollectionView {
5 |
6 | // При получении самого актуального значения высоты contentSize, запускаем обновление константы
7 | open override var contentSize: CGSize {
8 | didSet {
9 | fixHeight()
10 | }
11 | }
12 |
13 | // Создаем констрейнт с минимальным приоритетом, чтобы в случае, если высота контента будет больше,
14 | // чем может быть высота bottom sheet, не случился конфликт приоритетов.
15 | open lazy var collectionHeightConstraint: NSLayoutConstraint = {
16 | let constraint = heightAnchor.constraint(equalToConstant: 0)
17 | constraint.priority = .defaultLow
18 | constraint.isActive = true
19 | return constraint
20 | }()
21 |
22 | // Также, кроме высоты, учитываем и все дополнительные отступы и инсеты.
23 | open func fixHeight() {
24 | var height = collectionViewLayout.collectionViewContentSize.height
25 | + contentInset.top
26 | + contentInset.bottom
27 | + safeAreaInsets.bottom
28 | (collectionViewLayout as? UICollectionViewFlowLayout).map { height += $0.sectionInset.top }
29 | (collectionViewLayout as? UICollectionViewFlowLayout).map { height += $0.sectionInset.bottom }
30 |
31 | // Значение высоты равное 0 нет смысла обновлять, а равное infinity — ломает констрейнты, а вместе с ними и autoLayout
32 | if height != 0 && height != CGFloat.infinity {
33 | collectionHeightConstraint.constant = height
34 | }
35 | }
36 |
37 | open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
38 | guard gestureRecognizer === panGestureRecognizer else {
39 | return true
40 | }
41 |
42 | if contentOffset.y == -contentInset.top, panGestureRecognizer.velocity(in: nil).y > 0 {
43 | return false
44 | }
45 |
46 | if contentOffset.y < -contentInset.top {
47 | return false
48 | }
49 |
50 | return true
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/BSScrollableViews/BSScrollView.swift:
--------------------------------------------------------------------------------
1 | // https://habr.com/ru/companies/koshelek/articles/703260/
2 |
3 | import UIKit
4 |
5 | open class BSScrollView: UIScrollView {
6 |
7 | // При получении самого актуального значения высоты contentSize, запускаем обновление константы
8 | open override var contentSize: CGSize {
9 | didSet {
10 | fixHeight()
11 | }
12 | }
13 |
14 | // Создаем констрейнт с минимальным приоритетом, чтобы в случае, если высота контента будет больше,
15 | // чем может быть высота bottom sheet, не случился конфликт приоритетов.
16 | open lazy var scrollHeightConstraint: NSLayoutConstraint = {
17 | let constraint = heightAnchor.constraint(equalToConstant: 0)
18 | constraint.priority = .defaultLow
19 | constraint.isActive = true
20 | return constraint
21 | }()
22 |
23 | // Обновляем констрейнт высоты на основе содержимого и отступов.
24 | open func fixHeight() {
25 | var height = contentSize.height
26 | height += contentInset.top
27 | height += contentInset.bottom
28 | height += safeAreaInsets.bottom
29 |
30 | // Значение высоты равное 0 нет смысла обновлять, а равное infinity — ломает констрейнты, а вместе с ними и autoLayout
31 | if height != 0 && height != CGFloat.infinity {
32 | scrollHeightConstraint.constant = height
33 | }
34 | }
35 |
36 | open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
37 | guard gestureRecognizer === panGestureRecognizer else {
38 | return true
39 | }
40 |
41 | if contentOffset.y <= -contentInset.top, panGestureRecognizer.velocity(in: self).y > 0 {
42 | return false
43 | }
44 |
45 | if contentOffset.y < -contentInset.top {
46 | return false
47 | }
48 |
49 | return true
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/BSScrollableViews/BSTableView.swift:
--------------------------------------------------------------------------------
1 | // https://habr.com/ru/companies/koshelek/articles/703260/
2 |
3 | import UIKit
4 |
5 | open class BSTableView: UITableView {
6 |
7 | // При получении самого актуального значения высоты contentSize, запускаем обновление константы
8 | open override var contentSize: CGSize {
9 | didSet {
10 | fixHeight()
11 | }
12 | }
13 |
14 | // Создаем констрейнт с минимальным приоритетом, чтобы в случае, если высота контента будет больше,
15 | // чем может быть высота bottom sheet, не случился конфликт приоритетов.
16 | open lazy var collectionHeightConstraint: NSLayoutConstraint = {
17 | let constraint = heightAnchor.constraint(equalToConstant: 0)
18 | constraint.priority = .defaultLow
19 | constraint.isActive = true
20 | return constraint
21 | }()
22 |
23 | // Также, кроме высоты, учитываем и все дополнительные отступы и инсеты.
24 | open func fixHeight() {
25 | var height = contentSize.height
26 | height += contentInset.top
27 | height += contentInset.bottom
28 | height += safeAreaInsets.bottom
29 | height += (tableHeaderView?.frame.height ?? 0)
30 | height += (tableFooterView?.frame.height ?? 0)
31 |
32 | // Значение высоты равное 0 нет смысла обновлять, а равное infinity — ломает констрейнты, а вместе с ними и autoLayout
33 | if height != 0 && height != .infinity {
34 | collectionHeightConstraint.constant = height
35 | }
36 | }
37 |
38 | open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
39 | guard gestureRecognizer === panGestureRecognizer else {
40 | return true
41 | }
42 |
43 | if contentOffset.y == -contentInset.top, panGestureRecognizer.velocity(in: nil).y > 0 {
44 | return false
45 | }
46 |
47 | if contentOffset.y < -contentInset.top {
48 | return false
49 | }
50 |
51 | return true
52 | }
53 | }
54 |
55 |
56 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/BSTransitionDriver.swift:
--------------------------------------------------------------------------------
1 | // https://habr.com/ru/companies/koshelek/articles/703260/
2 | import UIKit
3 |
4 | // TODO: - тут наверное нужно рефакторить в будущем т.к. старт закрытия должен происходить в том месте, где происходит логика переходов между экранами
5 | final class BSTransitionDriver: UIPercentDrivenInteractiveTransition {
6 | private weak var presentedController: UIViewController?
7 |
8 | // Максимальное расстояние, на которое можно сместить презентованный контроллер — это высота контроллера,
9 | // поэтому используем её как максимально возможное смещение
10 | private var maxTranslation: CGFloat? {
11 | let height = presentedController?.view.frame.height ?? 0
12 | return height > 0 ? height : nil
13 | }
14 |
15 | // Жест, который будет отслеживать движение пальца.
16 | // Так мы сможем посчитать прямую зависимость между длиной сдвига и процентом анимации.
17 | private lazy var panRecognizer: UIPanGestureRecognizer = {
18 | let panRecognizer = UIPanGestureRecognizer(
19 | target: self,
20 | action: #selector(handleDismiss)
21 | )
22 | panRecognizer.delegate = self
23 | return panRecognizer
24 | }()
25 |
26 | // В случае, если этот флаг false, даёт возможность воспроизвести интерактивную анимацию как обычную.
27 | // По умолчанию всегда true.
28 | // Если не добавить условие интерактивного старта, то, при нажатии в область затемнения, анимация транзишена будет перехвачена driver’ом и остановится в стартовой позиции в ожидании дальнейших команд.
29 | // Добавляем условие, чтобы интерактивным транзишен становился только в случае, если случился жест свайпа.
30 | override var wantsInteractiveStart: Bool {
31 | get {
32 | panRecognizer.state == .began
33 | }
34 | set {
35 | super.wantsInteractiveStart = newValue
36 | }
37 | }
38 |
39 | init(controller: UIViewController) {
40 | super.init()
41 |
42 | controller.view.addGestureRecognizer(panRecognizer)
43 | presentedController = controller
44 | }
45 |
46 | @objc
47 | private func handleDismiss(_ sender: UIPanGestureRecognizer) {
48 | guard let maxTranslation = maxTranslation else { return }
49 | switch sender.state {
50 | case .began:
51 | let isRunning = percentComplete != 0
52 | // Чтобы избежать сбоев, проверяем, что анимация ещё не запущена другим способом.
53 | if !isRunning {
54 | presentedController?.dismiss(animated: true)
55 | }
56 |
57 | // На старте интерактивного транзишена анимация уже находится в состоянии паузы,
58 | // но если пользователь свайпнет и сразу передумает, то сможет поймать закрытие и поставить на паузу
59 | pause()
60 |
61 | case .changed:
62 | // На каждый шаг смещения будем обновлять процент анимации через update(_:) до момента,
63 | // пока пользователь не поднимет палец.
64 | let increment = sender.incrementToBottom(maxTranslation: maxTranslation)
65 | update(percentComplete + increment)
66 |
67 | case .ended, .cancelled:
68 | // Когда жест будет завершён или отменён, нужно будет рассчитать, как поступить с транзишеном.
69 | // Если смещение было больше половины или скорость смещения можно расценивать как быстрый свайп вниз, тогда мы вызываем finish() и транзишен закрытия завершается анимированно.
70 | // В противном случае отменяем транзишен и экран остаётся открытым.
71 | if sender.isProjectedToDownHalf(
72 | maxTranslation: maxTranslation,
73 | percentComplete: percentComplete
74 | ) {
75 | finish()
76 | } else {
77 | cancel()
78 | }
79 |
80 | case .failed:
81 | cancel()
82 |
83 | default:
84 | break
85 | }
86 | }
87 | }
88 |
89 | extension BSTransitionDriver: UIGestureRecognizerDelegate {
90 | // Жест будет обработан, если его скорость по оси Y больше 0, то есть направлен вниз, и скорость по оси Y больше чем X, чтобы не реагировать на боковые свайпы.
91 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
92 | let velocity = panRecognizer.velocity(in: nil)
93 | if velocity.y > 0, abs(velocity.y) > abs(velocity.x) {
94 | return true
95 | } else {
96 | return false
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/BSTransitioningDelegate.swift:
--------------------------------------------------------------------------------
1 | // https://habr.com/ru/companies/koshelek/articles/703260/
2 | import UIKit
3 |
4 | final class BSTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
5 |
6 | private var driver: BSTransitionDriver?
7 | var sheetSize: Navigation.BottomSheetSize = .auto
8 |
9 | // Cоздаем presentation controller.
10 | // Он является контейнером для презентуемого контроллера и отвечает за его положение и размеры.
11 | func presentationController(
12 | // контроллер, который хотим отобразить
13 | forPresented presented: UIViewController,
14 | // контроллер, поверх которого будет отображён презентуемый контроллер
15 | presenting: UIViewController?,
16 | // контроллер, который вызвал метод present(_:animate:completion:)
17 | source: UIViewController
18 | ) -> UIPresentationController? {
19 | driver = BSTransitionDriver(controller: presented)
20 | let presentationController = BSPresentationController(
21 | presentedViewController: presented,
22 | presenting: presenting ?? source
23 | )
24 | presentationController.sheetSize = sheetSize
25 | return presentationController
26 | }
27 |
28 | // Метод для создания анимации, с которой презентуемый контроллер будет появляться на экране.
29 | func animationController(
30 | forPresented presented: UIViewController,
31 | presenting: UIViewController,
32 | source: UIViewController
33 | ) -> UIViewControllerAnimatedTransitioning? {
34 | CoverVerticalPresentAnimatedTransitioning()
35 | }
36 |
37 | // Метод для создания анимации, с которой контроллер будет исчезать.
38 | func animationController(
39 | forDismissed dismissed: UIViewController
40 | ) -> UIViewControllerAnimatedTransitioning? {
41 | CoverVerticalDismissAnimatedTransitioning()
42 | }
43 |
44 | // Метод для анимации закрытия при свайпе.
45 | func interactionControllerForDismissal(
46 | using animator: UIViewControllerAnimatedTransitioning
47 | ) -> UIViewControllerInteractiveTransitioning? {
48 | driver
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/CoverVerticalDismissAnimatedTransitioning.swift:
--------------------------------------------------------------------------------
1 | // https://habr.com/ru/companies/koshelek/articles/703260/
2 | import UIKit
3 |
4 | final class CoverVerticalDismissAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
5 |
6 | private let duration: TimeInterval = 0.35
7 |
8 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
9 | duration
10 | }
11 |
12 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
13 | let animator = makeAnimator(using: transitionContext)
14 | animator?.startAnimation()
15 | }
16 |
17 | func interruptibleAnimator(
18 | using transitionContext: UIViewControllerContextTransitioning
19 | ) -> UIViewImplicitlyAnimating {
20 | makeAnimator(using: transitionContext) ?? UIViewPropertyAnimator()
21 | }
22 |
23 | // MARK: Private
24 | private func makeAnimator(
25 | using transitionContext: UIViewControllerContextTransitioning
26 | ) -> UIViewImplicitlyAnimating? {
27 | guard let fromView = transitionContext.view(forKey: .from)
28 | else {
29 | return nil
30 | }
31 |
32 | let animator = UIViewPropertyAnimator(
33 | duration: duration,
34 | controlPoint1: CGPoint(x: 0.2, y: 1),
35 | controlPoint2: CGPoint(x: 0.42, y: 1)
36 | ) {
37 | fromView.transform = CGAffineTransform.identity.translatedBy(x: 0, y: fromView.frame.height)
38 | }
39 |
40 | animator.addCompletion { _ in
41 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
42 | }
43 |
44 | return animator
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/CoverVerticalPresentAnimatedTransitioning.swift:
--------------------------------------------------------------------------------
1 | // https://habr.com/ru/companies/koshelek/articles/703260/
2 | import UIKit
3 |
4 | final class CoverVerticalPresentAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
5 | // Стандартное время транзишена в iOS — 0.35.
6 | private let duration: TimeInterval = 0.35
7 |
8 | // Перед стартом анимации UIKit запросит время анимации транзакции открытия экрана.
9 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
10 | duration
11 | }
12 |
13 | // Перед стартом транзишена UIKit вызовет этот метод с контекстом, в котором хранится необходимая информации об участниках.
14 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
15 | let animator = makeAnimator(using: transitionContext)
16 | animator?.startAnimation()
17 | }
18 |
19 | private func makeAnimator(
20 | using transitionContext: UIViewControllerContextTransitioning
21 | ) -> UIViewImplicitlyAnimating? {
22 | guard let toView = transitionContext.view(forKey: .to)
23 | else {
24 | return nil
25 | }
26 |
27 | // Анимация построена на смещении view по y из-за нижней границы экрана, поэтому для начала принудительно обновляем layout view контроллера.
28 | // Так как размеры и положение у нас заданы в BSPresentationController с помощью констрейнтов, то layoutIfNeeded спровоцирует UIKit на перерасчёт.
29 | let containerView = transitionContext.containerView
30 | containerView.layoutIfNeeded()
31 |
32 | // Смещаем view вниз за экран на его же высоту до старта анимации с помощью трансформации.
33 | toView.transform = CGAffineTransform.identity.translatedBy(x: 0, y: toView.frame.height)
34 |
35 | let animator = UIViewPropertyAnimator(
36 | duration: duration,
37 | controlPoint1: CGPoint(x: 0.2, y: 1),
38 | controlPoint2: CGPoint(x: 0.42, y: 1)
39 | ) {
40 | // В блоке аниматора вернём view к исходному положению.
41 | toView.transform = .identity
42 | }
43 |
44 | animator.addCompletion { _ in
45 | // После завершения анимации необходимо вызвать у контекста метод completeTransition(_ didComplete:) для индикации, что все анимации завершены со значением true, если анимация не была прервана.
46 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
47 | }
48 |
49 | return animator
50 | }
51 | }
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/BottomSheet/PullBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PullBar.swift
3 | //
4 | //
5 | // Created by Oleg Krasnov on 08/11/2023.
6 | //
7 |
8 | import UIKit
9 |
10 | // TODO: - Возможно нужно подумать о том, чтобы автоматически добавлять pull bar к bottom sheet.
11 | // Сейчас нужно добавлять на каждый экран отдельно
12 | open class PullBar: UIView {
13 |
14 | private let topCornerRadius: Int
15 | private let icon: UIImage?
16 | private let iconSize: CGSize
17 | private let pullBarBackgroundColor: UIColor
18 |
19 | private lazy var imageView = UIImageView(image: icon)
20 |
21 | public init(
22 | topCornerRadius: Int = 20,
23 | icon: UIImage? = .pullBarIcon,
24 | iconSize: CGSize = .init(width: 48, height: 4),
25 | pullBarBackgroundColor: UIColor = .white
26 | ) {
27 | self.topCornerRadius = topCornerRadius
28 | self.icon = icon
29 | self.iconSize = iconSize
30 | self.pullBarBackgroundColor = pullBarBackgroundColor
31 | super.init(frame: .zero)
32 | setupUI()
33 | }
34 |
35 | public required init?(coder: NSCoder) { nil }
36 |
37 | open override func layoutSubviews() {
38 | super.layoutSubviews()
39 | let path = UIBezierPath(
40 | roundedRect: bounds,
41 | byRoundingCorners:[.topRight, .topLeft],
42 | cornerRadii: CGSize(
43 | width: topCornerRadius,
44 | height: topCornerRadius
45 | )
46 | )
47 |
48 | let maskLayer = CAShapeLayer()
49 | maskLayer.path = path.cgPath
50 | layer.mask = maskLayer
51 | }
52 |
53 | open func setupUI() {
54 | backgroundColor = pullBarBackgroundColor
55 | imageView.translatesAutoresizingMaskIntoConstraints = false
56 | addSubview(imageView)
57 |
58 | NSLayoutConstraint.activate([
59 | imageView.widthAnchor.constraint(equalToConstant: iconSize.width),
60 | imageView.heightAnchor.constraint(equalToConstant: iconSize.height),
61 | imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
62 | imageView.centerXAnchor.constraint(equalTo: centerXAnchor)
63 | ])
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/HostingController.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UIKit
3 |
4 | public final class HostingController: UIHostingController,
5 | NavigationScreen,
6 | DismissCheckable,
7 | UIGestureRecognizerDelegate {
8 |
9 | // MARK: - Public properties
10 |
11 | public var tag: String
12 | public var isModal: Bool = false
13 | public weak var navigation: (any Router)?
14 | public var navigationCallback: (() -> Void)? = nil
15 | public var onBackCallback: () -> Void
16 | public var backButtonImage: UIImage? = UIImage(systemName: "chevron.backward")
17 | public var isNeedBackButton: Bool
18 | public var dataToSendFromModal: NullablePayload = nil
19 | public var output: (NullablePayload) -> Void
20 |
21 | // MARK: - Init
22 |
23 | public init(
24 | title: String,
25 | isNeedBackButton: Bool,
26 | tag: String,
27 | navigation: (any Router)?,
28 | content: ViewContent,
29 | output: @escaping (NullablePayload) -> Void = { _ in }
30 | ) {
31 | self.tag = tag
32 | self.navigation = navigation
33 | self.isNeedBackButton = isNeedBackButton
34 | self.onBackCallback = { [weak navigation] in
35 | navigation?.route(with: .pop(nil))
36 | }
37 | self.output = output
38 | super.init(rootView: content)
39 | self.title = title
40 | }
41 |
42 | @available(*, deprecated, message: "use init with params instead.")
43 | public required init?(coder _: NSCoder) {
44 | return nil
45 | }
46 |
47 | // MARK: - Lifecycle
48 |
49 | public override func viewWillAppear(_ animated: Bool) {
50 | super.viewWillAppear(animated)
51 | if isNeedBackButton {
52 | configureNavigationBackButton(#selector(onBack))
53 | } else {
54 | navigationItem.hidesBackButton = true
55 | }
56 | }
57 |
58 | public override func viewDidAppear(_ animated: Bool) {
59 | super.viewDidAppear(animated)
60 | navigationController?.interactivePopGestureRecognizer?.delegate = self
61 | }
62 |
63 | public override func viewDidDisappear(_ animated: Bool) {
64 | super.viewDidDisappear(animated)
65 | if isNeedBackButton {
66 | cleanBackNavigationButton()
67 | }
68 | }
69 |
70 | // MARK: - Public methods
71 |
72 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
73 | guard
74 | gestureRecognizer.isEqual(navigationController?.interactivePopGestureRecognizer)
75 | else {
76 | return true
77 | }
78 |
79 | onBack()
80 |
81 | return false
82 | }
83 |
84 | @objc
85 | public func onBack() {
86 | onBackCallback()
87 | }
88 |
89 | // TODO: - Подумать как использовать
90 | public func gotUpdatedData(_ payload: NullablePayload) {}
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/NavigationScreen.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// Main element of Navigation on Redux (Navidux). Uses for store/move screens in navigation stack.
4 | public protocol NavigationScreen: UIViewController, AnyObject, NavigationRestructable where Self: Equatable {
5 | /// - **tag**: The unique tag of the screen. Use for search in nav stack. Can be set on screen setup.
6 | var tag: String { get set }
7 | /// - **isModal**: property indicates that screen will be present as modal or not. Edited only from NavigationRouter.
8 | var isModal: Bool { get set }
9 | /// - **navigationCallback**: used for additional checking in navigation core and support consistency of the navigation state. Edited only from NavigationRouter.
10 | var navigationCallback: (() -> Void)? { get set }
11 | /// - **onBackCallback**: function that fired then user use back button or swipe. Can be set on screen setup.
12 | var onBackCallback: () -> Void { get set }
13 | /// - **dataToSendFromModal**: Data storage that will be used on dismiss screen and send to new top screen.
14 | var dataToSendFromModal: NullablePayload { get }
15 | /// - **gotUpdatedData**: function that fired on then upper screen remove from nav stack and current screen become topScreen. Can be overrided.
16 | @available(*, deprecated, message: "Please, use 'output' property instead")
17 | func gotUpdatedData(_ payload: NullablePayload)
18 | /// - **output**: property can be used to get call back when upper screen will be removed from nav stack and current screen will become topScreen
19 | var output: ((NullablePayload) -> Void) { get }
20 | }
21 |
22 | extension NavigationScreen {
23 | static func == (lhs: Self, rhs: Self) -> Bool {
24 | lhs.tag == rhs.tag
25 | }
26 | }
27 |
28 | public protocol DismissCheckable {
29 | var backButtonImage: UIImage? { get set }
30 | var isNeedBackButton: Bool { get set }
31 | func configureNavigationBackButton(_ selector: Selector)
32 | func cleanBackNavigationButton()
33 | func onBack()
34 | }
35 |
36 | public extension DismissCheckable where Self: UIViewController {
37 |
38 | func configureNavigationBackButton(_ selector: Selector) {
39 | navigationItem.hidesBackButton = true
40 |
41 | let backButton = UIBarButtonItem(
42 | image: backButtonImage,
43 | style: .plain,
44 | target: self,
45 | action: selector
46 | )
47 |
48 | navigationItem.leftBarButtonItem = backButton
49 | }
50 |
51 | func cleanBackNavigationButton() {
52 | guard navigationController?.viewControllers.first === self else {
53 | return
54 | }
55 | navigationItem.leftBarButtonItem = nil
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/ScreenConfig.swift:
--------------------------------------------------------------------------------
1 | public struct ScreenConfig: Equatable {
2 | public let navigationTitle: String
3 | public var isNeedSetBackButton: Bool
4 | public var initialPayload: NullablePayload
5 | public var output: ((NullablePayload) -> Void)
6 |
7 | /// - Parameters:
8 | /// - navigationTitle: Set navigation title for screen on init.
9 | /// - isNeedSetBackButton: Set or remove back button in navigation bar. Action on callback can be overrided in `NavigationScreen` with function `onBackCallback`.
10 | /// - initialPayload: Uses as additional parameter with some data to initialise `NavigationScreen` or some Module/Fabric.
11 | public init(
12 | navigationTitle: String = "",
13 | isNeedSetBackButton: Bool = true,
14 | initialPayload: NullablePayload = nil,
15 | output: ((NullablePayload) -> Void)? = nil
16 | ) {
17 | self.navigationTitle = navigationTitle
18 | self.isNeedSetBackButton = isNeedSetBackButton
19 | self.initialPayload = initialPayload
20 | self.output = output ?? { _ in }
21 | }
22 |
23 | public static func == (lhs: ScreenConfig, rhs: ScreenConfig) -> Bool {
24 | lhs.navigationTitle == rhs.navigationTitle
25 | && lhs.isNeedSetBackButton == rhs.isNeedSetBackButton
26 | && lhs.initialPayload?.data.hashValue == rhs.initialPayload?.data.hashValue
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Navidux/screens/ViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | open class ViewController: UIViewController,
4 | NavigationScreen,
5 | DismissCheckable,
6 | UIGestureRecognizerDelegate {
7 |
8 | // MARK: - Public properties
9 |
10 | public var tag: String
11 | public var isModal: Bool = false
12 | public weak var navigation: (any Router)?
13 | public var navigationCallback: (() -> Void)? = nil
14 | public var onBackCallback: () -> Void
15 | public var backButtonImage: UIImage? = UIImage(systemName: "chevron.backward")
16 | public var isNeedBackButton: Bool
17 | open var dataToSendFromModal: NullablePayload = nil
18 | public var output: ((NullablePayload) -> Void)
19 |
20 | // MARK: - Init
21 |
22 | public init(
23 | title: String = "",
24 | isNeedBackButton: Bool = true,
25 | navigation: (any Router)? = nil,
26 | tag: String = UUID().uuidString,
27 | output: @escaping (NullablePayload) -> Void = { _ in }
28 | ) {
29 | self.navigation = navigation
30 | self.tag = tag
31 | self.isNeedBackButton = isNeedBackButton
32 | self.onBackCallback = { [weak navigation] in
33 | navigation?.route(with: .pop(nil))
34 | }
35 | self.output = output
36 | super.init(nibName: nil, bundle: nil)
37 | self.title = title
38 | }
39 |
40 | @available(*, deprecated, message: "use init() instead.")
41 | public required init?(coder: NSCoder) {
42 | fatalError("init(coder:) has not been implemented")
43 | }
44 |
45 | // MARK: - Lifecycle
46 |
47 | open override func viewWillAppear(_ animated: Bool) {
48 | super.viewWillAppear(animated)
49 | if isNeedBackButton {
50 | configureNavigationBackButton(#selector(onBack))
51 | } else {
52 | navigationItem.hidesBackButton = true
53 | }
54 | }
55 |
56 | open override func viewDidAppear(_ animated: Bool) {
57 | super.viewDidAppear(animated)
58 | navigationController?.interactivePopGestureRecognizer?.delegate = self
59 | }
60 |
61 | open override func viewDidDisappear(_ animated: Bool) {
62 | super.viewDidDisappear(animated)
63 | if isNeedBackButton {
64 | cleanBackNavigationButton()
65 | }
66 | }
67 |
68 |
69 | // MARK: - Public methods
70 |
71 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
72 | guard
73 | gestureRecognizer.isEqual(navigationController?.interactivePopGestureRecognizer)
74 | else {
75 | return true
76 | }
77 |
78 | onBack()
79 |
80 | return false
81 | }
82 |
83 | @objc
84 | public func onBack() {
85 | onBackCallback()
86 | }
87 |
88 | open func gotUpdatedData(_ payload: NullablePayload) {
89 | //HINT: Do some action on getted response from previous screen
90 | debugPrint("Screen recieved data: \(String(describing: payload))")
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Tests/NaviduxTests/Fixtures/NaviduxFixture.swift:
--------------------------------------------------------------------------------
1 | @testable import Navidux
2 |
3 | struct NaviduxFixture {
4 | static let oneScreenTag = "MockNavigationScreen"
5 | static let mockScreenTag = "Dummy"
6 |
7 | static func mockNavigationScreen(
8 | coordinator: Router? = nil,
9 | tag: String = NaviduxFixture.mockScreenTag,
10 | output: ((NullablePayload) -> Void)? = nil
11 | ) -> any NavigationScreen {
12 | return ViewController(
13 | navigation: coordinator,
14 | tag: tag,
15 | output: output ?? { _ in }
16 | )
17 | }
18 |
19 | static func mockScreenConfig() -> ScreenConfig {
20 | ScreenConfig(
21 | navigationTitle: "Default Mock",
22 | output: { payload in
23 | print("OUTPUT_PAYLOAD: \(String(describing: payload))")
24 | }
25 | )
26 | }
27 | }
28 |
29 | struct ScreenFactoryFixture: ScreenFactory {
30 | var mockNavigationScreen: any NavigationScreen
31 |
32 | var findPersonScreenFactory: (Router, ScreenConfig) -> any NavigationScreen {
33 | { _, _ in
34 | mockNavigationScreen
35 | }
36 | }
37 |
38 | var employeePersonalInfoScreenFactory: (Router, ScreenConfig) -> any NavigationScreen {
39 | { _, _ in
40 | mockNavigationScreen
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tests/NaviduxTests/Fixtures/NaviduxScreenFixture.swift:
--------------------------------------------------------------------------------
1 | @testable import Navidux
2 |
3 | extension NaviduxScreen {
4 | public static let firstScreen = NaviduxScreen(
5 | screenClass: FirstController.self
6 | )
7 | public static let secondScreen = NaviduxScreen(
8 | screenClass: SecondController.self
9 | )
10 | public static let thirdScreen = NaviduxScreen(
11 | screenClass: ThirdController.self
12 | )
13 | }
14 |
15 | final class FirstController: ViewController {}
16 |
17 | final class SecondController: ViewController {}
18 |
19 | final class ThirdController: ViewController {}
20 |
--------------------------------------------------------------------------------
/Tests/NaviduxTests/Fixtures/NavigationControllerStub.swift:
--------------------------------------------------------------------------------
1 | @testable import Navidux
2 | import UIKit
3 |
4 | enum NavigationControllerCallingMethods: Equatable {
5 | case addToStack(tag: String)
6 | case removeLastFromStack
7 | case removeTillFromStack(tag: String)
8 | case rebuildNavStack(tags: [String])
9 | case pushViewController(tag: String?)
10 | case popViewController
11 | case popToViewController(tag: String?)
12 | case present(tag: String?)
13 | case dismiss
14 | case setViewControllers
15 | }
16 |
17 | final class NavigationControllerStub: NavigationController {
18 | var callingStack: [NavigationControllerCallingMethods] = []
19 |
20 | var screens: [any NavigationScreen] = []
21 |
22 | var topScreen: (any NavigationScreen)? {
23 | screens.last
24 | }
25 |
26 | var topViewController: UIViewController? {
27 | screens.last
28 | }
29 |
30 | var viewControllers: [UIViewController] = []
31 |
32 | func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
33 | self.viewControllers = viewControllers
34 | callingStack.append(.setViewControllers)
35 | }
36 |
37 | func addToStack(screen: any NavigationScreen) {
38 | screens.append(screen)
39 | callingStack.append(.addToStack(tag: screen.tag))
40 | }
41 |
42 | func removeLastFromStack() {
43 | screens.removeLast()
44 | callingStack.append(.removeLastFromStack)
45 | }
46 |
47 | func removeTillFromStack(screen: any NavigationScreen) {
48 | callingStack.append(.removeTillFromStack(tag: screen.tag))
49 | }
50 |
51 | func rebuildNavStack(with screens: [any NavigationScreen]) {
52 | self.screens = screens
53 | callingStack.append(.rebuildNavStack(tags: screens.map { $0.tag }))
54 | }
55 |
56 | func pushViewController(_ viewController: UIViewController, animated: Bool) {
57 | let vcTag = (viewController as? (any NavigationScreen))?.tag
58 | callingStack.append(.pushViewController(tag: vcTag))
59 | }
60 |
61 | @discardableResult
62 | func popViewController(animated: Bool) -> UIViewController? {
63 | callingStack.append(.popViewController)
64 | return nil
65 | }
66 |
67 | @discardableResult
68 | func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
69 | let vcTag = (viewController as? (any NavigationScreen))?.tag
70 | callingStack.append(.popToViewController(tag: vcTag))
71 | return nil
72 | }
73 |
74 | func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
75 | let vcTag = (viewControllerToPresent as? (any NavigationScreen))?.tag
76 | callingStack.append(.present(tag: vcTag))
77 | }
78 |
79 | func dismiss(animated flag: Bool, completion: (() -> Void)?) {
80 | callingStack.append(.dismiss)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/NaviduxTests/Fixtures/PayloadStub.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PayloadStub.swift
3 | //
4 | //
5 | // Created by Stanislav Anatskii on 09.10.2023.
6 | //
7 |
8 | @testable import Navidux
9 |
10 | struct PayloadStub {
11 | let value: Int
12 |
13 | init(value: Int) {
14 | self.value = value
15 | }
16 | }
17 |
18 | extension PayloadStub: Payload {
19 | var data: some Hashable {
20 | self
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/NaviduxTests/Fixtures/ScreenAssemblerStub.swift:
--------------------------------------------------------------------------------
1 | @testable import Navidux
2 |
3 | enum ScreenAssemblerStubAction: Equatable {
4 | case assembleScreen(Navidux.NaviduxScreen)
5 | case assembleAlert(Navidux.AlertConfiguration)
6 | }
7 |
8 | final class ScreenAssemblerStub: ScreenAssembler {
9 | var actions = [ScreenAssemblerStubAction]()
10 | var navigation: Router?
11 | var vcTag: String?
12 | var screenToPush: (any Navidux.NavigationScreen)?
13 |
14 | init(navigation: Router? = nil, vcTag: String? = nil, screenToPush: (any Navidux.NavigationScreen)? = nil) {
15 | self.navigation = navigation
16 | self.vcTag = vcTag
17 | self.screenToPush = screenToPush
18 | }
19 |
20 | func assemblyScreen(components: Navidux.ScreenAsseblyComponents) -> any Navidux.NavigationScreen {
21 | screenToPush ?? NaviduxFixture.mockNavigationScreen(
22 | coordinator: navigation,
23 | tag: vcTag ?? "",
24 | output: components.config.output
25 | )
26 | }
27 |
28 | func assemblyAlert(configuration: Navidux.AlertConfiguration) -> Navidux.AlertScreen {
29 | AlertScreen(configuration: configuration)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/NaviduxTests/NavigationControllerTests.swift:
--------------------------------------------------------------------------------
1 | @testable import Navidux
2 | import XCTest
3 |
4 | final class NavigationControllerTests: XCTestCase {
5 | let navigation = NavigationControllerImpl()
6 |
7 | func test_Init_withZeroScreens() {
8 | XCTAssertTrue(navigation.screens.isEmpty)
9 | }
10 |
11 | func test_topScreen_returnLastScreen() {
12 | let screen1 = NaviduxFixture.mockNavigationScreen(tag: "1")
13 | let screen2 = NaviduxFixture.mockNavigationScreen(tag: "2")
14 | navigation.screens = [screen1, screen2]
15 |
16 | XCTAssertNotNil(navigation.topScreen)
17 | XCTAssertEqual(navigation.topScreen?.tag, screen2.tag)
18 | }
19 |
20 | func test_addToStackScreen_updateScreenProperty() {
21 | let screen = NaviduxFixture.mockNavigationScreen()
22 |
23 | navigation.addToStack(screen: screen)
24 |
25 | XCTAssertEqual(navigation.screens.count, 1)
26 | XCTAssertEqual(navigation.screens.first?.tag, screen.tag)
27 | }
28 |
29 | func test_removeLastFromStack_updateScreenProperty() {
30 | let screen = NaviduxFixture.mockNavigationScreen()
31 | navigation.screens = [screen]
32 |
33 | navigation.removeLastFromStack()
34 |
35 | XCTAssertTrue(navigation.screens.count == 1)
36 | }
37 |
38 | func test_removeTillFromStack_removeLastToScreens() {
39 | let screen1 = NaviduxFixture.mockNavigationScreen(tag: "1")
40 | let screen2 = NaviduxFixture.mockNavigationScreen(tag: "2")
41 | let screen3 = NaviduxFixture.mockNavigationScreen(tag: "3")
42 | navigation.screens = [screen1, screen2, screen3]
43 |
44 | navigation.removeTillFromStack(screen: screen1)
45 |
46 | XCTAssertEqual(navigation.screens.count, 1)
47 | XCTAssertEqual(navigation.screens.first?.tag, screen1.tag)
48 | }
49 |
50 | func test_rebuildNavStack_changes_screens() {
51 | let screen1 = NaviduxFixture.mockNavigationScreen(tag: "1")
52 | let screen2 = NaviduxFixture.mockNavigationScreen(tag: "2")
53 | let screen3 = NaviduxFixture.mockNavigationScreen(tag: "3")
54 | navigation.screens = [screen1, screen2]
55 |
56 | navigation.rebuildNavStack(with: [screen3])
57 |
58 | XCTAssertEqual(navigation.screens.count, 1)
59 | XCTAssertEqual(navigation.screens.first?.tag, screen3.tag)
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/Tests/NaviduxTests/NavigationCoordinatorTests.swift:
--------------------------------------------------------------------------------
1 | @testable import Navidux
2 | import XCTest
3 |
4 | final class NavigationCoordinatorTests: XCTestCase {
5 | let navigationController = NavigationControllerStub()
6 | var expectedPayload: PayloadStub?
7 | lazy var navigationScreen = NaviduxFixture.mockNavigationScreen(
8 | tag: NaviduxFixture.oneScreenTag,
9 | output: { [weak self] payload in
10 | self?.expectedPayload = payload as? PayloadStub
11 | }
12 | )
13 | lazy var screenAssembler = ScreenAssemblerStub(
14 | vcTag: NaviduxFixture.oneScreenTag,
15 | screenToPush: navigationScreen
16 | )
17 |
18 | lazy var navigationCoordinator = NavigationCoordinator(
19 | navigationController,
20 | screenAssembler: screenAssembler
21 | )
22 |
23 | func test_pushFullscreen_addCallForPush() {
24 | screenAssembler.navigation = navigationCoordinator
25 |
26 | navigationCoordinator.route(
27 | with: .push(
28 | .firstScreen,
29 | NaviduxFixture.mockScreenConfig(),
30 | .fullscreen
31 | )
32 | )
33 |
34 | XCTAssertEqual(
35 | navigationController.callingStack,
36 | [
37 | .pushViewController(tag: NaviduxFixture.oneScreenTag),
38 | .addToStack(tag: NaviduxFixture.oneScreenTag)
39 | ]
40 | )
41 | XCTAssertFalse(navigationScreen.isModal)
42 | }
43 |
44 | func test_pushModal_addCallForPresentAndSetState() {
45 | screenAssembler.navigation = navigationCoordinator
46 |
47 | navigationCoordinator.route(
48 | with: .push(
49 | .firstScreen,
50 | NaviduxFixture.mockScreenConfig(),
51 | .modal
52 | )
53 | )
54 |
55 | XCTAssertEqual(
56 | navigationController.callingStack,
57 | [
58 | .present(tag: NaviduxFixture.oneScreenTag),
59 | .addToStack(tag: NaviduxFixture.oneScreenTag)
60 | ]
61 | )
62 |
63 | XCTAssertTrue(navigationCoordinator.state.hasOverlay)
64 | XCTAssertTrue(navigationScreen.isModal)
65 | }
66 |
67 | func test_pushBottomSheet_addCallForPresentAndSetState() {
68 | screenAssembler.navigation = navigationCoordinator
69 |
70 | navigationCoordinator.route(
71 | with: .push(
72 | .firstScreen,
73 | NaviduxFixture.mockScreenConfig(),
74 | .bottomSheet(.halfScreen)
75 | )
76 | )
77 |
78 | XCTAssertEqual(
79 | navigationController.callingStack,
80 | [
81 | .present(tag: NaviduxFixture.oneScreenTag),
82 | .addToStack(tag: NaviduxFixture.oneScreenTag)
83 | ]
84 | )
85 |
86 | XCTAssertTrue(navigationCoordinator.state.hasOverlay)
87 | XCTAssertTrue(navigationScreen.isModal)
88 | }
89 |
90 | func test_popFullscreen_addCallForPop() {
91 | screenAssembler.navigation = navigationCoordinator
92 |
93 | navigationCoordinator.route(
94 | with: .push(
95 | .firstScreen,
96 | NaviduxFixture.mockScreenConfig(),
97 | .fullscreen
98 | )
99 | )
100 |
101 | navigationCoordinator.route(with: .pop(nil))
102 |
103 | XCTAssertEqual(
104 | navigationController.callingStack,
105 | [
106 | .pushViewController(tag: NaviduxFixture.oneScreenTag),
107 | .addToStack(tag: NaviduxFixture.oneScreenTag),
108 | .popViewController,
109 | .removeLastFromStack
110 | ]
111 | )
112 | }
113 |
114 | func test_popModal_addCallForDismiss() {
115 | screenAssembler.navigation = navigationCoordinator
116 |
117 | navigationCoordinator.route(
118 | with: .push(
119 | .firstScreen,
120 | NaviduxFixture.mockScreenConfig(),
121 | .modal
122 | )
123 | )
124 |
125 | navigationCoordinator.route(with: .pop(nil))
126 |
127 | XCTAssertEqual(
128 | navigationController.callingStack,
129 | [
130 | .present(tag: NaviduxFixture.oneScreenTag),
131 | .addToStack(tag: NaviduxFixture.oneScreenTag),
132 | .dismiss,
133 | .removeLastFromStack
134 | ]
135 | )
136 | }
137 |
138 | func test_pushFullscreen_setCallback() {
139 | screenAssembler.navigation = navigationCoordinator
140 |
141 | navigationCoordinator.route(
142 | with: .push(
143 | .firstScreen,
144 | NaviduxFixture.mockScreenConfig(),
145 | .fullscreen
146 | )
147 | )
148 |
149 | XCTAssertNotNil(navigationScreen.navigationCallback)
150 | }
151 |
152 | //Спорный тест из-за restruct
153 | func test_popToFullscreen_addCallForPop() {
154 | screenAssembler.navigation = navigationCoordinator
155 |
156 | navigationCoordinator.route(
157 | with: .restruct(
158 | screens: [
159 | ScreenAsseblyComponents(screenType: .firstScreen, config: NaviduxFixture.mockScreenConfig()),
160 | ScreenAsseblyComponents(screenType: .secondScreen, config: NaviduxFixture.mockScreenConfig()),
161 | ScreenAsseblyComponents(screenType: .thirdScreen, config: NaviduxFixture.mockScreenConfig()),
162 | ],
163 | animationType: .backward
164 | )
165 | )
166 |
167 | navigationCoordinator.route(
168 | with: .popUntil(.firstScreen, nil)
169 | )
170 |
171 | XCTAssertEqual(
172 | navigationController.callingStack,
173 | [
174 | .popViewController,
175 | .rebuildNavStack(tags: [
176 | NaviduxFixture.oneScreenTag,
177 | NaviduxFixture.oneScreenTag,
178 | NaviduxFixture.oneScreenTag
179 | ]),
180 | ]
181 | )
182 | }
183 |
184 | //Спорный тест из-за restruct
185 | func test_popToModal_addCallForPop() {
186 | screenAssembler.navigation = navigationCoordinator
187 |
188 | navigationCoordinator.route(
189 | with: .restruct(
190 | screens: [
191 | ScreenAsseblyComponents(screenType: .firstScreen, config: NaviduxFixture.mockScreenConfig()),
192 | ScreenAsseblyComponents(screenType: .secondScreen, config: NaviduxFixture.mockScreenConfig()),
193 | ScreenAsseblyComponents(screenType: .thirdScreen, config: NaviduxFixture.mockScreenConfig()),
194 | ],
195 | animationType: .backward
196 | )
197 | )
198 | navigationCoordinator.route(
199 | with: .push(
200 | .firstScreen,
201 | NaviduxFixture.mockScreenConfig(),
202 | .modal
203 | )
204 | )
205 |
206 | navigationCoordinator.route(
207 | with: .popUntil(.firstScreen, nil)
208 | )
209 |
210 | XCTAssertEqual(
211 | navigationController.callingStack,
212 | [
213 | .popViewController,
214 | .rebuildNavStack(tags: [
215 | NaviduxFixture.oneScreenTag,
216 | NaviduxFixture.oneScreenTag,
217 | NaviduxFixture.oneScreenTag
218 | ]),
219 | .present(tag: NaviduxFixture.oneScreenTag),
220 | .addToStack(tag: NaviduxFixture.oneScreenTag)
221 | ]
222 | )
223 | }
224 |
225 | func test_popWithPayload() {
226 | screenAssembler.navigation = navigationCoordinator
227 |
228 | navigationCoordinator.route(with: .push(
229 | .firstScreen,
230 | NaviduxFixture.mockScreenConfig(),
231 | .fullscreen)
232 | )
233 |
234 | navigationCoordinator.route(with: .push(
235 | .secondScreen,
236 | NaviduxFixture.mockScreenConfig(),
237 | .fullscreen)
238 | )
239 |
240 | let outputPayload = PayloadStub(value: 123)
241 | navigationCoordinator.route(with: .pop(outputPayload))
242 |
243 | XCTAssertEqual(
244 | navigationController.callingStack,
245 | [
246 | .pushViewController(tag: NaviduxFixture.oneScreenTag),
247 | .addToStack(tag: NaviduxFixture.oneScreenTag),
248 |
249 | .pushViewController(tag: NaviduxFixture.oneScreenTag),
250 | .addToStack(tag: NaviduxFixture.oneScreenTag),
251 |
252 | .popViewController,
253 | .removeLastFromStack
254 | ]
255 | )
256 |
257 | XCTAssertEqual(navigationController.screens.count, 1)
258 |
259 | XCTAssertEqual(outputPayload, expectedPayload)
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/readme/Navidux_scheme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedMadRobot/navidux/e69482cee684b2cdb233092546561cf6a3acdb1e/readme/Navidux_scheme.png
--------------------------------------------------------------------------------
/readme/Roadmap.md:
--------------------------------------------------------------------------------
1 | # Roadmap
2 |
3 | - ~~Add functionality to push/present bottomsheet controllers.~~
4 | - Add functionality to work with tabbar.
5 | - Improve inner logic to use with "Rebuild Stack" function.
6 | - Update mechanic to deliver data between screens.
7 | - Work with UIWindow and UISplitViewController.
8 | - When initialize screens need create transitioDelegate and set in in new instance by default (see BSTransitionalDriver TODO)
9 |
--------------------------------------------------------------------------------