Skip to content

Commit 7e4c9c4

Browse files
authored
Merge pull request #4 from allaboutapps/feature/no-singleton
Removed singleton, Toolbox dependency
2 parents 7f2ac8f + 3ad3c0f commit 7e4c9c4

File tree

7 files changed

+152
-61
lines changed

7 files changed

+152
-61
lines changed

Package.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,9 @@ let package = Package(
1010
products: [
1111
.library(name: "ForceUpdate", targets: ["ForceUpdate"]),
1212
],
13-
dependencies: [
14-
.package(url: "https://github.com/allaboutapps/Toolbox.git", from: "5.0.0"),
15-
],
1613
targets: [
1714
.target(
1815
name: "ForceUpdate",
19-
dependencies: [
20-
"Toolbox",
21-
],
2216
resources: [
2317
.process("Resources"),
2418
]

Sources/ForceUpdate/ForceUpdateController.swift

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
11
import Combine
22
import Foundation
33
import os
4-
import Toolbox
54

65
/// A controller that handles all the ForceUpdate feature logic.
76
public actor ForceUpdateController {
8-
// MARK: Init
7+
// MARK: Interface
98

10-
private init() {}
9+
/// JSONDecoder used for decoding the App Store lookup result
10+
public let appStoreLookupDecoder: JSONDecoder
1111

12-
// MARK: Properties
12+
/// JSONDecoder used for decoding the Public Version lookup result
13+
public let publicVersionLookupDecoder: JSONDecoder
1314

14-
private var checkForUpdateTask: Task<Void, Never>?
15-
private nonisolated let onForceUpdateNeededSubject = PassthroughSubject<URL?, Never>()
15+
/// Configures the timeout used for fetching App Store informations
16+
public let appStoreLookupTimeout: TimeInterval
1617

17-
// MARK: Interface
18+
/// Configures the timeout used for fetching version info from configured version URL
19+
public let publicVersionLookupTimeout: TimeInterval
20+
21+
/// Configures the URL for fetching the public version JSON file hosted by you
22+
public let publicVersionURL: URL
23+
24+
/// Configures the URL for fetching the App Store information of your already published app.
25+
///
26+
/// Defaults to `https://itunes.apple.com/lookup?bundleId=\(Bundle.main.bundleIdentifier)&country=at`
27+
public let appStoreLookupURL: URL
1828

19-
/// Singleton
20-
public static let shared = ForceUpdateController()
29+
public init(
30+
publicVersionURL: URL,
31+
appStoreLookupURL: URL = URL(
32+
string: "https://itunes.apple.com/lookup?bundleId=\(Bundle.main.bundleIdentifier!)&country=at"
33+
)!,
34+
appStoreLookupDecoder: JSONDecoder = Decoders.iso801,
35+
publicVersionLookupDecoder: JSONDecoder = Decoders.standardJSON,
36+
appStoreLookupTimeout: TimeInterval = 120.0,
37+
publicVersionLookupTimeout: TimeInterval = 120.0
38+
) {
39+
self.publicVersionURL = publicVersionURL
40+
self.appStoreLookupURL = appStoreLookupURL
41+
self.appStoreLookupDecoder = appStoreLookupDecoder
42+
self.publicVersionLookupDecoder = publicVersionLookupDecoder
43+
self.appStoreLookupTimeout = appStoreLookupTimeout
44+
self.publicVersionLookupTimeout = publicVersionLookupTimeout
45+
}
2146

2247
/// AsyncSequence that emits a value if the force update screen should be displayed. Returns AppStore URL of the app.
2348
public private(set) nonisolated lazy var onForceUpdateNeededAsyncSequence = onForceUpdateNeededSubject.values
@@ -47,45 +72,6 @@ public actor ForceUpdateController {
4772
/// Returns the AppStore look up result, if available.
4873
public private(set) var appStoreLookUp: AppStoreLookUpResult?
4974

50-
/// JSONDecoder used for decoding the App Store lookup result
51-
public var appStoreLookupDecoder: JSONDecoder = Decoders.iso801
52-
53-
/// JSONDecoder used for decoding the Public Version lookup result
54-
public var publicVersionLookupDecoder: JSONDecoder = Decoders.standardJSON
55-
56-
/// Configures the timeout used for fetching App Store informations
57-
public var appStoreLookupTimeout: TimeInterval = 120.0
58-
59-
/// Configures the timeout used for fetching version info from configured version URL
60-
public var publicVersionLookupTimeout: TimeInterval = 120.0
61-
62-
/// Configures the URL for fetching the public version JSON file hosted by you
63-
public var publicVersionURL: URL!
64-
65-
/// Configures the URL for fetching the App Store information of your already published app.
66-
///
67-
/// Defaults to `https://itunes.apple.com/lookup?bundleId=\(Bundle.main.bundleIdentifier)&country=at`
68-
public var appStoreLookupURL: URL!
69-
70-
/// Call this before using the `ForceUpdateController` to configure it.
71-
public func configure(
72-
publicVersionURL: URL,
73-
appStoreLookupURL: URL = URL(
74-
string: "https://itunes.apple.com/lookup?bundleId=\(Bundle.main.bundleIdentifier!)&country=at"
75-
)!,
76-
appStoreLookupDecoder: JSONDecoder? = nil,
77-
publicVersionLookupDecoder: JSONDecoder? = nil,
78-
appStoreLookupTimeout: TimeInterval = 120.0,
79-
publicVersionLookupTimeout: TimeInterval = 120.0
80-
) {
81-
self.publicVersionURL = publicVersionURL
82-
self.appStoreLookupURL = appStoreLookupURL
83-
self.appStoreLookupDecoder = appStoreLookupDecoder ?? Decoders.iso801
84-
self.publicVersionLookupDecoder = publicVersionLookupDecoder ?? Decoders.standardJSON
85-
self.appStoreLookupTimeout = appStoreLookupTimeout
86-
self.publicVersionLookupTimeout = publicVersionLookupTimeout
87-
}
88-
8975
/// Checks for updates. Thread-safe.
9076
/// Fetches current version from AppStore and from project version JSON.
9177
public func checkForUpdate() async {
@@ -103,7 +89,10 @@ public actor ForceUpdateController {
10389
return await checkForUpdateTask.value
10490
}
10591

106-
// MARK: Helpers
92+
// MARK: Private
93+
94+
private var checkForUpdateTask: Task<Void, Never>?
95+
private nonisolated let onForceUpdateNeededSubject = PassthroughSubject<URL?, Never>()
10796

10897
private func internalCheckForUpdate() async {
10998
os_log(.info, "checking for app update...")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
3+
extension Array {
4+
/// Usage:
5+
/// let array = [1, 2, 3, 4]
6+
/// array[safeIndex: 6] => nil
7+
subscript(safeIndex index: Int) -> Element? {
8+
guard index >= 0, index < endIndex else {
9+
return nil
10+
}
11+
12+
return self[index]
13+
}
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension Bundle {
4+
var appVersion: String? {
5+
infoDictionary?["CFBundleShortVersionString"] as? String
6+
}
7+
8+
var semanticAppVersion: SemanticVersion? {
9+
appVersion.flatMap { SemanticVersion($0) }
10+
}
11+
}

Sources/ForceUpdate/Helpers/Decoders.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import Foundation
22

3-
enum Decoders {
4-
static let iso801: JSONDecoder = {
3+
public enum Decoders {
4+
public static let iso801: JSONDecoder = {
55
let decoder = JSONDecoder()
66
decoder.dateDecodingStrategy = .iso8601
77
return decoder
88
}()
99

10-
static let standardJSON: JSONDecoder = {
10+
public static let standardJSON: JSONDecoder = {
1111
let decoder = JSONDecoder()
1212
decoder.dateDecodingStrategy = .custom(Decoders.decodeDate)
1313
return decoder

Sources/ForceUpdate/Helpers/SemanticVersion+Extensions.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import Foundation
2-
import Toolbox
32

43
extension SemanticVersion {
5-
64
init?(_ optionalString: String?) {
75
guard let string = optionalString else { return nil }
86
self.init(string)
97
}
108
}
119

1210
extension SemanticVersion?: Comparable {
13-
1411
public static func < (lhs: SemanticVersion?, rhs: SemanticVersion?) -> Bool {
1512
guard let lhs, let rhs else { return false }
1613
return lhs < rhs
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Foundation
2+
3+
/// Semantic versioning implemented according to [semver.org](https://semver.org/spec/v2.0.0.html)
4+
///
5+
/// - Note: pre-release versions and build identifiers are not supported
6+
public struct SemanticVersion {
7+
public let major: Int
8+
public let minor: Int
9+
public let patch: Int
10+
11+
public init(major: Int, minor: Int, patch: Int = 0) {
12+
self.major = major
13+
self.minor = minor
14+
self.patch = patch
15+
}
16+
}
17+
18+
// MARK: Codable
19+
20+
extension SemanticVersion: Codable {
21+
public init(from decoder: Decoder) throws {
22+
let container = try decoder.singleValueContainer()
23+
let versionString = try container.decode(String.self)
24+
guard let version = SemanticVersion(versionString) else {
25+
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Version string: \(versionString) is invalid"))
26+
}
27+
self = version
28+
}
29+
30+
public func encode(to encoder: Encoder) throws {
31+
var container = encoder.singleValueContainer()
32+
try container.encode(description)
33+
}
34+
}
35+
36+
// MARK: Comparable
37+
38+
extension SemanticVersion: Comparable {
39+
public static func == (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
40+
return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch
41+
}
42+
43+
public static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
44+
if lhs.major != rhs.major {
45+
return lhs.major < rhs.major
46+
} else if lhs.minor != rhs.minor {
47+
return lhs.minor < rhs.minor
48+
} else {
49+
return lhs.patch < rhs.patch
50+
}
51+
}
52+
}
53+
54+
// MARK: - LosslessStringConvertible
55+
56+
extension SemanticVersion: LosslessStringConvertible {
57+
public var description: String { "\(major).\(minor).\(patch)" }
58+
59+
public init?(_ string: String) {
60+
let splitted = string.split(separator: ".")
61+
guard splitted.count >= 2 && splitted.count <= 3 else { return nil }
62+
guard let majorString = splitted[safeIndex: 0], let minorString = splitted[safeIndex: 1] else { return nil }
63+
guard let major = Int(majorString), let minor = Int(minorString) else { return nil }
64+
65+
self.major = major
66+
self.minor = minor
67+
68+
if let patchString = splitted[safeIndex: 2] {
69+
if let patch = Int(patchString) {
70+
self.patch = patch
71+
} else {
72+
return nil
73+
}
74+
} else {
75+
self.patch = 0
76+
}
77+
}
78+
}
79+
80+
// MARK: ExpressibleByStringLiteral
81+
82+
extension SemanticVersion: ExpressibleByStringLiteral {
83+
public init(stringLiteral: StaticString) {
84+
self = SemanticVersion("\(stringLiteral)")!
85+
}
86+
}

0 commit comments

Comments
 (0)