diff --git a/.gitignore b/.gitignore
index 330d167..0023a53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,90 +1,8 @@
-# Xcode
-#
-# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-
-## User settings
+.DS_Store
+/.build
+/Packages
xcuserdata/
-
-## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
-*.xcscmblueprint
-*.xccheckout
-
-## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
-build/
DerivedData/
-*.moved-aside
-*.pbxuser
-!default.pbxuser
-*.mode1v3
-!default.mode1v3
-*.mode2v3
-!default.mode2v3
-*.perspectivev3
-!default.perspectivev3
-
-## 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 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/
-
-# Accio dependency management
-Dependencies/
-.accio/
-
-# 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
-
-# Code Injection
-#
-# After new code Injection tools there's a generated folder /iOSInjectionProject
-# https://github.com/johnno1962/injectionforxcode
-
-iOSInjectionProject/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ExampleApp/ExampleApp.xcodeproj/project.pbxproj b/ExampleApp/ExampleApp.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..eff94c5
--- /dev/null
+++ b/ExampleApp/ExampleApp.xcodeproj/project.pbxproj
@@ -0,0 +1,388 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 60;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 774AAC8B2ABB281900593391 /* MLModelManager in Frameworks */ = {isa = PBXBuildFile; productRef = 774AAC8A2ABB281900593391 /* MLModelManager */; };
+ 774AAC8D2ABB283B00593391 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774AAC8C2ABB283B00593391 /* ViewModel.swift */; };
+ 775D3CD82ABC367A00690B59 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775D3CD72ABC367A00690B59 /* Constants.swift */; };
+ 775D3CDA2ABC6ED200690B59 /* YOLOv3Int8LUT.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 775D3CD92ABC6ED200690B59 /* YOLOv3Int8LUT.mlmodel */; };
+ 77E009892ABB27D9006B2508 /* ExampleAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E009882ABB27D9006B2508 /* ExampleAppApp.swift */; };
+ 77E0098B2ABB27D9006B2508 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E0098A2ABB27D9006B2508 /* ContentView.swift */; };
+ 77E0098D2ABB27DC006B2508 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77E0098C2ABB27DC006B2508 /* Assets.xcassets */; };
+ 77E009902ABB27DC006B2508 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 77E0098F2ABB27DC006B2508 /* Preview Assets.xcassets */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 774AAC8C2ABB283B00593391 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; };
+ 775D3CD72ABC367A00690B59 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
+ 775D3CD92ABC6ED200690B59 /* YOLOv3Int8LUT.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; path = YOLOv3Int8LUT.mlmodel; sourceTree = ""; };
+ 77E009852ABB27D9006B2508 /* ExampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 77E009882ABB27D9006B2508 /* ExampleAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppApp.swift; sourceTree = ""; };
+ 77E0098A2ABB27D9006B2508 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 77E0098C2ABB27DC006B2508 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 77E0098F2ABB27DC006B2508 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 77E009822ABB27D9006B2508 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 774AAC8B2ABB281900593391 /* MLModelManager in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 775D3CDB2ABC6FC700690B59 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 775D3CD92ABC6ED200690B59 /* YOLOv3Int8LUT.mlmodel */,
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+ 77E0097C2ABB27D9006B2508 = {
+ isa = PBXGroup;
+ children = (
+ 77E009872ABB27D9006B2508 /* ExampleApp */,
+ 77E009862ABB27D9006B2508 /* Products */,
+ );
+ sourceTree = "";
+ };
+ 77E009862ABB27D9006B2508 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 77E009852ABB27D9006B2508 /* ExampleApp.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 77E009872ABB27D9006B2508 /* ExampleApp */ = {
+ isa = PBXGroup;
+ children = (
+ 775D3CDB2ABC6FC700690B59 /* Resources */,
+ 77E009882ABB27D9006B2508 /* ExampleAppApp.swift */,
+ 77E0098A2ABB27D9006B2508 /* ContentView.swift */,
+ 77E0098C2ABB27DC006B2508 /* Assets.xcassets */,
+ 77E0098E2ABB27DC006B2508 /* Preview Content */,
+ 774AAC8C2ABB283B00593391 /* ViewModel.swift */,
+ 775D3CD72ABC367A00690B59 /* Constants.swift */,
+ );
+ path = ExampleApp;
+ sourceTree = "";
+ };
+ 77E0098E2ABB27DC006B2508 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 77E0098F2ABB27DC006B2508 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 77E009842ABB27D9006B2508 /* ExampleApp */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 77E009932ABB27DC006B2508 /* Build configuration list for PBXNativeTarget "ExampleApp" */;
+ buildPhases = (
+ 77E009812ABB27D9006B2508 /* Sources */,
+ 77E009822ABB27D9006B2508 /* Frameworks */,
+ 77E009832ABB27D9006B2508 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = ExampleApp;
+ packageProductDependencies = (
+ 774AAC8A2ABB281900593391 /* MLModelManager */,
+ );
+ productName = ExampleApp;
+ productReference = 77E009852ABB27D9006B2508 /* ExampleApp.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 77E0097D2ABB27D9006B2508 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1500;
+ TargetAttributes = {
+ 77E009842ABB27D9006B2508 = {
+ CreatedOnToolsVersion = 15.0;
+ };
+ };
+ };
+ buildConfigurationList = 77E009802ABB27D9006B2508 /* Build configuration list for PBXProject "ExampleApp" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 77E0097C2ABB27D9006B2508;
+ packageReferences = (
+ 774AAC892ABB281900593391 /* XCLocalSwiftPackageReference ".." */,
+ );
+ productRefGroup = 77E009862ABB27D9006B2508 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 77E009842ABB27D9006B2508 /* ExampleApp */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 77E009832ABB27D9006B2508 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 77E009902ABB27DC006B2508 /* Preview Assets.xcassets in Resources */,
+ 77E0098D2ABB27DC006B2508 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 77E009812ABB27D9006B2508 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 77E0098B2ABB27D9006B2508 /* ContentView.swift in Sources */,
+ 775D3CDA2ABC6ED200690B59 /* YOLOv3Int8LUT.mlmodel in Sources */,
+ 774AAC8D2ABB283B00593391 /* ViewModel.swift in Sources */,
+ 77E009892ABB27D9006B2508 /* ExampleAppApp.swift in Sources */,
+ 775D3CD82ABC367A00690B59 /* Constants.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 77E009912ABB27DC006B2508 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ 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;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ 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 = 17.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 77E009922ABB27DC006B2508 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ 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;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ 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 = 17.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 77E009942ABB27DC006B2508 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"ExampleApp/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = sarama.ExampleApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 77E009952ABB27DC006B2508 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"ExampleApp/Preview Content\"";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = sarama.ExampleApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 77E009802ABB27D9006B2508 /* Build configuration list for PBXProject "ExampleApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 77E009912ABB27DC006B2508 /* Debug */,
+ 77E009922ABB27DC006B2508 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 77E009932ABB27DC006B2508 /* Build configuration list for PBXNativeTarget "ExampleApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 77E009942ABB27DC006B2508 /* Debug */,
+ 77E009952ABB27DC006B2508 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 774AAC892ABB281900593391 /* XCLocalSwiftPackageReference ".." */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = ..;
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 774AAC8A2ABB281900593391 /* MLModelManager */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = MLModelManager;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 77E0097D2ABB27D9006B2508 /* Project object */;
+}
diff --git a/ExampleApp/ExampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ExampleApp/ExampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/ExampleApp/ExampleApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ExampleApp/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ExampleApp/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ExampleApp/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ExampleApp/ExampleApp/Assets.xcassets/AccentColor.colorset/Contents.json b/ExampleApp/ExampleApp/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/ExampleApp/ExampleApp/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ExampleApp/ExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/ExampleApp/ExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..13613e3
--- /dev/null
+++ b/ExampleApp/ExampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ExampleApp/ExampleApp/Assets.xcassets/Contents.json b/ExampleApp/ExampleApp/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ExampleApp/ExampleApp/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ExampleApp/ExampleApp/Constants.swift b/ExampleApp/ExampleApp/Constants.swift
new file mode 100644
index 0000000..c47a4c3
--- /dev/null
+++ b/ExampleApp/ExampleApp/Constants.swift
@@ -0,0 +1,15 @@
+//
+// Constants.swift
+// ExampleApp
+//
+// Created by Egzon Arifi on 21/09/2023.
+//
+
+import Foundation
+
+enum Constants {
+ enum Supabase {
+ static let url = URL(string: "your_base_url")!
+ static let apiKey = "your_api_key"
+ }
+}
diff --git a/ExampleApp/ExampleApp/ContentView.swift b/ExampleApp/ExampleApp/ContentView.swift
new file mode 100644
index 0000000..652a4cd
--- /dev/null
+++ b/ExampleApp/ExampleApp/ContentView.swift
@@ -0,0 +1,37 @@
+//
+// ContentView.swift
+// ExampleApp
+//
+// Created by Egzon Arifi on 20/09/2023.
+//
+
+import SwiftUI
+
+struct ContentView: View {
+ @ObservedObject private var viewModel: ViewModel = .init()
+
+ var body: some View {
+ VStack {
+ Image(systemName: "globe")
+ .imageScale(.large)
+ .foregroundStyle(.tint)
+ Text("Hello, world!")
+ if let description = viewModel.modelDescription {
+ Text(description)
+ }
+ if let progress = viewModel.downloadProgress {
+ ProgressView("Downloading", value: progress, total: 1.0)
+ } else {
+ Text("The Model is ready!").fontWeight(.bold)
+ }
+ }
+ .padding()
+ .task {
+ await viewModel.getYoloModel()
+ }
+ }
+}
+
+#Preview {
+ ContentView()
+}
diff --git a/ExampleApp/ExampleApp/ExampleAppApp.swift b/ExampleApp/ExampleApp/ExampleAppApp.swift
new file mode 100644
index 0000000..5361485
--- /dev/null
+++ b/ExampleApp/ExampleApp/ExampleAppApp.swift
@@ -0,0 +1,17 @@
+//
+// ExampleAppApp.swift
+// ExampleApp
+//
+// Created by Egzon Arifi on 20/09/2023.
+//
+
+import SwiftUI
+
+@main
+struct ExampleAppApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/ExampleApp/ExampleApp/Preview Content/Preview Assets.xcassets/Contents.json b/ExampleApp/ExampleApp/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ExampleApp/ExampleApp/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ExampleApp/ExampleApp/Resources/YOLOv3Int8LUT.mlmodel b/ExampleApp/ExampleApp/Resources/YOLOv3Int8LUT.mlmodel
new file mode 100644
index 0000000..a7367b9
Binary files /dev/null and b/ExampleApp/ExampleApp/Resources/YOLOv3Int8LUT.mlmodel differ
diff --git a/ExampleApp/ExampleApp/ViewModel.swift b/ExampleApp/ExampleApp/ViewModel.swift
new file mode 100644
index 0000000..2089816
--- /dev/null
+++ b/ExampleApp/ExampleApp/ViewModel.swift
@@ -0,0 +1,41 @@
+//
+// ViewModel.swift
+// ExampleApp
+//
+// Created by Egzon Arifi on 20/09/2023.
+//
+
+import Foundation
+import MLModelManager
+
+class ViewModel: ObservableObject {
+ private let mlModelManager: MLModelManager
+ @Published var modelDescription: String?
+ @Published var downloadProgress: Float?
+
+ init(mlModelManager: MLModelManager = .makeWithSupabase(baseURL: Constants.Supabase.url, apiKey: Constants.Supabase.apiKey)) {
+ self.mlModelManager = mlModelManager
+ }
+
+ func getYoloModel() async {
+ await mlModelManager.getModel(modelName: "Yolo",
+ bundledModelURL: Bundle.main.url(forResource: "YOLOv3Int8LUT", withExtension: ".mlmodelc")) { progress in
+ print("Yolo Progress: \(progress)")
+ self.downloadProgress = progress
+ } completion: { result, isFinal in
+
+ if isFinal {
+ self.downloadProgress = nil
+ }
+
+ switch result {
+ case .success(let model):
+ print(model.modelDescription)
+ self.modelDescription = model.description
+ case .failure(let error):
+ print(error.localizedDescription)
+ self.modelDescription = error.localizedDescription
+ }
+ }
+ }
+}
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..2ca527a
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,24 @@
+// swift-tools-version: 5.9
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "MLModelManager",
+ platforms: [.iOS(.v16), .macOS(.v13)],
+ products: [
+ // Products define the executables and libraries a package produces, making them visible to other packages.
+ .library(
+ name: "MLModelManager",
+ targets: ["MLModelManager"]),
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package, defining a module or a test suite.
+ // Targets can depend on other targets in this package and products from dependencies.
+ .target(
+ name: "MLModelManager"),
+ .testTarget(
+ name: "MLModelManagerTests",
+ dependencies: ["MLModelManager"]),
+ ]
+)
diff --git a/README.md b/README.md
index b93e068..d7479f1 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,93 @@
-# MLModelManager
\ No newline at end of file
+# MLModelManager SDK for iOS
+
+## Overview
+
+`MLModelManager` is an iOS SDK designed to streamline the process of fetching, managing, and compiling machine learning models. It offers seamless integration with your iOS apps, ensuring you can efficiently check local models, download new versions, and compile them for use.
+
+Features:
+
+- **Local Model Checking**: Quickly determine if the local version of a model matches the latest one.
+- **Automatic Downloads**: If a newer version of a model exists, the SDK can fetch it for you.
+- **Model Compilation**: Compile downloaded models, readying them for integration with your app.
+- **Model Storage**: Models are stored locally and can be managed to ensure only the latest version is kept.
+- **Seamless Integration**: Designed with Swift's modern features in mind, it integrates well with other iOS components.
+
+### Installation Steps:
+
+#### Prerequisites:
+
+- iOS 16.0 or later.
+- Xcode 13.0 or later.
+
+#### Swift Package Manager:
+
+Add the following lines to your `Package.swift` file:
+
+```swift
+let package = Package(
+ ...
+ dependencies: [
+ ...
+ .package(name: "MLModelManager", url: "https://github.com/saramaxyz/MLModelManager.git", branch: "main"), // Add the package
+ ],
+ targets: [
+ .target(
+ name: "YourTargetName",
+ dependencies: ["MLModelManager"] // Add as a dependency
+ )
+ ]
+)
+```
+
+Or in Xcode, File > Add Package Dependency and add the url:
+
+```url
+https://github.com/saramaxyz/MLModelManager.git
+```
+
+## Usage
+
+### Initialization:
+
+To start using `MLModelManager`, you'll first need to initialize it using your `apiKey`. Here's how you can do it:
+
+```swift
+import MLModelManager
+
+let apiKey = "YOUR_API_KEY_HERE"
+let modelManager = MLModelManager.make(apiKey: apiKey)
+```
+
+### Fetching a Model:
+
+After initializing the manager, fetching a model becomes a breeze.
+
+```swift
+modelManager.getModel(modelName: "YourModelName", bundledModelURL: nil, progress: { progress in
+ print("Download Progress: \(progress)")
+}, completion: { result, isFinal in
+ switch result {
+ case .success(let model):
+ // Use the fetched model
+ print(model.modelDescription)
+ case .failure(let error):
+ // Handle the error
+ print(error.localizedDescription)
+ }
+})
+
+```
+
+Replace `"YourModelName"` with the name of the model you wish to fetch.
+
+## Contributing
+
+If you find any bugs or have a feature request, please open an issue on GitHub. Contributions, issues, and feature requests are welcome!
+
+## Support
+
+For major concerns or assistance, you can reach out to the team directly @[AeroEdge](https://aeroedgeai.com)
+
+## License
+
+This SDK is under a specific license [MIT Licence](https://github.com/saramaxyz/MLModelManager/blob/develop/LICENSE). All rights reserved.
diff --git a/Sources/MLModelManager/Domain/Entities/ModelEntity.swift b/Sources/MLModelManager/Domain/Entities/ModelEntity.swift
new file mode 100644
index 0000000..ee41c3f
--- /dev/null
+++ b/Sources/MLModelManager/Domain/Entities/ModelEntity.swift
@@ -0,0 +1,18 @@
+//
+// ModelEntity.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+public struct ModelEntity {
+ public let name: String
+ public let version: Int
+ public let url: URL
+
+ public var versionedName: String {
+ "\(name)_\(version)"
+ }
+}
diff --git a/Sources/MLModelManager/Domain/Entities/ModelError.swift b/Sources/MLModelManager/Domain/Entities/ModelError.swift
new file mode 100644
index 0000000..e3f173a
--- /dev/null
+++ b/Sources/MLModelManager/Domain/Entities/ModelError.swift
@@ -0,0 +1,40 @@
+//
+// ModelError.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+public enum ModelError: Error {
+ case invalidURL
+ case fileNotFound
+ case networkError
+ case fileWriteError
+ case downloadError(String)
+ case compilationError(String)
+ case failedToLoadModel(String)
+ case modelNotFound
+
+ var localizedDescription: String {
+ switch self {
+ case .invalidURL:
+ return "The URL provided is invalid."
+ case .fileNotFound:
+ return "The file could not be found on the device."
+ case .downloadError(let message):
+ return "An error occurred during the download: \(message)"
+ case .compilationError(let message):
+ return "An error occurred during the compilation: \(message)"
+ case .networkError:
+ return "An error occurred during the network request"
+ case .fileWriteError:
+ return "An error occurred during the file writing"
+ case .failedToLoadModel(let message):
+ return "An error occurred while loading the model: \(message)"
+ case .modelNotFound:
+ return "The model not found!"
+ }
+ }
+}
diff --git a/Sources/MLModelManager/Domain/UseCases/ModelCheckerUseCase.swift b/Sources/MLModelManager/Domain/UseCases/ModelCheckerUseCase.swift
new file mode 100644
index 0000000..6ce847a
--- /dev/null
+++ b/Sources/MLModelManager/Domain/UseCases/ModelCheckerUseCase.swift
@@ -0,0 +1,12 @@
+//
+// ModelCheckerUseCase.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+protocol ModelCheckerUseCase {
+ func checkLocalModelVersion(modelName: String, remoteVersion: Int) -> Bool
+}
diff --git a/Sources/MLModelManager/Domain/UseCases/ModelCompilerUseCase.swift b/Sources/MLModelManager/Domain/UseCases/ModelCompilerUseCase.swift
new file mode 100644
index 0000000..8a7fabb
--- /dev/null
+++ b/Sources/MLModelManager/Domain/UseCases/ModelCompilerUseCase.swift
@@ -0,0 +1,13 @@
+//
+// ModelCompilerUseCase.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+import CoreML
+
+protocol ModelCompilerUseCase {
+ func compileModel(model: ModelEntity, from localURL: URL) async throws -> MLModel
+}
diff --git a/Sources/MLModelManager/Domain/UseCases/ModelDownloaderUseCase.swift b/Sources/MLModelManager/Domain/UseCases/ModelDownloaderUseCase.swift
new file mode 100644
index 0000000..152d307
--- /dev/null
+++ b/Sources/MLModelManager/Domain/UseCases/ModelDownloaderUseCase.swift
@@ -0,0 +1,36 @@
+//
+// ModelDownloaderUseCase.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+public protocol ModelDownloaderUseCase {
+ var delegate: ModelDownloadDelegate? { get set }
+
+ func downloadModel(_ model: ModelEntity, completion: @escaping (Result) -> Void)
+ func addModel(_ model: ModelEntity)
+ func addModels(_ models: [ModelEntity])
+}
+
+public extension ModelDownloaderUseCase {
+ func downloadModelAsync(_ model: ModelEntity) async throws -> URL {
+ addModel(model)
+ return try await withCheckedThrowingContinuation { continuation in
+ self.downloadModel(model) { result in
+ switch result {
+ case .success(let url):
+ continuation.resume(returning: url)
+ case .failure(let error):
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+}
+
+public protocol ModelDownloadDelegate: AnyObject {
+ func modelDownloadProgress(forModel modelName: String, progress: Float)
+}
diff --git a/Sources/MLModelManager/Domain/UseCases/ModelServer.swift b/Sources/MLModelManager/Domain/UseCases/ModelServer.swift
new file mode 100644
index 0000000..965f69f
--- /dev/null
+++ b/Sources/MLModelManager/Domain/UseCases/ModelServer.swift
@@ -0,0 +1,13 @@
+//
+// ModelServer.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+public protocol ModelServer {
+ func fetchRemoteModelVersion(for modelName: String) async throws -> Int
+ func fetchRemoteModelFile(for modelName: String, version: Int) async throws -> URL
+}
diff --git a/Sources/MLModelManager/Domain/UseCases/ModelStorable.swift b/Sources/MLModelManager/Domain/UseCases/ModelStorable.swift
new file mode 100644
index 0000000..61d6148
--- /dev/null
+++ b/Sources/MLModelManager/Domain/UseCases/ModelStorable.swift
@@ -0,0 +1,15 @@
+//
+// ModelStorable.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+public protocol ModelStorable {
+ func getLocalModelVersion(for modelName: String) -> Int?
+ func getLocalModelURL(for modelName: String, version: Int) -> URL?
+ func saveLocalModel(_ model: ModelEntity, url: URL)
+ func deleteOldVersions(of model: ModelEntity)
+}
diff --git a/Sources/MLModelManager/Infrastructure/ModelChecker.swift b/Sources/MLModelManager/Infrastructure/ModelChecker.swift
new file mode 100644
index 0000000..81a0bc2
--- /dev/null
+++ b/Sources/MLModelManager/Infrastructure/ModelChecker.swift
@@ -0,0 +1,26 @@
+//
+// ModelChecker.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+class ModelChecker: ModelCheckerUseCase {
+ private let localModelStore: ModelStorable
+
+ init(localModelStore: ModelStorable) {
+ self.localModelStore = localModelStore
+ }
+
+ func checkLocalModelVersion(modelName: String, remoteVersion: Int) -> Bool {
+ // Get the version of the local model
+ if let localVersion = localModelStore.getLocalModelVersion(for: modelName) {
+ return localVersion >= remoteVersion
+ }
+
+ // If the local version is nil, then it needs to be downloaded
+ return false
+ }
+}
diff --git a/Sources/MLModelManager/Infrastructure/ModelCompiler.swift b/Sources/MLModelManager/Infrastructure/ModelCompiler.swift
new file mode 100644
index 0000000..effbf35
--- /dev/null
+++ b/Sources/MLModelManager/Infrastructure/ModelCompiler.swift
@@ -0,0 +1,45 @@
+//
+// ModelCompiler.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+import CoreML
+
+class ModelCompiler: ModelCompilerUseCase {
+ private let fileManager: FileManager
+
+ init(fileManager: FileManager = .default) {
+ self.fileManager = fileManager
+ }
+
+ func compileModel(model: ModelEntity, from localURL: URL) async throws -> MLModel {
+ // Create a URL for the compiled model
+ let applicationSupportDirectoryURL = try fileManager.url(for: .applicationSupportDirectory,
+ in: .userDomainMask,
+ appropriateFor: nil,
+ create: true)
+ let compiledModelURL = applicationSupportDirectoryURL.appendingPathComponent("\(model.versionedName).mlmodelc")
+
+ // Check if the compiled model already exists, if yes, then return it
+ if fileManager.fileExists(atPath: compiledModelURL.path) {
+ let compiledModel = try MLModel(contentsOf: compiledModelURL)
+ return compiledModel
+ }
+
+ // If not, then compile the model from the localURL
+ do {
+ let tempCompiledURL = try await MLModel.compileModel(at: localURL)
+
+ // Move the compiled model from the temp location to our application support directory
+ try fileManager.moveItem(at: tempCompiledURL, to: compiledModelURL)
+
+ let compiledModel = try MLModel(contentsOf: compiledModelURL)
+ return compiledModel
+ } catch {
+ throw error
+ }
+ }
+}
diff --git a/Sources/MLModelManager/Infrastructure/ModelDownloader.swift b/Sources/MLModelManager/Infrastructure/ModelDownloader.swift
new file mode 100644
index 0000000..6a01b4d
--- /dev/null
+++ b/Sources/MLModelManager/Infrastructure/ModelDownloader.swift
@@ -0,0 +1,110 @@
+//
+// ModelDownloader.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+public class ModelDownloader: NSObject, ModelDownloaderUseCase {
+ private var downloadProgress: [URLSessionTask: Float] = [:]
+ private var downloadCompletionHandlers: [URLSessionTask: (Result) -> Void] = [:]
+ private var backgroundSession: URLSession!
+ private let modelStore: ModelLocalStore
+ private var modelDataSource: [String: ModelEntity] = [:]
+ public weak var delegate: ModelDownloadDelegate?
+ public var progressUpdate: ((Float) -> Void)?
+
+ public init(modelStore: ModelLocalStore) {
+ self.modelStore = modelStore
+ super.init()
+
+ let backgroundSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "com.app.backgroundModelDownload")
+ self.backgroundSession = URLSession(configuration: backgroundSessionConfiguration, delegate: self, delegateQueue: nil)
+ }
+
+ public func downloadModel(_ model: ModelEntity, completion: @escaping (Result) -> Void) {
+ let downloadTask = backgroundSession.downloadTask(with: model.url)
+ downloadCompletionHandlers[downloadTask] = completion
+ downloadTask.resume()
+ }
+
+ public func addModel(_ model: ModelEntity) {
+ modelDataSource[model.versionedName] = model
+ }
+
+ public func addModels(_ models: [ModelEntity]) {
+ models.forEach { addModel($0) }
+ }
+}
+
+extension ModelDownloader: URLSessionDownloadDelegate {
+ public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
+ guard let modelName = downloadTask.originalRequest?.url?.lastPathComponent,
+ let model = getModel(by: modelName),
+ let completionHandler = downloadCompletionHandlers[downloadTask] else {
+ return
+ }
+
+ do {
+ let destinationURL = modelStore.getLocalModelURL(for: model.name, version: model.version) ?? createDestinationURL(for: model)
+
+ // Check if directory exists, if not create it
+ let directoryURL = destinationURL.deletingLastPathComponent()
+ if !FileManager.default.fileExists(atPath: directoryURL.path) {
+ try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
+ }
+
+ // Delete older versions
+ deleteOldVersions(of: model)
+
+ // Move file to destination
+ if FileManager.default.fileExists(atPath: destinationURL.path) {
+ try FileManager.default.removeItem(at: destinationURL)
+ }
+ try FileManager.default.moveItem(at: location, to: destinationURL)
+
+ completionHandler(.success(destinationURL))
+ } catch {
+ print("Error detail: \(error.localizedDescription)")
+ completionHandler(.failure(error))
+ }
+ }
+
+
+ public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
+ // Update downloadProgress dictionary to track progress
+ let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
+ downloadProgress[downloadTask] = progress
+ // delegate?.modelDownloadDidProgress(self, taskId: downloadTask.taskIdentifier, progress: progress)
+
+ guard let modelName = downloadTask.originalRequest?.url?.lastPathComponent else {
+ return
+ }
+ delegate?.modelDownloadProgress(forModel: modelName, progress: progress)
+ }
+
+ public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+ // Handle errors
+ if let error = error, let completionHandler = downloadCompletionHandlers[task] {
+ completionHandler(.failure(error))
+ }
+ }
+}
+
+private extension ModelDownloader {
+ func getModel(by id: String) -> ModelEntity? {
+ guard let name = id.components(separatedBy: ".").first else { return nil }
+ return modelDataSource[name]
+ }
+
+ func createDestinationURL(for model: ModelEntity) -> URL {
+ let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+ return documentsDirectory.appendingPathComponent(model.versionedName + ".mlmodel")
+ }
+
+ func deleteOldVersions(of model: ModelEntity) {
+ modelStore.deleteOldVersions(of: model)
+ }
+}
diff --git a/Sources/MLModelManager/Infrastructure/ModelLocalStore.swift b/Sources/MLModelManager/Infrastructure/ModelLocalStore.swift
new file mode 100644
index 0000000..e56a304
--- /dev/null
+++ b/Sources/MLModelManager/Infrastructure/ModelLocalStore.swift
@@ -0,0 +1,89 @@
+//
+// ModelLocalStore.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import Foundation
+
+public class ModelLocalStore: ModelStorable {
+ private let fileManager: FileManager
+
+ public init(fileManager: FileManager = .default) {
+ self.fileManager = fileManager
+ }
+
+ public func getLocalModelVersion(for modelName: String) -> Int? {
+ let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+
+ do {
+ let files = try fileManager.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil)
+
+ // Filtering files which contain the modelName in their name
+ let modelFiles = files.filter { $0.lastPathComponent.contains("\(modelName)_") }
+
+ // Extracting version numbers and finding the highest version
+ let versions = modelFiles.compactMap { url -> Int? in
+ let fileName = url.lastPathComponent
+ guard let startRange = fileName.range(of: "\(modelName)_"),
+ let endRange = fileName.range(of: ".mlmodel") else { return nil }
+
+ // Extract the version number using the range between the modelName_ and .mlmodel
+ let versionString = fileName[startRange.upperBound.. URL? {
+ let modelNameWithVersion = "\(modelName)_\(version).mlmodel"
+ let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let fileURL = documentsDirectory.appendingPathComponent(modelNameWithVersion)
+
+ return fileManager.fileExists(atPath: fileURL.path) ? fileURL : nil
+ }
+
+ public func saveLocalModel(_ model: ModelEntity, url: URL) {
+ let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let destinationURL = documentsDirectory.appendingPathComponent(model.versionedName + ".mlmodel")
+
+ do {
+ if fileManager.fileExists(atPath: destinationURL.path) {
+ try fileManager.removeItem(at: destinationURL)
+ }
+
+ try fileManager.copyItem(at: url, to: destinationURL)
+ } catch {
+ print("Error saving local model: \(error.localizedDescription)")
+ }
+ }
+
+ public func deleteOldVersions(of model: ModelEntity) {
+ let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+
+ do {
+ // Getting a list of all files in the directory
+ let files = try fileManager.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil)
+
+ // Filtering files which contain the modelName in their name and are older versions
+ let olderModelFiles = files.filter {
+ $0.lastPathComponent.contains(model.name) && !$0.lastPathComponent.contains(model.versionedName)
+ }
+
+ // Removing older version files
+ for fileURL in olderModelFiles {
+ try fileManager.removeItem(at: fileURL)
+ }
+ } catch {
+ print("Error while deleting older versions of model: \(error.localizedDescription)")
+ }
+ }
+}
diff --git a/Sources/MLModelManager/Infrastructure/SupabaseModelServer.swift b/Sources/MLModelManager/Infrastructure/SupabaseModelServer.swift
new file mode 100644
index 0000000..326118a
--- /dev/null
+++ b/Sources/MLModelManager/Infrastructure/SupabaseModelServer.swift
@@ -0,0 +1,49 @@
+//
+// SupabaseModelServer.swift
+// ExampleApp
+//
+// Created by Egzon Arifi on 20/09/2023.
+//
+
+import Foundation
+
+public class SupabaseModelServer: ModelServer {
+ let baseURL: URL
+ let apiKey: String
+
+ public init(baseURL: URL, apiKey: String) {
+ self.baseURL = baseURL
+ self.apiKey = apiKey
+ }
+
+ public func fetchRemoteModelVersion(for modelName: String) async throws -> Int {
+ let url = URL(string: "\(baseURL)/rest/v1/ml_models_metadata?name=eq.\(modelName)&select=*")!
+
+ var request = URLRequest(url: url)
+ request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
+ request.addValue(apiKey, forHTTPHeaderField: "apiKey")
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+ throw ModelError.networkError
+ }
+
+ if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]],
+ let firstRecord = json.first,
+ let version = firstRecord["version"] as? Int {
+ return version
+ } else {
+ throw ModelError.failedToLoadModel("Failed to parse response")
+ }
+ }
+
+ public func fetchRemoteModelFile(for modelName: String, version: Int) async throws -> URL {
+ let fileURLString = "\(baseURL)/storage/v1/object/public/public/models/\(modelName)_\(version).mlmodel"
+ guard let fileURL = URL(string: fileURLString) else {
+ throw ModelError.failedToLoadModel("Invalid URL")
+ }
+ return fileURL
+ }
+}
diff --git a/Sources/MLModelManager/MLModelManager+Factory.swift b/Sources/MLModelManager/MLModelManager+Factory.swift
new file mode 100644
index 0000000..d1ebfa9
--- /dev/null
+++ b/Sources/MLModelManager/MLModelManager+Factory.swift
@@ -0,0 +1,25 @@
+//
+// MLModelManager.swift
+// ExampleApp
+//
+// Created by Egzon Arifi on 21/09/2023.
+//
+
+import Foundation
+
+public extension MLModelManager {
+ static func makeWithSupabase(baseURL: URL,
+ apiKey: String) -> MLModelManager {
+ let modelStore = ModelLocalStore()
+ let modelDownloader = ModelDownloader(modelStore: modelStore)
+ let manager = MLModelManager(modelChecker: ModelChecker(localModelStore: modelStore),
+ modelCompiler: ModelCompiler(),
+ modelDownloader: modelDownloader,
+ localModelStore: modelStore,
+ modelServer: SupabaseModelServer(baseURL: baseURL, apiKey: apiKey))
+
+ modelDownloader.delegate = manager
+
+ return manager
+ }
+}
diff --git a/Sources/MLModelManager/MLModelManager.swift b/Sources/MLModelManager/MLModelManager.swift
new file mode 100644
index 0000000..d72bfb8
--- /dev/null
+++ b/Sources/MLModelManager/MLModelManager.swift
@@ -0,0 +1,139 @@
+//
+// MLModelManager.swift
+//
+//
+// Created by Egzon Arifi on 19/09/2023.
+//
+
+import CoreML
+
+public class MLModelManager {
+ private let modelChecker: ModelCheckerUseCase
+ private let modelCompiler: ModelCompilerUseCase
+ private var modelDownloader: ModelDownloaderUseCase
+ private let localModelStore: ModelStorable
+ private let modelServer: ModelServer
+ private var downloadProgressClosures: [String: (Float) -> Void] = [:]
+
+ init(modelChecker: ModelCheckerUseCase,
+ modelCompiler: ModelCompilerUseCase,
+ modelDownloader: ModelDownloaderUseCase,
+ localModelStore: ModelStorable,
+ modelServer: ModelServer) {
+ self.modelChecker = modelChecker
+ self.modelCompiler = modelCompiler
+ self.modelDownloader = modelDownloader
+ self.localModelStore = localModelStore
+ self.modelServer = modelServer
+ }
+
+ public func getModel(
+ modelName: String,
+ bundledModelURL: URL?,
+ progress: ((Float) -> Void)?,
+ completion: @escaping (Result, Bool) -> Void
+ ) async {
+ do {
+ // Step 1: Fetch remote model version
+ let remoteVersion = try await modelServer.fetchRemoteModelVersion(for: modelName)
+
+ // Step 2: Check local model version
+ if modelChecker.checkLocalModelVersion(modelName: modelName, remoteVersion: remoteVersion),
+ let localVersion = self.localModelStore.getLocalModelVersion(for: modelName) {
+ // Load local model and return
+ let localModel = try await self.loadLocalModel(modelName: modelName, version: localVersion)
+ completion(.success(localModel), true) // true indicates that this is the final model and no newer version is available
+ } else {
+ // Step 3: If there's a local version, return it first
+ if let localOrBundledModel = try? await loadLocalOrBundledModel(modelName: modelName, bundledModelURL: bundledModelURL) {
+ completion(.success(localOrBundledModel), false) // false indicates that a newer version is being downloaded
+ }
+
+ // Now download the newer version from the server
+ if let progressClosure = progress {
+ self.downloadProgressClosures["\(modelName)_\(remoteVersion).mlmodel"] = progressClosure
+ }
+ do {
+ let newModel = try await self.downloadAndLoadModel(modelName: modelName, remoteVersion: remoteVersion, bundledModelURL: bundledModelURL)
+ completion(.success(newModel), true) // true indicates that this is the final model
+ } catch {
+ completion(.failure(error), true) // true indicates that this is the final callback
+ }
+ }
+ } catch {
+ // Handle error - try loading local or bundled version if available
+ do {
+ let fallbackModel = try await loadLocalOrBundledModel(modelName: modelName, bundledModelURL: bundledModelURL)
+ completion(.success(fallbackModel), true) // true indicates that this is the final model
+ } catch {
+ completion(.failure(error), true) // true indicates that this is the final callback
+ }
+ }
+ }
+}
+
+private extension MLModelManager {
+ func loadLocalModel(modelName: String, version: Int) async throws -> MLModel {
+ // Get the URL of the local model using the `ModelLocalStore` instance
+ guard let modelURL = localModelStore.getLocalModelURL(for: modelName, version: version) else {
+ throw ModelError.modelNotFound
+ }
+
+ // Try to load the MLModel from the obtained URL
+ do {
+ let modelEntity = ModelEntity(name: modelName, version: version, url: modelURL)
+ let model = try await modelCompiler.compileModel(model: modelEntity, from: modelURL)
+ return model
+ } catch {
+ // If there's an error in loading the model, throw a specific error
+ throw ModelError.failedToLoadModel(error.localizedDescription)
+ }
+ }
+
+ func downloadAndLoadModel(modelName: String, remoteVersion: Int, bundledModelURL: URL?) async throws -> MLModel {
+ do {
+ // Get the URL for the remote version of the model from the server
+ let remoteModelURL = try await modelServer.fetchRemoteModelFile(for: modelName, version: remoteVersion)
+
+ // Create a ModelEntity instance to represent the model
+ let modelEntity = ModelEntity(name: modelName, version: remoteVersion, url: remoteModelURL)
+
+ // Download the model using the ModelDownloader
+ let downloadURL = try await modelDownloader.downloadModelAsync(modelEntity)
+
+ // Load the newly downloaded model into memory
+ let mlModel = try await modelCompiler.compileModel(model: modelEntity, from: downloadURL)
+
+ return mlModel
+ } catch {
+ // Handle various error cases, e.g., trying to load a bundled model if the download failed
+ if let bundledModelURL = bundledModelURL {
+ let bundledModel = try MLModel(contentsOf: bundledModelURL)
+ return bundledModel
+ } else {
+ // If no bundled model is available, rethrow the error to be handled upstream
+ throw error
+ }
+ }
+ }
+
+ func loadLocalOrBundledModel(modelName: String, bundledModelURL: URL?) async throws -> MLModel {
+ if let latestLocalVersion = self.localModelStore.getLocalModelVersion(for: modelName) {
+ // If a local version is available, load it
+ return try await self.loadLocalModel(modelName: modelName, version: latestLocalVersion)
+ } else if let bundledModelURL = bundledModelURL {
+ // If no local version is available, try loading the bundled version
+ let bundledModel = try MLModel(contentsOf: bundledModelURL)
+ return bundledModel
+ } else {
+ // If neither a local version nor a bundled version is available, throw an error
+ throw ModelError.fileNotFound
+ }
+ }
+}
+
+extension MLModelManager: ModelDownloadDelegate {
+ public func modelDownloadProgress(forModel modelName: String, progress: Float) {
+ downloadProgressClosures[modelName]?(progress)
+ }
+}
diff --git a/Tests/MLModelManagerTests/MLModelManagerTests.swift b/Tests/MLModelManagerTests/MLModelManagerTests.swift
new file mode 100644
index 0000000..cc2dce0
--- /dev/null
+++ b/Tests/MLModelManagerTests/MLModelManagerTests.swift
@@ -0,0 +1,12 @@
+import XCTest
+@testable import MLModelManager
+
+final class MLModelManagerTests: XCTestCase {
+ func testExample() throws {
+ // XCTest Documentation
+ // https://developer.apple.com/documentation/xctest
+
+ // Defining Test Cases and Test Methods
+ // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
+ }
+}