From 3b061685ec46546807d653a7cab303c21a001b5d Mon Sep 17 00:00:00 2001 From: Chenfeng Yao <282696845@qq.com> Date: Sat, 24 Aug 2024 13:06:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=82=E6=AD=A5=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E5=92=8C=E6=9C=8D=E5=8A=A1=E6=B3=A8=E5=86=8C=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 77 ++- Demo/Demo.xcodeproj/project.pbxproj | 442 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 63 +++ Demo/Demo/Assets.xcassets/Contents.json | 6 + Demo/Demo/ContentView.swift | 91 ++++ Demo/Demo/Demo.entitlements | 10 + Demo/Demo/Demo.xcdatamodeld/.xccurrentversion | 8 + .../Demo.xcdatamodel/contents | 9 + Demo/Demo/DemoApp.swift | 37 ++ Demo/Demo/Persistence.swift | 63 +++ .../Preview Assets.xcassets/Contents.json | 6 + Demo/Demo/providers/AppServiceProvider.swift | 33 ++ .../Demo/providers/RouteServiceProvider.swift | 17 + Package.swift | 12 +- README.md | 2 +- Sources/DFService/DF.swift | 6 + Sources/DFService/DFError.swift | 14 + Sources/DFService/DFService.swift | 6 - Sources/DFService/ServiceValues.swift | 29 ++ Sources/DFService/contract/AppContract.swift | 3 - Sources/DFService/contract/DFApiCall.swift | 15 + Sources/DFService/contract/DFApiKey.swift | 27 ++ .../DFService/contract/DFApplication.swift | 44 ++ Sources/DFService/core/FactoryKey.swift | 26 ++ Sources/DFService/core/ServiceName.swift | 26 ++ Sources/DFService/core/ServiceProvider.swift | 68 +++ Sources/DFService/extension/app.bundle.swift | 59 ++- Sources/DFService/extension/app.view.swift | 11 + .../service/BootstrapServiceProvider.swift | 79 ++++ Sources/DFService/service/LogService.swift | 17 + .../DFService/service/MockApiService.swift | 2 + Sources/DFService/utils/Runtime.swift | 22 + Tests/DFServiceTests/DFServiceTests.swift | 24 +- 36 files changed, 1337 insertions(+), 43 deletions(-) create mode 100644 Demo/Demo.xcodeproj/project.pbxproj create mode 100644 Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Demo/Demo/Assets.xcassets/Contents.json create mode 100644 Demo/Demo/ContentView.swift create mode 100644 Demo/Demo/Demo.entitlements create mode 100644 Demo/Demo/Demo.xcdatamodeld/.xccurrentversion create mode 100644 Demo/Demo/Demo.xcdatamodeld/Demo.xcdatamodel/contents create mode 100644 Demo/Demo/DemoApp.swift create mode 100644 Demo/Demo/Persistence.swift create mode 100644 Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Demo/Demo/providers/AppServiceProvider.swift create mode 100644 Demo/Demo/providers/RouteServiceProvider.swift create mode 100644 Sources/DFService/DF.swift create mode 100644 Sources/DFService/DFError.swift delete mode 100644 Sources/DFService/DFService.swift create mode 100644 Sources/DFService/ServiceValues.swift delete mode 100644 Sources/DFService/contract/AppContract.swift create mode 100644 Sources/DFService/contract/DFApiCall.swift create mode 100644 Sources/DFService/contract/DFApiKey.swift create mode 100644 Sources/DFService/contract/DFApplication.swift create mode 100644 Sources/DFService/core/FactoryKey.swift create mode 100644 Sources/DFService/core/ServiceName.swift create mode 100644 Sources/DFService/core/ServiceProvider.swift create mode 100644 Sources/DFService/service/BootstrapServiceProvider.swift create mode 100644 Sources/DFService/service/LogService.swift create mode 100644 Sources/DFService/service/MockApiService.swift create mode 100644 Sources/DFService/utils/Runtime.swift diff --git a/.gitignore b/.gitignore index b748582..bef3f63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,73 @@ +# Xcode .DS_Store -/.build -/Packages -/*.xcodeproj +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +.idea +*.iml +## User settings xcuserdata/ -DerivedData/ -.swiftpm/config/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +## Xcode Patch +/*.xcodeproj +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +.swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output +.env +**/Package.resolved + .vscode/ diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..95d2fd6 --- /dev/null +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -0,0 +1,442 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 8F20EA202C778B4900A2737C /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F20EA1F2C778B4900A2737C /* DemoApp.swift */; }; + 8F20EA222C778B4900A2737C /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F20EA212C778B4900A2737C /* Persistence.swift */; }; + 8F20EA252C778B4900A2737C /* Demo.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 8F20EA232C778B4900A2737C /* Demo.xcdatamodeld */; }; + 8F20EA272C778B4900A2737C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F20EA262C778B4900A2737C /* ContentView.swift */; }; + 8F20EA292C778B4C00A2737C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F20EA282C778B4C00A2737C /* Assets.xcassets */; }; + 8F20EA2D2C778B4C00A2737C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F20EA2C2C778B4C00A2737C /* Preview Assets.xcassets */; }; + 8F20EA392C778D6B00A2737C /* DFService in Frameworks */ = {isa = PBXBuildFile; productRef = 8F20EA382C778D6B00A2737C /* DFService */; }; + 8F20EA3C2C77AEFA00A2737C /* RouteServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F20EA3B2C77AEFA00A2737C /* RouteServiceProvider.swift */; }; + 8F20EA3E2C77AF2B00A2737C /* AppServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F20EA3D2C77AF2B00A2737C /* AppServiceProvider.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8F20EA1C2C778B4900A2737C /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8F20EA1F2C778B4900A2737C /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; }; + 8F20EA212C778B4900A2737C /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + 8F20EA242C778B4900A2737C /* Demo.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Demo.xcdatamodel; sourceTree = ""; }; + 8F20EA262C778B4900A2737C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 8F20EA282C778B4C00A2737C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8F20EA2A2C778B4C00A2737C /* Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Demo.entitlements; sourceTree = ""; }; + 8F20EA2C2C778B4C00A2737C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 8F20EA342C778C2700A2737C /* DFService */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DFService; path = ..; sourceTree = ""; }; + 8F20EA3B2C77AEFA00A2737C /* RouteServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteServiceProvider.swift; sourceTree = ""; }; + 8F20EA3D2C77AF2B00A2737C /* AppServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppServiceProvider.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8F20EA192C778B4900A2737C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8F20EA392C778D6B00A2737C /* DFService in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8F20EA132C778B4900A2737C = { + isa = PBXGroup; + children = ( + 8F20EA332C778C2700A2737C /* Packages */, + 8F20EA1E2C778B4900A2737C /* Demo */, + 8F20EA1D2C778B4900A2737C /* Products */, + 8F20EA372C778D6B00A2737C /* Frameworks */, + ); + sourceTree = ""; + }; + 8F20EA1D2C778B4900A2737C /* Products */ = { + isa = PBXGroup; + children = ( + 8F20EA1C2C778B4900A2737C /* Demo.app */, + ); + name = Products; + sourceTree = ""; + }; + 8F20EA1E2C778B4900A2737C /* Demo */ = { + isa = PBXGroup; + children = ( + 8F20EA3A2C77AED800A2737C /* providers */, + 8F20EA1F2C778B4900A2737C /* DemoApp.swift */, + 8F20EA212C778B4900A2737C /* Persistence.swift */, + 8F20EA262C778B4900A2737C /* ContentView.swift */, + 8F20EA282C778B4C00A2737C /* Assets.xcassets */, + 8F20EA2A2C778B4C00A2737C /* Demo.entitlements */, + 8F20EA232C778B4900A2737C /* Demo.xcdatamodeld */, + 8F20EA2B2C778B4C00A2737C /* Preview Content */, + ); + path = Demo; + sourceTree = ""; + }; + 8F20EA2B2C778B4C00A2737C /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8F20EA2C2C778B4C00A2737C /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 8F20EA332C778C2700A2737C /* Packages */ = { + isa = PBXGroup; + children = ( + 8F20EA342C778C2700A2737C /* DFService */, + ); + name = Packages; + sourceTree = ""; + }; + 8F20EA372C778D6B00A2737C /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 8F20EA3A2C77AED800A2737C /* providers */ = { + isa = PBXGroup; + children = ( + 8F20EA3B2C77AEFA00A2737C /* RouteServiceProvider.swift */, + 8F20EA3D2C77AF2B00A2737C /* AppServiceProvider.swift */, + ); + path = providers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8F20EA1B2C778B4900A2737C /* Demo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8F20EA302C778B4C00A2737C /* Build configuration list for PBXNativeTarget "Demo" */; + buildPhases = ( + 8F20EA182C778B4900A2737C /* Sources */, + 8F20EA192C778B4900A2737C /* Frameworks */, + 8F20EA1A2C778B4900A2737C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 8F20EA362C778C3900A2737C /* PBXTargetDependency */, + ); + name = Demo; + packageProductDependencies = ( + 8F20EA382C778D6B00A2737C /* DFService */, + ); + productName = Demo; + productReference = 8F20EA1C2C778B4900A2737C /* Demo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8F20EA142C778B4900A2737C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + 8F20EA1B2C778B4900A2737C = { + CreatedOnToolsVersion = 14.2; + }; + }; + }; + buildConfigurationList = 8F20EA172C778B4900A2737C /* Build configuration list for PBXProject "Demo" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8F20EA132C778B4900A2737C; + productRefGroup = 8F20EA1D2C778B4900A2737C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8F20EA1B2C778B4900A2737C /* Demo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8F20EA1A2C778B4900A2737C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8F20EA2D2C778B4C00A2737C /* Preview Assets.xcassets in Resources */, + 8F20EA292C778B4C00A2737C /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8F20EA182C778B4900A2737C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8F20EA3E2C77AF2B00A2737C /* AppServiceProvider.swift in Sources */, + 8F20EA252C778B4900A2737C /* Demo.xcdatamodeld in Sources */, + 8F20EA3C2C77AEFA00A2737C /* RouteServiceProvider.swift in Sources */, + 8F20EA202C778B4900A2737C /* DemoApp.swift in Sources */, + 8F20EA272C778B4900A2737C /* ContentView.swift in Sources */, + 8F20EA222C778B4900A2737C /* Persistence.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 8F20EA362C778C3900A2737C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 8F20EA352C778C3900A2737C /* DFService */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 8F20EA2E2C778B4C00A2737C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8F20EA2F2C778B4C00A2737C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 8F20EA312C778B4C00A2737C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; + DEVELOPMENT_TEAM = DVP3U56PW8; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.yaochenfeng.Demo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8F20EA322C778B4C00A2737C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; + DEVELOPMENT_TEAM = DVP3U56PW8; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.yaochenfeng.Demo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8F20EA172C778B4900A2737C /* Build configuration list for PBXProject "Demo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8F20EA2E2C778B4C00A2737C /* Debug */, + 8F20EA2F2C778B4C00A2737C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8F20EA302C778B4C00A2737C /* Build configuration list for PBXNativeTarget "Demo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8F20EA312C778B4C00A2737C /* Debug */, + 8F20EA322C778B4C00A2737C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 8F20EA352C778C3900A2737C /* DFService */ = { + isa = XCSwiftPackageProductDependency; + productName = DFService; + }; + 8F20EA382C778D6B00A2737C /* DFService */ = { + isa = XCSwiftPackageProductDependency; + productName = DFService; + }; +/* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + 8F20EA232C778B4900A2737C /* Demo.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 8F20EA242C778B4900A2737C /* Demo.xcdatamodel */, + ); + currentVersion = 8F20EA242C778B4900A2737C /* Demo.xcdatamodel */; + path = Demo.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 8F20EA142C778B4900A2737C /* Project object */; +} diff --git a/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json b/Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..532cd72 --- /dev/null +++ b/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,63 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Demo/Assets.xcassets/Contents.json b/Demo/Demo/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/Demo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Demo/ContentView.swift b/Demo/Demo/ContentView.swift new file mode 100644 index 0000000..49cdb34 --- /dev/null +++ b/Demo/Demo/ContentView.swift @@ -0,0 +1,91 @@ +// +// ContentView.swift +// Demo +// +// Created by yaochenfeng on 2024/8/22. +// + +import SwiftUI +import CoreData + +struct ContentView: View { + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], + animation: .default) + private var items: FetchedResults + + var body: some View { + NavigationView { + List { + ForEach(items) { item in + NavigationLink { + Text("Item at \(item.timestamp!, formatter: itemFormatter)") + } label: { + Text(item.timestamp!, formatter: itemFormatter) + } + } + .onDelete(perform: deleteItems) + } + .toolbar { +#if os(iOS) + ToolbarItem(placement: .navigationBarTrailing) { + EditButton() + } +#endif + ToolbarItem { + Button(action: addItem) { + Label("Add Item", systemImage: "plus") + } + } + } + Text("Select an item") + } + } + + private func addItem() { + withAnimation { + let newItem = Item(context: viewContext) + newItem.timestamp = Date() + + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } + + private func deleteItems(offsets: IndexSet) { + withAnimation { + offsets.map { items[$0] }.forEach(viewContext.delete) + + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } +} + +private let itemFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .medium + return formatter +}() + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + .environment(\.managedObjectContext, PersistenceController.runtime.container.viewContext) + } +} diff --git a/Demo/Demo/Demo.entitlements b/Demo/Demo/Demo.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/Demo/Demo/Demo.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Demo/Demo/Demo.xcdatamodeld/.xccurrentversion b/Demo/Demo/Demo.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..00318b0 --- /dev/null +++ b/Demo/Demo/Demo.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Demo.xcdatamodel + + diff --git a/Demo/Demo/Demo.xcdatamodeld/Demo.xcdatamodel/contents b/Demo/Demo/Demo.xcdatamodeld/Demo.xcdatamodel/contents new file mode 100644 index 0000000..9ed2921 --- /dev/null +++ b/Demo/Demo/Demo.xcdatamodeld/Demo.xcdatamodel/contents @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Demo/Demo/DemoApp.swift b/Demo/Demo/DemoApp.swift new file mode 100644 index 0000000..efdd453 --- /dev/null +++ b/Demo/Demo/DemoApp.swift @@ -0,0 +1,37 @@ +// +// DemoApp.swift +// Demo +// +// Created by yaochenfeng on 2024/8/22. +// + +import SwiftUI +import DFService +@main +struct DemoApp: App { + init() { + context.bootstrap(.eager) + } + @ObservedObject + var context = AppContext() + @SceneBuilder + public var body: some Scene { + WindowGroup { + context.rootView + } + } +} + +extension DemoApp { + class AppContext: DFApplication, ObservableObject { + var providerType: [ServiceProvider.Type] { + return [AppServiceProvider.self] + } + + var loadProviders: [ServiceProvider] = [] + + @Published + var rootView: AnyView = AnyView(EmptyView()) + } +} + diff --git a/Demo/Demo/Persistence.swift b/Demo/Demo/Persistence.swift new file mode 100644 index 0000000..2b53281 --- /dev/null +++ b/Demo/Demo/Persistence.swift @@ -0,0 +1,63 @@ +// +// Persistence.swift +// Demo +// +// Created by yaochenfeng on 2024/8/22. +// + +import CoreData +import DFService +struct PersistenceController { + static var runtime: PersistenceController { + if DF.runtime.isPreview { + return Self.preview + } else { + return Self.shared + } + } + + fileprivate static let shared = PersistenceController() + fileprivate static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + for _ in 0..<10 { + let newItem = Item(context: viewContext) + newItem.timestamp = Date() + } + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() + + let container: NSPersistentContainer + + init(inMemory: Bool = false) { + container = NSPersistentContainer(name: "Demo") + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + container.viewContext.automaticallyMergesChangesFromParent = true + } +} diff --git a/Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json b/Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Demo/providers/AppServiceProvider.swift b/Demo/Demo/providers/AppServiceProvider.swift new file mode 100644 index 0000000..0a2e331 --- /dev/null +++ b/Demo/Demo/providers/AppServiceProvider.swift @@ -0,0 +1,33 @@ +import DFService + +class AppServiceProvider: ServiceProvider { + override func register() { + for provider in appProvider { + app.provider(provider) + } + super.register() + } + + var appProvider: [ServiceProvider.Type] { + return [ + RouteServiceProvider.self, + ] + } + + override var when: ProviderWhen { + .eager + } + + override func performAsyncStartup() async { + DispatchQueue.main.async { + self.app.rootView = AnyView(self.buildRoot()) + } + } + + + @ViewBuilder + func buildRoot() -> some View { + ContentView() + .environment(\.managedObjectContext, PersistenceController.runtime.container.viewContext) + } +} diff --git a/Demo/Demo/providers/RouteServiceProvider.swift b/Demo/Demo/providers/RouteServiceProvider.swift new file mode 100644 index 0000000..b0793c2 --- /dev/null +++ b/Demo/Demo/providers/RouteServiceProvider.swift @@ -0,0 +1,17 @@ +import DFService +import SwiftUI +class RouteServiceProvider: ServiceProvider { + + override func performAsyncStartup() async { + app.rootView = AnyView(buildRoot()) + } + override var when: ServiceProvider.ProviderWhen { + return .window + } + + + @ViewBuilder + func buildRoot() -> some View { + Text("root View") + } +} diff --git a/Package.swift b/Package.swift index 3dc3e5d..1c7c5d3 100644 --- a/Package.swift +++ b/Package.swift @@ -17,17 +17,23 @@ let package = Package( .library( name: "DFService", targets: ["DFService"]), + .library( + name: "DFService-Dynamic", + type: .dynamic, + targets: ["DFService"]), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), +// .package(url: "https://github.com/hmlongco/Factory.git", from: "2.3.0") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "DFService", - dependencies: []), + dependencies: [ +// .product(name: "Factory", package: "Factory") + ] + ), .testTarget( name: "DFServiceTests", dependencies: ["DFService"]), diff --git a/README.md b/README.md index e5f476c..0460118 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 常用协议 -- AppContract 应用 +- DFApplication 应用 ## 命名空间 app - UIView/NSView 转SwiftUI View diff --git a/Sources/DFService/DF.swift b/Sources/DFService/DF.swift new file mode 100644 index 0000000..e4090bd --- /dev/null +++ b/Sources/DFService/DF.swift @@ -0,0 +1,6 @@ +public struct DF { + public static let version = "0.1.0" +} + + +@_exported import SwiftUI diff --git a/Sources/DFService/DFError.swift b/Sources/DFService/DFError.swift new file mode 100644 index 0000000..c2da9c0 --- /dev/null +++ b/Sources/DFService/DFError.swift @@ -0,0 +1,14 @@ +import Foundation +/// 常用错误定义 +public enum DFError: Error { + /// 业务错误 + case biz(code: Int, msg: String) + /// 未实现 + case unImplemented(String = #function) + /// 自定义错误 + case custom(Error) + /// 未找到 + case notFound(Any = ()) + /// 转换失败 + case convert(from: String, to: String) +} diff --git a/Sources/DFService/DFService.swift b/Sources/DFService/DFService.swift deleted file mode 100644 index 288fdf8..0000000 --- a/Sources/DFService/DFService.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct DFService { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Sources/DFService/ServiceValues.swift b/Sources/DFService/ServiceValues.swift new file mode 100644 index 0000000..d22f590 --- /dev/null +++ b/Sources/DFService/ServiceValues.swift @@ -0,0 +1,29 @@ +public struct ServiceValues { + public static var shared = ServiceValues() + private var storage: [FactoryKey: Any] = [:] + + public init() {} + + public subscript(_ service: Service.Type) -> Service.Value { + get { + let key = FactoryKey(type: service) + if let value = storage[key] as? Service.Value { + return value + } + return service.defaultValue + } + set { + let key = FactoryKey(type: service, name: service.serviceName) + storage[key] = newValue + } + } + public func findBy(_ name: ServiceName) -> DFApiCall? { + guard let key = storage.keys.first(where: {$0.name == name}) else { + return nil + } + guard let value = storage[key] as? DFApiCall else { + return nil + } + return value + } +} diff --git a/Sources/DFService/contract/AppContract.swift b/Sources/DFService/contract/AppContract.swift deleted file mode 100644 index 3d8d227..0000000 --- a/Sources/DFService/contract/AppContract.swift +++ /dev/null @@ -1,3 +0,0 @@ -/// 应用协议 -public protocol AppContract { -} \ No newline at end of file diff --git a/Sources/DFService/contract/DFApiCall.swift b/Sources/DFService/contract/DFApiCall.swift new file mode 100644 index 0000000..ef459d5 --- /dev/null +++ b/Sources/DFService/contract/DFApiCall.swift @@ -0,0 +1,15 @@ +public class ApiCallConext { + let method: String + let param: Codable + + public init(method: String, param: Codable) { + self.method = method + self.param = param + } +} + +/// api调用 +public protocol DFApiCall { + /// 提供异步调用 + func callAsFunction( _ context: ApiCallConext) async throws -> Any +} diff --git a/Sources/DFService/contract/DFApiKey.swift b/Sources/DFService/contract/DFApiKey.swift new file mode 100644 index 0000000..c4291b4 --- /dev/null +++ b/Sources/DFService/contract/DFApiKey.swift @@ -0,0 +1,27 @@ +public protocol DFApiService { + associatedtype Value + static var defaultValue: Self.Value { get } + + static var serviceName: ServiceName { get } +} + +extension DFApplication { + public subscript(service: Service.Type) -> Service.Value { + get { + return serviceValues[service] + } + set { + ServiceValues.shared[service] = newValue + } + } +} + +protocol LogHandle { + func log() +} + +struct Mock: LogHandle { + func log() { + + } +} diff --git a/Sources/DFService/contract/DFApplication.swift b/Sources/DFService/contract/DFApplication.swift new file mode 100644 index 0000000..416cf8d --- /dev/null +++ b/Sources/DFService/contract/DFApplication.swift @@ -0,0 +1,44 @@ +import SwiftUI + +/// 应用协议 +public protocol DFApplication: AnyObject { + /// 应用默认注册的服务提供者 + var providerType: [ServiceProvider.Type] { get } + /// 已注册的服务提供者 + var loadProviders: [ServiceProvider] { get set } + var rootView: AnyView { get set } + var serviceValues: ServiceValues { get } +} + + +public extension DFApplication { + var serviceValues: ServiceValues { + return .shared + } + + @discardableResult + func provider(_ serviceType: T.Type = T.self) -> T { + + let loadProvider = loadProviders.first { loaded in + return loaded.name == serviceType.name + } + if let provider = loadProvider as? T { + return provider + } + let provider = serviceType.init(self) + loadProviders.append(provider) + provider.register() + self[LogService.self].debug("注册服务\(provider)") + return provider + } + + + func bootstrap(_ when: ServiceProvider.ProviderWhen) { + provider(BootstrapServiceProvider.self).bootstrap(when) + } + /// 重置启动,执行provider Shutdown + func reset(_ when: ServiceProvider.ProviderWhen) { + provider(BootstrapServiceProvider.self).bootstrap(when) + } + +} diff --git a/Sources/DFService/core/FactoryKey.swift b/Sources/DFService/core/FactoryKey.swift new file mode 100644 index 0000000..d1cf8f4 --- /dev/null +++ b/Sources/DFService/core/FactoryKey.swift @@ -0,0 +1,26 @@ +struct FactoryKey: Hashable { + @usableFromInline let type: ObjectIdentifier + @usableFromInline let key: String + let name: ServiceName + + @inlinable + @inline(__always) + init(type: Any.Type, key: String = "", name: ServiceName = ServiceName("")) { + self.type = ObjectIdentifier(type) + self.key = key + self.name = name + } + + @inlinable + @inline(__always) + public func hash(into hasher: inout Hasher) { + hasher.combine(self.type) + hasher.combine(self.key) + } + + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.type == rhs.type && lhs.key == rhs.key + } + +} diff --git a/Sources/DFService/core/ServiceName.swift b/Sources/DFService/core/ServiceName.swift new file mode 100644 index 0000000..f681786 --- /dev/null +++ b/Sources/DFService/core/ServiceName.swift @@ -0,0 +1,26 @@ +public struct ServiceName: RawRepresentable, Equatable, Hashable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + public init(_ value: String) { + self.rawValue = value + } + + + public static let logger = ServiceName("df.logger") + public static let router = ServiceName("df.router") +} + + +extension ServiceName: DFApiCall { + @discardableResult + public func callAsFunction(_ context: ApiCallConext) async throws -> Any { + + guard let value = ServiceValues.shared.findBy(self) else { + throw DFError.unImplemented() + } + return try await value.callAsFunction(context) + } +} diff --git a/Sources/DFService/core/ServiceProvider.swift b/Sources/DFService/core/ServiceProvider.swift new file mode 100644 index 0000000..a3710ce --- /dev/null +++ b/Sources/DFService/core/ServiceProvider.swift @@ -0,0 +1,68 @@ +/// 应用内服务提供者 +/// 在应用不同时机启动服务 +open class ServiceProvider { + public let app: DFApplication + /// 服务是否启动 + public internal(set) var isBooted: Bool = false + public required init(_ app: DFApplication) { + self.app = app + } + /// 注册服务 + open func register() {} + /// 模拟异步启动逻辑 + open func performAsyncStartup() async { + // 重写此方法以实现实际的异步启动逻辑 +// try? await Task.sleep(nanoseconds: 1_000_000_000)// 模拟1秒的异步启动 + } + + /// 模拟异步关闭逻辑 + open func performAsyncShutdown() async { + // 重写此方法以实现实际的异步关闭逻辑 +// try await Task.sleep(nanoseconds: 1_000_000_000) // 模拟1秒的异步关闭 + } + open var when: ProviderWhen { + return .root + } + + var name: String { + return Self.name + } +} + + +extension ServiceProvider: Equatable { + public static var name: String { + return String(reflecting: self.self) + } + public static func == (lhs: ServiceProvider, rhs: ServiceProvider) -> Bool { + return lhs.name == rhs.name + } +} + +extension ServiceProvider: Identifiable { + +} + +extension ServiceProvider { + /// 启动时机 + public struct ProviderWhen: RawRepresentable, Comparable { + public static func < (lhs: ProviderWhen, rhs: ProviderWhen) -> Bool { + return lhs.rawValue < rhs.rawValue + } + + public init(rawValue: Int) { + self.rawValue = rawValue + } + public let rawValue: Int + /// 默认启动 + public static let eager = ProviderWhen(rawValue: 0) + /// 窗口创建后启动 + public static let window = ProviderWhen(rawValue: 4) + /// 主窗口后启动 + public static let root = ProviderWhen(rawValue: 8) + + public static func + (lhs: ProviderWhen, rhs: Int) -> ProviderWhen { + return ProviderWhen(rawValue: lhs.rawValue + rhs) + } + } +} diff --git a/Sources/DFService/extension/app.bundle.swift b/Sources/DFService/extension/app.bundle.swift index 87bcc85..463aabf 100644 --- a/Sources/DFService/extension/app.bundle.swift +++ b/Sources/DFService/extension/app.bundle.swift @@ -1,30 +1,47 @@ import Foundation extension Space where Base == Bundle { - /// 应用名 egg: Example - public static var name: String { - Bundle.main.app.getInfo("CFBundleName") ?? "unkown" + /// 应用名 egg: Example + public static var name: String { + Bundle.main.app.getInfo("CFBundleName") ?? "unkown" + } + /// 应用名 egg: com.github.example + public static var id: String { + Bundle.main.app.getInfo("CFBundleIdentifier") ?? "unkown" + } + /// 版本 egg: 1.0.0 + public static var version: String { + Bundle.main.app.getInfo("CFBundleShortVersionString") ?? "0.0.0" + } + + /// build版本 egg: 1 + public static var buildVersion: String { + Bundle.main.app.getInfo("CFBundleVersion") ?? "0.0" + } + + /// 应用信息 + public func getInfo(_ key: String) -> T? { + guard let value = base.infoDictionary?[key] else { + return nil } - /// 应用名 egg: com.github.example - public static var id: String { - Bundle.main.app.getInfo("CFBundleIdentifier") ?? "unkown" + return BoxValue(value: value).optional() + } + /// 加载类 + public func loadClass(ofType type: T.Type, + named name: String? = nil) throws -> T.Type { + var name = name ?? String(reflecting: type.self) + if name.components(separatedBy: ".").count == 1, + let namespace: String = base.infoDictionary?["CFBundleExecutable"] as? String { + name = namespace.replacingOccurrences(of: " ", with: "_") + "." + name } - /// 版本 egg: 1.0.0 - public static var version: String { - Bundle.main.app.getInfo("CFBundleShortVersionString") ?? "0.0.0" + guard name.components(separatedBy: ".").count > 1 else { + throw DFError.notFound("module") } - - /// build版本 egg: 1 - public static var buildVersion: String { - Bundle.main.app.getInfo("CFBundleVersion") ?? "0.0" + guard let loadedClass = base.classNamed(name) else { throw DFError.notFound(type) } + guard let castedClass = loadedClass as? T.Type else { + throw DFError.convert(from: String(describing: loadedClass), to: name) } - /// 应用信息 - public func getInfo(_ key: String) -> T? { - guard let value = base.infoDictionary?[key] else { - return nil - } - return BoxValue(value: value).optional() - } + return castedClass + } } - diff --git a/Sources/DFService/extension/app.view.swift b/Sources/DFService/extension/app.view.swift index e23313b..39b3738 100644 --- a/Sources/DFService/extension/app.view.swift +++ b/Sources/DFService/extension/app.view.swift @@ -36,3 +36,14 @@ extension Space: View where Base: View { public typealias Body = Base } #endif + + +public extension View { + /// 链式操作 + func chain( + @ViewBuilder + transform: (Self) -> Content + ) -> some View { + transform(self) + } +} diff --git a/Sources/DFService/service/BootstrapServiceProvider.swift b/Sources/DFService/service/BootstrapServiceProvider.swift new file mode 100644 index 0000000..e68ab1e --- /dev/null +++ b/Sources/DFService/service/BootstrapServiceProvider.swift @@ -0,0 +1,79 @@ +import Foundation + +actor Bootstrap { + private weak var app: DFApplication? + + private var current: ServiceProvider.ProviderWhen = .eager + private var target: ServiceProvider.ProviderWhen = .eager + private var isRunning = false + init(app: DFApplication) { + self.app = app + } + + func bootstrap(_ when: ServiceProvider.ProviderWhen) async { + target = when + await bootIfNeed() + } + + func bootIfNeed() async { + guard let app = app else { + return + } + let currentProviders = app.loadProviders.filter { provider in + return !provider.isBooted && provider.when <= current + } + + guard !currentProviders.isEmpty else { + if current < target { + current = current + 1 + await bootIfNeed() + } + return + } + guard !isRunning else { + return + } + self.isRunning = true + for provider in currentProviders where !provider.isBooted { + let start_time = CFAbsoluteTimeGetCurrent() + var success = false + defer { + let end_time = CFAbsoluteTimeGetCurrent() + let cost_time = (end_time - start_time) * 1000 + app[LogService.self].debug("\(provider.name) 启动\(success)用时: \(cost_time)毫秒") + } + await provider.performAsyncStartup() + success = true + provider.isBooted = true + + } + self.isRunning = false + await bootIfNeed() + } +} + +class BootstrapServiceProvider: ServiceProvider { + let bootstrap: Bootstrap + required init(_ app: DFApplication) { + bootstrap = Bootstrap(app: app) + super.init(app) + } + override func register() { + app.providerType.forEach { serviceType in + app.provider(serviceType) + } + } + func bootstrap(_ when: ProviderWhen) { + Task(priority: .userInitiated){ + await bootstrap.bootstrap(when) + } + } + + func reset(_ when: ProviderWhen) { + + } + + override var when: ServiceProvider.ProviderWhen { + return .eager + } +} diff --git a/Sources/DFService/service/LogService.swift b/Sources/DFService/service/LogService.swift new file mode 100644 index 0000000..b3279da --- /dev/null +++ b/Sources/DFService/service/LogService.swift @@ -0,0 +1,17 @@ +public protocol DFLogHandle { + func debug(_ msg: String) +} + +public struct LogService: DFApiService { + public static let serviceName: ServiceName = .logger + + public static var defaultValue: DFLogHandle { + return MockApiService() + } +} + +extension MockApiService: DFLogHandle { + func debug(_ msg: String) { + debugPrint(msg) + } +} diff --git a/Sources/DFService/service/MockApiService.swift b/Sources/DFService/service/MockApiService.swift new file mode 100644 index 0000000..aae5caa --- /dev/null +++ b/Sources/DFService/service/MockApiService.swift @@ -0,0 +1,2 @@ +struct MockApiService { +} diff --git a/Sources/DFService/utils/Runtime.swift b/Sources/DFService/utils/Runtime.swift new file mode 100644 index 0000000..9b2890e --- /dev/null +++ b/Sources/DFService/utils/Runtime.swift @@ -0,0 +1,22 @@ +import Foundation + +extension DF { + public static let runtime = Runtime() + + public struct Runtime { + } +} +public extension DF.Runtime { + var isPreview: Bool { + return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + } + var isRunningInTests: Bool { + return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + } +} + +public extension DFApplication { + var runtime: DF.Runtime { + return DF.runtime + } +} diff --git a/Tests/DFServiceTests/DFServiceTests.swift b/Tests/DFServiceTests/DFServiceTests.swift index 9757ff6..c0493c6 100644 --- a/Tests/DFServiceTests/DFServiceTests.swift +++ b/Tests/DFServiceTests/DFServiceTests.swift @@ -2,10 +2,30 @@ import XCTest @testable import DFService final class DFServiceTests: XCTestCase { - func testExample() throws { + func testExample() async throws { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(DFService().text, "Hello, World!") + XCTAssertEqual(DF.version, "0.1.0") + XCTAssertEqual(DF.runtime.isRunningInTests, true) + } + + func testApiCalll() async throws { + let mockLog = MockLog() + ServiceValues.shared[LogService.self] = mockLog + try await ServiceName.logger(ApiCallConext(method: "debug", param: "你好呀")) + } +} + + +class MockLog: DFLogHandle, DFApiCall { + func callAsFunction(_ context: DFService.ApiCallConext) async throws -> Any { + return () + } + + func debug(_ msg: String) { + + } + }