Skip to content
This repository has been archived by the owner on Sep 5, 2023. It is now read-only.

Commit

Permalink
offline products consultation. Fix #35
Browse files Browse the repository at this point in the history
  • Loading branch information
philippeauriach committed Mar 5, 2019
1 parent 8d709ab commit 20723da
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 22 deletions.
3 changes: 2 additions & 1 deletion Cartfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ github "PiXeL16/IBLocalizable" "Swift4"
github "realm/realm-cocoa"
github "SnapKit/SnapKit"
github "scenee/FloatingPanel" "v1.3.5"
github "TimOliver/TOCropViewController"
github "TimOliver/TOCropViewController"
github "marmelroy/Zip" "1.1.0"
2 changes: 2 additions & 0 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ github "Alamofire/Alamofire" "4.8.1"
github "AliSoftware/OHHTTPStubs" "4dc6f36375f78c0b3cfe58d90bb8a4e21df5196e"
github "Daltron/NotificationBanner" "2.0.1"
github "DaveWoodCom/XCGLogger" "6.1.0"
github "Flinesoft/HandySwift" "2.8.0"
github "PiXeL16/IBLocalizable" "a0d7a8fab4cec66b592ac83f9efbc6f30bd21a9b"
github "Quick/Nimble" "v7.3.1"
github "Quick/Quick" "v1.3.2"
Expand All @@ -13,6 +14,7 @@ github "cbpowell/MarqueeLabel" "3.2.0"
github "hackiftekhar/IQKeyboardManager" "v5.0.6"
github "httpswift/swifter" "294dc8eaa7aed12f8695f43a5749b5c8f0f175b7"
github "kishikawakatsumi/KeychainAccess" "v3.1.2"
github "marmelroy/Zip" "1.1.0"
github "onevcat/Kingfisher" "5.1.0"
github "realm/realm-cocoa" "v3.13.1"
github "scenee/FloatingPanel" "v1.3.5"
Expand Down
18 changes: 18 additions & 0 deletions OpenFoodFacts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
30270C322226EAD000E6973D /* SummaryFooterCellController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 30270C312226EAD000E6973D /* SummaryFooterCellController.xib */; };
30270C342226F4A000E6973D /* operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30270C332226F4A000E6973D /* operators.swift */; };
30270C37222743D200E6973D /* EnvironmentImpactTableFormTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30270C36222743D200E6973D /* EnvironmentImpactTableFormTableViewController.swift */; };
3030EB84222E8290005A6169 /* OfflineProductsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3030EB83222E8290005A6169 /* OfflineProductsService.swift */; };
3030EB8A222E9202005A6169 /* RealmOfflineProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3030EB89222E9202005A6169 /* RealmOfflineProduct.swift */; };
3030EB8B222E992B005A6169 /* Zip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3030EB85222E8A08005A6169 /* Zip.framework */; };
3030EB8D222E9954005A6169 /* CSVStreamReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3030EB8C222E9954005A6169 /* CSVStreamReader.swift */; };
303C279D2201AB6B00159961 /* ScanProductSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303C279C2201AB6B00159961 /* ScanProductSummaryView.swift */; };
303C279F2201AB7B00159961 /* ScanProductSummaryView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 303C279E2201AB7B00159961 /* ScanProductSummaryView.xib */; };
303C27A12201ABC300159961 /* ManualBarcodeInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303C27A02201ABC300159961 /* ManualBarcodeInputView.swift */; };
Expand Down Expand Up @@ -307,6 +311,10 @@
30270C312226EAD000E6973D /* SummaryFooterCellController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SummaryFooterCellController.xib; sourceTree = "<group>"; };
30270C332226F4A000E6973D /* operators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = operators.swift; sourceTree = "<group>"; };
30270C36222743D200E6973D /* EnvironmentImpactTableFormTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentImpactTableFormTableViewController.swift; sourceTree = "<group>"; };
3030EB83222E8290005A6169 /* OfflineProductsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineProductsService.swift; sourceTree = "<group>"; };
3030EB85222E8A08005A6169 /* Zip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Zip.framework; path = Carthage/Build/iOS/Zip.framework; sourceTree = "<group>"; };
3030EB89222E9202005A6169 /* RealmOfflineProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmOfflineProduct.swift; sourceTree = "<group>"; };
3030EB8C222E9954005A6169 /* CSVStreamReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVStreamReader.swift; sourceTree = "<group>"; };
303C279C2201AB6B00159961 /* ScanProductSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanProductSummaryView.swift; sourceTree = "<group>"; };
303C279E2201AB7B00159961 /* ScanProductSummaryView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ScanProductSummaryView.xib; sourceTree = "<group>"; };
303C27A02201ABC300159961 /* ManualBarcodeInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualBarcodeInputView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -919,6 +927,7 @@
9526F8FB1FE1C5230008E1CC /* Crashlytics.framework in Frameworks */,
307BBEB921FA16B100E2DF9D /* FloatingPanel.framework in Frameworks */,
30DB17F522122E4A0010EE6F /* TOCropViewController.framework in Frameworks */,
3030EB8B222E992B005A6169 /* Zip.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1130,6 +1139,8 @@
children = (
957094AE1EA124E800268236 /* ProductService.swift */,
3000855D2207A3A500DC3896 /* TaxonomiesService.swift */,
3030EB83222E8290005A6169 /* OfflineProductsService.swift */,
3030EB8C222E9954005A6169 /* CSVStreamReader.swift */,
);
path = Network;
sourceTree = "<group>";
Expand Down Expand Up @@ -1176,6 +1187,7 @@
children = (
955BAFFD1FF828540046F419 /* RealmPendingUploadItem.swift */,
302525EB222307A200C2C830 /* RealmUserPreferences.swift */,
3030EB89222E9202005A6169 /* RealmOfflineProduct.swift */,
);
path = Realm;
sourceTree = "<group>";
Expand Down Expand Up @@ -1637,6 +1649,7 @@
95C265231E96D5C2004212EC /* Frameworks */ = {
isa = PBXGroup;
children = (
3030EB85222E8A08005A6169 /* Zip.framework */,
30DB17F422122E4A0010EE6F /* TOCropViewController.framework */,
307BBEB821FA16B100E2DF9D /* FloatingPanel.framework */,
9526F8F91FE1C5230008E1CC /* Crashlytics.framework */,
Expand Down Expand Up @@ -2232,6 +2245,7 @@
"$(SRCROOT)/Carthage/Build/iOS/RealmSwift.framework",
"$(SRCROOT)/Carthage/Build/iOS/FloatingPanel.framework",
"$(SRCROOT)/Carthage/Build/iOS/TOCropViewController.framework",
"$(SRCROOT)/Carthage/Build/iOS/Zip.framework",
);
name = Carthage;
outputPaths = (
Expand All @@ -2253,6 +2267,7 @@
"$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/RealmSwift.framework",
"$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/FloatingPanel.framework",
"$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/TOCropViewController.framework",
"$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Zip.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
Expand Down Expand Up @@ -2344,6 +2359,7 @@
958F33441F361A9D005269C5 /* IngredientsHeaderCellController.swift in Sources */,
956FF9551F1FF0CA0069D678 /* ProductTableViewCell.swift in Sources */,
95781BC31EC3A898003E3256 /* DoubleTransform.swift in Sources */,
3030EB84222E8290005A6169 /* OfflineProductsService.swift in Sources */,
9587DF731F84056F0069F0A6 /* PictureViewModel.swift in Sources */,
95781BBC1EC3A898003E3256 /* OFFReadAPIKeysJSON.swift in Sources */,
95A566141FD6037800C997C8 /* LocalizableTextField.swift in Sources */,
Expand Down Expand Up @@ -2413,9 +2429,11 @@
957CBEAC1F323B1F00A1B398 /* FormTableViewController.swift in Sources */,
957CBEC41F33B36400A1B398 /* SummaryFormTableViewController.swift in Sources */,
74C59E8B203FB9E2006C456F /* SharingManager.swift in Sources */,
3030EB8A222E9202005A6169 /* RealmOfflineProduct.swift in Sources */,
95D1D29C1FFC33B600595EA1 /* ShortcutParser.swift in Sources */,
9526F8F51FDF2A8B0008E1CC /* DataManagerClient.swift in Sources */,
958F335B1F37809A005269C5 /* PictureCallToActionView.swift in Sources */,
3030EB8D222E9954005A6169 /* CSVStreamReader.swift in Sources */,
95D060DD1F609DF70052012D /* LoadingCell.swift in Sources */,
95DB3BEA1FDD97D800E83B76 /* HistoryTableViewController.swift in Sources */,
958F334D1F377831005269C5 /* NutritionTableHeaderCellController.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

private func configureRealm() {
let config = Realm.Configuration(
schemaVersion: 21
schemaVersion: 23
)

Realm.Configuration.defaultConfiguration = config
Expand Down
1 change: 1 addition & 0 deletions Sources/Localization/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"product-detail.ingredients.allergens-list" = "Substances ou produits provoquant des allergies ou intolérances";
"product-detail.ingredients.allergens-alert.title" = "Contient des allergènes !";
"product-detail.ingredients.allergens-list.missing-infos" = "Ce produit n'est pas complet. En conséquence, nous n'avons pas pu évaluer la présence d'allergènes.";
"product-detail.ingredients.allergens-list.offline-product" = "Ce produit provient de la base de donnée \"hors-ligne\". En conséquence, nous n'avons pas pu évaluer la présence d'allergènes.";
"product-detail.ingredients.traces-list" = "Traces éventuelles";
"product-detail.ingredients.additives-list" = "Additifs";
"product-detail.ingredients.palm-oil-ingredients" = "Ingrédients issus de l'huile de palme";
Expand Down
6 changes: 6 additions & 0 deletions Sources/Models/DataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ protocol DataManagerProtocol {
isSummary: Bool,
onSuccess: @escaping (Product?) -> Void, onError: @escaping (Error) -> Void)

func getOfflineProduct(forCode: String) -> RealmOfflineProduct?

// User
func logIn(username: String, password: String, onSuccess: @escaping () -> Void, onError: @escaping (Error) -> Void)

Expand Down Expand Up @@ -94,6 +96,10 @@ class DataManager: DataManagerProtocol {
})
}

func getOfflineProduct(forCode: String) -> RealmOfflineProduct? {
return persistenceManager.getOfflineProduct(forCode: forCode)
}

// MARK: - User

func logIn(username: String, password: String, onSuccess: @escaping () -> Void, onError: @escaping (Error) -> Void) {
Expand Down
15 changes: 13 additions & 2 deletions Sources/Models/PersistenceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ protocol PersistenceManagerProtocol {
func save(additives: [Additive])
func additive(forCode: String) -> Additive?

// Offline
func save(offlineProducts: [RealmOfflineProduct])
func getOfflineProduct(forCode: String) -> RealmOfflineProduct?

// allergies settings
func addAllergy(toAllergen: Allergen)
func removeAllergy(toAllergen: Allergen)
Expand All @@ -52,14 +56,13 @@ class PersistenceManager: PersistenceManagerProtocol {

fileprivate func saveOrUpdate(objects: [Object]) {
let realm = getRealm()
print(Realm.Configuration.defaultConfiguration.fileURL!)

do {
try realm.write {
realm.add(objects, update: true)
}
} catch let error as NSError {
log.error(error)
log.error("ERROR SAVING INTO REALM \(error)")
Crashlytics.sharedInstance().recordError(error)
}
}
Expand Down Expand Up @@ -178,6 +181,14 @@ class PersistenceManager: PersistenceManagerProtocol {
return getRealm().object(ofType: Additive.self, forPrimaryKey: code)
}

func save(offlineProducts: [RealmOfflineProduct]) {
saveOrUpdate(objects: offlineProducts)
}

func getOfflineProduct(forCode: String) -> RealmOfflineProduct? {
return getRealm().object(ofType: RealmOfflineProduct.self, forPrimaryKey: forCode)
}

// MARK: User Preferences
fileprivate func getRealmUserPreferences() -> RealmUserPreferences {
if let prefs = getRealm().objects(RealmUserPreferences.self).first {
Expand Down
25 changes: 25 additions & 0 deletions Sources/Models/Realm/RealmOfflineProduct.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// RealmOfflineProduct.swift
// OpenFoodFacts
//
// Created by Philippe Auriach on 05/03/2019.
// Copyright © 2019 Andrés Pizá Bückmann. All rights reserved.
//

import Foundation
import RealmSwift

class RealmOfflineProduct: Object {

@objc dynamic var barcode = ""
@objc dynamic var name: String?
@objc dynamic var quantity: String?
@objc dynamic var brands: String?
@objc dynamic var nutritionGrade: String?
@objc dynamic var novaGroup: String?


override static func primaryKey() -> String? {
return "barcode"
}
}
157 changes: 157 additions & 0 deletions Sources/Network/CSVStreamReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//
// OfflineService.swift
// OpenFoodFacts
//
// Created by Philippe Auriach on 05/03/2019.
//

import Foundation

class CSVStreamReader {
let encoding: String.Encoding
let chunkSize: Int
let fileHandle: FileHandle
var buffer: Data
let delimPattern: Data
let csvDelimiter: String
var colHeaders: [String]?

init?(url: URL,
delimiter: String = "\n",
encoding: String.Encoding = .utf8,
chunkSize: Int = 4096,
csvDelimiter: String = ",",
colHeaders: [String]? = nil) {

if FileManager.default.fileExists(atPath: url.path) == false {
log.error("[CSVStreamReader] File do not exist, impossible to use CSVStreamReader")
return nil
}

do {
let fileHandle = try FileHandle(forReadingFrom: url)
self.fileHandle = fileHandle
} catch let error as NSError {
log.error("[CSVStreamReader] File handle no created ? \(error)")
return nil
}

self.chunkSize = chunkSize
self.encoding = encoding
buffer = Data(capacity: chunkSize)
delimPattern = delimiter.data(using: .utf8)!
self.csvDelimiter = csvDelimiter

self.colHeaders = colHeaders
}

deinit {
fileHandle.closeFile()
}

func nextLine() -> String? {
repeat {
if let range = buffer.range(of: delimPattern, options: [], in: buffer.startIndex ..< buffer.endIndex) {
let subData = buffer.subdata(in: buffer.startIndex ..< range.lowerBound)
let line = String(data: subData, encoding: encoding)
buffer.replaceSubrange(buffer.startIndex ..< range.upperBound, with: [])
return line
} else {
let tempData = fileHandle.readData(ofLength: chunkSize)
if tempData.isEmpty {
return (buffer.isEmpty == false) ? String(data: buffer, encoding: encoding) : nil
}
buffer.append(tempData)
}
} while true
}

func nextCSVLine() -> [String]? {
guard let line = nextLine() else {
return nil
}

return line.components(separatedBy: csvDelimiter)
.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }
}

func streamCSV(onLineItem: @escaping ([String: String]) -> Void) {
if colHeaders == nil {
colHeaders = nextCSVLine()
}

guard let colHeaders = colHeaders else {
log.error("[CSVStreamReader] Tried to stream in csv mode without headers ??")
return
}

var continueRepeat = true
repeat {
autoreleasepool {
var lineItem = [String: String]()
guard let line = nextCSVLine() else {
continueRepeat = false
return
}
for (index, col) in colHeaders.enumerated() where line.count > index {
lineItem[col] = line[index]
}
if lineItem.isEmpty == false {
onLineItem(lineItem)
}
}
} while continueRepeat
}
}

class TypedCSVStreamReader<T>: CSVStreamReader {

func batchStream(batchSize: Int, parse: @escaping ([String]) -> T?, treatBatch: @escaping ([T]) -> Void) {
var batch = [T]()

var continueRepeat = true
repeat {
autoreleasepool {
guard let line = nextLine() else {
continueRepeat = false
return
}
let datas = line.split(separator: "\t").map { $0.trimmingCharacters(in: .whitespaces )}
if datas.isEmpty == false, let parsed = parse(datas) {
batch.append(parsed)
}

if batch.count >= batchSize {
treatBatch(batch)
batch.removeAll()
}
}
} while continueRepeat

if batch.isEmpty == false {
treatBatch(batch)
batch.removeAll()
}
}

func batchStreamCSV(batchSize: Int, parse: @escaping ([String: String]) -> T?, treatBatch: @escaping ([T]) -> Void) {
var batch = [T]()

streamCSV { (lineItem: [String: String]) in
if let item = parse(lineItem) {
batch.append(item)
}

if batch.count >= batchSize {
treatBatch(batch)
batch.removeAll()
}
}

if batch.isEmpty == false {
treatBatch(batch)
batch.removeAll()
}
}

}
Loading

0 comments on commit 20723da

Please sign in to comment.