diff --git a/DXFeedFramework.xcodeproj/project.pbxproj b/DXFeedFramework.xcodeproj/project.pbxproj index 94bbdd57a..b54cdb533 100644 --- a/DXFeedFramework.xcodeproj/project.pbxproj +++ b/DXFeedFramework.xcodeproj/project.pbxproj @@ -1017,6 +1017,8 @@ 64F73BA32B67B28B0088EC37 /* NativePromise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePromise.swift; sourceTree = ""; }; 64F73BA52B67CD5B0088EC37 /* GraalException+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraalException+Ext.swift"; sourceTree = ""; }; 64F9C6C12B4BFD8F003ED014 /* DXFeedconnect.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DXFeedconnect.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 64FDD7B62C21A09000D4469B /* DxFeedReconnectSample.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DxFeedReconnectSample.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 64FDD7B72C21AF9D00D4469B /* SimpleAuthSample.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = SimpleAuthSample.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 803BAC0D29BFA50700FFAB1C /* DXFeedFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DXFeedFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 803BAC1029BFA50700FFAB1C /* DxFeedSwiftFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DxFeedSwiftFramework.h; sourceTree = ""; }; 803BAC1529BFA50700FFAB1C /* DXFeedFrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DXFeedFrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1237,6 +1239,8 @@ 6433B1302BCE87D4004EFED7 /* RequestProfile.playground */, 64A631CF2BDFAA27002E1002 /* OnDemandSample.playground */, 643A329C2BD15F2900F6F790 /* LastEventsConsole.playground */, + 64FDD7B62C21A09000D4469B /* DxFeedReconnectSample.playground */, + 64FDD7B72C21AF9D00D4469B /* SimpleAuthSample.playground */, ); path = Playgrounds; sourceTree = ""; diff --git a/DXFeedFramework/Api/Osub/TimeSeriesSubscriptionSymbol.swift b/DXFeedFramework/Api/Osub/TimeSeriesSubscriptionSymbol.swift index 904df0b0d..5e556cc5f 100644 --- a/DXFeedFramework/Api/Osub/TimeSeriesSubscriptionSymbol.swift +++ b/DXFeedFramework/Api/Osub/TimeSeriesSubscriptionSymbol.swift @@ -33,7 +33,7 @@ public class TimeSeriesSubscriptionSymbol: GenericIndexedEventSubscriptionSymbol /// - symbol: The event symbol. /// - date: Date. Just for easing initialization with date object convenience public init(symbol: AnyHashable, date: Date) { - self.init(symbol: symbol, fromTime: Long(date.timeIntervalSince1970) * 1000) + self.init(symbol: symbol, fromTime: date.millisecondsSince1970) } /// Initializes a new instance of the ``TimeSeriesSubscriptionSymbol`` class @@ -43,7 +43,7 @@ public class TimeSeriesSubscriptionSymbol: GenericIndexedEventSubscriptionSymbol /// - symbol: The event ``Symbol`` /// - date: Date. Just for easing initialization with date object convenience public init(symbol: Symbol, date: Date) { - self.init(symbol: symbol.stringValue, fromTime: Long(date.timeIntervalSince1970) * 1000) + self.init(symbol: symbol.stringValue, fromTime: date.millisecondsSince1970) } static func == (lhs: TimeSeriesSubscriptionSymbol, rhs: TimeSeriesSubscriptionSymbol) -> Bool { diff --git a/DXFeedFramework/Events/Market/Candles/Candle.swift b/DXFeedFramework/Events/Market/Candles/Candle.swift index 999e03d31..d4640a840 100644 --- a/DXFeedFramework/Events/Market/Candles/Candle.swift +++ b/DXFeedFramework/Events/Market/Candles/Candle.swift @@ -72,7 +72,7 @@ public class Candle: MarketEvent, ITimeSeriesEvent, ILastingEvent, CustomStringC """ DXFG_CANDLE_T \ eventSymbol: \(eventSymbol) \ -eventTime: \(Date(timeIntervalSince1970: TimeInterval(Double(time/1000)))) \ +eventTime: \(Date(millisecondsSince1970: time))) \ eventSymbol: \(eventSymbol), \ eventTime: \(eventTime), \ eventFlags: \(eventFlags), \ diff --git a/DXFeedFramework/Ipf/InstrumentProfileField.swift b/DXFeedFramework/Ipf/InstrumentProfileField.swift index 1b3d22a62..e9a328e15 100644 --- a/DXFeedFramework/Ipf/InstrumentProfileField.swift +++ b/DXFeedFramework/Ipf/InstrumentProfileField.swift @@ -274,7 +274,7 @@ extension InstrumentProfileField { guard let date = dateFormatter.date(from: value) else { return 0 } - parsedDates[value] = Entry(text: value, binary: date.millisecondsSince1970() / TimeUtil.day) + parsedDates[value] = Entry(text: value, binary: date.millisecondsSince1970 / TimeUtil.day) return parsedDates[value]?.binary ?? 0 } } diff --git a/DXFeedFramework/Native/OnDemandService/NativeOnDemandService.swift b/DXFeedFramework/Native/OnDemandService/NativeOnDemandService.swift index a5e21e524..3b838eca8 100644 --- a/DXFeedFramework/Native/OnDemandService/NativeOnDemandService.swift +++ b/DXFeedFramework/Native/OnDemandService/NativeOnDemandService.swift @@ -75,7 +75,7 @@ class NativeOnDemandService { try ErrorCheck.nativeCall(thread, dxfg_OnDemandService_replay(thread, native, - date.millisecondsSince1970())) + date.millisecondsSince1970)) } func replay(date: Date, speed: Double) throws { @@ -83,7 +83,7 @@ class NativeOnDemandService { try ErrorCheck.nativeCall(thread, dxfg_OnDemandService_replay2(thread, native, - date.millisecondsSince1970(), + date.millisecondsSince1970, speed)) } diff --git a/DXFeedFramework/Native/Subscription/NativeTimeSeriesSubscription.swift b/DXFeedFramework/Native/Subscription/NativeTimeSeriesSubscription.swift index 1eba384e6..153803c75 100644 --- a/DXFeedFramework/Native/Subscription/NativeTimeSeriesSubscription.swift +++ b/DXFeedFramework/Native/Subscription/NativeTimeSeriesSubscription.swift @@ -29,7 +29,7 @@ class NativeTimeSeriesSubscription { _ = try ErrorCheck.nativeCall(thread, dxfg_DXFeedTimeSeriesSubscription_setFromTime( thread, native, - 0)) + fromTime)) } } diff --git a/DXFeedFramework/OnDemand/OnDemandService.swift b/DXFeedFramework/OnDemand/OnDemandService.swift index 3798a3287..39d05f72d 100644 --- a/DXFeedFramework/OnDemand/OnDemandService.swift +++ b/DXFeedFramework/OnDemand/OnDemandService.swift @@ -114,7 +114,7 @@ public class OnDemandService { /// Changes on-demand historical data replay speed while continuing replay at current ``getTime``. /// Speed is measured with respect to the real-time playback speed. /// - Parameters: - /// - speed: on-demand historical data replay speed. + /// - speed: on-demand historical data replay speed. Should be > 0. /// - Throws: ``GraalException``. Rethrows exception from Java. public func setSpeed(_ speed: Double) throws { try native.setSpeed(speed) diff --git a/DXFeedFramework/Utils/Date+Ext.swift b/DXFeedFramework/Utils/Date+Ext.swift index f6aecb650..a476c7019 100644 --- a/DXFeedFramework/Utils/Date+Ext.swift +++ b/DXFeedFramework/Utils/Date+Ext.swift @@ -7,13 +7,12 @@ import Foundation public extension Date { - func millisecondsSince1970() -> TimeInterval { - return timeIntervalSince1970 * 1000 - } - func millisecondsSince1970() -> Long { - return Int64(timeIntervalSince1970 * 1000) - } + public var millisecondsSince1970: Long { Long(timeIntervalSince1970 * 1000) } + +// func millisecondsSince1970 -> Long { +// return Int64(timeIntervalSince1970 * 1000) +// } init(millisecondsSince1970: Long) { self.init(timeIntervalSince1970: Double(millisecondsSince1970) / 1000) diff --git a/DXFeedFrameworkTests/DXAsyncLastTest.swift b/DXFeedFrameworkTests/DXAsyncLastTest.swift index 69275c7cc..da34577c4 100644 --- a/DXFeedFrameworkTests/DXAsyncLastTest.swift +++ b/DXFeedFrameworkTests/DXAsyncLastTest.swift @@ -78,7 +78,7 @@ final class DXAsyncLastTest: XCTestCase { let date = Calendar.current.date(byAdding: .month, value: -1, to: Date())! guard let task = feed?.getTimeSeries(type: Candle.self, symbol: "AAPL{=1d}", - fromTime: date.millisecondsSince1970(), + fromTime: date.millisecondsSince1970, toTime: Long.max) else { XCTAssert(false, "Async task is nil") return diff --git a/DXFeedFrameworkTests/DXConnectionTest.swift b/DXFeedFrameworkTests/DXConnectionTest.swift index cf5c79510..c73cb9a91 100644 --- a/DXFeedFrameworkTests/DXConnectionTest.swift +++ b/DXFeedFrameworkTests/DXConnectionTest.swift @@ -64,6 +64,48 @@ final class DXConnectionTest: XCTestCase { wait(for: [receivedEventsExpectation], timeout: 20) } + func testDXLinkConnectionTheoPrice() throws { + throw XCTSkip("Just for reflection testing") + + // For token-based authorization, use the following address format: + // "dxlink:wss://demo.dxfeed.com/dxlink-ws[login=dxlink:token]" + let endpoint = try DXEndpoint.builder() + .build() + + let subscription = try endpoint.getFeed()?.createSubscription(TheoPrice.self) + let receivedEventsExpectation = expectation(description: "Received events") + let eventListener = DXConnectionListener(expectation: receivedEventsExpectation) + try subscription?.add(listener: eventListener) + try subscription?.addSymbols( + TimeSeriesSubscriptionSymbol(symbol: ".AAPL240524C110", fromTime: 10) + ) + try endpoint.connect("dxlink:wss://demo.dxfeed.com/dxlink-ws") + defer { + try? endpoint.closeAndAwaitTermination() + } + wait(for: [receivedEventsExpectation], timeout: 10) + } + + func testDXLinkConnectionGreeks() throws { + throw XCTSkip("Just for reflection testing") + + // For token-based authorization, use the following address format: + // "dxlink:wss://demo.dxfeed.com/dxlink-ws[login=dxlink:token]" + let endpoint = try DXEndpoint.builder() + .build() + + let subscription = try endpoint.getFeed()?.createSubscription(Greeks.self) + let receivedEventsExpectation = expectation(description: "Received events") + let eventListener = DXConnectionListener(expectation: receivedEventsExpectation) + try subscription?.add(listener: eventListener) + try subscription?.addSymbols(".AAPL240524C110") + try endpoint.connect("dxlink:wss://demo.dxfeed.com/dxlink-ws") + defer { + try? endpoint.closeAndAwaitTermination() + } + wait(for: [receivedEventsExpectation], timeout: 10) + } + func testConnection() throws { // For token-based authorization, use the following address format: // "demo.dxfeed.com:7300[login=entitle:token]" diff --git a/DXFeedFrameworkTests/DXPromiseTest.swift b/DXFeedFrameworkTests/DXPromiseTest.swift index cacf75746..fa83ad6bd 100644 --- a/DXFeedFrameworkTests/DXPromiseTest.swift +++ b/DXFeedFrameworkTests/DXPromiseTest.swift @@ -211,7 +211,7 @@ final class DXPromiseTest: XCTestCase { guard let promise = try feed?.getTimeSeriesPromise(type: Candle.self, symbol: "AAPL{=1d}", - fromTime: date.millisecondsSince1970(), + fromTime: date.millisecondsSince1970, toTime: Long.max) else { XCTAssert(false, "Empty promise") return diff --git a/DXFeedFrameworkTests/DXTimeSeriesSubscriptionTest.swift b/DXFeedFrameworkTests/DXTimeSeriesSubscriptionTest.swift index ce7579c29..d7b825561 100644 --- a/DXFeedFrameworkTests/DXTimeSeriesSubscriptionTest.swift +++ b/DXFeedFrameworkTests/DXTimeSeriesSubscriptionTest.swift @@ -36,8 +36,8 @@ final class DXTimeSeriesSubscriptionTest: XCTestCase { return anonymCl } try subscription?.add(listener: listener) - try subscription?.set(fromTime: 10000) - try subscription?.addSymbols(["ETH/USD:GDAX", "IBM"]) + try subscription?.set(fromTime: 1717045200) + try subscription?.addSymbols(["AAPL{=8h}"]) wait(for: [receivedEventsExpectation], timeout: 3.0) try endpoint.closeAndAwaitTermination() } diff --git a/Package.swift b/Package.swift index c6c98f7c5..61f438fbd 100644 --- a/Package.swift +++ b/Package.swift @@ -3,9 +3,9 @@ import PackageDescription -let version = "1.0.1_build" +let version = "1.1.3_build" let moduleName = "DXFeedFramework" -let checksum = "68a73daad279e200be8b54021725cceae5ad044359531c1130356d68bebdd3c1" +let checksum = "0529710169a58beb75042eb53f715ce529d88d01d05c03d57dde61fcc87bf1f5" let package = Package( name: moduleName, diff --git a/README.md b/README.md index d8c44da11..fb9e37182 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,11 @@ is a simple demonstration of how to get live updates for Instrument Profiles - [x] [ScheduleSample](https://github.com/dxFeed/dxfeed-graal-swift-api/blob/swift/Samples/Playgrounds/ScheduleSample.playground/Contents.swift) is a simple demonstration of how to get various scheduling information for instruments - [x] [FetchDailyCandles](https://github.com/dxFeed/dxfeed-graal-swift-api/blob/swift/Samples/Playgrounds/FetchDailyCandles.playground/Contents.swift) is a simple demonstration of how to fetch last N-days of candles for a specified symbol +- [x] [DxFeedReconnectSample](https://github.com/dxFeed/dxfeed-graal-swift-api/blob/swift/Samples/Playgrounds/DxFeedReconnectSample.playground/Contents.swift) + is a simple demonstration of how to connect to an endpoint, subscribe to market data events, handle reconnections + and re-subscribing. +- [x] [SimpleAuthSample](https://github.com/dxFeed/dxfeed-graal-swift-api/blob/swift/Samples/Playgrounds/SimpleAuthSample.playground/Contents.swift) + is a simple demonstration of how to connect to endpoint requires authentication token, subscribe to market data events, and handle periodic token updates. ## Current State diff --git a/Samples/Playgrounds/DxFeedReconnectSample.playground/Contents.swift b/Samples/Playgrounds/DxFeedReconnectSample.playground/Contents.swift new file mode 100644 index 000000000..b31897c7b --- /dev/null +++ b/Samples/Playgrounds/DxFeedReconnectSample.playground/Contents.swift @@ -0,0 +1,98 @@ +import Cocoa +import PlaygroundSupport +import DXFeedFramework + +// Empty Event Listener with handler +class Listener: DXEventListener, Hashable { + + static func == (lhs: Listener, rhs: Listener) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine("\(self):\(stringReference(self))") + } + var callback: ([MarketEvent]) -> Void = { _ in } + + func receiveEvents(_ events: [MarketEvent]) { + self.callback(events) + } + + init(overrides: (Listener) -> Listener) { + _ = overrides(self) + } +} + +// Empty Endpoint Listener with handler +class EndpoointStateListener: DXEndpointListener, Hashable { + func endpointDidChangeState(old: DXFeedFramework.DXEndpointState, new: DXFeedFramework.DXEndpointState) { + callback(old, new) + } + + static func == (lhs: EndpoointStateListener, rhs: EndpoointStateListener) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine("\(self):\(stringReference(self))") + } + var callback: (DXEndpointState, DXEndpointState) -> Void = { _, _ in } + + init(overrides: (EndpoointStateListener) -> EndpoointStateListener) { + _ = overrides(self) + } +} + +// Demonstrates how to connect to an endpoint, subscribe to market data events, +// handle reconnections and re-subscribing. +let address = "demo.dxfeed.com:7300" // The address of the DxFeed endpoint. +let symbol = "ETH/USD:GDAX" // The symbol for which we want to receive quotes. + +// Create new endpoint and add a listener for state changes. +let endpoint = try DXEndpoint.getInstance() +let stateListener = EndpoointStateListener { listener in + listener.callback = { old, new in + print("Connection state changed: \(old) -> \(new)") + } + return listener +} +endpoint.add(listener: stateListener) + +// Connect to the endpoint using the specified address. +try endpoint.connect(address) + +// Create a subscription for Quote events. +let subscriptionQuote = try endpoint.getFeed()?.createSubscription(Quote.self) +// Listener must be attached before symbols are added. +let listener = Listener { listener in + listener.callback = { events in + events.forEach { event in + print(event.toString()) + } + } + return listener +} +try subscriptionQuote?.add(listener: listener) + +// Add the specified symbol to the subscription. +try subscriptionQuote?.addSymbols(symbol) + +// Wait for five seconds to allow some quotes to be received. +Thread.sleep(forTimeInterval: 5) + +// Disconnect from the endpoint. +try endpoint.disconnect() + +// Wait for another five seconds to ensure quotes stop coming in. +Thread.sleep(forTimeInterval: 5) + +// Reconnect to the endpoint. +// The subscription is automatically re-subscribed, and quotes start coming into the listener again. +// Another address can also be passed on. +try endpoint.connect(address) + +// infinity execution +PlaygroundPage.current.needsIndefiniteExecution = true + +// to finish execution run this line +PlaygroundPage.current.finishExecution() diff --git a/Samples/Playgrounds/DxFeedReconnectSample.playground/contents.xcplayground b/Samples/Playgrounds/DxFeedReconnectSample.playground/contents.xcplayground new file mode 100644 index 000000000..1c968e7d1 --- /dev/null +++ b/Samples/Playgrounds/DxFeedReconnectSample.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Samples/Playgrounds/FetchDailyCandles.playground/Contents.swift b/Samples/Playgrounds/FetchDailyCandles.playground/Contents.swift index f4daf2992..6bf542bc1 100644 --- a/Samples/Playgrounds/FetchDailyCandles.playground/Contents.swift +++ b/Samples/Playgrounds/FetchDailyCandles.playground/Contents.swift @@ -5,10 +5,10 @@ import DXFeedFramework // await couldn't use directly in Playground(this is accompanied by errors like: execution stopped with unexpected state) Task { let baseSymbol = CandleSymbol.valueOf("AAPL", [CandlePeriod.day]) - var toTime: Long = Long(Date.now.timeIntervalSince1970 * 1000) - let fromFime: Long = Long(Calendar.current.date(byAdding: .day, + var toTime: Long = Date.now.millisecondsSince1970 + let fromFime: Long = Calendar.current.date(byAdding: .day, value: -20, - to: Date())!.timeIntervalSince1970 * 1000) + to: Date())!.millisecondsSince1970 let endpoint = try DXEndpoint.getInstance().connect("demo.dxfeed.com:7300") let feed = endpoint.getFeed() guard let task = feed?.getTimeSeries(type: Candle.self, diff --git a/Samples/Playgrounds/OnDemandSample.playground/Contents.swift b/Samples/Playgrounds/OnDemandSample.playground/Contents.swift index 8f99be6e5..befeec178 100644 --- a/Samples/Playgrounds/OnDemandSample.playground/Contents.swift +++ b/Samples/Playgrounds/OnDemandSample.playground/Contents.swift @@ -61,12 +61,12 @@ do { //// replaying events until end time reached while (onDemand.getTime ?? Date.init(timeIntervalSince1970: 0)) < toDate { - if let time: Long = onDemand.getTime?.millisecondsSince1970() { + if let time: Long = onDemand.getTime?.millisecondsSince1970 { let timeStr = (try? defaultTimeFormat.format(value: time)) ?? "empty string" let connectedState = (try? onDemand.getEndpoint()?.getState()) ?? .notConnected print("Current state is \(connectedState), on-demand time is \(timeStr)") } - Thread.sleep(forTimeInterval: 1000) + Thread.sleep(forTimeInterval: 1) } // close endpoint completely to release resources try onDemand.getEndpoint()?.closeAndAwaitTermination() diff --git a/Samples/Playgrounds/SimpleAuthSample.playground/Contents.swift b/Samples/Playgrounds/SimpleAuthSample.playground/Contents.swift new file mode 100644 index 000000000..7323f2a64 --- /dev/null +++ b/Samples/Playgrounds/SimpleAuthSample.playground/Contents.swift @@ -0,0 +1,98 @@ +import Cocoa +import PlaygroundSupport +import DXFeedFramework + +// Empty Event Listener with handler +class Listener: DXEventListener, Hashable { + + static func == (lhs: Listener, rhs: Listener) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine("\(self):\(stringReference(self))") + } + var callback: ([MarketEvent]) -> Void = { _ in } + + func receiveEvents(_ events: [MarketEvent]) { + self.callback(events) + } + + init(overrides: (Listener) -> Listener) { + _ = overrides(self) + } +} + +// Empty Endpoint Listener with handler +class EndpoointStateListener: DXEndpointListener, Hashable { + func endpointDidChangeState(old: DXFeedFramework.DXEndpointState, new: DXFeedFramework.DXEndpointState) { + callback(old, new) + } + + static func == (lhs: EndpoointStateListener, rhs: EndpoointStateListener) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine("\(self):\(stringReference(self))") + } + var callback: (DXEndpointState, DXEndpointState) -> Void = { _, _ in } + + init(overrides: (EndpoointStateListener) -> EndpoointStateListener) { + _ = overrides(self) + } +} + +let address = "demo.dxfeed.com:7300" + +func updateTokenAndReconnect() { + try? DXEndpoint.getInstance().connect("\(address)[login=entitle:\(generateToken())]") +} + +func generateToken() -> String { + let length = Int.random(in: 4...10) + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return String((0.. \(new)") + } + return listener +} +endpoint.add(listener: stateListener) + +// Set up a timer to periodically update the token and reconnect every 10 seconds. +// The first connection will be made immediately. +// After reconnection, all existing subscriptions will be re-subscribed automatically. +Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in + updateTokenAndReconnect() +}.fire() + +// Create a subscription for Quote events. +let subscriptionQuote = try endpoint.getFeed()?.createSubscription(Quote.self) +// Listener must be attached before symbols are added. +let listener = Listener { listener in + listener.callback = { events in + // Event listener that prints each received event. + events.forEach { event in + print(event.toString()) + } + } + return listener +} +try subscriptionQuote?.add(listener: listener) + +// Add the specified symbol to the subscription. +try subscriptionQuote?.addSymbols("ETH/USD:GDAX") + +// Keep the application running indefinitely. +PlaygroundPage.current.needsIndefiniteExecution = true + +// to finish execution run this line +PlaygroundPage.current.finishExecution() diff --git a/Samples/Playgrounds/SimpleAuthSample.playground/contents.xcplayground b/Samples/Playgrounds/SimpleAuthSample.playground/contents.xcplayground new file mode 100644 index 000000000..1c968e7d1 --- /dev/null +++ b/Samples/Playgrounds/SimpleAuthSample.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file