Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 8b848d7

Browse files
authored
Refactor app startup code - extracting services (#3859)
Task/Issue URL: https://app.asana.com/0/0/1208832732122404/f **Description**: - Extract service-related code from the app’s launching logic to improve maintainability and clarity. - Establish clear patterns for synchronizing crucial services, ensuring consistent behavior during app lifecycle events. - Design an intuitive API to simplify usage for developers interacting with the app lifecycle code.
1 parent e07d032 commit 8b848d7

File tree

62 files changed

+2657
-2016
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2657
-2016
lines changed

Core/PixelEvent.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,6 @@ extension Pixel {
962962
// MARK: Launch time
963963
case appDidFinishLaunchingTime(time: BucketAggregation)
964964
case appDidShowUITime(time: BucketAggregation)
965-
case appDidBecomeActiveTime(time: BucketAggregation)
966965

967966
// MARK: AI Chat
968967
case aiChatNoRemoteSettingsFound(settings: String)
@@ -1951,7 +1950,6 @@ extension Pixel.Event {
19511950
// MARK: Launch time
19521951
case .appDidFinishLaunchingTime(let time): return "m_debug_app-did-finish-launching-time-\(time)"
19531952
case .appDidShowUITime(let time): return "m_debug_app-did-show-ui-time-\(time)"
1954-
case .appDidBecomeActiveTime(let time): return "m_debug_app-did-become-active-time-\(time)"
19551953

19561954
// MARK: AI Chat
19571955
case .aiChatNoRemoteSettingsFound(let settings):
@@ -1963,7 +1961,7 @@ extension Pixel.Event {
19631961
case .openAIChatFromWidgetLockScreenComplication: return "m_aichat-widget-lock-screen-complication"
19641962

19651963
// MARK: Lifecycle
1966-
case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state-3"
1964+
case .appDidTransitionToUnexpectedState: return "m_debug_app-did-transition-to-unexpected-state-4"
19671965

19681966
case .debugBreakageExperiment: return "m_debug_breakage_experiment_u"
19691967

Core/StatisticsLoader.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ public class StatisticsLoader {
5454
}
5555

5656
public func load(completion: @escaping Completion = {}) {
57+
let completion = {
58+
self.refreshAppRetentionAtb()
59+
completion()
60+
}
5761
if statisticsStore.hasInstallStatistics {
5862
completion()
5963
return

Core/UserDefaultsPropertyWrapper.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public struct UserDefaultsWrapper<T> {
5757
case daxLastShownContextualOnboardingDialogType = "com.duckduckgo.ios.daxLastShownContextualOnboardingDialogType"
5858

5959
case notFoundCache = "com.duckduckgo.ios.favicons.notFoundCache"
60-
case faviconSizeNeedsMigration = "com.duckduckgo.ios.favicons.sizeNeedsMigration"
6160
case faviconTabsCacheNeedsCleanup = "com.duckduckgo.ios.favicons.tabsCacheNeedsCleanup"
6261

6362
case legacyCovidInfo = "com.duckduckgo.ios.home.covidInfo"

DuckDuckGo-iOS.xcodeproj/project.pbxproj

Lines changed: 128 additions & 35 deletions
Large diffs are not rendered by default.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// ATBAndVariantConfiguration.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2025 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
import Core
22+
import BrowserServicesKit
23+
24+
final class ATBAndVariantConfiguration {
25+
26+
lazy var variantManager = DefaultVariantManager()
27+
28+
func configure(onVariantAssigned: () -> Void) {
29+
cleanUpATBAndAssignVariant(onVariantAssigned: onVariantAssigned)
30+
}
31+
32+
private func cleanUpATBAndAssignVariant(onVariantAssigned: () -> Void) {
33+
AtbAndVariantCleanup.cleanup()
34+
variantManager.assignVariantIfNeeded { _ in
35+
onVariantAssigned()
36+
}
37+
}
38+
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// ContentBlockingConfiguration.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2025 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
import Core
22+
23+
final class ContentBlockingConfiguration {
24+
25+
static func configure() {
26+
ContentBlocking.shared.onCriticalError = {
27+
NotificationCenter.default.post(name: .appDidEncounterUnrecoverableState, object: nil)
28+
}
29+
// Explicitly prepare ContentBlockingUpdating instance before Tabs are created
30+
_ = ContentBlockingUpdating.shared
31+
}
32+
33+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// CrashHandlersConfiguration.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2025 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
import Core
22+
import Crashes
23+
24+
final class CrashHandlersConfiguration {
25+
26+
@UserDefaultsWrapper(key: .didCrashDuringCrashHandlersSetUp, defaultValue: false)
27+
static var didCrashDuringCrashHandlersSetUp: Bool
28+
29+
static func setupCrashHandlers() {
30+
if !didCrashDuringCrashHandlersSetUp {
31+
didCrashDuringCrashHandlersSetUp = true
32+
CrashLogMessageExtractor.setUp(swapCxaThrow: false)
33+
didCrashDuringCrashHandlersSetUp = false
34+
}
35+
}
36+
37+
static func handleCrashDuringCrashHandlersSetup() {
38+
if didCrashDuringCrashHandlersSetUp {
39+
Pixel.fire(pixel: .crashOnCrashHandlersSetUp)
40+
didCrashDuringCrashHandlersSetUp = false
41+
}
42+
}
43+
44+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// HistoryManagerConfiguration.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2025 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
22+
final class HistoryManagerConfiguration {
23+
24+
func onVariantAssigned() {
25+
// New users don't see the message
26+
HistoryMessageManager().dismiss()
27+
}
28+
29+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// KeyboardConfiguration.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2025 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import UIKit
21+
22+
struct KeyboardConfiguration {
23+
24+
static func configure() {
25+
#if targetEnvironment(simulator)
26+
if ProcessInfo.processInfo.environment["UITESTING"] == "true" {
27+
// Disable hardware keyboards.
28+
let setHardwareLayout = NSSelectorFromString("setHardwareLayout:")
29+
UITextInputMode.activeInputModes
30+
// Filter `UIKeyboardInputMode`s.
31+
.filter({ $0.responds(to: setHardwareLayout) })
32+
.forEach { $0.perform(setHardwareLayout, with: nil) }
33+
}
34+
#endif
35+
}
36+
37+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// OnboardingConfiguration.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2025 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
import Core
22+
23+
final class OnboardingConfiguration {
24+
25+
lazy var daxDialogs = DaxDialogs.shared
26+
27+
func migrate() {
28+
// Hide Dax Dialogs if users already completed old onboarding.
29+
DaxDialogsOnboardingMigrator().migrateFromOldToNewOboarding()
30+
}
31+
32+
// assign it here, because "did become active" is already too late and "viewWillAppear"
33+
// has already been called on the HomeViewController so won't show the home row CTA
34+
func onVariantAssigned() {
35+
let launchOptionsHandler = LaunchOptionsHandler()
36+
37+
// MARK: perform first time launch logic here
38+
// If it's running UI Tests check if the onboarding should be in a completed state.
39+
if launchOptionsHandler.isUITesting && launchOptionsHandler.isOnboardingCompleted {
40+
daxDialogs.dismiss()
41+
} else {
42+
daxDialogs.primeForUse()
43+
}
44+
}
45+
46+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// PersistentStoresConfiguration.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2025 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import Foundation
21+
import Core
22+
import Persistence
23+
24+
public extension NSNotification.Name {
25+
26+
static let databaseDidEncounterInsufficientDiskSpace = Notification.Name("com.duckduckgo.database.insufficient.disk.space")
27+
28+
}
29+
30+
final class PersistentStoresConfiguration {
31+
32+
let database = Database.shared
33+
let bookmarksDatabase = BookmarksDatabase.make()
34+
private let application: UIApplication
35+
private let notificationCenter: NotificationCenter
36+
37+
init(application: UIApplication = .shared,
38+
notificationCenter: NotificationCenter = .default) {
39+
self.application = application
40+
self.notificationCenter = notificationCenter
41+
}
42+
43+
func configure() {
44+
clearTemporaryDirectory()
45+
loadAndMigrateDatabase()
46+
loadAndMigrateBookmarksDatabase()
47+
}
48+
49+
private func clearTemporaryDirectory() {
50+
let tmp = FileManager.default.temporaryDirectory
51+
do {
52+
try FileManager.default.removeItem(at: tmp)
53+
} catch {
54+
Logger.general.error("Failed to delete tmp dir")
55+
}
56+
}
57+
58+
private func loadAndMigrateDatabase() {
59+
database.loadStore { [application] context, error in
60+
guard let context = context else {
61+
let parameters = [PixelParameters.applicationState: "\(application.applicationState.rawValue)",
62+
PixelParameters.dataAvailability: "\(application.isProtectedDataAvailable)"]
63+
switch error {
64+
case .none:
65+
fatalError("Could not create database stack: Unknown Error")
66+
case .some(CoreDataDatabase.Error.containerLocationCouldNotBePrepared(let underlyingError)):
67+
Pixel.fire(pixel: .dbContainerInitializationError,
68+
error: underlyingError,
69+
withAdditionalParameters: parameters)
70+
Thread.sleep(forTimeInterval: 1)
71+
fatalError("Could not create database stack: \(underlyingError.localizedDescription)")
72+
case .some(let error):
73+
Pixel.fire(pixel: .dbInitializationError,
74+
error: error,
75+
withAdditionalParameters: parameters)
76+
if error.isDiskFull {
77+
NotificationCenter.default.post(name: .databaseDidEncounterInsufficientDiskSpace, object: nil)
78+
return
79+
} else {
80+
Thread.sleep(forTimeInterval: 1)
81+
fatalError("Could not create database stack: \(error.localizedDescription)")
82+
}
83+
}
84+
}
85+
DatabaseMigration.migrate(to: context)
86+
}
87+
}
88+
89+
private func loadAndMigrateBookmarksDatabase() {
90+
switch BookmarksDatabaseSetup().loadStoreAndMigrate(bookmarksDatabase: bookmarksDatabase) {
91+
case .success:
92+
break
93+
case .failure(let error):
94+
Pixel.fire(pixel: .bookmarksCouldNotLoadDatabase,
95+
error: error)
96+
if error.isDiskFull {
97+
NotificationCenter.default.post(name: .databaseDidEncounterInsufficientDiskSpace, object: nil)
98+
} else {
99+
Thread.sleep(forTimeInterval: 1)
100+
fatalError("Could not create database stack: \(error.localizedDescription)")
101+
}
102+
}
103+
}
104+
105+
}
106+
107+
extension Error {
108+
109+
var isDiskFull: Bool {
110+
let nsError = self as NSError
111+
if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError, underlyingError.code == 13 {
112+
return true
113+
} else if nsError.userInfo["NSSQLiteErrorDomain"] as? Int == 13 {
114+
return true
115+
}
116+
return false
117+
}
118+
119+
}

0 commit comments

Comments
 (0)