Skip to content

Commit

Permalink
refactor!: Context APIs changes and documentation/onboarding (#180)
Browse files Browse the repository at this point in the history
release-as: 1.2.0
  • Loading branch information
fabriziodemaria authored Dec 10, 2024
1 parent 5369e04 commit 6eb5dc7
Show file tree
Hide file tree
Showing 18 changed files with 926 additions and 287 deletions.
4 changes: 2 additions & 2 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ excluded:
- ${PWD}/DerivedData
- ${PWD}/.build
- ${PWD}/Tools/*/.build
- ${PWD}/Sources/ConfidenceProvider/FlagResolver/
- ${PWD}/ConfidenceDemoApp

disabled_rules:
- discarded_notification_center_observer
Expand All @@ -19,8 +19,8 @@ analyzer_rules:
- unused_import

opt_in_rules:
- array_init
- attributes
- array_init
- closure_end_indentation
- closure_spacing
- collection_alignment
Expand Down
8 changes: 8 additions & 0 deletions ConfidenceDemoApp/ConfidenceDemoApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

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

/* Begin PBXFileReference section */
735EADF42CF9B64E007BC42C /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
C770C9682A739FA000C2AC8C /* ConfidenceDemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ConfidenceDemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
C770C9782A739FA100C2AC8C /* ConfidenceDemoAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfidenceDemoAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C770C9822A739FA100C2AC8C /* ConfidenceDemoAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfidenceDemoAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -103,6 +105,7 @@
C770C9AA2A73A06000C2AC8C /* Info.plist */,
C770C9992A739FBC00C2AC8C /* Assets.xcassets */,
C770C9972A739FBC00C2AC8C /* ConfidenceDemoApp.swift */,
735EADF42CF9B64E007BC42C /* LoginView.swift */,
C770C9982A739FBC00C2AC8C /* ContentView.swift */,
C770C9952A739FBC00C2AC8C /* Preview Content */,
);
Expand Down Expand Up @@ -243,6 +246,8 @@
Base,
);
mainGroup = C770C95F2A739FA000C2AC8C;
packageReferences = (
);
productRefGroup = C770C9692A739FA000C2AC8C /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -285,6 +290,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
735EADF52CF9B64E007BC42C /* LoginView.swift in Sources */,
C770C99C2A739FBC00C2AC8C /* ContentView.swift in Sources */,
C770C99B2A739FBC00C2AC8C /* ConfidenceDemoApp.swift in Sources */,
);
Expand Down Expand Up @@ -454,6 +460,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -485,6 +492,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "ConfidenceLogo.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
Expand Down
111 changes: 76 additions & 35 deletions ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift
Original file line number Diff line number Diff line change
@@ -1,50 +1,91 @@
import Confidence
import SwiftUI

class Status: ObservableObject {
enum State {
case unknown
case ready
case error(Error?)
}
@main
struct ConfidenceDemoApp: App {
@AppStorage("loggedUser")
private var loggedUser: String?
@AppStorage("appVersion")
private var appVersion = 0

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

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

@main
struct ConfidenceDemoApp: App {
@StateObject private var lifecycleObserver = ConfidenceAppLifecycleProducer()
confidence = Confidence
.Builder(clientSecret: secret, loggerLevel: .TRACE)
.withContext(initialContext: context)
.build()
do {
// NOTE: here we are activating all the flag values from storage, regardless of how `context` looks now
try confidence.activate()
} catch {
flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription))
}
// flaggingState.color is set here at startup and remains immutable until a user logs out
let eval = confidence.getEvaluation(
key: "swift-demoapp.color",
defaultValue: "Gray")
flaggingState.color = ContentView.getColor(
color: eval.value)
flaggingState.reason = eval.reason

self.appVersion = appVersion
self.loggedUser = loggedUser
updateConfidence()
}

var body: some Scene {
WindowGroup {
let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? ""
let confidence = Confidence.Builder(clientSecret: secret, loggerLevel: .TRACE)
.withContext(initialContext: [
"targeting_key": ConfidenceValue(string: UUID.init().uuidString),
"user_id": .init(string: "user2")
])
.build()

let status = Status()

ContentView(confidence: confidence, status: status)
.task {
do {
confidence.track(producer: lifecycleObserver)
try await self.setup(confidence: confidence)
status.state = .ready
} catch {
status.state = .error(error)
print(error.localizedDescription)
}
}
if loggedUser == nil {
LoginView(confidence: confidence)
.environmentObject(flaggingState)
} else {
ContentView(confidence: confidence)
.environmentObject(flaggingState)
}
}
}

private func updateConfidence() {
Task {
do {
flaggingState.state = .loading
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // simulating slow network
// The flags in storage are refreshed for the current `context`, and activated
// After this line, fresh (and potentially new) flags values can be accessed
try await confidence.fetchAndActivate()
flaggingState.state = .ready
} catch {
flaggingState.state = .error(ExperimentationFlags.CustomError(message: error.localizedDescription))
}
}
}
}

extension ConfidenceDemoApp {
func setup(confidence: Confidence) async throws {
try await confidence.fetchAndActivate()
class ExperimentationFlags: ObservableObject {
var color: Color = .red // This is set on applicaaton start, and reset on user logout
var reason: ResolveReason = .unknown
@Published var state: State = .notReady

enum State: Equatable {
case unknown
case notReady
case loading
case ready
case error(CustomError?)
}

public struct CustomError: Error, Equatable {
let message: String
}
}
160 changes: 123 additions & 37 deletions ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,145 @@ import Confidence
import Combine

struct ContentView: View {
@ObservedObject var status: Status
@StateObject var text = DisplayText()
@StateObject var color = FlagColor()
@EnvironmentObject
var flaggingState: ExperimentationFlags
@AppStorage("loggedUser")
private var loggedUser: String?
@State
private var isLoggingOut = false
@State
private var loggedOut = false

private let confidence: Confidence

init(confidence: Confidence, status: Status) {
init(confidence: Confidence, color: Color? = nil) {
self.confidence = confidence
self.status = status
}

var body: some View {
if case .ready = status.state {
NavigationStack {
VStack {
Image(systemName: "flag")
.imageScale(.large)
.foregroundColor(color.color)
.padding(10)
Text(text.text)
Button("Get remote flag value") {
text.text = confidence.getValue(key: "swift-demoapp.color", defaultValue: "ERROR")
if text.text == "Green" {
color.color = .green
} else if text.text == "Yellow" {
color.color = .yellow
} else {
color.color = .red
}
if let user = loggedUser {
Text("Hello \(user)")
.font(.largeTitle)
.padding()
}
Spacer()
NavigationLink(destination: AboutPage(confidence: confidence)) {
Text("Navigate")
.font(.headline)
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.blue)
.clipShape(Capsule())
}
Button("Flush 🚽") {
confidence.flush()
.padding()
Button(action: {
isLoggingOut = true
loggedUser = nil
flaggingState.state = .loading
flaggingState.color = .gray
Task {
await confidence.removeContextAndWait(key: "user_id")
flaggingState.state = .ready
}
loggedOut = true
}, label: {
Text("Logout")
.font(.headline)
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.red)
.clipShape(Capsule())
})
.navigationDestination(isPresented: $loggedOut) {
LoginView(confidence: confidence)
}
Spacer()
}
Spacer()
HStack {
Text("[1]")
if flaggingState.state == .loading && !isLoggingOut {
Text("Loading the text color...")
.font(.body)
} else {
let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "Gray")
Text("This text only appears after a successful flag fetching")
.font(.body)
.foregroundStyle(ContentView.getColor(color: eval.value))
Spacer()
Text("[\(eval.reason)]")
}
}.frame(maxWidth: .infinity, alignment: .leading)
.padding()
} else if case .error(let error) = status.state {
VStack {
Text("Provider Error")
Text(error?.localizedDescription ?? "An unknow error has occured.")
.foregroundColor(.red)
}
} else {
VStack {
ProgressView()
}
HStack {
let eval = confidence.getEvaluation(
key: "swift-demoapp.color",
defaultValue: "Gray")
Text("[2]")
Text("This text color dynamically changes on each flags fetch")
.font(.body)
.foregroundStyle(ContentView.getColor(
color: eval.value))
Spacer()
Text("[\(eval.reason)]")
}.frame(maxWidth: .infinity, alignment: .leading)
.padding()

HStack {
Text("[3]")
Text("This text color is fixed from app start, doesn't react on flag fetches")
.font(.body)
.foregroundStyle(flaggingState.color)
Spacer()
Text("[\(flaggingState.reason)]")
}.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
}

class DisplayText: ObservableObject {
@Published var text = "Hello World!"
static func getColor(color: String) -> Color {
switch color {
case "Green":
return .green
case "Yellow":
return .yellow
case "Gray":
return .gray
default:
return .red
}
}
}

struct AboutPage: View {
@State
private var textColor = Color.red
@State
private var reason = ResolveReason.unknown
private let confidence: Confidence

init(confidence: Confidence) {
self.confidence = confidence
}

class FlagColor: ObservableObject {
@Published var color: Color = .black
var body: some View {
HStack {
Text("This text color is set on onAppear, doesn't wait for flag fetch")
.font(.body)
.foregroundStyle(textColor)
.padding()
.onAppear {
let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "Gray")
textColor = ContentView.getColor(
color: eval.value)
reason = eval.reason
}
Spacer()
Text("[\(reason)]")
}
}
}
Loading

0 comments on commit 6eb5dc7

Please sign in to comment.