├── ressources ├── screenshot-2.png └── screenshot.png ├── Typography - WWDC22.swiftpm ├── Assets.xcassets │ ├── Contents.json │ ├── iphone.imageset │ │ ├── iphone.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── PlaceholderAppIcon-star-1024.png │ │ ├── PlaceholderAppIcon-star-60@2x.png │ │ ├── PlaceholderAppIcon-star-60@3x.png │ │ ├── PlaceholderAppIcon-star-76@2x.png │ │ ├── PlaceholderAppIcon-star-83.5@2x.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── .swiftpm │ ├── playgrounds │ │ ├── DocumentThumbnail.png │ │ ├── Workspace.plist │ │ ├── DocumentThumbnail.plist │ │ └── CachedManifest.plist │ └── xcode │ │ ├── package.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ └── henri.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ └── xcuserdata │ │ └── henri.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── Typeversity.swift ├── View │ ├── BaseView.swift │ ├── PagePlaygroundView.swift │ ├── ContentCustomViews │ │ └── fontsContentCustomView.swift │ ├── PlaygroundViews │ │ ├── WelcomePlaygroundView.swift │ │ ├── AppPlaygroundView.swift │ │ ├── KerningPlaygroundView.swift │ │ ├── FontsPlaygroundView.swift │ │ ├── HierarchyPlaygroundView.swift │ │ └── QuizPlaygroundView.swift │ ├── AboutView.swift │ ├── PageNavigationView.swift │ └── PageContentView.swift ├── Package.swift ├── ViewModel │ └── AppState.swift └── Model │ ├── Page.swift │ └── Content.swift ├── LICENSE └── README.md /ressources/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/ressources/screenshot-2.png -------------------------------------------------------------------------------- /ressources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/ressources/screenshot.png -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/Typography - WWDC22.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/iphone.imageset/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/Typography - WWDC22.swiftpm/Assets.xcassets/iphone.imageset/iphone.png -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Typeversity.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct Typeversity: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | BaseView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-1024.png -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-60@2x.png -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-60@3x.png -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-76@2x.png -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-star-83.5@2x.png -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/.swiftpm/xcode/xcuserdata/henri.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/iphone.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "iphone.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/.swiftpm/xcode/package.xcworkspace/xcuserdata/henri.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henribredt/Typography-WWDC22/HEAD/Typography - WWDC22.swiftpm/.swiftpm/xcode/package.xcworkspace/xcuserdata/henri.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemBlueColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/.swiftpm/playgrounds/Workspace.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppSettings 6 | 7 | appIconPlaceholderGlyphName 8 | star 9 | appSettingsVersion 10 | 1 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DocumentThumbnailConfiguration 6 | 7 | accentColorHash 8 | 9 | Fkd2iMDgBpnGz6RJejYS1+g8UyBitkslD+2JCBKO1Ug= 10 | 11 | appIconHash 12 | 13 | Ul7KHVCJ29y7ZwDZEMXgvCP7qiPuAmwOIkwrRUkOXyk= 14 | 15 | thumbnailIsPrerendered 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/BaseView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct BaseView: View { 4 | 5 | /// manage user progress 6 | @StateObject var appState = AppState() 7 | 8 | var body: some View { 9 | HStack(spacing: 0){ 10 | // Navigation 11 | PageNavigationView(appState: appState) 12 | .frame(width: 295) 13 | 14 | // Content 15 | PageContentView(appState: appState) 16 | 17 | // Divider 18 | Rectangle() 19 | .foregroundColor(Color(uiColor: UIColor.secondarySystemBackground)) 20 | .frame(width: 2) 21 | 22 | // Show the PlaygroundView 23 | PagePlaygroundView(appState: appState) 24 | 25 | 26 | } 27 | .ignoresSafeArea() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/.swiftpm/xcode/xcuserdata/henri.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Typeversity.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | Typography.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | AppModule 21 | 22 | primary 23 | 24 | 25 | Typeversity 26 | 27 | primary 28 | 29 | 30 | Typography 31 | 32 | primary 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Henri Bredt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PagePlaygroundView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PagePlaygroundView: View { 4 | 5 | /// manage user progress 6 | @ObservedObject private var appState: AppState 7 | 8 | public init(appState: AppState) { 9 | self.appState = appState 10 | } 11 | 12 | var body: some View { 13 | 14 | let playgroundViewtoDraw = BasicsCourse[appState.currentPage].playgroundView 15 | VStack{ 16 | switch playgroundViewtoDraw { 17 | case .welcomePlaygroundView: 18 | WelcomePlaygroundView(appState: appState) 19 | case .fontsPlaygroundView: 20 | FontsPlaygroundView(appState: appState) 21 | case .hierarchyPlaygroundView: 22 | HierarchyPlaygroundView(appState: appState) 23 | case .appPlaygroundView: 24 | AppPlaygroundView(appState: appState) 25 | case .kerningPlaygroundView: 26 | KerningPlaygroundView(appState: appState) 27 | case .quizPlaygroundView: 28 | QuizPlaygroundView(appState: appState) 29 | } 30 | } 31 | .padding(30) 32 | .padding(.top, 15) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | 3 | // WARNING: 4 | // This file is automatically generated. 5 | // Do not edit it by hand because the contents will be replaced. 6 | 7 | import PackageDescription 8 | import AppleProductTypes 9 | 10 | let package = Package( 11 | name: "Typography", 12 | platforms: [ 13 | .iOS("15.2") 14 | ], 15 | products: [ 16 | .iOSApplication( 17 | name: "Typography", 18 | targets: ["AppModule"], 19 | displayVersion: "1.0", 20 | bundleVersion: "1", 21 | iconAssetName: "AppIcon", 22 | accentColorAssetName: "AccentColor", 23 | supportedDeviceFamilies: [ 24 | .pad, 25 | .phone 26 | ], 27 | supportedInterfaceOrientations: [ 28 | .portrait, 29 | .landscapeRight, 30 | .landscapeLeft, 31 | .portraitUpsideDown(.when(deviceFamilies: [.pad])) 32 | ] 33 | ) 34 | ], 35 | targets: [ 36 | .executableTarget( 37 | name: "AppModule", 38 | path: ".", 39 | resources: [ 40 | .process("Resources") 41 | ] 42 | ) 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WWDC22 Swift Student Challenge Submission 2 | ### A Swift Student Challenge Winner 2022 🎉 3 | 4 | An educational iPad app teaching some fundamental rules of typography in a fun and interactive way. Created as a submission to the 2022 Apple Swift Student Challenge by Henri Bredt in April 2022. 5 | 6 | #### Get the app for free on the [App Store](https://apps.apple.com/app/id1620952845?l=en) 7 | 8 | | ![App screenshot](ressources/screenshot.png) | ![App screenshot](ressources/screenshot-2.png) | 9 | --- | --- 10 | 11 | The app is divided into three areas: a navigation bar (left), a content view for explaining the concepts (center) and a playground view, where the user can apply what he has learned in an interactive view (right). Every lesson challenges the user to accomplish a task in the playground view in order to progress through the course. 12 | 13 | ### Installation 14 | Download Typoversity on the [App Store](https://apps.apple.com/app/id1620952845?l=en). Alternatively, you can download the originally submitted Playground app in this repository and open it with Swift Playgrounds 4 on the iPad. 15 | 16 | ### Demo video 17 | [Watch on YouTube](https://www.youtube.com/watch?v=AiK6CGgM71w) 18 | 19 | ### What's next? 20 | As of right now, the app contains a welcome page, three lessons and a final quiz to fit the three-minute time frame for the challenge. I am planning to add a few more lessons (for example about spacing and alignment) in the weeks to come and then publish the app on the app store to help more people make good use of typography. 21 | #### UPDATE: Done ✅ 22 | --- 23 | All further development will take place in this [repository](https://github.com/henribredt/Typoversity). 24 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/.swiftpm/playgrounds/CachedManifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CachedManifest 6 | 7 | manifestData 8 | 9 | eyJkZXBlbmRlbmNpZXMiOltdLCJuYW1lIjoiVHlwb2dyYXBoeSIsInBhY2th 10 | Z2VLaW5kIjoicm9vdCIsInBsYXRmb3JtcyI6W3sib3B0aW9ucyI6W10sInBs 11 | YXRmb3JtTmFtZSI6ImlvcyIsInZlcnNpb24iOiIxNS4yIn1dLCJwcm9kdWN0 12 | cyI6W3sibmFtZSI6IlR5cG9ncmFwaHkiLCJzZXR0aW5ncyI6W3siZGlzcGxh 13 | eVZlcnNpb24iOlsiMS4wIl19LHsiYnVuZGxlVmVyc2lvbiI6WyIxIl19LHsi 14 | aU9TQXBwSW5mbyI6W3siYWNjZW50Q29sb3JBc3NldE5hbWUiOiJBY2NlbnRD 15 | b2xvciIsImNhcGFiaWxpdGllcyI6W10sImljb25Bc3NldE5hbWUiOiJBcHBJ 16 | Y29uIiwic3VwcG9ydGVkRGV2aWNlRmFtaWxpZXMiOlsicGFkIiwicGhvbmUi 17 | XSwic3VwcG9ydGVkSW50ZXJmYWNlT3JpZW50YXRpb25zIjpbeyJwb3J0cmFp 18 | dCI6e319LHsibGFuZHNjYXBlUmlnaHQiOnt9fSx7ImxhbmRzY2FwZUxlZnQi 19 | Ont9fSx7InBvcnRyYWl0VXBzaWRlRG93biI6eyJjb25kaXRpb24iOnsiZGV2 20 | aWNlRmFtaWxpZXMiOlsicGFkIl19fX1dfV19XSwidGFyZ2V0cyI6WyJBcHBN 21 | b2R1bGUiXSwidHlwZSI6eyJleGVjdXRhYmxlIjpudWxsfX1dLCJ0YXJnZXRN 22 | YXAiOnsiQXBwTW9kdWxlIjp7ImRlcGVuZGVuY2llcyI6W10sImV4Y2x1ZGUi 23 | OltdLCJuYW1lIjoiQXBwTW9kdWxlIiwicGF0aCI6Ii4iLCJyZXNvdXJjZXMi 24 | Olt7InBhdGgiOiJSZXNvdXJjZXMiLCJydWxlIjoicHJvY2VzcyJ9XSwic2V0 25 | dGluZ3MiOltdLCJ0eXBlIjoiZXhlY3V0YWJsZSJ9fSwidGFyZ2V0cyI6W3si 26 | ZGVwZW5kZW5jaWVzIjpbXSwiZXhjbHVkZSI6W10sIm5hbWUiOiJBcHBNb2R1 27 | bGUiLCJwYXRoIjoiLiIsInJlc291cmNlcyI6W3sicGF0aCI6IlJlc291cmNl 28 | cyIsInJ1bGUiOiJwcm9jZXNzIn1dLCJzZXR0aW5ncyI6W10sInR5cGUiOiJl 29 | eGVjdXRhYmxlIn1dLCJ0b29sc1ZlcnNpb24iOnsiX3ZlcnNpb24iOiI1LjUu 30 | MCJ9fQ== 31 | 32 | manifestHash 33 | 34 | 8jK8TabsCb/byMH+6kv/ljwBAVgvGClJWfb+iW/t/5M= 35 | 36 | schemaVersion 37 | 3 38 | swiftPMVersionString 39 | 5.5.0 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/ViewModel/AppState.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// Manages and saves user progress persistantly 4 | public class AppState: ObservableObject { 5 | 6 | public init() { 7 | // init currentPage with data from user defaults 8 | currentPage = UserDefaults.standard.integer(forKey: "currentPage") 9 | 10 | // init completionProgress with data from user defaults 11 | if let savedCompletionProgress = UserDefaults.standard.data(forKey: "completionProgress") { 12 | if let decodedCompletionProgress = try? JSONDecoder().decode([String].self, from: savedCompletionProgress) { 13 | completionProgress = decodedCompletionProgress 14 | return 15 | } 16 | } 17 | // Default to an empty array 18 | completionProgress = [String]() 19 | } 20 | 21 | /// Stores the currently opened page in user defaults and makes it available 22 | /// Used for restoring the last opened page when the user restarts the app 23 | @Published public var currentPage: Int { 24 | didSet{ 25 | UserDefaults.standard.set(currentPage, forKey: "currentPage") 26 | } 27 | } 28 | 29 | /// Stores the page completion progress in user defaults and makes it available 30 | @Published public var completionProgress: [String] { 31 | didSet{ 32 | if let encoded = try? JSONEncoder().encode(completionProgress) { 33 | UserDefaults.standard.set(encoded, forKey: "completionProgress") 34 | } 35 | } 36 | } 37 | 38 | /// Reset all user progress 39 | func resetCompletionProgress() { 40 | completionProgress = [String]() 41 | currentPage = 0 42 | } 43 | 44 | /// Save new view playgroundPage id as completed, will do noting, if the id already exists 45 | func appendToCompletionProgress(id: String) { 46 | if !completionProgress.contains(id) { 47 | completionProgress.append(id) 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/ContentCustomViews/fontsContentCustomView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // 4 | // 5 | // Created by Henri Bredt on 24.04.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FontsContentCustomView: View { 11 | @State private var selectedType = 0 12 | @State private var font: Font = .system(size: 30, weight: .medium, design: .default) 13 | @State private var text = "Font Sans Serif" 14 | 15 | var body: some View { 16 | VStack { 17 | Picker("Favorite Color", selection: $selectedType, content: { 18 | Text("Sans").tag(0) 19 | Text("Serif").tag(1) 20 | Text("Slab").tag(2) 21 | Text("Mono").tag(3) 22 | Text("Script").tag(4) 23 | }) 24 | .pickerStyle(SegmentedPickerStyle()) 25 | .onChange(of: selectedType){ newValue in 26 | switch newValue { 27 | case 0: 28 | font = .system(size: 30, weight: .medium, design: .default) 29 | text = "Sans Serif Font" 30 | case 1: 31 | font = .system(size: 30, weight: .medium, design: .serif) 32 | text = "Serif Font" 33 | case 2: 34 | font = Font.custom("American Typewriter", size: 30).weight(.regular) 35 | text = "Slab Serif Font" 36 | case 3: 37 | font = .system(size: 30, weight: .medium, design: .monospaced) 38 | text = "Monospaced Font" 39 | case 4: 40 | font = Font.custom("Snell Roundhand", size: 32).weight(.medium) 41 | text = "Script Font" 42 | 43 | default: 44 | font = .system(size: 30, weight: .medium, design: .default) 45 | text = "Font" 46 | } 47 | } 48 | 49 | Text(text) 50 | .font(font) 51 | .padding() 52 | .frame(height: 80) 53 | } 54 | 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "PlaceholderAppIcon-star-60@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "60x60" 38 | }, 39 | { 40 | "filename" : "PlaceholderAppIcon-star-60@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "idiom" : "ipad", 47 | "scale" : "1x", 48 | "size" : "20x20" 49 | }, 50 | { 51 | "idiom" : "ipad", 52 | "scale" : "2x", 53 | "size" : "20x20" 54 | }, 55 | { 56 | "idiom" : "ipad", 57 | "scale" : "1x", 58 | "size" : "29x29" 59 | }, 60 | { 61 | "idiom" : "ipad", 62 | "scale" : "2x", 63 | "size" : "29x29" 64 | }, 65 | { 66 | "idiom" : "ipad", 67 | "scale" : "1x", 68 | "size" : "40x40" 69 | }, 70 | { 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "40x40" 74 | }, 75 | { 76 | "idiom" : "ipad", 77 | "scale" : "1x", 78 | "size" : "76x76" 79 | }, 80 | { 81 | "filename" : "PlaceholderAppIcon-star-76@2x.png", 82 | "idiom" : "ipad", 83 | "scale" : "2x", 84 | "size" : "76x76" 85 | }, 86 | { 87 | "filename" : "PlaceholderAppIcon-star-83.5@2x.png", 88 | "idiom" : "ipad", 89 | "scale" : "2x", 90 | "size" : "83.5x83.5" 91 | }, 92 | { 93 | "filename" : "PlaceholderAppIcon-star-1024.png", 94 | "idiom" : "ios-marketing", 95 | "scale" : "1x", 96 | "size" : "1024x1024" 97 | } 98 | ], 99 | "info" : { 100 | "author" : "xcode", 101 | "version" : 1 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PlaygroundViews/WelcomePlaygroundView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // 4 | // 5 | // Created by Henri Bredt on 24.04.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WelcomePlaygroundView: View { 11 | 12 | // manage user progress 13 | @ObservedObject var appState: AppState 14 | 15 | 16 | /// currently opend page 17 | var currentPage : Page { 18 | BasicsCourse[appState.currentPage] 19 | } 20 | 21 | var body: some View { 22 | VStack{ 23 | Image(systemName: "character.cursor.ibeam") 24 | .resizable() 25 | .scaledToFit() 26 | .frame(width: 100, height: 100, alignment: .center) 27 | .foregroundColor(Color.accentColor.opacity(0.3)) 28 | .padding(.bottom, 50) 29 | HStack{ 30 | Spacer() 31 | Text("This is the playground view where you will be soving the callenges.") 32 | .multilineTextAlignment(.center) 33 | .padding(.bottom, 20) 34 | 35 | Spacer() 36 | } 37 | 38 | HStack{ 39 | if !appState.completionProgress.contains(currentPage.id) { 40 | Button { 41 | /// currently opend page 42 | let currentPage = BasicsCourse[appState.currentPage] 43 | // Mark lesson as completed 44 | appState.appendToCompletionProgress(id: currentPage.id) 45 | } label: { 46 | Text("Got it") 47 | .padding(12) 48 | .padding(.leading, 15) 49 | .padding(.trailing, 15) 50 | .background(Color.accentColor.opacity(0.1)) 51 | .cornerRadius(10) 52 | .transition(.scale.combined(with: .opacity)) 53 | } 54 | 55 | } else { 56 | Image(systemName: "checkmark.circle.fill") 57 | .resizable() 58 | .scaledToFit() 59 | .foregroundColor(Color.green) 60 | .frame(width: 40, height: 40) 61 | .padding(5) 62 | .padding(.trailing, 4) 63 | .transition(.scale.combined(with: .opacity)) 64 | } 65 | } 66 | .animation(Animation.timingCurve(0.44, 1.86, 0.61, 0.99, duration: 0.5), value: appState.completionProgress) 67 | 68 | } 69 | .animation(Animation.timingCurve(0.16, 0.9, 0.51, 1, duration: 0.3), value: appState.completionProgress) 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PlaygroundViews/AppPlaygroundView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AppPlaygroundView: View { 4 | 5 | // manage user progress 6 | @ObservedObject var appState: AppState 7 | 8 | @State private var articleAlignment : HorizontalAlignment = .leading 9 | @State private var alignmentIndex = 1 10 | 11 | var body: some View { 12 | let currentPage = BasicsCourse[appState.currentPage] 13 | 14 | VStack() { 15 | Image(uiImage: #imageLiteral(resourceName: "iphone.png")).resizable().scaledToFit() 16 | .background( 17 | AppView(articleAlignment: $articleAlignment) 18 | .padding(30) 19 | .padding(.top, 18) 20 | ) 21 | 22 | Picker(selection: $alignmentIndex, label: EmptyView()) { 23 | Image(systemName: "align.horizontal.left.fill").tag(0) 24 | Image(systemName: "align.horizontal.center.fill").tag(1) 25 | Image(systemName: "align.horizontal.right.fill").tag(2) 26 | } 27 | .pickerStyle(.segmented) 28 | .padding(.leading) 29 | .padding(.trailing) 30 | .padding(.top) 31 | 32 | 33 | Color(uiColor: .secondarySystemBackground) 34 | .cornerRadius(10) 35 | 36 | 37 | 38 | 39 | .onChange(of: alignmentIndex) { newValue in 40 | switch newValue { 41 | case 0: 42 | articleAlignment = .leading 43 | appState.appendToCompletionProgress(id: currentPage.id) 44 | case 1: 45 | articleAlignment = .center 46 | case 2: 47 | articleAlignment = .trailing 48 | default: 49 | articleAlignment = .leading 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | struct AppView: View { 57 | @Binding var articleAlignment : HorizontalAlignment 58 | 59 | var body: some View { 60 | ScrollView { 61 | VStack(alignment: articleAlignment){ 62 | 63 | Text("News Online") 64 | .font(.system(size: 35, design: .default)) 65 | .fontWeight(.bold) 66 | .lineSpacing(0) 67 | 68 | ForEach(["Economics", "Technology", "Sports", "All categories"], id: \.self) { category in 69 | HStack{ 70 | Text(category) 71 | Spacer() 72 | Image(systemName: "chevron.right") 73 | } 74 | } 75 | 76 | Text("Top Stories") 77 | .font(.title.bold()) 78 | 79 | VStack(alignment: articleAlignment){ 80 | Text("2h ago • Technology") 81 | .font(.caption) 82 | Text("Apple introduces next generation M1 Max chip beating out the competition") 83 | .font(.headline) 84 | Text("With the M1 Max Apple combined two M1 Pro with a custom bridged between, letting the two SOC's appear as a single compting unit. This groundbraking technology is likely to replace my battery draining Intel i7 haha, thanks for reading that far.") 85 | .font(.caption) 86 | } 87 | 88 | VStack(alignment: articleAlignment){ 89 | Text("2h ago • Technology") 90 | .font(.caption) 91 | Text("Apple announces WWDC22 online event with public viewing at Apple Park") 92 | .font(.headline) 93 | Text("With the M1 Max Apple combined two M1 Pro with a custom bridged between, letting the two SOC's appear as a single compting unit. This groundbraking technology is likely to replace my battery draining Intel i7 haha, thanks for reading that far.") 94 | .font(.caption) 95 | } 96 | Spacer() 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Model/Page.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | 4 | /// A Course consists out of pages which have a title and a few PageElements 5 | public struct Page: Equatable { 6 | 7 | /// unique identifier 8 | var id: String 9 | 10 | /// displayed in the navigation bar 11 | let title: String 12 | 13 | /// displayed small above the contentTitle, should follow the following convention if it is a lesson: *Lesson * 14 | let contentSubTitle: String 15 | 16 | /// displayed large in the header of a page 17 | let contentTitle: String 18 | 19 | /// SFSymbols name for a page 20 | let titleImageName: String 21 | 22 | /// value of type PlaygroundView (enumeration defined in Content.swift) that refers to a playgroundView that will be shown on the right 23 | let playgroundView: PlaygroundViews 24 | 25 | /// composition of a page, consists out of PageElements that will be drawn in the view 26 | let elements: [PageElement] 27 | 28 | // conform to equatable 29 | public static func == (lhs: Page, rhs: Page) -> Bool { 30 | return lhs.id == rhs.id 31 | } 32 | } 33 | 34 | /// common type of all PageElements that will be drawn on the page 35 | /// ensures that ever PageElement has an id and top & bottom spacing 36 | /// Objects of type PageElements shall not be created, instead uses childs like PageText that can be drawn 37 | class PageElement { 38 | 39 | var id: UUID 40 | var topSpacing: Bool 41 | var bottomSpacing: Bool 42 | 43 | init(_ topSpacing: Bool, _ bottomSpacing: Bool) { 44 | id = UUID() 45 | self.topSpacing = topSpacing 46 | self.bottomSpacing = bottomSpacing 47 | } 48 | 49 | } 50 | 51 | 52 | // MARK: PageElements: 53 | 54 | /// body text 55 | class PageText : PageElement { 56 | var text: String 57 | 58 | /// topSpacing and bottomSpacing has deafult values of false if not specified 59 | init(_ text: String, topSpacing: Bool = false, bottomSpacing: Bool = false) { 60 | self.text = text 61 | super.init(topSpacing, bottomSpacing) 62 | } 63 | } 64 | 65 | /// use when the pages topic needs to be divided into smaller subtopics, start a new topice with a PageHeadline 66 | class PageHeadline : PageElement { 67 | var text: String 68 | 69 | /// topSpacing and bottomSpacing has deafult values of false if not specified 70 | init(_ text: String, topSpacing: Bool = false, bottomSpacing: Bool = false) { 71 | self.text = text 72 | super.init(topSpacing, bottomSpacing) 73 | } 74 | } 75 | 76 | /// image, file must be accessible in Assets 77 | class PageImage : PageElement { 78 | var imageName: String 79 | 80 | /// topSpacing and bottomSpacing has deafult values of false if not specified 81 | init(imageName: String, topSpacing: Bool = false, bottomSpacing: Bool = false) { 82 | self.imageName = imageName 83 | super.init(topSpacing, bottomSpacing) 84 | } 85 | } 86 | 87 | /// highlighted task the user needs to complete 88 | /// subtasks are optional 89 | class PageTask : PageElement { 90 | var text: String 91 | var subTasks: [String]? 92 | 93 | /// topSpacing and bottomSpacing has deafult values of *false* if not specified 94 | /// subTasks has deafult values of *nil* if not specified 95 | init(_ text: String, subTasks: [String]? = nil, topSpacing: Bool = false, bottomSpacing: Bool = false) { 96 | self.text = text 97 | self.subTasks = subTasks 98 | super.init(topSpacing, bottomSpacing) 99 | } 100 | } 101 | 102 | /// simple divider that draws a seperating line 103 | class PageDivider : PageElement { 104 | 105 | /// topSpacing and bottomSpacing has deafult values of false if not specified 106 | init(topSpacing: Bool = false, bottomSpacing: Bool = false) { 107 | super.init(topSpacing, bottomSpacing) 108 | } 109 | } 110 | 111 | /// allows to darw any custom view inside of the content area 112 | /// A custom view must be registered in the eum PageCustomView and the switch case in PageContentView must cover that case for a view to appear 113 | class PageCustomView : PageElement { 114 | var customView: ContentCustomView 115 | 116 | /// topSpacing and bottomSpacing has deafult values of false if not specified 117 | init(_ customView: ContentCustomView, topSpacing: Bool = false, bottomSpacing: Bool = false) { 118 | self.customView = customView 119 | super.init(topSpacing, bottomSpacing) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/AboutView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AboutView: View { 4 | 5 | @Environment(\.dismiss) var dismiss 6 | 7 | var body: some View { 8 | 9 | VStack(spacing: 0){ 10 | HStack{ 11 | Spacer() 12 | Image(systemName: "signature") 13 | .resizable() 14 | .scaledToFit() 15 | .foregroundColor(Color(uiColor: .systemBackground)) 16 | .frame(width: 30, height: 30) 17 | .padding() 18 | .background( 19 | Color.accentColor 20 | .cornerRadius(15) 21 | ) 22 | Spacer() 23 | } 24 | 25 | VStack(spacing: 6){ 26 | Text("About this app") 27 | .font(.caption) 28 | .foregroundColor(.secondary) 29 | .padding(.top, 7) 30 | Text("Typography Basics Crashcourse") 31 | .font(.title2).fontWeight(.semibold) 32 | .multilineTextAlignment(.center) 33 | } 34 | .padding(.top) 35 | .padding(.bottom, 60) 36 | 37 | 38 | ScrollView(showsIndicators: false){ 39 | VStack(alignment: .leading, spacing: 35){ 40 | CalloutView( 41 | systemName: "swift", 42 | text: "This app was created as a submission for the Apple WWDC22 Swift Student Challenge by Henri Bredt in April 2022. To figure out how capable the iPad and the Swift Playgrounds app are, I have coded the app entirely on iPad. This year's WWDC is still ahead, looking forward to seeing you there and all the good new stuff!" 43 | ) 44 | 45 | CalloutView( 46 | systemName: "person.crop.circle", 47 | text: "I am a self-taught Swift developer and user experience design student with a passion for creating meaningful, simple and long-lasting products. Learn more about me on my [website](https://www.henribredt.de)." 48 | ) 49 | 50 | CalloutView( 51 | systemName: "book.closed.fill", 52 | text: "During the creation of this app project I used the following resources as inspiration and for reference: [Typography Tutorial - 10 rules to help you rule type](https://www.youtube.com/watch?v=QrNi9FmdlxY), [Summary of key rules (Typography)](https://practicaltypography.com/summary-of-key-rules.html), [The Beginner's Guide to Typography in Web Design](https://blog.hubspot.com/website/website-typography), [Thinking with Type](https://www.amazon.de/-/en/Ellen-Lupton-ebook/dp/B07PQ9VP3Q/)" 53 | ) 54 | } 55 | .padding(.leading, 35) 56 | .padding(.trailing, 35) 57 | } 58 | 59 | Spacer() 60 | 61 | Button { 62 | dismiss() 63 | } label: { 64 | Text("Dismiss") 65 | .padding(12) 66 | .padding(.leading, 6) 67 | .padding(.trailing, 6) 68 | .background(Color.accentColor.opacity(0.1)) 69 | .cornerRadius(10) 70 | } 71 | .padding() 72 | } 73 | .padding(60) 74 | } 75 | 76 | } 77 | 78 | // MARK: Callout View 79 | struct CalloutView: View { 80 | var systemName: String 81 | var text: String 82 | 83 | var body: some View { 84 | HStack(alignment: .top){ 85 | Image(systemName: systemName) 86 | .resizable() 87 | .scaledToFit() 88 | .foregroundColor(Color.accentColor) 89 | .frame(width: 20, height: 20) 90 | .padding(10) 91 | .background( 92 | Color.accentColor.opacity(0.1) 93 | .cornerRadius(10) 94 | ) 95 | .padding(.trailing, 20) 96 | Text(try! AttributedString(markdown: text)) 97 | .font(.footnote) 98 | .lineSpacing(1.1) 99 | } 100 | } 101 | } 102 | 103 | // MARK: String extension: toMarkdown() 104 | extension String { 105 | func toMarkdown() -> AttributedString { 106 | do { 107 | return try AttributedString(markdown: self) 108 | } catch { 109 | print("Error parsing Markdown for string \(self): \(error)") 110 | return AttributedString(self) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PlaygroundViews/KerningPlaygroundView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct KerningPlaygroundView: View { 4 | 5 | // manage user progress 6 | @ObservedObject var appState: AppState 7 | 8 | @State private var kerning = -6.0 9 | @State private var fontSize = 80.0 10 | 11 | var body: some View { 12 | VStack{ 13 | Spacer() 14 | Text("Hello!") 15 | .bold() 16 | .font(.system(size: fontSize)) 17 | .kerning(kerning) 18 | Spacer() 19 | 20 | // font size 21 | HStack{ 22 | if fontSize > 55 && fontSize < 65 { 23 | Image(systemName: "checkmark.circle.fill") 24 | .resizable() 25 | .scaledToFit() 26 | .foregroundColor(Color.green) 27 | .frame(width: 20, height: 20) 28 | .padding(5) 29 | .transition(.scale.combined(with: .opacity)) 30 | 31 | } else { 32 | Image(systemName: "textformat.size") 33 | .resizable() 34 | .scaledToFit() 35 | .foregroundColor(Color.accentColor) 36 | .frame(width: 20, height: 20) 37 | .padding(5) 38 | .transition(.scale.combined(with: .opacity)) 39 | } 40 | 41 | Text("Font size") 42 | .font(.callout) 43 | .padding(5) 44 | .animation(.none, value: kerning) 45 | Slider(value: $fontSize.animation(Animation.timingCurve(0.44, 1.86, 0.61, 0.99, duration: 0.5)), in: 25...85) 46 | .animation(.none, value: kerning) 47 | Text("\(fontSize, specifier: "%.00f")") 48 | .monospacedDigit() 49 | .font(.caption) 50 | .padding(5) 51 | .animation(.none, value: kerning) 52 | 53 | 54 | } 55 | .padding() 56 | .background( 57 | Color(uiColor: .secondarySystemBackground) 58 | ) 59 | .cornerRadius(10) 60 | .padding(.bottom, 5) 61 | 62 | // Kerning 63 | HStack{ 64 | if kerning > 1 && kerning < 3 { 65 | Image(systemName: "checkmark.circle.fill") 66 | .resizable() 67 | .scaledToFit() 68 | .foregroundColor(Color.green) 69 | .frame(width: 20, height: 20) 70 | .padding(5) 71 | .transition(.scale.combined(with: .opacity)) 72 | 73 | } else { 74 | Image(systemName: "textformat.abc.dottedunderline") 75 | .resizable() 76 | .scaledToFit() 77 | .foregroundColor(Color.accentColor) 78 | .frame(width: 20, height: 20) 79 | .padding(5) 80 | .transition(.scale.combined(with: .opacity)) 81 | } 82 | 83 | Text("Tracking") 84 | .font(.callout) 85 | .padding(5) 86 | .animation(.none, value: kerning) 87 | Slider(value: $kerning.animation(Animation.timingCurve(0.44, 1.86, 0.61, 0.99, duration: 0.5)), in: -8.0...8.0) 88 | .animation(.none, value: kerning) 89 | Text("\(kerning, specifier: "%.00f")") 90 | .monospacedDigit() 91 | .font(.caption) 92 | .padding(5) 93 | .animation(.none, value: kerning) 94 | 95 | 96 | } 97 | .padding() 98 | .background( 99 | Color(uiColor: .secondarySystemBackground) 100 | ) 101 | .cornerRadius(10) 102 | 103 | } 104 | .onChange(of: kerning) { newValue in 105 | checkChallengeCompleted() 106 | } 107 | .onChange(of: fontSize) { newValue in 108 | checkChallengeCompleted() 109 | } 110 | } 111 | 112 | func checkChallengeCompleted(){ 113 | if (kerning > 1 && kerning < 3) && (fontSize > 55 && fontSize < 65) { 114 | /// currently opend page 115 | let currentPage = BasicsCourse[appState.currentPage] 116 | // Mark lesson as completed 117 | appState.appendToCompletionProgress(id: currentPage.id) 118 | } 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PageNavigationView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PageNavigationView: View { 4 | 5 | public init(appState: AppState) { self.appState = appState } 6 | 7 | /// manage user progress 8 | @ObservedObject private var appState: AppState 9 | 10 | /// defines if the app shows the about window 11 | @State private var showingAboutView = false 12 | 13 | var body : some View { 14 | VStack(alignment: .leading, spacing: 0){ 15 | 16 | courseInfoHeader 17 | 18 | Text("Lessons") 19 | .font(.caption).fontWeight(.medium) 20 | .padding(.bottom, 10) 21 | 22 | pageOverview 23 | 24 | moreButton 25 | } 26 | .padding(18) 27 | .padding(.top, 25) 28 | .background(Color(uiColor: .secondarySystemBackground)) 29 | .animation(Animation.timingCurve(0.44, 1.86, 0.61, 0.99, duration: 0.5), value: appState.completionProgress) 30 | .sheet(isPresented: $showingAboutView){ 31 | AboutView() 32 | } 33 | } 34 | 35 | 36 | // MARK: courseInfoHeader 37 | /// contines course title and a progress bar 38 | var courseInfoHeader : some View { 39 | VStack(alignment: .leading, spacing: 4){ 40 | Text("Typography") 41 | .font(.footnote) 42 | Text("Basics Crashcourse") 43 | .font(.title3).fontWeight(.semibold) 44 | .padding(.bottom, 20) 45 | ProgressView(value: Float(appState.completionProgress.count), total: Float(BasicsCourse.count)) 46 | HStack{ 47 | Text("\((appState.completionProgress.count) * 100 / BasicsCourse.count) % completed") 48 | .font(.caption) 49 | .foregroundColor(.secondary) 50 | Spacer() 51 | Button { 52 | appState.resetCompletionProgress() 53 | } label: { 54 | Text("Reset progess") 55 | .font(.caption) 56 | .foregroundColor(.accentColor) 57 | } 58 | 59 | } 60 | .padding(.top, 2) 61 | .padding(.bottom, 60) 62 | } 63 | } 64 | 65 | // MARK: pageOverview 66 | /// shows all availabe pages in a course including its completion status and allows navigation 67 | var pageOverview: some View { 68 | VStack(alignment: .leading){ 69 | ScrollView{ 70 | VStack(spacing: 0){ 71 | ForEach(BasicsCourse, id: \.self.id) { page in 72 | Button { 73 | let index = BasicsCourse.firstIndex(of: page) ?? 0 74 | appState.currentPage = index 75 | } label: { 76 | 77 | HStack{ 78 | if (appState.completionProgress.contains(page.id)) { 79 | Image(systemName: "checkmark.circle.fill") 80 | .resizable() 81 | .scaledToFit() 82 | .foregroundColor(Color.green) 83 | .frame(width: 20, height: 20) 84 | .padding(5) 85 | .padding(.trailing, 4) 86 | .transition(.scale.combined(with: .opacity)) 87 | } else { 88 | Image(systemName: page.titleImageName) 89 | .resizable() 90 | .scaledToFit() 91 | .foregroundColor(Color.accentColor) 92 | .frame(width: 20, height: 20) 93 | .padding(5) 94 | .padding(.trailing, 4) 95 | .transition(.scale.combined(with: .opacity)) 96 | } 97 | Text(page.title) 98 | .font(.callout) 99 | .foregroundColor(.primary) 100 | Spacer() 101 | } 102 | .padding(10) 103 | .background(page.id == BasicsCourse[appState.currentPage].id ? Color.accentColor.opacity(0.1) : Color.clear ) 104 | .cornerRadius(10) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | // MARK: moreButton 113 | /// toggles a modal view showing background info about the app 114 | var moreButton: some View { 115 | Button { 116 | showingAboutView.toggle() 117 | } label: { 118 | HStack{ 119 | Image(systemName: "info.circle") 120 | .resizable() 121 | .scaledToFit() 122 | .foregroundColor(Color.accentColor) 123 | .frame(width: 17, height: 17) 124 | .padding(5) 125 | //.padding(.trailing, 2) 126 | .transition(.scale.combined(with: .opacity)) 127 | 128 | Text("About this app") 129 | .font(.footnote) 130 | .foregroundColor(.primary) 131 | Spacer() 132 | } 133 | } 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PlaygroundViews/FontsPlaygroundView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct FontsPlaygroundView: View { 4 | 5 | // manage user progress 6 | @ObservedObject var appState: AppState 7 | 8 | // subtitle 9 | @State private var subTitleSelection = 2 10 | @State private var subTitleFont: Font = Font.custom("American Typewriter", size: 14).weight(.regular) 11 | @State private var subTitleCorrect = false 12 | 13 | // title 14 | @State private var titleSelection = 1 15 | @State private var titleFont: Font = .system(size: 25, weight: .semibold, design: .serif) 16 | @State private var titleCorrect = false 17 | 18 | // body 19 | @State private var bodySelection = 1 20 | @State private var bodyFont: Font = .system(size: 17, weight: .regular, design: .serif) 21 | @State private var bodyCorrect = false 22 | 23 | var body: some View { 24 | VStack(alignment: .leading, spacing: 10){ 25 | Spacer() 26 | VStack(alignment: .leading, spacing: 10){ 27 | Text("2h ago • Technology") 28 | .font(subTitleFont) 29 | Text("Apple announces WWDC22 online event with keynote public viewing at Apple Park") 30 | .lineSpacing(1.5) 31 | .font(titleFont) 32 | .padding(.bottom, 12) 33 | Text("This morning Apple announced it will host its annual Worldwide Developers Conference this year again in an online format from June 6 through 10, free for all developers to attend. WWDC22 features the latest innovations on Apple plattforms, while giving developers access to engineers and technologies to learn how to create stunning apps and interactive experiences. For the first time there will be a public viewing of the keynote at Apple Park.") 34 | .lineSpacing(2.5) 35 | .font(bodyFont) 36 | } 37 | .padding() 38 | Spacer() 39 | Spacer() 40 | VStack(alignment: .leading){ 41 | FontSelector(selection: $subTitleSelection, correct: $subTitleCorrect, title: "Subtitle") 42 | FontSelector(selection: $titleSelection, correct: $titleCorrect, title: "Title") 43 | FontSelector(selection: $bodySelection, correct: $bodyCorrect, title: "Body") 44 | } 45 | .onChange(of: subTitleSelection) { newValue in 46 | updateFont(tag: newValue, font: &subTitleFont, size: 14, weight: .regular) 47 | if subTitleSelection == 0 { subTitleCorrect = true } else { subTitleCorrect = false } 48 | checkChallengeCompleted() 49 | } 50 | .onChange(of: titleSelection) { newValue in 51 | updateFont(tag: newValue, font: &titleFont, size: 25, weight: .semibold) 52 | if titleSelection == 0 { titleCorrect = true } else { titleCorrect = false } 53 | checkChallengeCompleted() 54 | } 55 | .onChange(of: bodySelection) { newValue in 56 | updateFont(tag: newValue, font: &bodyFont, size: 17, weight: .regular) 57 | if bodySelection == 0 { bodyCorrect = true } else { bodyCorrect = false } 58 | checkChallengeCompleted() 59 | } 60 | 61 | } 62 | } 63 | 64 | func updateFont(tag: Int, font: inout Font, size: CGFloat, weight: Font.Weight){ 65 | switch tag { 66 | case 0: 67 | font = .system(size: size, weight: weight, design: .default) 68 | case 1: 69 | font = .system(size: size, weight: weight, design: .serif) 70 | case 2: 71 | font = Font.custom("American Typewriter", size: size).weight(weight) 72 | case 3: 73 | font = .system(size: size, weight: weight, design: .monospaced) 74 | case 4: 75 | font = Font.custom("Snell Roundhand", size: size).weight(weight) 76 | 77 | default: 78 | font = .system(size: size, weight: weight, design: .default) 79 | } 80 | } 81 | 82 | func checkChallengeCompleted(){ 83 | if (subTitleCorrect && titleCorrect && bodyCorrect) { 84 | /// currently opend page 85 | let currentPage = BasicsCourse[appState.currentPage] 86 | // Mark lesson as completed 87 | appState.appendToCompletionProgress(id: currentPage.id) 88 | } 89 | } 90 | } 91 | 92 | struct FontSelector: View { 93 | 94 | @Binding var selection: Int 95 | @Binding var correct: Bool 96 | var title: String 97 | 98 | var body: some View { 99 | HStack(spacing: 30){ 100 | HStack(alignment: .center) { 101 | // Icon 102 | if (correct) { 103 | Image(systemName: "checkmark.circle.fill") 104 | .resizable() 105 | .scaledToFit() 106 | .foregroundColor(Color.green) 107 | .frame(width: 20, height: 20) 108 | .padding(5) 109 | .padding(.trailing, 4) 110 | .transition(.scale.combined(with: .opacity)) 111 | } else { 112 | Image(systemName: "character.textbox") 113 | .resizable() 114 | .scaledToFit() 115 | .foregroundColor(Color.accentColor) 116 | .frame(width: 20, height: 20) 117 | .padding(5) 118 | .padding(.trailing, 4) 119 | .transition(.scale.combined(with: .opacity)) 120 | } 121 | HStack{ 122 | Text(title) 123 | .font(.callout) 124 | Spacer() 125 | } 126 | .frame(width: 64) 127 | } 128 | .animation(Animation.timingCurve(0.44, 1.86, 0.61, 0.99, duration: 0.5), value: correct) 129 | 130 | HStack(spacing: 0) { 131 | Image(systemName: "chevron.down") 132 | .font(.caption.weight(.semibold)) 133 | .foregroundColor(.accentColor) 134 | .padding(.trailing, 12) 135 | Picker(title, selection: $selection, content: { 136 | Text("New York").tag(1) 137 | Text("San Francisco").tag(0) 138 | Text("San Francisco Mono").tag(3) 139 | Text("American Typewriter").tag(2) 140 | Text("Snell Roundhand").tag(4) 141 | }) 142 | Spacer() 143 | } 144 | .padding(.leading, 15) 145 | .padding(.trailing, 15) 146 | .padding(.top, 5) 147 | .padding(.bottom, 5) 148 | .background(Color.accentColor.opacity(0.1)) 149 | .cornerRadius(10) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PlaygroundViews/HierarchyPlaygroundView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct HierarchyPlaygroundView: View { 4 | // manage user progress 5 | @ObservedObject var appState: AppState 6 | 7 | // title 8 | @State private var titleFontSize : CGFloat = 21.0 9 | @State private var titleWeight : Font.Weight = .light 10 | 11 | var body: some View { 12 | VStack() { 13 | Image(uiImage: #imageLiteral(resourceName: "iphone.png")).resizable().scaledToFit() 14 | .background( 15 | HierarchyAppView(titleFontSize: $titleFontSize, titleWeight: $titleWeight) 16 | .padding(33) 17 | .padding(.top, 20) 18 | ) 19 | .padding(.bottom, 5) 20 | 21 | 22 | VStack(spacing: 15){ 23 | FontSizeSelector(fontSize: $titleFontSize) 24 | .padding() 25 | .background( 26 | Color(uiColor: .secondarySystemBackground) 27 | ) 28 | .cornerRadius(10) 29 | FontWeightSelector(fontWeight: $titleWeight) 30 | .padding() 31 | .background( 32 | Color(uiColor: .secondarySystemBackground) 33 | ) 34 | .cornerRadius(10) 35 | } 36 | .padding(.top) 37 | .onChange(of: titleFontSize) { newValue in 38 | checkChallengeCompleted() 39 | } 40 | .onChange(of: titleWeight) { newValue in 41 | checkChallengeCompleted() 42 | } 43 | } 44 | } 45 | 46 | func checkChallengeCompleted(){ 47 | if (titleWeight == .heavy || titleWeight == .bold) && (titleFontSize > 31 && titleFontSize < 41) { 48 | /// currently opend page 49 | let currentPage = BasicsCourse[appState.currentPage] 50 | // Mark lesson as completed 51 | appState.appendToCompletionProgress(id: currentPage.id) 52 | } 53 | } 54 | } 55 | 56 | struct HierarchyAppView: View { 57 | 58 | @Binding var titleFontSize : CGFloat 59 | @Binding var titleWeight : Font.Weight 60 | 61 | var body: some View { 62 | ScrollView { 63 | VStack(alignment: .leading){ 64 | 65 | Text("News Online") 66 | .font(.system(size: titleFontSize, design: .default)).fontWeight(titleWeight) 67 | .fontWeight(.bold) 68 | .lineSpacing(0) 69 | .padding(.bottom, 0.5) 70 | 71 | ForEach(["Economics", "Technology", "Sports", "All categories"], id: \.self) { category in 72 | HStack{ 73 | Text(category) 74 | Spacer() 75 | Image(systemName: "chevron.right") 76 | } 77 | } 78 | 79 | 80 | Text("Top Stories") 81 | .font(.system(size: titleFontSize-10, design: .default)).fontWeight(titleWeight) 82 | .padding(.top, 10) 83 | 84 | VStack(alignment: .leading, spacing: 4){ 85 | Text("2h ago • Technology") 86 | .font(.caption) 87 | Text("Apple introduces next generation M1 Max chip beating out the competition") 88 | .font(.headline) 89 | Text("With the M1 Max Apple combined two M1 Pro chiüs with a custom bridge between them, letting the two SOC's appear as a single compting unit. This groundbraking technology is shocking the chip industry.") 90 | .font(.caption) 91 | } 92 | .padding(.top, 1) 93 | 94 | VStack(alignment: .leading, spacing: 4){ 95 | Text("8h ago • Technology") 96 | .font(.caption) 97 | Text("Apple announces WWDC22 online event with public viewing at Apple Park") 98 | .font(.headline) 99 | Text("This morning Apple announced it will host its annual Worldwide Developers Conference this year again in an online format from June 6 through 10, free for all developers to attend. WWDC22 features the latest innovations on Apple plattforms, while giving developers access to engineers and technologies to learn how to create stunning apps and interactive experiences. For the first time there will be a public viewing of the keynote at Apple Park.") 100 | .font(.caption) 101 | } 102 | .padding(.top, 6) 103 | Spacer() 104 | } 105 | } 106 | } 107 | } 108 | 109 | struct FontSizeSelector: View { 110 | @Binding var fontSize: CGFloat 111 | 112 | var body: some View { 113 | HStack{ 114 | if fontSize > 31 && fontSize < 41 { 115 | Image(systemName: "checkmark.circle.fill") 116 | .resizable() 117 | .scaledToFit() 118 | .foregroundColor(Color.green) 119 | .frame(width: 20, height: 20) 120 | .padding(5) 121 | .transition(.scale.combined(with: .opacity)) 122 | 123 | } else { 124 | Image(systemName: "textformat.size") 125 | .resizable() 126 | .scaledToFit() 127 | .foregroundColor(Color.accentColor) 128 | .frame(width: 20, height: 20) 129 | .padding(5) 130 | .transition(.scale.combined(with: .opacity)) 131 | } 132 | 133 | Text("Size") 134 | .font(.callout) 135 | .padding(5) 136 | .animation(.none, value: fontSize) 137 | Slider(value: $fontSize.animation(Animation.timingCurve(0.44, 1.86, 0.61, 0.99, duration: 0.5)), in: 20...50) 138 | .animation(.none, value: fontSize) 139 | Text("\(fontSize, specifier: "%.00f")") 140 | .monospacedDigit() 141 | .font(.caption) 142 | .padding(5) 143 | .animation(.none, value: fontSize) 144 | } 145 | } 146 | } 147 | 148 | struct FontWeightSelector: View { 149 | @State private var selection: Int = 0 150 | @Binding var fontWeight: Font.Weight 151 | 152 | var body: some View { 153 | HStack{ 154 | if fontWeight == .heavy || fontWeight == .bold { 155 | Image(systemName: "checkmark.circle.fill") 156 | .resizable() 157 | .scaledToFit() 158 | .foregroundColor(Color.green) 159 | .frame(width: 20, height: 20) 160 | .padding(5) 161 | .transition(.scale.combined(with: .opacity)) 162 | 163 | } else { 164 | Image(systemName: "bold.underline") 165 | .resizable() 166 | .scaledToFit() 167 | .foregroundColor(Color.accentColor) 168 | .frame(width: 20, height: 20) 169 | .padding(5) 170 | .transition(.scale.combined(with: .opacity)) 171 | } 172 | 173 | Text("Weight") 174 | .font(.callout) 175 | .padding(5) 176 | .animation(.none, value: fontWeight) 177 | 178 | HStack(spacing: 0) { 179 | Image(systemName: "chevron.down") 180 | .font(.caption.weight(.semibold)) 181 | .foregroundColor(.accentColor) 182 | .padding(.trailing, 12) 183 | Picker("Fontweight", selection: $selection, content: { 184 | Text("Light").tag(0) 185 | Text("Regular").tag(1) 186 | Text("Medium").tag(2) 187 | Text("Semibold").tag(3) 188 | Text("Bold").tag(4) 189 | Text("Heavy").tag(5) 190 | }) 191 | Spacer() 192 | } 193 | .padding(.leading, 15) 194 | .padding(.trailing, 15) 195 | .padding(.top, 5) 196 | .padding(.bottom, 5) 197 | .background(Color.accentColor.opacity(0.1)) 198 | .cornerRadius(10) 199 | .onChange(of: selection) { newValue in 200 | switch newValue { 201 | case 0: fontWeight = .light 202 | case 1: fontWeight = .regular 203 | case 2: fontWeight = .medium 204 | case 3: fontWeight = .semibold 205 | case 4: fontWeight = .bold 206 | case 5: fontWeight = .heavy 207 | default: fontWeight = .regular 208 | } 209 | } 210 | } 211 | .animation(Animation.timingCurve(0.44, 1.86, 0.61, 0.99, duration: 0.5), value: fontWeight) 212 | } 213 | } 214 | 215 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/Model/Content.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // This file contains all content that app displays, organized with Pages 4 | let BasicsCourse : [Page] = [welcome, fonts, hierarchy, detail, quiz] 5 | 6 | /// All avalible PlaygroundViews that PageContentView can darw 7 | /// The switch case in PageContentView must cover that case for a view to appear 8 | enum PlaygroundViews { 9 | case welcomePlaygroundView 10 | case fontsPlaygroundView 11 | case hierarchyPlaygroundView 12 | case appPlaygroundView 13 | case kerningPlaygroundView 14 | case quizPlaygroundView 15 | } 16 | 17 | /// All avalible ContentCustomView that PageContentView can darw 18 | /// The switch case in PageContentView must cover that case for a view to appear 19 | enum ContentCustomView { 20 | case fontsContentCustomView 21 | } 22 | 23 | let welcome = Page( 24 | id: "basics_welcome", 25 | title: "Welcome", 26 | contentSubTitle: "Welcome", 27 | contentTitle: "Typography Basics Crashcourse", 28 | titleImageName: "graduationcap.fill", 29 | playgroundView: .welcomePlaygroundView, 30 | elements: [ 31 | PageText("This course introduces you to fundamental rules of typography. When you're done, you'll know how to work with text better than the vast majority of people. Please be aware that this course covers the basics, there's still a whole lot more to explore."), 32 | PageTask("To apply what you have learned, each lesson challenges you to accomplish a task. Interact with the controls in the Playground view on the right to solve the tasks. For now, simply click the blue button.", topSpacing: true), 33 | PageHeadline("Focus on readability", topSpacing: true), 34 | PageText("Good typography serves one purpose: It makes text greatly readable. It may also serve the purpose of creating an aesthetic visual appearance but readability is the major priority for every typographic work. Make sure that all your choices while working with text align with that goal."), 35 | PageDivider(topSpacing: true), 36 | PageHeadline("Break the rules", topSpacing: true), 37 | PageText("I have tried to write the fundamental rules of typography in such a way that they can help with decision-making in case of uncertainty. The rules are therefore much more a guideline than generally valid wisdom. If you know what you are doing, you can break these rules and still do great typographic work."), 38 | ] 39 | ) 40 | 41 | let fonts = Page( 42 | id: "basics_fonts", 43 | title: "Fonts", 44 | contentSubTitle: "Lesson 1", 45 | contentTitle: "Use one font", 46 | titleImageName: "textformat", 47 | playgroundView: .fontsPlaygroundView, 48 | elements: [ 49 | PageText("Fonts are the foundational building block of every typography work. There are countless fonts available, but they all fall into a few font categories. You can discover the most common of those below."), 50 | PageCustomView(.fontsContentCustomView, topSpacing: true), 51 | PageText("Each kind has its use cases but the by far most common font categories and the ones you should be using are Sans Serif and Serif fonts. Speaking generally, Sans Serif fonts should be preferred for screens and Serif fonts work best for print."), 52 | PageDivider(topSpacing: true), 53 | PageHeadline("Keep it simple", topSpacing: true), 54 | PageText("As long as you are not absolutely sure what you are doing, it's best to stick to just one font for a project. You can still create hierarchy, as we will discover in an upcoming lesson. Choose well established and perfectly readable fonts like Helvetica (Sans Serif) or Garamond (Serif). If you choose to work with two different typefaces for a project, make sure that these have a strong contrast and are easily distinguishable."), 55 | PageTask("You are designing an article preview for a newspaper app. In the playground on the right, apply what you have learned and select good fonts for that project.", topSpacing: true), 56 | PageHeadline("Summary", topSpacing: true), 57 | PageText("For screens use Sans Serif and for print use Serif fonts. Stick to one font for a project and prefer well-established typefaces."), 58 | ] 59 | ) 60 | 61 | let hierarchy = Page( 62 | id: "basics_hierarchy", 63 | title: "Hierarchy", 64 | contentSubTitle: "Lesson 2", 65 | contentTitle: "Skip a weight and create hierarchy", 66 | titleImageName: "text.alignleft", 67 | playgroundView: .hierarchyPlaygroundView, 68 | elements: [ 69 | PageText("Good typography lets readers easily understand the semantics of different paragraphs in a layout. We use hierarchy to achieve that desired effect."), 70 | PageHeadline("Font sizes", topSpacing: true), 71 | PageText("The first option to create hierarchy is the font size. The larger the font of a paragraph the more dominant and important it will appear. Make sure that you choose a consistent system of sizes which allows easy distinction. A good rule of thumb is to double the size for the next higher hierarchy."), 72 | PageText("Keep in mind that we also pursue the goal of making text greatly readable, so don't choose too small font sizes. For body text, go for a point size larger than 13 pixels for digital and 10 points for print."), 73 | PageDivider(topSpacing: true), 74 | PageHeadline("Font styles", topSpacing: true), 75 | PageText("You probably already know that fonts can have different weights. Thin, Regular or Semibold just to name a few. In addition to working with different font sizes, you can also use these weights to create hierarchy and empathise different paragraphs. Whenever you use more than one style of a font, make sure to skip a weight, otherwise, the difference between those two weights is too subtle. You can for example choose a combination of thin and medium, if there is a regular in between."), 76 | PageTask("The news app on the right currently lacks a clear hierarchy. The problem is the headline, adjust the font size and weight to fix that.", topSpacing: true), 77 | PageHeadline("Summary", topSpacing: true), 78 | PageText("For screens use Sans Serif and for print use Serif fonts. Stick to one font for a project and prefer well-established typefaces."), 79 | ] 80 | ) 81 | 82 | let detail = Page( 83 | id: "basics_detail", 84 | title: "Detail typography", 85 | contentSubTitle: "Lesson 3", 86 | contentTitle: "Pay attention to detail", 87 | titleImageName: "text.magnifyingglass", 88 | playgroundView: .kerningPlaygroundView, 89 | elements: [ 90 | PageText("For this last lesson, we will take a look at something more advanced in typography, kerning and tracking of letters and fonts."), 91 | PageHeadline("Make space", topSpacing: true), 92 | PageText("Both kerning and tracking improve the appearance and design of your text by adding or subtracting space between specific pairs of characters. With kerning, you can change the space between two characters and tracking changes the spacing of the whole paragraph. Adjust the tracking with great caution, as too much and too little can make reading a lot more difficult."), 93 | PageTask("Set the font size to about 60 px and then adjust the tracking to let the word appear a bit more spaced out.", topSpacing: true), 94 | PageHeadline("Summary", topSpacing: true), 95 | PageText("You can improve the readability of a font by adjusting its tracking. With kerning, you can change the distance between individual characters, that's often used for perfecting largely visible headlines."), 96 | ] 97 | ) 98 | 99 | let quiz = Page( 100 | id: "basics_quiz", 101 | title: "Final Quiz", 102 | contentSubTitle: "Final Quiz", 103 | contentTitle: "Check your knowledge", 104 | titleImageName: "brain.head.profile", 105 | playgroundView: .quizPlaygroundView, 106 | elements: [ 107 | PageText("Congratulations for making it that far! You've made your way through some of the most fundamental rules of typography, had the chance to apply them and now have a head start in typography. There's one last challenge for you that will put to the test, what you have learned."), 108 | PageTask("Check what you have learned and finish the final quiz on the right.", topSpacing: true), 109 | PageDivider(topSpacing: true), 110 | PageText("Thank you for taking the time to complete this course, I hope you enjoyed it and that you've learned something new. Have a great WWDC, see you there!", topSpacing: true), 111 | ] 112 | ) 113 | 114 | /* 115 | 116 | // not included yet 117 | let DemoPageContent = Page( 118 | id: "basics_alignment", 119 | title: "Alignment", 120 | contentSubTitle: "Lesson 2", 121 | contentTitle: "Left align text", 122 | titleImageName: "align.horizontal.left.fill", 123 | playgroundView: .appPlaygroundView, 124 | elements: [ 125 | PageText("For a perciese typography would want to align text to a signel line, perferrably the leeft alignment."), 126 | PageHeadline("Attention", topSpacing: true), 127 | PageText("If you design for a right to left language, you should right align the text"), 128 | PageTask("On the right you will see a typographic news app with a poor usage of alignment.", subTasks: ["Take some time to experiment, how diffrent combinations feel", "Use the controls below to fix the aligmentens"]), 129 | PageDivider(), 130 | PageHeadline("Break the rules", topSpacing: false), 131 | PageText("There can always be situations, in wich a centerd alignment is more appropriete, you can always do that, but when i doubt, left align your text."), 132 | ] 133 | ) 134 | 135 | // not inclueded 136 | let MorePage = Page( 137 | id: "basics_contrast", 138 | title: "Contrast", 139 | contentSubTitle: "Lesson 4", 140 | contentTitle: "The secret to fonts is use just one", 141 | titleImageName: "circle.lefthalf.filled", 142 | playgroundView: .kerningPlaygroundView, 143 | elements: [ 144 | PageHeadline("Introduction to Fonts", topSpacing: true), 145 | PageText("Fonts are the foundational building block of every tyograhphy work. Typograyh is making art with letters, and yes, thats art too!"), 146 | PageTask("Be happy", subTasks: ["Whats good?"]) 147 | ] 148 | ) 149 | 150 | */ 151 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PageContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PageContentView : View { 4 | 5 | /// manage user progress 6 | @ObservedObject private var appState: AppState 7 | 8 | /// currently opend page 9 | var currentPage : Page { 10 | BasicsCourse[appState.currentPage] 11 | } 12 | 13 | public init(appState: AppState) { 14 | self.appState = appState 15 | } 16 | 17 | var body: some View { 18 | VStack(alignment: .leading, spacing: 0){ 19 | pageHeader 20 | 21 | Rectangle() 22 | .cornerRadius(6) 23 | .foregroundColor(Color(uiColor: UIColor.secondarySystemBackground)) 24 | .frame(height: 2) 25 | .padding(.top, 25) 26 | .padding(.bottom, 15) 27 | 28 | pageContent 29 | } 30 | .padding(30) 31 | .padding(.top, 15) 32 | } 33 | 34 | 35 | /// used for element spacing 36 | let spacingValue : CGFloat = 16 37 | /// used for extra topSpacing and bottomSpacing 38 | let topBottomSpacingValue : CGFloat = 22 39 | 40 | // MARK: pageContent 41 | /// scrollable view that contains all PageElements of that page 42 | var pageContent : some View { 43 | ScrollView(showsIndicators: false){ 44 | ScrollViewReader { value in 45 | VStack(alignment: .leading, spacing: spacingValue){ 46 | ForEach(currentPage.elements, id: \.self.id) { element in 47 | 48 | // Page dawing "engine" 49 | 50 | // Draw PageSubTitle elements 51 | if let pageHeadline = element as? PageHeadline { 52 | Text(pageHeadline.text) 53 | .font(.body.bold()) 54 | .lineSpacing(3.5) 55 | .padding(.top, pageHeadline.topSpacing ? topBottomSpacingValue : 0) 56 | .padding(.bottom, pageHeadline.bottomSpacing ? topBottomSpacingValue : 0) 57 | } 58 | 59 | // Draw PageText elements 60 | if let pageText = element as? PageText { 61 | Text(pageText.text) 62 | .lineSpacing(3.5) 63 | .font(.callout) 64 | .padding(.top, pageText.topSpacing ? topBottomSpacingValue : 0) 65 | .padding(.bottom, pageText.bottomSpacing ? topBottomSpacingValue : 0) 66 | } 67 | 68 | // Draw PageImage elements 69 | if let pageImage = element as? PageImage { 70 | Image(pageImage.imageName) 71 | .resizable() 72 | .scaledToFit() 73 | .padding(.top, pageImage.topSpacing ? topBottomSpacingValue : 0) 74 | .padding(.bottom, pageImage.bottomSpacing ? topBottomSpacingValue : 0) 75 | } 76 | 77 | // Draw PageDivider elements 78 | if let pageDivider = element as? PageDivider { 79 | divider 80 | .padding(.top, pageDivider.topSpacing ? topBottomSpacingValue : 0) 81 | .padding(.bottom, pageDivider.bottomSpacing ? topBottomSpacingValue : 0) 82 | } 83 | 84 | // Draw PageTask elements 85 | if let pageTask = element as? PageTask { 86 | HStack{ 87 | Rectangle() 88 | .foregroundColor(.accentColor) 89 | .frame(width: 5) 90 | .cornerRadius(10) 91 | .padding(1) 92 | VStack(alignment: .leading){ 93 | Text("Challenge") 94 | .lineSpacing(3.5) 95 | .font(.footnote) 96 | .foregroundColor(.secondary) 97 | Text(pageTask.text) 98 | .font(.callout) 99 | .padding(.top, 2) 100 | 101 | if let subTasks = pageTask.subTasks { 102 | Rectangle() 103 | .frame(height: 2) 104 | .foregroundColor(Color.clear) 105 | 106 | ForEach(subTasks, id: \.self) { subTask in 107 | HStack(alignment: .top){ 108 | Text("•") 109 | //.padding(.leading, 6) 110 | .padding(.trailing, 6) 111 | Text(subTask) 112 | .font(.callout) 113 | .lineSpacing(3) 114 | } 115 | .padding(.top, 2) 116 | } 117 | } 118 | } 119 | .padding(5) 120 | Spacer() 121 | } 122 | .padding(10) 123 | .background(Color(uiColor: .secondarySystemBackground)) 124 | .cornerRadius(10) 125 | .padding(.top, pageTask.topSpacing ? topBottomSpacingValue : 0) 126 | .padding(.bottom, pageTask.bottomSpacing ? topBottomSpacingValue : 0) 127 | 128 | } 129 | 130 | // Draw PageCustomView elements 131 | if let pageCustomView = element as? PageCustomView { 132 | switch pageCustomView.customView { 133 | case ContentCustomView.fontsContentCustomView: 134 | FontsContentCustomView() 135 | .padding(.top, pageCustomView.topSpacing ? topBottomSpacingValue : 0) 136 | .padding(.bottom, pageCustomView.bottomSpacing ? topBottomSpacingValue : 0) 137 | } 138 | } 139 | } 140 | .onChange(of: appState.currentPage, perform: { x in 141 | value.scrollTo(currentPage.elements.first!.id, anchor: .center) 142 | }) 143 | } 144 | //compensate missing VStack spacing top and bottom 145 | .padding(.top, spacingValue) 146 | .padding(.bottom, spacingValue) 147 | 148 | 149 | 150 | // show footer navigation only if not on last page 151 | if appState.currentPage + 1 < BasicsCourse.count { 152 | divider 153 | navigationButtons 154 | .padding(.bottom, spacingValue) 155 | } 156 | } 157 | 158 | } 159 | .transition(.slide) 160 | } 161 | 162 | // MARK: navigationButtons 163 | /// navigation buttons for going back and forth in the pages, buttons will only be displayed if nagivagtion possible 164 | var navigationButtons : some View { 165 | VStack{ 166 | if appState.currentPage + 1 < BasicsCourse.count { 167 | Button { 168 | //appState.appendToCompletionProgress(id: currentPage.id) 169 | appState.currentPage += 1 170 | } label: { 171 | Spacer() 172 | Text("Next lesson") 173 | .fontWeight(.medium) 174 | .padding(5) 175 | Spacer() 176 | } 177 | .padding(10) 178 | .background(Color(uiColor: UIColor.secondarySystemBackground)) 179 | .cornerRadius(10) 180 | .padding(.bottom, appState.currentPage - 1 < 0 ? 10 : 0 ) 181 | .keyboardShortcut(.downArrow, modifiers: []) 182 | } 183 | 184 | // show back button 185 | if appState.currentPage - 1 >= 0 { 186 | Button { 187 | appState.currentPage -= 1 188 | } label: { 189 | Spacer() 190 | Text("Previous") 191 | .font(.callout) 192 | .foregroundColor(.secondary) 193 | .fontWeight(.medium) 194 | Spacer() 195 | } 196 | .padding(13) 197 | .keyboardShortcut(.upArrow, modifiers: []) 198 | } 199 | } 200 | .padding(.top, 15) 201 | } 202 | 203 | // MARK: divider 204 | /// darws a thin line with some spacign 205 | var divider: some View { 206 | Rectangle() 207 | .cornerRadius(6) 208 | .foregroundColor(Color(uiColor: UIColor.secondarySystemBackground)) 209 | .frame(height: 2) 210 | } 211 | 212 | // MARK: pageHeader 213 | /// shows icon, page number and title of the page 214 | var pageHeader: some View { 215 | VStack(spacing: 0){ 216 | HStack{ 217 | Spacer() 218 | Image(systemName: currentPage.titleImageName) 219 | .resizable() 220 | .scaledToFit() 221 | .foregroundColor(Color(uiColor: .systemBackground)) 222 | .frame(width: 30, height: 30) 223 | .padding() 224 | .background( 225 | Color.accentColor 226 | .cornerRadius(15) 227 | ) 228 | Spacer() 229 | } 230 | 231 | VStack(spacing: 6){ 232 | Text(currentPage.contentSubTitle) 233 | .font(.caption) 234 | .foregroundColor(.secondary) 235 | .padding(.top, 7) 236 | Text(currentPage.contentTitle) 237 | .font(.title2).fontWeight(.semibold) 238 | .multilineTextAlignment(.center) 239 | } 240 | .padding(.top) 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Typography - WWDC22.swiftpm/View/PlaygroundViews/QuizPlaygroundView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct QuizPlaygroundView: View { 4 | 5 | init(appState: AppState){ 6 | self.appState = appState 7 | } 8 | 9 | // manage user progress 10 | @ObservedObject var appState: AppState 11 | 12 | @Environment(\.colorScheme) private var colorScheme 13 | 14 | @State private var quizCompleted = false 15 | @State private var showingHUD = false 16 | @State private var currentQuestionIndex = 0 17 | @State private var currentAnswerIsCorrect = false 18 | @State private var currentAnswerIsFalse = false 19 | @State private var isAnimating = false 20 | 21 | @State var animateShake: Int = 0 22 | 23 | // Quiz data 24 | private var questions = ["If you wanted to design the text on a website, what font would you choose?", "Which statement is wrong?", "Too much tracking causes a text to become difficult to read because letters overlap."] 25 | private var answers = [["Slab Serif", "Mono spaced", "Sans Serif", "Serif"],["Digital body text should be larger than 13 px", "The combination of semibold and bold font weights create good hierarchy", "When subheadlines have a font size of 20px, 40px would be a good size for headlines", "_empty_"],["True", "False", "_empty_", "_empty_"]] 26 | private var correctAnswers = [2,1,1] 27 | 28 | var body: some View { 29 | ZStack(alignment: .bottom) { 30 | VStack(){ 31 | if !quizCompleted { 32 | HStack{ 33 | Spacer() 34 | VStack{ 35 | Text("Question \(currentQuestionIndex+1) of \(questions.count)") 36 | .font(.caption) 37 | .foregroundColor(.secondary) 38 | .padding(.bottom, 8) 39 | 40 | Text(questions[currentQuestionIndex]) 41 | .fontWeight(.semibold) 42 | .multilineTextAlignment(.center) 43 | .foregroundColor(Color.primary) 44 | .font(.title3) 45 | .transition(.scale) 46 | .lineSpacing(1.5) 47 | .modifier(ShakeEffect(animatableData: CGFloat(animateShake))) // Shake on wrong input 48 | } 49 | .padding(30) 50 | Spacer() 51 | } 52 | .frame(height: 250) 53 | .background(Color(uiColor: .secondarySystemBackground)) 54 | .cornerRadius(10) 55 | 56 | 57 | VStack{ 58 | ForEach(0..<4) { index in 59 | if answers[currentQuestionIndex][index] != "_empty_" { 60 | Button { 61 | if index == correctAnswers[currentQuestionIndex] { 62 | currentAnswerIsCorrect = true 63 | currentAnswerIsFalse = false 64 | } else { 65 | currentAnswerIsCorrect = false 66 | currentAnswerIsFalse = true 67 | } 68 | 69 | withAnimation(Animation.timingCurve(0.47, 1.62, 0.45, 0.99, duration: 0.4)) { 70 | showingHUD.toggle() 71 | isAnimating = true 72 | } 73 | 74 | 75 | // Auto dismiss HUD 76 | if (!currentAnswerIsCorrect) { 77 | withAnimation(.default) { 78 | animateShake += 1 79 | } 80 | DispatchQueue.main.asyncAfter(deadline: .now() + (2.5) ) { 81 | withAnimation() { 82 | showingHUD = false 83 | isAnimating = false 84 | currentAnswerIsFalse = false 85 | } 86 | } 87 | //Dismiss and show next question 88 | } else { 89 | DispatchQueue.main.asyncAfter(deadline: .now() + (1.7) ) { 90 | nextQuestion() 91 | withAnimation() { 92 | showingHUD = false 93 | isAnimating = false 94 | currentAnswerIsFalse = false 95 | } 96 | } 97 | } 98 | 99 | } label: { 100 | HStack{ 101 | Spacer() 102 | Text(answers[currentQuestionIndex][index]) 103 | .font(.callout) 104 | .foregroundColor(.accentColor) 105 | Spacer() 106 | } 107 | .padding(12) 108 | .background(Color.accentColor.opacity(0.13)) 109 | .cornerRadius(10) 110 | } 111 | .padding(3) 112 | } 113 | 114 | } 115 | .disabled(isAnimating) 116 | 117 | } 118 | .padding(.top, 35) 119 | .transition(.asymmetric(insertion: .move(edge: .trailing).combined(with: .opacity), removal: .opacity) ) 120 | Spacer() 121 | 122 | } else { 123 | // Quiz was completed 124 | completedView 125 | .transition(.asymmetric(insertion: .move(edge: .trailing).combined(with: .opacity), removal: .opacity) ) 126 | } 127 | } 128 | 129 | if showingHUD { 130 | HUD { 131 | if (currentAnswerIsCorrect) { 132 | HStack(spacing: 25) { 133 | HStack{ 134 | Image(systemName: "checkmark.circle.fill") 135 | .foregroundColor(.green) 136 | Text("That's correct") 137 | .padding(.leading, 5) 138 | .foregroundColor(Color.primary) 139 | } 140 | } 141 | } else { 142 | HStack{ 143 | Image(systemName: "xmark.circle.fill") 144 | .foregroundColor(.red) 145 | Text("That's wrong, try again") 146 | .padding(.leading, 5) 147 | } 148 | } 149 | } 150 | .zIndex(1) 151 | .transition(AnyTransition.move(edge: .bottom).combined(with: .opacity)) 152 | } 153 | } 154 | } 155 | 156 | func nextQuestion() { 157 | if currentQuestionIndex + 1 < questions.count { 158 | currentQuestionIndex += 1 159 | withAnimation { 160 | showingHUD = false 161 | isAnimating = false 162 | currentAnswerIsCorrect = false 163 | } 164 | } else { 165 | // mark as finished 166 | let currentPage = BasicsCourse[appState.currentPage] 167 | appState.appendToCompletionProgress(id: currentPage.id) 168 | 169 | DispatchQueue.main.asyncAfter(deadline: .now() + (0.01) ) { 170 | withAnimation(Animation.timingCurve(0.65, 0, 0.35, 1, duration: 0.4)){ 171 | quizCompleted = true 172 | } 173 | currentQuestionIndex = 0 174 | } 175 | } 176 | 177 | } 178 | 179 | var completedView: some View { 180 | HStack{ 181 | Spacer() 182 | VStack(spacing: 20){ 183 | Image(systemName: "checkmark.circle.fill") 184 | .foregroundColor(.green) 185 | .font(.largeTitle) 186 | Text("Congratulations, you have successfully completed the quiz") 187 | .padding(.leading, 5) 188 | .foregroundColor(Color.primary) 189 | .font(.title3.weight(.medium)) 190 | .multilineTextAlignment(.center) 191 | Button { 192 | withAnimation(Animation.timingCurve(0.65, 0, 0.35, 1, duration: 0.4)) { 193 | quizCompleted = false 194 | } 195 | } label: { 196 | Text("Restart") 197 | .padding(12) 198 | .padding(.leading, 7) 199 | .padding(.trailing, 7) 200 | .background(Color.accentColor.opacity(0.1)) 201 | .cornerRadius(10) 202 | } 203 | .padding(.top, 50) 204 | 205 | } 206 | Spacer() 207 | } 208 | } 209 | } 210 | 211 | 212 | 213 | struct HUD: View { 214 | 215 | @Environment(\.colorScheme) private var colorScheme 216 | 217 | @ViewBuilder let content: Content 218 | 219 | var body: some View { 220 | content 221 | .padding(.horizontal, 12) 222 | .padding(16) 223 | .background( 224 | Capsule() 225 | .foregroundColor(colorScheme == .dark ? Color(UIColor.secondarySystemBackground) : Color(UIColor.systemBackground)) 226 | .shadow(color: Color(.black).opacity(0.15), radius: 10, x: 0, y: 4) 227 | ) 228 | .padding(20) 229 | } 230 | } 231 | 232 | 233 | // Shake effect for wrong input of textfields 234 | // inspired by: https://www.objc.io/blog/2019/10/01/swiftui-shake-animation/ 235 | // to achieve the shake, implemented a modified version 236 | struct ShakeEffect: GeometryEffect { 237 | var animatableData: CGFloat 238 | 239 | func effectValue(size: CGSize) -> ProjectionTransform { 240 | ProjectionTransform(CGAffineTransform(translationX: 241 | 10 * sin(animatableData * .pi * CGFloat(3)), 242 | y: 0) 243 | ) 244 | } 245 | } 246 | --------------------------------------------------------------------------------