Skip to content

Commit 5f71b78

Browse files
Add dependency client for remote config. (#112)
* Add dependency client for remote config. * Update RemoteConfigKey.swift * Update RemoteConfigKey.swift * preconcurrency import --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 10ba53d commit 5f71b78

File tree

3 files changed

+104
-21
lines changed

3 files changed

+104
-21
lines changed

Examples/FirebaseDemo/ContentView.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ struct ContentView: View {
3333
}
3434
}
3535

36-
#Preview {
36+
#Preview("Promo: true") {
37+
let _ = prepareDependencies {
38+
$0.remoteConfig = MockRemoteConfig(config: ["showPromo": true])
39+
}
40+
41+
ContentView()
42+
}
43+
44+
#Preview("Promo: false") {
45+
let _ = prepareDependencies {
46+
$0.remoteConfig = MockRemoteConfig(config: ["showPromo": false])
47+
}
48+
3749
ContentView()
3850
}

Examples/FirebaseDemo/FirebaseDemoApp.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Dependencies
12
import FirebaseCore
23
import SwiftUI
34

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@preconcurrency import Combine
12
import Dependencies
23
@preconcurrency import FirebaseRemoteConfig
34
import Sharing
@@ -10,39 +11,108 @@ extension SharedReaderKey {
1011

1112
struct RemoteConfigKey<Value: Decodable & Sendable>: SharedReaderKey {
1213
let key: String
14+
let remoteConfig: any RemoteConfigClient
15+
16+
init(key: String) {
17+
self.key = key
18+
@Dependency(\.remoteConfig) var remoteConfig
19+
self.remoteConfig = remoteConfig
20+
}
21+
1322
var id: some Hashable { key }
14-
@Dependency(RemoteConfigDependencyKey.self) var remoteConfig
23+
1524
func load(context _: LoadContext<Value>, continuation: LoadContinuation<Value>) {
16-
remoteConfig.fetchAndActivate { changed, error in
17-
if let error {
18-
continuation.resume(throwing: error)
19-
} else {
20-
continuation.resume(with: Result { try remoteConfig.configValue(forKey: key).decoded() })
21-
}
22-
}
25+
remoteConfig.fetch(key: key, completion: continuation.resume(with:))
2326
}
2427
func subscribe(
2528
context _: LoadContext<Value>, subscriber: SharedSubscriber<Value>
2629
) -> SharedSubscription {
27-
let registration = remoteConfig.addOnConfigUpdateListener { update, error in
28-
guard error == nil else { return }
29-
remoteConfig.activate { changed, error in
30-
guard error == nil else { return }
31-
subscriber.yield(with: Result { try remoteConfig.configValue(forKey: key).decoded() })
32-
}
33-
}
30+
let cancellable = remoteConfig.addUpdateListener(
31+
key: key,
32+
subscriber: subscriber.yield(with:)
33+
)
3434
return SharedSubscription {
35-
registration.remove()
35+
cancellable.cancel()
3636
}
3737
}
3838
}
3939

40-
private enum RemoteConfigDependencyKey: DependencyKey {
41-
public static var liveValue: RemoteConfig {
42-
let remoteConfig = RemoteConfig.remoteConfig()
40+
protocol RemoteConfigClient: Sendable {
41+
func fetch<T: Decodable>(
42+
key: String,
43+
completion: @escaping (Result<T, any Error>) -> Void
44+
)
45+
func addUpdateListener<T: Decodable>(
46+
key: String,
47+
subscriber: @escaping (Result<T, any Error>) -> Void
48+
) -> AnyCancellable
49+
}
50+
51+
private enum RemoteConfigClientKey: DependencyKey {
52+
static var liveValue: any RemoteConfigClient {
53+
FirebaseRemoteConfig()
54+
}
55+
}
56+
57+
extension DependencyValues {
58+
var remoteConfig: any RemoteConfigClient {
59+
get { self[RemoteConfigClientKey.self] }
60+
set { self[RemoteConfigClientKey.self] = newValue }
61+
}
62+
}
63+
64+
struct FirebaseRemoteConfig: RemoteConfigClient {
65+
let remoteConfig = RemoteConfig.remoteConfig()
66+
init() {
4367
let settings = RemoteConfigSettings()
4468
settings.minimumFetchInterval = 0
4569
remoteConfig.configSettings = settings
46-
return remoteConfig
4770
}
71+
func fetch<T: Decodable>(key: String, completion: @escaping (Result<T, any Error>) -> Void) {
72+
remoteConfig.fetchAndActivate { _, error in
73+
completion(
74+
Result {
75+
try remoteConfig.configValue(forKey: key).decoded()
76+
}
77+
)
78+
}
79+
}
80+
func addUpdateListener<T: Decodable>(
81+
key: String,
82+
subscriber: @escaping (Result<T, any Error>) -> Void
83+
) -> AnyCancellable {
84+
let registration = remoteConfig.addOnConfigUpdateListener { _, error in
85+
guard error == nil else { return }
86+
remoteConfig.activate { changed, error in
87+
guard error == nil else { return }
88+
subscriber(Result { try remoteConfig.configValue(forKey: key).decoded() })
89+
}
90+
}
91+
return AnyCancellable { registration.remove() }
92+
}
93+
}
94+
95+
final class MockRemoteConfig: RemoteConfigClient {
96+
let config: [String: any Sendable]
97+
init(config: [String: any Sendable]) {
98+
self.config = config
99+
}
100+
func fetch<T: Decodable>(
101+
key: String,
102+
completion: @escaping (Result<T, any Error>) -> Void
103+
) {
104+
guard let value = config[key] as? T
105+
else {
106+
completion(.failure(NotFound()))
107+
return
108+
}
109+
completion(.success(value))
110+
}
111+
func addUpdateListener<T: Decodable>(
112+
key: String,
113+
subscriber: @escaping (Result<T, any Error>) -> Void
114+
) -> AnyCancellable {
115+
AnyCancellable {}
116+
}
117+
struct NotFound: Error {}
48118
}

0 commit comments

Comments
 (0)