fix: unavaialbe workspace in live activity

This commit is contained in:
Kaede Akino committed 2026-10-17 15:58:03 +08:00
1 parent 61b1092b08
commit cbda3f0712
9 files changed
+253 -224

No files matched your search

+7 -171
View File
@@ -18345,47 +18345,6 @@
}
}
},
"Connections Preferred (Pro)" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbindungen bevorzugen (Pro)"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Privilégier les connexions (Pro)"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "接続を優先(Pro)"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "연결 우선 (Pro)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "优先连接(Pro)"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "優先連線(Pro)"
}
}
}
},
"Contacting the App Store…" : {
"extractionState" : "manual",
"localizations" : {
@@ -31604,43 +31563,43 @@
}
}
},
"Free includes up to 3 monitoring cards. Upgrade to Pro for more." : {
"Free includes up to 3 hosts. Upgrade to Pro to unlock this host." : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Die kostenlose Version enthält bis zu 3 Überwachungskarten. Mit Pro können Sie weitere hinzufügen."
"value" : "Die kostenlose Version unterstützt bis zu 3 Hosts. Führe ein Upgrade auf Pro durch, um diesen Host freizuschalten."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "La version gratuite inclut jusqu’à 3 cartes de surveillance. Passez à Pro pour en ajouter davantage."
"value" : "La version gratuite prend en charge jusqu’à 3 hôtes. Passez à Pro pour déverrouiller cet hôte."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "無料版では監視カードを3枚まで追加できます。さらに追加するにはProにアップグレードしてください。"
"value" : "無料版ではホストを3台まで利用できます。Proにアップグレードして、このホストのロックを解除してください。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "무료 버전에는 모니터링 카드가 최대 3개까지 포함됩니다. 더 추가하려면 Pro로 업그레이드하세요."
"value" : "무료 버전은 호스트를 최대 3대까지 지원합니다. Pro로 업그레이드하여 이 호스트의 잠금을 해제하세요."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "免费版最多可添加3张监控卡片,升级Pro可添加更多。"
"value" : "免费版最多支持3台主机。升级Pro以解锁此主机。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "免費版最多可新增3張監控卡片,升級Pro可新增更多。"
"value" : "免費版最多支援3台主機。升級Pro以解鎖此主機。"
}
}
}
@@ -40160,47 +40119,6 @@
}
}
},
"Max Connections (Pro)" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Max. Verbindungen (Pro)"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connexions max. (Pro)"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "最大接続数(Pro)"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "최대 연결 수 (Pro)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "最大连接数(Pro)"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "連線數上限(Pro)"
}
}
}
},
"Max Output" : {
"extractionState" : "manual",
"localizations" : {
@@ -65853,47 +65771,6 @@
}
}
},
"Smart (Pro)" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Intelligent (Pro)"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Intelligent (Pro)"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "スマート(Pro)"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "스마트 (Pro)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "智能(Pro)"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "智慧(Pro)"
}
}
}
},
"Smart uses channels for files up to 64 MB and prefers additional connections for larger files. Transfers with an unknown size use channels." : {
"extractionState" : "manual",
"localizations" : {
@@ -82359,47 +82236,6 @@
}
}
},
"Upgrade to Pro to unlock this host." : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Führe ein Upgrade auf Pro durch, um diesen Host freizuschalten."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Passez à Pro pour déverrouiller cet hôte."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "Proにアップグレードしてこのホストをロック解除。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pro로 업그레이드하여 이 호스트의 잠금을 해제하세요."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "升级到Pro以解锁此主机。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "升級至Pro以解鎖此主機。"
}
}
}
},
"Upload" : {
"extractionState" : "manual",
"localizations" : {
@@ -15,10 +15,10 @@ public struct SFTPSettingsFields: View {
}
public var body: some View {
Picker(AppStrings.SFTP.concurrentStrategy, selection: access.strategy(for: model)) {
Text(access.isAllowed ? AppStrings.SFTP.smart : AppStrings.Pro.smartStrategy)
Text(AppStrings.SFTP.smart)
.tag(SFTPConcurrencyStrategy.smart)
Text(AppStrings.SFTP.channelsOnly).tag(SFTPConcurrencyStrategy.channelsOnly)
Text(access.isAllowed ? AppStrings.SFTP.connectionsPreferred : AppStrings.Pro.connectionsPreferredStrategy)
Text(AppStrings.SFTP.connectionsPreferred)
.tag(SFTPConcurrencyStrategy.connectionsPreferred)
}
if access.strategy(for: model).wrappedValue == .smart {
@@ -2,10 +2,6 @@ import Foundation
extension AppStrings {
public nonisolated enum Pro {
public static var smartStrategy: LocalizedStringResource { "Smart (Pro)" }
public static var connectionsPreferredStrategy: LocalizedStringResource { "Connections Preferred (Pro)" }
public static var maxConnections: LocalizedStringResource { "Max Connections (Pro)" }
public static var contactingTheAppStoreProgress: LocalizedStringResource {
"Contacting the App Store…"
}
@@ -14,7 +10,9 @@ extension AppStrings {
"Termind Pro is not available from the App Store right now."
}
public static var upgradeToUnlockHost: LocalizedStringResource { "Upgrade to Pro to unlock this host." }
public static var upgradeToUnlockHost: LocalizedStringResource {
"Free includes up to 3 hosts. Upgrade to Pro to unlock this host."
}
public static var verifyingAccess: LocalizedStringResource { "Checking Pro access…" }
public static var accessVerificationFailed: LocalizedStringResource {
"Unable to verify Pro access. Please try again."
+1 -1
View File
@@ -507,7 +507,7 @@ final class MobileAppModel {
updateWorkspaceActivity()
}
private func updateWorkspaceActivity() {
func updateWorkspaceActivity() {
// A failed read is not an authoritative empty list.
guard hasLoadedWorkspaceSnapshot else { return }
liveActivities.update(workspaceActivitySessions)
+3
View File
@@ -448,6 +448,7 @@ final class MobileSceneModel {
return
}
if WorkspaceActivityAttributes.accepts(url) {
app.updateWorkspaceActivity()
isWorkspaceUnavailable = false
pendingWorkspaceID = nil
selectedDestination = .workspaces
@@ -463,6 +464,8 @@ final class MobileSceneModel {
}
return
}
// Even an expired link must reconcile system cards against the authoritative local list.
app.updateWorkspaceActivity()
isWorkspaceUnavailable = false
let id = sourceID
guard let owner = app.workspaceOwner(id) else {
+45 -28
View File
@@ -12,20 +12,14 @@ final class MobileLiveActivityController {
private let services: MobileLiveActivityServices
private var focusedWorkspaceID: UUID?
private var pendingStart = false
private var pendingState: WorkspaceActivityAttributes.ContentState?
init(enabled: Bool = true, services: MobileLiveActivityServices = .live) {
self.enabled = enabled
self.services = services
if enabled {
let restored = services.activities()
activity = restored.first { $0.state == .active || $0.state == .stale }
activity = services.activities().first { $0.state == .active || $0.state == .stale }
focusedWorkspaceID = activity?.focusedWorkspaceID
let retainedID = activity?.id
updates = Task {
for duplicate in restored where duplicate.id != retainedID {
await duplicate.end()
}
}
}
}
@@ -35,38 +29,61 @@ final class MobileLiveActivityController {
update(sessions)
}
/// All starts, updates and ends share a serial queue, including close-last/open-new races.
/// Serialize system calls, but coalesce queued changes so closed workspaces are never recreated.
func start(_ sessions: [WorkspaceActivityAttributes.Session]) { schedule(sessions, mayStart: true) }
func update(_ sessions: [WorkspaceActivityAttributes.Session]) { schedule(sessions, mayStart: false) }
private func schedule(_ sessions: [WorkspaceActivityAttributes.Session], mayStart: Bool) {
guard enabled else { return }
let state = WorkspaceActivityAttributes.ContentState.snapshot(
pendingState = WorkspaceActivityAttributes.ContentState.snapshot(
sessions, focusedID: focusedWorkspaceID)
let previous = updates
if sessions.isEmpty {
pendingStart = false
} else if mayStart {
pendingStart = true
}
guard updates == nil else { return }
updates = Task { [weak self] in
await previous?.value
guard let self else { return }
if mayStart { pendingStart = true }
guard state.workspaceCount > 0 else {
pendingStart = false
if let activity { await activity.end() }
activity = nil
return
while let state = pendingState {
pendingState = nil
await reconcile(state)
}
if let activity, activity.state == .active || activity.state == .stale {
pendingStart = false
updates = nil
}
}
private func reconcile(_ state: WorkspaceActivityAttributes.ContentState) async {
// ActivityKit can expose old cards after startup. The cached handle is not the full inventory.
var activities = services.activities()
if let activity, !activities.contains(where: { $0.id == activity.id }) { activities.append(activity) }
let usable = activities.filter { $0.state == .active || $0.state == .stale }
let retained =
state.workspaceCount > 0
? usable.first(where: { $0.id == activity?.id }) ?? usable.first : nil
activity = retained
if retained != nil { pendingStart = false }
for other in activities where other.id != retained?.id { await other.end() }
// An open/close may have arrived while dismissal was suspended. Publish only the newest list.
guard pendingState == nil, state.workspaceCount > 0 else { return }
if let activity {
if activity.state == .active || activity.state == .stale {
await activity.update(state)
return
}
// An explicit open may arrive before activation. Retain its intent until a foreground update.
// Once consumed, ordinary updates cannot resurrect a user-dismissed activity.
guard pendingStart, services.canStart() else { return }
pendingStart = false
do {
activity = try services.request(state)
error = nil
} catch { self.error = error.localizedDescription }
await activity.end()
self.activity = nil
}
// An explicit open may arrive before activation. Retain its intent until a foreground update.
// Once consumed, ordinary updates cannot resurrect a user-dismissed activity.
guard pendingState == nil, pendingStart, services.canStart() else { return }
pendingStart = false
do {
activity = try services.request(state)
error = nil
} catch {
self.error = error.localizedDescription
}
}
}
@@ -64,6 +64,57 @@ struct MobileLiveActivityRestorationTests {
#expect(await eventually { existing.ended })
}
@Test func refreshAdoptsLateSystemActivityAndDismissesDuplicates() async {
let backend = Backend()
let controller = MobileLiveActivityController(services: backend.services)
let current = session()
controller.update([current])
for _ in 0..<20 { await Task.yield() }
let late = TestActivity(state: .stale)
let duplicate = TestActivity()
let ended = TestActivity(state: .ended)
backend.existing = [late, duplicate, ended]
controller.update([current])
#expect(await eventually { late.updates.last?.sessions.map(\.id) == [current.id] })
#expect(duplicate.ended && ended.ended)
#expect(!late.ended)
#expect(backend.requests.isEmpty)
}
@Test func emptySnapshotDismissesEveryLateSystemActivity() async {
let backend = Backend()
let controller = MobileLiveActivityController(services: backend.services)
controller.update([])
for _ in 0..<20 { await Task.yield() }
let late = TestActivity()
let ended = TestActivity(state: .ended)
backend.existing = [late, ended]
controller.update([])
#expect(await eventually { late.ended && ended.ended })
#expect(backend.requests.isEmpty)
}
@Test func explicitOpenDismissesEndedCardBeforeReplacingIt() async {
let backend = Backend()
let existing = TestActivity()
backend.existing = [existing]
let controller = MobileLiveActivityController(services: backend.services)
controller.update([session()])
#expect(await eventually { existing.updates.count == 1 })
existing.state = .ended
let next = session()
controller.start([next])
#expect(await eventually { backend.requests.count == 1 })
#expect(existing.ended)
#expect(backend.requests.first?.sessions.map(\.id) == [next.id])
}
@Test func closeLastThenOpenWaitsForOldActivityToFinishEnding() async {
let backend = Backend()
let existing = TestActivity()
@@ -71,21 +122,143 @@ struct MobileLiveActivityRestorationTests {
backend.existing = [existing]
let controller = MobileLiveActivityController(services: backend.services)
controller.update([])
#expect(await eventually { existing.endContinuation != nil })
let next = session()
controller.start([next])
controller.focus(next.id, in: [next])
#expect(await eventually { existing.endContinuation != nil })
#expect(backend.requests.isEmpty)
existing.endContinuation?.resume()
existing.endContinuation = nil
#expect(await eventually { backend.created.first?.updates.count == 1 })
#expect(await eventually { backend.requests.count == 1 })
#expect(existing.ended)
#expect(backend.requests.count == 1)
#expect(backend.created.first?.updates.first?.focusedWorkspaceID == next.id)
#expect(backend.requests.first?.focusedWorkspaceID == next.id)
#expect(backend.created.first?.ended == false)
}
@Test func rapidOpenCloseWhileEndingOnlyPublishesTheLatestWorkspace() async {
let backend = Backend()
let existing = TestActivity()
existing.suspendsEnd = true
backend.existing = [existing]
let controller = MobileLiveActivityController(services: backend.services)
controller.update([])
#expect(await eventually { existing.endContinuation != nil })
for _ in 0..<5 {
controller.start([session()])
controller.update([])
}
let latest = session()
controller.start([latest])
existing.endContinuation?.resume()
existing.endContinuation = nil
#expect(await eventually { backend.requests.last?.sessions.map(\.id) == [latest.id] })
#expect(backend.requests.count == 1)
#expect(backend.created.filter { !$0.ended }.count == 1)
}
@Test func closeDuringDuplicateCleanupDoesNotPublishTheObsoleteOpen() async {
let backend = Backend()
let existing = TestActivity()
let duplicate = TestActivity()
duplicate.suspendsEnd = true
backend.existing = [existing, duplicate]
let controller = MobileLiveActivityController(services: backend.services)
controller.start([session()])
#expect(await eventually { duplicate.endContinuation != nil })
controller.update([])
duplicate.endContinuation?.resume()
duplicate.endContinuation = nil
#expect(await eventually { existing.ended && duplicate.ended })
#expect(existing.updates.isEmpty)
#expect(backend.requests.isEmpty)
}
@Test func dismissingRetainedCardDuringCleanupDoesNotReplayItsStartIntent() async {
let backend = Backend()
let existing = TestActivity()
let duplicate = TestActivity()
duplicate.suspendsEnd = true
backend.existing = [existing, duplicate]
let controller = MobileLiveActivityController(services: backend.services)
let current = session()
controller.start([current])
#expect(await eventually { duplicate.endContinuation != nil })
existing.state = .dismissed
controller.update([current])
duplicate.endContinuation?.resume()
duplicate.endContinuation = nil
#expect(await eventually { existing.ended && duplicate.ended })
#expect(existing.updates.isEmpty)
#expect(backend.requests.isEmpty)
}
@Test func staleLinkAfterRepeatedOpenCloseCleansLateCardWithoutRebuilding() async throws {
let backend = Backend()
var dependencies = MobileAppDependencies.testing()
dependencies.liveActivities = MobileLiveActivityController(services: backend.services)
let app = try MobileAppModel(dependencies: dependencies)
let host = TermindDomain.Host(name: "Server", host: "example.invalid")
app.library.upsert(host)
let scene = MobileSceneModel(app: app, sessionID: "window")
var closedID: UUID?
for index in 1...3 {
let id = try #require(scene.workspaceSession.open(hostID: host.id, kind: .ssh))
closedID = id
#expect(await eventually { backend.requests.count == index })
scene.closeAllWorkspaces()
#expect(await eventually { backend.created.last?.ended == true })
}
let id = try #require(closedID)
let late = TestActivity(focusedWorkspaceID: id)
late.contentState = .snapshot(
[.init(id: id, title: "Closed", kind: "ssh", connected: false)], focusedID: id)
backend.existing.append(late)
scene.open(WorkspaceActivityAttributes.destinationURL(for: id))
#expect(scene.isWorkspaceUnavailable)
#expect(await eventually { late.ended })
#expect(!app.containsWorkspace(id))
#expect(app.workspaces.isEmpty)
#expect(backend.requests.count == 3)
}
@Test func staleLinkKeepsUnrelatedSavedWorkspaceAndDoesNotReconnectIt() async throws {
let name = "ActivityStaleLink.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: name))
defer { defaults.removePersistentDomain(forName: name) }
let descriptor = MobileWorkspaceDescriptor(hostID: UUID(), title: "Unavailable Host", kind: .ssh)
try MobileWorkspaceStore(defaults: defaults).save(.init(sessions: ["window": [descriptor]]))
let backend = Backend()
var dependencies = MobileAppDependencies.testing(defaults: defaults)
dependencies.liveActivities = MobileLiveActivityController(services: backend.services)
let app = try MobileAppModel(dependencies: dependencies)
let scene = MobileSceneModel(app: app, sessionID: "window")
for _ in 0..<20 { await Task.yield() }
let stale = session()
let late = TestActivity(focusedWorkspaceID: stale.id)
late.contentState = .snapshot([stale], focusedID: stale.id)
backend.existing.append(late)
scene.open(stale.destinationURL)
#expect(scene.isWorkspaceUnavailable)
#expect(await eventually { late.updates.last?.sessions.map(\.id) == [descriptor.id] })
#expect(!late.ended)
#expect(scene.workspaceSession.savedWorkspaceDescriptors == [descriptor])
#expect(app.workspaces.isEmpty)
#expect(backend.requests.isEmpty)
scene.closeAllWorkspaces()
#expect(await eventually { late.ended })
}
@Test(arguments: [false, true])
func coldLaunchPublishesSavedRecordsAndCanCloseWithoutConnecting(openBeforeClose: Bool) async throws {
let name = "ActivityRestoration.\(UUID())"
@@ -153,16 +326,19 @@ struct MobileLiveActivityRestorationTests {
let backend = Backend()
let existing = TestActivity()
existing.contentState = .snapshot([session()], focusedID: nil)
backend.existing = [existing]
let duplicate = TestActivity()
backend.existing = [existing, duplicate]
var dependencies = MobileAppDependencies.testing(defaults: defaults)
dependencies.liveActivities = MobileLiveActivityController(services: backend.services)
let app = try MobileAppModel(dependencies: dependencies)
app.sceneChanged(UUID(), phase: .active)
app.persistWorkspaceDescriptors()
let scene = MobileSceneModel(app: app, sessionID: "window")
scene.open(WorkspaceActivityAttributes.destinationURL(for: UUID()))
for _ in 0..<20 { await Task.yield() }
#expect(app.error != nil)
#expect(existing.updates.isEmpty)
#expect(!existing.ended)
#expect(!existing.ended && !duplicate.ended)
#expect(defaults.data(forKey: MobileWorkspaceStore.restorationKey) == damaged)
}
@@ -263,7 +439,7 @@ struct MobileLiveActivityRestorationTests {
var created: [TestActivity] = []
var services: MobileLiveActivityServices {
MobileLiveActivityServices(
activities: { self.existing },
activities: { (self.existing + self.created).filter { !$0.ended } },
canStart: { self.canStart },
request: {
self.requests.append($0)
@@ -296,6 +472,7 @@ struct MobileLiveActivityRestorationTests {
func end() async {
if suspendsEnd {
suspendsEnd = false
await withCheckedContinuation { endContinuation = $0 }
}
ended = true
+1 -3
View File
@@ -330,9 +330,7 @@ struct HostEditorView: View {
in: Host.portForwardingMaxConnectionsRange
) {
HStack {
Text(
app.proAccess.canUse(.portForwardingMultiConnection)
? AppStrings.Vault.maxConnections : AppStrings.Pro.maxConnections)
Text(AppStrings.Vault.maxConnections)
Spacer()
Text(forwardingConnections.wrappedValue, format: .number).monospacedDigit()
}
+8 -8
View File
@@ -2033,7 +2033,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 86;
CURRENT_PROJECT_VERSION = 87;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GENERATE_INFOPLIST_FILE = YES;
ICLOUD_CONTAINER_ENVIRONMENT = Development;
@@ -2162,7 +2162,7 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 86;
CURRENT_PROJECT_VERSION = 87;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TFIWidgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Termind Workspaces";
@@ -2345,7 +2345,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 86;
CURRENT_PROJECT_VERSION = 87;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GENERATE_INFOPLIST_FILE = YES;
ICLOUD_CONTAINER_ENVIRONMENT = Production;
@@ -2384,7 +2384,7 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 86;
CURRENT_PROJECT_VERSION = 87;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TFIWidgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Termind Workspaces";
@@ -2481,7 +2481,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 60;
CURRENT_PROJECT_VERSION = 61;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = F6SUNDBBN9;
@@ -2551,7 +2551,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 60;
CURRENT_PROJECT_VERSION = 61;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = F6SUNDBBN9;
@@ -2597,7 +2597,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 97;
CURRENT_PROJECT_VERSION = 98;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "";
@@ -2635,7 +2635,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 97;
CURRENT_PROJECT_VERSION = 98;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "";