Archived
Compare commits
15
Commits
b28c5a0502
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d7a0080e | ||
|
|
e5b3e549e8 | ||
|
|
465c465af2 | ||
|
|
6e91eaaaa2 | ||
|
|
1a1230a796 | ||
|
|
2c26be2ab5 | ||
|
|
938ca7ca98 | ||
|
|
b489f10c28 | ||
|
|
fdae680e02 | ||
|
|
e7fbe5c092 | ||
|
|
d0cff20be8 | ||
|
|
f9bc1965a5 | ||
|
|
6a7afe0647 | ||
|
|
9394a80ea2 | ||
|
|
ddda0ecd3e |
@@ -7,10 +7,16 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
D0001A0CABCDEF0100AB5001 /* CalendarrWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
3934CF4630544C2E003958B8 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3934CF4530544C2E003958B8 /* WidgetKit.framework */; };
|
||||||
|
3934CF4830544C2E003958B8 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3934CF4730544C2E003958B8 /* SwiftUI.framework */; };
|
||||||
|
3934CF5530544C2E003958B8 /* CalendarrWatchComplications.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3934CF4330544C2E003958B8 /* CalendarrWatchComplications.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
39E1B0010001000000000001 /* CalendarrWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 3934CF1A30544C01003958B8 /* CalendarrWatch.app */; platformFilters = (ios, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
AA9701000001000000000001 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA9701000002000000000001 /* AppAuth */; };
|
||||||
CA1E4D0A0002000000000001 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000001 /* CalendarrCore */; };
|
CA1E4D0A0002000000000001 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000001 /* CalendarrCore */; };
|
||||||
CA1E4D0A0002000000000002 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000002 /* CalendarrCore */; };
|
CA1E4D0A0002000000000002 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000002 /* CalendarrCore */; };
|
||||||
AA9701000001000000000001 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA9701000002000000000001 /* AppAuth */; };
|
CA1E4D0A0002000000000003 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000003 /* CalendarrCore */; };
|
||||||
|
CA1E4D0A0002000000000004 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000004 /* CalendarrCore */; };
|
||||||
|
D0001A0CABCDEF0100AB5001 /* CalendarrWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -21,6 +27,34 @@
|
|||||||
remoteGlobalIDString = C0000A01FB4E10100AB5001;
|
remoteGlobalIDString = C0000A01FB4E10100AB5001;
|
||||||
remoteInfo = "Calendarr iOS";
|
remoteInfo = "Calendarr iOS";
|
||||||
};
|
};
|
||||||
|
3934CF2730544C02003958B8 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 3934CF1930544C01003958B8;
|
||||||
|
remoteInfo = CalendarrWatch;
|
||||||
|
};
|
||||||
|
3934CF3130544C02003958B8 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 3934CF1930544C01003958B8;
|
||||||
|
remoteInfo = CalendarrWatch;
|
||||||
|
};
|
||||||
|
3934CF5330544C2E003958B8 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 3934CF4230544C2E003958B8;
|
||||||
|
remoteInfo = CalendarrWatchComplications;
|
||||||
|
};
|
||||||
|
39E1B0010003000000000001 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 3934CF1930544C01003958B8;
|
||||||
|
remoteInfo = CalendarrWatch;
|
||||||
|
};
|
||||||
D0001A0DABCDEF0100AB5001 /* PBXContainerItemProxy */ = {
|
D0001A0DABCDEF0100AB5001 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
@@ -31,6 +65,28 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
3934CF5A30544C2E003958B8 /* Embed Foundation Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
3934CF5530544C2E003958B8 /* CalendarrWatchComplications.appex in Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed Foundation Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
39E1B0010002000000000001 /* Embed Watch Content */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
||||||
|
dstSubfolderSpec = 16;
|
||||||
|
files = (
|
||||||
|
39E1B0010001000000000001 /* CalendarrWatch.app in Embed Watch Content */,
|
||||||
|
);
|
||||||
|
name = "Embed Watch Content";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D0001A0BABCDEF0100AB5001 /* Embed Foundation Extensions */ = {
|
D0001A0BABCDEF0100AB5001 /* Embed Foundation Extensions */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -46,11 +102,24 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
3927C7BC2FB99D0E00EAD8ED /* Calendarr iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Calendarr iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
3927C7BC2FB99D0E00EAD8ED /* Calendarr iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Calendarr iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3934CF1A30544C01003958B8 /* CalendarrWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CalendarrWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3934CF2630544C02003958B8 /* CalendarrWatchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CalendarrWatchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3934CF3030544C02003958B8 /* CalendarrWatchUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CalendarrWatchUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3934CF4330544C2E003958B8 /* CalendarrWatchComplications.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CalendarrWatchComplications.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3934CF4530544C2E003958B8 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
|
3934CF4730544C2E003958B8 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
C0000B01FC4E10100AB5001 /* Calendarr iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Calendarr iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
C0000B01FC4E10100AB5001 /* Calendarr iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Calendarr iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CalendarrWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CalendarrWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
3934CF5630544C2E003958B8 /* Exceptions for "CalendarrWatchComplications" folder in "CalendarrWatchComplications" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Info.plist,
|
||||||
|
);
|
||||||
|
target = 3934CF4230544C2E003958B8 /* CalendarrWatchComplications */;
|
||||||
|
};
|
||||||
D0001A0FABCDEF0100AB5001 /* Exceptions for "CalendarrWidgets" folder in "CalendarrWidgets" target */ = {
|
D0001A0FABCDEF0100AB5001 /* Exceptions for "CalendarrWidgets" folder in "CalendarrWidgets" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
@@ -66,6 +135,34 @@
|
|||||||
path = "Calendarr iOSTests";
|
path = "Calendarr iOSTests";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
3934CF1B30544C01003958B8 /* CalendarrWatch */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = CalendarrWatch;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
3934CF2930544C02003958B8 /* CalendarrWatchTests */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = CalendarrWatchTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
3934CF3330544C02003958B8 /* CalendarrWatchUITests */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = CalendarrWatchUITests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
3934CF4930544C2E003958B8 /* CalendarrWatchComplications */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
3934CF5630544C2E003958B8 /* Exceptions for "CalendarrWatchComplications" folder in "CalendarrWatchComplications" target */,
|
||||||
|
);
|
||||||
|
path = CalendarrWatchComplications;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
39E1B0010005000000000001 /* CalendarrWatchShared */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = CalendarrWatchShared;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C0000D01FC4E10100AB5001 /* Calendarr iOS */ = {
|
C0000D01FC4E10100AB5001 /* Calendarr iOS */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = "Calendarr iOS";
|
path = "Calendarr iOS";
|
||||||
@@ -94,6 +191,38 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
3934CF1730544C01003958B8 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
CA1E4D0A0002000000000003 /* CalendarrCore in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF2330544C02003958B8 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF2D30544C02003958B8 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF4030544C2E003958B8 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
3934CF4830544C2E003958B8 /* SwiftUI.framework in Frameworks */,
|
||||||
|
3934CF4630544C2E003958B8 /* WidgetKit.framework in Frameworks */,
|
||||||
|
CA1E4D0A0002000000000004 /* CalendarrCore in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
C0000801FB4E10100AB5001 /* Frameworks */ = {
|
C0000801FB4E10100AB5001 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -114,6 +243,15 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
3934CF4430544C2E003958B8 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3934CF4530544C2E003958B8 /* WidgetKit.framework */,
|
||||||
|
3934CF4730544C2E003958B8 /* SwiftUI.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C0000201FB4E10100AB5001 = {
|
C0000201FB4E10100AB5001 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -121,7 +259,13 @@
|
|||||||
D0001A03ABCDEF0100AB5001 /* CalendarrWidgets */,
|
D0001A03ABCDEF0100AB5001 /* CalendarrWidgets */,
|
||||||
D0001A04ABCDEF0100AB5001 /* Shared */,
|
D0001A04ABCDEF0100AB5001 /* Shared */,
|
||||||
3927C7BD2FB99D0F00EAD8ED /* Calendarr iOSTests */,
|
3927C7BD2FB99D0F00EAD8ED /* Calendarr iOSTests */,
|
||||||
|
3934CF1B30544C01003958B8 /* CalendarrWatch */,
|
||||||
|
3934CF2930544C02003958B8 /* CalendarrWatchTests */,
|
||||||
|
3934CF3330544C02003958B8 /* CalendarrWatchUITests */,
|
||||||
|
3934CF4930544C2E003958B8 /* CalendarrWatchComplications */,
|
||||||
|
3934CF4430544C2E003958B8 /* Frameworks */,
|
||||||
C0000C01FB4E10100AB5001 /* Products */,
|
C0000C01FB4E10100AB5001 /* Products */,
|
||||||
|
39E1B0010005000000000001 /* CalendarrWatchShared */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -131,6 +275,10 @@
|
|||||||
C0000B01FC4E10100AB5001 /* Calendarr iOS.app */,
|
C0000B01FC4E10100AB5001 /* Calendarr iOS.app */,
|
||||||
3927C7BC2FB99D0E00EAD8ED /* Calendarr iOSTests.xctest */,
|
3927C7BC2FB99D0E00EAD8ED /* Calendarr iOSTests.xctest */,
|
||||||
D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */,
|
D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */,
|
||||||
|
3934CF1A30544C01003958B8 /* CalendarrWatch.app */,
|
||||||
|
3934CF2630544C02003958B8 /* CalendarrWatchTests.xctest */,
|
||||||
|
3934CF3030544C02003958B8 /* CalendarrWatchUITests.xctest */,
|
||||||
|
3934CF4330544C2E003958B8 /* CalendarrWatchComplications.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -161,6 +309,102 @@
|
|||||||
productReference = 3927C7BC2FB99D0E00EAD8ED /* Calendarr iOSTests.xctest */;
|
productReference = 3927C7BC2FB99D0E00EAD8ED /* Calendarr iOSTests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
|
3934CF1930544C01003958B8 /* CalendarrWatch */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 3934CF3C30544C02003958B8 /* Build configuration list for PBXNativeTarget "CalendarrWatch" */;
|
||||||
|
buildPhases = (
|
||||||
|
3934CF1630544C01003958B8 /* Sources */,
|
||||||
|
3934CF1730544C01003958B8 /* Frameworks */,
|
||||||
|
3934CF1830544C01003958B8 /* Resources */,
|
||||||
|
3934CF5A30544C2E003958B8 /* Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
3934CF5430544C2E003958B8 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
3934CF1B30544C01003958B8 /* CalendarrWatch */,
|
||||||
|
39E1B0010005000000000001 /* CalendarrWatchShared */,
|
||||||
|
);
|
||||||
|
name = CalendarrWatch;
|
||||||
|
packageProductDependencies = (
|
||||||
|
CA1E4D0A0001000000000003 /* CalendarrCore */,
|
||||||
|
);
|
||||||
|
productName = CalendarrWatch;
|
||||||
|
productReference = 3934CF1A30544C01003958B8 /* CalendarrWatch.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
3934CF2530544C02003958B8 /* CalendarrWatchTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 3934CF3D30544C02003958B8 /* Build configuration list for PBXNativeTarget "CalendarrWatchTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
3934CF2230544C02003958B8 /* Sources */,
|
||||||
|
3934CF2330544C02003958B8 /* Frameworks */,
|
||||||
|
3934CF2430544C02003958B8 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
3934CF2830544C02003958B8 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
3934CF2930544C02003958B8 /* CalendarrWatchTests */,
|
||||||
|
);
|
||||||
|
name = CalendarrWatchTests;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = CalendarrWatchTests;
|
||||||
|
productReference = 3934CF2630544C02003958B8 /* CalendarrWatchTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
3934CF2F30544C02003958B8 /* CalendarrWatchUITests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 3934CF3E30544C02003958B8 /* Build configuration list for PBXNativeTarget "CalendarrWatchUITests" */;
|
||||||
|
buildPhases = (
|
||||||
|
3934CF2C30544C02003958B8 /* Sources */,
|
||||||
|
3934CF2D30544C02003958B8 /* Frameworks */,
|
||||||
|
3934CF2E30544C02003958B8 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
3934CF3230544C02003958B8 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
3934CF3330544C02003958B8 /* CalendarrWatchUITests */,
|
||||||
|
);
|
||||||
|
name = CalendarrWatchUITests;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = CalendarrWatchUITests;
|
||||||
|
productReference = 3934CF3030544C02003958B8 /* CalendarrWatchUITests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.ui-testing";
|
||||||
|
};
|
||||||
|
3934CF4230544C2E003958B8 /* CalendarrWatchComplications */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 3934CF5730544C2E003958B8 /* Build configuration list for PBXNativeTarget "CalendarrWatchComplications" */;
|
||||||
|
buildPhases = (
|
||||||
|
3934CF3F30544C2E003958B8 /* Sources */,
|
||||||
|
3934CF4030544C2E003958B8 /* Frameworks */,
|
||||||
|
3934CF4130544C2E003958B8 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
3934CF4930544C2E003958B8 /* CalendarrWatchComplications */,
|
||||||
|
39E1B0010005000000000001 /* CalendarrWatchShared */,
|
||||||
|
);
|
||||||
|
name = CalendarrWatchComplications;
|
||||||
|
packageProductDependencies = (
|
||||||
|
CA1E4D0A0001000000000004 /* CalendarrCore */,
|
||||||
|
);
|
||||||
|
productName = CalendarrWatchComplications;
|
||||||
|
productReference = 3934CF4330544C2E003958B8 /* CalendarrWatchComplications.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
C0000A01FB4E10100AB5001 /* Calendarr iOS */ = {
|
C0000A01FB4E10100AB5001 /* Calendarr iOS */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = C0001601FB4E10100AB5001 /* Build configuration list for PBXNativeTarget "Calendarr iOS" */;
|
buildConfigurationList = C0001601FB4E10100AB5001 /* Build configuration list for PBXNativeTarget "Calendarr iOS" */;
|
||||||
@@ -169,11 +413,13 @@
|
|||||||
C0000801FB4E10100AB5001 /* Frameworks */,
|
C0000801FB4E10100AB5001 /* Frameworks */,
|
||||||
C0000901FB4E10100AB5001 /* Resources */,
|
C0000901FB4E10100AB5001 /* Resources */,
|
||||||
D0001A0BABCDEF0100AB5001 /* Embed Foundation Extensions */,
|
D0001A0BABCDEF0100AB5001 /* Embed Foundation Extensions */,
|
||||||
|
39E1B0010002000000000001 /* Embed Watch Content */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
D0001A0EABCDEF0100AB5001 /* PBXTargetDependency */,
|
D0001A0EABCDEF0100AB5001 /* PBXTargetDependency */,
|
||||||
|
39E1B0010004000000000001 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
C0000D01FC4E10100AB5001 /* Calendarr iOS */,
|
C0000D01FC4E10100AB5001 /* Calendarr iOS */,
|
||||||
@@ -219,13 +465,27 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 2650;
|
LastSwiftUpdateCheck = 2700;
|
||||||
LastUpgradeCheck = 2640;
|
LastUpgradeCheck = 2640;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
3927C7BB2FB99D0E00EAD8ED = {
|
3927C7BB2FB99D0E00EAD8ED = {
|
||||||
CreatedOnToolsVersion = 26.5;
|
CreatedOnToolsVersion = 26.5;
|
||||||
TestTargetID = C0000A01FB4E10100AB5001;
|
TestTargetID = C0000A01FB4E10100AB5001;
|
||||||
};
|
};
|
||||||
|
3934CF1930544C01003958B8 = {
|
||||||
|
CreatedOnToolsVersion = 27.0;
|
||||||
|
};
|
||||||
|
3934CF2530544C02003958B8 = {
|
||||||
|
CreatedOnToolsVersion = 27.0;
|
||||||
|
TestTargetID = 3934CF1930544C01003958B8;
|
||||||
|
};
|
||||||
|
3934CF2F30544C02003958B8 = {
|
||||||
|
CreatedOnToolsVersion = 27.0;
|
||||||
|
TestTargetID = 3934CF1930544C01003958B8;
|
||||||
|
};
|
||||||
|
3934CF4230544C2E003958B8 = {
|
||||||
|
CreatedOnToolsVersion = 27.0;
|
||||||
|
};
|
||||||
C0000A01FB4E10100AB5001 = {
|
C0000A01FB4E10100AB5001 = {
|
||||||
CreatedOnToolsVersion = 26.4.1;
|
CreatedOnToolsVersion = 26.4.1;
|
||||||
};
|
};
|
||||||
@@ -256,6 +516,10 @@
|
|||||||
C0000A01FB4E10100AB5001 /* Calendarr iOS */,
|
C0000A01FB4E10100AB5001 /* Calendarr iOS */,
|
||||||
D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */,
|
D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */,
|
||||||
3927C7BB2FB99D0E00EAD8ED /* Calendarr iOSTests */,
|
3927C7BB2FB99D0E00EAD8ED /* Calendarr iOSTests */,
|
||||||
|
3934CF1930544C01003958B8 /* CalendarrWatch */,
|
||||||
|
3934CF2530544C02003958B8 /* CalendarrWatchTests */,
|
||||||
|
3934CF2F30544C02003958B8 /* CalendarrWatchUITests */,
|
||||||
|
3934CF4230544C2E003958B8 /* CalendarrWatchComplications */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -268,6 +532,34 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
3934CF1830544C01003958B8 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF2430544C02003958B8 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF2E30544C02003958B8 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF4130544C2E003958B8 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
C0000901FB4E10100AB5001 /* Resources */ = {
|
C0000901FB4E10100AB5001 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -292,6 +584,34 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
3934CF1630544C01003958B8 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF2230544C02003958B8 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF2C30544C02003958B8 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
3934CF3F30544C2E003958B8 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
C0000701FB4E10100AB5001 /* Sources */ = {
|
C0000701FB4E10100AB5001 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -314,6 +634,29 @@
|
|||||||
target = C0000A01FB4E10100AB5001 /* Calendarr iOS */;
|
target = C0000A01FB4E10100AB5001 /* Calendarr iOS */;
|
||||||
targetProxy = 3927C7C02FB99D0F00EAD8ED /* PBXContainerItemProxy */;
|
targetProxy = 3927C7C02FB99D0F00EAD8ED /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
3934CF2830544C02003958B8 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 3934CF1930544C01003958B8 /* CalendarrWatch */;
|
||||||
|
targetProxy = 3934CF2730544C02003958B8 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
3934CF3230544C02003958B8 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 3934CF1930544C01003958B8 /* CalendarrWatch */;
|
||||||
|
targetProxy = 3934CF3130544C02003958B8 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
3934CF5430544C2E003958B8 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 3934CF4230544C2E003958B8 /* CalendarrWatchComplications */;
|
||||||
|
targetProxy = 3934CF5330544C2E003958B8 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
39E1B0010004000000000001 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
platformFilters = (
|
||||||
|
ios,
|
||||||
|
);
|
||||||
|
target = 3934CF1930544C01003958B8 /* CalendarrWatch */;
|
||||||
|
targetProxy = 39E1B0010003000000000001 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
D0001A0EABCDEF0100AB5001 /* PBXTargetDependency */ = {
|
D0001A0EABCDEF0100AB5001 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */;
|
target = D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */;
|
||||||
@@ -372,6 +715,232 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
3934CF3630544C02003958B8 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = CalendarrWatch/CalendarrWatch.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.scarriffleservices.calendarr.ios;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
3934CF3730544C02003958B8 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = CalendarrWatch/CalendarrWatch.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.scarriffleservices.calendarr.ios;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
3934CF3830544C02003958B8 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppTests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CalendarrWatch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CalendarrWatch";
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
3934CF3930544C02003958B8 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppTests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CalendarrWatch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CalendarrWatch";
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
3934CF3A30544C02003958B8 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppUITests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
TEST_TARGET_NAME = CalendarrWatch;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
3934CF3B30544C02003958B8 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppUITests";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
TEST_TARGET_NAME = CalendarrWatch;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
3934CF5830544C2E003958B8 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = CalendarrWatchComplications/CalendarrWatchComplications.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = CalendarrWatchComplications/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
"@executable_path/../../../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp.complications;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
3934CF5930544C2E003958B8 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = CalendarrWatchComplications/CalendarrWatchComplications.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = CalendarrWatchComplications/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
"@executable_path/../../../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp.complications;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
C0001401FB4E10100AB5001 /* Debug */ = {
|
C0001401FB4E10100AB5001 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -521,11 +1090,13 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = 3.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -542,7 +1113,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -571,11 +1142,13 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = 3.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -592,7 +1165,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -627,7 +1200,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -661,7 +1234,7 @@
|
|||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -678,6 +1251,42 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
3934CF3C30544C02003958B8 /* Build configuration list for PBXNativeTarget "CalendarrWatch" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
3934CF3630544C02003958B8 /* Debug */,
|
||||||
|
3934CF3730544C02003958B8 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
3934CF3D30544C02003958B8 /* Build configuration list for PBXNativeTarget "CalendarrWatchTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
3934CF3830544C02003958B8 /* Debug */,
|
||||||
|
3934CF3930544C02003958B8 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
3934CF3E30544C02003958B8 /* Build configuration list for PBXNativeTarget "CalendarrWatchUITests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
3934CF3A30544C02003958B8 /* Debug */,
|
||||||
|
3934CF3B30544C02003958B8 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
3934CF5730544C2E003958B8 /* Build configuration list for PBXNativeTarget "CalendarrWatchComplications" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
3934CF5830544C2E003958B8 /* Debug */,
|
||||||
|
3934CF5930544C2E003958B8 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
C0000601FB4E10100AB5001 /* Build configuration list for PBXProject "Calendarr iOS" */ = {
|
C0000601FB4E10100AB5001 /* Build configuration list for PBXProject "Calendarr iOS" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
@@ -707,22 +1316,6 @@
|
|||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
|
||||||
CA1E4D0A0001000000000001 /* CalendarrCore */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = CalendarrCore;
|
|
||||||
};
|
|
||||||
CA1E4D0A0001000000000002 /* CalendarrCore */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = CalendarrCore;
|
|
||||||
};
|
|
||||||
AA9701000002000000000001 /* AppAuth */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = AA9701000003000000000001 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */;
|
|
||||||
productName = AppAuth;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
39450EFC302A266A0090DDD7 /* XCLocalSwiftPackageReference "../CalendarrKit" */ = {
|
39450EFC302A266A0090DDD7 /* XCLocalSwiftPackageReference "../CalendarrKit" */ = {
|
||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
@@ -740,6 +1333,30 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
AA9701000002000000000001 /* AppAuth */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = AA9701000003000000000001 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */;
|
||||||
|
productName = AppAuth;
|
||||||
|
};
|
||||||
|
CA1E4D0A0001000000000001 /* CalendarrCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = CalendarrCore;
|
||||||
|
};
|
||||||
|
CA1E4D0A0001000000000002 /* CalendarrCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = CalendarrCore;
|
||||||
|
};
|
||||||
|
CA1E4D0A0001000000000003 /* CalendarrCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = CalendarrCore;
|
||||||
|
};
|
||||||
|
CA1E4D0A0001000000000004 /* CalendarrCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = CalendarrCore;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = C0000301FB4E10100AB5001 /* Project object */;
|
rootObject = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "478d8419d86ca78c1539d3775396b811ce835c77379f2af98e26eefd35ee2311",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "appauth-ios",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/openid/AppAuth-iOS.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2781038865a80e2c425a1da12cc1327bcd56501f",
|
||||||
|
"version" : "1.7.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2700"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF1930544C01003958B8"
|
||||||
|
BuildableName = "CalendarrWatch.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF2530544C02003958B8"
|
||||||
|
BuildableName = "CalendarrWatchTests.xctest"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF2F30544C02003958B8"
|
||||||
|
BuildableName = "CalendarrWatchUITests.xctest"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
queueDebuggingEnabled = "No">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF1930544C01003958B8"
|
||||||
|
BuildableName = "CalendarrWatch.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF1930544C01003958B8"
|
||||||
|
BuildableName = "CalendarrWatch.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2700"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF4230544C2E003958B8"
|
||||||
|
BuildableName = "CalendarrWatchComplications.appex"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF1930544C01003958B8"
|
||||||
|
BuildableName = "CalendarrWatch.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2"
|
||||||
|
queueDebuggingEnabled = "No">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetKind"
|
||||||
|
value = ""
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetDefaultView"
|
||||||
|
value = "timeline"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetFamily"
|
||||||
|
value = "systemMedium"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF4230544C2E003958B8"
|
||||||
|
BuildableName = "CalendarrWatchComplications.appex"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2700"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D0001A01ABCDEF0100AB5001"
|
||||||
|
BuildableName = "CalendarrWidgets.appex"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2"
|
||||||
|
queueDebuggingEnabled = "No">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetKind"
|
||||||
|
value = ""
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetDefaultView"
|
||||||
|
value = "timeline"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetFamily"
|
||||||
|
value = "systemMedium"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
+21
-1
@@ -9,11 +9,31 @@
|
|||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>CalendarrWidgets.xcscheme_^#shared#^_</key>
|
<key>CalendarrWatch Watch App.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>3</integer>
|
||||||
|
</dict>
|
||||||
|
<key>CalendarrWatch.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>CalendarrWatchComplications.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>3</integer>
|
||||||
|
</dict>
|
||||||
|
<key>CalendarrWatchExtension.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>4</integer>
|
||||||
|
</dict>
|
||||||
|
<key>CalendarrWidgets.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>4</integer>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -91,11 +91,32 @@ struct CalEvent: Identifiable, Hashable {
|
|||||||
else if let n = json["id"] as? Int { id = String(n) }
|
else if let n = json["id"] as? Int { id = String(n) }
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
let isAllDay = json["allDay"] as? Bool ?? false
|
// A date-only string is an all-day event whether or not the flag says
|
||||||
let startDate = parseDate(startStr, allDay: isAllDay)
|
// so — `parseDate` already treats it that way. Trusting the flag alone
|
||||||
let endDate = parseDate(endStr, allDay: isAllDay)
|
// is what put "00:00" in front of birthdays everywhere they are shown.
|
||||||
|
let startIsDateOnly: Bool = {
|
||||||
|
let clean = startStr.trimmingCharacters(in: .whitespaces)
|
||||||
|
return clean.count == 10 && !clean.contains("T")
|
||||||
|
}()
|
||||||
|
let declaredAllDay = (json["allDay"] as? Bool ?? false) || startIsDateOnly
|
||||||
|
let startDate = parseDate(startStr, allDay: declaredAllDay)
|
||||||
|
let endDate = parseDate(endStr, allDay: declaredAllDay)
|
||||||
guard let s = startDate, let e = endDate else { return nil }
|
guard let s = startDate, let e = endDate else { return nil }
|
||||||
|
|
||||||
|
// An event running midnight to midnight across one or more whole days
|
||||||
|
// is all-day whatever the payload claims. Without this, a six-day
|
||||||
|
// holiday sent without the flag renders as "00:00 – 00:00" — a time
|
||||||
|
// range that is technically true and tells the reader nothing.
|
||||||
|
let cal = Calendar.current
|
||||||
|
// Requiring the end to land exactly on midnight too was too strict: a
|
||||||
|
// holiday stored as 16th 00:00 to 21st 01:00 slipped through and showed
|
||||||
|
// up as a one-hour slot. Starting on a day boundary and running into a
|
||||||
|
// later day is enough.
|
||||||
|
let spansWholeDays = e > s
|
||||||
|
&& cal.startOfDay(for: s) == s
|
||||||
|
&& cal.startOfDay(for: e) > cal.startOfDay(for: s)
|
||||||
|
let isAllDay = declaredAllDay || spansWholeDays
|
||||||
|
|
||||||
return CalEvent(
|
return CalEvent(
|
||||||
id: id,
|
id: id,
|
||||||
url: json["url"] as? String ?? "",
|
url: json["url"] as? String ?? "",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.scarriffleservices.calendarr</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct CalendarrWatchApp: App {
|
||||||
|
@State private var model = WatchSnapshotModel()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// The session has to be live before the first payload arrives, and a
|
||||||
|
// complication push can wake this app with no UI on screen at all.
|
||||||
|
WatchSnapshotReceiver.shared.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
WatchAgendaView()
|
||||||
|
.environment(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// The whole app: a scrolling agenda grouped by day, plus a way to reach the
|
||||||
|
/// calendar filter. Read-only by design — the snapshot carries neither
|
||||||
|
/// recurrence rules nor permissions, so it is deliberately not enough to edit.
|
||||||
|
///
|
||||||
|
/// Deliberately does **not** apply `snapshot.theme` to the background. The
|
||||||
|
/// always-on display and OLED make black the right choice here; the theme only
|
||||||
|
/// contributes the per-event accent colours.
|
||||||
|
struct WatchAgendaView: View {
|
||||||
|
@Environment(WatchSnapshotModel.self) private var model
|
||||||
|
@State private var showingFilter = false
|
||||||
|
|
||||||
|
private var calendar: Calendar {
|
||||||
|
var calendar = Calendar(identifier: .gregorian)
|
||||||
|
calendar.locale = WatchL10n.locale(model.language)
|
||||||
|
return calendar
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
content
|
||||||
|
.navigationTitle(WatchL10n.t("watch.title", model.language))
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button { showingFilter = true } label: {
|
||||||
|
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||||
|
}
|
||||||
|
.accessibilityLabel(WatchL10n.t("watch.calendars", model.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingFilter) { WatchCalendarFilterView() }
|
||||||
|
}
|
||||||
|
.onAppear { WatchSnapshotReceiver.shared.requestRefresh() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var content: some View {
|
||||||
|
if let snapshot = model.visible, !groups(in: snapshot).isEmpty {
|
||||||
|
List {
|
||||||
|
ForEach(groups(in: snapshot), id: \.day) { group in
|
||||||
|
Section(WatchEventFormat.dayHeader(group.day, language: model.language,
|
||||||
|
calendar: calendar)) {
|
||||||
|
ForEach(group.events) { event in
|
||||||
|
NavigationLink {
|
||||||
|
WatchEventDetailView(event: event,
|
||||||
|
calendarName: model.calendarName(for: event.calendarKey),
|
||||||
|
language: model.language)
|
||||||
|
} label: {
|
||||||
|
WatchEventRow(event: event, language: model.language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("\(WatchL10n.t("watch.as_of", model.language)) \(WatchEventFormat.time(snapshot.writtenAt, language: model.language))")
|
||||||
|
// Beyond the coverage window the snapshot carries no
|
||||||
|
// information, which is not the same as "nothing
|
||||||
|
// scheduled". Saying so is what keeps the empty tail of
|
||||||
|
// the list from reading as a free afternoon.
|
||||||
|
Text("\(WatchL10n.t("watch.no_information", model.language)) \(coverageEndLine(snapshot))")
|
||||||
|
if let error = WatchSnapshotReceiver.shared.lastError {
|
||||||
|
Text(error).foregroundStyle(.orange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.carousel)
|
||||||
|
} else {
|
||||||
|
WatchStatusView(result: model.result, language: model.language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DayGroup {
|
||||||
|
let day: Date
|
||||||
|
let events: [SnapshotEvent]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last day the snapshot can speak for, formatted for the footer.
|
||||||
|
private func coverageEndLine(_ snapshot: CalendarrSnapshot) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(model.language)
|
||||||
|
formatter.setLocalizedDateFormatFromTemplate("d MMM")
|
||||||
|
let lastCovered = calendar.date(byAdding: .day, value: -1, to: snapshot.coverageEnd)
|
||||||
|
return formatter.string(from: lastCovered ?? snapshot.coverageEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group the covered window by day. Days the snapshot cannot speak for are
|
||||||
|
/// simply absent — never rendered as "nothing scheduled", which would look
|
||||||
|
/// correct and be false.
|
||||||
|
private func groups(in snapshot: CalendarrSnapshot) -> [DayGroup] {
|
||||||
|
let calendar = self.calendar
|
||||||
|
let from = max(snapshot.coverageStart, calendar.startOfDay(for: Date()))
|
||||||
|
var buckets: [Date: [SnapshotEvent]] = [:]
|
||||||
|
|
||||||
|
for event in snapshot.events where event.end > from {
|
||||||
|
var day = calendar.startOfDay(for: max(event.start, from))
|
||||||
|
// A multi-day event appears on each day it covers.
|
||||||
|
while day < event.end && day < snapshot.coverageEnd {
|
||||||
|
buckets[day, default: []].append(event)
|
||||||
|
guard let next = calendar.date(byAdding: .day, value: 1, to: day) else { break }
|
||||||
|
day = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buckets
|
||||||
|
.map { DayGroup(day: $0.key, events: $0.value.sorted { $0.start < $1.start }) }
|
||||||
|
.sorted { $0.day < $1.day }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Narrows what this watch shows, on top of what the phone already filtered.
|
||||||
|
/// One toggle here affects the agenda and every complication at once.
|
||||||
|
struct WatchCalendarFilterView: View {
|
||||||
|
@Environment(WatchSnapshotModel.self) private var model
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
if model.calendars.isEmpty {
|
||||||
|
Text(WatchL10n.t("watch.waiting", model.language))
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
ForEach(model.calendars) { calendar in
|
||||||
|
row(calendar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(WatchL10n.t("watch.calendars", model.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func row(_ calendar: SnapshotCalendar) -> some View {
|
||||||
|
let shown = !model.isHidden(calendar.id)
|
||||||
|
return Button {
|
||||||
|
model.setHidden(calendar.id, shown)
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
// A checkbox, not a Toggle: a switch reads as a state indicator
|
||||||
|
// you wait on rather than a control you tap.
|
||||||
|
Image(systemName: shown ? "checkmark.square.fill" : "square")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.foregroundStyle(shown ? Color.accentColor : Color.secondary)
|
||||||
|
Circle()
|
||||||
|
.fill(Color(watchHex: calendar.colorHex))
|
||||||
|
.frame(width: 7, height: 7)
|
||||||
|
Text(calendar.name)
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityAddTraits(shown ? [.isSelected] : [])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Everything the snapshot carries about one event, which is deliberately not
|
||||||
|
/// everything the server knows: notes, recurrence and permissions are dropped
|
||||||
|
/// on the way out, so there is nothing here to edit.
|
||||||
|
struct WatchEventDetailView: View {
|
||||||
|
let event: SnapshotEvent
|
||||||
|
/// Nil when the calendar list has not arrived yet, which is possible right
|
||||||
|
/// after a first install.
|
||||||
|
let calendarName: String?
|
||||||
|
let language: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(alignment: .top, spacing: 6) {
|
||||||
|
Circle()
|
||||||
|
.fill(Color(watchHex: event.colorHex))
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
.padding(.top, 5)
|
||||||
|
Text(event.title)
|
||||||
|
.font(.headline)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label(WatchEventFormat.timeRange(event, language: language),
|
||||||
|
systemImage: "clock")
|
||||||
|
.font(.footnote)
|
||||||
|
|
||||||
|
Label(dateLine, systemImage: "calendar")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
if !event.location.isEmpty {
|
||||||
|
Label(event.location, systemImage: "mappin.and.ellipse")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let calendarName {
|
||||||
|
Label(calendarName, systemImage: "list.bullet.rectangle")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dateLine: String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.setLocalizedDateFormatFromTemplate("EEEE d MMMM")
|
||||||
|
return formatter.string(from: event.start)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// One event: a colour bar for the calendar, the time range, the title.
|
||||||
|
struct WatchEventRow: View {
|
||||||
|
let event: SnapshotEvent
|
||||||
|
let language: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 7) {
|
||||||
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.fill(Color(watchHex: event.colorHex))
|
||||||
|
.frame(width: 3)
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
Text(WatchEventFormat.timeRange(event, language: language))
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(event.title)
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
/// Local copy of the hex initialiser: `CalendarrCore` is Foundation-only by
|
||||||
|
/// design, so colour parsing cannot live there.
|
||||||
|
init(watchHex hex: String) {
|
||||||
|
let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||||
|
var value: UInt64 = 0
|
||||||
|
Scanner(string: cleaned).scanHexInt64(&value)
|
||||||
|
guard cleaned.count == 6 else { self = .gray; return }
|
||||||
|
self.init(red: Double((value >> 16) & 0xFF) / 255,
|
||||||
|
green: Double((value >> 8) & 0xFF) / 255,
|
||||||
|
blue: Double(value & 0xFF) / 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WatchEventFormat {
|
||||||
|
static func timeRange(_ event: SnapshotEvent, language: String,
|
||||||
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> String {
|
||||||
|
if event.isAllDay { return WatchL10n.t("watch.allday", language) }
|
||||||
|
|
||||||
|
let time = DateFormatter()
|
||||||
|
time.locale = WatchL10n.locale(language)
|
||||||
|
time.dateFormat = "HH:mm"
|
||||||
|
|
||||||
|
let start = time.string(from: event.start)
|
||||||
|
guard event.end > event.start else { return start }
|
||||||
|
|
||||||
|
// Without the weekday, an event running from Wednesday to Monday reads
|
||||||
|
// as a one-hour slot: "00:00 – 01:00" is true and useless.
|
||||||
|
if calendar.startOfDay(for: event.end) > calendar.startOfDay(for: event.start) {
|
||||||
|
let day = DateFormatter()
|
||||||
|
day.locale = WatchL10n.locale(language)
|
||||||
|
day.setLocalizedDateFormatFromTemplate("EEE")
|
||||||
|
return "\(day.string(from: event.start)) \(start) – "
|
||||||
|
+ "\(day.string(from: event.end)) \(time.string(from: event.end))"
|
||||||
|
}
|
||||||
|
return "\(start) – \(time.string(from: event.end))"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func time(_ date: Date, language: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.dateFormat = "HH:mm"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Heute", "Morgen", else "Do 11. Sep".
|
||||||
|
static func dayHeader(_ day: Date, language: String,
|
||||||
|
calendar: Calendar, now: Date = Date()) -> String {
|
||||||
|
if calendar.isDate(day, inSameDayAs: now) {
|
||||||
|
return WatchL10n.t("watch.today", language)
|
||||||
|
}
|
||||||
|
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: now)),
|
||||||
|
calendar.isDate(day, inSameDayAs: tomorrow) {
|
||||||
|
return WatchL10n.t("watch.tomorrow", language)
|
||||||
|
}
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.setLocalizedDateFormatFromTemplate("EEE d MMM")
|
||||||
|
return formatter.string(from: day)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Every reason there is nothing to show, named. "Never received", "signed out"
|
||||||
|
/// and "genuinely empty" call for different sentences, and showing the wrong one
|
||||||
|
/// sends the user looking in the wrong place.
|
||||||
|
struct WatchStatusView: View {
|
||||||
|
let result: SnapshotReadResult
|
||||||
|
let language: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Image(systemName: symbol)
|
||||||
|
.font(.system(size: 22))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(message)
|
||||||
|
.font(.footnote)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var symbol: String {
|
||||||
|
switch result {
|
||||||
|
case .ok: return "calendar"
|
||||||
|
case .loggedOut: return "person.crop.circle.badge.xmark"
|
||||||
|
case .neverWritten: return "iphone.and.arrow.forward"
|
||||||
|
case .incompatible: return "arrow.down.circle"
|
||||||
|
case .unreadable: return "exclamationmark.triangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var message: String {
|
||||||
|
switch result {
|
||||||
|
case .ok: return WatchL10n.t("watch.no_events", language)
|
||||||
|
case .loggedOut: return WatchL10n.t("watch.sign_in", language)
|
||||||
|
case .neverWritten: return WatchL10n.t("watch.waiting", language)
|
||||||
|
case .incompatible: return WatchL10n.t("watch.update", language)
|
||||||
|
case .unreadable: return WatchL10n.t("watch.unreadable", language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Foundation
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// What the watch views read. Holds the last received snapshot, the calendar
|
||||||
|
/// list, and this watch's filter.
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
final class WatchSnapshotModel {
|
||||||
|
private(set) var result: SnapshotReadResult = .neverWritten
|
||||||
|
private(set) var calendars: [SnapshotCalendar] = []
|
||||||
|
private(set) var hiddenKeys: Set<String> = WatchFilterStore.load()
|
||||||
|
|
||||||
|
private let store = SnapshotStore()
|
||||||
|
private var changeToken: SnapshotChangeNotifier.ObservationToken?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
reload()
|
||||||
|
// The receiver writes through SnapshotStore, which broadcasts. Listening
|
||||||
|
// here means the UI does not have to be notified separately.
|
||||||
|
changeToken = SnapshotChangeNotifier.observe { [weak self] in
|
||||||
|
Task { @MainActor in self?.reload() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reload() {
|
||||||
|
result = store.read()
|
||||||
|
calendars = store.readCalendars()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The snapshot with this watch's filter applied, or nil when there is
|
||||||
|
/// nothing usable to show.
|
||||||
|
var visible: CalendarrSnapshot? {
|
||||||
|
result.snapshot?.filtered(excludingCalendarKeys: hiddenKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
var language: String { result.snapshot?.language ?? "system" }
|
||||||
|
|
||||||
|
func isHidden(_ key: String) -> Bool { hiddenKeys.contains(key) }
|
||||||
|
|
||||||
|
/// The display name for an event's calendar. Events carry only a
|
||||||
|
/// `calendarKey`; the names travel separately, in the calendar list.
|
||||||
|
func calendarName(for key: String) -> String? {
|
||||||
|
calendars.first { $0.id == key }?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHidden(_ key: String, _ hidden: Bool) {
|
||||||
|
if hidden { hiddenKeys.insert(key) } else { hiddenKeys.remove(key) }
|
||||||
|
WatchFilterStore.save(hiddenKeys)
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import Foundation
|
||||||
|
import WatchConnectivity
|
||||||
|
import WidgetKit
|
||||||
|
@_spi(Writer) import CalendarrCore
|
||||||
|
|
||||||
|
/// Takes what the phone sends and writes it into this watch's own App Group
|
||||||
|
/// container, using the same `SnapshotStore` the phone writes with. One wire
|
||||||
|
/// format, one reader, no drift.
|
||||||
|
///
|
||||||
|
/// Only the app runs a `WCSession`; a widget extension cannot. The complications
|
||||||
|
/// always read what this wrote.
|
||||||
|
@MainActor
|
||||||
|
final class WatchSnapshotReceiver: NSObject {
|
||||||
|
static let shared = WatchSnapshotReceiver()
|
||||||
|
|
||||||
|
private let store = SnapshotStore()
|
||||||
|
private let sessions = SharedSessionStore()
|
||||||
|
|
||||||
|
/// Shown in the agenda footer, because a silent failure here looks exactly
|
||||||
|
/// like an empty calendar.
|
||||||
|
private(set) var lastError: String?
|
||||||
|
|
||||||
|
func activate() {
|
||||||
|
guard WCSession.isSupported() else { return }
|
||||||
|
WCSession.default.delegate = self
|
||||||
|
WCSession.default.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask the phone for the current state. Covers "the app is open in front of
|
||||||
|
/// me and looks stale"; the other two paths are push-driven.
|
||||||
|
func requestRefresh() {
|
||||||
|
let session = WCSession.default
|
||||||
|
guard session.activationState == .activated, session.isReachable else { return }
|
||||||
|
|
||||||
|
session.sendMessage(["request": "snapshot"],
|
||||||
|
replyHandler: { reply in
|
||||||
|
Task { @MainActor in self.apply(reply) }
|
||||||
|
},
|
||||||
|
errorHandler: { error in
|
||||||
|
Task { @MainActor in self.lastError = error.localizedDescription }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(_ dict: [String: Any]) {
|
||||||
|
guard !dict.isEmpty else { return }
|
||||||
|
do {
|
||||||
|
let payload = try WatchTransportPayload.decode(dict)
|
||||||
|
// This write posts SnapshotChangeNotifier, which is what the read
|
||||||
|
// model listens for — so the UI updates without being told twice.
|
||||||
|
try store.write(payload.snapshot)
|
||||||
|
try store.writeCalendars(payload.calendars)
|
||||||
|
if let session = payload.session { try sessions.write(session) }
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
lastError = nil
|
||||||
|
} catch {
|
||||||
|
lastError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WatchSnapshotReceiver: WCSessionDelegate {
|
||||||
|
nonisolated func session(_ session: WCSession,
|
||||||
|
activationDidCompleteWith state: WCSessionActivationState,
|
||||||
|
error: Error?) {
|
||||||
|
if let error {
|
||||||
|
Task { @MainActor in self.lastError = error.localizedDescription }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main path: latest state only, delivered when this app launches or wakes.
|
||||||
|
nonisolated func session(_ session: WCSession,
|
||||||
|
didReceiveApplicationContext applicationContext: [String: Any]) {
|
||||||
|
Task { @MainActor in self.apply(applicationContext) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The complication push, which is the only path that wakes this app while
|
||||||
|
/// it is not running.
|
||||||
|
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
||||||
|
Task { @MainActor in self.apply(userInfo) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.scarriffleservices.calendarr</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct CalendarrWatchComplicationBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
WatchNextEventWidget()
|
||||||
|
WatchCornerCountdownWidget()
|
||||||
|
WatchCornerTitleWidget()
|
||||||
|
WatchCornerTimeWidget()
|
||||||
|
WatchCountdownWidget()
|
||||||
|
WatchDateWidget()
|
||||||
|
WatchTodayCountWidget()
|
||||||
|
WatchNextTwoWidget()
|
||||||
|
WatchDatePlusEventWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension View {
|
||||||
|
/// Accessory families paint their own background; ours must stay out of
|
||||||
|
/// the way so the watch face's tint comes through.
|
||||||
|
func complicationChrome(_ language: String) -> some View {
|
||||||
|
containerBackground(for: .widget) { Color.clear }
|
||||||
|
.environment(\.locale, WatchL10n.locale(language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchNextEventWidget: Widget {
|
||||||
|
let kind = "WatchNextEvent"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
NextEventComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Nächster Termin")
|
||||||
|
.description("Zeit, Titel und Ort des nächsten Termins.")
|
||||||
|
.supportedFamilies([.accessoryRectangular, .accessoryInline])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchCornerCountdownWidget: Widget {
|
||||||
|
let kind = "WatchCornerCountdown"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
CornerCountdownComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Restzeit (Ecke)")
|
||||||
|
.description("Restzeit bis zum nächsten Termin, Name am Rand.")
|
||||||
|
.supportedFamilies([.accessoryCorner])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchCornerTitleWidget: Widget {
|
||||||
|
let kind = "WatchCornerTitle"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
CornerTitleComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Terminname (Ecke)")
|
||||||
|
.description("Name des nächsten Termins, Uhrzeit klein am Rand.")
|
||||||
|
.supportedFamilies([.accessoryCorner])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchCornerTimeWidget: Widget {
|
||||||
|
let kind = "WatchCornerTime"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
CornerTimeComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Uhrzeit (Ecke)")
|
||||||
|
.description("Uhrzeit des nächsten Termins, Name am Rand.")
|
||||||
|
.supportedFamilies([.accessoryCorner])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchCountdownWidget: Widget {
|
||||||
|
let kind = "WatchCountdown"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
CountdownComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Countdown")
|
||||||
|
.description("Ring, der bis zum nächsten Termin leerläuft.")
|
||||||
|
.supportedFamilies([.accessoryCircular, .accessoryInline])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchDateWidget: Widget {
|
||||||
|
let kind = "WatchDate"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
DateComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Datum")
|
||||||
|
.description("Tag und Monat.")
|
||||||
|
.supportedFamilies([.accessoryCircular, .accessoryInline])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchTodayCountWidget: Widget {
|
||||||
|
let kind = "WatchTodayCount"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
TodayCountComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Termine heute")
|
||||||
|
.description("Anzahl der heutigen Termine.")
|
||||||
|
.supportedFamilies([.accessoryCircular, .accessoryInline])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchNextTwoWidget: Widget {
|
||||||
|
let kind = "WatchNextTwo"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
NextTwoComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Die nächsten zwei")
|
||||||
|
.description("Die beiden nächsten Termine.")
|
||||||
|
.supportedFamilies([.accessoryRectangular])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchDatePlusEventWidget: Widget {
|
||||||
|
let kind = "WatchDatePlusEvent"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
DatePlusEventComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Datum + Termin")
|
||||||
|
.description("Datum und nächster Termin in einem Slot.")
|
||||||
|
.supportedFamilies([.accessoryRectangular])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
// A corner holds **one** value plus the curved bezel label. A second inner line
|
||||||
|
// runs into the curved text — do not add one. `.widgetLabel` is what makes
|
||||||
|
// WidgetKit lay the text along the edge; there is no arc to draw by hand.
|
||||||
|
|
||||||
|
/// Value: live countdown. Label: time and title.
|
||||||
|
struct CornerCountdownComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
Group {
|
||||||
|
if let next {
|
||||||
|
Text(WatchComplicationSupport.compactCountdown(
|
||||||
|
to: WatchComplicationSupport.countdownTarget(next, at: entry.date),
|
||||||
|
at: entry.date,
|
||||||
|
language: entry.language))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.6)
|
||||||
|
.widgetAccentable()
|
||||||
|
} else {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widgetLabel {
|
||||||
|
Text(WatchComplicationSupport.eventLabel(next, at: entry.date, language: entry.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Value: the appointment's clock time. Label: the title.
|
||||||
|
struct CornerTimeComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
Group {
|
||||||
|
if let next {
|
||||||
|
Text(next.isAllDay
|
||||||
|
? WatchL10n.t("watch.allday", entry.language)
|
||||||
|
: WatchComplicationSupport.time(next.start, language: entry.language))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.6)
|
||||||
|
.widgetAccentable()
|
||||||
|
} else {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widgetLabel {
|
||||||
|
Text(next?.title ?? WatchL10n.t("watch.no_events", entry.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Value: the appointment's name. Label: the time, small and curved.
|
||||||
|
///
|
||||||
|
/// This is the inverse of how the slot is shaped — the curved bezel is where a
|
||||||
|
/// corner has room for long text, and the inner area holds three or four
|
||||||
|
/// characters. The title therefore shrinks and truncates here. Built this way
|
||||||
|
/// on request; `CornerTimeComplicationView` is the same information with the
|
||||||
|
/// roles the other way round, so both can be compared on the face.
|
||||||
|
struct CornerTitleComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
Group {
|
||||||
|
if let next {
|
||||||
|
Text(next.title)
|
||||||
|
.font(.system(size: 13, weight: .medium))
|
||||||
|
.lineLimit(2)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.widgetAccentable()
|
||||||
|
} else {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widgetLabel {
|
||||||
|
Text(next.map {
|
||||||
|
$0.isAllDay
|
||||||
|
? WatchL10n.t("watch.allday", entry.language)
|
||||||
|
: WatchComplicationSupport.time($0.start, language: entry.language)
|
||||||
|
} ?? WatchL10n.t("watch.no_events", entry.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Time, title, location. The variant that answers the actual question.
|
||||||
|
struct NextEventComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
switch family {
|
||||||
|
case .accessoryInline:
|
||||||
|
inline(next)
|
||||||
|
default:
|
||||||
|
rectangular(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func rectangular(_ next: SnapshotEvent?) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
if let next {
|
||||||
|
Text(next.isAllDay
|
||||||
|
? WatchL10n.t("watch.allday", entry.language)
|
||||||
|
: WatchComplicationSupport.time(next.start, language: entry.language))
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.widgetAccentable()
|
||||||
|
Text(next.title)
|
||||||
|
.font(.system(size: 15))
|
||||||
|
.lineLimit(1)
|
||||||
|
if !next.location.isEmpty {
|
||||||
|
Text(next.location)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(systemName: "calendar").font(.system(size: 13)).widgetAccentable()
|
||||||
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 13))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func inline(_ next: SnapshotEvent?) -> some View {
|
||||||
|
let text: String = {
|
||||||
|
guard let next else { return WatchL10n.t("watch.no_events", entry.language) }
|
||||||
|
if next.isAllDay { return next.title }
|
||||||
|
return "\(WatchComplicationSupport.time(next.start, language: entry.language)) \(next.title)"
|
||||||
|
}()
|
||||||
|
return Label(text, systemImage: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next two, so back-to-back appointments are visible at once.
|
||||||
|
struct NextTwoComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let events = WatchComplicationSupport.upcoming(in: entry.snapshot, at: entry.date, limit: 2)
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
if events.isEmpty {
|
||||||
|
Image(systemName: "calendar").font(.system(size: 13)).widgetAccentable()
|
||||||
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 13))
|
||||||
|
} else {
|
||||||
|
ForEach(events) { event in
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Text(event.isAllDay
|
||||||
|
? WatchL10n.t("watch.allday", entry.language)
|
||||||
|
: WatchComplicationSupport.time(event.start, language: entry.language))
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.widgetAccentable()
|
||||||
|
Text(event.title)
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Weekday and date plus the next appointment, so one slot does both jobs.
|
||||||
|
struct DatePlusEventComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(dateLine)
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
.widgetAccentable()
|
||||||
|
if let next {
|
||||||
|
Text(next.isAllDay
|
||||||
|
? next.title
|
||||||
|
: "\(WatchComplicationSupport.time(next.start, language: entry.language)) \(next.title)")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.lineLimit(1)
|
||||||
|
} else {
|
||||||
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 14))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dateLine: String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(entry.language)
|
||||||
|
formatter.setLocalizedDateFormatFromTemplate("EEE d MMMM")
|
||||||
|
return formatter.string(from: entry.date).uppercased()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Attaches the curved bezel label that a face with a bezel slot — Infograph —
|
||||||
|
/// renders across the top, where the tick marks otherwise are. Faces without
|
||||||
|
/// that slot ignore it, so it costs nothing to always provide.
|
||||||
|
private extension View {
|
||||||
|
func eventBezelLabel(_ entry: WatchComplicationEntry) -> some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
return widgetLabel {
|
||||||
|
Text(WatchComplicationSupport.eventLabel(next, at: entry.date, language: entry.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A draining ring plus the remaining time.
|
||||||
|
struct CountdownComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
switch family {
|
||||||
|
case .accessoryInline:
|
||||||
|
inline(next)
|
||||||
|
default:
|
||||||
|
circular(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func circular(_ next: SnapshotEvent?) -> some View {
|
||||||
|
if let next {
|
||||||
|
let target = WatchComplicationSupport.countdownTarget(next, at: entry.date)
|
||||||
|
Gauge(value: WatchComplicationSupport.gaugeFraction(to: target, at: entry.date)) {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
} currentValueLabel: {
|
||||||
|
Text(WatchComplicationSupport.compactCountdown(to: target, at: entry.date,
|
||||||
|
language: entry.language))
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.6)
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.widgetAccentable()
|
||||||
|
.eventBezelLabel(entry)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
AccessoryWidgetBackground()
|
||||||
|
Image(systemName: "calendar").font(.system(size: 18))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func inline(_ next: SnapshotEvent?) -> some View {
|
||||||
|
if let next {
|
||||||
|
Label {
|
||||||
|
Text(WatchComplicationSupport.countdownTarget(next, at: entry.date), style: .timer)
|
||||||
|
} icon: {
|
||||||
|
Image(systemName: "timer")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Label(WatchL10n.t("watch.no_events", entry.language), systemImage: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Weekday and day number, the way a date is actually read. The ring appears
|
||||||
|
/// only while an appointment is running, and then shows how far through it we
|
||||||
|
/// are — an empty track around an idle day is noise, so there is no Gauge at
|
||||||
|
/// all in that case.
|
||||||
|
struct DateComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
private var day: Int {
|
||||||
|
Calendar(identifier: .gregorian).component(.day, from: entry.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var weekday: String {
|
||||||
|
WatchComplicationSupport.weekdayAbbreviation(entry.date, language: entry.language)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch family {
|
||||||
|
case .accessoryInline:
|
||||||
|
Label("\(weekday) \(day)", systemImage: "calendar")
|
||||||
|
default:
|
||||||
|
circular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var circular: some View {
|
||||||
|
if let running = WatchComplicationSupport.runningEvent(in: entry.snapshot, at: entry.date) {
|
||||||
|
Gauge(value: WatchComplicationSupport.progress(of: running, at: entry.date)) {
|
||||||
|
Text(weekday)
|
||||||
|
} currentValueLabel: {
|
||||||
|
dateStack
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.eventBezelLabel(entry)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
AccessoryWidgetBackground()
|
||||||
|
dateStack
|
||||||
|
}
|
||||||
|
.eventBezelLabel(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dateStack: some View {
|
||||||
|
VStack(spacing: -2) {
|
||||||
|
Text(weekday)
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
.widgetAccentable()
|
||||||
|
Text("\(day)")
|
||||||
|
.font(.system(size: 23, weight: .semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many appointments today. Says "busy", not "what".
|
||||||
|
struct TodayCountComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
private var count: Int {
|
||||||
|
WatchComplicationSupport.todayCount(in: entry.snapshot, at: entry.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch family {
|
||||||
|
case .accessoryInline:
|
||||||
|
Label("\(count) \(WatchL10n.t("watch.events_today", entry.language))",
|
||||||
|
systemImage: "calendar")
|
||||||
|
default:
|
||||||
|
Gauge(value: WatchComplicationSupport.todayRemainingFraction(in: entry.snapshot,
|
||||||
|
at: entry.date)) {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
} currentValueLabel: {
|
||||||
|
Text("\(count)")
|
||||||
|
.font(.system(size: 20, weight: .semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.6)
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.widgetAccentable()
|
||||||
|
.eventBezelLabel(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import AppIntents
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// One calendar option in a complication's configuration.
|
||||||
|
struct WatchCalendarEntity: AppEntity, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Kalender"
|
||||||
|
static var defaultQuery = WatchCalendarEntityQuery()
|
||||||
|
|
||||||
|
var displayRepresentation: DisplayRepresentation {
|
||||||
|
DisplayRepresentation(title: LocalizedStringResource(stringLiteral: name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the calendars from this watch's App Group container, so configuration
|
||||||
|
/// needs no network call and no phone.
|
||||||
|
struct WatchCalendarEntityQuery: EntityQuery {
|
||||||
|
func entities(for identifiers: [String]) async throws -> [WatchCalendarEntity] {
|
||||||
|
let wanted = Set(identifiers)
|
||||||
|
return SnapshotStore().readCalendars()
|
||||||
|
.filter { wanted.contains($0.id) }
|
||||||
|
.map { WatchCalendarEntity(id: $0.id, name: $0.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func suggestedEntities() async throws -> [WatchCalendarEntity] {
|
||||||
|
SnapshotStore().readCalendars().map { WatchCalendarEntity(id: $0.id, name: $0.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-complication override of the calendar selection. The in-app toggles are
|
||||||
|
/// the primary path — intent configuration on a watch face is awkward to
|
||||||
|
/// operate — so an empty selection means "whatever the app is set to".
|
||||||
|
struct WatchCalendarSelectionIntent: WidgetConfigurationIntent {
|
||||||
|
static var title: LocalizedStringResource = "Kalender auswählen"
|
||||||
|
static var description = IntentDescription("Leer = die Auswahl aus der Watch-App.")
|
||||||
|
|
||||||
|
// Must be optional: WidgetConfigurationIntent requires optional parameter
|
||||||
|
// types, and the watchOS SDK enforces it where iOS lets a bare array pass.
|
||||||
|
@Parameter(title: "Kalender")
|
||||||
|
var selectedCalendars: [WatchCalendarEntity]?
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import AppIntents
|
||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
struct WatchComplicationEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let snapshot: CalendarrSnapshot?
|
||||||
|
|
||||||
|
var language: String { snapshot?.language ?? "system" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds every Calendarr complication's timeline.
|
||||||
|
///
|
||||||
|
/// Entries land on event boundaries rather than on a fixed schedule, so the
|
||||||
|
/// complication flips in the exact minute the next appointment changes without
|
||||||
|
/// a network call and without spending refresh budget. New *data* arrives by
|
||||||
|
/// push from the phone, which is why the refresh policy is `.atEnd`: when the
|
||||||
|
/// timeline simply runs out, regenerating from the same snapshot is cheap and
|
||||||
|
/// correct.
|
||||||
|
struct WatchComplicationProvider: AppIntentTimelineProvider {
|
||||||
|
typealias Entry = WatchComplicationEntry
|
||||||
|
typealias Intent = WatchCalendarSelectionIntent
|
||||||
|
|
||||||
|
func placeholder(in context: Context) -> WatchComplicationEntry {
|
||||||
|
WatchComplicationEntry(date: .now, snapshot: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: WatchCalendarSelectionIntent,
|
||||||
|
in context: Context) async -> WatchComplicationEntry {
|
||||||
|
WatchComplicationEntry(date: .now, snapshot: resolved(configuration))
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: WatchCalendarSelectionIntent,
|
||||||
|
in context: Context) async -> Timeline<WatchComplicationEntry> {
|
||||||
|
let snapshot = resolved(configuration)
|
||||||
|
let now = Date()
|
||||||
|
let dates = snapshot.map { ComplicationTimeline.entryDates(from: now, in: $0) } ?? [now]
|
||||||
|
return Timeline(entries: dates.map { WatchComplicationEntry(date: $0, snapshot: snapshot) },
|
||||||
|
policy: .atEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the complication gallery offers before anyone configures anything.
|
||||||
|
/// Required on watchOS — iOS has a default implementation, the watch does
|
||||||
|
/// not, because the gallery has to be populated.
|
||||||
|
///
|
||||||
|
/// One entry, with nothing selected: that means "whatever the app is set
|
||||||
|
/// to", which is the filter the user already chose there.
|
||||||
|
func recommendations() -> [AppIntentRecommendation<WatchCalendarSelectionIntent>] {
|
||||||
|
[AppIntentRecommendation(intent: WatchCalendarSelectionIntent(),
|
||||||
|
description: Text("Calendarr"))]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The app-level filter applies first; a per-complication selection narrows
|
||||||
|
/// further. An empty selection means "whatever the app is set to".
|
||||||
|
private func resolved(_ configuration: WatchCalendarSelectionIntent) -> CalendarrSnapshot? {
|
||||||
|
guard case .ok(let snapshot) = SnapshotStore().read() else { return nil }
|
||||||
|
return snapshot
|
||||||
|
.filtered(excludingCalendarKeys: WatchFilterStore.load())
|
||||||
|
.filtered(toCalendarKeys: Set((configuration.selectedCalendars ?? []).map(\.id)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import Foundation
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
enum WatchComplicationSupport {
|
||||||
|
/// The first event that has not ended yet — including one running now,
|
||||||
|
/// which is what "what is next" means when you are already in a meeting.
|
||||||
|
/// Matches `WatchPushPolicy.nextEventIdentity`, deliberately.
|
||||||
|
static func nextEvent(in snapshot: CalendarrSnapshot?, at now: Date) -> SnapshotEvent? {
|
||||||
|
snapshot?.events.filter { $0.end > now }.min { $0.start < $1.start }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func upcoming(in snapshot: CalendarrSnapshot?, at now: Date, limit: Int) -> [SnapshotEvent] {
|
||||||
|
guard let snapshot else { return [] }
|
||||||
|
return Array(snapshot.events.filter { $0.end > now }.sorted { $0.start < $1.start }.prefix(limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func todayCount(in snapshot: CalendarrSnapshot?, at now: Date,
|
||||||
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Int {
|
||||||
|
guard let snapshot else { return 0 }
|
||||||
|
let dayStart = calendar.startOfDay(for: now)
|
||||||
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
||||||
|
return snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }.count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appointment happening right now, if there is one. All-day events are
|
||||||
|
/// excluded on purpose: one of those is "running" for twenty-four hours and
|
||||||
|
/// would hold the ring at some arbitrary fraction all day, which says
|
||||||
|
/// nothing.
|
||||||
|
static func runningEvent(in snapshot: CalendarrSnapshot?, at now: Date) -> SnapshotEvent? {
|
||||||
|
snapshot?.events
|
||||||
|
.filter { !$0.isAllDay && $0.start <= now && $0.end > now }
|
||||||
|
.min { $0.end < $1.end }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How far through an appointment we are, 0 at its start and 1 at its end.
|
||||||
|
static func progress(of event: SnapshotEvent, at now: Date) -> Double {
|
||||||
|
let total = event.end.timeIntervalSince(event.start)
|
||||||
|
guard total > 0 else { return 0 }
|
||||||
|
return min(1, max(0, now.timeIntervalSince(event.start) / total))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func weekdayAbbreviation(_ date: Date, language: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.dateFormat = "EEE"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
.replacingOccurrences(of: ".", with: "")
|
||||||
|
.uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a countdown counts to: the start if it has not begun, otherwise the
|
||||||
|
/// end. Counting up from a start time that has passed reads as broken.
|
||||||
|
static func countdownTarget(_ event: SnapshotEvent, at now: Date) -> Date {
|
||||||
|
event.start > now ? event.start : event.end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A countdown narrow enough for a corner or a circular gauge: at most
|
||||||
|
/// three characters. `Text(style: .timer)` would be live and free of
|
||||||
|
/// timeline entries, but renders `1:23:45` and overlaps whatever sits
|
||||||
|
/// beside it — so the timeline carries minute-granular entries instead and
|
||||||
|
/// this is recomputed per entry.
|
||||||
|
static func compactCountdown(to target: Date, at now: Date, language: String) -> String {
|
||||||
|
let seconds = target.timeIntervalSince(now)
|
||||||
|
guard seconds > 0 else { return WatchL10n.t("watch.now", language) }
|
||||||
|
let minutes = Int(seconds / 60)
|
||||||
|
if minutes < 1 { return WatchL10n.t("watch.now", language) }
|
||||||
|
if minutes < 60 { return "\(minutes)m" }
|
||||||
|
let hours = minutes / 60
|
||||||
|
if hours < 24 { return "\(hours)h" }
|
||||||
|
return "\(hours / 24)d"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How full a draining ring should be, over a two-hour lead window.
|
||||||
|
static func gaugeFraction(to target: Date, at now: Date) -> Double {
|
||||||
|
let window: TimeInterval = 2 * 3600
|
||||||
|
return min(1, max(0, target.timeIntervalSince(now) / window))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much of today is still ahead. Drains through the day, matching the
|
||||||
|
/// countdown ring rather than inventing a second direction.
|
||||||
|
static func dayRemainingFraction(at now: Date,
|
||||||
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Double {
|
||||||
|
let dayStart = calendar.startOfDay(for: now)
|
||||||
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
||||||
|
let total = dayEnd.timeIntervalSince(dayStart)
|
||||||
|
guard total > 0 else { return 0 }
|
||||||
|
return min(1, max(0, dayEnd.timeIntervalSince(now) / total))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The share of today's appointments still ahead. Zero when there are none,
|
||||||
|
/// which leaves the ring empty rather than full — an empty day should not
|
||||||
|
/// look like an untouched one.
|
||||||
|
static func todayRemainingFraction(in snapshot: CalendarrSnapshot?, at now: Date,
|
||||||
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Double {
|
||||||
|
guard let snapshot else { return 0 }
|
||||||
|
let dayStart = calendar.startOfDay(for: now)
|
||||||
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
||||||
|
let today = snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }
|
||||||
|
guard !today.isEmpty else { return 0 }
|
||||||
|
let ahead = today.filter { $0.end > now }.count
|
||||||
|
return min(1, max(0, Double(ahead) / Double(today.count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func time(_ date: Date, language: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.dateFormat = "HH:mm"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func monthAbbreviation(_ date: Date, language: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.dateFormat = "LLL"
|
||||||
|
return formatter.string(from: date).uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The curved label that accompanies a circular or corner complication:
|
||||||
|
/// time plus title, or a plain "no events".
|
||||||
|
///
|
||||||
|
/// On a corner this follows the screen edge. On a face with a bezel slot —
|
||||||
|
/// Infograph — the same label arcs across the top, where the tick marks
|
||||||
|
/// otherwise are. That is the position ClockKit called `graphicBezel`, and
|
||||||
|
/// in WidgetKit it is reached by attaching `.widgetLabel` to an
|
||||||
|
/// `accessoryCircular` widget.
|
||||||
|
static func eventLabel(_ event: SnapshotEvent?, at now: Date, language: String) -> String {
|
||||||
|
guard let event else { return WatchL10n.t("watch.no_events", language) }
|
||||||
|
if event.isAllDay { return event.title }
|
||||||
|
return "\(time(event.start, language: language)) \(event.title)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Foundation
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Which calendars this watch hides, on top of what the phone already filtered.
|
||||||
|
///
|
||||||
|
/// The snapshot arrives with hidden and banished calendars already removed, so
|
||||||
|
/// this narrows further and can never contradict the phone. It lives in the App
|
||||||
|
/// Group so one toggle in the app affects every complication at once.
|
||||||
|
enum WatchFilterStore {
|
||||||
|
private static let key = "watchHiddenCalendarKeys"
|
||||||
|
|
||||||
|
private static var defaults: UserDefaults? {
|
||||||
|
UserDefaults(suiteName: CalendarrAppGroup.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func load() -> Set<String> {
|
||||||
|
Set(defaults?.stringArray(forKey: key) ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func save(_ keys: Set<String>) {
|
||||||
|
defaults?.set(keys.sorted(), forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// German and English strings for the watch app and its complications.
|
||||||
|
///
|
||||||
|
/// A local copy rather than a shared one: `CalendarrCore` is deliberately
|
||||||
|
/// Foundation-only and holds no presentation concerns, and the iOS `WidgetL10n`
|
||||||
|
/// belongs to a target the watch cannot link against.
|
||||||
|
enum WatchL10n {
|
||||||
|
static func t(_ key: String, _ stored: String) -> String {
|
||||||
|
strings[resolve(stored)]?[key] ?? strings["en"]?[key] ?? key
|
||||||
|
}
|
||||||
|
|
||||||
|
static func locale(_ stored: String) -> Locale {
|
||||||
|
Locale(identifier: resolve(stored) == "de" ? "de_DE" : "en_US")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func resolve(_ stored: String) -> String {
|
||||||
|
if stored == "de" || stored == "en" { return stored }
|
||||||
|
let preferred = Locale.preferredLanguages.first ?? "en"
|
||||||
|
return preferred.lowercased().hasPrefix("de") ? "de" : "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let strings: [String: [String: String]] = [
|
||||||
|
"de": [
|
||||||
|
"watch.title": "Calendarr",
|
||||||
|
"watch.calendars": "Kalender",
|
||||||
|
"watch.allday": "Ganztägig",
|
||||||
|
"watch.no_events": "Keine Termine",
|
||||||
|
"watch.now": "jetzt",
|
||||||
|
"watch.today": "Heute",
|
||||||
|
"watch.tomorrow": "Morgen",
|
||||||
|
"watch.events_today": "Termine",
|
||||||
|
"watch.sign_in": "Auf dem iPhone anmelden",
|
||||||
|
"watch.update": "Watch-App aktualisieren",
|
||||||
|
"watch.waiting": "Warte auf Daten vom iPhone",
|
||||||
|
"watch.unreadable": "Daten nicht lesbar",
|
||||||
|
"watch.as_of": "Stand",
|
||||||
|
"watch.no_information": "Keine Daten nach",
|
||||||
|
],
|
||||||
|
"en": [
|
||||||
|
"watch.title": "Calendarr",
|
||||||
|
"watch.calendars": "Calendars",
|
||||||
|
"watch.allday": "All-day",
|
||||||
|
"watch.no_events": "No events",
|
||||||
|
"watch.now": "now",
|
||||||
|
"watch.today": "Today",
|
||||||
|
"watch.tomorrow": "Tomorrow",
|
||||||
|
"watch.events_today": "events",
|
||||||
|
"watch.sign_in": "Sign in on your iPhone",
|
||||||
|
"watch.update": "Update the watch app",
|
||||||
|
"watch.waiting": "Waiting for data from your iPhone",
|
||||||
|
"watch.unreadable": "Data unreadable",
|
||||||
|
"watch.as_of": "As of",
|
||||||
|
"watch.no_information": "No data after",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// CalendarrWatch_Watch_AppTests.swift
|
||||||
|
// CalendarrWatch Watch AppTests
|
||||||
|
//
|
||||||
|
// Created by Guido Schmit on 11.09.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
@testable import CalendarrWatch_Watch_App
|
||||||
|
|
||||||
|
struct CalendarrWatch_Watch_AppTests {
|
||||||
|
|
||||||
|
@Test func example() async throws {
|
||||||
|
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||||
|
// Swift Testing Documentation
|
||||||
|
// https://developer.apple.com/documentation/testing
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// CalendarrWatch_Watch_AppUITests.swift
|
||||||
|
// CalendarrWatch Watch AppUITests
|
||||||
|
//
|
||||||
|
// Created by Guido Schmit on 11.09.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class CalendarrWatch_Watch_AppUITests: XCTestCase {
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
|
||||||
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
|
continueAfterFailure = false
|
||||||
|
|
||||||
|
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testExample() throws {
|
||||||
|
// UI tests must launch the application that they test.
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
|
// XCUIAutomation Documentation
|
||||||
|
// https://developer.apple.com/documentation/xcuiautomation
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testLaunchPerformance() throws {
|
||||||
|
// This measures how long it takes to launch your application.
|
||||||
|
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||||
|
XCUIApplication().launch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Status:** All 17 tasks implemented and build-verified as of 2026-09-11. On-device behaviour is still unverified — see the handoff list in Task 17.
|
||||||
|
|
||||||
**Goal:** Ship a read-only Apple Watch app plus eight watch-face complications that show the next Calendarr appointment, fed from the iPhone over WatchConnectivity.
|
**Goal:** Ship a read-only Apple Watch app plus eight watch-face complications that show the next Calendarr appointment, fed from the iPhone over WatchConnectivity.
|
||||||
|
|
||||||
**Architecture:** The watch is a read-only consumer of the phone's snapshot. Every piece of logic that can be tested lives in `CalendarrCore` (testable with `swift test` on the dev machine); the watch targets hold only thin SwiftUI and WCSession glue, which can only be build-verified here. The phone trims its 42-day snapshot to a 14-day watch payload and sends it; the watch writes it into its *own* App Group container through the same `SnapshotStore`, so there is one wire format and one reader.
|
**Architecture:** The watch is a read-only consumer of the phone's snapshot. Every piece of logic that can be tested lives in `CalendarrCore` (testable with `swift test` on the dev machine); the watch targets hold only thin SwiftUI and WCSession glue, which can only be build-verified here. The phone trims its 42-day snapshot to a 14-day watch payload and sends it; the watch writes it into its *own* App Group container through the same `SnapshotStore`, so there is one wire format and one reader.
|
||||||
@@ -12,12 +14,26 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Note on the source files
|
||||||
|
|
||||||
|
`Calendarr iOS/`, `CalendarrWidgets/` and `Shared/` are
|
||||||
|
`PBXFileSystemSynchronizedRootGroup`s, so **new `.swift` files are picked up
|
||||||
|
automatically** — no `project.pbxproj` surgery to add them. This does not extend
|
||||||
|
to new *targets* (Task 9), and it is why a partial Info.plist has to live
|
||||||
|
outside those folders (Task 7).
|
||||||
|
|
||||||
## Prerequisite (blocks device verification only)
|
## Prerequisite (blocks device verification only)
|
||||||
|
|
||||||
Xcode 26.6 ships the watchOS 26.5 SDK. The target device runs watchOS 27 beta, so builds cannot be installed on it until an Xcode with watchOS 27 support is present. Every task below can be written and build-verified without it. Do not block on this; note it when handing back.
|
Xcode 26.6 ships the watchOS 26.5 SDK. The target device runs watchOS 27 beta, so builds cannot be installed on it until an Xcode with watchOS 27 support is present. Every task below can be written and build-verified without it. Do not block on this; note it when handing back.
|
||||||
|
|
||||||
## Working rules for this repo
|
## Working rules for this repo
|
||||||
|
|
||||||
|
- **Never pass `-sdk` to xcodebuild.** It overrides SDKROOT for every target in
|
||||||
|
the dependency graph, so the watch app gets built against the iOS SDK: the
|
||||||
|
build still succeeds, but `WKApplication` is missing from the result and
|
||||||
|
`DTSDKName` reads `iphonesimulator`. Select the platform with `-destination`
|
||||||
|
alone and let each target keep its own SDK.
|
||||||
|
|
||||||
- Never run or start the server, and never run the apps. Apps are **build-verified only**; behaviour, layout and complication refresh are verified by the user on the device.
|
- Never run or start the server, and never run the apps. Apps are **build-verified only**; behaviour, layout and complication refresh are verified by the user on the device.
|
||||||
- Commit messages in English. Commit after every task.
|
- Commit messages in English. Commit after every task.
|
||||||
- `CalendarrKit` is a **separate git repo** at `../CalendarrKit` with **no remote** — commit there, do not push. The iOS repo (`.`) pushes to `origin main`.
|
- `CalendarrKit` is a **separate git repo** at `../CalendarrKit` with **no remote** — commit there, do not push. The iOS repo (`.`) pushes to `origin main`.
|
||||||
@@ -44,6 +60,7 @@ Xcode 26.6 ships the watchOS 26.5 SDK. The target device runs watchOS 27 beta, s
|
|||||||
|
|
||||||
| File | Responsibility |
|
| File | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| `Calendarr-Info.plist` | Partial Info.plist for the two background-task array keys, merged into the generated one. At the project root, not in the target folder. |
|
||||||
| `Calendarr iOS/Services/BackgroundRefresh.swift` | `BGAppRefreshTask` registration and handling. |
|
| `Calendarr iOS/Services/BackgroundRefresh.swift` | `BGAppRefreshTask` registration and handling. |
|
||||||
| `Calendarr iOS/Services/WatchSyncService.swift` | WCSession sender on the phone. |
|
| `Calendarr iOS/Services/WatchSyncService.swift` | WCSession sender on the phone. |
|
||||||
| `CalendarrWatch/CalendarrWatchApp.swift` | watchOS app entry point. |
|
| `CalendarrWatch/CalendarrWatchApp.swift` | watchOS app entry point. |
|
||||||
@@ -500,7 +517,7 @@ import CalendarrCore
|
|||||||
- [ ] **Step 6: Build the iOS app to verify the adoption compiles**
|
- [ ] **Step 6: Build the iOS app to verify the adoption compiles**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `** BUILD SUCCEEDED **`.
|
Expected: `** BUILD SUCCEEDED **`.
|
||||||
@@ -1114,7 +1131,8 @@ Independently valuable: the home-screen widgets are currently only as fresh as t
|
|||||||
**Files:**
|
**Files:**
|
||||||
- Create: `Calendarr iOS/Services/BackgroundRefresh.swift`
|
- Create: `Calendarr iOS/Services/BackgroundRefresh.swift`
|
||||||
- Modify: `Calendarr iOS/CalendarrApp.swift:5-13`
|
- Modify: `Calendarr iOS/CalendarrApp.swift:5-13`
|
||||||
- Modify: `Calendarr iOS.xcodeproj/project.pbxproj` (two Info.plist build settings, both configurations)
|
- Create: `Calendarr-Info.plist` (project root)
|
||||||
|
- Modify: `Calendarr iOS.xcodeproj/project.pbxproj` (`INFOPLIST_FILE`, both configurations)
|
||||||
|
|
||||||
- [ ] **Step 1: Write the refresh service**
|
- [ ] **Step 1: Write the refresh service**
|
||||||
|
|
||||||
@@ -1230,37 +1248,51 @@ struct CalendarrApp: App {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Add the Info.plist keys**
|
- [x] **Step 3: Add the Info.plist keys**
|
||||||
|
|
||||||
In Xcode, select the **Calendarr iOS** target → Build Settings, and add to **both** Debug and Release:
|
Both keys are arrays, and Xcode does **not** expose either as an
|
||||||
|
`INFOPLIST_KEY_` build setting — verified by setting them, building, and finding
|
||||||
|
them absent from the result while `INFOPLIST_KEY_UIApplicationSupportsIndirect\
|
||||||
|
InputEvents` did land. Use a partial Info.plist instead; generation stays on and
|
||||||
|
merges into it.
|
||||||
|
|
||||||
- `INFOPLIST_KEY_UIBackgroundModes` = `fetch`
|
Create `Calendarr-Info.plist` at the **project root**:
|
||||||
- `INFOPLIST_KEY_BGTaskSchedulerPermittedIdentifiers` = `com.scarriffleservices.calendarr.ios.refresh`
|
|
||||||
|
|
||||||
- [ ] **Step 4: Build and verify the keys reached the built Info.plist**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/calendarr-dd CODE_SIGNING_ALLOWED=NO build
|
|
||||||
plutil -p "/tmp/calendarr-dd/Build/Products/Debug-iphonesimulator/Calendarr iOS.app/Info.plist" | grep -A3 -i "BGTaskScheduler\|UIBackgroundModes"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: `** BUILD SUCCEEDED **`, and the output shows both `UIBackgroundModes` containing `fetch` and `BGTaskSchedulerPermittedIdentifiers` containing the identifier.
|
|
||||||
|
|
||||||
If `INFOPLIST_KEY_BGTaskSchedulerPermittedIdentifiers` does not appear, the build setting is not supported by this Xcode. In that case create `Calendarr iOS/Info.plist` containing only the two keys, set `INFOPLIST_FILE = Calendarr iOS/Info.plist` and `GENERATE_INFOPLIST_FILE = NO` on the target, and re-run this step:
|
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array><string>fetch</string></array>
|
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array><string>com.scarriffleservices.calendarr.ios.refresh</string></array>
|
<array>
|
||||||
|
<string>com.scarriffleservices.calendarr.ios.refresh</string>
|
||||||
|
</array>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then set `INFOPLIST_FILE = "Calendarr-Info.plist"` on the **Calendarr iOS**
|
||||||
|
target in both configurations, leaving `GENERATE_INFOPLIST_FILE = YES`.
|
||||||
|
|
||||||
|
The root location is load-bearing: `Calendarr iOS/` is a
|
||||||
|
`PBXFileSystemSynchronizedRootGroup`, so a plist inside it is also copied as a
|
||||||
|
resource and the build fails with "Multiple commands produce .../Info.plist".
|
||||||
|
|
||||||
|
- [x] **Step 4: Build and verify the merge lost nothing**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/calendarr-dd CODE_SIGNING_ALLOWED=NO build
|
||||||
|
plutil -p "/tmp/calendarr-dd/Build/Products/Release-iphonesimulator/Calendarr iOS.app/Info.plist" | grep -A3 -i "BGTaskScheduler\|UIBackgroundModes"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the scheme builds **Release**, not Debug. Expected: `** BUILD SUCCEEDED **`
|
||||||
|
and both keys present. Diff the whole plist against a build without the change
|
||||||
|
to confirm no generated key was displaced.
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1505,7 +1537,7 @@ In `Calendarr iOS/CalendarrApp.swift`, in `AppState.logout()`, after the existin
|
|||||||
- [ ] **Step 5: Build the iOS app**
|
- [ ] **Step 5: Build the iOS app**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `** BUILD SUCCEEDED **`.
|
Expected: `** BUILD SUCCEEDED **`.
|
||||||
@@ -1591,7 +1623,7 @@ For both new bundle IDs, enable the **App Groups** capability and select `group.
|
|||||||
- [ ] **Step 6: Verify both targets build**
|
- [ ] **Step 6: Verify both targets build**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme CalendarrWatch -sdk watchsimulator -destination 'generic/platform=watchOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `** BUILD SUCCEEDED **` (an empty app at this point).
|
Expected: `** BUILD SUCCEEDED **` (an empty app at this point).
|
||||||
@@ -2325,7 +2357,7 @@ struct WatchCalendarFilterView: View {
|
|||||||
- [ ] **Step 2: Build the watch app**
|
- [ ] **Step 2: Build the watch app**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme CalendarrWatch -sdk watchsimulator -destination 'generic/platform=watchOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `** BUILD SUCCEEDED **`.
|
Expected: `** BUILD SUCCEEDED **`.
|
||||||
@@ -2988,7 +3020,7 @@ struct WatchDatePlusEventWidget: Widget {
|
|||||||
- [ ] **Step 5: Build the complications target**
|
- [ ] **Step 5: Build the complications target**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme CalendarrWatch -sdk watchsimulator -destination 'generic/platform=watchOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `** BUILD SUCCEEDED **` — the scheme builds the embedded extension too.
|
Expected: `** BUILD SUCCEEDED **` — the scheme builds the embedded extension too.
|
||||||
@@ -3022,8 +3054,8 @@ Expected: build succeeds against the native macOS SDK, every test passes.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd "../Calendarr iOS"
|
cd "../Calendarr iOS"
|
||||||
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
||||||
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme CalendarrWatch -sdk watchsimulator -destination 'generic/platform=watchOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
xcodebuild -project "Calendarr iOS.xcodeproj" -scheme "Calendarr iOS" -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `** BUILD SUCCEEDED **` twice.
|
Expected: `** BUILD SUCCEEDED **` twice.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Apple Watch app and watch-face complications
|
# Apple Watch app and watch-face complications
|
||||||
|
|
||||||
Date: 2026-09-11
|
Date: 2026-09-11
|
||||||
Status: approved — ready for implementation planning
|
Status: implemented — pending on-device verification
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user