Skip to content

Commit ccda0bd

Browse files
feat: Swift concurrency, async-await support added (#513)
* Add aync-await supported initialization method * Test cases added for aync-await initialization * Fetch qualified method aync-await support added * ODP fetch segment async-await test cases added * Start method updated * Remove unnecessary test cases, update legacy api * Update app side initialization * Update Tests/OptimizelyTests-APIs/OptimizelyClientTests_Init_Async_Await.swift Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * SegmentManager fetchQualifiedSegments method updated * Sample API udpated * Update sampple apis aync-await demo * Code documentation updated * Update Sources/Optimizely+Decide/OptimizelyUserContext.swift Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Revert back smaple APIs call --------- Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com>
1 parent 2f64936 commit ccda0bd

File tree

7 files changed

+412
-19
lines changed

7 files changed

+412
-19
lines changed

DemoSwiftApp/AppDelegate.swift

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,24 +120,38 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
120120

121121
addNotificationListeners()
122122

123-
// initialize SDK
124-
optimizely!.start { result in
125-
switch result {
126-
case .failure(let error):
127-
print("Optimizely SDK initiliazation failed: \(error)")
128-
case .success:
129-
print("Optimizely SDK initialized successfully!")
130-
@unknown default:
131-
print("Optimizely SDK initiliazation failed with unknown result")
123+
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
124+
Task {
125+
126+
do {
127+
try await optimizely.start()
128+
print("Optimizely SDK initialized successfully!")
129+
self.startWithRootViewController()
130+
} catch {
131+
print("Optimizely SDK initiliazation failed: \(error)")
132+
}
133+
134+
}
135+
} else {
136+
optimizely.start { result in
137+
switch result {
138+
case .failure(let error):
139+
print("Optimizely SDK initiliazation failed: \(error)")
140+
case .success:
141+
print("Optimizely SDK initialized successfully!")
142+
@unknown default:
143+
print("Optimizely SDK initiliazation failed with unknown result")
144+
}
145+
146+
self.startWithRootViewController()
132147
}
133-
134-
self.startWithRootViewController()
135-
136-
// For sample codes for APIs, see "Samples/SamplesForAPI.swift"
137-
//SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely)
138-
//SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely)
139-
//SamplesForAPI.checkAudienceSegments(optimizely: self.optimizely)
140148
}
149+
150+
// For sample codes for APIs, see "Samples/SamplesForAPI.swift"
151+
//SamplesForAPI.checkOptimizelyConfig(optimizely: self.optimizely)
152+
//SamplesForAPI.checkOptimizelyUserContext(optimizely: self.optimizely)
153+
//SamplesForAPI.chgiteckAudienceSegments(optimizely: self.optimizely)
154+
141155
}
142156

143157
func addNotificationListeners() {

DemoSwiftApp/Samples/SamplesForAPI.swift

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,14 +302,15 @@ class SamplesForAPI {
302302
defaultLogLevel: .debug)
303303

304304
guard let localDatafileUrl = Bundle.main.url(forResource: "demoTestDatafile", withExtension: "json"),
305-
let localDatafile = try? Data(contentsOf: localDatafileUrl)
305+
let localDatafile = try? Data(contentsOf: localDatafileUrl)
306306
else {
307307
fatalError("Local datafile cannot be found")
308308
}
309-
309+
310310
try? optimizely.start(datafile: localDatafile)
311-
311+
312312
let user = optimizely.createUserContext(userId: "user_123", attributes: ["location": "NY"])
313+
313314
user.fetchQualifiedSegments(options: [.ignoreCache]) { error in
314315
guard error == nil else {
315316
print("[AudienceSegments] \(error!)")
@@ -319,6 +320,27 @@ class SamplesForAPI {
319320
let decision = user.decide(key: "show_coupon", options: [.includeReasons])
320321
print("[AudienceSegments] decision: \(decision)")
321322
}
323+
324+
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
325+
Task { [user] in
326+
do {
327+
try await user.fetchQualifiedSegments(options: [.ignoreCache])
328+
let decision = user.decide(key: "show_coupon", options: [.includeReasons])
329+
print("[AudienceSegments] decision: \(decision)")
330+
} catch {
331+
print("[AudienceSegments] \(error)")
332+
}
333+
334+
// Without segment option
335+
do {
336+
try await user.fetchQualifiedSegments()
337+
let decision = user.decide(key: "show_coupon")
338+
print("[AudienceSegments] decision: \(decision)")
339+
} catch {
340+
print("[AudienceSegments] \(error)")
341+
}
342+
}
343+
}
322344
}
323345

324346
// MARK: - Initializations
@@ -386,6 +408,23 @@ class SamplesForAPI {
386408
}
387409

388410
print("activated variation: \(String(describing: variationKey))")
411+
412+
// [A3] Asynchronous initialization (aync-await)
413+
// 1. A datafile is downloaded from the server and the SDK is initialized with the datafile
414+
// 2. Polling datafile periodically.
415+
// The cached datafile is used immediately to update the SDK project config.
416+
optimizely = OptimizelyClient(sdkKey: "<Your_SDK_Key>",
417+
periodicDownloadInterval: 60)
418+
if #available(iOS 13, *) {
419+
Task { [optimizely] in
420+
do {
421+
try await optimizely.start()
422+
} catch {
423+
print("Optimizely SDK initiliazation failed: \(error)")
424+
}
425+
}
426+
}
427+
389428
}
390429

391430
}

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1978,6 +1978,8 @@
19781978
84F6BAB427FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */; };
19791979
84F6BADD27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */; };
19801980
84F6BADE27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */; };
1981+
98137C552A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98137C542A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift */; };
1982+
98137C572A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98137C562A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift */; };
19811983
BD1C3E8524E4399C0084B4DA /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B97DD93249D327F003DE606 /* SemanticVersion.swift */; };
19821984
BD64853C2491474500F30986 /* Optimizely.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E75167A22C520D400B2B157 /* Optimizely.h */; settings = {ATTRIBUTES = (Public, ); }; };
19831985
BD64853E2491474500F30986 /* Audience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169822C520D400B2B157 /* Audience.swift */; };
@@ -2414,6 +2416,8 @@
24142416
84E7ABBA27D2A1F100447CAE /* ThreadSafeLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeLogger.swift; sourceTree = "<group>"; };
24152417
84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP.swift; sourceTree = "<group>"; };
24162418
84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP_Decide.swift; sourceTree = "<group>"; };
2419+
98137C542A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Async_Await.swift; sourceTree = "<group>"; };
2420+
98137C562A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_ODP_Aync_Await.swift; sourceTree = "<group>"; };
24172421
BD6485812491474500F30986 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; };
24182422
C78CAF572445AD8D009FE876 /* OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyJSON.swift; sourceTree = "<group>"; };
24192423
C78CAF652446DB91009FE876 /* OptimizelyClientTests_OptimizelyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_OptimizelyJSON.swift; sourceTree = "<group>"; };
@@ -2946,6 +2950,7 @@
29462950
6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */,
29472951
6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */,
29482952
6E0A72D326C5B9AE00FF92B5 /* OptimizelyUserContextTests_ForcedDecisions.swift */,
2953+
98137C562A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift */,
29492954
84F6BAB227FCC5CF004BE62A /* OptimizelyUserContextTests_ODP.swift */,
29502955
84644AB228F0D2B3003FB9CB /* OptimizelyUserContextTests_ODP_2.swift */,
29512956
84F6BADC27FD011B004BE62A /* OptimizelyUserContextTests_ODP_Decide.swift */,
@@ -3019,6 +3024,7 @@
30193024
children = (
30203025
6E7519BC22C5211100B2B157 /* OptimizelyErrorTests.swift */,
30213026
6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */,
3027+
98137C542A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift */,
30223028
6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */,
30233029
6E7519C222C5211100B2B157 /* OptimizelyClientTests_Valid.swift */,
30243030
6E7519BE22C5211100B2B157 /* OptimizelyClientTests_Invalid.swift */,
@@ -4543,6 +4549,7 @@
45434549
6E994B3A25A3E6EA00999262 /* DecisionResponse.swift in Sources */,
45444550
6E75170A22C520D400B2B157 /* OptimizelyClient.swift in Sources */,
45454551
6E9B11AC22C5489300C22D81 /* OTUtils.swift in Sources */,
4552+
98137C572A42BA0F004896EB /* OptimizelyUserContextTests_ODP_Aync_Await.swift in Sources */,
45464553
6E75191C22C520D500B2B157 /* OPTNotificationCenter.swift in Sources */,
45474554
6E75180822C520D400B2B157 /* DataStoreFile.swift in Sources */,
45484555
6E7518EC22C520D400B2B157 /* ConditionHolder.swift in Sources */,
@@ -4618,6 +4625,7 @@
46184625
6E7518F822C520D500B2B157 /* UserAttribute.swift in Sources */,
46194626
6E7517A622C520D400B2B157 /* Array+Extension.swift in Sources */,
46204627
6E75191022C520D500B2B157 /* BackgroundingCallbacks.swift in Sources */,
4628+
98137C552A41E86F004896EB /* OptimizelyClientTests_Init_Async_Await.swift in Sources */,
46214629
6E7516F222C520D400B2B157 /* OptimizelyError.swift in Sources */,
46224630
);
46234631
runOnlyForDeploymentPostprocessing = 0;

Sources/Optimizely+Decide/OptimizelyUserContext.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,29 @@ extension OptimizelyUserContext {
214214
}
215215
}
216216

217+
/// Fetch (non-blocking) all qualified segments for the user context.
218+
///
219+
/// The segments fetched will be saved in **qualifiedSegments** and can be accessed any time.
220+
/// On failure, **qualifiedSegments** will be nil and one of these errors will be thrown:
221+
/// - OptimizelyError.invalidSegmentIdentifier
222+
/// - OptimizelyError.fetchSegmentsFailed(String)
223+
///
224+
/// - Parameters:
225+
/// - options: A set of options for fetching qualified segments (optional).
226+
/// - Throws: `OptimizelyError` if error is detected
227+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
228+
public func fetchQualifiedSegments(options: [OptimizelySegmentOption] = []) async throws {
229+
return try await withCheckedThrowingContinuation { continuation in
230+
fetchQualifiedSegments { error in
231+
if let error = error {
232+
continuation.resume(throwing: error)
233+
} else {
234+
continuation.resume()
235+
}
236+
}
237+
}
238+
}
239+
217240
/// Fetch (blocking) all qualified segments for the user context.
218241
///
219242
/// Note that this call will block the calling thread until fetching is completed.

Sources/Optimizely/OptimizelyClient.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,28 @@ open class OptimizelyClient: NSObject {
154154
}
155155
}
156156

157+
/// Start Optimizely SDK (async-await)
158+
///
159+
/// If an updated datafile is available in the server, it's downloaded and the SDK is configured with
160+
/// the updated datafile.
161+
///
162+
/// - Parameters:
163+
/// - resourceTimeout: timeout for datafile download (optional)
164+
/// - Throws: `OptimizelyError` if error is detected
165+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
166+
public func start(resourceTimeout: Double? = nil) async throws {
167+
return try await withCheckedThrowingContinuation { continuation in
168+
start(resourceTimeout: resourceTimeout) { result in
169+
switch result {
170+
case .success:
171+
continuation.resume()
172+
case .failure(let error):
173+
continuation.resume(throwing: error)
174+
}
175+
}
176+
}
177+
}
178+
157179
/// Start Optimizely SDK (Synchronous)
158180
///
159181
/// - Parameters:
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//
2+
// Copyright 2023, Optimizely, Inc. and contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import XCTest
18+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
19+
final class OptimizelyClientTests_Init_Async_Await: XCTestCase {
20+
let kUserId = "11111"
21+
let kExperimentKey = "exp_with_audience"
22+
let kFlagKey = "feature_1"
23+
let kVariationKey = "a"
24+
let kRevisionUpdated = "34"
25+
let kRevision = "241"
26+
27+
func testInitAsyncAwait() async throws {
28+
let testSdkKey = OTUtils.randomSdkKey // unique but consistent with registry + start
29+
30+
let handler = FakeDatafileHandler(mode: .successWithData)
31+
let optimizely = OptimizelyClient(sdkKey: testSdkKey,
32+
datafileHandler: handler)
33+
34+
try await optimizely.start()
35+
let user = OptimizelyUserContext(optimizely: optimizely, userId: self.kUserId)
36+
let decision = user.decide(key: self.kFlagKey)
37+
38+
XCTAssert(decision.variationKey == self.kVariationKey)
39+
}
40+
41+
func testInitAsyncAwait_fetchError() async throws {
42+
let testSdkKey = OTUtils.randomSdkKey // unique but consistent with registry + start
43+
44+
let handler = FakeDatafileHandler(mode: .failure)
45+
let optimizely = OptimizelyClient(sdkKey: testSdkKey,
46+
datafileHandler: handler)
47+
var _error: Error?
48+
do {
49+
try await optimizely.start()
50+
} catch {
51+
_error = error
52+
}
53+
54+
XCTAssertNotNil(_error)
55+
}
56+
57+
func testInitAsync_fetchNil_whenCacheLoadFailed() async {
58+
let testSdkKey = OTUtils.randomSdkKey // unique but consistent with registry + start
59+
60+
let handler = FakeDatafileHandler(mode: .failedToLoadFromCache)
61+
let optimizely = OptimizelyClient(sdkKey: testSdkKey,
62+
datafileHandler: handler)
63+
64+
var _error: Error?
65+
do {
66+
try await optimizely.start()
67+
} catch {
68+
_error = error
69+
}
70+
71+
XCTAssertNotNil(_error)
72+
}
73+
74+
}
75+
76+
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
77+
extension OptimizelyClientTests_Init_Async_Await {
78+
79+
enum DataFileResponse {
80+
case successWithData
81+
case failedToLoadFromCache
82+
case failure
83+
}
84+
85+
class FakeDatafileHandler: DefaultDatafileHandler {
86+
var mode: DataFileResponse
87+
var fileFlag: Bool = true
88+
89+
init(mode: DataFileResponse) {
90+
self.mode = mode
91+
}
92+
93+
required init() {
94+
fatalError("init() has not been implemented")
95+
}
96+
97+
override func downloadDatafile(sdkKey: String,
98+
returnCacheIfNoChange: Bool,
99+
resourceTimeoutInterval: Double?,
100+
completionHandler: @escaping DatafileDownloadCompletionHandler) {
101+
102+
switch mode {
103+
case .successWithData:
104+
let filename = fileFlag ? OptimizelyClientTests_Init_Async.JSONfilename : OptimizelyClientTests_Init_Async.JSONfilenameUpdated
105+
fileFlag.toggle()
106+
107+
let data = OTUtils.loadJSONDatafile(filename)
108+
completionHandler(.success(data))
109+
case .failedToLoadFromCache:
110+
completionHandler(.success(nil))
111+
case .failure:
112+
completionHandler(.failure(.dataFileInvalid))
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)