├── 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 | |  |  |
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 |
--------------------------------------------------------------------------------