start Expiranner project and first demo app build
This commit is contained in:
parent
2d9e7ad20b
commit
3a46505451
@ -1 +0,0 @@
|
||||
Subproject commit 74a95d489240619326e8abc1725d15b6e62031ab
|
||||
41
myApp/Expiranner/CLAUDE.md
Normal file
41
myApp/Expiranner/CLAUDE.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Expiranner Project Rules
|
||||
|
||||
## 1. Project Environment
|
||||
- Project Name: Expiranner
|
||||
- Root Folder: Expiranner (All operations must run from this root)
|
||||
- Subfolder: `IOS` (The main iOS project folder; do NOT recreate `Expiranner` subfolder)
|
||||
- Framework: SwiftUI
|
||||
- Local Storage: SwiftData (Do NOT use Core Data)
|
||||
- Language: Korean (All UI and logs must be in Korean)
|
||||
- Target: iOS 17+ (Required for SwiftData and VisionKit Revision 3)
|
||||
- Purpose: Personal use (Do NOT implement complex UI/UX designs, animations, or styling unless explicitly requested. Focus ONLY on functional logic)
|
||||
|
||||
## 2. Core Features & UX Flow
|
||||
- Main View (Calendar):
|
||||
- Must include a Year Picker at the top.
|
||||
- Must show months (1-12) as a grid or list.
|
||||
- Tapping a month opens a calendar for that month.
|
||||
- Tapping a day shows a list of saved product items.
|
||||
- Saved items must support swipe-to-delete.
|
||||
- Saved items must support date modification (moving to another Year/Month/Day).
|
||||
- Floating Action Button (FAB):
|
||||
- Must exist on the calendar view to start recording.
|
||||
- Opens full-screen Camera Scanner (`DataScannerViewController` or `VNDocumentCameraViewController`).
|
||||
- OCR & Recording Flow:
|
||||
1. Capture product label via camera.
|
||||
2. Extract Korean text using `Vision` framework (`VNRecognizeTextRequest`).
|
||||
3. Show a popup with thumbnail and extracted text.
|
||||
4. Popup must have [재촬영] (Recapture - returns to camera) and [적용] (Apply - opens date picker).
|
||||
5. Extracted text must be editable in the popup.
|
||||
6. Date picker must restrict selection to the currently viewed month only.
|
||||
7. Provide Haptic Feedback (vibration) upon saving data.
|
||||
8. Automatically return to the Camera Scanner immediately after saving.
|
||||
9. Provide an [X] button to exit scanner and return to Main View.
|
||||
|
||||
## 3. Data Model (SwiftData)
|
||||
- Always use `@Model` for data structure.
|
||||
- Never add unnecessary metadata fields unless specified.
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import SwiftData
|
||||
333
myApp/Expiranner/Expiranner.xcodeproj/project.pbxproj
Normal file
333
myApp/Expiranner/Expiranner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,333 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
725A9F872FFFAB2400846FFB /* CLAUDE.md in Resources */ = {isa = PBXBuildFile; fileRef = 725A9F862FFFAB2400846FFB /* CLAUDE.md */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
725A9F702FFFA9D400846FFB /* Expiranner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = Expiranner.app; path = "/Users/ceuak/Documents/ceuak/mycode/myApp/Expiranner/build/Debug-iphoneos/Expiranner.app"; sourceTree = "<absolute>"; };
|
||||
725A9F862FFFAB2400846FFB /* CLAUDE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CLAUDE.md; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
725A9F722FFFA9D400846FFB /* IOS */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = IOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
725A9F6D2FFFA9D400846FFB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
725A9F672FFFA9D400846FFB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
725A9F722FFFA9D400846FFB /* IOS */,
|
||||
725A9F862FFFAB2400846FFB /* CLAUDE.md */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
725A9F6F2FFFA9D400846FFB /* Expiranner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 725A9F7B2FFFA9D500846FFB /* Build configuration list for PBXNativeTarget "Expiranner" */;
|
||||
buildPhases = (
|
||||
725A9F6C2FFFA9D400846FFB /* Sources */,
|
||||
725A9F6D2FFFA9D400846FFB /* Frameworks */,
|
||||
725A9F6E2FFFA9D400846FFB /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
725A9F722FFFA9D400846FFB /* IOS */,
|
||||
);
|
||||
name = Expiranner;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = Expiranner;
|
||||
productReference = 725A9F702FFFA9D400846FFB /* Expiranner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
725A9F682FFFA9D400846FFB /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 2660;
|
||||
LastUpgradeCheck = 2660;
|
||||
TargetAttributes = {
|
||||
725A9F6F2FFFA9D400846FFB = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 725A9F6B2FFFA9D400846FFB /* Build configuration list for PBXProject "Expiranner" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 725A9F672FFFA9D400846FFB;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 725A9F672FFFA9D400846FFB;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
725A9F6F2FFFA9D400846FFB /* Expiranner */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
725A9F6E2FFFA9D400846FFB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
725A9F872FFFAB2400846FFB /* CLAUDE.md in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
725A9F6C2FFFA9D400846FFB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
725A9F792FFFA9D500846FFB /* 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;
|
||||
};
|
||||
725A9F7A2FFFA9D500846FFB /* 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;
|
||||
};
|
||||
725A9F7C2FFFA9D500846FFB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "제품 라벨을 촬영하여 유통기한을 기록하기 위해 카메라 접근 권한이 필요합니다.";
|
||||
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 = com.yechan.Expiranner;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
725A9F7D2FFFA9D500846FFB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "제품 라벨을 촬영하여 유통기한을 기록하기 위해 카메라 접근 권한이 필요합니다.";
|
||||
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 = com.yechan.Expiranner;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
725A9F6B2FFFA9D400846FFB /* Build configuration list for PBXProject "Expiranner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
725A9F792FFFA9D500846FFB /* Debug */,
|
||||
725A9F7A2FFFA9D500846FFB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
725A9F7B2FFFA9D500846FFB /* Build configuration list for PBXNativeTarget "Expiranner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
725A9F7C2FFFA9D500846FFB /* Debug */,
|
||||
725A9F7D2FFFA9D500846FFB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 725A9F682FFFA9D400846FFB /* Project object */;
|
||||
}
|
||||
7
myApp/Expiranner/Expiranner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
myApp/Expiranner/Expiranner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
Binary file not shown.
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>Expiranner.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@ -0,0 +1,36 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
myApp/Expiranner/IOS/Assets.xcassets/Contents.json
Normal file
6
myApp/Expiranner/IOS/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
115
myApp/Expiranner/IOS/ContentView.swift
Normal file
115
myApp/Expiranner/IOS/ContentView.swift
Normal file
@ -0,0 +1,115 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Expiranner
|
||||
//
|
||||
// 메인 뷰: 연도 선택 + 월(1~12) 그리드
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var selectedYear = Calendar.current.component(.year, from: Date())
|
||||
@State private var showScanner = false
|
||||
|
||||
private let years = Array(2020...2040)
|
||||
private let columns = Array(repeating: GridItem(.flexible()), count: 3)
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
VStack {
|
||||
Picker("연도", selection: $selectedYear) {
|
||||
ForEach(years, id: \.self) { year in
|
||||
Text(String(format: "%d년", year)).tag(year)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.font(.title2)
|
||||
|
||||
LazyVGrid(columns: columns, spacing: 16) {
|
||||
ForEach(1...12, id: \.self) { month in
|
||||
NavigationLink {
|
||||
MonthCalendarView(year: selectedYear, month: month)
|
||||
} label: {
|
||||
MonthCell(year: selectedYear, month: month)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
FloatingScanButton {
|
||||
showScanner = true
|
||||
}
|
||||
}
|
||||
.navigationTitle("Expiranner")
|
||||
.fullScreenCover(isPresented: $showScanner) {
|
||||
ScannerView(
|
||||
year: Calendar.current.component(.year, from: Date()),
|
||||
month: Calendar.current.component(.month, from: Date())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 월 셀: 해당 월에 저장된 아이템 개수 표시
|
||||
struct MonthCell: View {
|
||||
let year: Int
|
||||
let month: Int
|
||||
|
||||
@Query private var items: [ProductItem]
|
||||
|
||||
init(year: Int, month: Int) {
|
||||
self.year = year
|
||||
self.month = month
|
||||
let (start, end) = Self.monthBounds(year: year, month: month)
|
||||
_items = Query(filter: #Predicate<ProductItem> { $0.date >= start && $0.date < end })
|
||||
}
|
||||
|
||||
static func monthBounds(year: Int, month: Int) -> (Date, Date) {
|
||||
let calendar = Calendar.current
|
||||
let start = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? Date()
|
||||
let end = calendar.date(byAdding: .month, value: 1, to: start) ?? start
|
||||
return (start, end)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
Text("\(month)월")
|
||||
.font(.title3)
|
||||
Text(items.isEmpty ? " " : "\(items.count)개")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
|
||||
/// 플로팅 액션 버튼 (카메라 스캐너 실행)
|
||||
struct FloatingScanButton: View {
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: "camera.fill")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.white)
|
||||
.padding(18)
|
||||
.background(Circle().fill(.blue))
|
||||
.shadow(radius: 4)
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
.modelContainer(for: ProductItem.self, inMemory: true)
|
||||
}
|
||||
17
myApp/Expiranner/IOS/ExpirannerApp.swift
Normal file
17
myApp/Expiranner/IOS/ExpirannerApp.swift
Normal file
@ -0,0 +1,17 @@
|
||||
//
|
||||
// ExpirannerApp.swift
|
||||
// Expiranner
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
@main
|
||||
struct ExpirannerApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
.modelContainer(for: ProductItem.self)
|
||||
}
|
||||
}
|
||||
187
myApp/Expiranner/IOS/MonthCalendarView.swift
Normal file
187
myApp/Expiranner/IOS/MonthCalendarView.swift
Normal file
@ -0,0 +1,187 @@
|
||||
//
|
||||
// MonthCalendarView.swift
|
||||
// Expiranner
|
||||
//
|
||||
// 월별 캘린더: 날짜 그리드 + 선택한 날의 아이템 목록
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct MonthCalendarView: View {
|
||||
let year: Int
|
||||
let month: Int
|
||||
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var items: [ProductItem]
|
||||
|
||||
@State private var selectedDay: Int?
|
||||
@State private var showScanner = false
|
||||
@State private var editingItem: ProductItem?
|
||||
|
||||
private let columns = Array(repeating: GridItem(.flexible()), count: 7)
|
||||
private let weekdaySymbols = ["일", "월", "화", "수", "목", "금", "토"]
|
||||
|
||||
init(year: Int, month: Int) {
|
||||
self.year = year
|
||||
self.month = month
|
||||
let (start, end) = MonthCell.monthBounds(year: year, month: month)
|
||||
_items = Query(
|
||||
filter: #Predicate<ProductItem> { $0.date >= start && $0.date < end },
|
||||
sort: \ProductItem.date
|
||||
)
|
||||
}
|
||||
|
||||
private var daysInMonth: Int {
|
||||
let calendar = Calendar.current
|
||||
let start = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? Date()
|
||||
return calendar.range(of: .day, in: .month, for: start)?.count ?? 30
|
||||
}
|
||||
|
||||
/// 1일의 요일 오프셋 (일요일 시작 기준)
|
||||
private var firstWeekdayOffset: Int {
|
||||
let calendar = Calendar.current
|
||||
let start = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? Date()
|
||||
return calendar.component(.weekday, from: start) - 1
|
||||
}
|
||||
|
||||
private func day(of item: ProductItem) -> Int {
|
||||
Calendar.current.component(.day, from: item.date)
|
||||
}
|
||||
|
||||
private var selectedDayItems: [ProductItem] {
|
||||
guard let selectedDay else { return [] }
|
||||
return items.filter { day(of: $0) == selectedDay }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
VStack {
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
ForEach(weekdaySymbols, id: \.self) { symbol in
|
||||
Text(symbol)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
ForEach(0..<firstWeekdayOffset, id: \.self) { _ in
|
||||
Text("")
|
||||
}
|
||||
ForEach(1...daysInMonth, id: \.self) { day in
|
||||
let count = items.filter { self.day(of: $0) == day }.count
|
||||
Button {
|
||||
selectedDay = day
|
||||
} label: {
|
||||
VStack(spacing: 2) {
|
||||
Text("\(day)")
|
||||
.fontWeight(selectedDay == day ? .bold : .regular)
|
||||
Circle()
|
||||
.fill(count > 0 ? .red : .clear)
|
||||
.frame(width: 6, height: 6)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 4)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(selectedDay == day ? Color(.systemGray4) : .clear)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
if let selectedDay {
|
||||
List {
|
||||
Section("\(month)월 \(selectedDay)일 저장된 항목") {
|
||||
if selectedDayItems.isEmpty {
|
||||
Text("저장된 항목이 없습니다")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
ForEach(selectedDayItems) { item in
|
||||
Button {
|
||||
editingItem = item
|
||||
} label: {
|
||||
HStack {
|
||||
if let data = item.imageData, let image = UIImage(data: data) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 44, height: 44)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
Text(item.name)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.onDelete(perform: deleteItems)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
} else {
|
||||
Spacer()
|
||||
Text("날짜를 선택하면 저장된 항목이 표시됩니다")
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
FloatingScanButton {
|
||||
showScanner = true
|
||||
}
|
||||
}
|
||||
.navigationTitle("\(String(year))년 \(month)월")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.fullScreenCover(isPresented: $showScanner) {
|
||||
ScannerView(year: year, month: month)
|
||||
}
|
||||
.sheet(item: $editingItem) { item in
|
||||
ItemEditView(item: item)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItems(at offsets: IndexSet) {
|
||||
let targets = offsets.map { selectedDayItems[$0] }
|
||||
for item in targets {
|
||||
print("항목 삭제: \(item.name)")
|
||||
modelContext.delete(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 저장된 항목의 날짜(연/월/일) 수정 뷰
|
||||
struct ItemEditView: View {
|
||||
@Bindable var item: ProductItem
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("제품명") {
|
||||
TextField("제품명", text: $item.name)
|
||||
}
|
||||
Section("날짜 이동") {
|
||||
DatePicker("날짜", selection: $item.date, displayedComponents: .date)
|
||||
.datePickerStyle(.graphical)
|
||||
}
|
||||
}
|
||||
.navigationTitle("항목 수정")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("완료") {
|
||||
print("항목 수정 완료: \(item.name) → \(item.date)")
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
MonthCalendarView(year: 2026, month: 7)
|
||||
}
|
||||
.modelContainer(for: ProductItem.self, inMemory: true)
|
||||
}
|
||||
15
myApp/Expiranner/IOS/ProductItem.swift
Normal file
15
myApp/Expiranner/IOS/ProductItem.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class ProductItem {
|
||||
var name: String
|
||||
var date: Date
|
||||
var imageData: Data?
|
||||
|
||||
init(name: String, date: Date, imageData: Data? = nil) {
|
||||
self.name = name
|
||||
self.date = date
|
||||
self.imageData = imageData
|
||||
}
|
||||
}
|
||||
242
myApp/Expiranner/IOS/ScannerView.swift
Normal file
242
myApp/Expiranner/IOS/ScannerView.swift
Normal file
@ -0,0 +1,242 @@
|
||||
//
|
||||
// ScannerView.swift
|
||||
// Expiranner
|
||||
//
|
||||
// 전체 화면 카메라 스캐너: 촬영 → OCR → 팝업 → 날짜 선택 → 저장
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import VisionKit
|
||||
import Vision
|
||||
import UIKit
|
||||
|
||||
struct ScannerView: View {
|
||||
/// 현재 보고 있는 연/월 (날짜 선택 범위 제한용)
|
||||
let year: Int
|
||||
let month: Int
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@State private var scannerHolder = ScannerHolder()
|
||||
@State private var isScanning = true
|
||||
@State private var capturedImage: UIImage?
|
||||
@State private var recognizedText = ""
|
||||
@State private var showPopup = false
|
||||
@State private var showDatePicker = false
|
||||
@State private var selectedDate = Date()
|
||||
|
||||
/// 선택 가능한 날짜 범위: 현재 보고 있는 월로 제한
|
||||
private var monthRange: ClosedRange<Date> {
|
||||
let calendar = Calendar.current
|
||||
let start = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? Date()
|
||||
let last = calendar.date(byAdding: DateComponents(month: 1, day: -1), to: start) ?? start
|
||||
return start...last
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if DataScannerViewController.isSupported && DataScannerViewController.isAvailable {
|
||||
DataScannerRepresentable(holder: scannerHolder, isScanning: $isScanning)
|
||||
.ignoresSafeArea()
|
||||
} else {
|
||||
Color.black.ignoresSafeArea()
|
||||
Text("이 기기에서는 카메라 스캐너를 사용할 수 없습니다")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
print("스캐너 종료")
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.white)
|
||||
.padding(12)
|
||||
.background(Circle().fill(.black.opacity(0.5)))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
capturePhoto()
|
||||
} label: {
|
||||
Circle()
|
||||
.stroke(.white, lineWidth: 4)
|
||||
.frame(width: 72, height: 72)
|
||||
.overlay(Circle().fill(.white).frame(width: 58, height: 58))
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
|
||||
if showPopup {
|
||||
ocrPopup
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showDatePicker) {
|
||||
datePickerSheet
|
||||
}
|
||||
}
|
||||
|
||||
/// OCR 결과 팝업: 썸네일 + 편집 가능한 텍스트 + [재촬영]/[적용]
|
||||
private var ocrPopup: some View {
|
||||
VStack(spacing: 16) {
|
||||
if let capturedImage {
|
||||
Image(uiImage: capturedImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxHeight: 180)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
TextField("인식된 텍스트", text: $recognizedText, axis: .vertical)
|
||||
.lineLimit(1...5)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
HStack(spacing: 12) {
|
||||
Button("재촬영") {
|
||||
print("재촬영 선택")
|
||||
resumeScanning()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button("적용") {
|
||||
print("적용 선택: \(recognizedText)")
|
||||
selectedDate = defaultDate()
|
||||
showDatePicker = true
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(RoundedRectangle(cornerRadius: 14).fill(Color(.systemBackground)))
|
||||
.padding(24)
|
||||
}
|
||||
|
||||
/// 날짜 선택 시트: 현재 보고 있는 월로 제한
|
||||
private var datePickerSheet: some View {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
DatePicker("날짜", selection: $selectedDate, in: monthRange, displayedComponents: .date)
|
||||
.datePickerStyle(.graphical)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("\(String(year))년 \(month)월 날짜 선택")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("취소") { showDatePicker = false }
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("저장") { saveItem() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 기본 선택 날짜: 오늘이 해당 월에 포함되면 오늘, 아니면 월의 첫날
|
||||
private func defaultDate() -> Date {
|
||||
let today = Date()
|
||||
return monthRange.contains(today) ? today : monthRange.lowerBound
|
||||
}
|
||||
|
||||
private func capturePhoto() {
|
||||
guard let scanner = scannerHolder.scanner else {
|
||||
print("스캐너를 사용할 수 없습니다")
|
||||
return
|
||||
}
|
||||
Task {
|
||||
do {
|
||||
let photo = try await scanner.capturePhoto()
|
||||
print("사진 촬영 완료")
|
||||
isScanning = false
|
||||
capturedImage = photo
|
||||
recognizedText = recognizeText(in: photo)
|
||||
showPopup = true
|
||||
} catch {
|
||||
print("사진 촬영 실패: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vision 프레임워크로 한국어 텍스트 추출
|
||||
private func recognizeText(in image: UIImage) -> String {
|
||||
guard let cgImage = image.cgImage else { return "" }
|
||||
let request = VNRecognizeTextRequest()
|
||||
request.recognitionLanguages = ["ko-KR", "en-US"]
|
||||
request.recognitionLevel = .accurate
|
||||
request.usesLanguageCorrection = true
|
||||
let handler = VNImageRequestHandler(cgImage: cgImage)
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch {
|
||||
print("텍스트 인식 실패: \(error.localizedDescription)")
|
||||
return ""
|
||||
}
|
||||
let text = request.results?
|
||||
.compactMap { $0.topCandidates(1).first?.string }
|
||||
.joined(separator: "\n") ?? ""
|
||||
print("인식된 텍스트: \(text)")
|
||||
return text
|
||||
}
|
||||
|
||||
private func saveItem() {
|
||||
let imageData = capturedImage?.jpegData(compressionQuality: 0.5)
|
||||
let item = ProductItem(name: recognizedText, date: selectedDate, imageData: imageData)
|
||||
modelContext.insert(item)
|
||||
print("저장 완료: \(item.name) → \(selectedDate)")
|
||||
|
||||
// 저장 시 햅틱 피드백
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
|
||||
showDatePicker = false
|
||||
resumeScanning()
|
||||
}
|
||||
|
||||
/// 팝업을 닫고 카메라 스캐너로 복귀
|
||||
private func resumeScanning() {
|
||||
showPopup = false
|
||||
capturedImage = nil
|
||||
recognizedText = ""
|
||||
isScanning = true
|
||||
}
|
||||
}
|
||||
|
||||
/// DataScannerViewController 참조 보관용
|
||||
@Observable
|
||||
final class ScannerHolder {
|
||||
var scanner: DataScannerViewController?
|
||||
}
|
||||
|
||||
struct DataScannerRepresentable: UIViewControllerRepresentable {
|
||||
let holder: ScannerHolder
|
||||
@Binding var isScanning: Bool
|
||||
|
||||
func makeUIViewController(context: Context) -> DataScannerViewController {
|
||||
let scanner = DataScannerViewController(
|
||||
recognizedDataTypes: [.text(languages: ["ko-KR"])],
|
||||
qualityLevel: .balanced,
|
||||
isHighlightingEnabled: false
|
||||
)
|
||||
holder.scanner = scanner
|
||||
return scanner
|
||||
}
|
||||
|
||||
func updateUIViewController(_ scanner: DataScannerViewController, context: Context) {
|
||||
if isScanning, !scanner.isScanning {
|
||||
do {
|
||||
try scanner.startScanning()
|
||||
print("스캔 시작")
|
||||
} catch {
|
||||
print("스캔 시작 실패: \(error.localizedDescription)")
|
||||
}
|
||||
} else if !isScanning, scanner.isScanning {
|
||||
scanner.stopScanning()
|
||||
print("스캔 중지")
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
myApp/Expiranner/icon.jpeg
Normal file
BIN
myApp/Expiranner/icon.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 189 KiB |
Loading…
x
Reference in New Issue
Block a user