diff --git a/Gifty.xcodeproj/project.pbxproj b/Gifty.xcodeproj/project.pbxproj index 804b8d8..42b7296 100644 --- a/Gifty.xcodeproj/project.pbxproj +++ b/Gifty.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 43F711702A972EF400939C9C /* Const.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F7116F2A972EF400939C9C /* Const.swift */; }; 43F711752A9733E000939C9C /* DetectBarcodeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F711742A9733E000939C9C /* DetectBarcodeService.swift */; }; 43F711772A97481C00939C9C /* GiftBoxImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F711762A97481C00939C9C /* GiftBoxImage.swift */; }; + B7350A872AB02BA00026CF13 /* Sequence+.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7350A862AB02BA00026CF13 /* Sequence+.swift */; }; B79BC2BB2AAEE3F1005A54F7 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79BC2BA2AAEE3F1005A54F7 /* MainViewModel.swift */; }; B79BC2BD2AAEF7D0005A54F7 /* AppLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79BC2BC2AAEF7D0005A54F7 /* AppLogo.swift */; }; B79BC2C62AAEFDB7005A54F7 /* NanumSquareEB.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B79BC2C02AAEFDB7005A54F7 /* NanumSquareEB.ttf */; }; @@ -31,6 +32,7 @@ B79BC2C92AAEFDB7005A54F7 /* NanumSquareR.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B79BC2C32AAEFDB7005A54F7 /* NanumSquareR.ttf */; }; B79BC2CC2AAEFE0A005A54F7 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79BC2CB2AAEFE0A005A54F7 /* Font.swift */; }; B79BC2CE2AAF00B9005A54F7 /* DefaultLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79BC2CD2AAF00B9005A54F7 /* DefaultLabel.swift */; }; + B7BAFB652AB04FF0007D6183 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = B7BAFB642AB04FF0007D6183 /* Algorithms */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -71,6 +73,7 @@ 43F7116F2A972EF400939C9C /* Const.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Const.swift; sourceTree = ""; }; 43F711742A9733E000939C9C /* DetectBarcodeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectBarcodeService.swift; sourceTree = ""; }; 43F711762A97481C00939C9C /* GiftBoxImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftBoxImage.swift; sourceTree = ""; }; + B7350A862AB02BA00026CF13 /* Sequence+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+.swift"; sourceTree = ""; }; B79BC2BA2AAEE3F1005A54F7 /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; B79BC2BC2AAEF7D0005A54F7 /* AppLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogo.swift; sourceTree = ""; }; B79BC2C02AAEFDB7005A54F7 /* NanumSquareEB.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = NanumSquareEB.ttf; sourceTree = ""; }; @@ -86,6 +89,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B7BAFB652AB04FF0007D6183 /* Algorithms in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -203,6 +207,7 @@ 43F7116F2A972EF400939C9C /* Const.swift */, 43A134D92A9CE4B500428DC6 /* UITextField+LeftPadding.swift */, B79BC2CB2AAEFE0A005A54F7 /* Font.swift */, + B7350A862AB02BA00026CF13 /* Sequence+.swift */, ); path = Global; sourceTree = ""; @@ -269,6 +274,7 @@ ); name = Gifty; packageProductDependencies = ( + B7BAFB642AB04FF0007D6183 /* Algorithms */, ); productName = Gifty; productReference = 43D2A4542A964B2A002453BE /* Gifty.app */; @@ -343,6 +349,7 @@ ); mainGroup = 43D2A44B2A964B2A002453BE; packageReferences = ( + B7BAFB632AB04FF0007D6183 /* XCRemoteSwiftPackageReference "swift-algorithms" */, ); productRefGroup = 43D2A4552A964B2A002453BE /* Products */; projectDirPath = ""; @@ -404,6 +411,7 @@ 43F711752A9733E000939C9C /* DetectBarcodeService.swift in Sources */, 43F711702A972EF400939C9C /* Const.swift in Sources */, B79BC2BD2AAEF7D0005A54F7 /* AppLogo.swift in Sources */, + B7350A872AB02BA00026CF13 /* Sequence+.swift in Sources */, B79BC2BB2AAEE3F1005A54F7 /* MainViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -743,6 +751,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + B7BAFB632AB04FF0007D6183 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-algorithms.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + B7BAFB642AB04FF0007D6183 /* Algorithms */ = { + isa = XCSwiftPackageProductDependency; + package = B7BAFB632AB04FF0007D6183 /* XCRemoteSwiftPackageReference "swift-algorithms" */; + productName = Algorithms; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 43D2A44C2A964B2A002453BE /* Project object */; } diff --git a/Gifty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Gifty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..47f06e0 --- /dev/null +++ b/Gifty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + } + ], + "version" : 2 +} diff --git a/Gifty/Global/Sequence+.swift b/Gifty/Global/Sequence+.swift new file mode 100644 index 0000000..bb8d9f8 --- /dev/null +++ b/Gifty/Global/Sequence+.swift @@ -0,0 +1,28 @@ +// +// Sequence+.swift +// Gifty +// +// Created by 주동석 on 2023/09/12. +// + +import Foundation + +extension Sequence { + func asyncMap(_ transform: @escaping (Element) async -> T) async -> [T] { + return await withTaskGroup(of: T.self) { group in + var transformedElements = [T]() + + for element in self { + group.addTask { + return await transform(element) + } + } + + for await transformedElement in group { + transformedElements.append(transformedElement) + } + + return transformedElements + } + } +} diff --git a/Gifty/Presentation/ViewController/MainViewController.swift b/Gifty/Presentation/ViewController/MainViewController.swift index 540cc8e..e163c15 100644 --- a/Gifty/Presentation/ViewController/MainViewController.swift +++ b/Gifty/Presentation/ViewController/MainViewController.swift @@ -12,7 +12,6 @@ class MainViewController: UIViewController { private let viewModel = MainViewModel() private let defaultLabel = DefaultLabel(text: "불러온 사진이 없어요 🥲\n돋보기를 클릭해 기프티콘을 찾을 수 있어요!", size: 14, color: UIColor(hexCode: "333333")) private let appLogo = AppLogo() - private let detectBarcodeService = DetectBarcodeService() private let searchButton = SearchButton() private let rangePopup = RangePopup() private var cancellables = Set() @@ -79,8 +78,12 @@ extension MainViewController { rangePopup.setupAction( submit: UIAction { [self] _ in - rangePopup.removeFromSuperview() - detectBarcodeService.detectBarcodeInImage(images: []) + Task { + rangePopup.removeFromSuperview() + let barcodeImages = await viewModel.fetchPhotoImages() + print(viewModel.getBarcodeImageCount()) + print(barcodeImages) + } } ) } diff --git a/Gifty/Presentation/ViewModel/MainViewModel.swift b/Gifty/Presentation/ViewModel/MainViewModel.swift index 4520073..51807bb 100644 --- a/Gifty/Presentation/ViewModel/MainViewModel.swift +++ b/Gifty/Presentation/ViewModel/MainViewModel.swift @@ -6,9 +6,29 @@ // import UIKit +import Algorithms + +actor ProgressTracker { + private var _count: Int = 0 + private var _total: Int + + init(total: Int) { + self._total = total + } + + func increment() async { + _count += 1 + } + + var progressPercentage: Double { + return Double(_count) / Double(_total) * 100.0 + } +} class MainViewModel { private let photoLibraryService = PhotoLibraryService() + private let detectBarcodeService = DetectBarcodeService() + @Published var photoCount = 0 init(photoCount: Int = 0) { @@ -18,10 +38,50 @@ class MainViewModel { func setPermission() async -> UIAlertController? { let status = await photoLibraryService.requestPermission() self.photoCount = photoLibraryService.getPhotoCount() - + if (!status) { return photoLibraryService.createPhotoLibraryRequestAlert() } return nil } + + func getBarcodeImageCount() -> Int { + return detectBarcodeService.detectedBarcodes.count + } + + func fetchPhotoImages() async -> Set { + let photoAssets = photoLibraryService.getPhotoAssets() + + let chunkSize = 8 + let chunks = photoAssets.chunks(ofCount: chunkSize) + let progressTracker = ProgressTracker(total: photoAssets.count) + + for chunk in chunks { + await withTaskGroup(of: Void.self) { group in + for asset in chunk { + group.addTask { [self] in + let image = photoLibraryService.photoAssetToUIImage(photoAsset: asset) + guard let imageData = image.jpegData(compressionQuality: 0.8) else { + await updateProgress(using: progressTracker) + return + } + + detectBarcodeService.detectBarcodeInAsset(data: imageData) + await updateProgress(using: progressTracker) + } + } + } + } + + + print("바코드 찾기 끝") + return detectBarcodeService.detectedBarcodes + } + + + func updateProgress(using tracker: ProgressTracker) async { + await tracker.increment() + let progress = await tracker.progressPercentage + print("진행률: \(progress)%") + } } diff --git a/Gifty/Service/DetectBarcodeService.swift b/Gifty/Service/DetectBarcodeService.swift index 65fdb1a..0a3dcb3 100644 --- a/Gifty/Service/DetectBarcodeService.swift +++ b/Gifty/Service/DetectBarcodeService.swift @@ -9,47 +9,42 @@ import Foundation import UIKit import Vision +import Photos class DetectBarcodeService { - private var detectedBarcodes: Set = [] - - func detectBarcodeInImage(images: [UIImage]) { - for image in images { - guard let cgImage = image.cgImage else { + var detectedBarcodes: Set = [] + + func detectBarcodeInAsset(data: Data) { + let request = VNDetectBarcodesRequest { [self] (request, error) in + if let error = error { + print("Error detecting barcodes: \(error)") return } - let request = VNDetectBarcodesRequest { [weak self] (request, error) in - if let error = error { - print("Error detecting barcodes: \(error)") - return - } - - self?.processBarcodes(request: request, in: cgImage) - } - - let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) - request.revision = VNDetectBarcodesRequestRevision1 - - do { - try handler.perform([request]) - } catch { - print("Failed to perform request: \(error)") - } + + self.processBarcodes(request: request, data: data) + } + + let handler = VNImageRequestHandler(data: data, options: [:]) + + do { + try handler.perform([request]) + } catch { + print("Failed to perform request: \(error)") } } - - func processBarcodes(request: VNRequest, in cgImage: CGImage) { + + func processBarcodes(request: VNRequest, data: Data) { guard let results = request.results as? [VNBarcodeObservation] else { return } for barcode in results { - if let payload = barcode.payloadStringValue { - detectedBarcodes.insert(payload) - print("Detected barcode with value: \(payload) and symbology: \(barcode.symbology.rawValue)") + if barcode.payloadStringValue != nil, barcode.symbology == .code128 { + print("find") + if let image = UIImage(data: data) { + detectedBarcodes.insert(image) + } } } - - print("Find \(detectedBarcodes.count) barcodes") } } diff --git a/Gifty/Service/PhotoLibraryService.swift b/Gifty/Service/PhotoLibraryService.swift index d24e961..58bf0f8 100644 --- a/Gifty/Service/PhotoLibraryService.swift +++ b/Gifty/Service/PhotoLibraryService.swift @@ -56,4 +56,41 @@ class PhotoLibraryService { let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: .none) return fetchResult.count } + + func getPhotoImages() -> [UIImage?] { + let photoAssets = getPhotoAssets() + return photoAssets.map { asset in + self.photoAssetToUIImage(photoAsset: asset) + } + } + + func getPhotoAssets() -> [PHAsset] { + let fetchOptions = PHFetchOptions() + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions) + + var photoAssets: [PHAsset] = [] + for i in 0.. UIImage { + let manager = PHImageManager.default() + let option = PHImageRequestOptions() + var thumbnail = UIImage() + option.deliveryMode = .highQualityFormat + option.isSynchronous = true + + manager.requestImage(for: photoAsset, targetSize: CGSize(width: 550, height: 550), contentMode: .aspectFit, options: option, resultHandler: {(result, info)->Void in + if let result = result{ + thumbnail = result + } + }) + + return thumbnail + } }