Skip to content

Commit 6eb5dc7

Browse files
refactor!: Context APIs changes and documentation/onboarding (#180)
release-as: 1.2.0
1 parent 5369e04 commit 6eb5dc7

18 files changed

+926
-287
lines changed

.swiftlint.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ excluded:
44
- ${PWD}/DerivedData
55
- ${PWD}/.build
66
- ${PWD}/Tools/*/.build
7-
- ${PWD}/Sources/ConfidenceProvider/FlagResolver/
7+
- ${PWD}/ConfidenceDemoApp
88

99
disabled_rules:
1010
- discarded_notification_center_observer
@@ -19,8 +19,8 @@ analyzer_rules:
1919
- unused_import
2020

2121
opt_in_rules:
22-
- array_init
2322
- attributes
23+
- array_init
2424
- closure_end_indentation
2525
- closure_spacing
2626
- collection_alignment

ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/project.pbxproj

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
733219BF2BE3C11100747AC2 /* ConfidenceOpenFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 733219BE2BE3C11100747AC2 /* ConfidenceOpenFeature */; };
11+
735EADF52CF9B64E007BC42C /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 735EADF42CF9B64E007BC42C /* LoginView.swift */; };
1112
C770C99A2A739FBC00C2AC8C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C770C9962A739FBC00C2AC8C /* Preview Assets.xcassets */; };
1213
C770C99B2A739FBC00C2AC8C /* ConfidenceDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C770C9972A739FBC00C2AC8C /* ConfidenceDemoApp.swift */; };
1314
C770C99C2A739FBC00C2AC8C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C770C9982A739FBC00C2AC8C /* ContentView.swift */; };
@@ -35,6 +36,7 @@
3536
/* End PBXContainerItemProxy section */
3637

3738
/* Begin PBXFileReference section */
39+
735EADF42CF9B64E007BC42C /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
3840
C770C9682A739FA000C2AC8C /* ConfidenceDemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ConfidenceDemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
3941
C770C9782A739FA100C2AC8C /* ConfidenceDemoAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfidenceDemoAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4042
C770C9822A739FA100C2AC8C /* ConfidenceDemoAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfidenceDemoAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -103,6 +105,7 @@
103105
C770C9AA2A73A06000C2AC8C /* Info.plist */,
104106
C770C9992A739FBC00C2AC8C /* Assets.xcassets */,
105107
C770C9972A739FBC00C2AC8C /* ConfidenceDemoApp.swift */,
108+
735EADF42CF9B64E007BC42C /* LoginView.swift */,
106109
C770C9982A739FBC00C2AC8C /* ContentView.swift */,
107110
C770C9952A739FBC00C2AC8C /* Preview Content */,
108111
);
@@ -243,6 +246,8 @@
243246
Base,
244247
);
245248
mainGroup = C770C95F2A739FA000C2AC8C;
249+
packageReferences = (
250+
);
246251
productRefGroup = C770C9692A739FA000C2AC8C /* Products */;
247252
projectDirPath = "";
248253
projectRoot = "";
@@ -285,6 +290,7 @@
285290
isa = PBXSourcesBuildPhase;
286291
buildActionMask = 2147483647;
287292
files = (
293+
735EADF52CF9B64E007BC42C /* LoginView.swift in Sources */,
288294
C770C99C2A739FBC00C2AC8C /* ContentView.swift in Sources */,
289295
C770C99B2A739FBC00C2AC8C /* ConfidenceDemoApp.swift in Sources */,
290296
);
@@ -454,6 +460,7 @@
454460
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
455461
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
456462
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
463+
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
457464
LD_RUNPATH_SEARCH_PATHS = (
458465
"$(inherited)",
459466
"@executable_path/Frameworks",
@@ -485,6 +492,7 @@
485492
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
486493
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
487494
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
495+
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
488496
LD_RUNPATH_SEARCH_PATHS = (
489497
"$(inherited)",
490498
"@executable_path/Frameworks",
Loading

ConfidenceDemoApp/ConfidenceDemoApp/Assets.xcassets/AppIcon.appiconset/Contents.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"images" : [
33
{
4+
"filename" : "ConfidenceLogo.png",
45
"idiom" : "universal",
56
"platform" : "ios",
67
"size" : "1024x1024"
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,91 @@
11
import Confidence
22
import SwiftUI
33

4-
class Status: ObservableObject {
5-
enum State {
6-
case unknown
7-
case ready
8-
case error(Error?)
9-
}
4+
@main
5+
struct ConfidenceDemoApp: App {
6+
@AppStorage("loggedUser")
7+
private var loggedUser: String?
8+
@AppStorage("appVersion")
9+
private var appVersion = 0
1010

11-
@Published var state: State = .unknown
12-
}
11+
private let confidence: Confidence
12+
private let flaggingState = ExperimentationFlags()
13+
private let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? "<Empty Secret>"
1314

15+
init() {
16+
@AppStorage("appVersion") var appVersion = 0
17+
@AppStorage("loggedUser") var loggedUser: String?
18+
appVersion += 1 // Simulate update of the app on every new run
19+
var context = ["app_version": ConfidenceValue.init(integer: appVersion)]
20+
if let user = loggedUser {
21+
context["user_id"] = ConfidenceValue.init(string: user)
22+
}
1423

15-
@main
16-
struct ConfidenceDemoApp: App {
17-
@StateObject private var lifecycleObserver = ConfidenceAppLifecycleProducer()
24+
confidence = Confidence
25+
.Builder(clientSecret: secret, loggerLevel: .TRACE)
26+
.withContext(initialContext: context)
27+
.build()
28+
do {
29+
// NOTE: here we are activating all the flag values from storage, regardless of how `context` looks now
30+
try confidence.activate()
31+
} catch {
32+
flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription))
33+
}
34+
// flaggingState.color is set here at startup and remains immutable until a user logs out
35+
let eval = confidence.getEvaluation(
36+
key: "swift-demoapp.color",
37+
defaultValue: "Gray")
38+
flaggingState.color = ContentView.getColor(
39+
color: eval.value)
40+
flaggingState.reason = eval.reason
41+
42+
self.appVersion = appVersion
43+
self.loggedUser = loggedUser
44+
updateConfidence()
45+
}
1846

1947
var body: some Scene {
2048
WindowGroup {
21-
let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? ""
22-
let confidence = Confidence.Builder(clientSecret: secret, loggerLevel: .TRACE)
23-
.withContext(initialContext: [
24-
"targeting_key": ConfidenceValue(string: UUID.init().uuidString),
25-
"user_id": .init(string: "user2")
26-
])
27-
.build()
28-
29-
let status = Status()
30-
31-
ContentView(confidence: confidence, status: status)
32-
.task {
33-
do {
34-
confidence.track(producer: lifecycleObserver)
35-
try await self.setup(confidence: confidence)
36-
status.state = .ready
37-
} catch {
38-
status.state = .error(error)
39-
print(error.localizedDescription)
40-
}
41-
}
49+
if loggedUser == nil {
50+
LoginView(confidence: confidence)
51+
.environmentObject(flaggingState)
52+
} else {
53+
ContentView(confidence: confidence)
54+
.environmentObject(flaggingState)
55+
}
56+
}
57+
}
58+
59+
private func updateConfidence() {
60+
Task {
61+
do {
62+
flaggingState.state = .loading
63+
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // simulating slow network
64+
// The flags in storage are refreshed for the current `context`, and activated
65+
// After this line, fresh (and potentially new) flags values can be accessed
66+
try await confidence.fetchAndActivate()
67+
flaggingState.state = .ready
68+
} catch {
69+
flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription))
70+
}
4271
}
4372
}
4473
}
4574

46-
extension ConfidenceDemoApp {
47-
func setup(confidence: Confidence) async throws {
48-
try await confidence.fetchAndActivate()
75+
class ExperimentationFlags: ObservableObject {
76+
var color: Color = .red // This is set on applicaaton start, and reset on user logout
77+
var reason: ResolveReason = .unknown
78+
@Published var state: State = .notReady
79+
80+
enum State: Equatable {
81+
case unknown
82+
case notReady
83+
case loading
84+
case ready
85+
case error(CustomError?)
86+
}
87+
88+
public struct CustomError: Error, Equatable {
89+
let message: String
4990
}
5091
}

ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift

+123-37
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,145 @@ import Confidence
33
import Combine
44

55
struct ContentView: View {
6-
@ObservedObject var status: Status
7-
@StateObject var text = DisplayText()
8-
@StateObject var color = FlagColor()
6+
@EnvironmentObject
7+
var flaggingState: ExperimentationFlags
8+
@AppStorage("loggedUser")
9+
private var loggedUser: String?
10+
@State
11+
private var isLoggingOut = false
12+
@State
13+
private var loggedOut = false
914

1015
private let confidence: Confidence
1116

12-
init(confidence: Confidence, status: Status) {
17+
init(confidence: Confidence, color: Color? = nil) {
1318
self.confidence = confidence
14-
self.status = status
1519
}
1620

1721
var body: some View {
18-
if case .ready = status.state {
22+
NavigationStack {
1923
VStack {
20-
Image(systemName: "flag")
21-
.imageScale(.large)
22-
.foregroundColor(color.color)
23-
.padding(10)
24-
Text(text.text)
25-
Button("Get remote flag value") {
26-
text.text = confidence.getValue(key: "swift-demoapp.color", defaultValue: "ERROR")
27-
if text.text == "Green" {
28-
color.color = .green
29-
} else if text.text == "Yellow" {
30-
color.color = .yellow
31-
} else {
32-
color.color = .red
33-
}
24+
if let user = loggedUser {
25+
Text("Hello \(user)")
26+
.font(.largeTitle)
27+
.padding()
28+
}
29+
Spacer()
30+
NavigationLink(destination: AboutPage(confidence: confidence)) {
31+
Text("Navigate")
32+
.font(.headline)
33+
.foregroundColor(.white)
34+
.padding(.horizontal, 20)
35+
.padding(.vertical, 10)
36+
.background(Color.blue)
37+
.clipShape(Capsule())
3438
}
35-
Button("Flush 🚽") {
36-
confidence.flush()
39+
.padding()
40+
Button(action: {
41+
isLoggingOut = true
42+
loggedUser = nil
43+
flaggingState.state = .loading
44+
flaggingState.color = .gray
45+
Task {
46+
await confidence.removeContextAndWait(key: "user_id")
47+
flaggingState.state = .ready
48+
}
49+
loggedOut = true
50+
}, label: {
51+
Text("Logout")
52+
.font(.headline)
53+
.foregroundColor(.white)
54+
.padding(.horizontal, 20)
55+
.padding(.vertical, 10)
56+
.background(Color.red)
57+
.clipShape(Capsule())
58+
})
59+
.navigationDestination(isPresented: $loggedOut) {
60+
LoginView(confidence: confidence)
3761
}
62+
Spacer()
3863
}
64+
Spacer()
65+
HStack {
66+
Text("[1]")
67+
if flaggingState.state == .loading && !isLoggingOut {
68+
Text("Loading the text color...")
69+
.font(.body)
70+
} else {
71+
let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "Gray")
72+
Text("This text only appears after a successful flag fetching")
73+
.font(.body)
74+
.foregroundStyle(ContentView.getColor(color: eval.value))
75+
Spacer()
76+
Text("[\(eval.reason)]")
77+
}
78+
}.frame(maxWidth: .infinity, alignment: .leading)
3979
.padding()
40-
} else if case .error(let error) = status.state {
41-
VStack {
42-
Text("Provider Error")
43-
Text(error?.localizedDescription ?? "An unknow error has occured.")
44-
.foregroundColor(.red)
45-
}
46-
} else {
47-
VStack {
48-
ProgressView()
49-
}
80+
HStack {
81+
let eval = confidence.getEvaluation(
82+
key: "swift-demoapp.color",
83+
defaultValue: "Gray")
84+
Text("[2]")
85+
Text("This text color dynamically changes on each flags fetch")
86+
.font(.body)
87+
.foregroundStyle(ContentView.getColor(
88+
color: eval.value))
89+
Spacer()
90+
Text("[\(eval.reason)]")
91+
}.frame(maxWidth: .infinity, alignment: .leading)
92+
.padding()
93+
94+
HStack {
95+
Text("[3]")
96+
Text("This text color is fixed from app start, doesn't react on flag fetches")
97+
.font(.body)
98+
.foregroundStyle(flaggingState.color)
99+
Spacer()
100+
Text("[\(flaggingState.reason)]")
101+
}.frame(maxWidth: .infinity, alignment: .leading)
102+
.padding()
50103
}
51104
}
52-
}
53105

54-
class DisplayText: ObservableObject {
55-
@Published var text = "Hello World!"
106+
static func getColor(color: String) -> Color {
107+
switch color {
108+
case "Green":
109+
return .green
110+
case "Yellow":
111+
return .yellow
112+
case "Gray":
113+
return .gray
114+
default:
115+
return .red
116+
}
117+
}
56118
}
57119

120+
struct AboutPage: View {
121+
@State
122+
private var textColor = Color.red
123+
@State
124+
private var reason = ResolveReason.unknown
125+
private let confidence: Confidence
126+
127+
init(confidence: Confidence) {
128+
self.confidence = confidence
129+
}
58130

59-
class FlagColor: ObservableObject {
60-
@Published var color: Color = .black
131+
var body: some View {
132+
HStack {
133+
Text("This text color is set on onAppear, doesn't wait for flag fetch")
134+
.font(.body)
135+
.foregroundStyle(textColor)
136+
.padding()
137+
.onAppear {
138+
let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "Gray")
139+
textColor = ContentView.getColor(
140+
color: eval.value)
141+
reason = eval.reason
142+
}
143+
Spacer()
144+
Text("[\(reason)]")
145+
}
146+
}
61147
}

0 commit comments

Comments
 (0)