diff --git a/Icon.key b/Icon.key new file mode 100755 index 0000000..86b9464 Binary files /dev/null and b/Icon.key differ diff --git a/ResumeBuilder.xcodeproj/project.pbxproj b/ResumeBuilder.xcodeproj/project.pbxproj index eb09f78..43e585a 100644 --- a/ResumeBuilder.xcodeproj/project.pbxproj +++ b/ResumeBuilder.xcodeproj/project.pbxproj @@ -8,18 +8,69 @@ /* Begin PBXBuildFile section */ 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 */; }; 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 */ /* Begin PBXFileReference section */ 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 = ""; }; - E267D1752A8E0DE80069112B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + E267D1752A8E0DE80069112B /* CV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CV.swift; sourceTree = ""; }; E267D1772A8E0DE90069112B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; E267D17A2A8E0DE90069112B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; E267D17C2A8E0DE90069112B /* ResumeBuilder.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ResumeBuilder.entitlements; sourceTree = ""; }; + E267D1822A8E0F320069112B /* Color+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extension.swift"; sourceTree = ""; }; + E267D1842A8E11930069112B /* TopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopView.swift; sourceTree = ""; }; + E267D1892A8E140E0069112B /* LeftImageLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftImageLabel.swift; sourceTree = ""; }; + E267D18B2A8E1A890069112B /* TopViewImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopViewImage.swift; sourceTree = ""; }; + E267D18D2A8E1BEE0069112B /* RightImageLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightImageLabel.swift; sourceTree = ""; }; + E267D18F2A8E32B70069112B /* CareerStationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CareerStationView.swift; sourceTree = ""; }; + E267D1912A8E347A0069112B /* TitledCareerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledCareerSection.swift; sourceTree = ""; }; + E267D1932A8E38C60069112B /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; + E267D1952A8E3E760069112B /* TitledTextSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledTextSection.swift; sourceTree = ""; }; + E267D1972A8E43580069112B /* TitledSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledSection.swift; sourceTree = ""; }; + E267D1992A8E44F40069112B /* TitledIconSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledIconSection.swift; sourceTree = ""; }; + E267D19B2A8E45470069112B /* CareerStation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CareerStation.swift; sourceTree = ""; }; + E267D19D2A8E45540069112B /* TopInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopInfo.swift; sourceTree = ""; }; + E267D19F2A8E45620069112B /* CVInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVInfo.swift; sourceTree = ""; }; + E267D1A12A8E45AE0069112B /* SkillsSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkillsSet.swift; sourceTree = ""; }; + E267D1A62A8ECC170069112B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + E267D1A82A8F5B430069112B /* FlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowLayout.swift; sourceTree = ""; }; + E267D1AC2A8F694A0069112B /* PublicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicationView.swift; sourceTree = ""; }; + E267D1AE2A8F69830069112B /* Publication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publication.swift; sourceTree = ""; }; + E267D1B32A8F7FEE0069112B /* LeftBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftBorderView.swift; sourceTree = ""; }; + E267D1B52A8F96310069112B /* CVStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVStyle.swift; sourceTree = ""; }; + E267D1B72A8F9A2A0069112B /* HeaderStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderStyle.swift; sourceTree = ""; }; + E267D1B92A8F9D9C0069112B /* SkillStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkillStyle.swift; sourceTree = ""; }; + E267D1BB2A8FFF300069112B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; + E267D1BF2A9009780069112B /* CVLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVLanguage.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -27,6 +78,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E267D1882A8E12D60069112B /* SFSafeSymbols in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -53,10 +105,19 @@ isa = PBXGroup; children = ( E267D1732A8E0DE80069112B /* ResumeBuilderApp.swift */, - E267D1752A8E0DE80069112B /* ContentView.swift */, + E267D1A62A8ECC170069112B /* ContentView.swift */, + E267D1752A8E0DE80069112B /* CV.swift */, + E267D1B02A8F6E5B0069112B /* Main Elements */, + E267D1B22A8F6E9C0069112B /* Elements */, + E267D1B12A8F6E7F0069112B /* Generic Elements */, E267D1772A8E0DE90069112B /* Assets.xcassets */, E267D17C2A8E0DE90069112B /* ResumeBuilder.entitlements */, E267D1792A8E0DE90069112B /* Preview Content */, + E267D1822A8E0F320069112B /* Color+Extension.swift */, + E267D1932A8E38C60069112B /* Data.swift */, + E267D1A52A8EC34B0069112B /* Data */, + E267D1BE2A90095D0069112B /* Language */, + E267D1BD2A9009390069112B /* Style */, ); path = ResumeBuilder; sourceTree = ""; @@ -69,6 +130,70 @@ path = "Preview Content"; sourceTree = ""; }; + E267D1A52A8EC34B0069112B /* Data */ = { + isa = PBXGroup; + children = ( + E267D19F2A8E45620069112B /* CVInfo.swift */, + E267D19B2A8E45470069112B /* CareerStation.swift */, + E267D1A12A8E45AE0069112B /* SkillsSet.swift */, + E267D19D2A8E45540069112B /* TopInfo.swift */, + E267D1AE2A8F69830069112B /* Publication.swift */, + ); + path = Data; + sourceTree = ""; + }; + E267D1B02A8F6E5B0069112B /* Main Elements */ = { + isa = PBXGroup; + children = ( + E267D1842A8E11930069112B /* TopView.swift */, + E267D1912A8E347A0069112B /* TitledCareerSection.swift */, + E267D1992A8E44F40069112B /* TitledIconSection.swift */, + E267D1952A8E3E760069112B /* TitledTextSection.swift */, + ); + path = "Main Elements"; + sourceTree = ""; + }; + 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 = ""; + }; + E267D1B22A8F6E9C0069112B /* Elements */ = { + isa = PBXGroup; + children = ( + E267D18B2A8E1A890069112B /* TopViewImage.swift */, + E267D1AC2A8F694A0069112B /* PublicationView.swift */, + E267D18F2A8E32B70069112B /* CareerStationView.swift */, + ); + path = Elements; + sourceTree = ""; + }; + E267D1BD2A9009390069112B /* Style */ = { + isa = PBXGroup; + children = ( + E267D1B52A8F96310069112B /* CVStyle.swift */, + E267D1B72A8F9A2A0069112B /* HeaderStyle.swift */, + E267D1B92A8F9D9C0069112B /* SkillStyle.swift */, + ); + path = Style; + sourceTree = ""; + }; + E267D1BE2A90095D0069112B /* Language */ = { + isa = PBXGroup; + children = ( + E267D1BF2A9009780069112B /* CVLanguage.swift */, + ); + path = Language; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -85,6 +210,9 @@ dependencies = ( ); name = ResumeBuilder; + packageProductDependencies = ( + E267D1872A8E12D60069112B /* SFSafeSymbols */, + ); productName = ResumeBuilder; productReference = E267D1702A8E0DE80069112B /* ResumeBuilder.app */; productType = "com.apple.product-type.application"; @@ -113,6 +241,9 @@ Base, ); mainGroup = E267D1672A8E0DE80069112B; + packageReferences = ( + E267D1862A8E12D60069112B /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, + ); productRefGroup = E267D1712A8E0DE80069112B /* Products */; projectDirPath = ""; projectRoot = ""; @@ -139,7 +270,32 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; 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 */, ); runOnlyForDeploymentPostprocessing = 0; @@ -274,6 +430,8 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = CHResume; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -301,6 +459,8 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = CHResume; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -336,6 +496,25 @@ defaultConfigurationName = Release; }; /* 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 */; } diff --git a/ResumeBuilder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ResumeBuilder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..e8341d6 --- /dev/null +++ b/ResumeBuilder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "sfsafesymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols", + "state" : { + "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", + "version" : "4.1.1" + } + } + ], + "version" : 2 +} diff --git a/ResumeBuilder/Assets.xcassets/AccentColor.colorset/Contents.json b/ResumeBuilder/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..787e985 100644 --- a/ResumeBuilder/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/ResumeBuilder/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,33 @@ { "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" } ], diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json index 3f00db4..7b00416 100644 --- a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,51 +1,61 @@ { "images" : [ { + "filename" : "icon 9.jpg", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { + "filename" : "icon 8.jpg", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { + "filename" : "icon 7.jpg", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { + "filename" : "icon 6.jpg", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { + "filename" : "icon 5.jpg", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { + "filename" : "icon 4.jpg", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { + "filename" : "icon 3.jpg", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { + "filename" : "icon 2.jpg", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { + "filename" : "icon 1.jpg", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { + "filename" : "icon.jpg", "idiom" : "mac", "scale" : "2x", "size" : "512x512" diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 1.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 1.jpg new file mode 100644 index 0000000..0f9691a Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 1.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 2.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 2.jpg new file mode 100644 index 0000000..0f9691a Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 2.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 3.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 3.jpg new file mode 100644 index 0000000..8bcaea5 Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 3.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 4.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 4.jpg new file mode 100644 index 0000000..8bcaea5 Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 4.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 5.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 5.jpg new file mode 100644 index 0000000..5b47eb5 Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 5.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 6.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 6.jpg new file mode 100644 index 0000000..677d446 Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 6.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 7.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 7.jpg new file mode 100644 index 0000000..84fe1e8 Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 7.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 8.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 8.jpg new file mode 100644 index 0000000..84fe1e8 Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 8.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 9.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 9.jpg new file mode 100644 index 0000000..424f787 Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon 9.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon.jpg b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon.jpg new file mode 100644 index 0000000..4d499ca Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/AppIcon.appiconset/icon.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/Cover.imageset/Contents.json b/ResumeBuilder/Assets.xcassets/Cover.imageset/Contents.json new file mode 100644 index 0000000..e2dbb8c --- /dev/null +++ b/ResumeBuilder/Assets.xcassets/Cover.imageset/Contents.json @@ -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 + } +} diff --git a/ResumeBuilder/Assets.xcassets/Cover.imageset/photo.jpg b/ResumeBuilder/Assets.xcassets/Cover.imageset/photo.jpg new file mode 100644 index 0000000..b7eb52b Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/Cover.imageset/photo.jpg differ diff --git a/ResumeBuilder/Assets.xcassets/Github.imageset/Contents.json b/ResumeBuilder/Assets.xcassets/Github.imageset/Contents.json new file mode 100644 index 0000000..4e57300 --- /dev/null +++ b/ResumeBuilder/Assets.xcassets/Github.imageset/Contents.json @@ -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 + } +} diff --git a/ResumeBuilder/Assets.xcassets/Github.imageset/github 1.png b/ResumeBuilder/Assets.xcassets/Github.imageset/github 1.png new file mode 100644 index 0000000..530cf98 Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/Github.imageset/github 1.png differ diff --git a/ResumeBuilder/Assets.xcassets/Github.imageset/github.png b/ResumeBuilder/Assets.xcassets/Github.imageset/github.png new file mode 100644 index 0000000..402191e Binary files /dev/null and b/ResumeBuilder/Assets.xcassets/Github.imageset/github.png differ diff --git a/ResumeBuilder/CV.swift b/ResumeBuilder/CV.swift new file mode 100644 index 0000000..abfed59 --- /dev/null +++ b/ResumeBuilder/CV.swift @@ -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))) + } +} diff --git a/ResumeBuilder/Color+Extension.swift b/ResumeBuilder/Color+Extension.swift new file mode 100644 index 0000000..2ed0d4d --- /dev/null +++ b/ResumeBuilder/Color+Extension.swift @@ -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) + } +} diff --git a/ResumeBuilder/ContentView.swift b/ResumeBuilder/ContentView.swift index b31306b..4aa155d 100644 --- a/ResumeBuilder/ContentView.swift +++ b/ResumeBuilder/ContentView.swift @@ -1,26 +1,107 @@ -// -// ContentView.swift -// ResumeBuilder -// -// Created by CH on 17.08.23. -// - import SwiftUI +import SFSafeSymbols struct ContentView: View { + + let info: CVInfo + + let style: CVStyle + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") + VStack(alignment: .leading) { + HStack { + Button(action: createAndSavePDF) { + Label("Save", systemSymbol: .squareAndArrowUp) + } + .padding() + } + ScrollView(.vertical) { + CV(info: info, style: style) + }.frame(width: style.pageWidth) } - .padding() + } + + 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 { static var previews: some View { - ContentView() + ContentView(info: cvInfo, style: cvStyle) + .frame(width: 600, height: 600 * sqrt(2)) } } diff --git a/ResumeBuilder/Data.swift b/ResumeBuilder/Data.swift new file mode 100644 index 0000000..fb7f7d7 --- /dev/null +++ b/ResumeBuilder/Data.swift @@ -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." + ]) diff --git a/ResumeBuilder/Data/CVInfo.swift b/ResumeBuilder/Data/CVInfo.swift new file mode 100644 index 0000000..0ae0f73 --- /dev/null +++ b/ResumeBuilder/Data/CVInfo.swift @@ -0,0 +1,25 @@ +import Foundation + +struct Titled { + + let title: String + + let items: [Content] +} + +struct CVInfo { + + let top: TopInfo + + let work: Titled + + let education: Titled + + let publications: Titled + + let skills: Titled + + let about: Titled + + let footer: [String] +} diff --git a/ResumeBuilder/Data/CareerStation.swift b/ResumeBuilder/Data/CareerStation.swift new file mode 100644 index 0000000..e551d2f --- /dev/null +++ b/ResumeBuilder/Data/CareerStation.swift @@ -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 + } +} diff --git a/ResumeBuilder/Data/Publication.swift b/ResumeBuilder/Data/Publication.swift new file mode 100644 index 0000000..9e635e6 --- /dev/null +++ b/ResumeBuilder/Data/Publication.swift @@ -0,0 +1,12 @@ +import Foundation + +struct Publication: Identifiable { + + let venue: String + + let title: String + + var id: String { + title + venue + } +} diff --git a/ResumeBuilder/Data/SkillsSet.swift b/ResumeBuilder/Data/SkillsSet.swift new file mode 100644 index 0000000..16b9838 --- /dev/null +++ b/ResumeBuilder/Data/SkillsSet.swift @@ -0,0 +1,13 @@ +import Foundation +import SFSafeSymbols + +struct SkillsSet: Identifiable { + + let systemSymbol: SFSymbol + + let entries: [String] + + var id: String { + entries.joined() + } +} diff --git a/ResumeBuilder/Data/TopInfo.swift b/ResumeBuilder/Data/TopInfo.swift new file mode 100644 index 0000000..ededd7a --- /dev/null +++ b/ResumeBuilder/Data/TopInfo.swift @@ -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 +} diff --git a/ResumeBuilder/Elements/CareerStationView.swift b/ResumeBuilder/Elements/CareerStationView.swift new file mode 100644 index 0000000..59d005c --- /dev/null +++ b/ResumeBuilder/Elements/CareerStationView.swift @@ -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)) + } +} diff --git a/ResumeBuilder/Elements/PublicationView.swift b/ResumeBuilder/Elements/PublicationView.swift new file mode 100644 index 0000000..fa443ab --- /dev/null +++ b/ResumeBuilder/Elements/PublicationView.swift @@ -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) + } +} diff --git a/ResumeBuilder/Elements/TopViewImage.swift b/ResumeBuilder/Elements/TopViewImage.swift new file mode 100644 index 0000000..718baee --- /dev/null +++ b/ResumeBuilder/Elements/TopViewImage.swift @@ -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)) + } +} diff --git a/ResumeBuilder/Generic Elements/FlowLayout.swift b/ResumeBuilder/Generic Elements/FlowLayout.swift new file mode 100644 index 0000000..d13f03a --- /dev/null +++ b/ResumeBuilder/Generic Elements/FlowLayout.swift @@ -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 + 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)) + } +} diff --git a/ResumeBuilder/Generic Elements/LeftBorderView.swift b/ResumeBuilder/Generic Elements/LeftBorderView.swift new file mode 100644 index 0000000..f6548f7 --- /dev/null +++ b/ResumeBuilder/Generic Elements/LeftBorderView.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct LeftBorderView: 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") + } + } +} diff --git a/ResumeBuilder/Generic Elements/LeftImageLabel.swift b/ResumeBuilder/Generic Elements/LeftImageLabel.swift new file mode 100644 index 0000000..c13dedc --- /dev/null +++ b/ResumeBuilder/Generic Elements/LeftImageLabel.swift @@ -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) + } +} diff --git a/ResumeBuilder/Generic Elements/RightImageLabel.swift b/ResumeBuilder/Generic Elements/RightImageLabel.swift new file mode 100644 index 0000000..f41e62d --- /dev/null +++ b/ResumeBuilder/Generic Elements/RightImageLabel.swift @@ -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) + } +} diff --git a/ResumeBuilder/Generic Elements/TagView.swift b/ResumeBuilder/Generic Elements/TagView.swift new file mode 100644 index 0000000..bca7225 --- /dev/null +++ b/ResumeBuilder/Generic Elements/TagView.swift @@ -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) + } +} diff --git a/ResumeBuilder/Generic Elements/TitledSection.swift b/ResumeBuilder/Generic Elements/TitledSection.swift new file mode 100644 index 0000000..f925a75 --- /dev/null +++ b/ResumeBuilder/Generic Elements/TitledSection.swift @@ -0,0 +1,34 @@ +import SwiftUI + +struct TitledSection: 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") + } + } +} diff --git a/ResumeBuilder/Language/CVLanguage.swift b/ResumeBuilder/Language/CVLanguage.swift new file mode 100644 index 0000000..f70044c --- /dev/null +++ b/ResumeBuilder/Language/CVLanguage.swift @@ -0,0 +1,14 @@ +import Foundation + +struct Titles { + + let work: String + + let educationTitle: String + + let publicationTitle: String + + let skillsTitle: String + + let aboutTitle: String +} diff --git a/ResumeBuilder/Main Elements/TitledCareerSection.swift b/ResumeBuilder/Main Elements/TitledCareerSection.swift new file mode 100644 index 0000000..d5017c5 --- /dev/null +++ b/ResumeBuilder/Main Elements/TitledCareerSection.swift @@ -0,0 +1,45 @@ +import SwiftUI + +struct TitledCareerSection: View { + + let style: CVStyle.Section + + let content: Titled + + 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)) + } +} diff --git a/ResumeBuilder/Main Elements/TitledIconSection.swift b/ResumeBuilder/Main Elements/TitledIconSection.swift new file mode 100644 index 0000000..d9fbc6f --- /dev/null +++ b/ResumeBuilder/Main Elements/TitledIconSection.swift @@ -0,0 +1,63 @@ +import SwiftUI + +extension String: Identifiable { + + public var id: String { + self + } +} + +extension View { + public func addBorder(_ 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 + + 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)) + } +} diff --git a/ResumeBuilder/Main Elements/TitledTextSection.swift b/ResumeBuilder/Main Elements/TitledTextSection.swift new file mode 100644 index 0000000..993dcf5 --- /dev/null +++ b/ResumeBuilder/Main Elements/TitledTextSection.swift @@ -0,0 +1,32 @@ +import SwiftUI + +struct TitledTextSection: View { + + let content: Titled + + 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) + } +} diff --git a/ResumeBuilder/Main Elements/TopView.swift b/ResumeBuilder/Main Elements/TopView.swift new file mode 100644 index 0000000..cb206b0 --- /dev/null +++ b/ResumeBuilder/Main Elements/TopView.swift @@ -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)) + } +} diff --git a/ResumeBuilder/ResumeBuilder.entitlements b/ResumeBuilder/ResumeBuilder.entitlements index f2ef3ae..19afff1 100644 --- a/ResumeBuilder/ResumeBuilder.entitlements +++ b/ResumeBuilder/ResumeBuilder.entitlements @@ -2,9 +2,9 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + diff --git a/ResumeBuilder/ResumeBuilderApp.swift b/ResumeBuilder/ResumeBuilderApp.swift index c610307..1bdeef5 100644 --- a/ResumeBuilder/ResumeBuilderApp.swift +++ b/ResumeBuilder/ResumeBuilderApp.swift @@ -1,17 +1,10 @@ -// -// ResumeBuilderApp.swift -// ResumeBuilder -// -// Created by CH on 17.08.23. -// - import SwiftUI @main struct ResumeBuilderApp: App { var body: some Scene { WindowGroup { - ContentView() + ContentView(info: cvInfo, style: cvStyle) } } } diff --git a/ResumeBuilder/Style/CVStyle.swift b/ResumeBuilder/Style/CVStyle.swift new file mode 100644 index 0000000..12ac064 --- /dev/null +++ b/ResumeBuilder/Style/CVStyle.swift @@ -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) + } +} diff --git a/ResumeBuilder/Style/HeaderStyle.swift b/ResumeBuilder/Style/HeaderStyle.swift new file mode 100644 index 0000000..6a4587e --- /dev/null +++ b/ResumeBuilder/Style/HeaderStyle.swift @@ -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 + } +} diff --git a/ResumeBuilder/Style/SkillStyle.swift b/ResumeBuilder/Style/SkillStyle.swift new file mode 100644 index 0000000..b8b7e15 --- /dev/null +++ b/ResumeBuilder/Style/SkillStyle.swift @@ -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 + } +} +