First version
@ -8,18 +8,69 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
E267D1742A8E0DE80069112B /* ResumeBuilderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1732A8E0DE80069112B /* ResumeBuilderApp.swift */; };
|
E267D1742A8E0DE80069112B /* ResumeBuilderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1732A8E0DE80069112B /* ResumeBuilderApp.swift */; };
|
||||||
E267D1762A8E0DE80069112B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1752A8E0DE80069112B /* ContentView.swift */; };
|
E267D1762A8E0DE80069112B /* CV.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1752A8E0DE80069112B /* CV.swift */; };
|
||||||
E267D1782A8E0DE90069112B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E267D1772A8E0DE90069112B /* Assets.xcassets */; };
|
E267D1782A8E0DE90069112B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E267D1772A8E0DE90069112B /* Assets.xcassets */; };
|
||||||
E267D17B2A8E0DE90069112B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E267D17A2A8E0DE90069112B /* Preview Assets.xcassets */; };
|
E267D17B2A8E0DE90069112B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E267D17A2A8E0DE90069112B /* Preview Assets.xcassets */; };
|
||||||
|
E267D1832A8E0F320069112B /* Color+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1822A8E0F320069112B /* Color+Extension.swift */; };
|
||||||
|
E267D1852A8E11930069112B /* TopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1842A8E11930069112B /* TopView.swift */; };
|
||||||
|
E267D1882A8E12D60069112B /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E267D1872A8E12D60069112B /* SFSafeSymbols */; };
|
||||||
|
E267D18A2A8E140E0069112B /* LeftImageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1892A8E140E0069112B /* LeftImageLabel.swift */; };
|
||||||
|
E267D18C2A8E1A890069112B /* TopViewImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D18B2A8E1A890069112B /* TopViewImage.swift */; };
|
||||||
|
E267D18E2A8E1BEE0069112B /* RightImageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D18D2A8E1BEE0069112B /* RightImageLabel.swift */; };
|
||||||
|
E267D1902A8E32B70069112B /* CareerStationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D18F2A8E32B70069112B /* CareerStationView.swift */; };
|
||||||
|
E267D1922A8E347A0069112B /* TitledCareerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1912A8E347A0069112B /* TitledCareerSection.swift */; };
|
||||||
|
E267D1942A8E38C60069112B /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1932A8E38C60069112B /* Data.swift */; };
|
||||||
|
E267D1962A8E3E760069112B /* TitledTextSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1952A8E3E760069112B /* TitledTextSection.swift */; };
|
||||||
|
E267D1982A8E43580069112B /* TitledSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1972A8E43580069112B /* TitledSection.swift */; };
|
||||||
|
E267D19A2A8E44F40069112B /* TitledIconSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1992A8E44F40069112B /* TitledIconSection.swift */; };
|
||||||
|
E267D19C2A8E45470069112B /* CareerStation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D19B2A8E45470069112B /* CareerStation.swift */; };
|
||||||
|
E267D19E2A8E45540069112B /* TopInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D19D2A8E45540069112B /* TopInfo.swift */; };
|
||||||
|
E267D1A02A8E45620069112B /* CVInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D19F2A8E45620069112B /* CVInfo.swift */; };
|
||||||
|
E267D1A22A8E45AE0069112B /* SkillsSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1A12A8E45AE0069112B /* SkillsSet.swift */; };
|
||||||
|
E267D1A72A8ECC170069112B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1A62A8ECC170069112B /* ContentView.swift */; };
|
||||||
|
E267D1A92A8F5B430069112B /* FlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1A82A8F5B430069112B /* FlowLayout.swift */; };
|
||||||
|
E267D1AD2A8F694A0069112B /* PublicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1AC2A8F694A0069112B /* PublicationView.swift */; };
|
||||||
|
E267D1AF2A8F69830069112B /* Publication.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1AE2A8F69830069112B /* Publication.swift */; };
|
||||||
|
E267D1B42A8F7FEE0069112B /* LeftBorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1B32A8F7FEE0069112B /* LeftBorderView.swift */; };
|
||||||
|
E267D1B62A8F96310069112B /* CVStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1B52A8F96310069112B /* CVStyle.swift */; };
|
||||||
|
E267D1B82A8F9A2A0069112B /* HeaderStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1B72A8F9A2A0069112B /* HeaderStyle.swift */; };
|
||||||
|
E267D1BA2A8F9D9C0069112B /* SkillStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1B92A8F9D9C0069112B /* SkillStyle.swift */; };
|
||||||
|
E267D1BC2A8FFF300069112B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1BB2A8FFF300069112B /* TagView.swift */; };
|
||||||
|
E267D1C02A9009780069112B /* CVLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E267D1BF2A9009780069112B /* CVLanguage.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
E267D1702A8E0DE80069112B /* ResumeBuilder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ResumeBuilder.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
E267D1702A8E0DE80069112B /* ResumeBuilder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ResumeBuilder.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
E267D1732A8E0DE80069112B /* ResumeBuilderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumeBuilderApp.swift; sourceTree = "<group>"; };
|
E267D1732A8E0DE80069112B /* ResumeBuilderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumeBuilderApp.swift; sourceTree = "<group>"; };
|
||||||
E267D1752A8E0DE80069112B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
E267D1752A8E0DE80069112B /* CV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CV.swift; sourceTree = "<group>"; };
|
||||||
E267D1772A8E0DE90069112B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
E267D1772A8E0DE90069112B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
E267D17A2A8E0DE90069112B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
E267D17A2A8E0DE90069112B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
E267D17C2A8E0DE90069112B /* ResumeBuilder.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ResumeBuilder.entitlements; sourceTree = "<group>"; };
|
E267D17C2A8E0DE90069112B /* ResumeBuilder.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ResumeBuilder.entitlements; sourceTree = "<group>"; };
|
||||||
|
E267D1822A8E0F320069112B /* Color+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extension.swift"; sourceTree = "<group>"; };
|
||||||
|
E267D1842A8E11930069112B /* TopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopView.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1892A8E140E0069112B /* LeftImageLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftImageLabel.swift; sourceTree = "<group>"; };
|
||||||
|
E267D18B2A8E1A890069112B /* TopViewImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopViewImage.swift; sourceTree = "<group>"; };
|
||||||
|
E267D18D2A8E1BEE0069112B /* RightImageLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightImageLabel.swift; sourceTree = "<group>"; };
|
||||||
|
E267D18F2A8E32B70069112B /* CareerStationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CareerStationView.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1912A8E347A0069112B /* TitledCareerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledCareerSection.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1932A8E38C60069112B /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1952A8E3E760069112B /* TitledTextSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledTextSection.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1972A8E43580069112B /* TitledSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledSection.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1992A8E44F40069112B /* TitledIconSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledIconSection.swift; sourceTree = "<group>"; };
|
||||||
|
E267D19B2A8E45470069112B /* CareerStation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CareerStation.swift; sourceTree = "<group>"; };
|
||||||
|
E267D19D2A8E45540069112B /* TopInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopInfo.swift; sourceTree = "<group>"; };
|
||||||
|
E267D19F2A8E45620069112B /* CVInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVInfo.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1A12A8E45AE0069112B /* SkillsSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkillsSet.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1A62A8ECC170069112B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1A82A8F5B430069112B /* FlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowLayout.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1AC2A8F694A0069112B /* PublicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationView.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1AE2A8F69830069112B /* Publication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publication.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1B32A8F7FEE0069112B /* LeftBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftBorderView.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1B52A8F96310069112B /* CVStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVStyle.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1B72A8F9A2A0069112B /* HeaderStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderStyle.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1B92A8F9D9C0069112B /* SkillStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkillStyle.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1BB2A8FFF300069112B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||||
|
E267D1BF2A9009780069112B /* CVLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVLanguage.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -27,6 +78,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E267D1882A8E12D60069112B /* SFSafeSymbols in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -53,10 +105,19 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E267D1732A8E0DE80069112B /* ResumeBuilderApp.swift */,
|
E267D1732A8E0DE80069112B /* ResumeBuilderApp.swift */,
|
||||||
E267D1752A8E0DE80069112B /* ContentView.swift */,
|
E267D1A62A8ECC170069112B /* ContentView.swift */,
|
||||||
|
E267D1752A8E0DE80069112B /* CV.swift */,
|
||||||
|
E267D1B02A8F6E5B0069112B /* Main Elements */,
|
||||||
|
E267D1B22A8F6E9C0069112B /* Elements */,
|
||||||
|
E267D1B12A8F6E7F0069112B /* Generic Elements */,
|
||||||
E267D1772A8E0DE90069112B /* Assets.xcassets */,
|
E267D1772A8E0DE90069112B /* Assets.xcassets */,
|
||||||
E267D17C2A8E0DE90069112B /* ResumeBuilder.entitlements */,
|
E267D17C2A8E0DE90069112B /* ResumeBuilder.entitlements */,
|
||||||
E267D1792A8E0DE90069112B /* Preview Content */,
|
E267D1792A8E0DE90069112B /* Preview Content */,
|
||||||
|
E267D1822A8E0F320069112B /* Color+Extension.swift */,
|
||||||
|
E267D1932A8E38C60069112B /* Data.swift */,
|
||||||
|
E267D1A52A8EC34B0069112B /* Data */,
|
||||||
|
E267D1BE2A90095D0069112B /* Language */,
|
||||||
|
E267D1BD2A9009390069112B /* Style */,
|
||||||
);
|
);
|
||||||
path = ResumeBuilder;
|
path = ResumeBuilder;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -69,6 +130,70 @@
|
|||||||
path = "Preview Content";
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E267D1A52A8EC34B0069112B /* Data */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E267D19F2A8E45620069112B /* CVInfo.swift */,
|
||||||
|
E267D19B2A8E45470069112B /* CareerStation.swift */,
|
||||||
|
E267D1A12A8E45AE0069112B /* SkillsSet.swift */,
|
||||||
|
E267D19D2A8E45540069112B /* TopInfo.swift */,
|
||||||
|
E267D1AE2A8F69830069112B /* Publication.swift */,
|
||||||
|
);
|
||||||
|
path = Data;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E267D1B02A8F6E5B0069112B /* Main Elements */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E267D1842A8E11930069112B /* TopView.swift */,
|
||||||
|
E267D1912A8E347A0069112B /* TitledCareerSection.swift */,
|
||||||
|
E267D1992A8E44F40069112B /* TitledIconSection.swift */,
|
||||||
|
E267D1952A8E3E760069112B /* TitledTextSection.swift */,
|
||||||
|
);
|
||||||
|
path = "Main Elements";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E267D1B12A8F6E7F0069112B /* Generic Elements */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E267D1892A8E140E0069112B /* LeftImageLabel.swift */,
|
||||||
|
E267D18D2A8E1BEE0069112B /* RightImageLabel.swift */,
|
||||||
|
E267D1A82A8F5B430069112B /* FlowLayout.swift */,
|
||||||
|
E267D1972A8E43580069112B /* TitledSection.swift */,
|
||||||
|
E267D1B32A8F7FEE0069112B /* LeftBorderView.swift */,
|
||||||
|
E267D1BB2A8FFF300069112B /* TagView.swift */,
|
||||||
|
);
|
||||||
|
path = "Generic Elements";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E267D1B22A8F6E9C0069112B /* Elements */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E267D18B2A8E1A890069112B /* TopViewImage.swift */,
|
||||||
|
E267D1AC2A8F694A0069112B /* PublicationView.swift */,
|
||||||
|
E267D18F2A8E32B70069112B /* CareerStationView.swift */,
|
||||||
|
);
|
||||||
|
path = Elements;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E267D1BD2A9009390069112B /* Style */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E267D1B52A8F96310069112B /* CVStyle.swift */,
|
||||||
|
E267D1B72A8F9A2A0069112B /* HeaderStyle.swift */,
|
||||||
|
E267D1B92A8F9D9C0069112B /* SkillStyle.swift */,
|
||||||
|
);
|
||||||
|
path = Style;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E267D1BE2A90095D0069112B /* Language */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E267D1BF2A9009780069112B /* CVLanguage.swift */,
|
||||||
|
);
|
||||||
|
path = Language;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -85,6 +210,9 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = ResumeBuilder;
|
name = ResumeBuilder;
|
||||||
|
packageProductDependencies = (
|
||||||
|
E267D1872A8E12D60069112B /* SFSafeSymbols */,
|
||||||
|
);
|
||||||
productName = ResumeBuilder;
|
productName = ResumeBuilder;
|
||||||
productReference = E267D1702A8E0DE80069112B /* ResumeBuilder.app */;
|
productReference = E267D1702A8E0DE80069112B /* ResumeBuilder.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@ -113,6 +241,9 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = E267D1672A8E0DE80069112B;
|
mainGroup = E267D1672A8E0DE80069112B;
|
||||||
|
packageReferences = (
|
||||||
|
E267D1862A8E12D60069112B /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||||
|
);
|
||||||
productRefGroup = E267D1712A8E0DE80069112B /* Products */;
|
productRefGroup = E267D1712A8E0DE80069112B /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@ -139,7 +270,32 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E267D1762A8E0DE80069112B /* ContentView.swift in Sources */,
|
E267D1902A8E32B70069112B /* CareerStationView.swift in Sources */,
|
||||||
|
E267D1982A8E43580069112B /* TitledSection.swift in Sources */,
|
||||||
|
E267D1942A8E38C60069112B /* Data.swift in Sources */,
|
||||||
|
E267D19E2A8E45540069112B /* TopInfo.swift in Sources */,
|
||||||
|
E267D1922A8E347A0069112B /* TitledCareerSection.swift in Sources */,
|
||||||
|
E267D1B42A8F7FEE0069112B /* LeftBorderView.swift in Sources */,
|
||||||
|
E267D1B62A8F96310069112B /* CVStyle.swift in Sources */,
|
||||||
|
E267D1C02A9009780069112B /* CVLanguage.swift in Sources */,
|
||||||
|
E267D1762A8E0DE80069112B /* CV.swift in Sources */,
|
||||||
|
E267D1A22A8E45AE0069112B /* SkillsSet.swift in Sources */,
|
||||||
|
E267D1A72A8ECC170069112B /* ContentView.swift in Sources */,
|
||||||
|
E267D18E2A8E1BEE0069112B /* RightImageLabel.swift in Sources */,
|
||||||
|
E267D1962A8E3E760069112B /* TitledTextSection.swift in Sources */,
|
||||||
|
E267D1B82A8F9A2A0069112B /* HeaderStyle.swift in Sources */,
|
||||||
|
E267D18A2A8E140E0069112B /* LeftImageLabel.swift in Sources */,
|
||||||
|
E267D19A2A8E44F40069112B /* TitledIconSection.swift in Sources */,
|
||||||
|
E267D18C2A8E1A890069112B /* TopViewImage.swift in Sources */,
|
||||||
|
E267D19C2A8E45470069112B /* CareerStation.swift in Sources */,
|
||||||
|
E267D1BA2A8F9D9C0069112B /* SkillStyle.swift in Sources */,
|
||||||
|
E267D1AD2A8F694A0069112B /* PublicationView.swift in Sources */,
|
||||||
|
E267D1BC2A8FFF300069112B /* TagView.swift in Sources */,
|
||||||
|
E267D1A02A8E45620069112B /* CVInfo.swift in Sources */,
|
||||||
|
E267D1832A8E0F320069112B /* Color+Extension.swift in Sources */,
|
||||||
|
E267D1AF2A8F69830069112B /* Publication.swift in Sources */,
|
||||||
|
E267D1852A8E11930069112B /* TopView.swift in Sources */,
|
||||||
|
E267D1A92A8F5B430069112B /* FlowLayout.swift in Sources */,
|
||||||
E267D1742A8E0DE80069112B /* ResumeBuilderApp.swift in Sources */,
|
E267D1742A8E0DE80069112B /* ResumeBuilderApp.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -274,6 +430,8 @@
|
|||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = CHResume;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -301,6 +459,8 @@
|
|||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = CHResume;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@ -336,6 +496,25 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
E267D1862A8E12D60069112B /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 4.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
E267D1872A8E12D60069112B /* SFSafeSymbols */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E267D1862A8E12D60069112B /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||||
|
productName = SFSafeSymbols;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = E267D1682A8E0DE80069112B /* Project object */;
|
rootObject = E267D1682A8E0DE80069112B /* Project object */;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "sfsafesymbols",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
|
||||||
|
"version" : "4.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
@ -1,6 +1,33 @@
|
|||||||
{
|
{
|
||||||
"colors" : [
|
"colors" : [
|
||||||
{
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0",
|
||||||
|
"green" : "144",
|
||||||
|
"red" : "244"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0",
|
||||||
|
"green" : "144",
|
||||||
|
"red" : "244"
|
||||||
|
}
|
||||||
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1,51 +1,61 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 9.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "16x16"
|
"size" : "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 8.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "16x16"
|
"size" : "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 7.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "32x32"
|
"size" : "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 6.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "32x32"
|
"size" : "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 5.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 4.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 3.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 2.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon 1.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "512x512"
|
"size" : "512x512"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"filename" : "icon.jpg",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "512x512"
|
"size" : "512x512"
|
||||||
|
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 1.jpg
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 2.jpg
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 3.jpg
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 4.jpg
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 5.jpg
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 6.jpg
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 7.jpg
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 8.jpg
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 9.jpg
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon.jpg
Normal file
After Width: | Height: | Size: 377 KiB |
21
ResumeBuilder/Assets.xcassets/Cover.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "photo.jpg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
ResumeBuilder/Assets.xcassets/Cover.imageset/photo.jpg
vendored
Normal file
After Width: | Height: | Size: 2.5 MiB |
52
ResumeBuilder/Assets.xcassets/Github.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "github.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "github 1.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
ResumeBuilder/Assets.xcassets/Github.imageset/github 1.png
vendored
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
ResumeBuilder/Assets.xcassets/Github.imageset/github.png
vendored
Normal file
After Width: | Height: | Size: 23 KiB |
76
ResumeBuilder/CV.swift
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CV: View {
|
||||||
|
|
||||||
|
let info: CVInfo
|
||||||
|
|
||||||
|
let style: CVStyle
|
||||||
|
|
||||||
|
private var twoColumnSpacing: CGFloat {
|
||||||
|
style.columnSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TopView(info: info.top, style: style.header)
|
||||||
|
.frame(height: style.header.height)
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.accentColor)
|
||||||
|
.frame(height: style.header.lineWidth)
|
||||||
|
GeometryReader { geo in
|
||||||
|
let columnWidth = max(0, (geo.size.width - twoColumnSpacing)) / 2
|
||||||
|
HStack(alignment: .top, spacing: twoColumnSpacing) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TitledCareerSection(
|
||||||
|
style: style.section,
|
||||||
|
content: info.work)
|
||||||
|
TitledCareerSection(
|
||||||
|
style: style.section,
|
||||||
|
content: info.education)
|
||||||
|
}.frame(width: columnWidth)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TitledSection(
|
||||||
|
title: info.publications.title,
|
||||||
|
spacing: style.section.titleSpacing) {
|
||||||
|
ForEach(info.publications.items) { item in
|
||||||
|
PublicationView(
|
||||||
|
info: item,
|
||||||
|
borderSpacing: style.section.borderSpacing,
|
||||||
|
borderWidth: style.section.borderWidth)
|
||||||
|
.padding(.bottom, style.section.bottomSpacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TitledIconSection(
|
||||||
|
content: info.skills,
|
||||||
|
titleSpacing: style.section.titleSpacing,
|
||||||
|
width: columnWidth,
|
||||||
|
style: style.skillStyle)
|
||||||
|
TitledTextSection(
|
||||||
|
content: info.about,
|
||||||
|
titleSpacing: style.section.titleSpacing,
|
||||||
|
paragraphSpacing: style.section.paragraphSpacing)
|
||||||
|
}.frame(width: columnWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
ForEach(info.footer) { text in
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.aspectRatio(1 / sqrt(2), contentMode: .fit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CV_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
CV(info: cvInfo, style: cvStyle)
|
||||||
|
.previewLayout(.fixed(width: 600, height: 600 * sqrt(2)))
|
||||||
|
}
|
||||||
|
}
|
16
ResumeBuilder/Color+Extension.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
|
||||||
|
init(_ r: Int, _ g: Int, _ b: Int) {
|
||||||
|
self.init(Double(r) / 255, Double(g) / 255, Double(b) / 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(r: Int, g: Int, b: Int) {
|
||||||
|
self.init(Double(r) / 255, Double(g) / 255, Double(b) / 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ r: Double, _ g: Double, _ b: Double) {
|
||||||
|
self.init(red: r, green: g, blue: b)
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +1,107 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// ResumeBuilder
|
|
||||||
//
|
|
||||||
// Created by CH on 17.08.23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
|
let info: CVInfo
|
||||||
|
|
||||||
|
let style: CVStyle
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(alignment: .leading) {
|
||||||
Image(systemName: "globe")
|
HStack {
|
||||||
.imageScale(.large)
|
Button(action: createAndSavePDF) {
|
||||||
.foregroundColor(.accentColor)
|
Label("Save", systemSymbol: .squareAndArrowUp)
|
||||||
Text("Hello, world!")
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
CV(info: info, style: style)
|
||||||
|
}.frame(width: style.pageWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createAndSavePDF() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let pdfURL = renderPDF() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let url = showSavePanel() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writePDF(at: pdfURL, to: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showSavePanel() -> URL? {
|
||||||
|
let savePanel = NSSavePanel()
|
||||||
|
savePanel.allowedContentTypes = [.pdf]
|
||||||
|
savePanel.canCreateDirectories = true
|
||||||
|
savePanel.isExtensionHidden = false
|
||||||
|
savePanel.allowsOtherFileTypes = false
|
||||||
|
savePanel.title = "Save PDF"
|
||||||
|
savePanel.message = "Choose a location to save a PDF of the resume"
|
||||||
|
savePanel.nameFieldLabel = "File name:"
|
||||||
|
savePanel.nameFieldStringValue = "CV.pdf"
|
||||||
|
|
||||||
|
let response = savePanel.runModal()
|
||||||
|
guard response == .OK else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return savePanel.url
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writePDF(at source: URL, to destination: URL) {
|
||||||
|
do {
|
||||||
|
if FileManager.default.fileExists(atPath: destination.path) {
|
||||||
|
try FileManager.default.removeItem(at: destination)
|
||||||
|
}
|
||||||
|
try FileManager.default.copyItem(at: source, to: destination)
|
||||||
|
} catch {
|
||||||
|
print("Failed to save pdf: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var content: some View {
|
||||||
|
CV(info: info, style: style)
|
||||||
|
.frame(width: style.pageWidth, height: style.pageHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func renderPDF() -> URL? {
|
||||||
|
let pdfURL = URL.documentsDirectory.appending(path: "cv.pdf")
|
||||||
|
let renderer = ImageRenderer(content: content)
|
||||||
|
|
||||||
|
var didFinish = false
|
||||||
|
renderer.render { size, context in
|
||||||
|
var box = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||||
|
|
||||||
|
guard let pdf = CGContext(pdfURL as CFURL, mediaBox: &box, nil) else {
|
||||||
|
print("Failed to create CGContext")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let options: [CFString: Any] = [
|
||||||
|
kCGPDFContextMediaBox: CGRect(origin: .zero, size: size)
|
||||||
|
]
|
||||||
|
|
||||||
|
pdf.beginPDFPage(options as CFDictionary)
|
||||||
|
context(pdf)
|
||||||
|
pdf.endPDFPage()
|
||||||
|
pdf.closePDF()
|
||||||
|
didFinish = true
|
||||||
|
}
|
||||||
|
guard didFinish else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
print("PDF created")
|
||||||
|
return pdfURL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView()
|
ContentView(info: cvInfo, style: cvStyle)
|
||||||
|
.frame(width: 600, height: 600 * sqrt(2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
120
ResumeBuilder/Data.swift
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
let cvStyle = CVStyle(
|
||||||
|
pageWidth: 600,
|
||||||
|
header: HeaderStyle(
|
||||||
|
height: 100,
|
||||||
|
lineWidth: 1,
|
||||||
|
iconHeight: 20,
|
||||||
|
imageShadowSize: 5,
|
||||||
|
imageBorderWidth: 2),
|
||||||
|
columnSpacing: 10,
|
||||||
|
section: .init(
|
||||||
|
titleSpacing: 10,
|
||||||
|
borderSpacing: 5,
|
||||||
|
borderWidth: 3,
|
||||||
|
bottomSpacing: 10,
|
||||||
|
paragraphSpacing: 5),
|
||||||
|
skillStyle: SkillStyle(
|
||||||
|
iconSize: 20,
|
||||||
|
rowSpacing: 3,
|
||||||
|
verticalTagSpacing: 3,
|
||||||
|
horizontalGap: 5,
|
||||||
|
tagBackground: .gray.opacity(0.1),
|
||||||
|
tagRounding: 8)
|
||||||
|
)
|
||||||
|
|
||||||
|
let cvInfo = CVInfo(
|
||||||
|
top: TopInfo(
|
||||||
|
imageName: "Cover",
|
||||||
|
name: "Christoph Hagen",
|
||||||
|
tagLine: "Problem solver and creative mind with a favour for interdisciplinary work.",
|
||||||
|
place: "Würzburg, Germany",
|
||||||
|
ageText: "Age 32",
|
||||||
|
web: "christophhagen.de",
|
||||||
|
email: "jobs@christophhagen.de",
|
||||||
|
phone: "Upon Request",
|
||||||
|
github: "github.com/christophhagen"),
|
||||||
|
work: .init(title: "Work experience", items: [
|
||||||
|
CareerStation(
|
||||||
|
time: "Jul 2020 - Jul 2023",
|
||||||
|
location: "Braunschweig, Germany",
|
||||||
|
title: "German Aerospace Center",
|
||||||
|
subtitle: "Systems engineer",
|
||||||
|
text: "Responsible for aircraft systems and avionics of a high-altitude solar drone, safety, and software."),
|
||||||
|
CareerStation(
|
||||||
|
time: "Mar 2018 - Dec 2019",
|
||||||
|
location: "Würzburg, Germany",
|
||||||
|
title: "Julius-Maximilians-Universität",
|
||||||
|
subtitle: "Research Assistant",
|
||||||
|
text: "Working on privacy and security technologies in the Secure Software Systems group."),
|
||||||
|
CareerStation(
|
||||||
|
time: "Jul 2017 - Oct 2017",
|
||||||
|
location: "Tokyo, Japan",
|
||||||
|
title: "National Institute of Informatics",
|
||||||
|
subtitle: "Research Intern (Intelligent Robotics)",
|
||||||
|
text: "Topic: Concept Acquisition through interactions between Humans and Robots"),
|
||||||
|
CareerStation(
|
||||||
|
time: "Sep 2014 - Nov 2016",
|
||||||
|
location: "Würzburg, Germany",
|
||||||
|
title: "Julius-Maximilians-Universität",
|
||||||
|
subtitle: "Research & Teaching Assistant",
|
||||||
|
text: "Teaching exercises and robotics workshops, design of a modular robot arm connector.")
|
||||||
|
]),
|
||||||
|
education: .init(title: "Education", items: [
|
||||||
|
CareerStation(
|
||||||
|
time: "Oct 2015 - Sep 2017",
|
||||||
|
location: "Kiruna, Sweden",
|
||||||
|
title: "Luleå University of Technology",
|
||||||
|
subtitle: "M. Sc. in Space Technology",
|
||||||
|
text: "Erasmus Mundus Double Degree Master with courses on robotics, satellite design and control, atmosphere and space physics."),
|
||||||
|
CareerStation(
|
||||||
|
time: "Oct 2015 - Sep 2017",
|
||||||
|
location: "Espoo, Finland",
|
||||||
|
title: "Aalto University of Electrical Engineering",
|
||||||
|
subtitle: "M. Sc. in Space Robotics and Automation",
|
||||||
|
text: "Thesis topic: A Bluetooth based intra-satellite communication system"),
|
||||||
|
CareerStation(
|
||||||
|
time: "Oct 2013 - Aug 2015",
|
||||||
|
location: "Würzburg, Germany",
|
||||||
|
title: "Julius-Maximilians-Universität",
|
||||||
|
subtitle: "B. Sc. in Aerospace Computer Science",
|
||||||
|
text: "Mobile robotics, satellite subsystems, real-time systems, mathematics and physics.")
|
||||||
|
]),
|
||||||
|
publications: .init(title: "Publications", items: [
|
||||||
|
Publication(
|
||||||
|
venue: "33rd Anual INCOSE International Symposium 2023",
|
||||||
|
title: "Model Based Verification and Validation Planning for a Solar Powered High-Altitude Platform"),
|
||||||
|
Publication(
|
||||||
|
venue: "ACM Transactions on Privacy and Security 2022",
|
||||||
|
title: "Contact Discovery in Mobile Messengers: Low-cost Attacks, Quantitative Analyses, and Efficient Mitigations"),
|
||||||
|
Publication(
|
||||||
|
venue: "Network and Distributed Systems Symposium 2021",
|
||||||
|
title: "All the Numbers are US: Large-scale Abuse of Contact Discovery in Mobile Messengers")
|
||||||
|
]),
|
||||||
|
skills: .init(title: "Skills", items: [
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .characterBubble,
|
||||||
|
entries: ["German", "English"]),
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .keyboard,
|
||||||
|
entries: ["Swift", "C", "C++", "Python"]),
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .display2,
|
||||||
|
entries: ["iOS", "Embedded", "macOS", "Linux"]),
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .theatermaskAndPaintbrush,
|
||||||
|
entries: ["UI design", "CAD", "Woodworking", "Electronics", "Photo/Video editing"]),
|
||||||
|
SkillsSet(
|
||||||
|
systemSymbol: .personFillCheckmark,
|
||||||
|
entries: ["Problem solving", "Decision making", "Analytical thinking", "Optimizing"])
|
||||||
|
]),
|
||||||
|
about: .init(title: "About", items: [
|
||||||
|
"I'm interested in acquiring knowledge and new skills, developing cutting-edge technologies, and finding efficient solutions.",
|
||||||
|
"I usually work on various creative projects, including woodworking, electronics, sewing, and programming. I also love being active in nature."
|
||||||
|
]),
|
||||||
|
footer: [
|
||||||
|
"Design by Christoph Hagen, 2023.",
|
||||||
|
"Please use the information in this document responsibly. Consider the environmental impact before printing."
|
||||||
|
])
|
25
ResumeBuilder/Data/CVInfo.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Titled<Content> {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
let items: [Content]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CVInfo {
|
||||||
|
|
||||||
|
let top: TopInfo
|
||||||
|
|
||||||
|
let work: Titled<CareerStation>
|
||||||
|
|
||||||
|
let education: Titled<CareerStation>
|
||||||
|
|
||||||
|
let publications: Titled<Publication>
|
||||||
|
|
||||||
|
let skills: Titled<SkillsSet>
|
||||||
|
|
||||||
|
let about: Titled<String>
|
||||||
|
|
||||||
|
let footer: [String]
|
||||||
|
}
|
18
ResumeBuilder/Data/CareerStation.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CareerStation: Identifiable {
|
||||||
|
|
||||||
|
let time: String
|
||||||
|
|
||||||
|
let location: String
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
let subtitle: String?
|
||||||
|
|
||||||
|
let text: String?
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
title + time + location
|
||||||
|
}
|
||||||
|
}
|
12
ResumeBuilder/Data/Publication.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Publication: Identifiable {
|
||||||
|
|
||||||
|
let venue: String
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
title + venue
|
||||||
|
}
|
||||||
|
}
|
13
ResumeBuilder/Data/SkillsSet.swift
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Foundation
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct SkillsSet: Identifiable {
|
||||||
|
|
||||||
|
let systemSymbol: SFSymbol
|
||||||
|
|
||||||
|
let entries: [String]
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
entries.joined()
|
||||||
|
}
|
||||||
|
}
|
22
ResumeBuilder/Data/TopInfo.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct TopInfo {
|
||||||
|
|
||||||
|
let imageName: String
|
||||||
|
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
let tagLine: String
|
||||||
|
|
||||||
|
let place: String
|
||||||
|
|
||||||
|
let ageText: String
|
||||||
|
|
||||||
|
let web: String
|
||||||
|
|
||||||
|
let email: String
|
||||||
|
|
||||||
|
let phone: String
|
||||||
|
|
||||||
|
let github: String
|
||||||
|
}
|
53
ResumeBuilder/Elements/CareerStationView.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct CareerStationView: View {
|
||||||
|
|
||||||
|
let info: CareerStation
|
||||||
|
|
||||||
|
let borderSpacing: CGFloat
|
||||||
|
|
||||||
|
let borderWidth: CGFloat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
LeftBorderView(color: .accentColor, spacing: borderSpacing, borderWidth: borderWidth) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
RightImageLabel(info.time, systemSymbol: .calendar)
|
||||||
|
.padding(.leading, -4)
|
||||||
|
Spacer()
|
||||||
|
RightImageLabel(info.location, systemSymbol: .pin)
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(info.title)
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.regular)
|
||||||
|
if let subtitle = info.subtitle {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
if let text = info.text {
|
||||||
|
Text(text)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CareerStationView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
CareerStationView(info: .init(
|
||||||
|
time: "Jul 2020 - Jul 2023",
|
||||||
|
location: "Braunschweig, Germany",
|
||||||
|
title: "German Aerospace Center",
|
||||||
|
subtitle: "Systems engineer",
|
||||||
|
text: "Responsible for aircraft systems and avionics of a high-altitude solar drone, safety, and software."),
|
||||||
|
borderSpacing: 5,
|
||||||
|
borderWidth: 3)
|
||||||
|
.previewLayout(.fixed(width: 300, height: 100))
|
||||||
|
}
|
||||||
|
}
|
33
ResumeBuilder/Elements/PublicationView.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PublicationView: View {
|
||||||
|
|
||||||
|
let info: Publication
|
||||||
|
|
||||||
|
let borderSpacing: CGFloat
|
||||||
|
|
||||||
|
let borderWidth: CGFloat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
LeftBorderView(color: .accentColor, spacing: borderSpacing, borderWidth: borderWidth) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(info.venue)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(info.title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.regular)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PublicationView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
PublicationView(info: .init(
|
||||||
|
venue: "My venue",
|
||||||
|
title: "The publication title"),
|
||||||
|
borderSpacing: 5,
|
||||||
|
borderWidth: 3)
|
||||||
|
}
|
||||||
|
}
|
32
ResumeBuilder/Elements/TopViewImage.swift
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TopViewImage: View {
|
||||||
|
|
||||||
|
let image: String
|
||||||
|
|
||||||
|
let shadow: CGFloat
|
||||||
|
|
||||||
|
let lineWidth: CGFloat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Image(image)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(1, contentMode: .fit)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.overlay {
|
||||||
|
Circle().stroke(.gray, lineWidth: lineWidth)
|
||||||
|
}
|
||||||
|
.shadow(radius: shadow)
|
||||||
|
.padding(lineWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TopViewImage_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TopViewImage(
|
||||||
|
image: "Cover",
|
||||||
|
shadow: 5,
|
||||||
|
lineWidth: 2)
|
||||||
|
.previewLayout(.fixed(width: 150, height: 150))
|
||||||
|
}
|
||||||
|
}
|
145
ResumeBuilder/Generic Elements/FlowLayout.swift
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@available(iOS 16.0, *)
|
||||||
|
struct FlowLayout: Layout {
|
||||||
|
var alignment: Alignment = .center
|
||||||
|
var spacing: CGFloat?
|
||||||
|
|
||||||
|
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
|
||||||
|
let result = FlowResult(
|
||||||
|
in: proposal.replacingUnspecifiedDimensions().width,
|
||||||
|
subviews: subviews,
|
||||||
|
alignment: alignment,
|
||||||
|
spacing: spacing
|
||||||
|
)
|
||||||
|
return result.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
|
||||||
|
let result = FlowResult(
|
||||||
|
in: proposal.replacingUnspecifiedDimensions().width,
|
||||||
|
subviews: subviews,
|
||||||
|
alignment: alignment,
|
||||||
|
spacing: spacing
|
||||||
|
)
|
||||||
|
for row in result.rows {
|
||||||
|
let rowXOffset = (bounds.width - row.frame.width) * alignment.horizontal.percent
|
||||||
|
for index in row.range {
|
||||||
|
let xPos = rowXOffset + row.frame.minX + row.xOffsets[index - row.range.lowerBound] + bounds.minX
|
||||||
|
let rowYAlignment = (row.frame.height - subviews[index].sizeThatFits(.unspecified).height) *
|
||||||
|
alignment.vertical.percent
|
||||||
|
let yPos = row.frame.minY + rowYAlignment + bounds.minY
|
||||||
|
subviews[index].place(at: CGPoint(x: xPos, y: yPos), anchor: .topLeading, proposal: .unspecified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FlowResult {
|
||||||
|
var bounds = CGSize.zero
|
||||||
|
var rows = [Row]()
|
||||||
|
|
||||||
|
struct Row {
|
||||||
|
var range: Range<Int>
|
||||||
|
var xOffsets: [Double]
|
||||||
|
var frame: CGRect
|
||||||
|
}
|
||||||
|
|
||||||
|
init(in maxPossibleWidth: Double, subviews: Subviews, alignment: Alignment, spacing: CGFloat?) {
|
||||||
|
var itemsInRow = 0
|
||||||
|
var remainingWidth = maxPossibleWidth.isFinite ? maxPossibleWidth : .greatestFiniteMagnitude
|
||||||
|
var rowMinY = 0.0
|
||||||
|
var rowHeight = 0.0
|
||||||
|
var xOffsets: [Double] = []
|
||||||
|
for (index, subview) in zip(subviews.indices, subviews) {
|
||||||
|
let idealSize = subview.sizeThatFits(.unspecified)
|
||||||
|
if index != 0 && widthInRow(index: index, idealWidth: idealSize.width) > remainingWidth {
|
||||||
|
// Finish the current row without this subview.
|
||||||
|
finalizeRow(index: max(index - 1, 0), idealSize: idealSize)
|
||||||
|
}
|
||||||
|
addToRow(index: index, idealSize: idealSize)
|
||||||
|
|
||||||
|
if index == subviews.count - 1 {
|
||||||
|
// Finish this row; it's either full or we're on the last view anyway.
|
||||||
|
finalizeRow(index: index, idealSize: idealSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func spacingBefore(index: Int) -> Double {
|
||||||
|
guard itemsInRow > 0 else { return 0 }
|
||||||
|
return spacing ?? subviews[index - 1].spacing.distance(to: subviews[index].spacing, along: .horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func widthInRow(index: Int, idealWidth: Double) -> Double {
|
||||||
|
idealWidth + spacingBefore(index: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addToRow(index: Int, idealSize: CGSize) {
|
||||||
|
let width = widthInRow(index: index, idealWidth: idealSize.width)
|
||||||
|
|
||||||
|
xOffsets.append(maxPossibleWidth - remainingWidth + spacingBefore(index: index))
|
||||||
|
// Allocate width to this item (and spacing).
|
||||||
|
remainingWidth -= width
|
||||||
|
// Ensure the row height is as tall as the tallest item.
|
||||||
|
rowHeight = max(rowHeight, idealSize.height)
|
||||||
|
// Can fit in this row, add it.
|
||||||
|
itemsInRow += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeRow(index: Int, idealSize: CGSize) {
|
||||||
|
let rowWidth = maxPossibleWidth - remainingWidth
|
||||||
|
rows.append(
|
||||||
|
Row(
|
||||||
|
range: index - max(itemsInRow - 1, 0) ..< index + 1,
|
||||||
|
xOffsets: xOffsets,
|
||||||
|
frame: CGRect(x: 0, y: rowMinY, width: rowWidth, height: rowHeight)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bounds.width = max(bounds.width, rowWidth)
|
||||||
|
let ySpacing = spacing ?? ViewSpacing().distance(to: ViewSpacing(), along: .vertical)
|
||||||
|
bounds.height += rowHeight + (rows.count > 1 ? ySpacing : 0)
|
||||||
|
rowMinY += rowHeight + ySpacing
|
||||||
|
itemsInRow = 0
|
||||||
|
rowHeight = 0
|
||||||
|
xOffsets.removeAll()
|
||||||
|
remainingWidth = maxPossibleWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension HorizontalAlignment {
|
||||||
|
var percent: Double {
|
||||||
|
switch self {
|
||||||
|
case .leading: return 0
|
||||||
|
case .trailing: return 1
|
||||||
|
default: return 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension VerticalAlignment {
|
||||||
|
var percent: Double {
|
||||||
|
switch self {
|
||||||
|
case .top: return 0
|
||||||
|
case .bottom: return 1
|
||||||
|
default: return 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct FlowLayout_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
FlowLayout(alignment: .leading) {
|
||||||
|
ForEach(["Swift", "C", "C++", "Python"]) { tag in
|
||||||
|
Text(tag)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.gray.opacity(0.1))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 200, height: 150))
|
||||||
|
}
|
||||||
|
}
|
35
ResumeBuilder/Generic Elements/LeftBorderView.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LeftBorderView<Content>: View where Content: View {
|
||||||
|
|
||||||
|
let color: Color
|
||||||
|
|
||||||
|
let spacing: CGFloat
|
||||||
|
|
||||||
|
let borderWidth: CGFloat
|
||||||
|
|
||||||
|
private let content: Content
|
||||||
|
|
||||||
|
init(color: Color, spacing: CGFloat, borderWidth: CGFloat, @ViewBuilder content: () -> Content) {
|
||||||
|
self.color = color
|
||||||
|
self.spacing = spacing
|
||||||
|
self.borderWidth = borderWidth
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: spacing) {
|
||||||
|
Rectangle()
|
||||||
|
.fill(color)
|
||||||
|
.frame(width: borderWidth)
|
||||||
|
content
|
||||||
|
}.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LeftBorderView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LeftBorderView(color: .orange, spacing: 5, borderWidth: 3) {
|
||||||
|
Text("Some")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
ResumeBuilder/Generic Elements/LeftImageLabel.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct LeftImageLabel: View {
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
let systemSymbol: SFSymbol
|
||||||
|
|
||||||
|
init(_ text: String, systemSymbol: SFSymbol) {
|
||||||
|
self.text = text
|
||||||
|
self.systemSymbol = systemSymbol
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text(text)
|
||||||
|
Image(systemSymbol: systemSymbol)
|
||||||
|
.frame(width: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LeftImageLabel_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LeftImageLabel("Home address", systemSymbol: .house)
|
||||||
|
}
|
||||||
|
}
|
28
ResumeBuilder/Generic Elements/RightImageLabel.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct RightImageLabel: View {
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
let systemSymbol: SFSymbol
|
||||||
|
|
||||||
|
init(_ text: String, systemSymbol: SFSymbol) {
|
||||||
|
self.text = text
|
||||||
|
self.systemSymbol = systemSymbol
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image(systemSymbol: systemSymbol)
|
||||||
|
.frame(width: 20)
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RightImageLabel_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
RightImageLabel("Home address", systemSymbol: .house)
|
||||||
|
}
|
||||||
|
}
|
33
ResumeBuilder/Generic Elements/TagView.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TagView: View {
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
let rounding: CGFloat
|
||||||
|
|
||||||
|
let color: Color
|
||||||
|
|
||||||
|
init(_ text: String, rounding: CGFloat, color: Color) {
|
||||||
|
self.text = text
|
||||||
|
self.rounding = rounding
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(text)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.padding(.horizontal, rounding)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: rounding)
|
||||||
|
.fill(color)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TagView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TagView("Text", rounding: 8, color: .gray)
|
||||||
|
}
|
||||||
|
}
|
34
ResumeBuilder/Generic Elements/TitledSection.swift
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TitledSection<Content>: View where Content: View {
|
||||||
|
|
||||||
|
private let content: Content
|
||||||
|
|
||||||
|
private let title: String
|
||||||
|
|
||||||
|
private let spacing: CGFloat
|
||||||
|
|
||||||
|
init(title: String, spacing: CGFloat, @ViewBuilder content: () -> Content) {
|
||||||
|
self.title = title
|
||||||
|
self.spacing = spacing
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Text(title)
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.padding(.bottom, spacing)
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TitledSection_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TitledSection(title: "Title", spacing: 10) {
|
||||||
|
Text("Some more text")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
ResumeBuilder/Language/CVLanguage.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Titles {
|
||||||
|
|
||||||
|
let work: String
|
||||||
|
|
||||||
|
let educationTitle: String
|
||||||
|
|
||||||
|
let publicationTitle: String
|
||||||
|
|
||||||
|
let skillsTitle: String
|
||||||
|
|
||||||
|
let aboutTitle: String
|
||||||
|
}
|
45
ResumeBuilder/Main Elements/TitledCareerSection.swift
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TitledCareerSection: View {
|
||||||
|
|
||||||
|
let style: CVStyle.Section
|
||||||
|
|
||||||
|
let content: Titled<CareerStation>
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TitledSection(title: content.title, spacing: style.titleSpacing) {
|
||||||
|
ForEach(content.items) { item in
|
||||||
|
CareerStationView(
|
||||||
|
info: item,
|
||||||
|
borderSpacing: style.borderSpacing,
|
||||||
|
borderWidth: style.borderWidth)
|
||||||
|
.padding(.bottom, style.bottomSpacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TitledItemSection_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TitledCareerSection(
|
||||||
|
style: .init(),
|
||||||
|
content: .init(
|
||||||
|
title: "Work experience",
|
||||||
|
items: [
|
||||||
|
.init(
|
||||||
|
time: "Jul 2020 - Jul 2023",
|
||||||
|
location: "Braunschweig, Germany",
|
||||||
|
title: "German Aerospace Center",
|
||||||
|
subtitle: "Systems engineer",
|
||||||
|
text: "Responsible for aircraft systems and avionics of a high-altitude solar drone, safety, and software."),
|
||||||
|
.init(
|
||||||
|
time: "Jul 2020 - Jul 2023",
|
||||||
|
location: "Braunschweig, Germany",
|
||||||
|
title: "German Aerospace Center",
|
||||||
|
subtitle: "Systems engineer",
|
||||||
|
text: "Responsible for aircraft systems and avionics of a high-altitude solar drone, safety, and software.")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.previewLayout(.fixed(width: 350, height: 400))
|
||||||
|
}
|
||||||
|
}
|
63
ResumeBuilder/Main Elements/TitledIconSection.swift
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension String: Identifiable {
|
||||||
|
|
||||||
|
public var id: String {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
public func addBorder<S>(_ content: S, width: CGFloat = 1, cornerRadius: CGFloat) -> some View where S : ShapeStyle {
|
||||||
|
let roundedRect = RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
return clipShape(roundedRect)
|
||||||
|
.overlay(roundedRect.strokeBorder(content, lineWidth: width))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TitledIconSection: View {
|
||||||
|
|
||||||
|
let content: Titled<SkillsSet>
|
||||||
|
|
||||||
|
let titleSpacing: CGFloat
|
||||||
|
|
||||||
|
let width: CGFloat
|
||||||
|
|
||||||
|
let style: SkillStyle
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TitledSection(title: content.title, spacing: titleSpacing) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
ForEach(content.items) { item in
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
Image(systemSymbol: item.systemSymbol)
|
||||||
|
.frame(
|
||||||
|
width: style.iconSize,
|
||||||
|
height: style.iconSize)
|
||||||
|
.padding(.leading, style.horizontalGap)
|
||||||
|
FlowLayout(alignment: .leading, spacing: style.verticalTagSpacing) {
|
||||||
|
ForEach(item.entries) { tag in
|
||||||
|
TagView(
|
||||||
|
tag,
|
||||||
|
rounding: style.tagRounding,
|
||||||
|
color: style.tagBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.padding(.bottom, style.rowSpacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TitledIconSection_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TitledIconSection(
|
||||||
|
content: .init(title: "Title", items: [
|
||||||
|
.init(systemSymbol: .keyboard, entries: ["Swift", "C", "C++", "Python"])
|
||||||
|
]),
|
||||||
|
titleSpacing: 10,
|
||||||
|
width: 200, style: SkillStyle())
|
||||||
|
.previewLayout(.fixed(width: 230, height: 300))
|
||||||
|
}
|
||||||
|
}
|
32
ResumeBuilder/Main Elements/TitledTextSection.swift
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TitledTextSection: View {
|
||||||
|
|
||||||
|
let content: Titled<String>
|
||||||
|
|
||||||
|
let titleSpacing: CGFloat
|
||||||
|
|
||||||
|
let paragraphSpacing: CGFloat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TitledSection(title: content.title, spacing: titleSpacing) {
|
||||||
|
ForEach(content.items) { text in
|
||||||
|
Text(text)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.padding(.bottom, paragraphSpacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TitledTextSection_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TitledTextSection(
|
||||||
|
content: .init(
|
||||||
|
title: "Title",
|
||||||
|
items: ["Some longer or shorter text to explain some feature."]),
|
||||||
|
titleSpacing: 10,
|
||||||
|
paragraphSpacing: 5)
|
||||||
|
}
|
||||||
|
}
|
76
ResumeBuilder/Main Elements/TopView.swift
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct TopView: View {
|
||||||
|
|
||||||
|
let info: TopInfo
|
||||||
|
|
||||||
|
let style: HeaderStyle
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geo in
|
||||||
|
let sideWidth = max(0, (geo.size.width - geo.size.height) / 2)
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Text(info.name)
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Text(info.tagLine)
|
||||||
|
.font(.subheadline)
|
||||||
|
.padding(.trailing, style.imageShadowSize)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
HStack {
|
||||||
|
RightImageLabel(info.place, systemSymbol: .house)
|
||||||
|
.padding(.leading, -4)
|
||||||
|
RightImageLabel(info.ageText, systemSymbol: .hourglass)
|
||||||
|
}.font(.subheadline)
|
||||||
|
}
|
||||||
|
.frame(width: sideWidth)
|
||||||
|
TopViewImage(
|
||||||
|
image: info.imageName,
|
||||||
|
shadow: style.imageShadowSize,
|
||||||
|
lineWidth: style.imageBorderWidth)
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
LeftImageLabel(info.web, systemSymbol: .globe)
|
||||||
|
.frame(maxHeight: style.iconHeight)
|
||||||
|
Spacer()
|
||||||
|
LeftImageLabel(info.email, systemSymbol: .envelope)
|
||||||
|
.frame(maxHeight: style.iconHeight)
|
||||||
|
Spacer()
|
||||||
|
LeftImageLabel(info.phone, systemSymbol: .phone)
|
||||||
|
.frame(maxHeight: style.iconHeight)
|
||||||
|
Spacer()
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
Text(info.github)
|
||||||
|
Image("Github")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(1.0, contentMode: .fit)
|
||||||
|
.padding(2)
|
||||||
|
.frame(width: style.iconHeight)
|
||||||
|
}.frame(maxHeight: style.iconHeight)
|
||||||
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.frame(width: sideWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TopView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TopView(info: .init(
|
||||||
|
imageName: "Cover",
|
||||||
|
name: "Christoph Hagen",
|
||||||
|
tagLine: "Problem solver and creative mind with a favour for interdisciplinary work.",
|
||||||
|
place: "Würzburg, Germany",
|
||||||
|
ageText: "Age 32",
|
||||||
|
web: "christophhagen.de",
|
||||||
|
email: "jobs@christophhagen.de",
|
||||||
|
phone: "Upon Request",
|
||||||
|
github: "github.com/christophhagen"),
|
||||||
|
style: HeaderStyle())
|
||||||
|
.previewLayout(.fixed(width: 540, height: 120))
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
//
|
|
||||||
// ResumeBuilderApp.swift
|
|
||||||
// ResumeBuilder
|
|
||||||
//
|
|
||||||
// Created by CH on 17.08.23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct ResumeBuilderApp: App {
|
struct ResumeBuilderApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView(info: cvInfo, style: cvStyle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
ResumeBuilder/Style/CVStyle.swift
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CVStyle {
|
||||||
|
|
||||||
|
struct Section {
|
||||||
|
|
||||||
|
let titleSpacing: CGFloat
|
||||||
|
|
||||||
|
let borderSpacing: CGFloat
|
||||||
|
|
||||||
|
let borderWidth: CGFloat
|
||||||
|
|
||||||
|
let bottomSpacing: CGFloat
|
||||||
|
|
||||||
|
let paragraphSpacing: CGFloat
|
||||||
|
|
||||||
|
init(titleSpacing: CGFloat = 10, borderSpacing: CGFloat = 5, borderWidth: CGFloat = 3, bottomSpacing: CGFloat = 10, paragraphSpacing: CGFloat = 5) {
|
||||||
|
self.titleSpacing = titleSpacing
|
||||||
|
self.borderSpacing = borderSpacing
|
||||||
|
self.borderWidth = borderWidth
|
||||||
|
self.bottomSpacing = bottomSpacing
|
||||||
|
self.paragraphSpacing = paragraphSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pageWidth: CGFloat
|
||||||
|
|
||||||
|
let header: HeaderStyle
|
||||||
|
|
||||||
|
let columnSpacing: CGFloat
|
||||||
|
|
||||||
|
let section: Section
|
||||||
|
|
||||||
|
let skillStyle: SkillStyle
|
||||||
|
|
||||||
|
var pageHeight: CGFloat {
|
||||||
|
pageWidth * sqrt(2)
|
||||||
|
}
|
||||||
|
}
|
26
ResumeBuilder/Style/HeaderStyle.swift
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct HeaderStyle {
|
||||||
|
|
||||||
|
/// The total height of the header
|
||||||
|
let height: CGFloat
|
||||||
|
|
||||||
|
/// The width of the line beneath the header
|
||||||
|
let lineWidth: CGFloat
|
||||||
|
|
||||||
|
/// The height of the icons
|
||||||
|
let iconHeight: CGFloat
|
||||||
|
|
||||||
|
/// The size of the shadow around the center image
|
||||||
|
let imageShadowSize: CGFloat
|
||||||
|
|
||||||
|
let imageBorderWidth: CGFloat
|
||||||
|
|
||||||
|
init(height: CGFloat = 100, lineWidth: CGFloat = 1, iconHeight: CGFloat = 20, imageShadowSize: CGFloat = 5, imageBorderWidth: CGFloat = 2) {
|
||||||
|
self.height = height
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
self.iconHeight = iconHeight
|
||||||
|
self.imageShadowSize = imageShadowSize
|
||||||
|
self.imageBorderWidth = imageBorderWidth
|
||||||
|
}
|
||||||
|
}
|
27
ResumeBuilder/Style/SkillStyle.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SkillStyle {
|
||||||
|
|
||||||
|
let iconSize: CGFloat
|
||||||
|
|
||||||
|
let rowSpacing: CGFloat
|
||||||
|
|
||||||
|
let verticalTagSpacing: CGFloat
|
||||||
|
|
||||||
|
let horizontalGap: CGFloat
|
||||||
|
|
||||||
|
let tagBackground: Color
|
||||||
|
|
||||||
|
let tagRounding: CGFloat
|
||||||
|
|
||||||
|
init(iconSize: CGFloat = 20, rowSpacing: CGFloat = 3, verticalTagSpacing: CGFloat = 3, horizontalGap: CGFloat = 5, tagBackground: Color = .gray, tagRounding: CGFloat = 8) {
|
||||||
|
self.iconSize = iconSize
|
||||||
|
self.rowSpacing = rowSpacing
|
||||||
|
self.verticalTagSpacing = verticalTagSpacing
|
||||||
|
self.horizontalGap = horizontalGap
|
||||||
|
self.tagBackground = tagBackground
|
||||||
|
self.tagRounding = tagRounding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|