diff --git a/Core/Pixel.swift b/Core/Pixel.swift index c9d4e38470..04f48ee03f 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -161,6 +161,9 @@ public struct PixelParameters { public static let retriedPixel = "retriedPixel" public static let time = "time" + + public static let appState = "state" + public static let appEvent = "event" } public struct PixelValues { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 5fc44638c9..3ae8222431 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -895,6 +895,9 @@ extension Pixel { case appDidShowUITime(time: BucketAggregation) case appDidBecomeActiveTime(time: BucketAggregation) + // MARK: Lifecycle + case appDidTransitionToUnexpectedState + } } @@ -1784,6 +1787,9 @@ extension Pixel.Event { case .appDidShowUITime(let time): return "m_debug_app-did-show-ui-time-\(time)" case .appDidBecomeActiveTime(let time): return "m_debug_app-did-become-active-time-\(time)" + // MARK: Lifecycle + case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state" + } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bf273e30fe..18cea15fdb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -980,6 +980,13 @@ CB9B873C278C8FEA001F4906 /* WidgetEducationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9B873B278C8FEA001F4906 /* WidgetEducationView.swift */; }; CB9B873E278C93C2001F4906 /* HomeMessage.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CB9B873D278C93C2001F4906 /* HomeMessage.xcassets */; }; CBAA195A27BFE15600A4BD49 /* NSManagedObjectContextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */; }; + CBAD0EF92CFE1D3B006267B8 /* Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EF82CFE1D35006267B8 /* Init.swift */; }; + CBAD0EFB2CFE1D41006267B8 /* Launched.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */; }; + CBAD0EFD2CFE1D4B006267B8 /* Active.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFC2CFE1D48006267B8 /* Active.swift */; }; + CBAD0EFF2CFE1D50006267B8 /* Inactive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */; }; + CBAD0F012CFE1D57006267B8 /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F002CFE1D54006267B8 /* Background.swift */; }; + CBAD0F062CFE2711006267B8 /* AppStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */; }; + CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */; }; CBC83E3429B631780008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3329B631780008E19C /* Configuration */; }; CBC88EE12C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */; }; CBC88EE52C8097B500F0F8C5 /* URLCredentialCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC88EE42C8097B500F0F8C5 /* URLCredentialCreator.swift */; }; @@ -2811,6 +2818,13 @@ CBA1DE942AF6D579007C9457 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = ""; }; CBAA195927BFE15600A4BD49 /* NSManagedObjectContextExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextExtension.swift; sourceTree = ""; }; CBAA195B27C3982A00A4BD49 /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = ""; }; + CBAD0EF82CFE1D35006267B8 /* Init.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Init.swift; sourceTree = ""; }; + CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Launched.swift; sourceTree = ""; }; + CBAD0EFC2CFE1D48006267B8 /* Active.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Active.swift; sourceTree = ""; }; + CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inactive.swift; sourceTree = ""; }; + CBAD0F002CFE1D54006267B8 /* Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Background.swift; sourceTree = ""; }; + CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateMachine.swift; sourceTree = ""; }; + CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateTransitions.swift; sourceTree = ""; }; CBB6B2542AF6D543006B777C /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = ""; }; CBC7AB542AF6D583008CB798 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; CBC88EE02C7F834300F0F8C5 /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; @@ -5434,6 +5448,28 @@ name = Resources; sourceTree = ""; }; + CBAD0EF72CFE1D14006267B8 /* AppStates */ = { + isa = PBXGroup; + children = ( + CBAD0EF82CFE1D35006267B8 /* Init.swift */, + CBAD0EFA2CFE1D3F006267B8 /* Launched.swift */, + CBAD0EFC2CFE1D48006267B8 /* Active.swift */, + CBAD0EFE2CFE1D4E006267B8 /* Inactive.swift */, + CBAD0F002CFE1D54006267B8 /* Background.swift */, + ); + path = AppStates; + sourceTree = ""; + }; + CBAD0F042CFE1DA2006267B8 /* AppLifecycle */ = { + isa = PBXGroup; + children = ( + CBAD0F052CFE270D006267B8 /* AppStateMachine.swift */, + CBAD0F072CFE27D5006267B8 /* AppStateTransitions.swift */, + CBAD0EF72CFE1D14006267B8 /* AppStates */, + ); + path = AppLifecycle; + sourceTree = ""; + }; D62EC3B72C24695800FC9D04 /* DuckPlayer */ = { isa = PBXGroup; children = ( @@ -6317,6 +6353,7 @@ F1C5ECF31E37812900C599A4 /* Application */ = { isa = PBXGroup; children = ( + CBAD0F042CFE1DA2006267B8 /* AppLifecycle */, 83BE9BC2215D69C1009844D9 /* AppConfigurationFetch.swift */, CB24F70E29A3EB15006DCC58 /* AppConfigurationURLProvider.swift */, 84E341951E2F7EFB00BDBA6F /* AppDelegate.swift */, @@ -7499,6 +7536,7 @@ BDE91CDE2C62B90F0005CB74 /* UnifiedFeedbackRootView.swift in Sources */, D65625A12C232F5E006EF297 /* SettingsDuckPlayerView.swift in Sources */, D6FEB8B52B74994000C3615F /* HeadlessWebViewCoordinator.swift in Sources */, + CBAD0EF92CFE1D3B006267B8 /* Init.swift in Sources */, 9F96F73F2C914C57009E45D5 /* OnboardingGradient.swift in Sources */, 6FE1273D2C204C2500EB5724 /* FavoritesView.swift in Sources */, 8528AE81212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld in Sources */, @@ -7536,6 +7574,7 @@ 8590CB69268A4E190089F6BF /* DebugEtagStorage.swift in Sources */, C1CDA3162AFB9C7F006D1476 /* AutofillNeverPromptWebsitesManager.swift in Sources */, D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */, + CBAD0F012CFE1D57006267B8 /* Background.swift in Sources */, F1CA3C371F045878005FADB3 /* PrivacyStore.swift in Sources */, 31DE43C42C2C60E800F8C51F /* DuckPlayerModalPresenter.swift in Sources */, 37FCAAC029930E26000E420A /* FailedAssertionView.swift in Sources */, @@ -7602,6 +7641,7 @@ BDFF031D2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift in Sources */, F1BE54581E69DE1000FCF649 /* TutorialSettings.swift in Sources */, 1EE52ABB28FB1D6300B750C1 /* UIImageExtension.swift in Sources */, + CBAD0EFF2CFE1D50006267B8 /* Inactive.swift in Sources */, 858650D12469BCDE00C36F8A /* DaxDialogs.swift in Sources */, 9F5E5AB02C3E4C6000165F54 /* ContextualOnboardingPresenter.swift in Sources */, 310D091B2799F54900DC0060 /* DownloadManager.swift in Sources */, @@ -7642,6 +7682,7 @@ 859DB8132CE6263C001F7210 /* TextZoomStorage.swift in Sources */, D65625952C22D382006EF297 /* TabViewController.swift in Sources */, 8C4838B5221C8F7F008A6739 /* GestureToolbarButton.swift in Sources */, + CBAD0EFD2CFE1D4B006267B8 /* Active.swift in Sources */, 310ECFDD282A8BB0005029B3 /* EnableAutofillSettingsTableViewCell.swift in Sources */, 859DB8172CE6263C001F7210 /* TextZoomLevel.swift in Sources */, BDE91CD62C6294020005CB74 /* FeedbackCategoryProviding.swift in Sources */, @@ -7737,6 +7778,7 @@ D6F93E3E2B50A8A0004C268D /* SubscriptionSettingsView.swift in Sources */, 1D200C9B2BA31A6A00108701 /* AboutView.swift in Sources */, 851B12CC22369931004781BC /* AtbAndVariantCleanup.swift in Sources */, + CBAD0F062CFE2711006267B8 /* AppStateMachine.swift in Sources */, D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */, 85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */, 3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */, @@ -7929,6 +7971,7 @@ 311BD1B12836C0CA00AEF6C1 /* AutofillLoginListAuthenticator.swift in Sources */, B652DF13287C373A00C12A9C /* ScriptSourceProviding.swift in Sources */, 854A012B2A54412600FCC628 /* ActivityViewController.swift in Sources */, + CBAD0F082CFE27E2006267B8 /* AppStateTransitions.swift in Sources */, F1CA3C391F045885005FADB3 /* PrivacyUserDefaults.swift in Sources */, 6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */, 6FE1274B2C20943500EB5724 /* ShortcutItemView.swift in Sources */, @@ -8015,6 +8058,7 @@ 983D71B12A286E810072E26D /* SyncDebugViewController.swift in Sources */, 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */, F103073B1E7C91330059FEC7 /* BookmarksDataSource.swift in Sources */, + CBAD0EFB2CFE1D41006267B8 /* Launched.swift in Sources */, 6FD3F80F2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift in Sources */, 85864FBC24D31EF300E756FF /* SuggestionTrayViewController.swift in Sources */, D64648AF2B5993890033090B /* SubscriptionEmailViewModel.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 840a3ceb31..1eead15b30 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -117,6 +117,8 @@ import os.log private var didFinishLaunchingStartTime: CFAbsoluteTime? + private let appStateMachine = AppStateMachine() + override init() { super.init() @@ -131,6 +133,7 @@ import os.log // swiftlint:disable:next cyclomatic_complexity func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + appStateMachine.handle(.launching(application, launchOptions: launchOptions)) didFinishLaunchingStartTime = CFAbsoluteTimeGetCurrent() defer { if let didFinishLaunchingStartTime { @@ -587,6 +590,8 @@ import os.log func applicationDidBecomeActive(_ application: UIApplication) { guard !testing else { return } + appStateMachine.handle(.activating(application)) + defer { if let didFinishLaunchingStartTime { let launchTime = CFAbsoluteTimeGetCurrent() - didFinishLaunchingStartTime @@ -690,6 +695,7 @@ import os.log } func applicationWillResignActive(_ application: UIApplication) { + appStateMachine.handle(.suspending(application)) Task { @MainActor in await refreshShortcuts() await vpnWorkaround.removeRedditSessionWorkaround() @@ -782,6 +788,7 @@ import os.log } func applicationDidEnterBackground(_ application: UIApplication) { + appStateMachine.handle(.backgrounding(application)) displayBlankSnapshotWindow() autoClear?.startClearingTimer() lastBackgroundDate = Date() @@ -827,6 +834,7 @@ import os.log func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { Logger.sync.debug("App launched with url \(url.absoluteString)") + appStateMachine.handle(.openURL(url)) // If showing the onboarding intro ignore deeplinks guard mainViewController?.needsToShowOnboardingIntro() == false else { diff --git a/DuckDuckGo/AppLifecycle/AppStateMachine.swift b/DuckDuckGo/AppLifecycle/AppStateMachine.swift new file mode 100644 index 0000000000..53fb3b1bf0 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStateMachine.swift @@ -0,0 +1,53 @@ +// +// AppStateMachine.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +enum AppEvent { + + case launching(UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) + case activating(UIApplication) + case backgrounding(UIApplication) + case suspending(UIApplication) + + case openURL(URL) + +} + +protocol AppState { + + func apply(event: AppEvent) -> any AppState + +} + +protocol AppEventHandler { + + func handle(_ event: AppEvent) + +} + +final class AppStateMachine: AppEventHandler { + + private(set) var currentState: any AppState = Init() + + func handle(_ event: AppEvent) { + currentState = currentState.apply(event: event) + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStateTransitions.swift b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift new file mode 100644 index 0000000000..8e30596fee --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStateTransitions.swift @@ -0,0 +1,118 @@ +// +// AppStateTransitions.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import os.log +import Core + +extension Init { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .launching(let application, let launchOptions): + return Launched(application: application, launchOptions: launchOptions) + default: + return handleUnexpectedEvent(event) + } + } + +} + +extension Launched { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .activating(let application): + return Active(application: application) + case .openURL: + return self + case .launching, .suspending, .backgrounding: + return handleUnexpectedEvent(event) + } + } + +} + +extension Active { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .suspending(let application): + return Inactive(application: application) + case .launching, .activating, .backgrounding, .openURL: + return handleUnexpectedEvent(event) + } + } + +} + +extension Inactive { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .backgrounding(let application): + return Background(application: application) + case .activating(let application): + return Active(application: application) + case .launching, .suspending, .openURL: + return handleUnexpectedEvent(event) + } + } + +} + +extension Background { + + func apply(event: AppEvent) -> any AppState { + switch event { + case .activating(let application): + return Active(application: application) + case .openURL: + return self + case .launching, .suspending, .backgrounding: + return handleUnexpectedEvent(event) + } + } + +} + +extension AppEvent { + + var rawValue: String { + switch self { + case .launching: return "launching" + case .activating: return "activating" + case .backgrounding: return "backgrounding" + case .suspending: return "suspending" + case .openURL: return "openURL" + } + } + +} + +extension AppState { + + func handleUnexpectedEvent(_ event: AppEvent) -> Self { + Logger.lifecycle.error("Invalid transition (\(event.rawValue)) for state (\(type(of: self)))") + DailyPixel.fireDailyAndCount(pixel: .appDidTransitionToUnexpectedState, + withAdditionalParameters: [PixelParameters.appState: String(describing: type(of: self)), + PixelParameters.appEvent: event.rawValue]) + return self + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Active.swift b/DuckDuckGo/AppLifecycle/AppStates/Active.swift new file mode 100644 index 0000000000..df99c36d50 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Active.swift @@ -0,0 +1,28 @@ +// +// Active.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Active: AppState { + + init(application: UIApplication) { + + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Background.swift b/DuckDuckGo/AppLifecycle/AppStates/Background.swift new file mode 100644 index 0000000000..9332e41b6b --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Background.swift @@ -0,0 +1,28 @@ +// +// Background.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Background: AppState { + + init(application: UIApplication) { + + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift new file mode 100644 index 0000000000..888ef34e09 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Inactive.swift @@ -0,0 +1,28 @@ +// +// Inactive.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Inactive: AppState { + + init(application: UIApplication) { + + } + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Init.swift b/DuckDuckGo/AppLifecycle/AppStates/Init.swift new file mode 100644 index 0000000000..d68d714ea5 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Init.swift @@ -0,0 +1,22 @@ +// +// Init.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +struct Init: AppState { + +} diff --git a/DuckDuckGo/AppLifecycle/AppStates/Launched.swift b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift new file mode 100644 index 0000000000..674bcf0b91 --- /dev/null +++ b/DuckDuckGo/AppLifecycle/AppStates/Launched.swift @@ -0,0 +1,28 @@ +// +// Launched.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +struct Launched: AppState { + + init(application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { + + } + +}