From 6c59d66afa47ce89dc710b2bf66cf3784bd66e00 Mon Sep 17 00:00:00 2001
From: Mustafa Merza <mustafa.merza95@gmail.com>
Date: Sun, 21 Jul 2024 10:34:18 +0300
Subject: [PATCH] - Added custom main view, main view model and main
 coordinator to be reusable.

- Added custom modifier to handle layout changes.
---
 MiniScanner.xcodeproj/project.pbxproj         | 32 +++++++++++++++
 .../Extensions/View/View+LayoutChanges.swift  | 38 ++++++++++++++++++
 .../Common/Data/Model/SupportedLanguage.swift |  4 ++
 .../Common/Presentation/MainCoordinator.swift | 39 +++++++++++++++++++
 .../Common/Presentation/MainView.swift        | 33 ++++++++++++++++
 .../Common/Presentation/MainViewModel.swift   | 21 ++++++++++
 6 files changed, 167 insertions(+)
 create mode 100644 MiniScanner/Extensions/View/View+LayoutChanges.swift
 create mode 100644 MiniScanner/Features/Common/Presentation/MainCoordinator.swift
 create mode 100644 MiniScanner/Features/Common/Presentation/MainView.swift
 create mode 100644 MiniScanner/Features/Common/Presentation/MainViewModel.swift

diff --git a/MiniScanner.xcodeproj/project.pbxproj b/MiniScanner.xcodeproj/project.pbxproj
index 2066f5c..f2abd73 100644
--- a/MiniScanner.xcodeproj/project.pbxproj
+++ b/MiniScanner.xcodeproj/project.pbxproj
@@ -194,6 +194,10 @@
 		672C46DA2C48018400497EF0 /* Color+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672C46D92C48018400497EF0 /* Color+Colors.swift */; };
 		672C46DC2C48018D00497EF0 /* UIColors+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 672C46DB2C48018D00497EF0 /* UIColors+Colors.swift */; };
 		67807F8C2C48F49B00D1F168 /* HandleAppLanguageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67807F8B2C48F49B00D1F168 /* HandleAppLanguageUseCase.swift */; };
+		67807F952C49306C00D1F168 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67807F942C49306C00D1F168 /* MainCoordinator.swift */; };
+		67807F992C49320100D1F168 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67807F982C49320100D1F168 /* MainViewModel.swift */; };
+		67807F9B2C49325A00D1F168 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67807F9A2C49325A00D1F168 /* MainView.swift */; };
+		67807FA02C4934BD00D1F168 /* View+LayoutChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67807F9F2C4934BD00D1F168 /* View+LayoutChanges.swift */; };
 		B827E5196CC419E773B843E1 /* Pods_MiniScanner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9A37DC9F9A8E3AF632DFB98 /* Pods_MiniScanner.framework */; };
 		EC0CF1FE254D8BBF00888722 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0CF1FD254D8BBF00888722 /* AppDelegate.swift */; };
 		EC0CF200254D8BBF00888722 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0CF1FF254D8BBF00888722 /* SceneDelegate.swift */; };
@@ -438,6 +442,10 @@
 		672C46D92C48018400497EF0 /* Color+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Colors.swift"; sourceTree = "<group>"; };
 		672C46DB2C48018D00497EF0 /* UIColors+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColors+Colors.swift"; sourceTree = "<group>"; };
 		67807F8B2C48F49B00D1F168 /* HandleAppLanguageUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleAppLanguageUseCase.swift; sourceTree = "<group>"; };
+		67807F942C49306C00D1F168 /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = "<group>"; };
+		67807F982C49320100D1F168 /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = "<group>"; };
+		67807F9A2C49325A00D1F168 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
+		67807F9F2C4934BD00D1F168 /* View+LayoutChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+LayoutChanges.swift"; sourceTree = "<group>"; };
 		E8AF4FB39674DF589D719DCF /* Pods-MiniScanner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MiniScanner.release.xcconfig"; path = "Target Support Files/Pods-MiniScanner/Pods-MiniScanner.release.xcconfig"; sourceTree = "<group>"; };
 		E9A37DC9F9A8E3AF632DFB98 /* Pods_MiniScanner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MiniScanner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		EC0CF1FA254D8BBF00888722 /* MiniScanner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MiniScanner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1000,6 +1008,7 @@
 				672C46A62C47E8D700497EF0 /* DI */,
 				672C46C42C47E98A00497EF0 /* Data */,
 				672C46BD2C47E98900497EF0 /* Domain */,
+				67807F972C49313C00D1F168 /* Presentation */,
 			);
 			path = Common;
 			sourceTree = "<group>";
@@ -1103,6 +1112,24 @@
 			path = Style;
 			sourceTree = "<group>";
 		};
+		67807F972C49313C00D1F168 /* Presentation */ = {
+			isa = PBXGroup;
+			children = (
+				67807F9A2C49325A00D1F168 /* MainView.swift */,
+				67807F982C49320100D1F168 /* MainViewModel.swift */,
+				67807F942C49306C00D1F168 /* MainCoordinator.swift */,
+			);
+			path = Presentation;
+			sourceTree = "<group>";
+		};
+		67807F9C2C49348B00D1F168 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				67807F9F2C4934BD00D1F168 /* View+LayoutChanges.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 		98E49D3F46C8E62718825860 /* Pods */ = {
 			isa = PBXGroup;
 			children = (
@@ -1193,6 +1220,7 @@
 		EC8A9B0A254DC2FD00F9AF99 /* Extensions */ = {
 			isa = PBXGroup;
 			children = (
+				67807F9C2C49348B00D1F168 /* View */,
 				672C46D82C48017C00497EF0 /* Style */,
 				672C46492C47BD8800497EF0 /* String+StringKeys.swift */,
 				EC0CF21C254D8F3900888722 /* String+Extensions.swift */,
@@ -1496,6 +1524,7 @@
 				539996952C27130000671340 /* ConstraintConstantTarget.swift in Sources */,
 				672C46622C47C74300497EF0 /* DependencyInjector.swift in Sources */,
 				539996902C27130000671340 /* UILayoutSupport+Extensions.swift in Sources */,
+				67807F952C49306C00D1F168 /* MainCoordinator.swift in Sources */,
 				53014F992C11A8E80071CE39 /* CGPoint+Utils.swift in Sources */,
 				53E7D3392C1B00880025A1D3 /* FSPageViewLayout.swift in Sources */,
 				53BEB1422C2967E0005A3567 /* FoldersViewController.swift in Sources */,
@@ -1521,6 +1550,7 @@
 				EC702546254E1E9E00BE1958 /* WalkthroughModel.swift in Sources */,
 				53D9D1C22C1AF521004D1C1C /* StickerView.swift in Sources */,
 				672C46D52C47F8D000497EF0 /* HandleAppStartUseCase.swift in Sources */,
+				67807FA02C4934BD00D1F168 /* View+LayoutChanges.swift in Sources */,
 				539996A22C27130000671340 /* ConstraintDirectionalInsets.swift in Sources */,
 				53E7D3352C1B00880025A1D3 /* FSPagerView.swift in Sources */,
 				672C46B32C47E8D700497EF0 /* DependencyManager.swift in Sources */,
@@ -1571,6 +1601,7 @@
 				53014F9E2C11A8E80071CE39 /* UIImage+Orientation.swift in Sources */,
 				53014FB82C11A8E80071CE39 /* Transformable.swift in Sources */,
 				53014FA22C11A8E80071CE39 /* ScannedPageViewController.swift in Sources */,
+				67807F9B2C49325A00D1F168 /* MainView.swift in Sources */,
 				5399969D2C27130000671340 /* Constraint.swift in Sources */,
 				EC8A9B1D254DCEC600F9AF99 /* File.swift in Sources */,
 				67807F8C2C48F49B00D1F168 /* HandleAppLanguageUseCase.swift in Sources */,
@@ -1580,6 +1611,7 @@
 				53014FBB2C11A8E80071CE39 /* CaptureSession+Orientation.swift in Sources */,
 				53014F942C11A8E80071CE39 /* DeviceOrientationHelper.swift in Sources */,
 				EC8A9B27254DE91B00F9AF99 /* DocumentPreviewViewController.swift in Sources */,
+				67807F992C49320100D1F168 /* MainViewModel.swift in Sources */,
 				5399964A2C26E86700671340 /* CropperConstantValues.m in Sources */,
 				53014F9F2C11A8E80071CE39 /* UIImage+Utils.swift in Sources */,
 				53CD5F582C1504CF0010424B /* UIButton+Extensions.swift in Sources */,
diff --git a/MiniScanner/Extensions/View/View+LayoutChanges.swift b/MiniScanner/Extensions/View/View+LayoutChanges.swift
new file mode 100644
index 0000000..8c57593
--- /dev/null
+++ b/MiniScanner/Extensions/View/View+LayoutChanges.swift
@@ -0,0 +1,38 @@
+//
+//  View+LayoutChanges.swift
+//  MiniScanner
+//
+//  Created by Mustafa Merza on 7/18/24.
+//  Copyright © 2024 AppsNectar. All rights reserved.
+//
+
+import SwiftUI
+
+struct LayoutChangesModifier: ViewModifier {
+    
+    @State var layoutDirection: LayoutDirection = .leftToRight
+    
+    @Inject var getLanguageUseCase: GetLanguageUseCase
+    
+    func body(content: Content) -> some View {
+        
+        content
+            .onAppear {
+                updateLayoutDirection()
+            }
+            .environment(\.layoutDirection, layoutDirection)
+    }
+    
+    private func updateLayoutDirection() {
+        let language = getLanguageUseCase.execute()
+        layoutDirection = language.layoutDirection
+    }
+}
+
+extension View {
+    
+    func handleLayoutChanges() -> some View {
+        ModifiedContent(content: self,
+                        modifier: LayoutChangesModifier())
+    }
+}
diff --git a/MiniScanner/Features/Common/Data/Model/SupportedLanguage.swift b/MiniScanner/Features/Common/Data/Model/SupportedLanguage.swift
index 12a94a7..3883c3b 100644
--- a/MiniScanner/Features/Common/Data/Model/SupportedLanguage.swift
+++ b/MiniScanner/Features/Common/Data/Model/SupportedLanguage.swift
@@ -33,6 +33,10 @@ enum SupportedLanguage: CaseIterable {
     var isRTL: Bool {
         LanguageManager.shared.isRightToLeft
     }
+    
+    var layoutDirection: LayoutDirection {
+        isRTL ? .rightToLeft : .leftToRight
+    }
 }
 
 extension SupportedLanguage {
diff --git a/MiniScanner/Features/Common/Presentation/MainCoordinator.swift b/MiniScanner/Features/Common/Presentation/MainCoordinator.swift
new file mode 100644
index 0000000..c086666
--- /dev/null
+++ b/MiniScanner/Features/Common/Presentation/MainCoordinator.swift
@@ -0,0 +1,39 @@
+//
+//  MainCoordinator.swift
+//  MiniScanner
+//
+//  Created by Mustafa Merza on 7/18/24.
+//  Copyright © 2024 AppsNectar. All rights reserved.
+//
+
+import SwiftUI
+
+protocol MainCoordinator {
+    
+    var navigationController: UINavigationController { get set }
+    
+    func start()
+    
+    func coordinate(to coordinator: MainCoordinator)
+    
+    func coordinateBack()
+
+    func coordinateToView(_ view: some View, title: String?)
+}
+
+extension MainCoordinator {
+    
+    func coordinate(to coordinator: MainCoordinator) {
+        coordinator.start()
+    }
+    
+    func coordinateBack() {
+        navigationController.popViewController(animated: true)
+    }
+
+    func coordinateToView(_ view: some View, title: String? = nil) {
+        let hostingController = UIHostingController(rootView: view)
+        hostingController.title = title
+        navigationController.pushViewController(hostingController, animated: true)
+    }
+}
diff --git a/MiniScanner/Features/Common/Presentation/MainView.swift b/MiniScanner/Features/Common/Presentation/MainView.swift
new file mode 100644
index 0000000..18675c4
--- /dev/null
+++ b/MiniScanner/Features/Common/Presentation/MainView.swift
@@ -0,0 +1,33 @@
+//
+//  MainView.swift
+//  MiniScanner
+//
+//  Created by Mustafa Merza on 7/18/24.
+//  Copyright © 2024 AppsNectar. All rights reserved.
+//
+
+import SwiftUI
+
+struct MainView<ViewModel: MainViewModel, Content: View>: View {
+    
+    var viewModel: ViewModel
+    
+    @ViewBuilder var content:  () -> Content
+    
+    init(viewModel: ViewModel,
+         @ViewBuilder content: @escaping () -> Content) {
+        self.viewModel = viewModel
+        self.content = content
+    }
+    
+    var body: some View {
+        
+        content()
+            .frame(maxWidth: .infinity, maxHeight: .infinity)
+            .onAppear {
+                viewModel.onAppear()
+            }
+            .onDisappear() { viewModel.onDisappear() }
+            .handleLayoutChanges()
+    }
+}
diff --git a/MiniScanner/Features/Common/Presentation/MainViewModel.swift b/MiniScanner/Features/Common/Presentation/MainViewModel.swift
new file mode 100644
index 0000000..de1b2a6
--- /dev/null
+++ b/MiniScanner/Features/Common/Presentation/MainViewModel.swift
@@ -0,0 +1,21 @@
+//
+//  MainViewModel.swift
+//  MiniScanner
+//
+//  Created by Mustafa Merza on 7/18/24.
+//  Copyright © 2024 AppsNectar. All rights reserved.
+//
+
+import Foundation
+
+protocol MainViewModel: ObservableObject {
+    
+    func onAppear()
+    
+    func onDisappear()
+}
+
+extension MainViewModel {
+    
+    func onDisappear() { }
+}
-- 
GitLab