Skip to content

Commit e707b0b

Browse files
authored
Merge pull request #96 from worthbak/wb/store-json-fetch
v0.2.0: async/await model overhaul; iPad support
2 parents 299b9d9 + 26ec9c4 commit e707b0b

30 files changed

+9940
-3483
lines changed

InventoryWatch.xcodeproj/project.pbxproj

Lines changed: 62 additions & 20 deletions
Large diffs are not rendered by default.

InventoryWatch/ContentView.swift

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import SwiftUI
99

1010
struct ContentView: View {
11-
@EnvironmentObject var model: Model
11+
@EnvironmentObject var model: ViewModel
1212

1313
@AppStorage("lastUpdateDate") private var lastUpdateDate: String = ""
1414
@AppStorage("preferredProductType") private var preferredProductType: String = "MacBookPro"
@@ -58,8 +58,8 @@ struct ContentView: View {
5858
}
5959

6060

61-
if let preferredStoreInfo = model.preferredStoreInfo {
62-
Text("\(shouldIncludeNearbyStores ? "near" : "at") \(preferredStoreInfo)")
61+
if let preferredStoreName = model.preferredStoreName {
62+
Text("\(shouldIncludeNearbyStores ? "near" : "at") \(preferredStoreName)")
6363
.font(.title2)
6464
}
6565
}
@@ -90,9 +90,9 @@ struct ContentView: View {
9090

9191
ForEach(model.availableParts, id: \.0.storeNumber) { data in
9292
Text("\(Text(data.0.storeName).font(storeFont)) \(Text(data.0.locationDescription).font(cityFont))")
93-
94-
let sortedProductNames = data.1.map { model.productName(forSKU: $0.partNumber) }
95-
.sortedNumerically()
93+
94+
let sortedProductNames = data.1.map { $0.partName }
95+
.sortedNumerically()
9696

9797
ForEach(sortedProductNames, id: \.self) { productName in
9898
Text(productName)
@@ -127,7 +127,7 @@ struct ContentView: View {
127127
}
128128

129129
Button(
130-
action: { model.fetchLatestInventory() },
130+
action: { Task { await model.fetchLatestInventory() } },
131131
label: { Image(systemName: "arrow.clockwise") }
132132
)
133133
.buttonStyle(BorderlessButtonStyle())
@@ -145,15 +145,17 @@ struct ContentView: View {
145145
alignment: .center
146146
)
147147
.onAppear {
148-
model.fetchLatestInventory()
149-
NotificationManager.shared.requestNotificationPermissions()
148+
Task {
149+
await model.fetchLatestInventory()
150+
NotificationManager.shared.requestNotificationPermissions()
151+
}
150152
}
151153
}
152154
}
153155

154-
struct ContentView_Previews: PreviewProvider {
155-
static var previews: some View {
156-
ContentView()
157-
.environmentObject(Model.testData)
158-
}
159-
}
156+
//struct ContentView_Previews: PreviewProvider {
157+
// static var previews: some View {
158+
// ContentView()
159+
// .environmentObject(Model.testData)
160+
// }
161+
//}

InventoryWatch/Countries.swift

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import Foundation
99

1010
struct Country: Hashable {
1111
let name: String
12-
13-
#warning("storePathComponent is unused currently")
14-
let storePathComponent: String
12+
let shortcode: String
13+
let locale: String
1514
let skuCode: String
1615

1716
private static let GermanyAltCode = "FD"
@@ -34,7 +33,7 @@ struct Country: Hashable {
3433
default:
3534
return nil
3635
}
37-
case .iPadWifi, .iPadCellular:
36+
case .iPadMiniWifi, .iPadMiniCellular, .iPad10thGenWifi, .iPad10thGenCellular, .iPadProM2_11in_Wifi, .iPadProM2_11in_Cellular, .iPadProM2_13in_Wifi, .iPadProM2_13in_Cellular:
3837
switch name {
3938
case "Germany":
4039
return Country.GermanyAltCode
@@ -64,6 +63,20 @@ struct Country: Hashable {
6463
default:
6564
return nil
6665
}
66+
67+
case .ApplePencilUSBCAdapter:
68+
switch self.name {
69+
case "United States", "Canada":
70+
return "AM"
71+
case "Germany", "United Kingdom", "France", "Austria", "Netherlands", "Italy":
72+
return "ZM"
73+
case "Thailand":
74+
return "ZA"
75+
case "Australia", "South Korea", "Japan", "Hong Kong":
76+
return "FE"
77+
default:
78+
return nil
79+
}
6780
default:
6881
return nil
6982
}
@@ -72,38 +85,25 @@ struct Country: Hashable {
7285

7386
let USData = Country(
7487
name: "United States",
75-
storePathComponent: "",
88+
shortcode: "US",
89+
locale: "en_US",
7690
skuCode: "LL"
7791
)
7892

79-
let Countries: [String: Country] = [
80-
"US": USData,
81-
"CA": Country(name: "Canada", storePathComponent: "/ca", skuCode: "LL"),
82-
"AU": Country(name: "Australia", storePathComponent: "/au", skuCode: "X"),
83-
"DE": Country(name: "Germany", storePathComponent: "/de", skuCode: "D"),
84-
"UK": Country(name: "United Kingdom", storePathComponent: "/uk", skuCode: "B"),
85-
"KR": Country(name: "South Korea", storePathComponent: "/kr", skuCode: "KH"),
86-
"HK": Country(name: "Hong Kong", storePathComponent: "/hk", skuCode: "ZP"),
87-
"FR": Country(name: "France", storePathComponent: "/fr", skuCode: "FN"),
88-
"IT": Country(name: "Italy", storePathComponent: "/it", skuCode: "T"),
89-
"JP": Country(name: "Japan", storePathComponent: "/jp", skuCode: "J"),
90-
"AT": Country(name: "Austria", storePathComponent: "/at", skuCode: "D"),
91-
"NL": Country(name: "Netherlands", storePathComponent: "/nl", skuCode: "N"),
92-
"TH": Country(name: "Thailand", storePathComponent: "/th", skuCode: "TH")
93-
];
94-
95-
let OrderedCountries = [
96-
"US",
97-
"CA",
98-
"AU",
99-
"DE",
100-
"UK",
101-
"KR",
102-
"HK",
103-
"FR",
104-
"IT",
105-
"JP",
106-
"AT",
107-
"NL",
108-
"TH"
93+
let AllCountries = [
94+
USData,
95+
Country(name: "Canada", shortcode: "CA", locale: "en_CA", skuCode: "LL"),
96+
Country(name: "Australia", shortcode: "AU", locale: "en_AU", skuCode: "X"),
97+
Country(name: "Germany", shortcode: "DE", locale: "de_DE", skuCode: "D"),
98+
Country(name: "United Kingdom", shortcode: "UK", locale: "en_GB", skuCode: "B"),
99+
Country(name: "South Korea", shortcode: "KR", locale: "ko_KR", skuCode: "KH"),
100+
Country(name: "Hong Kong", shortcode: "HK", locale: "en_HK", skuCode: "ZP"),
101+
Country(name: "France", shortcode: "FR", locale: "fr_FR", skuCode: "FN"),
102+
Country(name: "Italy", shortcode: "IT", locale: "it_IT", skuCode: "T"),
103+
Country(name: "Japan", shortcode: "JP", locale: "ja_JP", skuCode: "J"),
104+
Country(name: "Austria", shortcode: "AT", locale: "de_AT", skuCode: "D"),
105+
Country(name: "Netherlands", shortcode: "NL", locale: "nl_NL", skuCode: "N"),
106+
Country(name: "Thailand", shortcode: "TH", locale: "th_TH", skuCode: "TH"),
109107
]
108+
let Countries: [CountryCode: Country] = Dictionary(uniqueKeysWithValues: AllCountries.map { ($0.shortcode, $0) })
109+
let OrderedCountries: [CountryCode] = AllCountries.map { $0.shortcode }

InventoryWatch/Helpers.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
import Foundation
99

10+
typealias SKUString = String
11+
typealias CountryCode = String
12+
1013
extension Array where Element == String {
1114
func sortedNumerically() -> [Element] {
1215
sorted { lhs, rhs in
@@ -18,3 +21,44 @@ extension Array where Element == String {
1821
func compareNumeric(_ version1: String, _ version2: String) -> ComparisonResult {
1922
return version1.compare(version2, options: .numeric)
2023
}
24+
25+
// From: https://github.com/JohnSundell/AsyncCompatibilityKit/blob/main/Sources/URLSession%2BAsync.swift
26+
@available(macOS, deprecated: 12.0, message: "AsyncCompatibilityKit is only useful when targeting iOS versions earlier than 15")
27+
public extension URLSession {
28+
/// Start a data task with a URL using async/await.
29+
/// - parameter url: The URL to send a request to.
30+
/// - returns: A tuple containing the binary `Data` that was downloaded,
31+
/// as well as a `URLResponse` representing the server's response.
32+
/// - throws: Any error encountered while performing the data task.
33+
func data(from url: URL) async throws -> (Data, URLResponse) {
34+
try await data(for: URLRequest(url: url))
35+
}
36+
37+
/// Start a data task with a `URLRequest` using async/await.
38+
/// - parameter request: The `URLRequest` that the data task should perform.
39+
/// - returns: A tuple containing the binary `Data` that was downloaded,
40+
/// as well as a `URLResponse` representing the server's response.
41+
/// - throws: Any error encountered while performing the data task.
42+
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
43+
var dataTask: URLSessionDataTask?
44+
45+
return try await withTaskCancellationHandler(
46+
operation: {
47+
try await withCheckedThrowingContinuation { continuation in
48+
dataTask = self.dataTask(with: request) { data, response, error in
49+
guard let data = data, let response = response else {
50+
let error = error ?? URLError(.badServerResponse)
51+
return continuation.resume(throwing: error)
52+
}
53+
continuation.resume(returning: (data, response))
54+
}
55+
56+
dataTask?.resume()
57+
}
58+
},
59+
onCancel: { [dataTask] in
60+
dataTask?.cancel()
61+
}
62+
)
63+
}
64+
}

InventoryWatch/InventoryWatchApp.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import SwiftUI
99

1010
@main
1111
struct InventoryWatchApp: App {
12-
@StateObject var model = Model()
12+
@StateObject var model = ViewModel()
1313

1414
var body: some Scene {
1515
WindowGroup {
@@ -20,7 +20,7 @@ struct InventoryWatchApp: App {
2020
.commands {
2121
CommandGroup(replacing: .newItem) {
2222
Button(action: {
23-
model.fetchLatestInventory()
23+
Task { await model.fetchLatestInventory() }
2424
}, label: {
2525
Text("Reload Inventory")
2626
})
Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -111,24 +111,19 @@ struct AnalyticsData: Codable, Equatable {
111111
return
112112
}
113113

114-
var request = URLRequest(url: url)
115-
116-
let bodyData = data.toJsonData
117-
118-
// Change the URLRequest to a POST request
119-
request.httpMethod = "POST"
120-
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
121-
request.httpBody = bodyData
122-
123-
let session = URLSession.shared
124-
let task = session.dataTask(with: request) { (data, response, error) in
125-
126-
if let error = error {
127-
// Handle HTTP request error
114+
Task {
115+
let bodyData = data.toJsonData
116+
117+
var request = URLRequest(url: url)
118+
request.httpMethod = "POST"
119+
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
120+
request.httpBody = bodyData
121+
do {
122+
let (_, response) = try await URLSession.shared.data(for: request)
123+
print("successfully updated analytics: response code \((response as? HTTPURLResponse)?.statusCode ?? 0)")
124+
} catch {
128125
print(error)
129126
}
130127
}
131-
132-
task.resume()
133128
}
134129
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//
2+
// DefaultsVendor.swift
3+
// InventoryWatch
4+
//
5+
// Created by Worth Baker on 10/25/22.
6+
//
7+
8+
import Foundation
9+
import Combine
10+
11+
extension UserDefaults {
12+
13+
@objc dynamic var preferredStoreNumber: String {
14+
get { string(forKey: "preferredStoreNumber") ?? "R032" }
15+
set { setValue(newValue, forKey: "preferredStoreNumber") }
16+
}
17+
18+
@objc dynamic var lastUpdateDate: String? {
19+
get { string(forKey: "lastUpdateDate") }
20+
set { setValue(newValue, forKey: "lastUpdateDate") }
21+
}
22+
23+
@objc dynamic var preferredUpdateInterval: Int {
24+
get {
25+
if let currentValue = object(forKey: "preferredUpdateInterval") as? Int {
26+
return currentValue
27+
} else {
28+
// WB 10/28/22: Changing default update interval from 1min to 5min
29+
let presetValue = 5
30+
set(presetValue, forKey: "preferredUpdateInterval")
31+
32+
return presetValue
33+
}
34+
}
35+
set { setValue(newValue, forKey: "preferredUpdateInterval") }
36+
}
37+
}
38+
39+
struct DefaultsVendor {
40+
41+
var preferredCountry: Country {
42+
let value = UserDefaults.standard.string(forKey: "preferredCountry") ?? "US"
43+
44+
return Countries[value] ?? USData
45+
}
46+
47+
var preferredProductType: ProductType {
48+
let value = UserDefaults.standard.string(forKey: "preferredProductType") ?? "MacBookPro"
49+
50+
return ProductType(rawValue: value) ?? .MacBookPro
51+
}
52+
53+
var countryPathElement: String {
54+
let country = preferredCountry
55+
if country.shortcode == "US" {
56+
return ""
57+
} else {
58+
return country.shortcode + "/"
59+
}
60+
}
61+
62+
var preferredStoreNumber: String {
63+
get { return UserDefaults.standard.preferredStoreNumber }
64+
set { UserDefaults.standard.preferredStoreNumber = newValue }
65+
}
66+
67+
var lastUpdateDate: String? {
68+
get { return UserDefaults.standard.lastUpdateDate }
69+
set { UserDefaults.standard.lastUpdateDate = newValue }
70+
}
71+
72+
// unused - keeping this around as an example implementation
73+
private var preferredStoreNumberStream: AnyPublisher<String, Never> {
74+
return UserDefaults.standard
75+
.publisher(for: \.preferredStoreNumber)
76+
.eraseToAnyPublisher()
77+
}
78+
79+
var preferredUpdateInterval: Int {
80+
return UserDefaults.standard.preferredUpdateInterval
81+
}
82+
83+
var shouldIncludeNearbyStores: Bool {
84+
let value = UserDefaults.standard.object(forKey: "shouldIncludeNearbyStores") as? Bool
85+
86+
return value ?? true
87+
}
88+
89+
var preferredSKUs: Set<String> {
90+
guard let defaults = UserDefaults.standard.string(forKey: "preferredSKUs") else {
91+
return []
92+
}
93+
94+
return defaults.components(separatedBy: ",").reduce(into: Set<String>()) { partialResult, next in
95+
partialResult.insert(next)
96+
}
97+
}
98+
99+
var customSkuData: (sku: String, nickname: String)? {
100+
guard
101+
let sku = UserDefaults.standard.string(forKey: "customSku"),
102+
let name = UserDefaults.standard.string(forKey: "customSkuNickname")
103+
else {
104+
return nil
105+
}
106+
107+
return (sku, name)
108+
}
109+
110+
var showResultsOnlyForPreferredModels: Bool {
111+
UserDefaults.standard.bool(forKey: "showResultsOnlyForPreferredModels")
112+
}
113+
114+
var notifyOnlyForPreferredModels: Bool {
115+
UserDefaults.standard.bool(forKey: "notifyOnlyForPreferredModels")
116+
}
117+
}

0 commit comments

Comments
 (0)