├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .vscode └── settings.json ├── ComponentizeDemo ├── Application │ ├── Application.podspec │ ├── ApplicationProtocol.podspec │ ├── Protocol │ │ └── ApplicationProtocol.swift │ └── Source │ │ └── Application.swift └── Auth │ ├── Auth.podspec │ ├── AuthProtocol.podspec │ ├── Protocol │ └── AuthProtocol.swift │ └── Source │ └── Auth.swift ├── LICENSE ├── MIOSwiftyArchitecture.podspec ├── Package.resolved ├── Package.swift ├── Podfile ├── Podfile.lock ├── README-AppDocker.md ├── README.md ├── SwiftArchitecture.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── SAD.xcscheme │ └── SwiftArchitectureUITests.xcscheme ├── SwiftArchitecture.xcworkspace └── xcshareddata │ └── WorkspaceSettings.xcsettings ├── SwiftArchitectureTests ├── Info.plist └── swiftArchitectureTests.swift ├── SwiftArchitectureUITests ├── Info.plist ├── MockComponents.swift └── swiftArchitectureUITests.swift ├── SwiftyArchitecture ├── AppDelegate.swift ├── AppLinkRegistery.plist ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── launch_swifty_icon.imageset │ │ ├── Contents.json │ │ └── iconfinder_332_Swift_logo_4375493.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Base │ ├── AppDock │ │ ├── AppContext │ │ │ ├── AppContext+Standard.swift │ │ │ ├── AppContext.swift │ │ │ ├── AppDockConstants.swift │ │ │ └── UserContext.swift │ │ └── DataCenter │ │ │ ├── Accessor.swift │ │ │ ├── DataCenter.swift │ │ │ ├── Private │ │ │ └── Requests │ │ │ │ └── RequestRecords.swift │ │ │ ├── RealmDataBase.swift │ │ │ ├── ScheduledDatabase.swift │ │ │ └── Store.swift │ ├── Assistance │ │ ├── Algo │ │ │ └── Stack.swift │ │ ├── Combine │ │ │ └── CombineSupport.swift │ │ ├── Concurrency │ │ │ ├── Actor+Support.swift │ │ │ └── Concurrency+Support.swift │ │ ├── ConstValue&Function │ │ │ ├── Consts.swift │ │ │ ├── Path.swift │ │ │ ├── PublicFunctions.swift │ │ │ └── UIRelevance.swift │ │ ├── Extensions │ │ │ ├── Array+Extensions.swift │ │ │ ├── CollectionType+Extension.swift │ │ │ ├── Dictionary+Extension.swift │ │ │ ├── Double+Extension.swift │ │ │ ├── NSDate+Extentsions.swift │ │ │ ├── String+Extensions.swift │ │ │ ├── UIColor+Extensions.swift │ │ │ └── UIView+Extensions.swift │ │ └── Kits │ │ │ ├── Atomic │ │ │ ├── Atomic.swift │ │ │ └── RwQueue.swift │ │ │ ├── SystemLog │ │ │ └── LogInterface.swift │ │ │ └── TextInputLimiter │ │ │ └── TextInputLimiter.swift │ ├── Componentize │ │ ├── Common │ │ │ └── Consts+Componentize.swift │ │ ├── Module │ │ │ ├── Initiator+Utils.swift │ │ │ ├── Initiator.swift │ │ │ ├── ModuleManager.swift │ │ │ └── SessionModule.swift │ │ └── Navigation │ │ │ ├── LinkParser.swift │ │ │ ├── Navigation+PageGroup.swift │ │ │ ├── Navigation+Private.swift │ │ │ ├── Navigation.swift │ │ │ ├── NavigationUtils.swift │ │ │ └── README-Navigation.md │ ├── Networking │ │ ├── API │ │ │ ├── API+Concurrency.swift │ │ │ ├── API.swift │ │ │ ├── ApiDelegate.swift │ │ │ ├── ApiRouter.swift │ │ │ └── ResponseSerializer.swift │ │ ├── Generators │ │ │ ├── Errors.swift │ │ │ └── KMRequestGenerator.swift │ │ ├── Protocols │ │ │ ├── ApiCallbackProtocol.swift │ │ │ └── ApiInfoProtocol.swift │ │ └── Server │ │ │ └── Server.swift │ ├── Persistance │ │ ├── DatabaseManager.swift │ │ ├── DefaultDatabase.swift │ │ └── Protocols │ │ │ ├── DatabaseCommand.swift │ │ │ └── KMPersistance.swift │ ├── RxExtension │ │ ├── DelayWorker.swift │ │ ├── ProducerQueue.swift │ │ └── RxExtension.swift │ ├── Testable │ │ ├── ModuleManager+Testable.swift │ │ └── Networking │ │ │ └── API+Testable.swift │ └── Tests │ │ ├── APITests.swift │ │ ├── AssistanceTests.swift │ │ ├── ConcurrencySupportTests.swift │ │ ├── InitiatorTests.swift │ │ └── RxTests.swift ├── Demo │ ├── ApiManager │ │ ├── BaseAPI.swift │ │ ├── TestAPI.swift │ │ └── TestFailureAPI.swift │ ├── Appearance │ │ ├── Theme.swift │ │ ├── ThemeTestViewController.swift │ │ └── ThemeTestViewController.xib │ ├── CommonModule.swift │ ├── GlobalImports.swift │ ├── InfiniteTableView │ │ └── InfiniteTableViewController.swift │ ├── Model │ │ ├── UserModel.swift │ │ └── Video.swift │ ├── ModulesRegistery.plist │ ├── Persistance │ │ ├── RealmTests.swift │ │ └── UserTable.swift │ ├── Services │ │ ├── NotificationService.swift │ │ └── UserService.swift │ ├── UIDectector.swift │ └── ViewController │ │ ├── Auth │ │ ├── AuthTestViewController.swift │ │ └── AuthTestViewController.xib │ │ ├── Code Block │ │ ├── CodeBlockEditorViewController.swift │ │ ├── CodeBlockEditorViewController.xib │ │ ├── SALayoutManager.swift │ │ ├── SATextStorage.swift │ │ └── SATextView.swift │ │ ├── Combine │ │ ├── Combine.swift │ │ ├── CombineTestsViewController.swift │ │ └── CombineTestsViewController.xib │ │ ├── Concurrency │ │ ├── ConcurrencyTestViewController.swift │ │ └── ConcurrencyTestViewController.xib │ │ ├── DataCenter │ │ ├── DataCenterTestViewController.swift │ │ ├── DataCenterTestViewController.xib │ │ ├── TestObjModifyVC.swift │ │ └── TestObjModifyVC.xib │ │ ├── InternalTestVC.swift │ │ ├── Navigation │ │ └── HomeNavigationHandler.swift │ │ └── ViewController.swift ├── Info.plist ├── Resource │ └── swiftArchitecture-Bridging-Header.h ├── SceneDelegate.swift └── swiftArchitecture.xcdatamodeld │ ├── .xccurrentversion │ └── swiftArchitecture.xcdatamodel │ └── contents ├── pod_publish.sh └── swiftArchitecture.xcworkspace ├── contents.xcworkspacedata └── xcshareddata └── IDEWorkspaceChecks.plist /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | *.DS_Store 20 | 21 | # CocoaPods 22 | # 23 | # We recommend against adding the Pods directory to your .gitignore. However 24 | # you should judge for yourself, the pros and cons are mentioned at: 25 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 26 | # 27 | 28 | Pods/ 29 | 30 | 31 | # Carthage 32 | # 33 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 34 | # Carthage/Checkouts 35 | 36 | Carthage/Build 37 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /ComponentizeDemo/Application/Application.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint UploadAcceleratorSDK.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'Application' 11 | s.version = '0.0.1' 12 | s.summary = 'Breezy architecture in Swift for building iOS applications.' 13 | s.description = <<-DESC 14 | * Breezy architecture in Swift for building iOS applications. It offers lots of functions which simple and easy to use for developer. 15 | DESC 16 | s.homepage = 'https://github.com/Mioke/SwiftArchitectureWithPOP' 17 | # s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'Mioke Klein' => 'mioke0428@gmail.com' } 19 | s.source = { :git => 'https://github.com/Mioke/SwiftArchitectureWithPOP.git', :tag => s.version.to_s } 20 | 21 | s.ios.deployment_target = '11.0' 22 | s.swift_versions = '5.0' 23 | 24 | # s.frameworks = 'UIKit', 'Foundation' 25 | # s.libraries = 'c++', 'sqlite3' 26 | 27 | s.source_files = 'Source/**/*.swift' 28 | 29 | s.dependency 'AuthProtocol' 30 | 31 | s.dependency 'ApplicationProtocol' 32 | s.dependency 'MIOSwiftyArchitecture' 33 | 34 | # s.xcconfig = { "SWIFT_OBJC_BRIDGING_HEADER" => "SwiftyArchitecture/Resource/swiftArchitecture-Bridging-Header.h" } 35 | # s.module_map = 'SwiftyArchitecture/Resource/module.modulemap' 36 | 37 | end 38 | -------------------------------------------------------------------------------- /ComponentizeDemo/Application/ApplicationProtocol.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint UploadAcceleratorSDK.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'ApplicationProtocol' 11 | s.version = '0.0.1' 12 | s.summary = 'Breezy architecture in Swift for building iOS applications.' 13 | s.description = <<-DESC 14 | * Breezy architecture in Swift for building iOS applications. It offers lots of functions which simple and easy to use for developer. 15 | DESC 16 | s.homepage = 'https://github.com/Mioke/SwiftArchitectureWithPOP' 17 | # s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'Mioke Klein' => 'mioke0428@gmail.com' } 19 | s.source = { :git => 'https://github.com/Mioke/SwiftArchitectureWithPOP.git', :tag => s.version.to_s } 20 | 21 | s.ios.deployment_target = '11.0' 22 | s.swift_versions = '5.0' 23 | 24 | # s.frameworks = 'UIKit', 'Foundation' 25 | # s.libraries = 'c++', 'sqlite3' 26 | 27 | s.source_files = 'Protocol/**/*.swift' 28 | 29 | s.dependency 'MIOSwiftyArchitecture' 30 | 31 | # s.xcconfig = { "SWIFT_OBJC_BRIDGING_HEADER" => "SwiftyArchitecture/Resource/swiftArchitecture-Bridging-Header.h" } 32 | # s.module_map = 'SwiftyArchitecture/Resource/module.modulemap' 33 | 34 | end 35 | -------------------------------------------------------------------------------- /ComponentizeDemo/Application/Protocol/ApplicationProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MIOSwiftyArchitecture 3 | 4 | public protocol ApplicationProtocol { 5 | func startApplication() 6 | } 7 | 8 | public extension ModuleIdentifier { 9 | static let application: ModuleIdentifier = "com.klein.module.application" 10 | } 11 | -------------------------------------------------------------------------------- /ComponentizeDemo/Application/Source/Application.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MIOSwiftyArchitecture 3 | import ApplicationProtocol 4 | import AuthProtocol 5 | import RxSwift 6 | 7 | class ApplicationModule: SessionModuleProtocol, ApplicationProtocol { 8 | 9 | static func sessionDidAuthenticate(with userID: String) { 10 | KitLogger.info() 11 | } 12 | 13 | static func sessionDidDeauthenticate(with userID: String) { 14 | KitLogger.info() 15 | } 16 | 17 | static var moduleIdentifier: ModuleIdentifier { 18 | return .application 19 | } 20 | required init() { } 21 | 22 | let disposeBag = DisposeBag() 23 | 24 | func moduleDidLoad(with manager: ModuleManager) { 25 | if let user = manager.session?.userID { 26 | print(user) 27 | } 28 | } 29 | 30 | func startApplication() { 31 | // AppContext.standard 32 | // .setup() 33 | // .subscribe { finished in 34 | // print(AppContext.standard.previousLaunchedUserId as Any) 35 | // guard let authModule = try? self.moduleManager?.bridge.resolve(.auth) as? AuthServiceProtocol else { 36 | // fatalError() 37 | // } 38 | // if let _ = AppContext.standard.previousLaunchedUserId { 39 | // authModule.refreshAuthenticationIfNeeded(completion: { user in 40 | // print(user) 41 | // }) 42 | // } 43 | // } 44 | // .disposed(by: disposeBag) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ComponentizeDemo/Auth/Auth.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint UploadAcceleratorSDK.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'Auth' 11 | s.version = '0.0.1' 12 | s.summary = 'Breezy architecture in Swift for building iOS applications.' 13 | s.description = <<-DESC 14 | * Breezy architecture in Swift for building iOS applications. It offers lots of functions which simple and easy to use for developer. 15 | DESC 16 | s.homepage = 'https://github.com/Mioke/SwiftArchitectureWithPOP' 17 | # s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'Mioke Klein' => 'mioke0428@gmail.com' } 19 | s.source = { :git => 'https://github.com/Mioke/SwiftArchitectureWithPOP.git', :tag => s.version.to_s } 20 | 21 | s.ios.deployment_target = '11.0' 22 | s.swift_versions = '5.0' 23 | 24 | # s.frameworks = 'UIKit', 'Foundation' 25 | # s.libraries = 'c++', 'sqlite3' 26 | 27 | s.source_files = 'Source/**/*.swift' 28 | 29 | s.dependency 'AuthProtocol' 30 | s.dependency 'MIOSwiftyArchitecture' 31 | 32 | s.dependency 'ApplicationProtocol' 33 | 34 | # s.xcconfig = { "SWIFT_OBJC_BRIDGING_HEADER" => "SwiftyArchitecture/Resource/swiftArchitecture-Bridging-Header.h" } 35 | # s.module_map = 'SwiftyArchitecture/Resource/module.modulemap' 36 | 37 | end 38 | -------------------------------------------------------------------------------- /ComponentizeDemo/Auth/AuthProtocol.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint UploadAcceleratorSDK.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'AuthProtocol' 11 | s.version = '0.0.1' 12 | s.summary = 'Breezy architecture in Swift for building iOS applications.' 13 | s.description = <<-DESC 14 | * Breezy architecture in Swift for building iOS applications. It offers lots of functions which simple and easy to use for developer. 15 | DESC 16 | s.homepage = 'https://github.com/Mioke/SwiftArchitectureWithPOP' 17 | # s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'Mioke Klein' => 'mioke0428@gmail.com' } 19 | s.source = { :git => 'https://github.com/Mioke/SwiftArchitectureWithPOP.git', :tag => s.version.to_s } 20 | 21 | s.ios.deployment_target = '11.0' 22 | s.swift_versions = '5.0' 23 | 24 | # s.frameworks = 'UIKit', 'Foundation' 25 | # s.libraries = 'c++', 'sqlite3' 26 | 27 | s.source_files = 'Protocol/**/*.swift' 28 | 29 | s.dependency 'MIOSwiftyArchitecture' 30 | 31 | # s.xcconfig = { "SWIFT_OBJC_BRIDGING_HEADER" => "SwiftyArchitecture/Resource/swiftArchitecture-Bridging-Header.h" } 32 | # s.module_map = 'SwiftyArchitecture/Resource/module.modulemap' 33 | 34 | end 35 | -------------------------------------------------------------------------------- /ComponentizeDemo/Auth/Protocol/AuthProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MIOSwiftyArchitecture 3 | import RxSwift 4 | 5 | public class TestUser: UserProtocol, Codable { 6 | 7 | public var id: String 8 | public var age: Int 9 | public var token: String 10 | public var expiration: Date? 11 | 12 | public static let modelVersion: Int = 1 13 | 14 | public enum CodingKeys: CodingKey { 15 | case id 16 | case age 17 | case token 18 | case expiration 19 | } 20 | 21 | public init(id: String, age: Int, token: String) { 22 | self.id = id 23 | self.age = age 24 | self.token = token 25 | self.expiration = Date().offsetWeek(1) 26 | } 27 | } 28 | 29 | extension TestUser { 30 | public var customDebugDescription: String { 31 | return "id - \(id), age - \(age)\ntoken: \(token)" 32 | } 33 | } 34 | 35 | public protocol AuthServiceProtocol { 36 | func authenticate(completion: @escaping (TestUser) -> ()) throws 37 | func deauthenticate(completion: @escaping (Error?) -> Void) 38 | func refreshAuthenticationIfNeeded(completion: @escaping (TestUser) -> ()) -> Void 39 | } 40 | 41 | public extension ModuleIdentifier { 42 | static let auth: ModuleIdentifier = "com.klein.module.auth" 43 | } 44 | -------------------------------------------------------------------------------- /ComponentizeDemo/Auth/Source/Auth.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MIOSwiftyArchitecture 3 | import AuthProtocol 4 | 5 | class AuthModule: ModuleProtocol, AuthServiceProtocol { 6 | 7 | static var moduleIdentifier: ModuleIdentifier { 8 | return .auth 9 | } 10 | required init() { } 11 | 12 | func moduleDidLoad(with manager: ModuleManager) { 13 | 14 | } 15 | 16 | func authenticate(completion: @escaping (TestUser) -> ()) throws { 17 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 18 | let user = TestUser(id: "15780", age: 25, token: self.genRandomToken()) 19 | ModuleManager.default.beginUserSession(with: user.id) 20 | self.markAsReleasing() 21 | completion(user) 22 | } 23 | } 24 | 25 | func genRandomToken() -> String { 26 | let len = 12 27 | let codec = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-=" 28 | 29 | return (0.. ()) { 36 | // if let previousUID = AppContext.standard.previousLaunchedUserId { 37 | // let previousUser = User(id: previousUID) 38 | // let previousContext = AppContext(user: previousUser) 39 | // // get token from previous context data center, like: 40 | // // let credential = previousContext.store.object(with: previousUID, type: Credential.Type) 41 | // print(previousContext) 42 | // // and then do some refresh token logic here. 43 | // } 44 | } 45 | 46 | func deauthenticate(completion: @escaping (Error?) -> Void) { 47 | 48 | } 49 | } 50 | 51 | extension AuthModule: ModuleInitiatorProtocol { 52 | static var identifier: String { moduleIdentifier } 53 | static var priority: Initiator.Priority { .high } 54 | static var dependencies: [String] { [] } 55 | static var operation: Initiator.Operation { 56 | return { 57 | print("Auth module initiator operation running.") 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Klein Mioke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MIOSwiftyArchitecture.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint UploadAcceleratorSDK.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'MIOSwiftyArchitecture' 11 | s.version = '2.0.1' 12 | s.summary = 'Breezy architecture in Swift for building iOS applications.' 13 | s.description = <<-DESC 14 | * Breezy architecture in Swift for building iOS applications. It offers lots of functions which simple and easy to use for developer. 15 | DESC 16 | s.homepage = 'https://github.com/Mioke/SwiftArchitectureWithPOP' 17 | s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'Mioke Klein' => 'mioke0428@gmail.com' } 19 | s.source = { :git => 'https://github.com/Mioke/SwiftArchitectureWithPOP.git', :tag => s.version.to_s } 20 | 21 | s.ios.deployment_target = '13.0' 22 | s.swift_versions = '5' 23 | 24 | # s.frameworks = 'UIKit', 'Foundation' 25 | # s.libraries = 'c++', 'sqlite3' 26 | 27 | # s.source_files = 'SwiftyArchitecture/Base/**/*.swift' 28 | s.default_subspecs = 'Assistance', 'Networking', 'RxExtension', 'AppDock', 'Componentize' 29 | 30 | s.subspec 'Assistance' do |ss| 31 | ss.frameworks = 'UIKit', 'Foundation' 32 | ss.source_files = 'SwiftyArchitecture/Base/Assistance/**/*.swift' 33 | end 34 | 35 | s.subspec 'Networking' do |ss| 36 | ss.frameworks = 'UIKit', 'Foundation' 37 | ss.source_files = 'SwiftyArchitecture/Base/Networking/**/*.swift' 38 | ss.dependency 'Alamofire', '~> 5.4' 39 | ss.dependency 'ObjectMapper', '~> 4.2' 40 | ss.dependency 'MIOSwiftyArchitecture/Assistance' 41 | end 42 | 43 | # s.subspec 'Persistance' do |ss| 44 | # ss.frameworks = 'UIKit', 'Foundation' 45 | # ss.source_files = 'SwiftyArchitecture/Base/Persistance/**/*.swift' 46 | # ss.dependency 'FMDB' 47 | # ss.dependency 'MIOSwiftyArchitecture/Assistance' 48 | # end 49 | 50 | s.subspec 'RxExtension' do |ss| 51 | ss.source_files = 'SwiftyArchitecture/Base/RxExtension/**/*.swift' 52 | ss.dependency 'MIOSwiftyArchitecture/Assistance' 53 | ss.dependency 'MIOSwiftyArchitecture/Networking' 54 | ss.dependency 'RxSwift', '~> 6.2' 55 | end 56 | 57 | s.subspec 'AppDock' do |ss| 58 | ss.source_files = 'SwiftyArchitecture/Base/AppDock/**/*.swift' 59 | ss.dependency 'MIOSwiftyArchitecture/Assistance' 60 | ss.dependency 'MIOSwiftyArchitecture/Networking' 61 | ss.dependency 'MIOSwiftyArchitecture/RxExtension' 62 | ss.dependency 'RxSwift', '~> 6.2' 63 | ss.dependency 'RxCocoa', '~> 6.2' 64 | ss.dependency 'RxRealm' # 5.0.4 65 | ss.dependency 'RealmSwift' # 10.45.2 66 | ss.dependency "Realm" 67 | end 68 | 69 | s.subspec 'Componentize' do |ss| 70 | ss.source_files = 'SwiftyArchitecture/Base/Componentize/**/*.swift' 71 | ss.dependency 'MIOSwiftyArchitecture/Assistance' 72 | ss.dependency 'Swinject', '~> 2.8' 73 | ss.dependency 'RxSwift', '~> 6.2' 74 | end 75 | 76 | s.subspec 'Testable' do |ss| 77 | ss.source_files = 'SwiftyArchitecture/Base/Testable/**/*.swift' 78 | ss.dependency 'MIOSwiftyArchitecture/Assistance' 79 | ss.dependency 'MIOSwiftyArchitecture/Networking' 80 | ss.dependency 'MIOSwiftyArchitecture/RxExtension' 81 | ss.dependency 'MIOSwiftyArchitecture/AppDock' 82 | ss.dependency 'MIOSwiftyArchitecture/Componentize' 83 | end 84 | 85 | s.test_spec 'Tests' do |test_spec| 86 | test_spec.source_files = 'SwiftyArchitecture/Base/Tests/**/*.swift' 87 | # test_spec.dependency 'OCMock' # This dependency will only be linked with your tests. 88 | test_spec.dependency 'MIOSwiftyArchitecture/Assistance' 89 | test_spec.dependency 'MIOSwiftyArchitecture/Networking' 90 | test_spec.dependency 'MIOSwiftyArchitecture/RxExtension' 91 | test_spec.dependency 'MIOSwiftyArchitecture/AppDock' 92 | test_spec.dependency 'MIOSwiftyArchitecture/Componentize' 93 | test_spec.dependency 'MIOSwiftyArchitecture/Testable' 94 | 95 | test_spec.dependency 'RxSwift', '~> 6.2' 96 | end 97 | 98 | # s.xcconfig = { "SWIFT_OBJC_BRIDGING_HEADER" => "SwiftyArchitecture/Resource/swiftArchitecture-Bridging-Header.h" } 99 | # s.module_map = 'SwiftyArchitecture/Resource/module.modulemap' 100 | 101 | end 102 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "alamofire", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Alamofire/Alamofire.git", 7 | "state" : { 8 | "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", 9 | "version" : "5.8.1" 10 | } 11 | }, 12 | { 13 | "identity" : "objectmapper", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/tristanhimmelman/ObjectMapper.git", 16 | "state" : { 17 | "revision" : "0b96a734de3ea1c87374ae677064f86adb0716ec", 18 | "version" : "4.2.0" 19 | } 20 | }, 21 | { 22 | "identity" : "realm-core", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/realm/realm-core.git", 25 | "state" : { 26 | "revision" : "7227d6a447821c28895daa099b6c7cd4c99d461b", 27 | "version" : "13.25.1" 28 | } 29 | }, 30 | { 31 | "identity" : "realm-swift", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/realm/realm-swift.git", 34 | "state" : { 35 | "revision" : "836cc4b8619886f979f8961c3f592a82b0741591", 36 | "version" : "10.45.3" 37 | } 38 | }, 39 | { 40 | "identity" : "rxrealm", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/Mioke/RxRealm.git", 43 | "state" : { 44 | "branch" : "main", 45 | "revision" : "f930c360fef32372bc839c6939d4e0ee8f8276ac" 46 | } 47 | }, 48 | { 49 | "identity" : "rxswift", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/ReactiveX/RxSwift.git", 52 | "state" : { 53 | "revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4", 54 | "version" : "6.6.0" 55 | } 56 | } 57 | ], 58 | "version" : 2 59 | } 60 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftyAppArch", 8 | platforms: [.iOS(.v13)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, making them visible to other packages. 11 | .library( 12 | name: "SwiftyAppArch", 13 | targets: ["SwiftyAppArch"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/realm/realm-swift.git", from: "10.45.2"), 17 | .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0"), 18 | .package(url: "https://github.com/tristanhimmelman/ObjectMapper.git", from: "4.2.0"), 19 | .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.6.0"), 20 | // .package(url: "https://github.com/Swinject/Swinject.git", from: "2.8.4"), 21 | .package(url: "https://github.com/Mioke/RxRealm.git", branch: "main"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package, defining a module or a test suite. 25 | // Targets can depend on other targets in this package and products from dependencies. 26 | .target( 27 | name: "SwiftyAppArch", 28 | dependencies: [ 29 | .product(name: "RealmSwift", package: "realm-swift"), 30 | .product(name: "Alamofire", package: "Alamofire"), 31 | .product(name: "ObjectMapper", package: "ObjectMapper"), 32 | .product(name: "RxSwift", package: "RxSwift"), 33 | .product(name: "RxCocoa", package: "RxSwift"), 34 | .product(name: "RxRealm", package: "RxRealm"), 35 | ], 36 | path: "./SwiftyArchitecture/Base", 37 | exclude: ["Persistance", "Tests"]), 38 | // .testTarget( 39 | // name: "SwiftyAppArchTests", 40 | // dependencies: ["SwiftyAppArch"]), 41 | ], 42 | swiftLanguageVersions: [.v5] 43 | ) 44 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '15.0' 3 | use_frameworks! 4 | install! 'cocoapods', :preserve_pod_file_structure => true 5 | 6 | target "SAD" do 7 | # networking 8 | pod 'Alamofire' 9 | 10 | # rx 11 | pod 'RxSwift', '~> 6.0' 12 | pod 'RxDataSources' 13 | 14 | # persistence 15 | 16 | # RxRealm not up-to-date, so we have to modify it ourselves. 17 | pod 'RxRealm', :git => 'https://github.com/Mioke/RxRealm.git', :branch => 'main' 18 | pod 'RealmSwift' 19 | 20 | pod 'MIOSwiftyArchitecture', :path => './', :testspecs => ['Tests'] 21 | pod 'MIOSwiftyArchitecture/Testable', :path => './' 22 | 23 | 24 | 25 | # componentization 26 | pod 'Application', :path => './ComponentizeDemo/Application' 27 | pod 'ApplicationProtocol', :path => './ComponentizeDemo/Application' 28 | 29 | pod 'Auth', :path => './ComponentizeDemo/Auth' 30 | pod 'AuthProtocol', :path => './ComponentizeDemo/Auth' 31 | 32 | # pod 'Notification', :path => './ComponentizeDemo/Notification' 33 | # pod 'NotificationProtocol', :path => './ComponentizeDemo/Notification' 34 | 35 | pod 'Swinject' 36 | pod 'SwinjectAutoregistration' 37 | 38 | # other utils 39 | pod 'SnapKit' 40 | pod 'FLEX' 41 | 42 | target 'SwiftArchitectureTests' do 43 | 44 | end 45 | 46 | target 'SwiftArchitectureUITests' do 47 | 48 | end 49 | end 50 | 51 | # pod 'FMDB/FTS' # FMDB with FTS 52 | # pod 'FMDB/standalone' # FMDB with latest SQLite amalgamation source 53 | # pod 'FMDB/standalone/FTS' # FMDB with latest SQLite amalgamation source and FTS 54 | # pod 'FMDB/SQLCipher' # FMDB with SQLCipher 55 | 56 | 57 | post_install do |installer_representation| 58 | installer_representation.pods_project.targets.each do |target| 59 | 60 | target.build_configurations.each do |config| 61 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /SwiftArchitecture.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftArchitecture.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftArchitecture.xcodeproj/xcshareddata/xcschemes/SAD.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /SwiftArchitecture.xcodeproj/xcshareddata/xcschemes/SwiftArchitectureUITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 29 | 35 | 36 | 37 | 38 | 39 | 49 | 50 | 56 | 57 | 59 | 60 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /SwiftArchitecture.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftArchitectureTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftArchitectureTests/swiftArchitectureTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // swiftArchitectureTests.swift 3 | // swiftArchitectureTests 4 | // 5 | // Created by Klein Mioke on 15/11/23. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SAD 11 | @testable import MIOSwiftyArchitecture 12 | @testable import Auth 13 | @testable import AuthProtocol 14 | 15 | class swiftArchitectureTests: XCTestCase { 16 | 17 | override func setUp() { 18 | super.setUp() 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | 21 | } 22 | 23 | override func tearDown() { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | super.tearDown() 26 | } 27 | 28 | func testExample() { 29 | // This is an example of a functional test case. 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testPerformanceExample() { 34 | // This is an example of a performance test case. 35 | self.measure { 36 | // Put the code you want to measure the time of here. 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /SwiftArchitectureUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftArchitectureUITests/MockComponents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockComponents.swift 3 | // SwiftArchitectureUITests 4 | // 5 | // Created by KelanJiang on 2022/5/24. 6 | // Copyright © 2022 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import MIOSwiftyArchitecture 11 | import AuthProtocol 12 | import Auth 13 | import ApplicationProtocol 14 | import Application 15 | 16 | 17 | class MockAuth: ModuleProtocol { 18 | 19 | func moduleDidLoad(with manager: ModuleManager) { 20 | // mocked module will never running this method. 21 | } 22 | 23 | static var moduleIdentifier: ModuleIdentifier { 24 | return .auth 25 | } 26 | 27 | required init() { 28 | 29 | } 30 | } 31 | 32 | extension MockAuth: AuthServiceProtocol { 33 | 34 | func authenticate(completion: @escaping (User) -> ()) throws { 35 | let user = User(id: "15780") 36 | ModuleManager.default.beginUserSession(with: user.id) 37 | self.markAsReleasing() 38 | } 39 | 40 | func refreshAuthenticationIfNeeded(completion: @escaping (User) -> ()) { 41 | if let previousUID = AppContext.standard.previousLaunchedUserId { 42 | let previousUser = User(id: previousUID) 43 | let previousContext = AppContext(user: previousUser) 44 | // get token from previous context data center, like: 45 | // let credential = previousContext.store.object(with: previousUID, type: Credential.Type) 46 | print(previousContext) 47 | // and then do some refresh token logic here. 48 | } 49 | } 50 | 51 | func deauthenticate(completion: @escaping (Error?) -> Void) { 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SwiftArchitectureUITests/swiftArchitectureUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // swiftArchitectureUITests.swift 3 | // swiftArchitectureUITests 4 | // 5 | // Created by Klein Mioke on 15/11/23. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import MIOSwiftyArchitecture 11 | import ApplicationProtocol 12 | 13 | class swiftArchitectureUITests: XCTestCase { 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | 20 | // In UI tests it is usually best to stop immediately when a failure occurs. 21 | continueAfterFailure = false 22 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 23 | XCUIApplication().launch() 24 | 25 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 26 | let auth = MockAuth() 27 | guard ModuleManager.default.bridge.inject(instance: auth) else { 28 | fatalError() 29 | } 30 | } 31 | 32 | override func tearDown() { 33 | // Put teardown code here. This method is called after the invocation of each test method in the class. 34 | super.tearDown() 35 | } 36 | 37 | func testExample() { 38 | // Use recording to get started writing UI tests. 39 | // Use XCTAssert and related functions to verify your tests produce the correct results. 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /SwiftyArchitecture/AppLinkRegistery.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Modules 6 | 7 | 8 | id 9 | home 10 | handler 11 | SAD.HomeNavigationHandler 12 | paths 13 | 14 | messages 15 | 16 | target 17 | SAD.InfiniteTableViewController 18 | 19 | data-center 20 | 21 | target 22 | SAD.DataCenterTestViewController 23 | 24 | theme 25 | 26 | isAccessibleExternally 27 | 28 | target 29 | SAD.ThemeTestViewController 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Assets.xcassets/launch_swifty_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "iconfinder_332_Swift_logo_4375493.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Assets.xcassets/launch_swifty_icon.imageset/iconfinder_332_Swift_logo_4375493.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mioke/SwiftyArchitecture/445c9b450057c7681089bd7ca179df6eb04481d3/SwiftyArchitecture/Assets.xcassets/launch_swifty_icon.imageset/iconfinder_332_Swift_logo_4375493.png -------------------------------------------------------------------------------- /SwiftyArchitecture/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/AppDock/AppContext/AppDockConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDockConstants.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/2/17. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Consts { 11 | static let appDockDomain = "com.mioke.swiftyarchitecture.appdock" 12 | } 13 | 14 | public extension KitErrors { 15 | private static var noDelegate: Info { 16 | .init(code: .noDelegate, message: "The delegate of this function hasn't been set yet.") 17 | } 18 | static let noDelegateError: NSError = error(domain: Consts.appDockDomain, info: noDelegate) 19 | 20 | 21 | private static var alreadyAuthenticatedInfo: Info { 22 | return .init(code: .alreadyAuthenticated, message: "AppContext already in a custom user context, can't authenticate a new user now.") 23 | } 24 | 25 | static var alreadyAuthenticated : NSError { 26 | return error(domain: Consts.appDockDomain, info: alreadyAuthenticatedInfo) 27 | } 28 | 29 | private static var notFoundInfo: Info { 30 | .init(code: .notFound, message: "The record can't be found in Store.") 31 | } 32 | 33 | /// File or data record cannot be found in the sandbox or database. 34 | static var notFound: NSError { 35 | error(domain: Consts.appDockDomain, info: notFoundInfo) 36 | } 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/AppDock/AppContext/UserContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserContext.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/2/16. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import RealmSwift 11 | 12 | public protocol UserProtocol: DataConvertible { 13 | /// The identifier of a user, the primary key of the conversion data of the user model. 14 | var id: String { get set } 15 | 16 | /// Your customized user model version. Because of sometimes your user model will got updated when your code grows, 17 | /// like from `Userv1` to `Userv2` it will add more properties in it, so when the model changes, please update this 18 | /// version number to a new one (larger), and the setup process will let you go into a migration process. 19 | static var modelVersion: Int { get } 20 | } 21 | 22 | /// The authentication states. 23 | public enum AuthState { 24 | /// Not logged in, has no session or user data. 25 | case unauthenticated 26 | /// Logged in and stored all the newest user data. 27 | case authenticated 28 | /// Has previous user's data but the application has already restarted, needs to be refreshed. 29 | case presession 30 | } 31 | 32 | public protocol AuthControllerDelegate: AnyObject { 33 | /// Called when Standard AppContext is demanded to trigger a new user login process. 34 | /// - Returns: The authentication process. 35 | func authenticate() -> Observable 36 | 37 | /// When the Standard AppContext found a user context cache in sandbox, or `refreshAuthentication` is called, 38 | /// this function will be called to decide whether the user context should be refreshed. 39 | /// - Parameters: 40 | /// - user: The previous user data. 41 | /// - isStartup: is called from application start process. 42 | /// - Returns: A decission of whether should refresh the user data. 43 | func shouldRefreshAuthentication(with user: UserProtocol, isStartup: Bool) -> Bool 44 | 45 | /// Called when there is needed to refresh the current user context. 46 | /// - Parameter user: The old user data. 47 | /// - Returns: The refresh process and return a new user in the end. 48 | func refreshAuthentication(with user: UserProtocol) -> Observable 49 | 50 | /// Called when the AppContext is logging out. 51 | /// - Returns: The deauthentication process. 52 | func deauthenticate() -> ObservableSignal 53 | } 54 | 55 | public class AuthController: NSObject { 56 | 57 | public weak var delegate: AuthControllerDelegate? 58 | 59 | init(delegate: AuthControllerDelegate?) { 60 | self.delegate = delegate 61 | } 62 | } 63 | 64 | class DefaultUser: UserProtocol, Codable { 65 | static let _id = "__standard__" 66 | static let modelVersion: Int = 1 67 | 68 | var id: String = DefaultUser._id 69 | 70 | enum CodingKeys: CodingKey { 71 | case id 72 | } 73 | } 74 | 75 | class _UserMeta: Object { 76 | @Persisted(primaryKey: true) var key: Int 77 | @Persisted var currentUserId: String 78 | @Persisted var isLoggedIn: Bool 79 | } 80 | 81 | class _UserData: Object { 82 | @Persisted(primaryKey: true) var userId: String 83 | @Persisted var codableData: Foundation.Data 84 | @Persisted var version: Int 85 | } 86 | 87 | class _User { 88 | struct Wrapper: Codable { 89 | var `class`: String 90 | var data: Foundation.Data 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/AppDock/DataCenter/Accessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Accessor.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2020/4/10. 6 | // Copyright © 2020 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxRealm 12 | import RealmSwift 13 | 14 | public class Accessor { 15 | 16 | public static func context(_ type: Store.ContextType) -> Self { 17 | return self.init(contextType: type) 18 | } 19 | 20 | let contextType: Store.ContextType 21 | 22 | internal required init(contextType: Store.ContextType) { 23 | self.contextType = contextType 24 | } 25 | 26 | private var store: ObservableDataBase { 27 | return AppContext.current.store.context(contextType) 28 | } 29 | 30 | public var all: Observable<[T]> { 31 | return store.objects(with: T.self).map { $0.toArray() } 32 | } 33 | 34 | public func object(with key: KeyType) -> Observable { 35 | return store.object(with: key, type: T.self) 36 | } 37 | 38 | public func objects(with predicate: NSPredicate) -> Observable<[T]> { 39 | return store.objects(with: T.self, predicate: predicate).map { $0.toArray() } 40 | } 41 | 42 | public func objects(with query: @escaping (Query) -> Query) -> Observable<[T]> { 43 | return store.objects(with: T.self, where: query).map { $0.toArray() } 44 | } 45 | 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/AppDock/DataCenter/Private/Requests/RequestRecords.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestRecords.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by Mioke Klein on 2021/5/7. 6 | // 7 | 8 | import UIKit 9 | 10 | struct RequestRecordNode { 11 | let requestTime: CFAbsoluteTime 12 | let freshness: RequestFreshness 13 | 14 | var expirationDate: CFAbsoluteTime { 15 | guard case .seconds(let seconds) = freshness else { 16 | return 0 17 | } 18 | return requestTime + CFAbsoluteTime(seconds) 19 | } 20 | } 21 | 22 | internal class RequestRecords: NSObject { 23 | 24 | @ThreadSafe 25 | var map: [String: RequestRecordNode] = [:] 26 | 27 | func key(ofType t: Request) -> String { 28 | var key = String(describing: type(of: t.self)) 29 | if let params = t.params, 30 | let data = try? JSONSerialization.data(withJSONObject: params, options: []) { 31 | key += ".\(data.hashValue)" 32 | } 33 | return key 34 | } 35 | 36 | func last(ofRequest request: Request) -> (key: String, node: RequestRecordNode)? { 37 | let key = self.key(ofType: request) 38 | if let node = map[key] { 39 | return (key, node) 40 | } 41 | return nil 42 | } 43 | 44 | func update(request: Request) -> Void { 45 | let freshness = T.requestFreshness 46 | if case .none = freshness { 47 | return 48 | } 49 | let time = CFAbsoluteTimeGetCurrent() 50 | map[key(ofType: request)] = RequestRecordNode(requestTime: time, freshness: freshness) 51 | } 52 | 53 | func shouldSend(request: Request) -> Bool { 54 | guard let info = self.last(ofRequest: request) else { 55 | return true 56 | } 57 | let should = info.node.expirationDate < CFAbsoluteTimeGetCurrent() 58 | if should { 59 | map[info.key] = nil 60 | } 61 | return should 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/AppDock/DataCenter/RealmDataBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmDataBase.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2020/4/10. 6 | // Copyright © 2020 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxRealm 11 | import RxSwift 12 | import RealmSwift 13 | 14 | public typealias RealmMigrationBlock = RealmSwift.MigrationBlock 15 | 16 | public final class RealmDataBase: NSObject { 17 | 18 | public enum `Type` { 19 | case file 20 | case memory 21 | } 22 | 23 | private var realmConfiguration: Realm.Configuration 24 | 25 | /// The read-write context of Realm. 26 | public var realm: Realm { 27 | get throws { return try currentThreadInstance } 28 | } 29 | // TODO: - Add read-only context and syncing context logic. 30 | // public var readOnly: Realm 31 | 32 | public private(set) var type: RealmDataBase.`Type` = .file 33 | 34 | internal var internalQueue: DispatchQueue = .init(label: Consts.domainPrefix + ".RDB.internal", qos: .default) 35 | internal lazy var internalScheduler: SchedulerType = SerialDispatchQueueScheduler( 36 | queue: internalQueue, 37 | internalSerialQueueName: Consts.domainPrefix + ".RDB.internal") 38 | 39 | /// Create a file type database using an app context's information. 40 | /// - Parameters: 41 | /// - context: The app context contains user information. 42 | /// - migration: The customize migration handler. 43 | public init(location: URL, schemaVersion: UInt64, migration: @escaping RealmMigrationBlock) throws { 44 | self.realmConfiguration = Realm.Configuration( 45 | fileURL: location, 46 | schemaVersion: schemaVersion, 47 | migrationBlock: migration, 48 | deleteRealmIfMigrationNeeded: true 49 | ) 50 | } 51 | 52 | init(realmConfiguration: Realm.Configuration) { 53 | self.realmConfiguration = realmConfiguration 54 | super.init() 55 | } 56 | 57 | public static func inMemoryDatabase(appContext context: AppContext) -> RealmDataBase { 58 | var config = Realm.Configuration.defaultConfiguration; 59 | config.inMemoryIdentifier = "RLM.MEMORY.\(context.userId)"; 60 | let db = RealmDataBase(realmConfiguration: config) 61 | db.type = .memory 62 | return db 63 | } 64 | 65 | public var currentThreadInstance: Realm { 66 | get throws { 67 | return try Realm(configuration: realmConfiguration) 68 | } 69 | } 70 | 71 | public func invalidate() { 72 | try? realm.invalidate() 73 | } 74 | } 75 | 76 | extension Realm { 77 | 78 | public func safeWrite(_ block: (Realm) throws -> Void) throws -> Void { 79 | if isInWriteTransaction { 80 | try block(self) 81 | try commitWrite() 82 | } else { 83 | try self.write { try block(self) } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/AppDock/DataCenter/ScheduledDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScheduledDatabase.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/7/26. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol ScheduledDatabase: AnyObject { 11 | var rwQueue: RwQueue { get set } 12 | func write(_ execution: (Self) -> T) -> T 13 | func read(_ execution: (Self) -> T) -> T 14 | 15 | func `async`(write execution: @escaping (Self) -> Void) 16 | func `async`(read execution: @escaping (Self) -> Void) 17 | } 18 | 19 | extension ScheduledDatabase { 20 | 21 | public func write(_ execution: (Self) -> T) -> T { 22 | return rwQueue.syncWrite { execution(self) } 23 | } 24 | 25 | public func read(_ execution: (Self) -> T) -> T { 26 | return rwQueue.syncRead { execution(self) } 27 | } 28 | 29 | public func async(write execution: @escaping (Self) -> Void) { 30 | rwQueue.write { execution(self) } 31 | } 32 | 33 | public func async(read execution: @escaping (Self) -> Void) { 34 | rwQueue.read { execution(self) } 35 | } 36 | } 37 | 38 | // Realm doesn't need to conform to `ScheduledDatabase` because it's thread-safe with MVCC: 39 | 40 | /** 41 | - **Don't lock to read:** 42 | Realm Database's Multiversion Concurrency Control (MVCC) architecture eliminates the need to lock for read operations. The values you read will never be corrupted or in a partially-modified state. You can freely read from realms on any thread without the need for locks or mutexes. Unnecessary locking would be a performance bottleneck since each thread might need to wait its turn before reading. 43 | - **Avoid writes on the UI thread if you write on a background thread:** 44 | You can write to a realm from any thread, but there can be only one writer at a time. Consequently, write transactions block each other. A write on the UI thread may result in your app appearing unresponsive while it waits for a write on a background thread to complete. If you are using Device Sync, avoid writing on the UI thread as Sync writes on a background thread. 45 | - **Don't pass live objects, collections, or realms to other threads:** 46 | Live objects, collections, and realm instances are thread-confined: that is, they are only valid on the thread on which they were created. Practically speaking, this means you cannot pass live instances to other threads. However, Realm Database offers several mechanisms for sharing objects across threads. 47 | */ 48 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Algo/Stack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stack.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2023/4/7. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Stack: Sequence { 11 | private var elements: [Element] 12 | 13 | /// The elements will be popped in the order they appear in the stack 14 | public init(_ sequence: S) where S: Sequence, S.Element == Element { 15 | elements = sequence.reversed() 16 | } 17 | 18 | public init() { 19 | elements = [] 20 | } 21 | 22 | public mutating func push(_ element: Element) { 23 | elements.append(element) 24 | } 25 | 26 | public mutating func pop() -> Element? { 27 | return elements.popLast() 28 | } 29 | 30 | public __consuming func makeIterator() -> Stack.Iterator { 31 | return Iterator(iterator: elements.reversed().makeIterator()) 32 | } 33 | 34 | public struct Iterator: IteratorProtocol { 35 | private var iterator: Array.Iterator 36 | init(iterator: Array.Iterator) { 37 | self.iterator = iterator 38 | } 39 | 40 | public mutating func next() -> Element? { 41 | return iterator.next() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Combine/CombineSupport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineSupport.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2023/7/12. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | 12 | @dynamicMemberLookup 13 | public struct CombineContext { 14 | public let base: T 15 | 16 | public init(base: T) { 17 | self.base = base 18 | } 19 | 20 | public subscript( 21 | dynamicMember keyPath: ReferenceWritableKeyPath 22 | ) -> AnyPublisher where T: NSObject { 23 | base.publisher(for: keyPath).eraseToAnyPublisher() 24 | } 25 | } 26 | 27 | public protocol CombineCompatible { 28 | associatedtype CombineBase 29 | 30 | var combine: CombineContext { get set } 31 | static var combine: CombineContext.Type { get set } 32 | } 33 | 34 | public extension CombineCompatible { 35 | 36 | var combine: CombineContext { 37 | get { CombineContext(base: self) } 38 | set { } 39 | } 40 | 41 | static var combine: CombineContext.Type { 42 | get { CombineContext.self } 43 | set { } 44 | } 45 | } 46 | 47 | extension NSObject: CombineCompatible { } 48 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Concurrency/Actor+Support.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Actor+Support.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2024/1/4. 6 | // 7 | 8 | import Foundation 9 | 10 | // Copied from Realm 11 | 12 | @available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) 13 | public extension Actor { 14 | func verifier() -> (@Sendable () -> Void) { 15 | #if swift(>=5.9) 16 | // When possible use the official API for actor checking 17 | if #available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) { 18 | return { 19 | self.preconditionIsolated() 20 | } 21 | } 22 | #endif 23 | 24 | // This exploits a hole in Swift's type system to construct a function 25 | // which is isolated to the current actor, and then casts away that 26 | // information. This results in runtime warnings/aborts if it's called 27 | // from outside the actor when actor data race checking is enabled. 28 | let fn: () -> Void = { _ = self } 29 | return unsafeBitCast(fn, to: (@Sendable () -> Void).self) 30 | } 31 | 32 | // Asynchronously invoke the given block on the actor. This takes a 33 | // non-sendable function because the function is invoked on the same actor 34 | // it was defined on, and just goes through some hops in between. 35 | nonisolated func invoke(_ fn: @escaping () -> Void) { 36 | let fn = unsafeBitCast(fn, to: (@Sendable () -> Void).self) 37 | Task { 38 | await doInvoke(fn) 39 | } 40 | } 41 | 42 | private func doInvoke(_ fn: @Sendable () -> Void) { 43 | fn() 44 | } 45 | 46 | // A helper to invoke a regular isolated sendable function with this actor 47 | func invoke(_ fn: @Sendable (isolated Self) async throws -> T) async rethrows -> T { 48 | try await fn(self) 49 | } 50 | } 51 | 52 | public extension MainActor { 53 | /// Explicitly mark this function is running in a asynchronized way. 54 | nonisolated static func `async`(fn: @escaping () -> Void) { 55 | MainActor.shared.invoke(fn) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/ConstValue&Function/Consts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // swiftArchitecture 4 | // 5 | // Created by jiangkelan on 28/06/2017. 6 | // Copyright © 2017 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | #if swift(<5.5.2) 13 | #error("SwiftyArchitecture only support Swift version >= 5.5.2 for concurrency support purpose.") 14 | #endif 15 | 16 | public struct Consts { 17 | // - Domains 18 | /// The domain prefix of this framework, used in any identifier like networking, queue etc. 19 | public static let domainPrefix = "com.mioke.swiftyarchitecture" 20 | /// The global used or non-specific domain identifier. 21 | public static let defaultDomain = domainPrefix + ".default" 22 | /// The domain of networking kit in this kit. 23 | public static let networkingDomain = domainPrefix + ".networking" 24 | 25 | // - Time 26 | public static let milisecondsPerSecond: UInt64 = 1_000 27 | public static let microsecondsPerSecond: UInt64 = 1_000_000 28 | public static let nanosecondsPerSecond: UInt64 = 1_000_000_000 29 | } 30 | 31 | extension Consts { 32 | public static let kitName = "MIOSwiftyArchitecture" 33 | } 34 | 35 | internal func todo_error() -> NSError { 36 | return NSError(domain: "", code: 777, userInfo: nil) 37 | } 38 | 39 | public class KitErrors { 40 | 41 | public struct Info { 42 | let code: KitErrors.Codes 43 | let message: String? 44 | } 45 | 46 | public static func error(domain: String, info: KitErrors.Info) -> NSError { 47 | return .init( 48 | domain: domain, 49 | code: info.code.rawValue, 50 | userInfo: [NSLocalizedDescriptionKey: info.message ?? ""] 51 | ) 52 | } 53 | } 54 | 55 | extension KitErrors { 56 | 57 | public enum Codes: Int { 58 | // general 59 | case noDelegate 60 | case unknown 61 | case deallocated 62 | case todo = 777 63 | 64 | // networking 65 | case responseError = 1000 66 | case apiConstructionFailed 67 | 68 | // persistance 69 | case notFound = 2000 70 | 71 | // app dock 72 | case alreadyAuthenticated = 3000 73 | 74 | // componentize 75 | case graphCycle = 4000 76 | } 77 | } 78 | 79 | 80 | // MARK: - Intnernal errors 81 | 82 | public extension KitErrors { 83 | 84 | private static var deallocatedInfo: Info { 85 | .init(code: .deallocated, message: "The instance has already been deallocated.") 86 | } 87 | static let deallocated: NSError = error(domain: Consts.defaultDomain, info: KitErrors.deallocatedInfo) 88 | 89 | private static var todoInfo: Info { 90 | .init(code: .todo, message: "The author is too lazy to complete this.") 91 | } 92 | static let todo: NSError = error(domain: Consts.defaultDomain, info: KitErrors.todoInfo) 93 | 94 | 95 | private static var unknownErrorInfo: Info { 96 | return .init(code: .unknown, message: "Unknown error") 97 | } 98 | 99 | static var unknown: NSError { 100 | return error(domain: Consts.networkingDomain, info: unknownErrorInfo) 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/ConstValue&Function/Path.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Path.swift 3 | // FileMail 4 | // 5 | // Created by yizhong zhuang on 2017/5/15. 6 | // Copyright © 2017年 xlvip. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | 12 | class Path: NSObject { 13 | static var homePath: String { 14 | return NSHomeDirectory() 15 | } 16 | 17 | static var tempPath: String { 18 | return NSTemporaryDirectory() 19 | } 20 | 21 | static var docPath: String { 22 | let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) 23 | return paths.first ?? NSHomeDirectory() + "/Documents" 24 | } 25 | 26 | static var appPath: String { 27 | return Bundle.main.bundlePath 28 | } 29 | 30 | static var libPath: String { 31 | let paths = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true) 32 | return paths.first ?? NSHomeDirectory() + "/Library" 33 | } 34 | 35 | static var cachePath: String { 36 | let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) 37 | return paths.first ?? NSHomeDirectory() + "/Library/Cashes" 38 | } 39 | 40 | 41 | // 创建目录,如果已存在则返回true,不存在创建目录成功返回true,失败返回false 42 | static func createDirIfNeeded(at dirPath: URL) -> Bool { 43 | var isDir = ObjCBool(false) 44 | let exist = FileManager.default.fileExists(atPath: dirPath.absoluteString, isDirectory: &isDir) 45 | if exist && isDir.boolValue { // dir exists 46 | return true 47 | } 48 | do { 49 | try FileManager.default.createDirectory(at: dirPath, withIntermediateDirectories: true, attributes: nil) 50 | } catch let error as NSError { 51 | debugPrint("create file dir error:\(error.localizedDescription), url:\(dirPath)") 52 | return false 53 | } 54 | 55 | return true 56 | } 57 | 58 | static func copyFilesFile(from url: String, to dir: String) throws -> Void { 59 | var err: NSError? = nil 60 | copyFilesFile(url: URL(fileURLWithPath: url), toDir: dir) { (error) in 61 | err = error as NSError? 62 | } 63 | if let error = err { 64 | throw error 65 | } 66 | } 67 | 68 | fileprivate static func copyFilesFile(url: URL, toDir: String, completion: (Error?) -> Void) -> Void { 69 | var error: NSError? 70 | let fileCoordinator = NSFileCoordinator.init() 71 | if url.startAccessingSecurityScopedResource() { 72 | url.stopAccessingSecurityScopedResource() 73 | return 74 | } 75 | fileCoordinator.coordinate( 76 | readingItemAt: url, 77 | options: .withoutChanges, 78 | error: &error, 79 | byAccessor: { (url) in 80 | debugPrint("待拷贝文件:\(url.absoluteString)") 81 | let toUrl = URL.init(fileURLWithPath: toDir.appending("/\(url.lastPathComponent)")) 82 | do { 83 | try FileManager.default.copyItem(at: url, to: toUrl) 84 | completion(nil) 85 | } catch let error { 86 | debugPrint("拷贝文件失败:\(error)") 87 | completion(error) 88 | } 89 | debugPrint("stopAccessingSecurityScopedResource:\(url.stopAccessingSecurityScopedResource())") 90 | }) 91 | if let err = error { 92 | debugPrint("拷贝files中文件出错:\(err)") 93 | debugPrint("stopAccessingSecurityScopedResource:\(url.stopAccessingSecurityScopedResource())") 94 | completion(err) 95 | } 96 | } 97 | } 98 | 99 | extension FileManager { 100 | 101 | func createDiractoryIfNeeded(at url: URL) throws { 102 | var isDirectory: ObjCBool = false 103 | if fileExists(atPath: url.path, isDirectory: &isDirectory) { 104 | if !isDirectory.boolValue { 105 | // let this throw a `file exists error` 106 | try createDirectory(at: url, withIntermediateDirectories: true) 107 | } 108 | } else { 109 | try createDirectory(at: url, withIntermediateDirectories: true) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/ConstValue&Function/PublicFunctions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // publicFunctions.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/11/30. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Dispatch 11 | 12 | /** 13 | 代码区块区分. 14 | 15 | - parameter name: 区块功能描述 16 | - parameter closure: 执行功能 17 | */ 18 | public func scope(_ name: String, closure: () -> ()) -> Void { 19 | closure() 20 | } 21 | 22 | public func guardOnMainQueue(sync: Bool = false, _ block: @escaping () -> ()) -> Void { 23 | if Thread.current.isMainThread { 24 | block() 25 | } else { 26 | sync ? DispatchQueue.main.sync(execute: block) : DispatchQueue.main.async(execute: block) 27 | } 28 | } 29 | 30 | public protocol DataConvertible { 31 | func data() throws -> Data 32 | static func object(from data: Data) throws -> Self 33 | } 34 | 35 | public extension DataConvertible where Self: Codable { 36 | func data() throws -> Data { 37 | return try JSONEncoder().encode(self) 38 | } 39 | 40 | static func object(from data: Data) throws -> Self { 41 | return try JSONDecoder().decode(Self.self, from: data) 42 | } 43 | } 44 | 45 | public final class WeakObject { 46 | public weak var reference: T? 47 | 48 | public init(referencing: T) { 49 | self.reference = referencing 50 | } 51 | 52 | public var isEmpty: Bool { reference == nil } 53 | } 54 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/ConstValue&Function/UIRelevance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIRelevance.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/11/30. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// UI相关类,可以用extension添加App UI属性 12 | final public class UI { 13 | /* 缓存相关属性,减少调用方法的次数 */ 14 | fileprivate static let screenHeight: CGFloat = UIScreen.main.bounds.size.height 15 | fileprivate static let screenWidth: CGFloat = UIScreen.main.bounds.size.width 16 | 17 | public class var SCREEN_HEIGHT: CGFloat { 18 | get { 19 | return screenHeight 20 | } 21 | } 22 | public class var SCREEN_WIDTH: CGFloat { 23 | get { 24 | return screenWidth 25 | } 26 | } 27 | /// For changing the default font. 28 | public static var _defaultFont: UIFont = UIFont.systemFont(ofSize: 12) 29 | /** 30 | Default font of application 31 | 32 | - parameter size: Size of the font 33 | */ 34 | public class func defaultFont(ofSize size: CGFloat) -> UIFont { 35 | return self._defaultFont.withSize(size) 36 | } 37 | } 38 | 39 | 40 | public protocol ReusableView { } 41 | public protocol NibLoadableView: AnyObject { } 42 | 43 | extension ReusableView where Self: UITableViewCell { 44 | public static var reusedIdentifier: String { 45 | return String(describing: self.self) 46 | } 47 | } 48 | 49 | extension NibLoadableView where Self: UIView { 50 | public static var NibName: String { 51 | return String(describing: self.self) 52 | } 53 | } 54 | 55 | extension UITableView { 56 | public func registerNib(_: T.Type) -> Void where T: ReusableView, T: NibLoadableView { 57 | let nib = UINib(nibName: T.NibName, bundle: nil) 58 | self.register(nib, forCellReuseIdentifier: T.reusedIdentifier) 59 | } 60 | 61 | public func register(_ type: T.Type) -> Void where T: ReusableView, T: UITableViewCell { 62 | self.register(type, forCellReuseIdentifier: T.reusedIdentifier) 63 | } 64 | 65 | public func dequeReusableCell(forIndexPath ip: IndexPath) -> T where T: ReusableView { 66 | guard let cell = self.dequeueReusableCell(withIdentifier: T.reusedIdentifier, for: ip) as? T else { 67 | fatalError("couldn't deque cell with identifier: \(T.reusedIdentifier)") 68 | } 69 | return cell 70 | } 71 | } 72 | 73 | public extension UIApplication { 74 | 75 | /// Get all windows from all scenes. 76 | static var availableWindows: [UIWindow] { 77 | if #available(iOS 13, *) { 78 | return UIApplication.shared.connectedScenes.flatMap { scene -> [UIWindow] in 79 | guard let windowScene = scene as? UIWindowScene else { return [] } 80 | return windowScene.windows 81 | } 82 | } else { 83 | return UIApplication.shared.windows 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Extensions/CollectionType+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionType+Extension.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/4. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Collection where Iterator.Element: Comparable { 12 | 13 | // var isSorted: Bool { 14 | // 15 | // if self.count < 3 { return true } 16 | // 17 | // var compare: Int? 18 | // 19 | // for i in self.startIndex ..< self.endIndex { 20 | // if compare == nil { 21 | // if self[i] > self[Collection.index(after: i)] { compare = 0 } 22 | // else { compare = 1 } 23 | // continue 24 | // } 25 | // if compare! == 1 { 26 | // if self[i] > self[Collection.index(after: i)] { return false } 27 | // } else { 28 | // if self[i] < self[Collection.index(after: i)] { return false } 29 | // } 30 | // } 31 | // return true 32 | // } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Extensions/Dictionary+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+Extension.swift 3 | // FileMail 4 | // 5 | // Created by jiangkelan on 24/05/2017. 6 | // Copyright © 2017 xlvip. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Dictionary { 12 | 13 | static func +(lhs: Dictionary, rhs: Dictionary) -> Dictionary { 14 | var rst = lhs 15 | for pair in rhs { 16 | rst.updateValue(pair.value, forKey: pair.key) 17 | } 18 | return rst 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Extensions/Double+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+Extension.swift 3 | // FileMail 4 | // 5 | // Created by jiangkelan on 22/05/2017. 6 | // Copyright © 2017 xlvip. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Double { 12 | /// Rounds the double to decimal places value 13 | func roundTo(places: Int) -> Double { 14 | let divisor = pow(10.0, Double(places)) 15 | return (self * divisor).rounded() / divisor 16 | } 17 | 18 | /* 19 | // Formate to file size string with KB, MB, GB 20 | func fileSizeFormate() -> String { 21 | var totalBytes = self / 1024.0 22 | var multiplyFactor = 0 23 | let tokens = ["KB","MB","GB","TB"] 24 | while totalBytes > 1024 { 25 | totalBytes /= 1024.0 26 | multiplyFactor += 1 27 | } 28 | return "\(String(format:"%.2f",totalBytes))\(tokens[multiplyFactor])" 29 | } 30 | */ 31 | } 32 | 33 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Extensions/NSDate+Extentsions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSDate+Extentsions.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 5/16/16. 6 | // Copyright © 2016 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | var dateFormatter: DateFormatter? = nil 12 | 13 | extension Date { 14 | 15 | public var year: Int { 16 | get { 17 | return (Calendar.current as NSCalendar).component(NSCalendar.Unit.year, from: self) 18 | } 19 | } 20 | public var month: Int { 21 | get { 22 | return (Calendar.current as NSCalendar).component(NSCalendar.Unit.month, from: self) 23 | } 24 | } 25 | public var day: Int { 26 | get { 27 | return (Calendar.current as NSCalendar).component(NSCalendar.Unit.day, from: self) 28 | } 29 | } 30 | 31 | public var weekday: Int { 32 | get { 33 | return (Calendar.current as NSCalendar).ordinality(of: NSCalendar.Unit.weekday, in: NSCalendar.Unit.weekOfYear, for: self) 34 | } 35 | } 36 | 37 | public var weekOfMonth: Int { 38 | get { 39 | return (Calendar.current as NSCalendar).component(NSCalendar.Unit.weekOfMonth, from: self) 40 | } 41 | } 42 | 43 | public func offsetMonth(_ offset: Int) -> Date { 44 | guard offset != 0 else { return self } 45 | var comps = DateComponents() 46 | comps.month = offset 47 | return (Calendar.current as NSCalendar).date(byAdding: comps, to: self, options: NSCalendar.Options.wrapComponents)! 48 | } 49 | 50 | public func offsetDay(_ offset: Int) -> Date { 51 | guard offset != 0 else { return self } 52 | var comps = DateComponents() 53 | comps.day = offset 54 | return (Calendar.current as NSCalendar).date(byAdding: comps, to: self, options: NSCalendar.Options.wrapComponents)! 55 | } 56 | 57 | public func offsetWeek(_ offset: Int) -> Date { 58 | guard offset != 0 else { return self } 59 | var comps = DateComponents() 60 | comps.weekOfYear = offset 61 | return (Calendar.current as NSCalendar).date(byAdding: comps, to: self, options: NSCalendar.Options.wrapComponents)! 62 | } 63 | 64 | public static func numberOfDaysInMonth(_ date: Date) -> Int { 65 | return (Calendar.current as NSCalendar).range(of: NSCalendar.Unit.day, in: NSCalendar.Unit.month, for: date).length 66 | } 67 | 68 | public var firstWeekdayInMonth: Int { 69 | get { 70 | var comps = (Calendar.current as NSCalendar).components([NSCalendar.Unit.year, NSCalendar.Unit.month], from: self) 71 | comps.day = 1 72 | if let newDate = Calendar.current.date(from: comps) { 73 | return newDate.weekday 74 | } 75 | assert(false, "NSDate.firstWeekdayInMonth error, can't generate the first day in month") 76 | return 0 77 | } 78 | } 79 | 80 | public func isMonthEqualToDate(_ date: Date) -> Bool { 81 | let cal = Calendar.current 82 | let selfComps = (cal as NSCalendar).components([NSCalendar.Unit.year, .month], from: self) 83 | let other = (cal as NSCalendar).components([.year, .month], from: date) 84 | 85 | return selfComps.year == other.year && selfComps.month == other.month 86 | } 87 | 88 | public func isDayEqualToDate(_ date: Date) -> Bool { 89 | let cal = Calendar.current 90 | let selfComps = (cal as NSCalendar).components([NSCalendar.Unit.year, .month, .day], from: self) 91 | let other = (cal as NSCalendar).components([.year, .month, .day], from: date) 92 | 93 | return selfComps.year == other.year && selfComps.month == other.month && selfComps.day == other.day 94 | } 95 | 96 | public func isWeekEqualToDate(_ date: Date) -> Bool { 97 | let cal = Calendar.current 98 | let selfComps = (cal as NSCalendar).components([NSCalendar.Unit.year, .weekOfYear, .yearForWeekOfYear], from: self) 99 | let other = (cal as NSCalendar).components([.year, .month, .yearForWeekOfYear], from: date) 100 | 101 | return selfComps.yearForWeekOfYear == other.yearForWeekOfYear && selfComps.weekOfYear == other.weekOfYear 102 | } 103 | 104 | public var originTimeOfDay: TimeInterval { 105 | get { 106 | let comps = (Calendar.current as NSCalendar).components([.year, .month, .day], from: self) 107 | return Calendar.current.date(from: comps)!.timeIntervalSince1970 108 | } 109 | } 110 | 111 | public func stringWithFormat(_ format: String) -> String { 112 | 113 | if dateFormatter == nil { dateFormatter = DateFormatter() } 114 | 115 | let formatter = dateFormatter! 116 | formatter.dateFormat = format 117 | return formatter.string(from: self) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/2. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | /** 14 | Be able to use range to get substring, e.x.: "abced"[0..<1] = "a" 15 | */ 16 | public subscript (r: Range) -> String { 17 | get { 18 | let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound) 19 | let endIndex = self.index(startIndex, offsetBy: r.upperBound - r.lowerBound) 20 | 21 | let substring = self.prefix(upTo: endIndex).suffix(from: startIndex) 22 | return String.init(substring) 23 | } 24 | } 25 | /** 26 | Be able to use range to get substring, e.x.: "abced"[0...1] = "ab" 27 | */ 28 | public subscript (r: ClosedRange) -> String { 29 | get { 30 | let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound) 31 | let endIndex = self.index(startIndex, offsetBy: r.upperBound - r.lowerBound) 32 | 33 | let substring = self.prefix(through: endIndex).suffix(from: startIndex) 34 | return String.init(substring) 35 | } 36 | } 37 | /// length of String, number of characters -- Swift 2.0 38 | public var length: Int { 39 | return self.count 40 | } 41 | 42 | /* 43 | /// MD5 of string, need to #import in bridge file 44 | var MD5: String { 45 | 46 | func another() { 47 | let cString = self.cString(using: String.Encoding.utf8) 48 | let length = CUnsignedInt( 49 | self.lengthOfBytes(using: String.Encoding.utf8) 50 | ) 51 | let result = UnsafeMutablePointer.allocate( 52 | capacity: Int(CC_MD5_DIGEST_LENGTH) 53 | ) 54 | 55 | CC_MD5(cString!, length, result) 56 | 57 | return String(format: 58 | "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", 59 | result[0], result[1], result[2], result[3], 60 | result[4], result[5], result[6], result[7], 61 | result[8], result[9], result[10], result[11], 62 | result[12], result[13], result[14], result[15]) 63 | } 64 | 65 | let length = Int(CC_MD5_DIGEST_LENGTH) 66 | var digest = [UInt8](repeating: 0, count: length) 67 | 68 | if let d = self.data(using: String.Encoding.utf8) { 69 | _ = d.withUnsafeBytes { (body: UnsafePointer) in 70 | CC_MD5(body, CC_LONG(d.count), &digest) 71 | } 72 | } 73 | 74 | return (0.. UIColor { 14 | 15 | let hexString = hex.replacingOccurrences(of: "#", with: "") 16 | let scanner = Scanner(string: hexString) 17 | 18 | var rgbValue: UInt64 = 0 19 | scanner.scanHexInt64(&rgbValue) 20 | 21 | return UIColor( 22 | red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, 23 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, 24 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0, 25 | alpha: 1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Extensions/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // swiftArchitecture 4 | // 5 | // Created by jiangkelan on 5/12/16. 6 | // Copyright © 2016 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIView { 13 | 14 | public var x: CGFloat { 15 | set { 16 | var frame = self.frame 17 | frame.origin.x = newValue 18 | self.frame = frame 19 | } 20 | get { 21 | return frame.origin.x 22 | } 23 | } 24 | 25 | public var y: CGFloat { 26 | set { 27 | var frame = self.frame 28 | frame.origin.y = newValue 29 | self.frame = frame 30 | } 31 | get { 32 | return frame.origin.y 33 | } 34 | } 35 | 36 | public var height: CGFloat { 37 | get { 38 | return frame.size.height 39 | } 40 | set { 41 | var f = frame 42 | f.size.height = newValue 43 | frame = f 44 | } 45 | } 46 | 47 | public var width: CGFloat { 48 | get { 49 | return frame.size.width 50 | } 51 | set { 52 | var f = frame 53 | f.size.width = newValue 54 | frame = f 55 | } 56 | } 57 | } 58 | 59 | 60 | public func CGRectGetCenter(_ rect: CGRect) -> CGPoint { 61 | return CGPoint(x: rect.midX, y: rect.midY) 62 | } 63 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Kits/Atomic/RwQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RwQueue.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/6/16. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The `RwQueue` is only be used in a simple way because it's a heavy work when enqueue actions and switch the threads. 11 | /// So if `RwQueue` contains huge amount of work to do, it may leads to unexpected behavior and bad performance. 12 | public class RwQueue { 13 | 14 | public let queue: DispatchQueue 15 | 16 | public init(qos: DispatchQoS = .default, label: String? = nil) { 17 | self.queue = .init(label: "com.mioke.swiftyarchitecture.rwqueue" + (!label.isEmpty ? ".\(label!)" : ""), 18 | qos: qos, 19 | attributes: .concurrent) 20 | } 21 | 22 | public func read(action: @escaping () -> ()) { 23 | queue.async(execute: action) 24 | } 25 | 26 | public func write(action: @escaping () -> ()) { 27 | queue.async(flags: .barrier, execute: action) 28 | } 29 | 30 | public func syncRead(_ closure: () throws -> T) rethrows -> T { 31 | return try queue.sync(execute: closure) 32 | } 33 | 34 | public func syncWrite(_ closure: () throws -> T) rethrows -> T { 35 | return try queue.sync(flags: .barrier, execute: closure) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Kits/SystemLog/LogInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogInterface.swift 3 | // Alamofire 4 | // 5 | // Created by KelanJiang on 2022/6/7. 6 | // 7 | 8 | import Foundation 9 | import os 10 | 11 | public enum LogLevel { 12 | case error, info, verbose, debug 13 | } 14 | 15 | public class KitLogger { 16 | public static let instance: KitLogger = .init() 17 | private init() {} 18 | 19 | public typealias Event = (level: LogLevel, message: String, file: String, line: Int) 20 | 21 | public var events: ((Event) -> Void)? 22 | 23 | public static func log(level: LogLevel, message: String, file: String = #file, line: Int = #line) { 24 | if let events = instance.events { 25 | events((level: level, message: message, file: file, line: line)) 26 | } else { 27 | NSLog("[\(level.tag())] \(URL(fileURLWithPath: file).lastPathComponent):\(line) ~> \(message)") 28 | } 29 | } 30 | } 31 | 32 | private extension LogLevel { 33 | func tag() -> String { 34 | switch self { 35 | case .debug: 36 | return "💉" 37 | case .info: 38 | return "📄" 39 | case .verbose: 40 | return "🛰" 41 | case .error: 42 | return "🧯" 43 | } 44 | } 45 | } 46 | 47 | public extension KitLogger { 48 | static func debug(_ message: String = #function, file: String = #file, line: Int = #line) { 49 | log(level: .debug, message: message, file: file, line: line) 50 | } 51 | 52 | static func info(_ message: String = #function, file: String = #file, line: Int = #line) { 53 | log(level: .info, message: message, file: file, line: line) 54 | } 55 | 56 | static func verbose(_ message: String = #function, file: String = #file, line: Int = #line) { 57 | log(level: .verbose, message: message, file: file, line: line) 58 | } 59 | 60 | static func error(_ message: String = #function, file: String = #file, line: Int = #line) { 61 | log(level: .error, message: message, file: file, line: line) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Assistance/Kits/TextInputLimiter/TextInputLimiter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextInputLimiter.swift 3 | // FileMail 4 | // 5 | // Created by jiangkelan on 20/05/2017. 6 | // Copyright © 2017 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol Inputer: AnyObject { 12 | } 13 | 14 | extension UITextField: Inputer { } 15 | extension UITextView: Inputer { } 16 | 17 | public class InputLimiter: NSObject { 18 | 19 | public var maxCount: Int 20 | public var reachMaxHandler: (() -> Void)? 21 | public var contentDidChanged: ((String?) -> Void)? 22 | 23 | weak var inputer: T? 24 | 25 | public init(with inputer: T, maxCount: Int) { 26 | self.maxCount = maxCount 27 | self.inputer = inputer 28 | 29 | super.init() 30 | } 31 | 32 | @objc fileprivate func contentChanged() -> Void { 33 | guard let inputer = inputer else { 34 | return 35 | } 36 | if let textfield = inputer as? UITextField { 37 | self._inputerDidChanged(textfield) 38 | } else if let textView = inputer as? UITextView { 39 | self._inputerDidChanged(textView) 40 | } 41 | } 42 | 43 | // text field 44 | fileprivate func _inputerDidChanged(_ inputer: UITextField) -> Void { 45 | 46 | guard let text = inputer.text else { 47 | return 48 | } 49 | 50 | if let selected = inputer.markedTextRange, 51 | let _ = inputer.position(from: selected.start, offset: 0) { 52 | 53 | } else { 54 | if text.length > self.maxCount { 55 | inputer.text = text[0.. Void { 64 | 65 | guard let text = inputer.text else { 66 | return 67 | } 68 | 69 | if let selected = inputer.markedTextRange, 70 | let _ = inputer.position(from: selected.start, offset: 0) { 71 | 72 | } else { 73 | if text.length > self.maxCount { 74 | // let rangeIndex = (text as NSString).rangeOfComposedCharacterSequence(at: self.maxCount) 75 | // if rangeIndex.length == 1 { 76 | // inputer.text = text[0.. Void { 91 | inputer?.addTarget(self, action: #selector(contentChanged), for: .editingChanged) 92 | } 93 | public func unload() -> Void { 94 | inputer?.removeTarget(self, action: #selector(contentChanged), for: .editingChanged) 95 | } 96 | } 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Componentize/Common/Consts+Componentize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Consts.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/6/2. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Consts { 11 | static let componentizeDomain = "com.mioke.swiftyarchitecture.componentize" 12 | } 13 | 14 | extension KitErrors { 15 | static var graphCycleInfo: Info { 16 | .init(code: .graphCycle, message: "Detacted graph circle.") 17 | } 18 | static let graphCycle: Error = error(domain: Consts.componentizeDomain, info: KitErrors.graphCycleInfo) 19 | } 20 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Componentize/Module/SessionModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionModule.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by Mioke Klein on 2022/1/8. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol SessionModuleProtocol: ModuleProtocol { 11 | static func sessionDidAuthenticate(with userID: String) 12 | static func sessionDidDeauthenticate(with userID: String) 13 | } 14 | 15 | public struct UserSession { 16 | public let userID: String 17 | } 18 | 19 | public extension ModuleManager { 20 | func beginUserSession(with userID: String) { 21 | session = UserSession(userID: userID) 22 | moduleMap.values 23 | .compactMap { $0 as? SessionModuleProtocol.Type } 24 | .forEach { $0.sessionDidAuthenticate(with: userID) } 25 | } 26 | 27 | func endUserSession(with userID: String) { 28 | session = nil 29 | moduleMap.values 30 | .compactMap { $0 as? SessionModuleProtocol.Type } 31 | .forEach { $0.sessionDidDeauthenticate(with: userID) } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Componentize/Navigation/LinkParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkParser.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2023/4/6. 6 | // 7 | 8 | import Foundation 9 | 10 | /// @discussion: should provide the configuration key in url string? 11 | public struct ConfigKeys { 12 | public static let presentationMode: String = "_pm" 13 | public static let animated: String = "_a" 14 | 15 | public enum PresentationModeValue: String { 16 | case push = "push" 17 | case present = "present" 18 | } 19 | } 20 | 21 | struct LinkParser { 22 | 23 | enum ParseError: Error { 24 | case cannotGenerateUrl 25 | case notCompatibleToConfig 26 | } 27 | 28 | var externalLinkScheme: String 29 | var inAppLinkScheme: String 30 | var host: String 31 | 32 | func parse(urlString: String) throws -> NavigationURL { 33 | guard let navigation = NavigationURL(from: urlString) else { 34 | throw ParseError.cannotGenerateUrl 35 | } 36 | try validate(url: navigation) 37 | return navigation 38 | } 39 | 40 | func validate(url: NavigationURL) throws { 41 | guard url.scheme == inAppLinkScheme || url.scheme == externalLinkScheme, 42 | url.host == host 43 | else { 44 | throw ParseError.notCompatibleToConfig 45 | } 46 | } 47 | 48 | func isExternalLink(url: NavigationURL) -> Bool { 49 | return url.scheme == externalLinkScheme 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Componentize/Navigation/Navigation+PageGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Navigation+PageGroup.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2023/4/7. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Navigation { 11 | 12 | func validatePageGroupFunction() throws { 13 | if !enablePageGroup { 14 | throw NavigationError.pageGroupDisabled 15 | } 16 | } 17 | 18 | public func popCurrentPageGroup(animated: Bool) throws -> Void { 19 | try validatePageGroupFunction() 20 | guard let current = pageGroupStack.pop(), let root = current.beginIndex else { 21 | return 22 | } 23 | switch current.presentationMode { 24 | case .present(style: _): 25 | root.dismiss(animated: animated) 26 | case .push: 27 | guard let nav = root.navigationController, 28 | let index = nav.viewControllers.firstIndex(of: root), 29 | index > 0 30 | else { return } 31 | root.navigationController?.popToViewController(nav.viewControllers[index - 1], animated: animated) 32 | } 33 | } 34 | 35 | public func pop(to pageGroup: Navigation.PageGroup) throws { 36 | try validatePageGroupFunction() 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Componentize/Navigation/NavigationUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationUtils.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2023/4/4. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension URL { 12 | var getHost: String? { 13 | if #available(iOS 16.0, *) { 14 | return self.host() 15 | } else { 16 | return self.host 17 | } 18 | } 19 | 20 | var getPath: String { 21 | if #available(iOS 16.0, *) { 22 | return self.path() 23 | } else { 24 | return self.path 25 | } 26 | } 27 | } 28 | 29 | extension CharacterSet { 30 | /// Creates a CharacterSet from RFC 3986 allowed characters. 31 | /// 32 | /// RFC 3986 states that the following characters are "reserved" characters. 33 | /// 34 | /// - General Delimiters: ":", "#", "[", "]", "@", "?", "/" 35 | /// - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" 36 | /// 37 | /// In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow 38 | /// query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/" 39 | /// should be percent-escaped in the query string. 40 | public static let fixedURLQueryAllowed: CharacterSet = { 41 | let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 42 | let subDelimitersToEncode = "!$&'()*+,;=" 43 | let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") 44 | 45 | return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters) 46 | }() 47 | } 48 | 49 | func escape(_ string: String) -> String { 50 | return string.addingPercentEncoding(withAllowedCharacters: .fixedURLQueryAllowed) ?? string 51 | } 52 | 53 | class NavigationUtils { 54 | static func topPresentingViewController() -> UIViewController? { 55 | guard let window = applicationWindow(), var controller = window.rootViewController else { 56 | return nil 57 | } 58 | while let presenting = controller.presentedViewController, !presenting.isBeingDismissed { 59 | controller = presenting 60 | } 61 | return controller 62 | } 63 | 64 | static func frontmostViewController() -> UIViewController? { 65 | guard let controller = topPresentingViewController() else { return nil } 66 | 67 | func _frontmost(of vc: UIViewController) -> UIViewController? { 68 | if let controller = vc as? UINavigationController { 69 | return controller.topViewController ?? controller 70 | } else if let controller = vc as? UITabBarController, let selected = controller.selectedViewController { 71 | return _frontmost(of: selected) 72 | } else { 73 | return vc 74 | } 75 | } 76 | return _frontmost(of: controller) 77 | } 78 | 79 | static func applicationWindow() -> UIWindow? { 80 | if #available(iOS 15.0, *), 81 | let windowScene = UIApplication.shared.connectedScenes.first(where: { $0 as? UIWindowScene != nil }) as? UIWindowScene, 82 | let key = windowScene.keyWindow { 83 | return key 84 | } 85 | return UIApplication.shared.delegate?.window ?? nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Componentize/Navigation/README-Navigation.md: -------------------------------------------------------------------------------- 1 | # Navigation 2 | 3 | There're some edge cases of the `Navigation` situations. 4 | 5 | ### Internal Link 6 | 7 | When dealing with internal links, the principle of the process logic is that we do additional logics as less as possible. 8 | 9 | - When current view controller hasn't got a navigation controller, it will generate an error when navigation want to push a new view controller. 10 | - When navigation want to present a new view controller and the controller is not a navigation controller, we don't automatically add one navigation controller for it. 11 | 12 | ### Universal Link 13 | 14 | When universal link doesn't explict mark the presentation mode: 15 | 16 | - When current view controller hasn't got a navigation controller, the translator will mark the presentation mode to present and add one navigation for it when the target generate a non-navigation controller. 17 | 18 | 19 | ### Other cases 20 | 21 | - When calling `navigate(to urlString: String, configuration: Navigation.Configuration)`, maybe `urlString` contains some configuration keys. We always use the configuration in the URL string and overwrite them into the configuration passed in the function varible. 22 | - 23 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/API/API+Concurrency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API+Concurrency.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2024/1/3. 6 | // 7 | 8 | import Foundation 9 | #if canImport(_Concurrency) 10 | 11 | import _Concurrency 12 | 13 | public extension API { 14 | 15 | /// [Concurrency API] Send a request of GET or POST methods. 16 | /// - Parameter params: The request's parameters. 17 | /// - Returns: The request's success result model. 18 | func request(with params: T.RequestParam?) async throws -> T.ResultType { 19 | return try await withCheckedThrowingContinuation { continuation in 20 | sendRequest(with: params).response { api, data, error in 21 | if let error { continuation.resume(throwing: error) } 22 | if let data { continuation.resume(returning: data) } 23 | } 24 | } 25 | } 26 | 27 | } 28 | 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/API/ApiDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiDelegate.swift 3 | // swiftArchitecture 4 | // 5 | // Created by jiangkelan on 05/09/2017. 6 | // Copyright © 2017 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public typealias ApiDelegateResponse = (_ api: API, _ data: T.ResultType?, _ error: NSError?) -> Void 12 | 13 | open class ApiDelegate: NSObject { 14 | 15 | private var response: ApiDelegateResponse? 16 | 17 | open func response(_ resp: @escaping ApiDelegateResponse) -> Void { 18 | self.response = resp 19 | } 20 | } 21 | 22 | extension ApiDelegate: ApiCallbackProtocol { 23 | 24 | public typealias APIType = T 25 | 26 | public func API(_ api: API, finishedWithResult result: T.ResultType) { 27 | self.response?(api, result, nil) 28 | } 29 | 30 | public func API(_ api: API, failedWithError error: NSError) { 31 | self.response?(api, nil, error) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/API/ApiRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/5/27. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ApiRouter: AnyObject { 11 | func route(api: API) throws -> Void 12 | } 13 | 14 | protocol APIRouterMokerDataSource: AnyObject { 15 | func construct(type: T.Type) -> ((T.RequestParam?) -> T.ResultType)? where T : ApiInfoProtocol 16 | } 17 | 18 | class ApiRouterContainer { 19 | 20 | static let shared: ApiRouterContainer = .init() 21 | 22 | private let builtinRouter = BuiltinApiRouter() 23 | private lazy var routerMocker = { () -> ApiRouterMocker in 24 | let mocker = ApiRouterMocker() 25 | mocker.datasource = self 26 | return mocker 27 | }() 28 | 29 | private var injectedTypesMap: [String: Any] = [:] 30 | 31 | func router(withType type: T.Type) -> ApiRouter { 32 | let name = String(reflecting: type) 33 | return injectedTypesMap[name] != nil ? routerMocker : builtinRouter 34 | } 35 | 36 | func injectAPI(with type: T.Type, 37 | customize: @escaping (_ param: T.RequestParam?) -> T.ResultType) 38 | -> Void where T: ApiInfoProtocol { 39 | injectedTypesMap[String(reflecting: type)] = customize 40 | } 41 | 42 | func remove(type: T.Type) { 43 | injectedTypesMap[String(reflecting: type)] = nil 44 | } 45 | 46 | func reset() { 47 | injectedTypesMap = [:] 48 | } 49 | } 50 | 51 | extension ApiRouterContainer: APIRouterMokerDataSource { 52 | 53 | func construct(type: T.Type) -> ((T.RequestParam?) -> T.ResultType)? where T : ApiInfoProtocol { 54 | let name = String(reflecting: type) 55 | return injectedTypesMap[name] as? (T.RequestParam?) -> T.ResultType 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/API/ResponseSerializer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseSerializer.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2021/5/12. 6 | // 7 | 8 | import UIKit 9 | import Alamofire 10 | import ObjectMapper 11 | 12 | public typealias DataResponse = Alamofire.AFDataResponse 13 | 14 | public protocol ResponseSerializerProtocol { 15 | associatedtype SerializedObject 16 | func serialize(data: DataResponse) throws -> SerializedObject 17 | } 18 | 19 | open class ResponseSerializer: NSObject, ResponseSerializerProtocol { 20 | public func serialize(data: DataResponse) throws -> SerializedObject { 21 | throw todo_error() 22 | } 23 | } 24 | 25 | extension ResponseSerializerProtocol where SerializedObject: ApiInfoProtocol { 26 | public var api: API { 27 | return API() 28 | } 29 | } 30 | 31 | /// Placeholder for ProtocBuffer. 32 | open class ProtocResponseSerializer: ResponseSerializer { 33 | 34 | public override func serialize(data: DataResponse) throws -> SerializedObject { 35 | throw todo_error() 36 | } 37 | } 38 | 39 | /// A wrapper of new type `DecodableResponseSerializer` in Alamofire provided from 5.0 40 | final public class JSONCodableResponseSerializer: ResponseSerializer { 41 | public override func serialize(data: DataResponse) throws -> SerializedObject { 42 | let serializer = Alamofire.DecodableResponseSerializer() 43 | let result = try serializer.serialize( 44 | request: data.request, 45 | response: data.response, 46 | data: data.data, 47 | error: data.error) 48 | return result 49 | } 50 | } 51 | 52 | /// A serializer for models conformed to `Mappable`. 53 | final public class JSONMappableResponseSerializer: ResponseSerializer { 54 | public override func serialize(data: DataResponse) throws -> SerializedObject { 55 | if let resp = data.response, resp.statusCode != 200 { 56 | throw todo_error() 57 | } 58 | if let error = data.error { 59 | throw error 60 | } 61 | guard let data = data.data, 62 | let jsonString = String(data: data, encoding: .utf8), 63 | let result = T.init(JSONString: jsonString, context: nil) 64 | else { 65 | throw todo_error() 66 | } 67 | return result 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/Generators/Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Errors.swift 3 | // swiftArchitecture 4 | // 5 | // Created by jiangkelan on 16/06/2017. 6 | // Copyright © 2017 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension KitErrors { 12 | 13 | static var responseErrorInfo: Info { 14 | return .init(code: .responseError, message: "Unexpected response value") 15 | } 16 | 17 | static var apiConstructionInfo: Info { 18 | return .init(code: .apiConstructionFailed, message: "Unexpected API construction result") 19 | } 20 | 21 | // Errors 22 | 23 | static var responseError: NSError { 24 | return error(domain: Consts.networkingDomain, info: responseErrorInfo) 25 | } 26 | 27 | static var apiConstructionError: NSError { 28 | return error(domain: Consts.networkingDomain, info: apiConstructionInfo) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/Generators/KMRequestGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KMRequestGenerator.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/7. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Alamofire 11 | 12 | /// Generator of reuqest 13 | public class KMRequestGenerator: NSObject { 14 | 15 | /// Session configuration for default HTTP request 16 | private static var defaultConfiguration: URLSessionConfiguration { 17 | let configuration = URLSessionConfiguration.af.default 18 | return configuration 19 | } 20 | 21 | /// Session configurtion for default HTTPS request 22 | private static var httpsConfiguration: URLSessionConfiguration { 23 | let configuration = URLSessionConfiguration.af.default 24 | return configuration 25 | } 26 | 27 | /// Default session manager for HTTP requests 28 | public static var defaultManager: Alamofire.Session = { 29 | return Alamofire.Session(configuration: defaultConfiguration) 30 | }() 31 | 32 | /// Default session manager for HTTPs request, you can change it for customized 33 | public static var httpsManager: Alamofire.Session = { 34 | return Alamofire.Session(configuration: httpsConfiguration) 35 | }() 36 | 37 | public class func generateRequest( 38 | withApi api: API, 39 | params: T.RequestParam?) throws -> DataRequest { 40 | 41 | let manager = T.server.supportHTTPS ? httpsManager : defaultManager 42 | manager.session.configuration.timeoutIntervalForRequest = api.timeoutInterval 43 | 44 | var parameters: Parameters? = nil 45 | if let params = params { 46 | let data = try JSONEncoder().encode(params) 47 | parameters = try JSONSerialization.jsonObject(with: data, options: []) as? Parameters 48 | } 49 | 50 | let req = manager.request(api.apiURL, 51 | method: T.httpMethod, 52 | parameters: parameters, 53 | encoding: T.encoding, 54 | headers: api.HTTPHeaders) 55 | 56 | // TODO: Do additional configuration or signature etc. 57 | KitLogger.log( 58 | level: .info, 59 | message: "Send request:\n\tRequest Info:\(req.description)\n\tRequest Headers:\(req.request?.allHTTPHeaderFields ?? [:])\n\tParam:\(parameters ?? [:])") 60 | 61 | return req 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/Protocols/ApiCallbackProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCallbackProtocol.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/7. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Describe callback of an api. 12 | public protocol ApiCallbackProtocol: NSObjectProtocol { 13 | 14 | associatedtype APIType: ApiInfoProtocol 15 | 16 | /// Success callback 17 | /// 18 | /// - Parameters: 19 | /// - apiManager: api manager which finished 20 | /// - result: result of response 21 | func API(_ api: API, finishedWithResult result: APIType.ResultType) -> Void 22 | 23 | /// If API returns error or undefined exception, will call this method in delegate. 24 | /// - ATTENTION: **DON'T** try to solve problems here, only do the reflection after error occured 25 | /// - Parameters: 26 | /// - api: API 27 | /// - error: The error occured 28 | func API(_ api: API, failedWithError error: NSError) -> Void 29 | } 30 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/Protocols/ApiInfoProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiInfoProtocol.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/7. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | /** 13 | * Protocol that descripe what infomation should API provide, 14 | - attention: `apiVersion` and `apiName` __shouldn't begin and end with '/'__. Just like "v2", "user/login" is good. If you don't have a versioned-API, return an empty string. 15 | */ 16 | public protocol ApiInfoProtocol: NSObjectProtocol { 17 | 18 | /// The version of api 19 | static var apiVersion: String { get } 20 | 21 | /// The name of api 22 | static var apiName: String { get } 23 | 24 | /// The HTTP method of this api 25 | static var httpMethod: Alamofire.HTTPMethod { get } 26 | 27 | /// The server for this api to request 28 | static var server: Server { get } 29 | 30 | /// Max counts of auto retry machanism, you can return different counts for different error. 31 | /// 32 | /// - Parameter code: Error's code 33 | /// - Returns: Counts of auto retry machanism 34 | static func autoRetryMaxCount(withErrorCode code: Int) -> Int? 35 | 36 | /// The time of retry interval between two request 37 | /// 38 | /// - Parameter code: Error's code 39 | /// - Returns: Time interval for the error 40 | static func retryTimeInterval(withErrorCode code: Int) -> UInt64? 41 | 42 | /// HTTP headers of request 43 | /// 44 | /// - Returns: HTTP headers 45 | static func headers() -> Alamofire.HTTPHeaders? 46 | 47 | /// Encoding function of api's parameters, default is `Alamofire.URLEncoding.default`(aka `methodDependent`) 48 | static var encoding: Alamofire.ParameterEncoding { get } 49 | 50 | associatedtype RequestParam: Codable 51 | 52 | associatedtype ResultType 53 | /// Response serializer type, default is .JSON, that request will call `responseJSON` method when get response. 54 | static var responseSerializer: ResponseSerializer { get } 55 | } 56 | 57 | extension ApiInfoProtocol { 58 | 59 | public static func autoRetryMaxCount(withErrorCode code: Int) -> Int? { 60 | return nil 61 | } 62 | 63 | public static func retryTimeInterval(withErrorCode code: Int) -> UInt64? { 64 | return nil 65 | } 66 | 67 | public static func headers() -> Alamofire.HTTPHeaders? { 68 | return nil 69 | } 70 | 71 | public static var encoding: Alamofire.ParameterEncoding { 72 | return URLEncoding.default 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Networking/Server/Server.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Server.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/7. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Server model 12 | open class Server: NSObject { 13 | 14 | /// The environment model, describe different running environment of the same server logic. 15 | public enum Environment: Hashable { 16 | case live 17 | case custom(String) 18 | } 19 | 20 | /// Initiate configuration of a server. 21 | public struct Configuration { 22 | public let liveURL: URL 23 | public var customURLs: [Server.Environment: URL] 24 | } 25 | 26 | /// The current environment option for this server. 27 | public private(set) var currentEnvironment: Server.Environment = .live { 28 | didSet { 29 | if oldValue != currentEnvironment { 30 | // need notification? 31 | } 32 | } 33 | } 34 | 35 | var configuratoin: Server.Configuration 36 | 37 | public init(configuation: Server.Configuration) { 38 | self.configuratoin = configuation 39 | } 40 | 41 | /// Get the url of current environment. 42 | public var url: URL { 43 | switch currentEnvironment { 44 | case .live: 45 | return configuratoin.liveURL 46 | case .custom(_): 47 | return configuratoin.customURLs[currentEnvironment]! 48 | } 49 | } 50 | 51 | public func `switch`(to customID: String) throws { 52 | let newEnv = Environment.custom(customID) 53 | guard let _ = configuratoin.customURLs[newEnv] else { 54 | throw todo_error() 55 | } 56 | currentEnvironment = newEnv 57 | } 58 | 59 | public func `switch`(to env: Server.Environment) throws { 60 | guard env == .live || configuratoin.customURLs[env] != nil else { 61 | throw todo_error() 62 | } 63 | currentEnvironment = env 64 | } 65 | 66 | } 67 | 68 | /// Customize data process operation of server 69 | public protocol ServerDataProcessProtocol { 70 | 71 | /// Handle the error data that may can handled uniformly. This function will be only called when the request data 72 | /// can't serialized to the type it expected. 73 | /// 74 | /// - Important: Only throw a error when the server can handle it. 75 | /// - Parameter data: The original response data. 76 | func handle(data: Data) throws -> Void 77 | } 78 | 79 | extension Server { 80 | 81 | /// Whether current environment support HTTPS, judged by the scheme of url is `https`. 82 | var supportHTTPS: Bool { 83 | self.url.scheme?.lowercased().contains("https") ?? false 84 | } 85 | } 86 | 87 | extension Server { 88 | 89 | /// Initialize method. 90 | /// - Parameters: 91 | /// - live: The server url of live environment. 92 | /// - customEnvironments: Custom environments urls. 93 | public convenience init(live: URL, customEnvironments: [Server.Environment: URL]) { 94 | let configuation: Server.Configuration = .init(liveURL: live, customURLs: customEnvironments) 95 | self.init(configuation: configuation) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Persistance/DatabaseManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseManager.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/1. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | 12 | /// Real database command executor 13 | final public class DatabaseManager: NSObject { 14 | 15 | /// Query function 16 | public class func database(_ databaseQueue: FMDatabaseQueue, query: String, withArgumentsInArray args: [Any]?) -> Array<[AnyHashable: Any]> { 17 | 18 | var rstArray = Array<[AnyHashable: Any]>() 19 | 20 | databaseQueue.inTransaction { (db: FMDatabase?, roolback: UnsafeMutablePointer?) in 21 | guard let db = db else { 22 | return 23 | } 24 | if let rst = db.executeQuery(query, withArgumentsIn: args ?? []) { 25 | while rst.next() { 26 | if let dic = rst.resultDictionary { 27 | rstArray.append(dic) 28 | } 29 | } 30 | rst.close() 31 | } 32 | } 33 | return rstArray 34 | } 35 | /// execute with dictionary parameters 36 | @discardableResult 37 | public class func database(_ databaseQueue: FMDatabaseQueue, execute: String, withArgumentsInDictionary args: [String: Any]?) -> Bool { 38 | 39 | var isSuccess = false 40 | databaseQueue.inTransaction { (db: FMDatabase?, roolback: UnsafeMutablePointer?) in 41 | guard let db = db else { 42 | return 43 | } 44 | isSuccess = db.executeUpdate(execute, withParameterDictionary: args ?? [:]) 45 | } 46 | return isSuccess 47 | } 48 | /// execute with array parameters 49 | @discardableResult 50 | public class func database(_ databaseQueue: FMDatabaseQueue, execute: String, withArgumentsInArray args: [Any]?) -> Bool { 51 | var isSuccess = false 52 | databaseQueue.inTransaction { (db: FMDatabase?, roolback: UnsafeMutablePointer?) in 53 | guard let db = db else { 54 | return 55 | } 56 | isSuccess = db.executeUpdate(execute, withArgumentsIn: args ?? []) 57 | } 58 | return isSuccess 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Persistance/DefaultDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultDatabase.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/1. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | 12 | /// Default database 13 | final public class DefaultDatabase: KMPersistanceDatabase, DatabaseManagerProtocol { 14 | 15 | public static let instance: KMPersistanceDatabase = DefaultDatabase() 16 | 17 | public var path: String 18 | public var database: FMDatabaseQueue 19 | public var databaseName: String 20 | 21 | public override init() { 22 | 23 | self.databaseName = "default.db" 24 | self.path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/" + self.databaseName 25 | self.database = FMDatabaseQueue(path: self.path) ?? FMDatabaseQueue() 26 | 27 | super.init() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Persistance/Protocols/DatabaseCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseCommand.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/10. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Database command generator. 12 | public class DatabaseCommand: NSObject { 13 | 14 | @discardableResult 15 | public class func createTable(with table: TableProtocol, inDatabase database: KMPersistanceDatabase) -> Bool { 16 | 17 | // TODO: If the table is already there, do nothing. 18 | // Now execute the sql will cause an error of "table existes" 19 | // To solve this problem, we can create a default table in every database to record what tables it has. 20 | 21 | let params = NSMutableArray() 22 | 23 | for key in table.tableColumnInfo.keys { 24 | params.add("'\(key)' \(table.tableColumnInfo[key]!)") 25 | } 26 | 27 | let sql = "create table if not exists '\(table.tableName)' (\(params.componentsJoined(by: ",")))" 28 | 29 | return database.execute(sql, withArgumentsInDictionary: nil) 30 | } 31 | 32 | public class func replaceCommand(with table: TableProtocol, record: RecordProtocol) -> String { 33 | 34 | guard let params = record.dictionaryRepresentation(in: table) else { 35 | assert(false, "DatabaseCommand REPLACE params should not be ampty") 36 | return "" 37 | } 38 | var sql = "replace into \(table.tableName) (" 39 | 40 | let content = NSMutableArray() 41 | let values = NSMutableArray() 42 | 43 | for key in params.keys { 44 | content.add("'\(key)'") 45 | values.add(":\(key)") 46 | } 47 | sql += "\(content.componentsJoined(by: ","))) values (\(values.componentsJoined(by: ",")))" 48 | 49 | print(sql) 50 | 51 | return sql 52 | } 53 | 54 | public class func queryCommand(with table: TableProtocol, select: String?, condition: DatabaseCommandCondition) -> String { 55 | 56 | let selectSql = select == nil ? "*" : "'\(select!)'" 57 | var sql = "select \(selectSql) from \(table.tableName)" 58 | condition.applyCondition(to: &sql) 59 | 60 | return sql 61 | } 62 | 63 | public class func deleteCommand(with table: TableProtocol, condition: DatabaseCommandCondition) -> String { 64 | 65 | var sql = "delete from \(table.tableName)" 66 | condition.applyCondition(to: &sql) 67 | 68 | return sql 69 | } 70 | } 71 | 72 | // MARK: - Command condition 73 | 74 | /// Database command condition object. 75 | public class DatabaseCommandCondition: NSObject { 76 | 77 | /// Where condition, like: `"uid=812, age>12"` 78 | public var whereConditions: String? 79 | // TODO: --- whereConditionsParams --- 80 | // var whereConditionsParams: [String: AnyObject]? 81 | 82 | /// For ordering result. 83 | public var orderBy: String? 84 | 85 | /// DESC or ASC 86 | public var isDESC: Bool? 87 | 88 | /// For limiting result 89 | public var limit: Int? 90 | 91 | /// For distinct result 92 | public var isDistinct: Bool? 93 | 94 | public func applyCondition(to command: inout String) { 95 | 96 | if self.whereConditions != nil/* && self.whereConditionsParams != nil */{ 97 | command.append(" where \(self.whereConditions!)") 98 | } 99 | if self.orderBy != nil { 100 | command.append(" order by \(self.orderBy!)") 101 | } 102 | if self.isDESC != nil { 103 | command.append(" \(self.isDESC! ? "DESC" : "ASC")") 104 | } 105 | if self.limit != nil { 106 | command.append(" limit \(self.limit!)") 107 | } 108 | if let isDistinct = self.isDistinct, isDistinct { 109 | command.replaceSubrange(command.index(command.startIndex, offsetBy: 6) ..< command.index(command.startIndex, offsetBy: 6), with: " distinct") 110 | } 111 | } 112 | } 113 | 114 | 115 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/RxExtension/DelayWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DelayWorker.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/6/22. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | 11 | public class DelayWorker { 12 | 13 | public enum Event { 14 | case skip 15 | case value(Swift.Result) 16 | } 17 | 18 | public static func delay( 19 | work: Observable, 20 | interval: DispatchTimeInterval, 21 | onQueue: DispatchQueue = .global()) 22 | -> Observable> { 23 | return .create { observer in 24 | let worker = { () -> Disposable in 25 | return work.subscribe { rst in 26 | observer.onNext(.value(.success(rst))) 27 | } onError: { error in 28 | observer.onNext(.value(.failure(error))) 29 | } onCompleted: { 30 | observer.onCompleted() 31 | } 32 | } 33 | 34 | if interval == .seconds(0) { 35 | // run this worker synchronizely. 36 | return worker() 37 | } else { 38 | observer.onNext(.skip) 39 | let disposable = CompositeDisposable.init() 40 | DispatchQueue.global().asyncAfter(deadline: .now() + interval) { 41 | _ = disposable.insert(worker()) 42 | } 43 | return disposable 44 | } 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/RxExtension/ProducerQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProducerQueue.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/6/16. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | 11 | /// Enqueue *cold signals*, start them one by one, start one after the previous one sent completed. 12 | open class ProducerQueue { 13 | 14 | struct ProducerWrapper { 15 | let id: Int64 16 | let producer: Observable 17 | } 18 | 19 | struct ResultWrapper { 20 | let id: Int64 21 | let result: Swift.Result 22 | } 23 | 24 | private let idGen: IdGenerator = .init() 25 | private var producers: [ProducerWrapper] = [] 26 | private let queue: DispatchQueue = .init(label: Consts.domainPrefix + ".producerqueue", qos: .utility) 27 | private let cancel: DisposeBag = .init() 28 | private let subject: PublishSubject = .init() 29 | 30 | private var isProcessing: Atomic = .init(value: false) 31 | 32 | public func enqueue(producer: Observable) -> Observable { 33 | let id = idGen.gen() 34 | let producerWrapper = ProducerWrapper(id: id, producer: producer) 35 | check(producerWrapper) 36 | return subject.asObservable() 37 | .filter { $0.id == id } 38 | .flatMapLatest({ (result) -> Observable in 39 | switch result.result { 40 | case .success(let value): 41 | return .just(value) 42 | case .failure(let error): 43 | return .error(error) 44 | } 45 | }) 46 | } 47 | 48 | private func check(_ wrapper: ProducerWrapper? = nil) { 49 | queue.async { [weak self] in 50 | guard let self else { return } 51 | if let wrapper = wrapper { 52 | producers.append(wrapper) 53 | } 54 | 55 | guard !isProcessing.value, let next = producers.first else { return } 56 | 57 | let id = next.id 58 | isProcessing.swap(true) 59 | producers.removeFirst() 60 | 61 | next.producer 62 | // This place shouldn't change the queue, because producer may have it's own queue and we shouldn't change 63 | // the expectation of users. 64 | .subscribe({ [weak self] event in 65 | guard let self else { return } 66 | var result: ResultWrapper? = nil 67 | switch event { 68 | case .next(let value): 69 | result = ResultWrapper(id: id, result: .success(value)) 70 | case .error(let error): 71 | result = ResultWrapper(id: id, result: .failure(error)) 72 | isProcessing.swap(false) 73 | check() 74 | case .completed: 75 | isProcessing.swap(false) 76 | check() 77 | } 78 | if let result = result { 79 | subject.onNext(result) 80 | } 81 | }) 82 | .disposed(by: cancel) 83 | } 84 | } 85 | 86 | } 87 | 88 | class IdGenerator { 89 | private let _lock: UnfairLock = .init() 90 | private var id: Int64 = 0 91 | 92 | func gen() -> Int64 { 93 | _lock.lock() 94 | defer { 95 | id += 1 96 | _lock.unlock() 97 | } 98 | return id 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/RxExtension/RxExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxExtension.swift 3 | // SAD 4 | // 5 | // Created by jiangkelan on 16/10/2017. 6 | // Copyright © 2017 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | public extension API { 13 | func rxSendRequest(with params: T.RequestParam?) -> Observable { 14 | return Observable.create { [weak self] observer in 15 | guard let self else { 16 | observer.onCompleted() 17 | return Disposables.create() 18 | } 19 | sendRequest(with: params).response({ (api, data, error) in 20 | if let error = error { 21 | observer.onError(error) 22 | } 23 | else if let data = data { 24 | observer.on(.next(data)) 25 | observer.onCompleted() 26 | } 27 | }) 28 | return Disposables.create(with: cancel) 29 | } 30 | } 31 | } 32 | 33 | public extension BehaviorSubject { 34 | var value: Element? { 35 | return try? value() 36 | } 37 | } 38 | 39 | public extension Observable { 40 | func mapToSignal() -> ObservableSignal { 41 | return self.map { _ in () } 42 | } 43 | 44 | func then(_ observale: Observable) -> Observable { 45 | return self.flatMapLatest { _ -> Observable in 46 | return observale 47 | } 48 | } 49 | 50 | func then(_ function: @escaping (Element) -> Observable) -> Observable { 51 | return flatMapLatest(function) 52 | } 53 | } 54 | 55 | /// When Observable's value is just a signal without usable value, we can just call it `ObservableSignal`. 56 | public typealias ObservableSignal = Observable 57 | 58 | public extension ObservableSignal { 59 | /// Send a signal with meaning `success`,`ok`,`notify` etc., a wrapper of `.just(())` and make it more readable. 60 | static var signal: ObservableSignal { .just(()) } 61 | } 62 | 63 | extension Observable { 64 | static var deallocatedError: Observable { 65 | return .error(KitErrors.deallocated) 66 | } 67 | } 68 | 69 | public extension AnyObserver where Element == Void { 70 | func signal() -> Void { 71 | self.onNext(()) 72 | } 73 | } 74 | 75 | public extension Reactive where Base: AnyObject { 76 | 77 | var lifetime: DisposeBag { 78 | guard let disposeBag = objc_getAssociatedObject(self.base, #function) as? DisposeBag else { 79 | let disposeBag = DisposeBag.init() 80 | objc_setAssociatedObject(self.base, #function, disposeBag, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 81 | return disposeBag 82 | } 83 | return disposeBag 84 | } 85 | } 86 | 87 | #if canImport(_Concurrency) 88 | 89 | @available(iOS 13, *) 90 | public extension UnsubscribeToken { 91 | 92 | /// Extern lifetime corresponding to an object. 93 | /// - Parameter object: An object. 94 | func bindLifetime(to object: T) { 95 | object.rx 96 | .deallocating 97 | .subscribe(onNext: { [weak self] _ in 98 | self?.unsubscribe() 99 | }) 100 | .disposed(by: object.rx.lifetime) 101 | } 102 | } 103 | 104 | #endif 105 | 106 | public extension Observable { 107 | 108 | /// Create a `Observable` using a throwing subscribe function. 109 | static func throwingCreate( 110 | _ subscribe: @escaping (AnyObserver) throws -> Disposable 111 | ) -> Observable { 112 | return create { observer in 113 | do { 114 | return try subscribe(observer) 115 | } catch { 116 | observer.onError(error) 117 | return Disposables.create() 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Testable/ModuleManager+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModuleManager+Testable.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/5/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ModuleBridge { 11 | public func inject(instance: ModuleProtocol) -> Bool { 12 | self.injected[type(of: instance).moduleIdentifier] = instance 13 | return true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Testable/Networking/API+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModuleManager+Testable.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/5/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public class APIMocker { 11 | 12 | public static func mock(type: T.Type, customize: @escaping (T.RequestParam?) -> T.ResultType) -> Void { 13 | ApiRouterContainer.shared.injectAPI(with: T.self, customize: customize) 14 | } 15 | 16 | public static func recover(type: T.Type) -> Void { 17 | ApiRouterContainer.shared.remove(type: T.self) 18 | } 19 | 20 | public static func reset() { 21 | ApiRouterContainer.shared.reset() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Tests/APITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APITests.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/5/30. 6 | // 7 | 8 | import Foundation 9 | @testable import MIOSwiftyArchitecture 10 | import XCTest 11 | import RxSwift 12 | import Alamofire 13 | 14 | class APITests: XCTestCase { 15 | 16 | var cancel: DisposeBag = .init() 17 | 18 | override func setUp() { 19 | super.setUp() 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | } 25 | 26 | let testApi = API() 27 | 28 | func testMock() { 29 | APIMocker.mock(type: UserAPI.self) { params in 30 | return User(userId: "1", name: "Klein") 31 | } 32 | 33 | let expect = XCTestExpectation(description: "Api test mock") 34 | 35 | testApi.rxSendRequest(with: ["id": "1"]) 36 | .subscribe { event in 37 | print(event) 38 | guard case .next(_) = event else { return } 39 | expect.fulfill() 40 | } 41 | .disposed(by: cancel) 42 | 43 | wait(for: [expect], timeout: 1) 44 | } 45 | 46 | func testRemoveMock() { 47 | APIMocker.recover(type: UserAPI.self) 48 | 49 | let expect = XCTestExpectation(description: "RemoveMock") 50 | testApi.rxSendRequest(with: ["id": "1"]) 51 | .subscribe { event in 52 | print(event) 53 | guard case .error(_) = event else { return } 54 | expect.fulfill() 55 | } 56 | .disposed(by: cancel) 57 | wait(for: [expect], timeout: 5) 58 | } 59 | } 60 | 61 | struct User: Codable { 62 | var userId: String 63 | var name: String 64 | } 65 | 66 | let MioDemoServer: Server = .init(live: URL(string: "https://www.baidu.com")!, 67 | customEnvironments: [ 68 | .custom("Dev"): URL(string: "https://www.baidu.com")!, 69 | .custom("Staging"): URL(string: "https://www.baidu.com")!, 70 | ]) 71 | 72 | final class UserAPI: NSObject, ApiInfoProtocol { 73 | typealias RequestParam = [String: String] 74 | typealias ResultType = User 75 | 76 | static var apiVersion: String { 77 | get { return "" } 78 | } 79 | static var apiName: String { 80 | get { return "s" } 81 | } 82 | static var server: Server { 83 | get { return MioDemoServer } 84 | } 85 | static var httpMethod: Alamofire.HTTPMethod { 86 | get { return .get } 87 | } 88 | 89 | static var responseSerializer: MIOSwiftyArchitecture.ResponseSerializer { 90 | return MIOSwiftyArchitecture.JSONCodableResponseSerializer() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Tests/InitiatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModuleManager+Testable.swift 3 | // MIOSwiftyArchitecture 4 | // 5 | // Created by KelanJiang on 2022/5/23. 6 | // 7 | 8 | import Foundation 9 | @testable import MIOSwiftyArchitecture 10 | import XCTest 11 | 12 | class InitiatorTestCase: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | } 17 | 18 | override func tearDown() { 19 | super.tearDown() 20 | } 21 | 22 | func testDemo() { 23 | let graph: DirectedGraph = .init() 24 | 25 | let a = DirectedGraphNode(value: 1) 26 | let b = DirectedGraphNode(value: 2) 27 | let c = DirectedGraphNode(value: 3) 28 | let d = DirectedGraphNode(value: 4) 29 | 30 | a.out = [b] 31 | b.out = [c, d] 32 | c.out = [a] 33 | 34 | a.in = [c] 35 | b.in = [a] 36 | c.in = [b] 37 | d.in = [b] 38 | 39 | graph.nodes = [a, b, c, d] 40 | 41 | XCTAssertTrue(graph.checkCircle()) 42 | } 43 | 44 | func testNoCircle() { 45 | let graph: DirectedGraph = .init() 46 | 47 | let a = DirectedGraphNode(value: 1) 48 | let b = DirectedGraphNode(value: 2) 49 | let c = DirectedGraphNode(value: 3) 50 | let d = DirectedGraphNode(value: 4) 51 | 52 | a.out = [b] 53 | b.out = [c, d] 54 | c.out = [d] 55 | 56 | 57 | b.in = [a] 58 | c.in = [b] 59 | d.in = [b, c] 60 | 61 | graph.nodes = [a, b, c, d] 62 | 63 | XCTAssertFalse(graph.checkCircle()) 64 | } 65 | 66 | func testVisit() { 67 | let graph: DirectedGraph = .init() 68 | 69 | let a = DirectedGraphNode(value: 1) 70 | let b = DirectedGraphNode(value: 2) 71 | let c = DirectedGraphNode(value: 3) 72 | let d = DirectedGraphNode(value: 4) 73 | 74 | a.out = [b] 75 | b.out = [c, d] 76 | c.out = [d] 77 | 78 | 79 | b.in = [a] 80 | c.in = [b] 81 | d.in = [b, c] 82 | 83 | graph.nodes = [a, b, c, d] 84 | 85 | var route: [Int] = [] 86 | graph.dfsForEach { node in 87 | route.append(node.value) 88 | } 89 | print(route) 90 | XCTAssert(route.last == 1) 91 | } 92 | 93 | func testBuildTaskGraph() { 94 | let tasks: [Initiator.Task] = [ 95 | .init(id: "1", dependencies: ["2", "3"], operation: {}), 96 | .init(id: "2", dependencies: ["4"], operation: {}), 97 | .init(id: "3", dependencies: ["4"], operation: {}), 98 | .init(id: "4", dependencies: [], operation: {}), 99 | ] 100 | 101 | let graph = TaskGraphBuilder.buildGraph(with: tasks) 102 | XCTAssertFalse(graph.checkCircle()) 103 | 104 | var path: [String] = [] 105 | graph.dfsForEach { node in 106 | path.append(node.value.identifier) 107 | } 108 | print(path) 109 | 110 | XCTAssert(path.count == 4) 111 | XCTAssert(path.first == "4" && path.last == "1") 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Base/Tests/RxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxTests.swift 3 | // MIOSwiftyArchitecture-Unit-Tests 4 | // 5 | // Created by KelanJiang on 2022/6/9. 6 | // 7 | 8 | import Foundation 9 | @testable import MIOSwiftyArchitecture 10 | import XCTest 11 | import RxSwift 12 | 13 | class RxTestCase: XCTestCase { 14 | 15 | override func setUp() { 16 | super.setUp() 17 | } 18 | 19 | override func tearDown() { 20 | super.tearDown() 21 | } 22 | 23 | var cancel: DisposeBag = .init() 24 | 25 | func testProducerQueue() { 26 | let queue = ProducerQueue() 27 | let expect = XCTestExpectation() 28 | 29 | let ob1 = Observable.create { observer in 30 | observer.onNext(1) 31 | observer.onNext(2) 32 | observer.onCompleted() 33 | return Disposables.create() 34 | } 35 | 36 | let ob2 = Observable.create { observer in 37 | observer.onNext(3) 38 | observer.onCompleted() 39 | return Disposables.create() 40 | } 41 | 42 | queue.enqueue(producer: ob1).subscribe { event in 43 | print(event) 44 | }.disposed(by: cancel) 45 | 46 | queue.enqueue(producer: ob2).subscribe { event in 47 | print(event) 48 | expect.fulfill() 49 | }.disposed(by: cancel) 50 | 51 | wait(for: [expect], timeout: 5) 52 | } 53 | 54 | func testDelayWorker() { 55 | let work = Observable.create { observer in 56 | observer.onNext(1) 57 | observer.onCompleted() 58 | return Disposables.create() 59 | } 60 | 61 | let expect = XCTestExpectation() 62 | 63 | DelayWorker.delay(work: work, interval: .seconds(3)) 64 | .debug(#function + "1", trimOutput: true) 65 | .do(onCompleted: { 66 | expect.fulfill() 67 | }) 68 | .subscribe() 69 | .disposed(by: cancel) 70 | 71 | DelayWorker.delay(work: work, interval: .microseconds(0)) 72 | .debug(#function + "2", trimOutput: true) 73 | .subscribe() 74 | .disposed(by: cancel) 75 | 76 | wait(for: [expect], timeout: 5) 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ApiManager/BaseAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseAPI.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2023/3/16. 6 | // Copyright © 2023 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ApplicationBasedApiInfoProtocol: ApiInfoProtocol { } 12 | 13 | extension ApplicationBasedApiInfoProtocol where ResultType: Decodable { 14 | static var responseSerializer: ResponseSerializer { 15 | return MIOSwiftyArchitecture.JSONCodableResponseSerializer() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ApiManager/TestAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAPI.swift 3 | // swiftArchitecture 4 | // 5 | // Created by jiangkelan on 29/12/2016. 6 | // Copyright © 2016 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Alamofire 11 | import MIOSwiftyArchitecture 12 | 13 | let MioDemoServer: Server = .init(live: URL(string: "https://www.baidu.com")!, 14 | customEnvironments: [ 15 | .custom("Dev"): URL(string: "https://www.baidu.com")!, 16 | .custom("Staging"): URL(string: "https://www.baidu.com")!, 17 | ]) 18 | 19 | struct EmptyRequestParam: Codable { } 20 | 21 | class TestAPI: NSObject, ApplicationBasedApiInfoProtocol { 22 | typealias RequestParam = EmptyRequestParam 23 | typealias ResultType = TestAPI.Result 24 | 25 | static var apiVersion: String { 26 | get { return "" } 27 | } 28 | static var apiName: String { 29 | get { return "s" } 30 | } 31 | static var server: Server { 32 | get { return MioDemoServer } 33 | } 34 | static var httpMethod: Alamofire.HTTPMethod { 35 | get { return .get } 36 | } 37 | static func headers() -> HTTPHeaders? { 38 | return ["Cookie": "uid=123456"] 39 | } 40 | } 41 | 42 | extension TestAPI { 43 | 44 | struct Result: Codable { 45 | let isFinal: Bool 46 | let objects: [TestObj] 47 | 48 | enum CodingKeys: String, CodingKey { 49 | case isFinal = "is_final" 50 | case objects = "objs" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ApiManager/TestFailureAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestFailureAPI.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2020/4/8. 6 | // Copyright © 2020 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | //class TestFailureAPI: API { 12 | // 13 | //} 14 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/Appearance/Theme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Theme.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2022/6/21. 6 | // Copyright © 2022 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class ThemeUI { 13 | static var themeResourceDidChange: Notification.Name = .init(rawValue: "SwiftyArchitecture.ThemeUI.themeDidChange") 14 | static var current: Resource = DynamicResource() { 15 | didSet { NotificationCenter.default.post(name: themeResourceDidChange, object: nil) } 16 | } 17 | } 18 | 19 | protocol Colors { 20 | var text: UIColor { get } 21 | var background: UIColor { get } 22 | } 23 | 24 | protocol Icons { 25 | var appIcon: UIImage { get } 26 | } 27 | 28 | protocol Resource { 29 | var color: Colors { get } 30 | var icon: Icons { get } 31 | } 32 | 33 | class DynamicColors: Colors { 34 | 35 | unowned var resource: DynamicResource? 36 | 37 | init(resource: DynamicResource) { 38 | self.resource = resource 39 | } 40 | 41 | var text: UIColor { 42 | return create(light: .label, dark: .white) 43 | } 44 | 45 | var background: UIColor { 46 | return create(light: .white, dark: .black) 47 | } 48 | 49 | func create(light: UIColor, dark: UIColor) -> UIColor { 50 | return UIColor.init { [weak self] trait in 51 | guard let resource = self?.resource else { return light } 52 | switch resource.currentSetting { 53 | case .light: 54 | return light 55 | case .dark: 56 | return dark 57 | case .followSystem: 58 | return trait.userInterfaceStyle == .dark ? dark: light 59 | } 60 | } 61 | } 62 | } 63 | 64 | class DynamicIcons: Icons { 65 | 66 | unowned var resource: DynamicResource? 67 | 68 | init(resource: DynamicResource) { 69 | self.resource = resource 70 | } 71 | 72 | var appIcon: UIImage { 73 | return .init(named: "launch_swifty_icon")! 74 | } 75 | } 76 | 77 | class DynamicResource: Resource { 78 | lazy var color: Colors = DynamicColors.init(resource: self) 79 | lazy var icon: Icons = DynamicIcons.init(resource: self) 80 | 81 | enum Settings: Int { 82 | case followSystem = 0 83 | case light 84 | case dark 85 | 86 | func toStyle() -> UIUserInterfaceStyle { 87 | switch self { 88 | case .dark: return .dark 89 | case .light: return .light 90 | case .followSystem: return .unspecified 91 | } 92 | } 93 | } 94 | 95 | var currentSetting: Settings = .followSystem { 96 | didSet { 97 | UIView.animate(withDuration: 0.15, delay: 0, options: []) { 98 | UIApplication.availableWindows.forEach { window in 99 | window.overrideUserInterfaceStyle = self.currentSetting.toStyle() 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | // MARK: - example for other static resource 107 | 108 | class DefaultColors: Colors { 109 | var text: UIColor { 110 | return .label 111 | } 112 | var background: UIColor { 113 | return .white 114 | } 115 | } 116 | 117 | class DefaultIcons: Icons { 118 | var appIcon: UIImage { 119 | return .init(named: "launch_swifty_icon")! 120 | } 121 | } 122 | 123 | class DefaultResource: Resource { 124 | var color: Colors = DefaultColors() 125 | var icon: Icons = DefaultIcons() 126 | } 127 | 128 | class TestCaseResources { 129 | func testSwitchResource() -> Void { 130 | ThemeUI.current = DefaultResource() 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/Appearance/ThemeTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeTestViewController.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2022/6/21. 6 | // Copyright © 2022 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MIOSwiftyArchitecture 11 | 12 | class ThemeTestViewController: ViewController { 13 | 14 | @IBOutlet weak var tableView: UITableView! 15 | @IBOutlet weak var selection: UISegmentedControl! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | // Do any additional setup after loading the view. 21 | 22 | tableView.rowHeight = 44 23 | tableView.register(ThemeTestCell.self) 24 | 25 | tableView.reloadData() 26 | 27 | guard let dynamic = ThemeUI.current as? DynamicResource else { fatalError() } 28 | selection.selectedSegmentIndex = dynamic.currentSetting.rawValue 29 | } 30 | 31 | var dataSource: [String] = (0...100).map { "Messages \($0)" } 32 | /* 33 | // MARK: - Navigation 34 | 35 | // In a storyboard-based application, you will often want to do a little preparation before navigation 36 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 37 | // Get the new view controller using segue.destination. 38 | // Pass the selected object to the new view controller. 39 | } 40 | */ 41 | 42 | @IBAction func selectionDidChange(_ sender: Any) { 43 | guard let segment = sender as? UISegmentedControl, 44 | let selection = DynamicResource.Settings.init(rawValue: segment.selectedSegmentIndex), 45 | let dynamic = ThemeUI.current as? DynamicResource 46 | else { return } 47 | 48 | dynamic.currentSetting = selection 49 | } 50 | } 51 | 52 | extension ThemeTestViewController: UITableViewDelegate, UITableViewDataSource { 53 | 54 | static var textAtrributes: [NSAttributedString.Key : Any] = [ 55 | .foregroundColor: ThemeUI.current.color.text, 56 | .font: UIFont.systemFont(ofSize: 16) 57 | ] 58 | 59 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 60 | let cell = tableView.dequeReusableCell(forIndexPath: indexPath) as ThemeTestCell 61 | cell.label.attributedText = NSAttributedString(string: dataSource[indexPath.row], attributes: Self.textAtrributes) 62 | return cell 63 | } 64 | 65 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 66 | return dataSource.count 67 | } 68 | 69 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 70 | return 44 71 | } 72 | 73 | } 74 | 75 | extension ThemeTestViewController: NavigationTargetProtocol { 76 | static func createTarget(subPaths: [String], queries: [MIOSwiftyArchitecture.NavigationURL.QueryItem], configuration: Navigation.Configuration) -> [UIViewController] { 77 | KitLogger.info("Navigation action : \(subPaths), queries: \(queries)") 78 | return [ThemeTestViewController.init(nibName: nil, bundle: nil)] 79 | } 80 | } 81 | 82 | class ThemeTestCell: UITableViewCell { 83 | 84 | let label: UILabel 85 | 86 | public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 87 | self.label = .init(frame: .zero) 88 | super.init(style: style, reuseIdentifier: reuseIdentifier) 89 | 90 | self.contentView.addSubview(self.label) 91 | self.label.textColor = ThemeUI.current.color.text 92 | self.label.font = .systemFont(ofSize: 16) 93 | 94 | self.backgroundColor = ThemeUI.current.color.background 95 | 96 | self.label.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 16).isActive = true 97 | self.label.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true 98 | self.label.translatesAutoresizingMaskIntoConstraints = false 99 | } 100 | 101 | required init?(coder: NSCoder) { 102 | fatalError("init(coder:) has not been implemented") 103 | } 104 | 105 | } 106 | 107 | extension ThemeTestCell: ReusableView { } 108 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/Appearance/ThemeTestViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/CommonModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonModule.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2023/4/6. 6 | // Copyright © 2023 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MIOSwiftyArchitecture 11 | 12 | public extension ModuleIdentifier { 13 | static let common: ModuleIdentifier = "com.klein.module.auth" 14 | } 15 | 16 | class CommonModule: ModuleProtocol { 17 | required init() { } 18 | 19 | static var moduleIdentifier: MIOSwiftyArchitecture.ModuleIdentifier { 20 | .common 21 | } 22 | 23 | func moduleDidLoad(with manager: MIOSwiftyArchitecture.ModuleManager) { 24 | 25 | } 26 | } 27 | 28 | extension CommonModule: ModuleInitiatorProtocol { 29 | static var identifier: String { 30 | .common 31 | } 32 | 33 | static var operation: MIOSwiftyArchitecture.Initiator.Operation = { 34 | navigation = .init(inAppLinkScheme: "sa-interal", externalLinkScheme: "sa", host: "com.mioke.swifty-architecture-demo") 35 | if let url = Bundle.main.url(forResource: "AppLinkRegistery", withExtension: ".plist") { 36 | try! navigation?.setRegisteryFile(at: url) 37 | } 38 | 39 | APIMocker.mock(type: UserAPI.self) { param in 40 | if let param = param { 41 | print(param) 42 | } 43 | let reply = UserAPI.Reply(users: [UserAPI.Reply.User(userId: "10025", name: "Klein")]) 44 | return reply 45 | } 46 | } 47 | 48 | static var priority: MIOSwiftyArchitecture.Initiator.Priority { 49 | .high 50 | } 51 | 52 | static var dependencies: [String] { 53 | [] 54 | } 55 | } 56 | 57 | public var navigation: Navigation? = nil 58 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/GlobalImports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalImports.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2023/3/16. 6 | // Copyright © 2023 KleinMioke. All rights reserved. 7 | // 8 | 9 | @_exported import MIOSwiftyArchitecture 10 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/Model/Video.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Video.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2021/10/26. 6 | // Copyright © 2021 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct Video: Codable { 12 | 13 | enum State: String, Codable { 14 | case unknown, streaming, archived 15 | } 16 | 17 | let name: String 18 | 19 | // @CodableDefault(defaultValue: .unknown) 20 | var state: Video.State = .unknown 21 | 22 | init(from decoder: Decoder) throws { 23 | let container = try decoder.container(keyedBy: CodingKeys.self) 24 | self.name = try container.decode(String.self, forKey: .name) 25 | self.state = try container.decodeIfPresent(Video.State.self, forKey: .state) ?? self.state // not a good solution 26 | } 27 | } 28 | 29 | @propertyWrapper class CodableDefault: Codable { 30 | var defaultValue: T 31 | 32 | var value: T? 33 | var wrappedValue: T { 34 | get { return value ?? defaultValue } 35 | set { value = newValue } 36 | } 37 | 38 | init(wrappedValue: T? = nil, defaultValue: T) { 39 | value = wrappedValue 40 | self.defaultValue = defaultValue 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ModulesRegistery.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | modules 6 | 7 | SAD.CommonModule 8 | Application.ApplicationModule 9 | Auth.AuthModule 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/Persistance/RealmTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmTests.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2023/4/10. 6 | // Copyright © 2023 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import RxRealm 12 | import MIOSwiftyArchitecture 13 | import RxSwift 14 | 15 | class RealmTests { 16 | 17 | static let shared: RealmTests = .init() 18 | 19 | let api = API.init() 20 | let disposables: DisposeBag = .init() 21 | 22 | func testObjectChanges() { 23 | 24 | let request = Request.init(params: .init(uids: ["10025"])) 25 | 26 | DataCenter.update(with: request) 27 | .subscribe { event in 28 | print(event) 29 | } 30 | .disposed(by: disposables) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/Persistance/UserTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserTable.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/12/10. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | //class UserTable: KMPersistanceTable, TableProtocol { 12 | // 13 | // var database: KMPersistanceDatabase { 14 | // return DefaultDatabase.instance 15 | // } 16 | // 17 | // var tableName: String { 18 | // return "user_table" 19 | // } 20 | // 21 | // var tableColumnInfo: [String: String] { 22 | // return [ 23 | // "user_id": "Integer primary key", 24 | // "user_name": "text default NULL" 25 | // ] 26 | // } 27 | //} 28 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/Services/NotificationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationService.swift 3 | // swiftArchitecture 4 | // 5 | // Created by jiangkelan on 03/01/2017. 6 | // Copyright © 2017 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UserNotifications 11 | 12 | class NotificationService: NSObject { 13 | 14 | private static let service: NotificationService = NotificationService() 15 | class func current() -> NotificationService { 16 | return NotificationService.service 17 | } 18 | 19 | private var requestCompletionblock: ((Bool, Error?) -> Void)? 20 | @available(iOS 10.0, *) 21 | func setRequestCompletion(block: ((Bool, Error?) -> Void)?) -> Void { 22 | self.requestCompletionblock = block 23 | } 24 | 25 | func requestAuthorization() -> Void { 26 | if #available(iOS 10.0, *) { 27 | let completion = self.requestCompletionblock ?? 28 | { (granted: Bool, error :Error?) in 29 | 30 | } 31 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge], 32 | completionHandler: completion) 33 | } else { 34 | // Fallback on earlier versions 35 | let types: UIUserNotificationType = [.alert, .badge, .sound] 36 | 37 | let settings = UIUserNotificationSettings(types: types, categories: nil) 38 | 39 | UIApplication.shared.registerUserNotificationSettings(settings) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/Services/UserService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserService.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/11/24. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MIOSwiftyArchitecture 11 | import RxSwift 12 | import AuthProtocol 13 | 14 | class UserService: NSObject { 15 | 16 | static let shared: UserService = .init() 17 | 18 | @discardableResult 19 | func login() -> ObservableSignal { 20 | return AppContext.standard.triggerLogin() 21 | } 22 | 23 | func logout() -> ObservableSignal { 24 | AppContext.current.logout() 25 | } 26 | 27 | func genRandomToken() -> String { 28 | let len = 12 29 | let codec = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-=" 30 | 31 | return (0.. Observable { 41 | return .throwingCreate { observer in 42 | if let authModule = try ModuleManager.default.bridge.resolve(.auth) as? AuthServiceProtocol { 43 | try authModule.authenticate { user in 44 | observer.onNext(user) 45 | observer.onCompleted() 46 | } 47 | } 48 | return Disposables.create() 49 | } 50 | } 51 | 52 | func shouldRefreshAuthentication(with user: UserProtocol, isStartup: Bool) -> Bool { 53 | // we can refresh authentication everytime when app startup. 54 | if isStartup { 55 | return true 56 | } 57 | // or we can trust the `expiration` of the user we stored. 58 | guard let user = user as? TestUser else { 59 | assert(false, "This application should use `TestUser` as the UserProtocol.") 60 | return true 61 | } 62 | guard let expiration = user.expiration else { return true } 63 | return expiration < Date() 64 | } 65 | 66 | func refreshAuthentication(with user: UserProtocol) -> Observable { 67 | guard let user = user as? TestUser else { return .error(KitErrors.unknown) } 68 | 69 | return .just(TestUser(id: user.id, age: user.age + 1, token: UserService.shared.genRandomToken())) 70 | .delay(.seconds(3), scheduler: SerialDispatchQueueScheduler.init(qos: .default)) 71 | } 72 | 73 | func deauthenticate() -> ObservableSignal { 74 | return .signal 75 | } 76 | } 77 | 78 | enum UserServiceError: Error { 79 | case unknown 80 | } 81 | 82 | extension UserService : StandardAppContextStoreProtocol { 83 | 84 | func standardAppContext(_ context: AppContext, migrate userData: Data, from version: Int) -> UserProtocol? { 85 | if let jsonObj = try? JSONSerialization.jsonObject(with: userData), let dic = jsonObj as? [String: Any] { 86 | print(dic) 87 | } 88 | return nil 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Auth/AuthTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthTestViewController.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2022/9/2. 6 | // Copyright © 2022 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MIOSwiftyArchitecture 11 | import RxCocoa 12 | import RxSwift 13 | import AuthProtocol 14 | 15 | class AuthTestViewController: UIViewController { 16 | 17 | @IBOutlet weak var displayLabel: UILabel! 18 | @IBOutlet weak var statusLabel: UILabel! 19 | 20 | var listeners: DisposeBag = .init() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | // Do any additional setup after loading the view. 26 | 27 | refreshDisplay() 28 | } 29 | 30 | func refreshDisplay() -> Void { 31 | if let user = AppContext.current.user as? TestUser { 32 | displayLabel.text = user.customDebugDescription 33 | 34 | } else if AppContext.current == AppContext.standard { 35 | displayLabel.text = "Current is default user" 36 | } 37 | 38 | listeners = .init() 39 | AppContext.current.authState.map { "\($0)" } 40 | .asDriver(onErrorJustReturn: "Error") 41 | .debug("AuthVC") 42 | .drive(statusLabel.rx.text) 43 | .disposed(by: listeners) 44 | } 45 | 46 | 47 | @IBAction func onLogin(_ sender: Any) { 48 | UserService.shared 49 | .login() 50 | .subscribe { event in 51 | switch event { 52 | case .next(): 53 | self.refreshDisplay() 54 | case .completed, .error: 55 | break 56 | } 57 | } 58 | .disposed(by: self.rx.lifetime) 59 | } 60 | 61 | @IBAction func onRefresh(_ sender: Any) { 62 | 63 | } 64 | 65 | @IBAction func onLogout(_ sender: Any) { 66 | UserService.shared.logout() 67 | .subscribe { [weak self] event in 68 | if case .next = event { 69 | self?.refreshDisplay() 70 | } 71 | } 72 | .disposed(by: rx.lifetime) 73 | } 74 | 75 | /* 76 | // MARK: - Navigation 77 | 78 | // In a storyboard-based application, you will often want to do a little preparation before navigation 79 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 80 | // Get the new view controller using segue.destination. 81 | // Pass the selected object to the new view controller. 82 | } 83 | */ 84 | 85 | } 86 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Code Block/CodeBlockEditorViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | import Combine 4 | 5 | class CodeBlockEditorViewController: UIViewController { 6 | 7 | var textView: SATextView = buildTextView() 8 | @IBOutlet weak var codeEnableSwitch: UISwitch! 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | 13 | view.addSubview(textView) 14 | view.sendSubviewToBack(textView) 15 | textView.snp.makeConstraints { make in 16 | make.edges.equalTo(view.safeAreaLayoutGuide) 17 | } 18 | 19 | textView.delegate = self 20 | textView.editType = .code 21 | 22 | textView.layoutManager.delegate = self 23 | 24 | codeEnableSwitch.addTarget(self, action: #selector(codeSwitchDidChange), for: .valueChanged) 25 | } 26 | 27 | static func buildTextView() -> SATextView { 28 | let storage = NSTextStorage() 29 | 30 | let container = SATextContainer(size: .zero) 31 | container.widthTracksTextView = true 32 | container.heightTracksTextView = true 33 | 34 | let layoutManager = SALayoutManager() 35 | storage.addLayoutManager(layoutManager) 36 | layoutManager.addTextContainer(container) 37 | 38 | return SATextView(frame: .zero, textContainer: container) 39 | } 40 | 41 | @objc 42 | func codeSwitchDidChange() { 43 | textView.editType = codeEnableSwitch.isOn ? .code : .plain 44 | } 45 | } 46 | 47 | extension CodeBlockEditorViewController: UITextViewDelegate { 48 | 49 | func textViewDidChange(_ textView: UITextView) { 50 | // print(textView.textContainer, textView.textContainer.layoutManager, textView.textContainer.layoutManager?.textContainers) 51 | 52 | } 53 | } 54 | 55 | extension CodeBlockEditorViewController: NSLayoutManagerDelegate { 56 | 57 | // func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { 58 | // let charIndex = layoutManager.characterIndexForGlyph(at: glyphIndex) 59 | // print("#1", "g:\(glyphIndex)", "c:\(charIndex)", rect) 60 | // 61 | // if let theString = layoutManager.textStorage?.string { 62 | // let theChar = theString[theString.index(theString.startIndex, offsetBy: charIndex)] 63 | // print("#2 the char: \(theChar)") 64 | // } 65 | // 66 | // guard let storage = layoutManager.textStorage, storage.length > charIndex + 1 else { return 0.5 } 67 | // 68 | // let preAttribute = storage.attribute(.SACodeBlock, at: charIndex, effectiveRange: nil) 69 | // let afterAttribute = storage.attribute(.SACodeBlock, at: charIndex + 1, effectiveRange: nil) 70 | // 71 | // if preAttribute == nil, afterAttribute != nil { 72 | // return 20 73 | // } 74 | // 75 | // return 0.5 76 | // } 77 | 78 | func layoutManager(_ layoutManager: NSLayoutManager, paragraphSpacingBeforeGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { 79 | let charIndex = layoutManager.characterIndexForGlyph(at: glyphIndex) 80 | print("#1", "g:\(glyphIndex)", "c:\(charIndex)", rect) 81 | 82 | if let theString = layoutManager.textStorage?.string, layoutManager.textStorage?.length ?? 0 > charIndex { 83 | let theChar = theString[theString.index(theString.startIndex, offsetBy: charIndex)] 84 | print("#2 the char: \(theChar)") 85 | } 86 | 87 | guard let storage = layoutManager.textStorage, charIndex > 0 else { return 0.5 } 88 | 89 | let currentAttribute = storage.attribute(.SACodeBlock, at: charIndex, effectiveRange: nil) 90 | let preAttribute = storage.attribute(.SACodeBlock, at: charIndex - 1, effectiveRange: nil) 91 | 92 | if preAttribute == nil, currentAttribute != nil { 93 | return 20 94 | } 95 | return 0.5 96 | } 97 | 98 | func layoutManager(_ layoutManager: NSLayoutManager, paragraphSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { 99 | let charIndex = layoutManager.characterIndexForGlyph(at: glyphIndex) 100 | print("#3", "g:\(glyphIndex)", "c:\(charIndex)", rect) 101 | 102 | if let theString = layoutManager.textStorage?.string, layoutManager.textStorage?.length ?? 0 > charIndex { 103 | let theChar = theString[theString.index(theString.startIndex, offsetBy: charIndex)] 104 | print("#4 the char: \(theChar)") 105 | } 106 | return 0.5 107 | } 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Code Block/CodeBlockEditorViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Code Block/SALayoutManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SALayoutManager.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2022/12/5. 6 | // Copyright © 2022 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SALayoutManager: NSLayoutManager { 12 | 13 | override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { 14 | super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin) 15 | // 16 | // guard let nsstring = self.textStorage?.string as? NSString else { return } 17 | // print("JKL - prepare to show range: \(glyphsToShow), \n\ttext: \(nsstring.substring(with: glyphsToShow))") 18 | } 19 | 20 | override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) { 21 | super.drawBackground(forGlyphRange: glyphsToShow, at: origin) 22 | 23 | // guard let nsstring = self.textStorage?.string as? NSString else { return } 24 | // print("JKL - prepare to draw background: \(glyphsToShow), \n at origin: \(origin),\n text: \(nsstring.substring(with: glyphsToShow))") 25 | } 26 | 27 | // override func fillBackgroundRectArray(_ rectArray: UnsafePointer, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) { 28 | //#if DEBUG 29 | // let rects = transfromArrayPointerToArray(rectArray, count: rectCount) 30 | // print(rects, charRange, color) 31 | //#endif 32 | // 33 | // guard let attributedString = self.textStorage?.attributedSubstring(from: charRange), 34 | //// let isCodeBlock = attributedString.attribute(.SACodeBlock, at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: attributedString.length)) as? Bool, 35 | // let isCodeBlock = attributedString.attribute(.SACodeBlock, at: 0, effectiveRange: nil) as? Bool, 36 | // isCodeBlock 37 | // else { 38 | // super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color) 39 | // return 40 | // } 41 | // 42 | // var codeRect = CGRect(origin: rectArray.pointee.origin, 43 | // size: .init(width: UIScreen.main.bounds.width - rectArray.pointee.origin.x * 2, height: 0)) 44 | // // find largest 45 | // var p = rectArray 46 | // (0 ..< rectCount - 1).forEach { _ in 47 | // p = p.pointee.maxY < p.successor().pointee.maxY ? p.successor() : p 48 | // } 49 | // codeRect.size.height = p.pointee.origin.y + p.pointee.size.height - rectArray.pointee.origin.y 50 | // 51 | //// let roundedRectPath = UIBezierPath(roundedRect: codeRect, cornerRadius: 8) 52 | //// roundedRectPath.stroke() 53 | // 54 | // withUnsafePointer(to: codeRect) { pointer in 55 | // p = pointer 56 | // } 57 | // 58 | // super.fillBackgroundRectArray(p, count: 1, forCharacterRange: charRange, color: color) 59 | // } 60 | 61 | } 62 | 63 | 64 | func transfromArrayPointerToArray(_ pointer: UnsafePointer, count: Int) -> [T] { 65 | var p: UnsafePointer = pointer 66 | return (0 ..< count).map { _ in 67 | defer { p = p.successor() } 68 | return p.pointee 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Code Block/SATextStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SATextStorage.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2022/12/21. 6 | // Copyright © 2022 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SATextStorage: NSTextStorage { 12 | 13 | } 14 | 15 | 16 | class SATextContainer: NSTextContainer { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Code Block/SATextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SATextView.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2022/12/2. 6 | // Copyright © 2022 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SATextView: UITextView { 12 | 13 | /* 14 | // Only override draw() if you perform custom drawing. 15 | // An empty implementation adversely affects performance during animation. 16 | override func draw(_ rect: CGRect) { 17 | // Drawing code 18 | } 19 | */ 20 | enum EditType { 21 | case plain 22 | case code 23 | } 24 | 25 | var editType: EditType = .plain { 26 | didSet { editTypeDidChanged() } 27 | } 28 | 29 | override init(frame: CGRect, textContainer: NSTextContainer?) { 30 | super.init(frame: frame, textContainer: textContainer) 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | super.init(coder: coder) 35 | } 36 | 37 | func editTypeDidChanged() { 38 | switch editType { 39 | case .code: 40 | let style = NSMutableParagraphStyle() 41 | // style.paragraphSpacing = 16 42 | // style.paragraphSpacingBefore = 16 43 | 44 | self.typingAttributes = [ 45 | .SACodeBlock: true, 46 | .backgroundColor: UIColor.systemGroupedBackground, 47 | .paragraphStyle: style 48 | ] 49 | case .plain: 50 | self.typingAttributes = [:] 51 | } 52 | } 53 | } 54 | 55 | extension NSAttributedString.Key { 56 | static let SACodeBlock = NSAttributedString.Key.init(rawValue: "SACodeBlock") 57 | } 58 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Combine/Combine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Combine.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2023/6/27. 6 | // Copyright © 2023 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | 13 | class TestCombineUsages { 14 | 15 | static let shared: TestCombineUsages = .init() 16 | 17 | var cancelables: [AnyCancellable] = [] 18 | 19 | @Published var count: Int = 0 20 | 21 | private init() { 22 | foo2() 23 | } 24 | 25 | func foo() { 26 | let publisher: NotificationCenter.Publisher = .init(center: .default, 27 | name: UIResponder.keyboardWillShowNotification) 28 | 29 | publisher 30 | .receive(on: RunLoop.main) 31 | .sink { completion in 32 | print(completion) 33 | } receiveValue: { value in 34 | print(value) 35 | } 36 | .store(in: &cancelables) 37 | 38 | } 39 | 40 | var subscriber: AnySubscriber = AnySubscriber( 41 | receiveSubscription: { subscription in 42 | print(subscription) 43 | }, 44 | receiveValue: { value in 45 | print(3, value) 46 | return .unlimited 47 | }, 48 | receiveCompletion: { completion in 49 | print(completion) 50 | }) 51 | 52 | func foo2() { 53 | // 54 | // $count 55 | // .handleEvents { subscription in 56 | // print("subscription", subscription) 57 | // } receiveOutput: { value in 58 | // print("output", value) 59 | // } receiveCancel: { 60 | // print("cancel") 61 | // } receiveRequest: { demand in 62 | // print("demand", demand) 63 | // } 64 | // .subscribe(subscriber) 65 | 66 | $count.subscribe(subscriber) 67 | 68 | $count 69 | .handleEvents { subscription in 70 | print("subscription", subscription) 71 | } receiveOutput: { value in 72 | print("output", value) 73 | } receiveCancel: { 74 | print("cancel") 75 | } receiveRequest: { demand in 76 | print("demand", demand) 77 | } 78 | .sink { value in 79 | print(1, value) 80 | } 81 | .store(in: &cancelables) 82 | 83 | $count 84 | .sink { value in 85 | print(2, value) 86 | } 87 | .store(in: &cancelables) 88 | 89 | 90 | } 91 | 92 | func foo3() { 93 | CTUser() 94 | .combine 95 | .name 96 | .sink { (value: String?) in 97 | 98 | } 99 | .store(in: &cancelables) 100 | 101 | } 102 | } 103 | 104 | class CTUser: NSObject { 105 | var name: String? 106 | } 107 | 108 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Combine/CombineTestsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineTestsViewController.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2023/6/27. 6 | // Copyright © 2023 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CombineTestsViewController: UIViewController { 12 | 13 | let manager = TestCombineUsages.shared 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | // Do any additional setup after loading the view. 19 | 20 | } 21 | 22 | override func viewDidAppear(_ animated: Bool) { 23 | super.viewDidAppear(animated) 24 | 25 | manager.count += 1 26 | } 27 | 28 | 29 | /* 30 | // MARK: - Navigation 31 | 32 | // In a storyboard-based application, you will often want to do a little preparation before navigation 33 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 34 | // Get the new view controller using segue.destination. 35 | // Pass the selected object to the new view controller. 36 | } 37 | */ 38 | 39 | } 40 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Combine/CombineTestsViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Concurrency/ConcurrencyTestViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/DataCenter/DataCenterTestViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/DataCenter/TestObjModifyVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestObjModifyVC.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2020/4/17. 6 | // Copyright © 2020 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxRealm 13 | import RealmSwift 14 | 15 | class TestObjModifyVC: UIViewController { 16 | 17 | @IBOutlet weak var keyLabel: UILabel! 18 | @IBOutlet weak var valueTextField: UITextField! 19 | 20 | var objectKey: String = "" 21 | 22 | var disposeBag = DisposeBag() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | // Do any additional setup after loading the view. 28 | 29 | bindData() 30 | valueTextField.delegate = self 31 | } 32 | 33 | func bindData() -> Void { 34 | let obj = AppContext.current.cache.object(with: objectKey, type: TestObj.self).compactMap { $0 } 35 | 36 | obj.map { $0.key } 37 | .bind(to: self.keyLabel.rx.text) 38 | .disposed(by: self.disposeBag) 39 | 40 | obj.map { "\($0.value)" }.asDriver(onErrorJustReturn: "") 41 | .drive(valueTextField.rx.text) 42 | .disposed(by: self.disposeBag) 43 | 44 | let editingEnd = valueTextField.rx 45 | .controlEvent(UIControl.Event.editingDidEnd) 46 | .map { [weak self] in self?.valueTextField.text } 47 | 48 | // editingEnd 49 | // .compactMap { (text) -> UITextField? in 50 | // if let text = text, let _ = Int(text) { 51 | // return nil 52 | // } 53 | // return self.valueTextField 54 | // } 55 | // .withLatestFrom(obj) 56 | // .subscribe(onNext: { (testObj) in 57 | // self.valueTextField.text = "\(testObj.value)" 58 | // }) 59 | // .disposed(by: self.disposeBag) 60 | 61 | 62 | Observable 63 | .combineLatest(obj, editingEnd) 64 | .flatMapLatest { [weak self] (testObj, text) -> ObservableSignal in 65 | if let text = text, let value = Int(text) { 66 | return AppContext.current.store 67 | .context(.cache) 68 | .update { _ in 69 | testObj.value = value 70 | } 71 | } else { 72 | self?.valueTextField.text = "\(testObj.value)" 73 | return .signal 74 | } 75 | } 76 | .subscribe() 77 | .disposed(by: self.disposeBag) 78 | 79 | } 80 | 81 | 82 | /* 83 | // MARK: - Navigation 84 | 85 | // In a storyboard-based application, you will often want to do a little preparation before navigation 86 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 87 | // Get the new view controller using segue.destination. 88 | // Pass the selected object to the new view controller. 89 | } 90 | */ 91 | 92 | } 93 | 94 | extension TestObjModifyVC: UITextFieldDelegate { 95 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 96 | textField.resignFirstResponder() 97 | return true 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/Navigation/HomeNavigationHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeNavigationHandler.swift 3 | // SAD 4 | // 5 | // Created by KelanJiang on 2023/4/4. 6 | // Copyright © 2023 KleinMioke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MIOSwiftyArchitecture 11 | 12 | class HomeNavigationHandler: NavigationModuleHandlerProtocol { 13 | static func shouldNavigate(to url: MIOSwiftyArchitecture.NavigationURL, 14 | decisionHandler: @escaping (MIOSwiftyArchitecture.Navigation.Decision) -> Void) { 15 | KitLogger.info() 16 | if let firstPath = url.paths.first, firstPath == "messages" { 17 | // DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { 18 | decisionHandler(.pass) 19 | // } 20 | } else { 21 | decisionHandler(.pass) 22 | } 23 | } 24 | 25 | static func willNavigate(to url: MIOSwiftyArchitecture.NavigationURL) { 26 | 27 | } 28 | 29 | static func didNavigate(to url: MIOSwiftyArchitecture.NavigationURL) { 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Demo/ViewController/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // swiftArchitecture 4 | // 5 | // Created by Klein Mioke on 15/11/23. 6 | // Copyright © 2015年 KleinMioke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MIOSwiftyArchitecture 11 | 12 | open class ViewController: UIViewController { 13 | 14 | open override func viewWillAppear(_ animated: Bool) { 15 | KitLogger.info("\(self) will appear") 16 | super.viewWillAppear(animated) 17 | } 18 | 19 | open override func viewDidAppear(_ animated: Bool) { 20 | KitLogger.info("\(self) did appear") 21 | super.viewDidAppear(animated) 22 | } 23 | 24 | open override func viewWillDisappear(_ animated: Bool) { 25 | KitLogger.info("\(self) will disappear") 26 | super.viewWillDisappear(animated) 27 | } 28 | 29 | open override func viewDidDisappear(_ animated: Bool) { 30 | KitLogger.info("\(self) did disappear") 31 | super.viewDidDisappear(animated) 32 | } 33 | 34 | deinit { 35 | KitLogger.info("\(self) deinit") 36 | } 37 | } 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | $(PRODUCT_NAME) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Editor 28 | CFBundleURLSchemes 29 | 30 | sa 31 | 32 | 33 | 34 | CFBundleVersion 35 | 1.0.0 36 | LSRequiresIPhoneOS 37 | 38 | NSAppTransportSecurity 39 | 40 | NSAllowsArbitraryLoads 41 | 42 | 43 | UILaunchStoryboardName 44 | LaunchScreen 45 | UIMainStoryboardFile 46 | Main 47 | UIRequiredDeviceCapabilities 48 | 49 | armv7 50 | 51 | UISupportedInterfaceOrientations 52 | 53 | UIInterfaceOrientationPortrait 54 | UIInterfaceOrientationLandscapeLeft 55 | UIInterfaceOrientationLandscapeRight 56 | 57 | UISupportedInterfaceOrientations~ipad 58 | 59 | UIInterfaceOrientationPortrait 60 | UIInterfaceOrientationPortraitUpsideDown 61 | UIInterfaceOrientationLandscapeLeft 62 | UIInterfaceOrientationLandscapeRight 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /SwiftyArchitecture/Resource/swiftArchitecture-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | //#import 6 | //#import 7 | 8 | //#import "FMDB.h" 9 | //#import 10 | -------------------------------------------------------------------------------- /SwiftyArchitecture/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // SecTrustTest 4 | // 5 | // Created by KelanJiang on 2022/6/13. 6 | // 7 | 8 | import UIKit 9 | import MIOSwiftyArchitecture 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let windowScene = (scene as? UIWindowScene) else { return } 21 | 22 | print(#function) 23 | 24 | let root = InternalTestVC() 25 | let nav = UINavigationController(rootViewController: root) 26 | 27 | self.window = { 28 | let window = UIWindow(windowScene: windowScene) 29 | window.rootViewController = nav 30 | window.makeKeyAndVisible() 31 | return window 32 | }() 33 | } 34 | 35 | func sceneDidDisconnect(_ scene: UIScene) { 36 | // Called as the scene is being released by the system. 37 | // This occurs shortly after the scene enters the background, or when its session is discarded. 38 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 39 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 40 | 41 | print(#function) 42 | } 43 | 44 | func sceneDidBecomeActive(_ scene: UIScene) { 45 | // Called when the scene has moved from an inactive state to an active state. 46 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 47 | 48 | print(#function) 49 | } 50 | 51 | func sceneWillResignActive(_ scene: UIScene) { 52 | // Called when the scene will move from an active state to an inactive state. 53 | // This may occur due to temporary interruptions (ex. an incoming phone call). 54 | 55 | print(#function) 56 | } 57 | 58 | func sceneWillEnterForeground(_ scene: UIScene) { 59 | // Called as the scene transitions from the background to the foreground. 60 | // Use this method to undo the changes made on entering the background. 61 | 62 | print(#function) 63 | } 64 | 65 | func sceneDidEnterBackground(_ scene: UIScene) { 66 | // Called as the scene transitions from the foreground to the background. 67 | // Use this method to save data, release shared resources, and store enough scene-specific state information 68 | // to restore the scene back to its current state. 69 | 70 | print(#function) 71 | } 72 | 73 | 74 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { 75 | KitLogger.info("open \(URLContexts)") 76 | 77 | if navigation!.handle(open: URLContexts) { 78 | return 79 | } 80 | 81 | // do else handler. 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /SwiftyArchitecture/swiftArchitecture.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | swiftArchitecture.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftyArchitecture/swiftArchitecture.xcdatamodeld/swiftArchitecture.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pod_publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Script.sh 4 | # swiftArchitecture 5 | # 6 | # Created by KelanJiang on 2020/4/9. 7 | # Copyright © 2020 KleinMioke. All rights reserved. 8 | 9 | pod lib lint --verbose --allow-warnings 10 | 11 | pod trunk push --verbose --allow-warnings 12 | -------------------------------------------------------------------------------- /swiftArchitecture.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /swiftArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------