├── .gitignore
├── LICENSE
├── NefEditorClient.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ └── swiftpm
│ └── Package.resolved
├── NefEditorClient
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── 1024.png
│ │ ├── 120-1.png
│ │ ├── 120.png
│ │ ├── 152.png
│ │ ├── 167.png
│ │ ├── 180.png
│ │ ├── 20.png
│ │ ├── 29.png
│ │ ├── 40-1.png
│ │ ├── 40-2.png
│ │ ├── 40.png
│ │ ├── 58-1.png
│ │ ├── 58.png
│ │ ├── 60.png
│ │ ├── 76.png
│ │ ├── 80-1.png
│ │ ├── 80.png
│ │ ├── 87.png
│ │ ├── Contents.json
│ │ ├── nef@128.png
│ │ ├── nef@16.png
│ │ ├── nef@256.png
│ │ ├── nef@32.png
│ │ └── nef@512.png
│ ├── Contents.json
│ ├── badges
│ │ ├── Contents.json
│ │ ├── bow-actions-badge.imageset
│ │ │ ├── Contents.json
│ │ │ └── bow-actions-badge.png
│ │ ├── bow-platform-badge.imageset
│ │ │ ├── Contents.json
│ │ │ └── bow-platform-badge.png
│ │ └── nef-playgrounds-badge.imageset
│ │ │ ├── Contents.json
│ │ │ └── nef-playgrounds-badge.png
│ ├── bow-arch-background.imageset
│ │ ├── Contents.json
│ │ └── card-bow-arch-16_9 (1).pdf
│ ├── bow-arch-brand.imageset
│ │ ├── Contents.json
│ │ └── bow-arch (1).pdf
│ ├── bow-background.imageset
│ │ ├── Contents.json
│ │ └── card-bow-16_9 (1).pdf
│ ├── bow-brand.imageset
│ │ ├── Contents.json
│ │ └── bow-brand (1).pdf
│ ├── bow-lite-background.imageset
│ │ ├── Contents.json
│ │ └── bow-lite-card.pdf
│ ├── bow-lite-brand.imageset
│ │ ├── Contents.json
│ │ └── bow-lite-brand.pdf
│ ├── bow-openapi-brand.imageset
│ │ ├── Contents.json
│ │ └── bow-open-api (1).pdf
│ ├── bow-openapi.imageset
│ │ ├── Contents.json
│ │ ├── bowopenapi120.png
│ │ ├── bowopenapi40.png
│ │ └── bowopenapi80.png
│ ├── card.colorset
│ │ └── Contents.json
│ ├── form.colorset
│ │ └── Contents.json
│ ├── fortyseven.imageset
│ │ ├── Contents.json
│ │ └── fortyseven.pdf
│ ├── github-icon.imageset
│ │ ├── Contents.json
│ │ ├── github-dark@2x.png
│ │ ├── github-dark@3x.png
│ │ ├── github-light@2x.png
│ │ └── github-light@3x.png
│ ├── mainBackground.colorset
│ │ └── Contents.json
│ ├── nef-icon.imageset
│ │ ├── 256.png
│ │ ├── 512.png
│ │ ├── Contents.json
│ │ ├── nef@256.png
│ │ └── nef@512.png
│ ├── nef.imageset
│ │ ├── @2x.png
│ │ ├── @3x.png
│ │ └── Contents.json
│ ├── nefColor.colorset
│ │ └── Contents.json
│ ├── shadow.colorset
│ │ └── Contents.json
│ ├── splash-launchimage.imageset
│ │ ├── Contents.json
│ │ └── launchscreen-h@2x.png
│ └── splash-logo.imageset
│ │ ├── Contents.json
│ │ └── header-image.png
├── Authentication
│ ├── State
│ │ ├── AuthenticationInfo.swift
│ │ └── AuthenticationState.swift
│ └── View
│ │ └── SignInButton.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Catalog
│ ├── Component
│ │ └── CatalogComponent.swift
│ ├── Extensions
│ │ └── URLQueryItem+Recipe.swift
│ ├── Logic
│ │ ├── CatalogAction.swift
│ │ └── CatalogDispatcher.swift
│ ├── State
│ │ ├── Catalog.swift
│ │ ├── CatalogItem.swift
│ │ ├── CatalogSection.swift
│ │ ├── Dependency.swift
│ │ ├── FeaturedRecipe.swift
│ │ ├── Recipe.swift
│ │ └── TagViewModel.swift
│ └── View
│ │ ├── CatalogItemGridView.swift
│ │ ├── CatalogSectionView.swift
│ │ ├── FeaturedRecipeView.swift
│ │ ├── RecipeCatalogView.swift
│ │ ├── RegularRecipeView.swift
│ │ ├── TagCloud.swift
│ │ └── TagView.swift
├── Credits
│ ├── Component
│ │ └── CreditsComponent.swift
│ ├── Logic
│ │ ├── CreditsAction.swift
│ │ └── CreditsDispatcher.swift
│ ├── State
│ │ └── Library.swift
│ └── View
│ │ └── CreditsView.swift
├── DeepLink
│ ├── Extensions
│ │ └── URL+Recipe.swift
│ ├── Logic
│ │ ├── DeepLinkAction.swift
│ │ └── DeepLinkDispatcher.swift
│ └── State
│ │ └── DeepLinkState.swift
├── Detail
│ ├── Component
│ │ └── CatalogDetailComponent.swift
│ ├── Logic
│ │ ├── CatalogDetailAction.swift
│ │ └── CatalogDetailDispatcher.swift
│ └── View
│ │ ├── CatalogItemDetailView.swift
│ │ ├── DependencyListView.swift
│ │ └── DependencyView.swift
├── Edit
│ ├── Component
│ │ └── EditComponent.swift
│ ├── Logic
│ │ ├── EditAction.swift
│ │ └── EditDispatcher.swift
│ ├── State
│ │ └── EditState.swift
│ └── View
│ │ └── EditRecipeMetadataView.swift
├── Extensions
│ ├── Array+Extensions.swift
│ ├── Bundle+Extensions.swift
│ └── URLSession+IO.swift
├── FAQ
│ ├── Component
│ │ └── FAQComponent.swift
│ ├── Logic
│ │ ├── FAQAction.swift
│ │ └── FAQDispatcher.swift
│ └── View
│ │ └── FAQView.swift
├── Generation
│ ├── Component
│ │ └── GenerationComponent.swift
│ ├── Logic
│ │ ├── GenerationAction.swift
│ │ └── GenerationDispatcher.swift
│ ├── State
│ │ └── GenerationState.swift
│ └── View
│ │ ├── GenerationErrorView.swift
│ │ ├── GenerationPlaygroundBookView.swift
│ │ ├── GenerationSigninView.swift
│ │ ├── GenerationSuccessView.swift
│ │ └── GenerationView.swift
├── GitHubExtensions
│ └── Data+Equatable.swift
├── Info.plist
├── Main
│ ├── Component
│ │ ├── AppComponent.swift
│ │ └── AppModalComponent.swift
│ ├── Dependencies
│ │ └── AppDependencies.swift
│ ├── Logic
│ │ ├── AppAction.swift
│ │ ├── AppDispatcher.swift
│ │ └── MainDispatcher.swift
│ ├── State
│ │ ├── AppModalState.swift
│ │ ├── AppState.swift
│ │ ├── ICloudStatus.swift
│ │ └── PanelState.swift
│ └── View
│ │ ├── AppModalView.swift
│ │ └── AppView.swift
├── NefEditorClient.entitlements
├── NefEditorClientRelease.entitlements
├── Persistence
│ ├── ICloudPersistence.swift
│ ├── Persistence.swift
│ └── UserPreferences.swift
├── Preview Content
│ ├── Preview Assets.xcassets
│ │ └── Contents.json
│ └── PreviewData.swift
├── Requirements
│ ├── Component
│ │ └── RepositoryDetailComponent.swift
│ ├── Logic
│ │ ├── RepositoryDetailAction.swift
│ │ └── RepositoryDetailDispatcher.swift
│ ├── State
│ │ ├── RepositoryDetailState.swift
│ │ └── Requirement.swift
│ └── View
│ │ ├── EmptyRequirementsView.swift
│ │ ├── ErrorRequirementsView.swift
│ │ ├── LoadingRequirementsView.swift
│ │ ├── RepositoryDetailView.swift
│ │ ├── RequirementListView.swift
│ │ └── RequirementView.swift
├── SceneDelegate.swift
├── Search
│ ├── Component
│ │ └── SearchComponent.swift
│ ├── Logic
│ │ ├── SearchAction.swift
│ │ └── SearchDispatcher.swift
│ ├── State
│ │ └── SearchState.swift
│ └── View
│ │ ├── EmptySearchView.swift
│ │ ├── ErrorSearchView.swift
│ │ ├── InitialSearchView.swift
│ │ ├── LoadingSearchView.swift
│ │ ├── RepositoryGridView.swift
│ │ ├── RepositoryView.swift
│ │ └── SearchView.swift
├── Theme
│ ├── ActionButtonStyle.swift
│ ├── ActivityIndicator.swift
│ ├── ActivityTextView.swift
│ ├── ActivityViewController.swift
│ ├── AnimationView.swift
│ ├── AvatarView.swift
│ ├── CardView.swift
│ ├── ColorPalette.swift
│ ├── GridView.swift
│ ├── Image+System.swift
│ ├── KeyboardPadding.swift
│ ├── LibraryButtonStyle.swift
│ ├── LoadingView.swift
│ ├── LottieAnimation.swift
│ ├── NavigationBarButtonStyle.swift
│ ├── Rectangle+Separator.swift
│ ├── SearchBar.swift
│ ├── SectionTitle.swift
│ ├── TextButtonStyle.swift
│ ├── TextStyle.swift
│ ├── URLImage.swift
│ ├── View+Conditional.swift
│ ├── View+Hover.swift
│ ├── View+Modal.swift
│ └── ViewStyle.swift
├── ViewModifiers
│ └── ScaledFont.swift
└── WhatsNew
│ ├── Component
│ └── WhatsNewComponent.swift
│ ├── Logic
│ ├── WhatsNewAction.swift
│ └── WhatsNewDispatcher.swift
│ └── View
│ ├── BadgeGeneratorCard.swift
│ └── WhatsNewView.swift
├── NefEditorClientTests
├── AppDispatcherTests.swift
├── Info.plist
└── NefEditorClientTests.swift
├── README.md
├── github.yaml
└── nef.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | GitHub/**
2 | NefAPI/**
3 | **/Animations/**
4 |
5 | ## User settings
6 | xcuserdata/
7 | .DS_Store
8 |
9 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
10 | build/
11 | DerivedData/
12 |
13 | ## Obj-C/Swift specific
14 | *.hmap
15 |
16 | ## App packaging
17 | *.ipa
18 | *.dSYM.zip
19 | *.dSYM
20 |
21 | ## Playgrounds
22 | timeline.xctimeline
23 | playground.xcworkspace
24 |
25 | # Swift Package Manager
26 | #
27 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
28 | Packages/
29 | Package.pins
30 | *.xcodeproj
31 | .swiftpm
32 | .build
33 |
--------------------------------------------------------------------------------
/NefEditorClient.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/NefEditorClient.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/NefEditorClient.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Bow",
6 | "repositoryURL": "https://github.com/bow-swift/bow.git",
7 | "state": {
8 | "branch": "master",
9 | "revision": "a33173c8a111cbb492fe0ae0f16b58a0adb945d2",
10 | "version": null
11 | }
12 | },
13 | {
14 | "package": "BowArch",
15 | "repositoryURL": "https://github.com/bow-swift/bow-arch.git",
16 | "state": {
17 | "branch": "master",
18 | "revision": "9fbbcc703f196ba9286522365e71b2019ac30c7e",
19 | "version": null
20 | }
21 | },
22 | {
23 | "package": "Lottie",
24 | "repositoryURL": "https://github.com/airbnb/lottie-ios.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "4e5877425dae5c10792fc9d22d53dc6bf6824dc1",
28 | "version": "3.1.8"
29 | }
30 | },
31 | {
32 | "package": "nef-editor-server",
33 | "repositoryURL": "https://github.com/47deg/nef-editor-server",
34 | "state": {
35 | "branch": "master",
36 | "revision": "edadad0f37f90fd4a4b7cf17ee4551d2b132f4a5",
37 | "version": null
38 | }
39 | },
40 | {
41 | "package": "RxSwift",
42 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "002d325b0bdee94e7882e1114af5ff4fe1e96afa",
46 | "version": "5.1.1"
47 | }
48 | },
49 | {
50 | "package": "SwiftCheck",
51 | "repositoryURL": "https://github.com/bow-swift/SwiftCheck.git",
52 | "state": {
53 | "branch": null,
54 | "revision": "748359f9a95edf94d0c4664102f104f56b1ff1fb",
55 | "version": "0.12.1"
56 | }
57 | },
58 | {
59 | "package": "ZIPFoundation",
60 | "repositoryURL": "https://github.com/weichsel/ZIPFoundation.git",
61 | "state": {
62 | "branch": null,
63 | "revision": "ec32d62d412578542c0ffb7a6ce34d3e64b43b94",
64 | "version": "0.9.11"
65 | }
66 | }
67 | ]
68 | },
69 | "version": 1
70 | }
71 |
--------------------------------------------------------------------------------
/NefEditorClient/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @UIApplicationMain
4 | class AppDelegate: UIResponder, UIApplicationDelegate {
5 |
6 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
7 | // Override point for customization after application launch.
8 | return true
9 | }
10 |
11 | // MARK: UISceneSession Lifecycle
12 |
13 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
14 | // Called when a new scene session is being created.
15 | // Use this method to select a configuration to create the new scene with.
16 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/120-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/120-1.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40-1.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40-2.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/58-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/58-1.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/80-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/80-1.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "58.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "87.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "80.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "120.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120-1.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "180.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "40-1.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "58-1.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "40-2.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "80-1.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "76.png",
89 | "idiom" : "ipad",
90 | "scale" : "1x",
91 | "size" : "76x76"
92 | },
93 | {
94 | "filename" : "152.png",
95 | "idiom" : "ipad",
96 | "scale" : "2x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "167.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "83.5x83.5"
104 | },
105 | {
106 | "filename" : "1024.png",
107 | "idiom" : "ios-marketing",
108 | "scale" : "1x",
109 | "size" : "1024x1024"
110 | },
111 | {
112 | "filename" : "nef@16.png",
113 | "idiom" : "mac",
114 | "scale" : "1x",
115 | "size" : "16x16"
116 | },
117 | {
118 | "filename" : "nef@32.png",
119 | "idiom" : "mac",
120 | "scale" : "1x",
121 | "size" : "32x32"
122 | },
123 | {
124 | "filename" : "nef@128.png",
125 | "idiom" : "mac",
126 | "scale" : "1x",
127 | "size" : "128x128"
128 | },
129 | {
130 | "filename" : "nef@256.png",
131 | "idiom" : "mac",
132 | "scale" : "1x",
133 | "size" : "256x256"
134 | },
135 | {
136 | "filename" : "nef@512.png",
137 | "idiom" : "mac",
138 | "scale" : "1x",
139 | "size" : "512x512"
140 | }
141 | ],
142 | "info" : {
143 | "author" : "xcode",
144 | "version" : 1
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@128.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@16.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@256.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@32.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/AppIcon.appiconset/nef@512.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/badges/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/badges/bow-actions-badge.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "bow-actions-badge.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/badges/bow-actions-badge.imageset/bow-actions-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/badges/bow-actions-badge.imageset/bow-actions-badge.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/badges/bow-platform-badge.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "bow-platform-badge.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/badges/bow-platform-badge.imageset/bow-platform-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/badges/bow-platform-badge.imageset/bow-platform-badge.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/badges/nef-playgrounds-badge.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "nef-playgrounds-badge.png",
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/badges/nef-playgrounds-badge.imageset/nef-playgrounds-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/badges/nef-playgrounds-badge.imageset/nef-playgrounds-badge.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-arch-background.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "card-bow-arch-16_9 (1).pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-arch-background.imageset/card-bow-arch-16_9 (1).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-arch-background.imageset/card-bow-arch-16_9 (1).pdf
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-arch-brand.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "bow-arch (1).pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-arch-brand.imageset/bow-arch (1).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-arch-brand.imageset/bow-arch (1).pdf
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-background.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "card-bow-16_9 (1).pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-background.imageset/card-bow-16_9 (1).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-background.imageset/card-bow-16_9 (1).pdf
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-brand.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "bow-brand (1).pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-brand.imageset/bow-brand (1).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-brand.imageset/bow-brand (1).pdf
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-lite-background.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "bow-lite-card.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-lite-background.imageset/bow-lite-card.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-lite-background.imageset/bow-lite-card.pdf
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-lite-brand.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "bow-lite-brand.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-lite-brand.imageset/bow-lite-brand.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-lite-brand.imageset/bow-lite-brand.pdf
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-openapi-brand.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "bow-open-api (1).pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-openapi-brand.imageset/bow-open-api (1).pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-openapi-brand.imageset/bow-open-api (1).pdf
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-openapi.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "bowopenapi40.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "bowopenapi80.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "bowopenapi120.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi120.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi40.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/bow-openapi.imageset/bowopenapi80.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/card.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "255",
9 | "green" : "255",
10 | "red" : "255"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "1.000",
27 | "green" : "1.000",
28 | "red" : "1.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | },
33 | {
34 | "appearances" : [
35 | {
36 | "appearance" : "luminosity",
37 | "value" : "dark"
38 | }
39 | ],
40 | "color" : {
41 | "color-space" : "srgb",
42 | "components" : {
43 | "alpha" : "1.000",
44 | "blue" : "0.118",
45 | "green" : "0.110",
46 | "red" : "0.110"
47 | }
48 | },
49 | "idiom" : "universal"
50 | }
51 | ],
52 | "info" : {
53 | "author" : "xcode",
54 | "version" : 1
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/form.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "247",
9 | "green" : "242",
10 | "red" : "242"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "247",
27 | "green" : "242",
28 | "red" : "242"
29 | }
30 | },
31 | "idiom" : "universal"
32 | },
33 | {
34 | "appearances" : [
35 | {
36 | "appearance" : "luminosity",
37 | "value" : "dark"
38 | }
39 | ],
40 | "color" : {
41 | "color-space" : "srgb",
42 | "components" : {
43 | "alpha" : "1.000",
44 | "blue" : "30",
45 | "green" : "28",
46 | "red" : "28"
47 | }
48 | },
49 | "idiom" : "universal"
50 | }
51 | ],
52 | "info" : {
53 | "author" : "xcode",
54 | "version" : 1
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/fortyseven.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "fortyseven.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/fortyseven.imageset/fortyseven.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/fortyseven.imageset/fortyseven.pdf
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/github-icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "appearances" : [
9 | {
10 | "appearance" : "luminosity",
11 | "value" : "dark"
12 | }
13 | ],
14 | "idiom" : "universal",
15 | "scale" : "1x"
16 | },
17 | {
18 | "filename" : "github-dark@2x.png",
19 | "idiom" : "universal",
20 | "scale" : "2x"
21 | },
22 | {
23 | "appearances" : [
24 | {
25 | "appearance" : "luminosity",
26 | "value" : "dark"
27 | }
28 | ],
29 | "filename" : "github-light@2x.png",
30 | "idiom" : "universal",
31 | "scale" : "2x"
32 | },
33 | {
34 | "filename" : "github-dark@3x.png",
35 | "idiom" : "universal",
36 | "scale" : "3x"
37 | },
38 | {
39 | "appearances" : [
40 | {
41 | "appearance" : "luminosity",
42 | "value" : "dark"
43 | }
44 | ],
45 | "filename" : "github-light@3x.png",
46 | "idiom" : "universal",
47 | "scale" : "3x"
48 | }
49 | ],
50 | "info" : {
51 | "author" : "xcode",
52 | "version" : 1
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/github-icon.imageset/github-dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/github-icon.imageset/github-dark@2x.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/github-icon.imageset/github-dark@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/github-icon.imageset/github-dark@3x.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/github-icon.imageset/github-light@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/github-icon.imageset/github-light@2x.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/github-icon.imageset/github-light@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/github-icon.imageset/github-light@3x.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/mainBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.969",
9 | "green" : "0.949",
10 | "red" : "0.949"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.969",
27 | "green" : "0.949",
28 | "red" : "0.949"
29 | }
30 | },
31 | "idiom" : "universal"
32 | },
33 | {
34 | "appearances" : [
35 | {
36 | "appearance" : "luminosity",
37 | "value" : "dark"
38 | }
39 | ],
40 | "color" : {
41 | "color-space" : "srgb",
42 | "components" : {
43 | "alpha" : "1.000",
44 | "blue" : "0",
45 | "green" : "0",
46 | "red" : "0"
47 | }
48 | },
49 | "idiom" : "universal"
50 | }
51 | ],
52 | "info" : {
53 | "author" : "xcode",
54 | "version" : 1
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nef-icon.imageset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef-icon.imageset/256.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nef-icon.imageset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef-icon.imageset/512.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nef-icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "appearances" : [
9 | {
10 | "appearance" : "luminosity",
11 | "value" : "dark"
12 | }
13 | ],
14 | "idiom" : "universal",
15 | "scale" : "1x"
16 | },
17 | {
18 | "filename" : "256.png",
19 | "idiom" : "universal",
20 | "scale" : "2x"
21 | },
22 | {
23 | "appearances" : [
24 | {
25 | "appearance" : "luminosity",
26 | "value" : "dark"
27 | }
28 | ],
29 | "filename" : "nef@256.png",
30 | "idiom" : "universal",
31 | "scale" : "2x"
32 | },
33 | {
34 | "filename" : "512.png",
35 | "idiom" : "universal",
36 | "scale" : "3x"
37 | },
38 | {
39 | "appearances" : [
40 | {
41 | "appearance" : "luminosity",
42 | "value" : "dark"
43 | }
44 | ],
45 | "filename" : "nef@512.png",
46 | "idiom" : "universal",
47 | "scale" : "3x"
48 | }
49 | ],
50 | "info" : {
51 | "author" : "xcode",
52 | "version" : 1
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nef-icon.imageset/nef@256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef-icon.imageset/nef@256.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nef-icon.imageset/nef@512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef-icon.imageset/nef@512.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nef.imageset/@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef.imageset/@2x.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nef.imageset/@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/nef.imageset/@3x.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nef.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/nefColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "125",
9 | "green" : "6",
10 | "red" : "57"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "125",
27 | "green" : "6",
28 | "red" : "57"
29 | }
30 | },
31 | "idiom" : "universal"
32 | },
33 | {
34 | "appearances" : [
35 | {
36 | "appearance" : "luminosity",
37 | "value" : "dark"
38 | }
39 | ],
40 | "color" : {
41 | "color-space" : "srgb",
42 | "components" : {
43 | "alpha" : "1.000",
44 | "blue" : "255",
45 | "green" : "87",
46 | "red" : "160"
47 | }
48 | },
49 | "idiom" : "universal"
50 | }
51 | ],
52 | "info" : {
53 | "author" : "xcode",
54 | "version" : 1
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/shadow.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0",
9 | "green" : "0",
10 | "red" : "0"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "light"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.000",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | },
33 | {
34 | "appearances" : [
35 | {
36 | "appearance" : "luminosity",
37 | "value" : "dark"
38 | }
39 | ],
40 | "color" : {
41 | "color-space" : "srgb",
42 | "components" : {
43 | "alpha" : "1.000",
44 | "blue" : "1.000",
45 | "green" : "0.341",
46 | "red" : "0.627"
47 | }
48 | },
49 | "idiom" : "universal"
50 | }
51 | ],
52 | "info" : {
53 | "author" : "xcode",
54 | "version" : 1
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/splash-launchimage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "launchscreen-h@2x.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/splash-launchimage.imageset/launchscreen-h@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/splash-launchimage.imageset/launchscreen-h@2x.png
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/splash-logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "header-image.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Assets.xcassets/splash-logo.imageset/header-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bow-swift/nef-editor-client/57c03203e5503d1464cb1b1e1c8c3668b27c5d5d/NefEditorClient/Assets.xcassets/splash-logo.imageset/header-image.png
--------------------------------------------------------------------------------
/NefEditorClient/Authentication/State/AuthenticationInfo.swift:
--------------------------------------------------------------------------------
1 | struct AuthenticationInfo: Equatable {
2 | let user: String
3 | let identityToken: String
4 | let authorizationCode: String
5 | }
6 |
--------------------------------------------------------------------------------
/NefEditorClient/Authentication/State/AuthenticationState.swift:
--------------------------------------------------------------------------------
1 | enum AuthenticationState: Equatable {
2 | case authenticated(token: String)
3 | case unauthenticated
4 | }
5 |
--------------------------------------------------------------------------------
/NefEditorClient/Authentication/View/SignInButton.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import SwiftUI
3 | import AuthenticationServices
4 |
5 | struct SignInButton: UIViewRepresentable {
6 | let style: ASAuthorizationAppleIDButton.Style
7 | let onSignIn: (Either) -> Void
8 |
9 | class Coordinator: NSObject, ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate {
10 | let onSignIn: (Either) -> Void
11 |
12 | init(onSignIn: @escaping (Either) -> Void) {
13 | self.onSignIn = onSignIn
14 | }
15 |
16 | @objc func didTapButton() {
17 | let request = ASAuthorizationAppleIDProvider().createRequest()
18 | let authorizationController = ASAuthorizationController(authorizationRequests: [request])
19 | authorizationController.presentationContextProvider = self
20 | authorizationController.delegate = self
21 | authorizationController.performRequests()
22 | }
23 |
24 | func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
25 | let lastController = UIApplication.shared.windows.last?.rootViewController
26 | return lastController!.view.window!
27 | }
28 |
29 | func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
30 | guard let credentials = authorization.credential as? ASAuthorizationAppleIDCredential,
31 | let identityTokenRaw = credentials.identityToken,
32 | let authorizationCodeRaw = credentials.authorizationCode,
33 | let identityToken = String(data: identityTokenRaw, encoding: .utf8),
34 | let authorizationCode = String(data: authorizationCodeRaw, encoding: .utf8) else {
35 | onSignIn(.left(ASAuthorizationError(ASAuthorizationError.failed)))
36 | return
37 | }
38 |
39 | let info = AuthenticationInfo(
40 | user: credentials.user,
41 | identityToken: identityToken,
42 | authorizationCode: authorizationCode)
43 |
44 | onSignIn(.right(info))
45 | }
46 |
47 | func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
48 | onSignIn(.left(error))
49 | }
50 | }
51 |
52 | func makeCoordinator() -> SignInButton.Coordinator {
53 | Coordinator(onSignIn: onSignIn)
54 | }
55 |
56 | func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
57 | let button = ASAuthorizationAppleIDButton(authorizationButtonType: .signIn,
58 | authorizationButtonStyle: style)
59 | button.addTarget(context.coordinator,
60 | action: #selector(Coordinator.didTapButton),
61 | for: .touchUpInside)
62 | return button
63 | }
64 |
65 | func updateUIView(_ button: ASAuthorizationAppleIDButton, context: Context) -> Void {}
66 | }
67 |
--------------------------------------------------------------------------------
/NefEditorClient/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/Component/CatalogComponent.swift:
--------------------------------------------------------------------------------
1 | import BowArch
2 |
3 | typealias CatalogComponent = StoreComponent
4 |
5 | func catalogComponent(catalog: Catalog, selectedItem: CatalogItem?) -> CatalogComponent {
6 | CatalogComponent(
7 | initialState: (catalog, selectedItem)) { state, handle in
8 | RecipeCatalogView(catalog: state.0, selectedItem: state.1, handle: handle)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/Extensions/URLQueryItem+Recipe.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Array where Element == URLQueryItem {
4 |
5 | var recipe: Recipe? {
6 | guard let title = value(of: "name"),
7 | let dependency = dependency else { return nil }
8 |
9 | return .init(title: title,
10 | description: value(of: "description") ?? "",
11 | dependencies: [dependency])
12 | }
13 |
14 | // MARK: Recipe
15 | private var dependency: Dependency? {
16 | guard let repository = value(of: "name"),
17 | let url = value(of: "url"),
18 | let owner = value(of: "owner"),
19 | let avatar = value(of: "avatar"),
20 | let requirement = requirement else { return nil }
21 |
22 | return .init(repository: repository,
23 | owner: owner,
24 | url: url,
25 | avatar: avatar,
26 | requirement: requirement,
27 | products: products)
28 | }
29 |
30 | private var requirement: Requirement? {
31 | if let branch = value(of: "branch") {
32 | return .branch(.init(name: branch))
33 | } else if let tag = value(of: "tag") {
34 | return .version(.init(name: tag))
35 | } else {
36 | return nil
37 | }
38 | }
39 |
40 | private var products: Dependency.Products {
41 | let products = values(of: "products").map { $0.trimmingCharacters(in: .whitespaces) }
42 | return products.count > 0 ? .selected(names: products) : .all
43 | }
44 |
45 | // MARK: URLQueryItem
46 | private func value(of key: String) -> String? {
47 | first { $0.name == key }?.value
48 | }
49 |
50 | private func values(of key: String) -> [String] {
51 | filter { $0.name == "\(key)[]" }
52 | .compactMap(\.value)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/Logic/CatalogAction.swift:
--------------------------------------------------------------------------------
1 | enum CatalogAction: Equatable {
2 | case addRecipe
3 | case duplicate(item: CatalogItem)
4 | case remove(item: CatalogItem)
5 | case select(item: CatalogItem)
6 | }
7 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/Logic/CatalogDispatcher.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import BowArch
3 | import Foundation
4 |
5 | typealias CatalogDispatcher = StateDispatcher
6 |
7 | let catalogDispatcher = CatalogDispatcher.pure { action in
8 |
9 | switch action {
10 | case .addRecipe:
11 | return addRecipe()
12 |
13 | case .select(item: let item):
14 | return select(item: item)
15 |
16 | case .duplicate(item: let item):
17 | return duplicate(item: item)
18 |
19 | case .remove(item: let item):
20 | return remove(item: item)
21 | }
22 | }
23 |
24 | func addRecipe() -> State {
25 | .modify { state in
26 | state.copy(modalState: .edit(.newRecipe))
27 | }^
28 | }
29 |
30 | func duplicate(item: CatalogItem) -> State {
31 | .modify { state in
32 | let recipe = item.recipe.copy(id: UUID(), title: item.recipe.title + " copy")
33 | let newItem = CatalogItem.regular(recipe)
34 | let newCatalog = state.catalog.appending(newItem)
35 | return state.copy(catalog: newCatalog, selectedItem: newItem)
36 | }^
37 | }
38 |
39 | func remove(item: CatalogItem) -> State {
40 | .modify { state in
41 | if item.isEditable {
42 | let newCatalog = state.catalog.removing(item)
43 | let selectedItem = state.selectedItem == item ? nil : state.selectedItem
44 | return state.copy(catalog: newCatalog, selectedItem: selectedItem)
45 | } else {
46 | return state
47 | }
48 | }^
49 | }
50 |
51 | func select(item: CatalogItem) -> State {
52 | .modify { state in
53 | if state.selectedItem == item {
54 | return state.copy(selectedItem: .some(nil))
55 | } else {
56 | return state.copy(selectedItem: item)
57 | }
58 | }^
59 | }
60 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/State/Catalog.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 |
3 | struct Catalog: Equatable {
4 | let featured: CatalogSection
5 | let userCreated: CatalogSection
6 |
7 | var sections: [CatalogSection] {
8 | [featured, userCreated]
9 | }
10 |
11 | static var initial: Catalog {
12 | let bow = CatalogItem.featured(
13 | FeaturedRecipe(
14 | recipe: Recipe(
15 | title: "FP with Bow",
16 | description: "Get all modules in Bow 0.8.0 to write FP in Swift",
17 | dependencies: [
18 | Dependency(
19 | repository: "bow",
20 | owner: "bow-swift",
21 | url: "https://github.com/bow-swift/bow",
22 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4",
23 | requirement: .version(Tag(name: "0.8.0")),
24 | products: .all)
25 | ]),
26 | backgroundImage: "bow-background",
27 | textColor: .white)
28 | )
29 |
30 | let bowArch = CatalogItem.featured(
31 | FeaturedRecipe(
32 | recipe: Recipe(
33 | title: "Architecture",
34 | description: "Try Bow Arch to explore the possibilities of Functional Architecture",
35 | dependencies: [
36 | Dependency(
37 | repository: "bow-arch",
38 | owner: "bow-swift",
39 | url: "https://github.com/bow-swift/bow-arch",
40 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4",
41 | requirement: .branch(Branch(name: "0.1.0")),
42 | products: .all)
43 | ]),
44 | backgroundImage: "bow-arch-background",
45 | textColor: .black)
46 | )
47 |
48 | let bowLite = CatalogItem.featured(
49 | FeaturedRecipe(
50 | recipe: Recipe(
51 | title: "FP with Bow Lite",
52 | description: "Play with the lightweight version of Bow",
53 | dependencies: [
54 | Dependency(
55 | repository: "bow-lite",
56 | owner: "bow-swift",
57 | url: "https://github.com/bow-swift/bow-lite",
58 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4",
59 | requirement: .version(Tag(name: "0.1.0")),
60 | products: .all)
61 | ]),
62 | backgroundImage: "bow-lite-background",
63 | textColor: .white)
64 | )
65 |
66 | let featured = CatalogSection(
67 | title: "Featured",
68 | items: [bow, bowArch, bowLite])
69 | let myRecipes = CatalogSection(
70 | title: "My recipes",
71 | action: CatalogSectionAction(icon: "plus", action: .addRecipe),
72 | items: [])
73 |
74 | return Catalog(featured: featured, userCreated: myRecipes)
75 | }
76 |
77 | func userCreated(_ recipes: [Recipe]) -> Catalog {
78 | Catalog(featured: featured,
79 | userCreated: CatalogSection(title: self.userCreated.title,
80 | action: self.userCreated.action,
81 | items: recipes.map(CatalogItem.regular)))
82 | }
83 |
84 | func appending(_ item: CatalogItem) -> Catalog {
85 | Catalog(featured: featured, userCreated: userCreated.appending(item))
86 | }
87 |
88 | func replacing(_ item: CatalogItem, by newItem: CatalogItem) -> Catalog {
89 | Catalog(featured: featured, userCreated: userCreated.replacing(item, by: newItem))
90 | }
91 |
92 | func removing(_ item: CatalogItem) -> Catalog {
93 | Catalog(featured: featured, userCreated: userCreated.removing(item))
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/State/CatalogItem.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import Foundation
3 |
4 | enum CatalogItem: Equatable, Identifiable {
5 | case featured(FeaturedRecipe)
6 | case regular(Recipe)
7 |
8 | private func fold(
9 | _ ifFeatured: (FeaturedRecipe) -> A,
10 | _ ifRegular: (Recipe) -> A
11 | ) -> A {
12 | switch self {
13 | case .featured(let featured): return ifFeatured(featured)
14 | case .regular(let recipe): return ifRegular(recipe)
15 | }
16 | }
17 |
18 | private func fold(
19 | _ ifFeatured: KeyPath,
20 | _ ifRegular: KeyPath
21 | ) -> A {
22 | self.fold({ $0[keyPath: ifFeatured] },
23 | { $0[keyPath: ifRegular] })
24 | }
25 |
26 | var title: String {
27 | self.fold(\.recipe.title,
28 | \.title)
29 | }
30 |
31 | var description: String {
32 | self.fold(\.recipe.description,
33 | \.description)
34 | }
35 |
36 | var dependencies: [Dependency] {
37 | self.fold(\.recipe.dependencies,
38 | \.dependencies)
39 | }
40 |
41 | var isEditable: Bool {
42 | self.fold(constant(false), constant(true))
43 | }
44 |
45 | var recipe: Recipe {
46 | self.fold(\.recipe, \.self)
47 | }
48 |
49 | var id: UUID {
50 | self.fold(\.id, \.id)
51 | }
52 |
53 | func appending(dependency: Dependency) -> CatalogItem {
54 | self.fold(
55 | CatalogItem.featured,
56 | { recipe in
57 | CatalogItem.regular(recipe.appending(dependency: dependency))
58 | }
59 | )
60 | }
61 |
62 | func removing(dependency: Dependency) -> CatalogItem {
63 | self.fold(
64 | CatalogItem.featured,
65 | { recipe in
66 | CatalogItem.regular(recipe.removing(dependency: dependency))
67 | }
68 | )
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/State/CatalogSection.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct CatalogSection: Equatable {
4 | let title: String
5 | let action: CatalogSectionAction?
6 | let items: [CatalogItem]
7 |
8 | init(title: String, action: CatalogSectionAction? = nil, items: [CatalogItem]) {
9 | self.title = title
10 | self.action = action
11 | self.items = items
12 | }
13 |
14 | func copy(items: [CatalogItem]) -> CatalogSection {
15 | CatalogSection(
16 | title: self.title,
17 | action: self.action,
18 | items: items)
19 | }
20 |
21 | func appending(_ item: CatalogItem) -> CatalogSection {
22 | copy(items: self.items + [item])
23 | }
24 |
25 | func replacing(_ item: CatalogItem, by newItem: CatalogItem) -> CatalogSection {
26 | copy(items: self.items.map { current in
27 | (current == item) ? newItem : current
28 | })
29 | }
30 |
31 | func removing(_ item: CatalogItem) -> CatalogSection {
32 | copy(items: self.items.filter { current in
33 | current != item
34 | })
35 | }
36 | }
37 |
38 | struct CatalogSectionAction: Equatable {
39 | let icon: String
40 | let action: CatalogAction
41 | }
42 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/State/Dependency.swift:
--------------------------------------------------------------------------------
1 | struct Dependency: Equatable, Identifiable {
2 | enum Products: Equatable {
3 | case all
4 | case selected(names: [String])
5 | }
6 |
7 | let repository: String
8 | let owner: String
9 | let url: String
10 | let avatar: String
11 | let requirement: Requirement
12 | let products: Products
13 |
14 | var id: String {
15 | "\(url):\(requirement.id)"
16 | }
17 | }
18 |
19 | // MARK: - Codable
20 |
21 | extension Dependency: Codable { }
22 |
23 | extension Dependency.Products: Codable {
24 | private enum CodingKeys: String, CodingKey {
25 | case all
26 | case selected
27 | }
28 |
29 | public func encode(to encoder: Encoder) throws {
30 | var container = encoder.container(keyedBy: CodingKeys.self)
31 |
32 | switch self {
33 | case .all:
34 | try container.encode([String](), forKey: .all)
35 | case .selected(let products):
36 | try container.encode(products, forKey: .selected)
37 | }
38 | }
39 |
40 | public init(from decoder: Decoder) throws {
41 | let container = try decoder.container(keyedBy: CodingKeys.self)
42 |
43 | if let products = try? container.decode([String].self, forKey: .selected) {
44 | self = .selected(names: products)
45 | } else {
46 | self = .all
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/State/FeaturedRecipe.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct FeaturedRecipe: Equatable, Identifiable {
4 | let recipe: Recipe
5 | let backgroundImage: String
6 | let textColor: Color
7 |
8 | var id: UUID {
9 | recipe.id
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/State/Recipe.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct Recipe: Equatable, Identifiable, Codable {
4 | let id: UUID
5 | let title: String
6 | let description: String
7 | let dependencies: [Dependency]
8 |
9 | init(id: UUID = UUID(),
10 | title: String,
11 | description: String,
12 | dependencies: [Dependency]) {
13 | self.id = id
14 | self.title = title
15 | self.description = description
16 | self.dependencies = dependencies
17 | }
18 |
19 | func copy(
20 | id: UUID? = nil,
21 | title: String? = nil,
22 | description: String? = nil,
23 | dependencies: [Dependency]? = nil
24 | ) -> Recipe {
25 | Recipe(
26 | id: id ?? self.id,
27 | title: title ?? self.title,
28 | description: description ?? self.description,
29 | dependencies: dependencies ?? self.dependencies)
30 | }
31 |
32 | func appending(dependency: Dependency) -> Recipe {
33 | if !contains(dependency: dependency) {
34 | return copy(dependencies: self.dependencies + [dependency])
35 | } else {
36 | return replacing(dependency: dependency)
37 | }
38 | }
39 |
40 | func removing(dependency: Dependency) -> Recipe {
41 | copy(dependencies: self.dependencies.filter { dep in
42 | dep != dependency
43 | })
44 | }
45 |
46 | private func contains(dependency: Dependency) -> Bool {
47 | self.dependencies.first { dep in
48 | dep.url == dependency.url
49 | } != nil
50 | }
51 |
52 | private func replacing(dependency: Dependency) -> Recipe {
53 | copy(
54 | dependencies: self.dependencies.map { dep in
55 | (dep.url == dependency.url)
56 | ? dependency
57 | : dep
58 | }
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/State/TagViewModel.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct TagViewModel {
4 | let text: String
5 | let foregroundColor: Color
6 | let backgroundColor: Color
7 |
8 | init(text: String, foregroundColor: Color = .gray, backgroundColor: Color = Color.gray.opacity(0.2)) {
9 | self.text = text
10 | self.foregroundColor = foregroundColor
11 | self.backgroundColor = backgroundColor
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/View/CatalogItemGridView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CatalogItemGridView: View {
4 | let items: [CatalogItem]
5 | let selectedItem: CatalogItem?
6 | let columns: Int
7 | let handle: (CatalogAction) -> Void
8 |
9 | var body: some View {
10 | GridView(
11 | rows: self.rows,
12 | columns: self.columns) { row, column in
13 | self.viewForItem(row: row, column: column)
14 | .aspectRatio(16/9, contentMode: .fit)
15 | .onTapGesture {
16 | if let item = self.item(row: row, column: column) {
17 | self.handle(.select(item: item))
18 | }
19 | }
20 | .safeHoverEffect()
21 | }
22 | }
23 |
24 | private var rows: Int {
25 | Int(ceil(Double(items.count) / Double(columns)))
26 | }
27 |
28 | private func indexFor(row: Int, column: Int) -> Int {
29 | row * self.columns + column
30 | }
31 |
32 | private func item(row: Int, column: Int) -> CatalogItem? {
33 | items[safe: indexFor(row: row, column: column)]
34 | }
35 |
36 | private func viewForItem(row: Int, column: Int) -> some View {
37 | if let item = item(row: row, column: column) {
38 | switch item {
39 | case .regular(let recipe):
40 | return AnyView(
41 | RegularRecipeView(recipe: recipe, isSelected: item == selectedItem)
42 | .contextMenu {
43 | self.duplicateButton(for: item)
44 | self.removeButton(for: item)
45 | })
46 | case .featured(let featured):
47 | return AnyView(
48 | FeaturedRecipeView(featured: featured, isSelected: item == selectedItem)
49 | .contextMenu {
50 | self.duplicateButton(for: item)
51 | })
52 | }
53 | } else {
54 | return AnyView(Color.clear)
55 | }
56 | }
57 |
58 | private func duplicateButton(for item: CatalogItem) -> some View {
59 | Button(action: { self.handle(.duplicate(item: item)) }) {
60 | Text("Duplicate recipe")
61 | Image.duplicate
62 | }
63 | }
64 |
65 | private func removeButton(for item: CatalogItem) -> some View {
66 | Button(action: { self.handle(.remove(item: item)) }) {
67 | Text("Remove recipe")
68 | Image.trash
69 | }
70 | }
71 | }
72 |
73 | #if DEBUG
74 | struct RecipeGridView_Previews: PreviewProvider {
75 | static var previews: some View {
76 | Group {
77 | ScrollView {
78 | CatalogItemGridView(items: sampleFeaturedRecipes, selectedItem: nil, columns: 2) { _ in }
79 | }
80 |
81 | ScrollView {
82 | CatalogItemGridView(items: sampleRecipes, selectedItem: nil, columns: 3) { _ in }
83 | }
84 | }
85 | }
86 | }
87 | #endif
88 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/View/CatalogSectionView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CatalogSectionView: View {
4 | let section: CatalogSection
5 | let selectedItem: CatalogItem?
6 | let columns: Int
7 | let handle: (CatalogAction) -> Void
8 |
9 | var body: some View {
10 | VStack(alignment: .leading) {
11 | sectionTitle(for: self.section)
12 | .padding(.top, 16)
13 | .padding(.horizontal, 16)
14 |
15 | if section.items.isEmpty {
16 | Text("There are no recipes yet.")
17 | .activityStyle()
18 | .multilineTextAlignment(.center)
19 | .frame(maxWidth: .infinity)
20 | .padding(24)
21 | } else {
22 | CatalogItemGridView(
23 | items: section.items,
24 | selectedItem: self.selectedItem,
25 | columns: self.columns,
26 | handle: self.handle)
27 | .padding(.horizontal, 16)
28 | }
29 | }
30 | }
31 |
32 | func sectionTitle(for section: CatalogSection) -> some View {
33 | guard let action = section.action else {
34 | return SectionTitle(title: section.title)
35 | }
36 |
37 | return SectionTitle(title: section.title,
38 | action: .init(icon: Image(systemName: action.icon),
39 | handle: self.handle(action.action)))
40 | }
41 | }
42 |
43 | #if DEBUG
44 | struct CatalogSectionView_Previews: PreviewProvider {
45 | static var previews: some View {
46 | ScrollView {
47 | CatalogSectionView(section: sampleRecipesSection, selectedItem: nil, columns: 2) { _ in }
48 | }
49 | }
50 | }
51 | #endif
52 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/View/FeaturedRecipeView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct FeaturedRecipeView: View {
4 | @Environment(\.colorScheme) var colorScheme
5 |
6 | let featured: FeaturedRecipe
7 | let isSelected: Bool
8 |
9 | var body: some View {
10 | ZStack(alignment: .topLeading) {
11 | Image(featured.backgroundImage)
12 | .resizable()
13 | .mask(RoundedRectangle(cornerRadius: 8))
14 | .if(colorScheme == .light,
15 | then: {
16 | $0.shadow(
17 | color: self.isSelected ?
18 | Color.shadow.opacity(0.6) :
19 | Color.shadow.opacity(0.2),
20 | radius: self.isSelected ? 6 : 2,
21 | x: 1,
22 | y: 1)
23 | })
24 | .if(colorScheme == .dark && isSelected,
25 | then: {
26 | $0.overlay(
27 | RoundedRectangle(cornerRadius: 8)
28 | .stroke(Color.white, lineWidth: 2)
29 | )
30 | })
31 |
32 |
33 | VStack(alignment: .leading, spacing: 4) {
34 |
35 | Text(featured.recipe.title)
36 | .titleStyle()
37 | .foregroundColor(featured.textColor)
38 |
39 | Text(featured.recipe.description)
40 | .font(.callout)
41 | .lineLimit(2)
42 | .foregroundColor(featured.textColor)
43 |
44 | Spacer()
45 |
46 | TagCloud(tags: featured.tags(textColor: featured.textColor))
47 | }
48 | .padding()
49 | }
50 | }
51 | }
52 |
53 | private extension FeaturedRecipe {
54 | func tags(textColor: Color) -> [TagViewModel] {
55 | self.recipe.dependencies.map { dependency in
56 | TagViewModel(
57 | text: dependency.repository,
58 | foregroundColor: textColor,
59 | backgroundColor: textColor.opacity(0.2))
60 | }
61 | }
62 | }
63 |
64 | #if DEBUG
65 | struct FeaturedRecipeView_Previews: PreviewProvider {
66 | static var previews: some View {
67 | FeaturedRecipeView(featured: sampleFeaturedRecipe, isSelected: false)
68 | .aspectRatio(16/9, contentMode: .fit)
69 | .previewLayout(.fixed(width: 300, height: 300))
70 | .padding()
71 | }
72 | }
73 | #endif
74 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/View/RecipeCatalogView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct RecipeCatalogView: View {
4 | let catalog: Catalog
5 | let selectedItem: CatalogItem?
6 | let handle: (CatalogAction) -> Void
7 |
8 | var body: some View {
9 | GeometryReader { geometry in
10 | ScrollView(showsIndicators: false) {
11 | VStack {
12 | ForEach(self.catalog.sections, id: \.title) { section in
13 | CatalogSectionView(
14 | section: section,
15 | selectedItem: self.selectedItem,
16 | columns: self.columns(for: geometry.size),
17 | handle: self.handle)
18 | }
19 | }
20 | }
21 | .transition(.identity)
22 | .animation(nil)
23 | }
24 | }
25 |
26 | private func columns(for size: CGSize) -> Int {
27 | let minimumCardWidth: CGFloat = Card.minimumWidth
28 | return max(Int(floor(size.width / minimumCardWidth)), 1)
29 | }
30 | }
31 |
32 | #if DEBUG
33 | struct RecipeCatalogView_Previews: PreviewProvider {
34 | static var previews: some View {
35 | RecipeCatalogView(catalog: sampleCatalog, selectedItem: nil) { _ in }
36 | .previewLayout(.fixed(width: 910, height: 1024))
37 | }
38 | }
39 | #endif
40 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/View/RegularRecipeView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct RegularRecipeView: View {
4 | let recipe: Recipe
5 | let isSelected: Bool
6 |
7 | var body: some View {
8 | CardView(isSelected: isSelected) {
9 | VStack(alignment: .leading, spacing: 8) {
10 | Text(self.recipe.title)
11 | .titleStyle()
12 | Text(self.recipe.description)
13 | .activityStyle()
14 |
15 | Spacer()
16 |
17 | TagCloud(
18 | tags: self.recipe.tags,
19 | layout: TagCloud.Layouts.multiline(spacing: 4, lines: 2))
20 | }.padding()
21 | }
22 | }
23 | }
24 |
25 | private extension Recipe {
26 | var tags: [TagViewModel] {
27 | self.dependencies.map { dependency in
28 | TagViewModel(text: dependency.repository)
29 | }
30 | }
31 | }
32 |
33 | #if DEBUG
34 | struct RegularRecipeView_Previews: PreviewProvider {
35 | static var previews: some View {
36 | RegularRecipeView(recipe: sampleRecipe, isSelected: false)
37 | .aspectRatio(16/9, contentMode: .fit)
38 | .frame(maxWidth: 350, maxHeight: 300)
39 | .previewLayout(.sizeThatFits)
40 | .padding()
41 | }
42 | }
43 | #endif
44 |
--------------------------------------------------------------------------------
/NefEditorClient/Catalog/View/TagView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct TagView: View {
4 | let tag: TagViewModel
5 |
6 | var body: some View {
7 | Text(tag.text)
8 | .lineLimit(1)
9 | .font(.system(.callout, design: .monospaced))
10 | .foregroundColor(tag.foregroundColor)
11 | .padding(4)
12 | .background(
13 | RoundedRectangle(cornerRadius: 4)
14 | .fill(tag.backgroundColor)
15 | )
16 | }
17 | }
18 |
19 | #if DEBUG
20 | struct TagView_Previews: PreviewProvider {
21 | static var previews: some View {
22 | Group {
23 | TagView(tag: sampleBowTag)
24 | TagView(tag: sampleNefTag)
25 | }
26 | .previewLayout(.sizeThatFits)
27 | .padding(16)
28 | }
29 | }
30 | #endif
31 |
--------------------------------------------------------------------------------
/NefEditorClient/Credits/Component/CreditsComponent.swift:
--------------------------------------------------------------------------------
1 | import BowArch
2 |
3 | typealias CreditsComponent = StoreComponent
4 |
5 | func creditsComponent() -> CreditsComponent {
6 | CreditsComponent(initialState: ()) { _, handle in
7 | CreditsView(handle: handle)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Credits/Logic/CreditsAction.swift:
--------------------------------------------------------------------------------
1 | enum CreditsAction {
2 | case librarySelected(Library)
3 | case dismissCredits
4 | }
5 |
--------------------------------------------------------------------------------
/NefEditorClient/Credits/Logic/CreditsDispatcher.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Bow
3 | import BowArch
4 | import BowEffects
5 |
6 | typealias CreditsDispatcher = StateDispatcher
7 |
8 | let creditsDispatcher = CreditsDispatcher.effectful { input in
9 | switch input {
10 | case .librarySelected(let library):
11 | return open(library: library)
12 | case .dismissCredits:
13 | return EnvIO.pure(dismissModal())^
14 | }
15 | }
16 |
17 | func open(library: Library) -> EnvIO> {
18 | EnvIO.later(.main) {
19 | if let url = library.url {
20 | UIApplication.shared.open(url, completionHandler: nil)
21 | }
22 | }.as(.modify(id)^)^
23 | }
24 |
--------------------------------------------------------------------------------
/NefEditorClient/Credits/State/Library.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum Library {
4 | case bow
5 | case bowArch
6 | case bowOpenAPI
7 | case bowLite
8 |
9 | var url: URL? {
10 | switch self {
11 | case .bow:
12 | return URL(string: "https://bow-swift.io")
13 | case .bowArch:
14 | return URL(string: "https://arch.bow-swift.io")
15 | case .bowOpenAPI:
16 | return URL(string: "https://openapi.bow-swift.io")
17 | case .bowLite:
18 | return URL(string: "https://github.com/bow-swift/bow-lite")
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/NefEditorClient/Credits/View/CreditsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CreditsView: View {
4 | let handle: (CreditsAction) -> Void
5 |
6 | var body: some View {
7 | VStack {
8 | Spacer()
9 | self.version()
10 | self.sponsor()
11 | Spacer()
12 | self.librarySection()
13 | }.padding()
14 | .navigationBarTitle("Credits", displayMode: .inline)
15 | .navigationBarItems(leading:
16 | Button("Cancel") {
17 | self.handle(.dismissCredits)
18 | }.navigationBarButtonStyle()
19 | )
20 | }
21 |
22 | var appVersion: String {
23 | if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
24 | return "Version \(version)"
25 | } else {
26 | return ""
27 | }
28 | }
29 |
30 | func version() -> some View {
31 | Group {
32 | Image.appIcon.resizable()
33 | .frame(width: 256, height: 256)
34 | .mask(RoundedRectangle(cornerRadius: 16))
35 |
36 | Text("nef Playgrounds").largeTitleStyle()
37 |
38 | Text(appVersion)
39 | .activityStyle()
40 | }
41 | }
42 |
43 | func sponsor() -> some View {
44 | HStack {
45 | Text("Proudly sponsored by")
46 | Image.fortySeven.resizable()
47 | .frame(width: 24, height: 24)
48 | }.padding()
49 | }
50 |
51 | func librarySection() -> some View {
52 | Group {
53 | Text("This application is powered by:")
54 | .font(.caption)
55 | .padding(.bottom, 16)
56 |
57 | HStack(alignment: .center, spacing: 4) {
58 | libraryView(image: .bowArch, name: "Bow Arch", library: .bowArch)
59 |
60 | libraryView(image: .bow, name: "Bow", library: .bow)
61 |
62 | libraryView(image: .bowLite, name: "Bow Lite", library: .bowLite)
63 |
64 | libraryView(image: .bowOpenAPI, name: "Bow OpenAPI", library: .bowOpenAPI)
65 | }
66 | }
67 | }
68 |
69 | func libraryView(image: Image, name: String, library: Library) -> some View {
70 | Button(action: { self.handle(.librarySelected(library)) }) {
71 | HStack {
72 | Spacer()
73 |
74 | VStack {
75 | image.resizable()
76 | .frame(width: 40, height: 40)
77 | .mask(RoundedRectangle(cornerRadius: 8))
78 |
79 | Text(name)
80 | .font(.callout)
81 | }
82 |
83 | Spacer()
84 | }
85 | }.buttonStyle(LibraryButtonStyle())
86 | }
87 | }
88 |
89 | #if DEBUG
90 | struct CreditsView_Previews: PreviewProvider {
91 | static var previews: some View {
92 | NavigationView {
93 | CreditsView { _ in }
94 | }.navigationViewStyle(StackNavigationViewStyle())
95 | .previewLayout(.fixed(width: 500, height: 500))
96 | }
97 | }
98 | #endif
99 |
--------------------------------------------------------------------------------
/NefEditorClient/DeepLink/Extensions/URL+Recipe.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | func schemeRecipe(incomingURL: URL) -> Recipe? {
4 | guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
5 | let action = incomingURL.lastPathComponent.isEmpty ? components.host : incomingURL.lastPathComponent,
6 | let params = components.queryItems else { return nil }
7 |
8 | switch action {
9 | case "recipe":
10 | return params.recipe
11 | default:
12 | return nil
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/NefEditorClient/DeepLink/Logic/DeepLinkAction.swift:
--------------------------------------------------------------------------------
1 | import BowOptics
2 |
3 | enum DeepLinkAction: AutoPrism {
4 | case generateRecipe(Recipe)
5 | case regularInitialization
6 | }
7 |
--------------------------------------------------------------------------------
/NefEditorClient/DeepLink/Logic/DeepLinkDispatcher.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import BowEffects
3 | import BowArch
4 |
5 | typealias DeepLinkDispatcher = StateDispatcher
6 |
7 | let deepLinkDispatcher = DeepLinkDispatcher.workflow { action in
8 | switch action {
9 | case .generateRecipe(let recipe):
10 | return initialLoad(recipe)
11 | case .regularInitialization:
12 | return [initialLoad()]
13 | }
14 | }
15 |
16 | func addNewRecipe(_ recipe: Recipe) -> State {
17 | .modify { state in
18 | state.addRecipe(recipe)
19 | }^
20 | }
21 |
22 | func clearDeepLink() -> State {
23 | .modify { state in
24 | state.copy(deepLinkState: DeepLinkState.none)
25 | }^
26 | }
27 |
28 | func generatePlayground(newRecipe recipe: Recipe) -> EnvIO> {
29 | EnvIO.pure(
30 | clearDeepLink()
31 | .followedBy(addNewRecipe(recipe))
32 | .followedBy(generatePlayground(for: .regular(recipe)))^
33 | )^
34 | }
35 |
36 | func initialLoad(_ recipe: Recipe) -> [EnvIO>] {
37 | [
38 | initialLoad(),
39 | generatePlayground(newRecipe: recipe),
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/NefEditorClient/DeepLink/State/DeepLinkState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum DeepLinkState: Equatable {
4 | case none
5 | case recipe(Recipe)
6 | }
7 |
--------------------------------------------------------------------------------
/NefEditorClient/Detail/Component/CatalogDetailComponent.swift:
--------------------------------------------------------------------------------
1 | import BowArch
2 |
3 | typealias CatalogDetailComponent = StoreComponent
4 |
5 | func catalogDetailComponent(state: CatalogItem) -> CatalogDetailComponent {
6 | CatalogDetailComponent(
7 | initialState: state,
8 | render: CatalogItemDetailView.init)
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Detail/Logic/CatalogDetailAction.swift:
--------------------------------------------------------------------------------
1 | enum CatalogDetailAction {
2 | case edit(item: CatalogItem)
3 | case searchDependency
4 | case remove(Dependency)
5 | case dismissDetail
6 | case generatePlayground(for: CatalogItem)
7 | }
8 |
--------------------------------------------------------------------------------
/NefEditorClient/Detail/Logic/CatalogDetailDispatcher.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import BowArch
3 |
4 | typealias CatalogDetailDispatcher = StateDispatcher
5 |
6 | let catalogDetailDispatcher = CatalogDetailDispatcher.pure { action in
7 | switch action {
8 | case .edit(item: let item):
9 | return edit(item: item)
10 | case .searchDependency:
11 | return searchDependency()
12 | case .remove(let dependency):
13 | return remove(dependency: dependency)
14 | case .dismissDetail:
15 | return clearSelection()
16 | case .generatePlayground(for: let item):
17 | return generatePlayground(for: item)
18 | }
19 | }
20 |
21 | func edit(item: CatalogItem) -> State {
22 | if case let .regular(recipe) = item {
23 | return .modify { state in
24 | state.copy(modalState: .edit(.editRecipe(recipe)))
25 | }^
26 | } else {
27 | return .modify(id)^
28 | }
29 | }
30 |
31 | func searchDependency() -> State {
32 | .modify { state in
33 | state.copy(panelState: .search)
34 | }^
35 | }
36 |
37 | func remove(dependency: Dependency) -> State {
38 | .modify { state in
39 | if let selected = state.selectedItem {
40 | let newSelected = selected.removing(dependency: dependency)
41 | let newCatalog = state.catalog.replacing(selected, by: newSelected)
42 | return state.copy(catalog: newCatalog, selectedItem: newSelected)
43 | } else {
44 | return state
45 | }
46 | }^
47 | }
48 |
49 | func clearSelection() -> State {
50 | .modify { state in
51 | if state.panelState == .search {
52 | return state.copy(panelState: .catalog)
53 | } else {
54 | return state.copy(selectedItem: .some(nil))
55 | }
56 | }^
57 | }
58 |
59 | func generatePlayground(for item: CatalogItem) -> State {
60 | .modify { state in
61 | state.copy(modalState: .generation(.initial(state.authenticationState, item)))
62 | }^
63 | }
64 |
--------------------------------------------------------------------------------
/NefEditorClient/Detail/View/CatalogItemDetailView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct CatalogItemDetailView: View {
4 | let item: CatalogItem
5 | let handle: (CatalogDetailAction) -> Void
6 |
7 | var body: some View {
8 | CardView {
9 | VStack(alignment: .leading, spacing: 8) {
10 | HStack {
11 | Text(self.item.title)
12 | .largeTitleStyle()
13 |
14 | Spacer()
15 |
16 | if self.item.isEditable{
17 | Button(action: {
18 | self.handle(.edit(item: self.item))
19 | }) {
20 | Image.pencil
21 | }
22 | .buttonStyle(ActionButtonStyle())
23 | .offset(y: 4)
24 | }
25 |
26 | Button(action: {
27 | self.handle(.dismissDetail)
28 | }) {
29 | Image.close
30 | }.buttonStyle(ActionButtonStyle())
31 | .offset(y: 4)
32 | }
33 |
34 | Text(self.item.description)
35 | .foregroundColor(.gray)
36 |
37 | Rectangle.separator
38 | .padding(.top, 8)
39 |
40 | HStack(alignment: .center) {
41 | Text("Dependencies")
42 | .titleStyle()
43 |
44 | Spacer()
45 |
46 | if self.item.isEditable {
47 | Button(action: {
48 | self.handle(.searchDependency)
49 | }) {
50 | Image.plus
51 | }
52 | .buttonStyle(ActionButtonStyle())
53 | .alignmentGuide(.firstTextBaseline) { d in d[.bottom] * 0.82 }
54 | }
55 | }.padding(.top, 24)
56 |
57 | DependencyListView(
58 | dependencies: self.item.dependencies,
59 | isEditable: self.item.isEditable
60 | ) { dependency in
61 | self.handle(.remove(dependency))
62 | }
63 |
64 | Button(action: { self.handle(.generatePlayground(for: self.item)) }) {
65 | HStack(spacing: 12) {
66 | Image.nefClear
67 | .resizable()
68 | .frame(width: 32, height: 32)
69 | Text("Create Swift Playground")
70 | }
71 | }.frame(maxWidth: .infinity)
72 | .buttonStyle(TextButtonStyle())
73 | }.padding()
74 | }
75 | }
76 | }
77 |
78 | #if DEBUG
79 | struct CatalogItemDetailView_Previews: PreviewProvider {
80 | static var previews: some View {
81 | CatalogItemDetailView(item: .regular(sampleRecipe)) { _ in }
82 | .padding()
83 | }
84 | }
85 | #endif
86 |
--------------------------------------------------------------------------------
/NefEditorClient/Detail/View/DependencyListView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct DependencyListView: View {
4 | let dependencies: [Dependency]
5 | let isEditable: Bool
6 | let onRemoveDependency: (Dependency) -> Void
7 |
8 | var body: some View {
9 | List {
10 | ForEach(Array(dependencies.enumerated()), id: \.element.id) { item in
11 | DependencyView(dependency: item.element)
12 | .if(self.isEditable) { view in
13 | view.contextMenu {
14 | Button(action: { self.onRemoveDependency(item.element) }) {
15 | Text("Delete dependency")
16 | Image.trash
17 | }
18 | }
19 | }
20 | }.if(isEditable) { view in
21 | view.onDelete { indexSet in
22 | if let index = indexSet.first {
23 | self.onRemoveDependency(self.dependencies[index])
24 | }
25 | }
26 | }.listRowBackground(Color.card)
27 | }.background(Color.clear)
28 | }
29 | }
30 |
31 | #if DEBUG
32 | struct DependencyListView_Previews: PreviewProvider {
33 | static var previews: some View {
34 | DependencyListView(dependencies: Array(repeating: bowDependency, count: 15),
35 | isEditable: false) { _ in }
36 | }
37 | }
38 | #endif
39 |
--------------------------------------------------------------------------------
/NefEditorClient/Detail/View/DependencyView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct DependencyView: View {
4 | let dependency: Dependency
5 |
6 | var body: some View {
7 | HStack(alignment: .center) {
8 | VStack(alignment: .leading, spacing: 8) {
9 | Text(dependency.repository)
10 | .font(.body)
11 |
12 | HStack {
13 | AvatarView(avatar: dependency.avatar)
14 | .frame(width: 24, height: 24)
15 | .mask(Circle())
16 |
17 | Text(dependency.owner)
18 | .font(.caption)
19 | .foregroundColor(.gray)
20 | }
21 | }
22 | Spacer()
23 | TagView(tag: TagViewModel(text: dependency.requirement.title))
24 | }.padding(.vertical, 4)
25 | }
26 | }
27 |
28 | #if DEBUG
29 | struct DependencyView_Previews: PreviewProvider {
30 | static var previews: some View {
31 | Group {
32 | DependencyView(dependency: bowDependency)
33 | DependencyView(dependency: bowArchDependency)
34 | }.previewLayout(.sizeThatFits)
35 | }
36 | }
37 | #endif
38 |
--------------------------------------------------------------------------------
/NefEditorClient/Edit/Component/EditComponent.swift:
--------------------------------------------------------------------------------
1 | import BowArch
2 |
3 | typealias EditComponent = StoreComponent
4 |
5 | func editComponent(state: EditState) -> EditComponent {
6 | EditComponent(
7 | initialState: state,
8 | render: EditRecipeMetadataView.init)
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Edit/Logic/EditAction.swift:
--------------------------------------------------------------------------------
1 | enum EditAction {
2 | case saveRecipe(title: String, description: String)
3 | case dismissEdition
4 | }
5 |
--------------------------------------------------------------------------------
/NefEditorClient/Edit/Logic/EditDispatcher.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import BowArch
3 |
4 | typealias EditDispatcher = StateDispatcher
5 |
6 | let editDispatcher = EditDispatcher.pure { input in
7 | switch input {
8 | case let .saveRecipe(title: title, description: description):
9 | return saveRecipe(title: title, description: description)
10 |
11 | case .dismissEdition:
12 | return dismissModal()
13 | }
14 | }
15 |
16 | func saveRecipe(title: String, description: String) -> State {
17 | .modify { state in
18 | guard let editState = state.modalState.editState else { return state }
19 |
20 | switch editState {
21 | case .newRecipe:
22 | let recipe = createRecipe(title: title, description: description)
23 | return state.addRecipe(recipe)
24 |
25 | case .editRecipe(let recipe):
26 | let editedRecipe = edit(recipe: recipe, title: title, description: description)
27 | let newCatalog = state.catalog.replacing(.regular(recipe), by: .regular(editedRecipe))
28 | return state.copy(modalState: .noModal, catalog: newCatalog, selectedItem: .regular(editedRecipe))
29 | }
30 | }^
31 | }
32 |
33 | func createRecipe(title: String, description: String) -> Recipe {
34 | .init(title: title.isEmpty ? "Empty title" : title,
35 | description: description,
36 | dependencies: [])
37 | }
38 |
39 | func edit(recipe: Recipe, title: String, description: String) -> Recipe {
40 | recipe.copy(title: title.isEmpty ? recipe.title : title,
41 | description: description)
42 | }
43 |
44 | extension AppState {
45 | func addRecipe(_ recipe: Recipe) -> Self {
46 | let catalogItem: CatalogItem = .regular(recipe)
47 | let newCatalog = catalog.appending(catalogItem)
48 | return self.copy(modalState: .noModal, catalog: newCatalog, selectedItem: catalogItem)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/NefEditorClient/Edit/State/EditState.swift:
--------------------------------------------------------------------------------
1 | enum EditState: Equatable {
2 | case newRecipe
3 | case editRecipe(Recipe)
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/NefEditorClient/Edit/View/EditRecipeMetadataView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct EditRecipeMetadataView: View {
4 | let state: EditState
5 | @State var title: String
6 | @State var description: String
7 | let handle: (EditAction) -> Void
8 |
9 | init(state: EditState, handle: @escaping (EditAction) -> Void) {
10 | self.state = state
11 |
12 | switch state {
13 | case .editRecipe(let recipe):
14 | self._title = State(initialValue: recipe.title)
15 | self._description = State(initialValue: recipe.description)
16 | default:
17 | self._title = State(initialValue: "")
18 | self._description = State(initialValue: "")
19 | }
20 |
21 | self.handle = handle
22 | }
23 |
24 | var body: some View {
25 | ZStack {
26 | Color.form.edgesIgnoringSafeArea(.all)
27 | Form {
28 | Section(header: Text("Title")) {
29 | TextField("Enter a title for your nef recipe", text: $title)
30 | }
31 |
32 | Section(header: Text("Description")) {
33 | TextField("Enter a description for your nef recipe", text: $description)
34 | }
35 | }
36 | }.navigationBarItems(
37 | leading:
38 | Button("Cancel") {
39 | self.handle(.dismissEdition)
40 | }.navigationBarButtonStyle(),
41 | trailing:
42 | Button("Save") {
43 | self.handle(.saveRecipe(title: self.title, description: self.description))
44 | }.navigationBarButtonStyle()
45 | )
46 | .navigationBarTitle(self.navigationTitle)
47 | }
48 |
49 | private var navigationTitle: String {
50 | switch state {
51 | case .newRecipe:
52 | return "New recipe"
53 | case .editRecipe(_):
54 | return "Edit recipe"
55 | }
56 | }
57 | }
58 |
59 | #if DEBUG
60 | struct EditRecipeMetadataView_Preview: PreviewProvider {
61 | static var previews: some View {
62 | NavigationView {
63 | EditRecipeMetadataView(
64 | state: .newRecipe) { _ in }
65 | .navigationBarTitle("Edit recipe")
66 | }
67 | .navigationViewStyle(StackNavigationViewStyle())
68 | }
69 | }
70 | #endif
71 |
--------------------------------------------------------------------------------
/NefEditorClient/Extensions/Array+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Array {
4 | public subscript(safe index: Int) -> Element? {
5 | (index >= 0 && index < self.count) ?
6 | self[index] :
7 | nil
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Extensions/Bundle+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Bundle {
4 | var version: String { "\(appVersion) [\(buildVersion)]" }
5 |
6 | var appVersion: String {
7 | infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
8 | }
9 |
10 | var buildVersion: String {
11 | infoDictionary?["CFBundleVersion"] as? String ?? ""
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/NefEditorClient/Extensions/URLSession+IO.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Bow
3 | import BowEffects
4 | import NefEditorError
5 |
6 | enum DownloadTaskError: Error {
7 | case malformedURL
8 | case dataCorrupted
9 | case serverError
10 | case errorResponse(Reason)
11 | }
12 |
13 |
14 | extension URLSession {
15 | func downloadDataTaskIO(with request: URLRequest) -> IO, Data> {
16 | func downloadTaskHandler(data: Data, statusCode: Int) -> Either, Data> {
17 | switch statusCode {
18 | case 200 ..< 300:
19 | return .right(data)
20 | default:
21 | if let error = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
22 | return .left(.errorResponse(error.reason))
23 | } else {
24 | return .left(.serverError)
25 | }
26 | }
27 | }
28 |
29 | return IO.async { callback in
30 | self.downloadTask(with: request) { url, response, error in
31 | guard let httpResponse = response as? HTTPURLResponse else {
32 | callback(.left(.malformedURL)); return
33 | }
34 |
35 | guard let url = url, let data = try? Data(contentsOf: url) else {
36 | callback(.left(.dataCorrupted)); return
37 | }
38 |
39 | let either = downloadTaskHandler(data: data, statusCode: httpResponse.statusCode)
40 | return callback(either)
41 | }.resume()
42 | }^
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/NefEditorClient/FAQ/Component/FAQComponent.swift:
--------------------------------------------------------------------------------
1 | import BowArch
2 |
3 | typealias FAQComponent = StoreComponent
4 |
5 | func faqComponent() -> FAQComponent {
6 | FAQComponent(initialState: ()) { _, handle in
7 | FAQView(handle: handle)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/FAQ/Logic/FAQAction.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum FAQAction {
4 | case visitNef
5 | case visit47Degrees
6 | case visitBowSwift
7 | case followBowSwift
8 | case dismissFAQ
9 |
10 | var url: URL? {
11 | switch self {
12 | case .visitNef:
13 | return URL(string: "https://github.com/bow-swift/nef")
14 | case .visit47Degrees:
15 | return URL(string: "https://www.47deg.com")
16 | case .visitBowSwift:
17 | return URL(string: "https://github.com/bow-swift")
18 | case .followBowSwift:
19 | return URL(string: "https://www.twitter.com/bow_swift")
20 | default:
21 | return nil
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/NefEditorClient/FAQ/Logic/FAQDispatcher.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Bow
3 | import BowEffects
4 | import BowArch
5 | import Foundation
6 |
7 | typealias FAQDispatcher = StateDispatcher
8 |
9 | let faqDispatcher = FAQDispatcher.effectful { input in
10 | switch input {
11 | case .dismissFAQ:
12 | return EnvIO.pure(dismissModal())^
13 | default:
14 | return open(url: input.url)
15 | }
16 | }
17 |
18 | func open(url: URL?) -> EnvIO> {
19 | EnvIO.later(.main) {
20 | if let url = url {
21 | UIApplication.shared.open(url, completionHandler: nil)
22 | }
23 | }.as(.modify(id)^)^
24 | }
25 |
--------------------------------------------------------------------------------
/NefEditorClient/Generation/Component/GenerationComponent.swift:
--------------------------------------------------------------------------------
1 | import BowArch
2 |
3 | typealias GenerationComponent = StoreComponent
4 |
5 | func generationComponent(state: GenerationState) -> GenerationComponent {
6 | GenerationComponent(
7 | initialState: state) { state, handle in
8 | GenerationView(state: state, handle: handle)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/NefEditorClient/Generation/Logic/GenerationAction.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import Foundation
3 |
4 | enum GenerationAction {
5 | case authenticationResult(Either, item: CatalogItem)
6 | case dismissGeneration
7 | case generate(item: CatalogItem, token: String)
8 | case sharePlayground
9 | case dismissShare
10 | }
11 |
--------------------------------------------------------------------------------
/NefEditorClient/Generation/State/GenerationState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum GenerationState: Equatable {
4 | case notGenerating
5 | case authenticating
6 | case initial(AuthenticationState, CatalogItem)
7 | case generating(CatalogItem)
8 | case finished(CatalogItem, URL, SharingState)
9 | case error(GenerationError)
10 |
11 | var shouldShowShare: Bool {
12 | guard case let .finished(_, _, sharingState) = self else {
13 | return false
14 | }
15 | return sharingState == .sharing
16 | }
17 | }
18 |
19 | enum SharingState: Equatable {
20 | case notSharing
21 | case sharing
22 | }
23 |
24 | enum GenerationError: Error, Equatable, CustomStringConvertible {
25 | case invalidAuthentication
26 | case invalidBearer
27 | case networkFailure
28 | case dataCorrupted
29 |
30 | var description: String {
31 | switch self {
32 | case .invalidAuthentication:
33 | return "There was a problem with your authentication. Please, try again later."
34 | case .invalidBearer:
35 | return "Oops! Your session has expired, please login again."
36 | case .networkFailure:
37 | return "An error ocurred while obtaining your Swift Playground. Please, try again later."
38 | case .dataCorrupted:
39 | return "Data for this Playground is corrupted. Please, try to generate it again."
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/NefEditorClient/Generation/View/GenerationErrorView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct GenerationErrorView: View {
4 | let message: String
5 |
6 | var body: some View {
7 | VStack {
8 | AnimationView(animation: .init(lottie: .generalError))
9 | .aspectRatio(contentMode: .fit)
10 | .frame(width: 256, height: 256)
11 |
12 | Text(message).activityStyle()
13 | Spacer()
14 | }.padding(.top, 56)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/NefEditorClient/Generation/View/GenerationPlaygroundBookView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct GenerationPlaygroundBookView: View {
4 | let playgroundName: String
5 |
6 | var body: some View {
7 | VStack {
8 | AnimationView(animation: AnimationView.Animation(lottie: .playgroundLoading, isLoop: true))
9 | .aspectRatio(contentMode: .fill)
10 | .frame(height: 448)
11 |
12 | Text(
13 | """
14 | Generating Swift Playground '\(playgroundName)'...
15 |
16 |
17 | Please wait, this may take several minutes.
18 | """
19 | ).activityStyle()
20 | .multilineTextAlignment(.center)
21 |
22 | Spacer()
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/NefEditorClient/Generation/View/GenerationSigninView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct GenerationSigninView: View {
4 | var body: some View {
5 | VStack {
6 | AnimationView(animation: AnimationView.Animation(lottie: .generalLoading, isLoop: true))
7 | .aspectRatio(contentMode: .fill)
8 | .frame(width: 400, height: 200)
9 |
10 | Text("Signing in...")
11 | .activityStyle()
12 | Spacer()
13 | }.padding(.top, 56)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/NefEditorClient/Generation/View/GenerationSuccessView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct GenerationSuccessView: View {
4 | let animation: LottieAnimation
5 |
6 | var body: some View {
7 | AnimationView(animation: .init(lottie: animation))
8 | .aspectRatio(contentMode: .fit)
9 | .frame(width: 256, height: 256)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/NefEditorClient/GitHubExtensions/Data+Equatable.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 |
3 | extension Repository: Equatable {
4 | public static func ==(lhs: Repository, rhs: Repository) -> Bool {
5 | lhs.fullName == rhs.fullName
6 | }
7 | }
8 |
9 | extension Tag: Equatable {
10 | public static func ==(lhs: Tag, rhs: Tag) -> Bool {
11 | lhs.name == rhs.name
12 | }
13 | }
14 |
15 | extension Branch: Equatable {
16 | public static func ==(lhs: Branch, rhs: Branch) -> Bool {
17 | lhs.name == rhs.name
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/NefEditorClient/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleURLTypes
20 |
21 |
22 | CFBundleTypeRole
23 | Viewer
24 | CFBundleURLName
25 | com.47deg.nef-playgrounds
26 | CFBundleURLSchemes
27 |
28 | nef-playgrounds
29 |
30 |
31 |
32 | CFBundleVersion
33 | $(CURRENT_PROJECT_VERSION)
34 | ITSAppUsesNonExemptEncryption
35 |
36 | LSApplicationCategoryType
37 | public.app-category.developer-tools
38 | LSRequiresIPhoneOS
39 |
40 | UIApplicationSceneManifest
41 |
42 | UIApplicationSupportsMultipleScenes
43 |
44 | UISceneConfigurations
45 |
46 | UIWindowSceneSessionRoleApplication
47 |
48 |
49 | UISceneConfigurationName
50 | Default Configuration
51 | UISceneDelegateClassName
52 | $(PRODUCT_MODULE_NAME).SceneDelegate
53 |
54 |
55 |
56 |
57 | UILaunchStoryboardName
58 | LaunchScreen
59 | UIRequiredDeviceCapabilities
60 |
61 | armv7
62 |
63 | UIRequiresFullScreen
64 |
65 | UIStatusBarHidden~ipad
66 |
67 | UISupportedInterfaceOrientations
68 |
69 | UIInterfaceOrientationPortrait
70 | UIInterfaceOrientationLandscapeLeft
71 | UIInterfaceOrientationLandscapeRight
72 |
73 | UISupportedInterfaceOrientations~ipad
74 |
75 | UIInterfaceOrientationPortrait
76 | UIInterfaceOrientationPortraitUpsideDown
77 | UIInterfaceOrientationLandscapeLeft
78 | UIInterfaceOrientationLandscapeRight
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/Component/AppComponent.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GitHub
3 | import NefAPI
4 | import Bow
5 | import BowArch
6 | import BowOptics
7 | import BowEffects
8 |
9 | typealias AppComponent =
10 | StoreComponent<
11 | AppDependencies,
12 | AppState,
13 | AppAction,
14 | AppView
15 | >
16 |
17 | typealias AppComponentView = AppComponent
18 |
19 | func appComponent(urlContexts: Set) -> AppComponentView {
20 | let deepLinkState = urlContexts.first.map(\.url).flatMap(schemeRecipe).flatMap(DeepLinkState.recipe) ?? .none
21 | return appComponent(deepLinkState: deepLinkState)
22 | }
23 |
24 | func appComponent(userActivity: NSUserActivity) -> AppComponentView {
25 | let deepLinkState = userActivity.webpageURL.flatMap(schemeRecipe).flatMap(DeepLinkState.recipe) ?? .none
26 | return appComponent(deepLinkState: deepLinkState)
27 | }
28 |
29 | fileprivate func appComponent(deepLinkState: DeepLinkState) -> AppComponentView {
30 | let ref = IORef.unsafe(nil)
31 | let gitHubConfig = makeGitHubConfig()
32 | let nefConfig = makeNefConfig()
33 | let persistence = ICloudPersistence()
34 | let dependencies = AppDependencies(persistence: persistence,
35 | gitHubConfig: gitHubConfig,
36 | nefConfig: nefConfig)
37 |
38 | let initialState = AppState(
39 | panelState: .catalog,
40 | modalState: .noModal,
41 | searchState: SearchState(loadingState: .initial, modalState: .noModal),
42 | deepLinkState: deepLinkState,
43 | catalog: Catalog.initial,
44 | selectedItem: nil,
45 | iCloudStatus: .enabled,
46 | iCloudAlert: .hidden,
47 | authenticationState: .unauthenticated)
48 |
49 | return AppComponent (
50 | initialState: initialState,
51 | environment: dependencies,
52 | dispatcher: appDispatcher
53 | ) { state, handle in
54 | AppView(
55 | state: state,
56 |
57 | catalog: catalogComponent(catalog: state.catalog, selectedItem: state.selectedItem)
58 | .using(handle, transformInput: AppAction.prism(for: AppAction.catalogAction)),
59 |
60 | search: searchComponent(config: gitHubConfig, state: state.searchState)
61 | .using(handle, transformInput: AppAction.prism(for: AppAction.searchAction)),
62 |
63 | detail: { item in
64 | catalogDetailComponent(state: item)
65 | .using(handle, transformInput: AppAction.prism(for: AppAction.catalogDetailAction))
66 | },
67 |
68 | modal: { state in
69 | appModalComponent(state: state)
70 | .using(handle, transformInput: Prism.identity)
71 | },
72 |
73 | handle: handle)
74 | }.onEffect { component in
75 | let oldRecipes = IO.var()
76 | let newRecipes = component.store().state.catalog.userCreated.items.map(\.recipe)
77 |
78 | return binding(
79 | oldRecipes <- ref.get(),
80 | |<-((oldRecipes.get != newRecipes) ?
81 | persist(state: component.store().state).provide(persistence) :
82 | IO.lazy()),
83 | |<-ref.set(newRecipes),
84 | yield: ())
85 | }
86 | }
87 |
88 | func makeGitHubConfig() -> GitHub.API.Config {
89 | GitHub.API.Config(basePath: "https://api.github.com")
90 | }
91 |
92 | func makeNefConfig() -> NefAPI.API.Config {
93 | let configuration = URLSessionConfiguration.default
94 | configuration.timeoutIntervalForRequest = 420
95 | configuration.timeoutIntervalForResource = 420
96 |
97 | let session = URLSession(configuration: configuration)
98 |
99 | return NefAPI.API.Config(
100 | basePath: "https://nef.47deg.com",
101 | session: session)
102 | .appending(contentType: .json)
103 | }
104 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/Component/AppModalComponent.swift:
--------------------------------------------------------------------------------
1 | import BowArch
2 |
3 | typealias AppModalComponent = StoreComponent>
4 |
5 | func appModalComponent(state: AppModalState) -> AppModalComponent {
6 | AppModalComponent(initialState: state) { state, handle in
7 | AppModalView(
8 | state: state,
9 |
10 | editView: { editState in
11 | editComponent(state: editState)
12 | .using(handle, transformInput: AppAction.prism(for: AppAction.editAction))
13 | },
14 |
15 | generationView: { generationState in
16 | generationComponent(state: generationState)
17 | .using(handle, transformInput: AppAction.prism(for: AppAction.generationAction))
18 | },
19 |
20 | creditsView: creditsComponent()
21 | .using(handle, transformInput: AppAction.prism(for: AppAction.creditsAction)),
22 |
23 | faqView: faqComponent()
24 | .using(handle, transformInput: AppAction.prism(for: AppAction.faqAction)),
25 |
26 | whatsNewView: whatsNewComponent()
27 | .using(handle, transformInput: AppAction.prism(for: AppAction.whatsNewAction))
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/Dependencies/AppDependencies.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import NefAPI
3 |
4 | struct AppDependencies {
5 | let persistence: Persistence
6 | let gitHubConfig: GitHub.API.Config
7 | let nefConfig: NefAPI.API.Config
8 | }
9 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/Logic/AppAction.swift:
--------------------------------------------------------------------------------
1 | import BowOptics
2 |
3 | enum AppAction: AutoPrism {
4 | case catalogAction(CatalogAction)
5 | case searchAction(SearchAction)
6 | case editAction(EditAction)
7 | case catalogDetailAction(CatalogDetailAction)
8 | case generationAction(GenerationAction)
9 | case creditsAction(CreditsAction)
10 | case faqAction(FAQAction)
11 | case whatsNewAction(WhatsNewAction)
12 | case initialLoad(DeepLinkAction)
13 | case dismissModal
14 | case showICloudAlert
15 | case dismissICloudAlert
16 | case showSettings
17 | case showCredits
18 | case showFAQ
19 | case showWhatsNew
20 | }
21 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/Logic/AppDispatcher.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import BowEffects
3 | import BowOptics
4 | import BowArch
5 |
6 | typealias AppDispatcher = StateDispatcher
7 |
8 | let appDispatcher: AppDispatcher = mainDispatcher
9 | .widen(transformEnvironment: \.persistence)
10 |
11 | .combine(catalogDispatcher.widen(
12 | transformEnvironment: id,
13 | transformInput: AppAction.prism(for: AppAction.catalogAction)))
14 |
15 | .combine(editDispatcher.widen(
16 | transformEnvironment: id,
17 | transformInput: AppAction.prism(for: AppAction.editAction)))
18 |
19 | .combine(catalogDetailDispatcher.widen(
20 | transformEnvironment: id,
21 | transformInput: AppAction.prism(for: AppAction.catalogDetailAction)))
22 |
23 | .combine(addDependencyDispatcher.widen(
24 | transformEnvironment: id,
25 | transformInput: AppAction.prism(for: AppAction.searchAction) +
26 | SearchAction.prism(for: SearchAction.repositoryDetailAction)))
27 |
28 | .combine(searchDispatcher.widen(
29 | transformEnvironment: \.gitHubConfig,
30 | transformState: AppState.searchStateLens,
31 | transformInput: AppAction.prism(for: AppAction.searchAction)))
32 |
33 | .combine(generationDispatcher.widen(
34 | transformEnvironment: \.nefConfig,
35 | transformInput: AppAction.prism(for: AppAction.generationAction)))
36 |
37 | .combine(creditsDispatcher.widen(
38 | transformEnvironment: id,
39 | transformInput: AppAction.prism(for: AppAction.creditsAction)))
40 |
41 | .combine(faqDispatcher.widen(
42 | transformEnvironment: id,
43 | transformInput: AppAction.prism(for: AppAction.faqAction)))
44 |
45 | .combine(whatsNewDispatcher.widen(
46 | transformEnvironment: \.persistence,
47 | transformInput: AppAction.prism(for: AppAction.whatsNewAction)))
48 |
49 | .combine(deepLinkDispatcher.widen(
50 | transformEnvironment: \.persistence,
51 | transformInput: AppAction.prism(for: AppAction.initialLoad)))
52 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/State/AppModalState.swift:
--------------------------------------------------------------------------------
1 | enum AppModalState: Equatable {
2 | case noModal
3 | case edit(EditState)
4 | case credits
5 | case generation(GenerationState)
6 | case faq
7 | case whatsNew
8 |
9 | var editState: EditState? {
10 | guard case let .edit(state) = self else {
11 | return nil
12 | }
13 | return state
14 | }
15 |
16 | var generationState: GenerationState? {
17 | guard case let .generation(state) = self else {
18 | return nil
19 | }
20 | return state
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/State/AppState.swift:
--------------------------------------------------------------------------------
1 | import BowOptics
2 |
3 | struct AppState: Equatable {
4 | let panelState: PanelState
5 | let modalState: AppModalState
6 | let searchState: SearchState
7 | let deepLinkState: DeepLinkState
8 | let catalog: Catalog
9 | let selectedItem: CatalogItem?
10 | let iCloudStatus: ICloudStatus
11 | let iCloudAlert: ICloudAlert
12 | let authenticationState: AuthenticationState
13 |
14 | func copy(
15 | panelState: PanelState? = nil,
16 | modalState: AppModalState? = nil,
17 | searchState: SearchState? = nil,
18 | deepLinkState: DeepLinkState? = nil,
19 | catalog: Catalog? = nil,
20 | selectedItem: CatalogItem?? = nil,
21 | iCloudStatus: ICloudStatus? = nil,
22 | iCloudAlert: ICloudAlert? = nil,
23 | authenticationState: AuthenticationState? = nil
24 | ) -> AppState {
25 | AppState(
26 | panelState: panelState ?? self.panelState,
27 | modalState: modalState ?? self.modalState,
28 | searchState: searchState ?? self.searchState,
29 | deepLinkState: deepLinkState ?? self.deepLinkState,
30 | catalog: catalog ?? self.catalog,
31 | selectedItem: selectedItem ?? self.selectedItem,
32 | iCloudStatus: iCloudStatus ?? self.iCloudStatus,
33 | iCloudAlert: iCloudAlert ?? self.iCloudAlert,
34 | authenticationState: authenticationState ?? self.authenticationState
35 | )
36 | }
37 |
38 | static var searchStateLens: Lens {
39 | Lens(
40 | get: { app in app.searchState },
41 | set: { app, search in app.copy(searchState: search) }
42 | )
43 | }
44 |
45 | static var catalogLens: Lens {
46 | Lens(
47 | get: { app in (app.catalog, app.selectedItem) },
48 | set: { app, new in app.copy(catalog: new.0, selectedItem: new.1) }
49 | )
50 | }
51 |
52 | static var selectedItemLens: Lens {
53 | Lens(
54 | get: { app in app.selectedItem },
55 | set: { app, item in app.copy(selectedItem: item) })
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/State/ICloudStatus.swift:
--------------------------------------------------------------------------------
1 | enum ICloudStatus {
2 | case enabled
3 | case disabled
4 | }
5 |
6 | enum ICloudAlert {
7 | case shown
8 | case hidden
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/State/PanelState.swift:
--------------------------------------------------------------------------------
1 | enum PanelState: Equatable {
2 | case catalog
3 | case search
4 | }
5 |
--------------------------------------------------------------------------------
/NefEditorClient/Main/View/AppModalView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct AppModalView: View {
4 | let state: AppModalState
5 | let editView: (EditState) -> EditView
6 | let generationView: (GenerationState) -> GenerationView
7 | let creditsView: CreditsView
8 | let faqView: FAQView
9 | let whatsNewView: WhatsNewView
10 |
11 | var body: some View {
12 | switch state {
13 | case .noModal:
14 | return AnyView(EmptyView())
15 | case .edit(let editState):
16 | return AnyView(editView(editState))
17 | case .credits:
18 | return AnyView(creditsView)
19 | case .generation(let generationState):
20 | return AnyView(generationView(generationState))
21 | case .faq:
22 | return AnyView(faqView)
23 | case .whatsNew:
24 | return AnyView(whatsNewView)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/NefEditorClient/NefEditorClient.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.applesignin
6 |
7 | Default
8 |
9 | com.apple.developer.associated-domains
10 |
11 | applinks:badge.bow-swift.io
12 |
13 | com.apple.developer.icloud-container-identifiers
14 |
15 | iCloud.com.47deg.nef-editor-recipes
16 |
17 | com.apple.developer.icloud-services
18 |
19 | CloudDocuments
20 |
21 | com.apple.developer.ubiquity-container-identifiers
22 |
23 | iCloud.com.47deg.nef-editor-recipes
24 |
25 | com.apple.developer.ubiquity-kvstore-identifier
26 | $(TeamIdentifierPrefix)$(CFBundleIdentifier)
27 | com.apple.security.app-sandbox
28 |
29 | com.apple.security.network.client
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/NefEditorClient/NefEditorClientRelease.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.applesignin
6 |
7 | Default
8 |
9 | com.apple.developer.associated-domains
10 |
11 | applinks:badge.bow-swift.io
12 |
13 | com.apple.developer.icloud-container-identifiers
14 |
15 | iCloud.com.47deg.nef-editor-recipes
16 |
17 | com.apple.developer.icloud-services
18 |
19 | CloudDocuments
20 |
21 | com.apple.developer.ubiquity-container-identifiers
22 |
23 | iCloud.com.47deg.nef-editor-recipes
24 |
25 | com.apple.developer.ubiquity-kvstore-identifier
26 | $(TeamIdentifierPrefix)$(CFBundleIdentifier)
27 | com.apple.security.app-sandbox
28 |
29 | com.apple.security.network.client
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/NefEditorClient/Persistence/ICloudPersistence.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Bow
3 | import BowEffects
4 |
5 | class ICloudPersistence: Persistence {
6 | private var documents: URL? {
7 | FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
8 | }
9 |
10 | private var preferencesStore: NSUbiquitousKeyValueStore { .default }
11 |
12 | private var recipes: URL? {
13 | documents?
14 | .appendingPathComponent("nefRecipes")
15 | .appendingPathExtension("json")
16 | }
17 |
18 | var isPersistenceAvailable: Bool {
19 | self.recipes != nil
20 | }
21 |
22 | // MARK: recipes
23 | func loadUserRecipes() -> EnvIO {
24 | EnvIO.invoke { _ in
25 | guard let recipes = self.recipes else { return [] }
26 | let decoder = JSONDecoder()
27 | let data = try Data.init(contentsOf: recipes)
28 | return try decoder.decode([Recipe].self, from: data)
29 | }
30 | }
31 |
32 | func saveUserRecipes(_ recipes: [Recipe]) -> EnvIO {
33 | EnvIO.invoke { _ in
34 | guard let recipesFile = self.recipes else { return }
35 | let encoder = JSONEncoder()
36 | let data = try encoder.encode(recipes)
37 | try data.write(to: recipesFile)
38 | }
39 | }
40 |
41 | // MARK: user preferences
42 | func loadUserPreferences() -> EnvIO {
43 | EnvIO.invoke { _ in
44 | guard let data = self.preferencesStore.data(forKey: StoreKey.userPreferences),
45 | let preferences = try? JSONDecoder().decode(UserPreferences.self, from: data) else {
46 | throw UserPreferencesError.invalidData
47 | }
48 |
49 | return preferences
50 | }
51 | }
52 |
53 | func updateUserPreferences(bundleVersion: String) -> EnvIO {
54 | EnvIO.invoke { _ in
55 | let userPreferences = UserPreferences(lastVersionShown: bundleVersion)
56 | let data = try JSONEncoder().encode(userPreferences)
57 | self.preferencesStore.set(data, forKey: StoreKey.userPreferences)
58 | self.preferencesStore.synchronize()
59 | }
60 | }
61 |
62 | // MARK: - Constants
63 | private enum StoreKey {
64 | static let userPreferences = "user-preferences"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/NefEditorClient/Persistence/Persistence.swift:
--------------------------------------------------------------------------------
1 | import BowEffects
2 |
3 | protocol Persistence {
4 | var isPersistenceAvailable: Bool { get }
5 | func loadUserRecipes() -> EnvIO
6 | func saveUserRecipes(_ recipes: [Recipe]) -> EnvIO
7 | func loadUserPreferences() -> EnvIO
8 | func updateUserPreferences(bundleVersion: String) -> EnvIO
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Persistence/UserPreferences.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct UserPreferences: Codable {
4 | let lastVersionShown: String
5 | }
6 |
7 | enum UserPreferencesError: Error {
8 | case invalidData
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/NefEditorClient/Preview Content/PreviewData.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import SwiftUI
3 |
4 | #if DEBUG
5 |
6 | let bow = Repository(
7 | name: "bow",
8 | fullName: "bow-swift/bow",
9 | _description: "🏹 Bow is a library for Typed Functional Programming in Swift",
10 | _private: false,
11 | htmlUrl: "https://github.com/bow-swift/bow",
12 | stargazersCount: 407,
13 | owner: Owner(login: "bow-swift", avatarUrl: "https://avatars2.githubusercontent.com/u/44965417?s=200&v=4"))
14 |
15 | let nef = Repository(
16 | name: "nef",
17 | fullName: "bow-swift/nef",
18 | _description: "💊 steroids for Xcode Playgrounds",
19 | _private: false,
20 | htmlUrl: "https://github.com/bow-swift/nef",
21 | stargazersCount: 134,
22 | owner: Owner(login: "bow-swift", avatarUrl: "https://avatars2.githubusercontent.com/u/44965417?s=200&v=4"))
23 |
24 | let version = Requirement.version(Tag(name: "0.7.5"))
25 | let branch = Requirement.branch(Branch(name: "master"))
26 |
27 | let sampleRepo = bow
28 | let sampleRepos = Array(repeating: bow, count: 17)
29 | let sampleSearchResults = [bow, nef]
30 |
31 | let sampleBowTag = TagViewModel(text: "bow")
32 | let sampleNefTag = TagViewModel(text: "nef", foregroundColor: .yellow, backgroundColor: .purple)
33 |
34 | let sampleRequirements = Array(repeating: version, count: 5) + Array(repeating: branch, count: 3)
35 |
36 | let bowDependency = Dependency(
37 | repository: "bow",
38 | owner: "bow-swift",
39 | url: "https://github.com/bow-swift/bow",
40 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4",
41 | requirement: .version(Tag(name: "0.8.0")),
42 | products: .all)
43 |
44 | let bowArchDependency = Dependency(
45 | repository: "bow-arch",
46 | owner: "bow-swift",
47 | url: "https://github.com/bow-swift/bow-arch",
48 | avatar: "https://avatars3.githubusercontent.com/u/44965417?v=4",
49 | requirement: .branch(Branch(name: "0.1.0")),
50 | products: .all)
51 |
52 | let sampleRecipe = Recipe(
53 | title: "FP Basics",
54 | description: "Learn Functional Programming",
55 | dependencies: [
56 | bowDependency,
57 | bowArchDependency
58 | ])
59 |
60 | let sampleRecipes = Array(repeating: sampleRecipe, count: 13).map(CatalogItem.regular)
61 |
62 | let sampleFeaturedRecipe = FeaturedRecipe(
63 | recipe: sampleRecipe,
64 | backgroundImage: "bow-background",
65 | textColor: .white)
66 |
67 | let sampleFeaturedRecipes = Array(repeating: sampleFeaturedRecipe, count: 2).map(CatalogItem.featured)
68 |
69 | let sampleRecipesSection = CatalogSection(
70 | title: "My recipes",
71 | action: CatalogSectionAction(icon: "plus", action: .addRecipe),
72 | items: sampleRecipes)
73 | let sampleFeaturedSection = CatalogSection(
74 | title: "Featured",
75 | items: sampleFeaturedRecipes)
76 |
77 | let sampleCatalog = Catalog(featured: sampleFeaturedSection, userCreated: sampleRecipesSection)
78 |
79 | #endif
80 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/Component/RepositoryDetailComponent.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import BowArch
3 |
4 | typealias RepositoryDetailComponent = StoreComponent
5 |
6 | func repositoryDetail(
7 | config: API.Config,
8 | state: SearchModalState
9 | ) -> RepositoryDetailComponent {
10 |
11 | RepositoryDetailComponent(
12 | initialState: state,
13 | environment: config,
14 | dispatcher: repositoryDetailDispatcher,
15 | render: RepositoryDetailView.init)
16 | }
17 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/Logic/RepositoryDetailAction.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 |
3 | enum RepositoryDetailAction {
4 | case loadRequirements(Repository)
5 | case dependencySelected(Requirement, from: Repository)
6 | case dismissDetails
7 | }
8 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/Logic/RepositoryDetailDispatcher.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import Bow
3 | import BowEffects
4 | import BowArch
5 |
6 | typealias RepositoryDetailDispatcher = StateDispatcher
7 |
8 | let repositoryDetailDispatcher = RepositoryDetailDispatcher.effectful { action in
9 | switch action {
10 | case .loadRequirements(let repository):
11 | return loadRequirements(repository: repository).handleError { _ in
12 | onError(repository: repository)
13 | }^
14 | case .dependencySelected(_, from: _):
15 | return EnvIO.pure(.modify(id)^)^
16 | case .dismissDetails:
17 | return EnvIO.pure(.set(.noModal)^)^
18 | }
19 | }
20 |
21 | func loadRequirements(
22 | repository: Repository
23 | ) -> EnvIO> {
24 |
25 | let tags = EnvIO.var()
26 | let branches = EnvIO.var()
27 | let requirements = EnvIO.var()
28 |
29 | return binding(
30 | (tags, branches) <- parallel(loadTags(repository: repository),
31 | loadBranches(repository: repository)),
32 | requirements <- EnvIO.pure(merge(tags: tags.get, branches: branches.get)),
33 | yield: requirementsLoaded(requirements.get, for: repository))^
34 | }
35 |
36 | func onError(
37 | repository: Repository
38 | ) -> State {
39 | .set(.repositoryDetail(
40 | .error(repository,
41 | message: "An error ocurred trying to fetch tags and branches for the repository '\(repository.fullName)'"))
42 | )^
43 | }
44 |
45 | func loadTags(
46 | repository: Repository
47 | ) -> EnvIO {
48 | API.repository.getVersions(fullName: repository.fullName)
49 | .mapError(id)
50 | }
51 |
52 | func loadBranches(
53 | repository: Repository
54 | ) -> EnvIO {
55 | API.repository.getBranches(fullName: repository.fullName)
56 | .mapError(id)
57 | }
58 |
59 | func merge(tags: Tags, branches: Branches) -> [Requirement] {
60 | tags.map(Requirement.version) +
61 | branches.map(Requirement.branch)
62 | }
63 |
64 | func requirementsLoaded(
65 | _ requirements: [Requirement],
66 | for repository: Repository
67 | ) -> State {
68 | if requirements.isEmpty {
69 | return .set(.repositoryDetail(.empty(repository)))^
70 | } else {
71 | return .set(.repositoryDetail(.loaded(repository, requirements: requirements)))^
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/State/RepositoryDetailState.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 |
3 | enum RepositoryDetailState: Equatable {
4 | case loading(Repository)
5 | case empty(Repository)
6 | case loaded(Repository, requirements: [Requirement])
7 | case error(Repository, message: String)
8 |
9 | var repository: Repository {
10 | switch self {
11 | case .loading(let repo),
12 | .empty(let repo),
13 | .loaded(let repo, requirements: _),
14 | .error(let repo, message: _): return repo
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/State/Requirement.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 |
3 | enum Requirement: Equatable, Codable {
4 | case version(Tag)
5 | case branch(Branch)
6 |
7 | var title: String {
8 | switch self {
9 | case .version(let tag): return tag.name
10 | case .branch(let branch): return branch.name
11 | }
12 | }
13 |
14 | enum Keys: CodingKey {
15 | case version
16 | case branch
17 | }
18 |
19 | init(from decoder: Decoder) throws {
20 | let container = try decoder.container(keyedBy: Keys.self)
21 | if let tag = try? container.decode(Tag.self, forKey: .version) {
22 | self = .version(tag)
23 | } else if let branch = try? container.decode(Branch.self, forKey: .branch) {
24 | self = .branch(branch)
25 | } else {
26 | throw Swift.DecodingError.dataCorrupted(
27 | Swift.DecodingError.Context(
28 | codingPath: container.codingPath,
29 | debugDescription: "Error decoding Requirement")
30 | )
31 | }
32 | }
33 |
34 | func encode(to encoder: Encoder) throws {
35 | var container = encoder.container(keyedBy: Keys.self)
36 | switch self {
37 | case let .version(tag):
38 | try container.encode(tag, forKey: .version)
39 | case let .branch(branch):
40 | try container.encode(branch, forKey: .branch)
41 | }
42 | }
43 | }
44 |
45 | extension Requirement: Identifiable {
46 | var id: String {
47 | title
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/View/EmptyRequirementsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GitHub
3 |
4 | struct EmptyRequirementsView: View {
5 | let repository: Repository
6 |
7 | var body: some View {
8 | ActivityTextView(message: "'\(repository.fullName)' does not contain any tag or branch.")
9 | }
10 | }
11 |
12 | #if DEBUG
13 | struct EmptyRequirementsView_Previews: PreviewProvider {
14 | static var previews: some View {
15 | EmptyRequirementsView(repository: sampleRepo)
16 | .previewLayout(.sizeThatFits)
17 | }
18 | }
19 | #endif
20 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/View/ErrorRequirementsView.swift:
--------------------------------------------------------------------------------
1 | typealias ErrorRequirementsView = ActivityTextView
2 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/View/LoadingRequirementsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GitHub
3 |
4 | struct LoadingRequirementsView: View {
5 | let repository: Repository
6 |
7 | var body: some View {
8 | LoadingView(message: "Loading tags and branches for repository '\(repository.fullName)'...")
9 | }
10 | }
11 |
12 | #if DEBUG
13 | struct LoadingRequirementsView_Previews: PreviewProvider {
14 | static var previews: some View {
15 | LoadingRequirementsView(repository: sampleRepo)
16 | .previewLayout(.sizeThatFits)
17 | }
18 | }
19 | #endif
20 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/View/RepositoryDetailView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GitHub
3 |
4 | struct RepositoryDetailView: View {
5 | let state: SearchModalState
6 | let handle: (RepositoryDetailAction) -> Void
7 |
8 | var body: some View {
9 | self.contentView
10 | .navigationBarTitle(self.title)
11 | .navigationBarItems(leading:
12 | Button("Cancel") {
13 | self.handle(.dismissDetails)
14 | }.navigationBarButtonStyle())
15 | .onAppear {
16 | self.loadRequirements()
17 | }
18 | }
19 |
20 | private var title: String {
21 | switch state {
22 | case .noModal: return ""
23 | case .repositoryDetail(let detail): return detail.repository.name
24 | }
25 | }
26 |
27 | private var contentView: some View {
28 | switch state {
29 | case .noModal: return AnyView(EmptyView())
30 | case .repositoryDetail(let detail):
31 | switch detail {
32 | case .empty(let repo):
33 | return AnyView(EmptyRequirementsView(repository: repo))
34 | case .loading(let repo):
35 | return AnyView(LoadingRequirementsView(repository: repo))
36 | case .loaded(let repository, requirements: let requirements):
37 | return AnyView(RequirementListView(requirements: requirements) { selected in
38 | self.handle(.dependencySelected(selected, from: repository))
39 | })
40 | case .error(_, message: let message):
41 | return AnyView(ErrorRequirementsView(message: message))
42 | }
43 | }
44 | }
45 |
46 | private func loadRequirements() {
47 | switch state {
48 | case .repositoryDetail(let detail):
49 | self.handle(.loadRequirements(detail.repository))
50 | case .noModal: return
51 | }
52 | }
53 | }
54 |
55 | #if DEBUG
56 | struct RepositoryDetailView_Previews: PreviewProvider {
57 | static var previews: some View {
58 | Group {
59 | NavigationView {
60 | RepositoryDetailView(state: .repositoryDetail(.empty(bow))) { _ in }
61 | }.navigationViewStyle(StackNavigationViewStyle())
62 |
63 | NavigationView {
64 | RepositoryDetailView(state: .repositoryDetail(.loading(bow))) { _ in }
65 | }.navigationViewStyle(StackNavigationViewStyle())
66 |
67 | NavigationView {
68 | RepositoryDetailView(state: .repositoryDetail(.loaded(bow, requirements: [version, branch]))) { _ in }
69 | }.navigationViewStyle(StackNavigationViewStyle())
70 |
71 | NavigationView {
72 | RepositoryDetailView(state: .repositoryDetail(.error(bow, message: "Could not load tags or branches."))) { _ in }
73 | }.navigationViewStyle(StackNavigationViewStyle())
74 | }.previewLayout(.fixed(width: 500, height: 500))
75 | }
76 | }
77 | #endif
78 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/View/RequirementListView.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import SwiftUI
3 |
4 | struct RequirementListView: View {
5 | let requirements: [Requirement]
6 | let onRequirementSelected: (Requirement) -> ()
7 |
8 | var body: some View {
9 | List {
10 | ForEach(requirements, id: \.title) { requirement in
11 | Button(action: { self.onRequirementSelected(requirement) }) {
12 | RequirementView(requirement: requirement)
13 | }
14 | }
15 | }
16 | }
17 | }
18 |
19 | #if DEBUG
20 | struct RequirementListView_Previews: PreviewProvider {
21 | static var previews: some View {
22 | RequirementListView(requirements: sampleRequirements) { _ in }
23 | .previewLayout(.sizeThatFits)
24 | }
25 | }
26 | #endif
27 |
--------------------------------------------------------------------------------
/NefEditorClient/Requirements/View/RequirementView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct RequirementView: View {
4 | let requirement: Requirement
5 |
6 | var body: some View {
7 | HStack {
8 | Image(systemName: self.requirement.iconName)
9 | Text(self.requirement.title)
10 | Spacer()
11 | }.padding()
12 | }
13 | }
14 |
15 | private extension Requirement {
16 | var iconName: String {
17 | switch self {
18 | case .version: return "tag"
19 | case .branch: return "arrow.branch"
20 | }
21 | }
22 | }
23 |
24 | #if DEBUG
25 | struct RequirementView_Previews: PreviewProvider {
26 | static var previews: some View {
27 | Group {
28 | RequirementView(requirement: version)
29 |
30 | RequirementView(requirement: branch)
31 | }.frame(width: 400)
32 | .previewLayout(.sizeThatFits)
33 | }
34 | }
35 | #endif
36 |
--------------------------------------------------------------------------------
/NefEditorClient/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GitHub
3 | import BowArch
4 |
5 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
6 | var window: UIWindow?
7 |
8 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
9 | // For some reason, I cannot set the background color of a SwiftUI List 🤷🏻♂️
10 | UITableView.appearance().backgroundColor = .clear
11 |
12 | // Remove any extra separator in the Lists
13 | UITableView.appearance().tableFooterView = UIView()
14 |
15 | if let userActivity = connectionOptions.userActivities.first {
16 | loadScene(scene, contentView: appComponent(userActivity: userActivity))
17 | } else {
18 | loadScene(scene, contentView: appComponent(urlContexts: connectionOptions.urlContexts))
19 | }
20 | }
21 |
22 | func scene(_ scene: UIScene, openURLContexts urlContexts: Set) {
23 | loadScene(scene, contentView: appComponent(urlContexts: urlContexts))
24 | }
25 |
26 | func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
27 | loadScene(scene, contentView: appComponent(userActivity: userActivity))
28 | }
29 |
30 | private func loadScene(_ scene: UIScene, contentView: V) {
31 | guard let windowScene = scene as? UIWindowScene else { return }
32 |
33 | window = UIWindow(windowScene: windowScene)
34 | window?.rootViewController = UIHostingController(rootView: contentView)
35 | window?.makeKeyAndVisible()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/Component/SearchComponent.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import Bow
3 | import BowArch
4 |
5 | typealias SearchComponent = StoreComponent>
6 |
7 | func searchComponent(
8 | config: API.Config,
9 | state: SearchState
10 | ) -> SearchComponent {
11 | SearchComponent(
12 | initialState: state,
13 | environment: config,
14 | dispatcher: searchDispatcher) { state, handle in
15 | SearchView(
16 | state: state,
17 | detail: repositoryDetail(config: config, state: state.modalState)
18 | .using(handle, transformInput: SearchAction.prism(for: SearchAction.repositoryDetailAction)),
19 | handle: handle
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/Logic/SearchAction.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import BowOptics
3 |
4 | enum SearchAction: AutoPrism {
5 | case search(query: String)
6 | case showDetails(Repository)
7 | case dismissDetails
8 | case cancelSearch
9 | case repositoryDetailAction(RepositoryDetailAction)
10 | }
11 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/Logic/SearchDispatcher.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import Bow
3 | import BowArch
4 | import BowEffects
5 |
6 | typealias SearchDispatcher = StateDispatcher
7 |
8 | let searchDispatcher = SearchDispatcher.workflow { action in
9 | switch action {
10 |
11 | case .search(query: let query):
12 | return search(query: query)
13 |
14 | case .showDetails(let repository):
15 | return [EnvIO.pure(showDetails(repository))^]
16 |
17 | case .dismissDetails:
18 | return [EnvIO.pure(dismissDetails())^]
19 |
20 | case .repositoryDetailAction(let detailAction):
21 | switch detailAction {
22 | case .dependencySelected(_, from: _):
23 | return [EnvIO.pure(dismissDetails())^]
24 | default:
25 | return []
26 | }
27 |
28 | case .cancelSearch:
29 | return []
30 | }
31 | }.combine(repositoryDetailDispatcher.widen(
32 | transformState: SearchState.modalStateLens,
33 | transformInput: SearchAction.prism(for: SearchAction.repositoryDetailAction)
34 | ))
35 |
36 | func search(
37 | query: String
38 | ) -> [EnvIO>] {
39 | [
40 | EnvIO.pure(setLoading(query: query))^,
41 | backgroundSearch(query: query)
42 | .handleError { _ in onError(query: query) }^
43 | ]
44 | }
45 |
46 | func setLoading(query: String) -> State {
47 | .modify { state in
48 | state.copy(loadingState: .loading(query: query))
49 | }^
50 | }
51 |
52 | func backgroundSearch(query: String) -> EnvIO> {
53 | let repositories = EnvIO.var()
54 |
55 | return binding(
56 | continueOn(.global(qos: .background)),
57 | repositories <- gitHubSearch(query: query),
58 | yield: showResults(repositories: repositories.get, for: query)
59 | )^
60 | }
61 |
62 | func gitHubSearch(
63 | query: String
64 | ) -> EnvIO {
65 | API.search.searchRepositories(q: "\(query)+language:Swift" )
66 | .bimap(id, { result in result.items })
67 | }
68 |
69 | func showResults(repositories: Repositories, for query: String) -> State {
70 | .modify { state in
71 | let newState: SearchLoadingState = (repositories.isEmpty)
72 | ? .empty(query: query)
73 | : .loaded(repositories)
74 | return state.copy(loadingState: newState)
75 | }^
76 | }
77 |
78 | func onError(
79 | query: String
80 | ) -> State {
81 | .modify { state in
82 | state.copy(loadingState: .error(message: "An error happened while performing your query '\(query)'"))
83 | }^
84 | }
85 |
86 | func showDetails(
87 | _ repository: Repository
88 | ) -> State {
89 | .modify { state in
90 | state.copy(modalState: .repositoryDetail(.loading(repository)))
91 | }^
92 | }
93 |
94 | func dismissDetails() -> State {
95 | .modify { state in
96 | state.copy(modalState: .noModal)
97 | }^
98 | }
99 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/State/SearchState.swift:
--------------------------------------------------------------------------------
1 | import GitHub
2 | import BowOptics
3 |
4 | struct SearchState: Equatable {
5 | let loadingState: SearchLoadingState
6 | let modalState: SearchModalState
7 |
8 | func copy(
9 | loadingState: SearchLoadingState? = nil,
10 | modalState: SearchModalState? = nil
11 | ) -> SearchState {
12 | SearchState(
13 | loadingState: loadingState ?? self.loadingState,
14 | modalState: modalState ?? self.modalState)
15 | }
16 |
17 | static var modalStateLens: Lens {
18 | Lens(
19 | get: { search in search.modalState },
20 | set: { search, modal in search.copy(modalState: modal) })
21 | }
22 | }
23 |
24 | enum SearchLoadingState: Equatable {
25 | case initial
26 | case loading(query: String)
27 | case empty(query: String)
28 | case loaded(Repositories)
29 | case error(message: String)
30 | }
31 |
32 | enum SearchModalState: Equatable {
33 | case noModal
34 | case repositoryDetail(RepositoryDetailState)
35 | }
36 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/View/EmptySearchView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct EmptySearchView: View {
4 | let query: String
5 |
6 | var body: some View {
7 | ActivityTextView(message: "Your query '\(query)' did not produce any results.")
8 | }
9 | }
10 |
11 | #if DEBUG
12 | struct EmptySearchView_Previews: PreviewProvider {
13 | static var previews: some View {
14 | EmptySearchView(query: "Bow")
15 | .previewLayout(.sizeThatFits)
16 | }
17 | }
18 | #endif
19 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/View/ErrorSearchView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | typealias ErrorSearchView = ActivityTextView
4 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/View/InitialSearchView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct InitialSearchView: View {
4 | var body: some View {
5 | ActivityTextView(message: "Search GitHub for your favorite Swift repositories and add them to your nef recipe.")
6 | }
7 | }
8 |
9 | #if DEBUG
10 | struct InitialSearchView_Previews: PreviewProvider {
11 | static var previews: some View {
12 | InitialSearchView()
13 | .previewLayout(.sizeThatFits)
14 | }
15 | }
16 | #endif
17 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/View/LoadingSearchView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct LoadingSearchView: View {
4 | let query: String
5 |
6 | var body: some View {
7 | LoadingView(message: "Searching Swift repositories with query '\(query)'...")
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/View/RepositoryGridView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GitHub
3 |
4 | struct RepositoryGridView: View {
5 | let repositories: [Repository]
6 | let columns: Int
7 | let onRepositorySelected: (Repository) -> ()
8 |
9 | var body: some View {
10 | GridView(rows: self.rows, columns: self.columns) { row, column in
11 | self.viewForItemAt(row, column)
12 | .aspectRatio(16/9, contentMode: .fit)
13 | .onTapGesture {
14 | if let repository = self.itemAt(row, column) {
15 | self.onRepositorySelected(repository)
16 | }
17 | }
18 | .safeHoverEffect()
19 | }
20 | }
21 |
22 | private var rows: Int {
23 | Int(ceil(Double(repositories.count) / Double(columns)))
24 | }
25 |
26 | private func itemAt(_ row: Int, _ column: Int) -> Repository? {
27 | repositories[safe: indexAt(row, column)]
28 | }
29 |
30 | private func indexAt(_ row: Int, _ column: Int) -> Int {
31 | row * self.columns + column
32 | }
33 |
34 | private func viewForItemAt(_ row: Int, _ column: Int) -> some View {
35 | if let repository = itemAt(row, column) {
36 | return AnyView(RepositoryView(repository: repository))
37 | } else {
38 | return AnyView(Color.clear)
39 | }
40 | }
41 | }
42 |
43 | #if DEBUG
44 | struct RepositoryGridView_Previews: PreviewProvider {
45 |
46 | static var previews: some View {
47 | Group {
48 | RepositoryGridView(repositories: sampleRepos, columns: 3) { _ in }
49 |
50 | ScrollView {
51 | RepositoryGridView(repositories: sampleRepos, columns: 3) { _ in }
52 | }
53 | }
54 | }
55 | }
56 | #endif
57 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/View/RepositoryView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GitHub
3 |
4 | struct RepositoryView: View {
5 | let repository: Repository
6 |
7 | var body: some View {
8 | CardView {
9 | VStack(alignment: .leading, spacing: 8) {
10 | Text(self.repository.name)
11 | .titleStyle()
12 | .padding(.bottom, 4)
13 |
14 | Text(self.repository._description ?? "No description")
15 |
16 | Spacer()
17 |
18 | HStack(alignment: .lastTextBaseline) {
19 | HStack {
20 | AvatarView(avatar: self.repository.owner.avatarUrl)
21 | .frame(width: 24, height: 24)
22 | .mask(Circle())
23 | Text(self.repository.owner.login)
24 | }
25 |
26 | Spacer()
27 |
28 | self.labeledImage(
29 | "star.fill",
30 | tint: .yellow,
31 | text: "\(self.repository.stargazersCount)")
32 | }
33 | }.padding()
34 | }
35 | }
36 |
37 | private func labeledImage(
38 | _ image: String,
39 | tint: Color? = nil,
40 | text: String) -> some View {
41 |
42 | HStack {
43 | Image(systemName: image).foregroundColor(tint)
44 | Text(text)
45 | }
46 | }
47 | }
48 |
49 | #if DEBUG
50 | struct RepositoryView_Previews: PreviewProvider {
51 | static var previews: some View {
52 | Group {
53 | RepositoryView(repository: sampleRepo)
54 | .frame(maxWidth: 300, maxHeight: 200)
55 | .aspectRatio(4/3, contentMode: .fit)
56 | .previewLayout(.sizeThatFits)
57 | .padding()
58 | }
59 | }
60 | }
61 | #endif
62 |
--------------------------------------------------------------------------------
/NefEditorClient/Search/View/SearchView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import GitHub
3 |
4 | struct SearchView: View {
5 | let state: SearchState
6 | let detail: Detail
7 | @State var query: String = ""
8 | let handle: (SearchAction) -> Void
9 |
10 | let isDetailPresented: Binding
11 |
12 | init(
13 | state: SearchState,
14 | detail: Detail,
15 | handle: @escaping (SearchAction) -> Void) {
16 | self.state = state
17 | self.detail = detail
18 | self.handle = handle
19 | self.isDetailPresented = Binding(
20 | get: { state.modalState != .noModal },
21 | set: { newState in
22 | if !newState {
23 | handle(.dismissDetails)
24 | }
25 | })
26 | }
27 |
28 | var body: some View {
29 | VStack {
30 | self.searchView
31 | self.contentView.fill.layoutPriority(1)
32 | }.modal(isPresented: isDetailPresented) {
33 | self.detail
34 | }.modifier(KeyboardPadding())
35 | }
36 |
37 | private var searchView: some View {
38 | CardView {
39 | HStack {
40 | SearchBar(placeholder: "Search repositories...", query: self.$query) { query in
41 | self.handle(.search(query: query))
42 | }
43 |
44 | Button(action: { self.handle(.cancelSearch) }) {
45 | Text("Cancel").foregroundColor(.nef)
46 | }.padding(.trailing)
47 | }
48 | }.padding(.top).padding(.trailing)
49 | }
50 |
51 | private var contentView: some View {
52 | switch state.loadingState {
53 | case .initial:
54 | return AnyView(InitialSearchView())
55 | case .loading(let query):
56 | return AnyView(LoadingSearchView(query: query))
57 | case .empty(let query):
58 | return AnyView(EmptySearchView(query: query))
59 | case .loaded(let repositories):
60 | return AnyView(loadedView(repositories: repositories))
61 | case .error(let message):
62 | return AnyView(ErrorSearchView(message: message))
63 | }
64 | }
65 |
66 | private func loadedView(repositories: Repositories) -> some View {
67 | GeometryReader { geometry in
68 | ScrollView {
69 | RepositoryGridView(
70 | repositories: repositories,
71 | columns: self.columns(for: geometry.size.width)) { repository in
72 | self.handle(.showDetails(repository))
73 | }
74 | .padding(.trailing)
75 | .padding(.bottom)
76 | .fill
77 | }
78 | }
79 | }
80 |
81 | private func columns(for width: CGFloat) -> Int {
82 | let minimumCardWidth: CGFloat = Card.minimumWidth
83 | return max(Int(floor(width / minimumCardWidth)), 1)
84 | }
85 | }
86 |
87 | #if DEBUG
88 | struct SearchView_Previews: PreviewProvider {
89 | static func state(_ loadingState: SearchLoadingState) -> SearchState {
90 | SearchState(loadingState: loadingState, modalState: .noModal)
91 | }
92 |
93 | static var previews: some View {
94 | Group {
95 | SearchView(state: state(.initial), detail: EmptyView()) { _ in }
96 |
97 | SearchView(state: state(.empty(query: "Bow")), detail: EmptyView()) { _ in }
98 |
99 | SearchView(state: state(.loading(query: "Bow")), detail: EmptyView()) { _ in }
100 |
101 | SearchView(state: state(.loaded(sampleSearchResults)), detail: EmptyView()) { _ in }
102 |
103 | SearchView(state: state(.loaded(sampleRepos)), detail: EmptyView()) { _ in }
104 |
105 | SearchView(state: state(.error(message: "Unexpected error happened.")), detail: EmptyView()) { _ in }
106 | }
107 | .previewLayout(.fixed(width: 910, height: 1024))
108 | }
109 | }
110 | #endif
111 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/ActionButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ActionButtonStyle: ButtonStyle {
4 | func makeBody(configuration: Self.Configuration) -> some View {
5 | configuration.label
6 | .padding(8)
7 | .foregroundColor(.white)
8 | .contentShape(Circle())
9 | .background(
10 | Circle().fill(
11 | configuration.isPressed
12 | ? Color.nef.opacity(0.7)
13 | : Color.nef))
14 | .safeHoverEffect()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/ActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ActivityIndicator: UIViewRepresentable {
4 | @Binding var isAnimating: Bool
5 | let style: UIActivityIndicatorView.Style
6 |
7 | init(isAnimating: Binding, style: UIActivityIndicatorView.Style = .medium) {
8 | self._isAnimating = isAnimating
9 | self.style = style
10 | }
11 |
12 | func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView {
13 | UIActivityIndicatorView(style: style)
14 | }
15 |
16 | func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) {
17 | isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
18 | }
19 | }
20 |
21 | #if DEBUG
22 | struct ActivityIndicator_Previews: PreviewProvider {
23 | static var previews: some View {
24 | Group {
25 | ActivityIndicator(isAnimating: .constant(true))
26 | .previewLayout(.sizeThatFits)
27 |
28 | ActivityIndicator(isAnimating: .constant(true), style: .large)
29 | .previewLayout(.sizeThatFits)
30 |
31 | ActivityIndicator(isAnimating: .constant(false), style: .large)
32 | .previewLayout(.sizeThatFits)
33 | }
34 | }
35 | }
36 | #endif
37 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/ActivityTextView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ActivityTextView: View {
4 | let message: String
5 |
6 | var body: some View {
7 | Text(message)
8 | .activityStyle()
9 | .multilineTextAlignment(.center)
10 | }
11 | }
12 |
13 | #if DEBUG
14 | struct ActivityTextView_Previews: PreviewProvider {
15 | static var previews: some View {
16 | ActivityTextView(message: "This is a sample message.")
17 | .previewLayout(.sizeThatFits)
18 | }
19 | }
20 | #endif
21 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/ActivityViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 |
4 | struct ActivityViewController: UIViewControllerRepresentable {
5 | let activityItems: [Any]
6 | let applicationActivities: [UIActivity]?
7 |
8 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController {
9 | let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
10 | return controller
11 | }
12 |
13 | func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {}
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/AnimationView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct AnimationView: View {
4 | struct Animation: Identifiable {
5 | var id: LottieAnimation { lottie }
6 |
7 | let lottie: LottieAnimation
8 | let isLoop: Bool
9 |
10 | init(lottie: LottieAnimation, isLoop: Bool = false) {
11 | self.lottie = lottie
12 | self.isLoop = isLoop
13 | }
14 | }
15 |
16 | let animation: Animation
17 |
18 | init(animation: Animation) {
19 | self.animation = animation
20 | }
21 |
22 | var body: some View {
23 | self.animationView()
24 | }
25 |
26 | private func animationView() -> some View {
27 | guard let lottieView = animation.lottie.view(isLoop: animation.isLoop) else {
28 | return AnyView(ActivityIndicator(isAnimating: .constant(true), style: .large))
29 | }
30 |
31 | return AnyView(
32 | GeometryReader { geometry in
33 | lottieView
34 | .offset(self.animation.lottie.fixOffset(size: geometry.size))
35 | .aspectRatio(contentMode: .fill)
36 | .frame(width: geometry.size.width, height: geometry.size.height)
37 | }
38 | )
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/AvatarView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct AvatarView: View {
4 | let avatar: String
5 |
6 | var body: some View {
7 | // GitHub API has a parameter 's' that lets us control the size of the received image
8 | if let url = URL(string: avatar + "&s=24") {
9 | return AnyView(
10 | URLImage(url: url, placeholder: Image.person)
11 | )
12 | } else {
13 | return AnyView(
14 | Image.person
15 | )
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/CardView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | enum Card {
4 | static var minimumWidth: CGFloat {
5 | let screen = UIScreen.main.bounds
6 | let width = max(screen.size.width, screen.size.height)
7 | if width > 1200 {
8 | return 420
9 | } else {
10 | return 340
11 | }
12 | }
13 | }
14 |
15 | struct CardView: View {
16 | @Environment(\.colorScheme) var colorScheme
17 |
18 | let isSelected: Bool
19 | let content: () -> Content
20 |
21 | init(isSelected: Bool = false,
22 | @ViewBuilder content: @escaping () -> Content) {
23 | self.isSelected = isSelected
24 | self.content = content
25 | }
26 |
27 | var body: some View {
28 | ZStack {
29 | RoundedRectangle(cornerRadius: 8)
30 | .fill(Color.card)
31 | .if(colorScheme == .light,
32 | then: {
33 | $0.shadow(
34 | color: self.isSelected ?
35 | Color.shadow.opacity(0.3) :
36 | Color.shadow.opacity(0.1),
37 | radius: self.isSelected ? 6 : 2,
38 | x: 1,
39 | y: 1)
40 | })
41 | .if(isSelected && colorScheme == .dark,
42 | then: {
43 | $0.overlay(
44 | RoundedRectangle(cornerRadius: 8)
45 | .stroke(Color.shadow, lineWidth: 2)
46 | )
47 | })
48 | self.content()
49 | }
50 | }
51 | }
52 |
53 | #if DEBUG
54 | struct CardView_Preview: PreviewProvider {
55 | static var previews: some View {
56 | Group {
57 | cardSized(width: 200, height: 200)
58 | cardSized(width: 400, height: 100)
59 | cardSized(width: 100, height: 400)
60 | }
61 | }
62 |
63 | static func cardSized(width: CGFloat, height: CGFloat) -> some View {
64 | CardView {
65 | EmptyView()
66 | }.frame(width: width, height: height)
67 | .previewLayout(.sizeThatFits)
68 | .padding()
69 | }
70 | }
71 | #endif
72 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/ColorPalette.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Color {
4 | static var nef: Color {
5 | Color("nefColor")
6 | }
7 |
8 | static var card: Color {
9 | Color("card")
10 | }
11 |
12 | static var shadow: Color {
13 | Color("shadow")
14 | }
15 |
16 | static var form: Color {
17 | Color("form")
18 | }
19 |
20 | static var mainBackground: Color {
21 | Color("mainBackground")
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/GridView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct GridView: View {
4 | let rows: Int
5 | let columns: Int
6 | let horizontalSpacing: CGFloat
7 | let verticalSpacing: CGFloat
8 | let horizontalAlignment: HorizontalAlignment
9 | let verticalAlignment: VerticalAlignment
10 | let cellAt: (Int, Int) -> Cell
11 |
12 | init(
13 | rows: Int,
14 | columns: Int,
15 | horizontalSpacing: CGFloat = 8,
16 | verticalSpacing: CGFloat = 8,
17 | horizontalAlignment: HorizontalAlignment = .center,
18 | verticalAlignment: VerticalAlignment = .center,
19 | @ViewBuilder cellAt: @escaping (Int, Int) -> Cell) {
20 | self.rows = rows
21 | self.columns = columns
22 | self.horizontalSpacing = horizontalSpacing
23 | self.verticalSpacing = verticalSpacing
24 | self.horizontalAlignment = horizontalAlignment
25 | self.verticalAlignment = verticalAlignment
26 | self.cellAt = cellAt
27 | }
28 |
29 | var body: some View {
30 | VStack(alignment: self.horizontalAlignment, spacing: self.horizontalSpacing) {
31 | ForEach(0 ..< self.rows, id: \.self) { row in
32 | HStack(alignment: self.verticalAlignment, spacing: self.verticalSpacing) {
33 | ForEach(0 ..< self.columns, id: \.self) { column in
34 | Group {
35 | if column != 0 {
36 | Spacer(minLength: 0)
37 | }
38 | self.cellAt(row, column)
39 | }
40 | }
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | #if DEBUG
48 | struct GridView_Previews: PreviewProvider {
49 | static var previews: some View {
50 | Group {
51 | GridView(rows: 4, columns: 3) { row, column in
52 | ZStack {
53 | Circle().fill(Color.gray)
54 | Text("\(row), \(column)")
55 | }
56 | }
57 |
58 | ScrollView {
59 | GridView(rows: 20, columns: 6, horizontalSpacing: 24, verticalSpacing: 16) { row, column in
60 | ZStack {
61 | RoundedRectangle(cornerRadius: 8).fill(Color.blue)
62 | Text("\(row), \(column)")
63 | }.aspectRatio(4/3, contentMode: .fit)
64 | }
65 | }
66 | }
67 | }
68 | }
69 | #endif
70 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/Image+System.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Image {
4 | static var pencil: Image {
5 | Image(systemName: "pencil")
6 | }
7 |
8 | static var plus: Image {
9 | Image(systemName: "plus")
10 | }
11 |
12 | static var duplicate: Image {
13 | Image(systemName: "plus.square.on.square")
14 | }
15 |
16 | static var trash: Image {
17 | Image(systemName: "trash")
18 | }
19 |
20 | static var person: Image {
21 | Image(systemName: "person.crop.circle")
22 | }
23 |
24 | static var close: Image {
25 | Image(systemName: "xmark")
26 | }
27 |
28 | static var warning: Image {
29 | Image(systemName: "exclamationmark.triangle.fill")
30 | }
31 |
32 | static var info: Image {
33 | Image(systemName: "info.circle.fill")
34 | }
35 |
36 | static var faq: Image {
37 | Image(systemName: "questionmark.circle.fill")
38 | }
39 |
40 | static var whatsNew: Image {
41 | Image(systemName: "sparkles")
42 | }
43 |
44 | static var heart: Image {
45 | Image(systemName: "suit.heart.fill")
46 | }
47 |
48 | static var wand: Image {
49 | Image(systemName: "wand.and.stars")
50 | }
51 |
52 |
53 | static var nefClear: Image {
54 | Image("nef")
55 | }
56 |
57 | static var appIcon: Image {
58 | Image("nef-icon")
59 | }
60 |
61 | static var githubIcon: Image {
62 | Image("github-icon")
63 | }
64 |
65 | static var bow: Image {
66 | Image("bow-brand")
67 | }
68 |
69 | static var bowArch: Image {
70 | Image("bow-arch-brand")
71 | }
72 |
73 | static var bowOpenAPI: Image {
74 | Image("bow-openapi-brand")
75 | }
76 |
77 | static var bowLite: Image {
78 | Image("bow-lite-brand")
79 | }
80 |
81 | static var fortySeven: Image {
82 | Image("fortyseven")
83 | }
84 |
85 | static var badgePlatform: Image {
86 | Image("bow-platform-badge")
87 | }
88 |
89 | static var badgeActions: Image {
90 | Image("bow-actions-badge")
91 | }
92 |
93 | static var badgeNef: Image {
94 | Image("nef-playgrounds-badge")
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/KeyboardPadding.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Combine
3 |
4 | struct KeyboardPadding: ViewModifier {
5 | @State var currentHeight: CGFloat = 0
6 | let maxPadding: CGFloat
7 |
8 | init(maxPadding: CGFloat = .infinity) {
9 | self.maxPadding = maxPadding
10 | }
11 |
12 | func body(content: Content) -> some View {
13 | content
14 | .padding(.bottom, min(currentHeight, maxPadding))
15 | .edgesIgnoringSafeArea(currentHeight == 0 ? [] : .bottom)
16 | .onAppear(perform: subscribeToKeyboardEvents)
17 | }
18 |
19 | private func subscribeToKeyboardEvents() {
20 | NotificationCenter.Publisher(
21 | center: NotificationCenter.default,
22 | name: UIResponder.keyboardWillShowNotification
23 | ).compactMap { notification in
24 | notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect
25 | }.map { rect in
26 | rect.height
27 | }.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
28 |
29 | NotificationCenter.Publisher(
30 | center: NotificationCenter.default,
31 | name: UIResponder.keyboardWillHideNotification
32 | ).compactMap { _ in
33 | CGFloat.zero
34 | }.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/LibraryButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct LibraryButtonStyle: ButtonStyle {
4 | func makeBody(configuration: Self.Configuration) -> some View {
5 | configuration.label
6 | .padding(4)
7 | .contentShape(RoundedRectangle(cornerRadius: 8))
8 | .background(RoundedRectangle(cornerRadius: 8)
9 | .fill(configuration.isPressed
10 | ? Color.nef.opacity(0.1)
11 | : Color.clear))
12 | .safeHoverEffect()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/LoadingView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct LoadingView: View {
4 | let message: String
5 |
6 | var body: some View {
7 | VStack {
8 | ActivityIndicator(isAnimating: .constant(true), style: .large)
9 | Text(message).activityStyle()
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/LottieAnimation.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Lottie
3 |
4 | enum LottieAnimation: String {
5 | case generalError = "general-error"
6 | case generalLoading = "general-loading"
7 | case githubSearch = "github-search"
8 | case playgroundLoading = "playgroundbook-loading"
9 | case playgroundSuccess = "playgroundbook-success"
10 |
11 | func fixOffset(size: CGSize) -> CGSize {
12 | switch self {
13 | case .playgroundLoading:
14 | return .init(width: -size.width*0.2, height: 0)
15 | default:
16 | return .zero
17 | }
18 | }
19 | }
20 |
21 | extension LottieAnimation {
22 | func view(isLoop: Bool) -> AnimationLottieView? {
23 | guard let animation = Lottie.Animation.named(rawValue, subdirectory: "Animations") else { return nil }
24 | return AnimationLottieView(id: rawValue, animation: animation, isLoop: isLoop)
25 | }
26 | }
27 |
28 | struct AnimationLottieView: UIViewRepresentable, Identifiable {
29 | let id: String
30 | let animation: Lottie.Animation
31 | let isLoop: Bool
32 |
33 | func makeCoordinator() -> Coordinator {
34 | Coordinator(self)
35 | }
36 |
37 | func makeUIView(context: UIViewRepresentableContext) -> UIView {
38 | UIView()
39 | }
40 |
41 | func updateUIView(_ view: UIView, context: UIViewRepresentableContext) {
42 | let animationView = Lottie.AnimationView()
43 | animationView.contentMode = .scaleAspectFit
44 | animationView.translatesAutoresizingMaskIntoConstraints = false
45 |
46 | view.subviews.first?.removeFromSuperview()
47 | view.addSubview(animationView)
48 |
49 | NSLayoutConstraint.activate([
50 | animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
51 | animationView.heightAnchor.constraint(equalTo: view.heightAnchor)
52 | ])
53 |
54 | animationView.animation = animation
55 | animationView.loopMode = isLoop ? .loop : .playOnce
56 | animationView.play()
57 | }
58 |
59 | class Coordinator: NSObject {
60 | let parent: AnimationLottieView
61 |
62 | init(_ animationView: AnimationLottieView) {
63 | self.parent = animationView
64 | super.init()
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/NavigationBarButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Button {
4 | func navigationBarButtonStyle() -> some View {
5 | self.buttonStyle(NavigationBarButtonStyle())
6 | }
7 | }
8 |
9 | struct NavigationBarButtonStyle: ButtonStyle {
10 | func makeBody(configuration: Self.Configuration) -> some View {
11 | configuration.label
12 | .foregroundColor(.nef)
13 | .padding(8)
14 | .contentShape(RoundedRectangle(cornerRadius: 4))
15 | .safeHoverEffect()
16 |
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/Rectangle+Separator.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Rectangle {
4 | static var separator: some View {
5 | Rectangle()
6 | .fill(Color.gray)
7 | .frame(height: 0.5)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/SearchBar.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SearchBar: UIViewRepresentable {
4 | let placeholder: String
5 | @Binding var query: String
6 | let barStyle: UIBarStyle
7 | let onSearch: (String) -> Void
8 |
9 | init(placeholder: String,
10 | query: Binding,
11 | barStyle: UIBarStyle = .default,
12 | onSearch: @escaping (String) -> Void) {
13 | self.placeholder = placeholder
14 | self._query = query
15 | self.barStyle = barStyle
16 | self.onSearch = onSearch
17 | }
18 |
19 | class SearchCoordinator: NSObject, UISearchBarDelegate {
20 | @Binding var query: String
21 | let onSearch: (String) -> Void
22 |
23 | init(query: Binding, onSearch: @escaping (String) -> Void) {
24 | self._query = query
25 | self.onSearch = onSearch
26 | }
27 |
28 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
29 | self.query = searchText
30 | }
31 |
32 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
33 | self.onSearch(self.query)
34 | }
35 | }
36 |
37 | func makeUIView(context: UIViewRepresentableContext) -> UISearchBar {
38 | let searchBar = UISearchBar()
39 | searchBar.delegate = context.coordinator
40 | searchBar.placeholder = self.placeholder
41 | searchBar.barStyle = self.barStyle
42 | searchBar.searchBarStyle = .minimal
43 | return searchBar
44 | }
45 |
46 | func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext) {
47 | uiView.text = query
48 | }
49 |
50 | func makeCoordinator() -> SearchCoordinator {
51 | SearchCoordinator(query: $query, onSearch: self.onSearch)
52 | }
53 | }
54 |
55 | #if DEBUG
56 | struct SearchBar_Previews: PreviewProvider {
57 | static var previews: some View {
58 | Group {
59 | SearchBar(placeholder: "Search repository...",
60 | query: .constant(""),
61 | onSearch: { _ in })
62 | .previewLayout(.sizeThatFits)
63 |
64 | SearchBar(placeholder: "Search repository...",
65 | query: .constant("Bow"),
66 | barStyle: .black,
67 | onSearch: { _ in })
68 | .previewLayout(.sizeThatFits)
69 | }
70 | }
71 | }
72 | #endif
73 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/SectionTitle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SectionTitle: View {
4 | struct Action {
5 | let icon: Image
6 | let handle: () -> Void
7 |
8 | init(icon: Image, handle: @autoclosure @escaping () -> Void) {
9 | self.icon = icon
10 | self.handle = handle
11 | }
12 | }
13 |
14 | let title: String
15 | let action: Action?
16 |
17 | init(title: String, action: Action? = nil) {
18 | self.title = title
19 | self.action = action
20 | }
21 |
22 | var body: some View {
23 | HStack(alignment: .firstTextBaseline) {
24 | Text(title)
25 | .largeTitleStyle()
26 | self.actionView()
27 | .alignmentGuide(.firstTextBaseline) { d in d[.bottom] * 0.82 }
28 | .padding(.leading, 16)
29 | Spacer()
30 | }
31 | }
32 |
33 | private func actionView() -> some View {
34 | guard let action = action else {
35 | return AnyView(EmptyView())
36 | }
37 |
38 | return AnyView(
39 | Button(action: action.handle) { action.icon }
40 | .buttonStyle(ActionButtonStyle())
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/TextButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct TextButtonStyle: ButtonStyle {
4 | func makeBody(configuration: Self.Configuration) -> some View {
5 | configuration.label
6 | .padding()
7 | .font(Font.system(.body).bold())
8 | .foregroundColor(.white)
9 | .frame(maxWidth: .infinity)
10 | .contentShape(RoundedRectangle(cornerRadius: 8))
11 | .background(RoundedRectangle(cornerRadius: 8)
12 | .fill(configuration.isPressed
13 | ? Color.nef.opacity(0.7)
14 | : Color.nef))
15 | .safeHoverEffect()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/TextStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Text {
4 | func largeTitleStyle() -> Text {
5 | self.bold().font(.largeTitle)
6 | }
7 |
8 | func titleStyle() -> Text {
9 | self.bold().font(.title)
10 | }
11 |
12 | func activityStyle() -> Text {
13 | self.font(.callout)
14 | .foregroundColor(Color.gray)
15 | }
16 |
17 | func cardTitleStyle() -> some View {
18 | self.scaledFont(.system(desiredSize: 20, weight: .thin))
19 | .foregroundColor(.gray)
20 | }
21 |
22 | func cardBodyStyle() -> some View {
23 | self.scaledFont(.system(desiredSize: 20, weight: .thin))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/URLImage.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Bow
3 | import BowEffects
4 |
5 | struct URLImage: View {
6 | @ObservedObject private var loader: ImageLoader
7 | private let placeholder: Placeholder?
8 |
9 | init(url: URL, placeholder: Placeholder? = nil) {
10 | loader = ImageLoader(url: url)
11 | self.placeholder = placeholder
12 | }
13 |
14 | var body: some View {
15 | image.onAppear(perform: loader.load)
16 | }
17 |
18 | private var image: some View {
19 | Group {
20 | if loader.image != nil {
21 | Image(uiImage: loader.image!)
22 | .resizable()
23 | } else {
24 | placeholder
25 | }
26 | }
27 | }
28 | }
29 |
30 | private class ImageLoader: ObservableObject {
31 | @Published var image: UIImage?
32 | private let url: URL
33 | private static let downloadQueue = DispatchQueue(label: "download-images")
34 |
35 | init(url: URL) {
36 | self.url = url
37 | }
38 |
39 | func load() {
40 | URLSession.shared.downloadTaskIO(with: self.url)
41 | .flatMap { result in
42 | IO.invoke {
43 | try Data.init(contentsOf: result.url)
44 | }
45 | }.map(UIImage.init(data:))
46 | .handleError { _ in nil }^
47 | .unsafeRunAsync(on: ImageLoader.downloadQueue) { either in
48 | DispatchQueue.main.async { [weak self] in
49 | self?.image = either.getOrElse(nil)
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/View+Conditional.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import SwiftUI
3 |
4 | extension View {
5 | func `if`(
6 | _ condition: Bool,
7 | then f: @escaping (Self) -> Then,
8 | else g: @escaping (Self) -> Else
9 | ) -> some View {
10 | Group {
11 | if condition {
12 | f(self)
13 | } else {
14 | g(self)
15 | }
16 | }
17 | }
18 |
19 | func `if`(
20 | _ condition: Bool,
21 | then f: @escaping (Self) -> Then
22 | ) -> some View {
23 | self.if(condition,
24 | then: f,
25 | else: { $0 })
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/View+Hover.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 | func safeHoverEffect() -> some View {
5 | if #available(iOS 13.4, *) {
6 | return AnyView(self.hoverEffect())
7 | } else {
8 | return AnyView(self)
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/View+Modal.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 | func modal(
5 | isPresented: Binding,
6 | withNavigation: Bool = true,
7 | @ViewBuilder content: @escaping () -> V
8 | ) -> some View {
9 | background(
10 | EmptyView().sheet(isPresented: isPresented) {
11 | Group {
12 | if withNavigation {
13 | NavigationView {
14 | content()
15 | }.navigationViewStyle(StackNavigationViewStyle())
16 | } else {
17 | content()
18 | }
19 | }
20 | }
21 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/NefEditorClient/Theme/ViewStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 | var fill: some View {
5 | self.frame(maxWidth: .infinity, maxHeight: .infinity)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/NefEditorClient/ViewModifiers/ScaledFont.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - ViewModifier
4 | struct ScaledFont: ViewModifier {
5 | @Environment(\.sizeCategory) var sizeCategory
6 | var font: ScaledFontType
7 |
8 | func body(content: Content) -> some View {
9 | content.font(font.dynamic)
10 | }
11 | }
12 |
13 | // MARK: - Models
14 | enum ScaledFontType {
15 | case system(desiredSize: CGFloat, weight: Font.Weight)
16 | case custom(desiredSize: CGFloat, name: String)
17 |
18 | var dynamic: Font {
19 | let scaledSize = UIFontMetrics.default.scaledValue(for: desiredSize)
20 |
21 | switch self {
22 | case let .custom(_, name):
23 | return .custom(name, size: scaledSize)
24 | case let .system(_, weight):
25 | return .system(size: scaledSize, weight: weight)
26 | }
27 | }
28 |
29 | var desiredSize: CGFloat {
30 | switch self {
31 | case let .system(desiredSize, _):
32 | return desiredSize
33 | case let .custom(desiredSize, _):
34 | return desiredSize
35 | }
36 | }
37 | }
38 |
39 | // MARK: - Helpers
40 | extension View {
41 | func scaledFont(_ font: ScaledFontType) -> some View {
42 | modifier(ScaledFont(font: font))
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/NefEditorClient/WhatsNew/Component/WhatsNewComponent.swift:
--------------------------------------------------------------------------------
1 | import BowArch
2 |
3 | typealias WhatsNewComponent = StoreComponent
4 |
5 | func whatsNewComponent() -> WhatsNewComponent {
6 | WhatsNewComponent(initialState: ()) { _, handle in
7 | WhatsNewView(handle: handle)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/NefEditorClient/WhatsNew/Logic/WhatsNewAction.swift:
--------------------------------------------------------------------------------
1 | import Bow
2 | import Foundation
3 |
4 | enum WhatsNewAction {
5 | case openGenerator
6 | case dismiss
7 | }
8 |
--------------------------------------------------------------------------------
/NefEditorClient/WhatsNew/Logic/WhatsNewDispatcher.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Bow
3 | import BowEffects
4 | import BowArch
5 |
6 | typealias WhatsNewDispatcher = StateDispatcher
7 |
8 | let whatsNewDispatcher = WhatsNewDispatcher.effectful { action in
9 | switch action {
10 | case .openGenerator:
11 | return open(url: URL(string: "https://badge.bow-swift.io"))
12 | case .dismiss:
13 | return updateUserPreferences().as(dismissModal())^
14 | }
15 | }
16 |
17 |
18 | func updateUserPreferences() -> EnvIO {
19 | EnvIO.accessM { persistence in
20 | let bundleVersion = Bundle.main.version
21 | return persistence.updateUserPreferences(bundleVersion: bundleVersion)^
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/NefEditorClient/WhatsNew/View/BadgeGeneratorCard.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct BadgeGeneratorCard: View {
4 | let handle: (WhatsNewAction) -> Void
5 |
6 | var body: some View {
7 | VStack {
8 | Text("Badge generator")
9 | .cardTitleStyle()
10 | .frame(maxWidth: .infinity, alignment: .leading)
11 | .padding(.init(top: 24, leading: 24, bottom: 0, trailing: 24))
12 |
13 | HStack {
14 | Image.appIcon.resizable().frame(width: 120, height: 120)
15 | .clipShape(Circle())
16 | .overlay(Circle().stroke(lineWidth: 4).foregroundColor(.nef))
17 | Image.heart
18 | .font(.body).foregroundColor(.red)
19 | .padding()
20 | Image.githubIcon.resizable().frame(width: 120, height: 120)
21 | .clipShape(Circle())
22 | }.padding(24)
23 |
24 | Divider().padding(.horizontal, 24)
25 |
26 | HStack {
27 | Spacer()
28 | Image.badgePlatform.opacity(0.5)
29 | Image.badgeActions.opacity(0.5)
30 | Image.badgeNef
31 | Spacer()
32 | }
33 |
34 | Text("You can attach a nef badge to your GitHub Swift repo, to let users try your project directly in their iPads.")
35 | .cardBodyStyle()
36 | .multilineTextAlignment(.center)
37 | .padding(.init(top: 18, leading: 44, bottom: 0, trailing: 44))
38 |
39 | Button(action: { self.handle(.openGenerator) }) {
40 | HStack {
41 | Image.wand.resizable().frame(width: 30, height: 30)
42 | Text("Open badge generator")
43 | .font(.body)
44 | .foregroundColor(Color.blue)
45 | }
46 | }.padding(44)
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/NefEditorClient/WhatsNew/View/WhatsNewView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct WhatsNewView: View {
4 | @Environment(\.colorScheme) var colorScheme
5 | let handle: (WhatsNewAction) -> Void
6 |
7 | var body: some View {
8 | VStack {
9 | whatsNewCard()
10 | Spacer()
11 | Button("Got it!", action: { self.handle(.dismiss) })
12 | .frame(maxWidth: .infinity)
13 | .buttonStyle(TextButtonStyle())
14 | .padding()
15 |
16 | }.navigationBarTitle("What's New", displayMode: .inline)
17 | }
18 |
19 | private func whatsNewCard() -> some View {
20 | BadgeGeneratorCard(handle: handle)
21 | }
22 | }
23 |
24 | #if DEBUG
25 | struct WhatsNewView_Previews: PreviewProvider {
26 | static var previews: some View {
27 | Group {
28 | WhatsNewView() { _ in }.environment(\.colorScheme, .light)
29 | WhatsNewView() { _ in }.preferredColorScheme(.dark)
30 | }.previewLayout(.fixed(width: 800, height: 800))
31 | }
32 | }
33 | #endif
34 |
--------------------------------------------------------------------------------
/NefEditorClientTests/AppDispatcherTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import Bow
3 | import BowEffects
4 | import BowArch
5 | @testable import NefEditorClient
6 |
7 | class AppDispatcherTests: XCTestCase {
8 |
9 | func testAddDependency() {
10 | let id = UUID()
11 | let recipe = Recipe(id: id, title: "A", description: "B", dependencies: [])
12 | let updatedRecipe = Recipe(id: id, title: "A", description: "B", dependencies: [Dependency(repository: bow.name, url: bow.htmlUrl, requirement: sampleRequirements[0])])
13 |
14 | let appState = AppState(
15 | panelState: .catalog,
16 | editState: .notEditing,
17 | searchState: SearchState(loadingState: .initial, modalState: .noModal),
18 | catalog: Catalog(featured: CatalogSection(title: "1", items: []),
19 | userCreated: CatalogSection(title: "2", items: [.regular(recipe)])),
20 | selectedItem: .regular(recipe))
21 |
22 | let expected = AppState(
23 | panelState: .catalog,
24 | editState: .notEditing,
25 | searchState: SearchState(loadingState: .initial, modalState: .noModal),
26 | catalog: Catalog(featured: CatalogSection(title: "1", items: []),
27 | userCreated: CatalogSection(title: "2", items: [.regular(updatedRecipe)])),
28 | selectedItem: .regular(updatedRecipe))
29 |
30 | assert(dispatcher: addDependencyDispatcher,
31 | on: .dependencySelected(sampleRequirements[0], from: bow),
32 | initialState: appState,
33 | expectedState: expected)
34 | }
35 |
36 | func testAddDependencyAppDispatcher() {
37 | let id = UUID()
38 | let recipe = Recipe(id: id, title: "A", description: "B", dependencies: [])
39 | let updatedRecipe = Recipe(id: id, title: "A", description: "B", dependencies: [Dependency(repository: bow.name, url: bow.htmlUrl, requirement: sampleRequirements[0])])
40 |
41 | let appState = AppState(
42 | panelState: .catalog,
43 | editState: .notEditing,
44 | searchState: SearchState(loadingState: .initial, modalState: .noModal),
45 | catalog: Catalog(featured: CatalogSection(title: "1", items: []),
46 | userCreated: CatalogSection(title: "2", items: [.regular(recipe)])),
47 | selectedItem: .regular(recipe))
48 |
49 | let expected = AppState(
50 | panelState: .catalog,
51 | editState: .notEditing,
52 | searchState: SearchState(loadingState: .initial, modalState: .noModal),
53 | catalog: Catalog(featured: CatalogSection(title: "1", items: []),
54 | userCreated: CatalogSection(title: "2", items: [.regular(updatedRecipe)])),
55 | selectedItem: .regular(updatedRecipe))
56 |
57 | assert(dispatcher: appDispatcher,
58 | on: .searchAction(
59 | .repositoryDetailAction(
60 | .dependencySelected(sampleRequirements[0], from: bow))),
61 | initialState: appState,
62 | expectedState: expected)
63 | }
64 |
65 | func assert(
66 | dispatcher: StateDispatcher,
67 | on input: I,
68 | with environment: E,
69 | initialState: S,
70 | expectedState: S) {
71 |
72 | let actions = dispatcher.on(input)
73 | let finalState = actions.reduce(initialState) { state, next in
74 | try! next.map { action in
75 | action^.runS(state)^.value
76 | }^
77 | .provide(environment)
78 | .unsafeRunSync()
79 | }
80 |
81 | XCTAssertEqual(finalState, expectedState)
82 | }
83 |
84 | func assert(
85 | dispatcher: StateDispatcher,
86 | on input: I,
87 | initialState: S,
88 | expectedState: S) {
89 |
90 | assert(
91 | dispatcher: dispatcher,
92 | on: input,
93 | with: (),
94 | initialState: initialState,
95 | expectedState: expectedState)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/NefEditorClientTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/NefEditorClientTests/NefEditorClientTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NefEditorClientTests.swift
3 | // NefEditorClientTests
4 | //
5 | // Created by Tomás Ruiz López on 30/03/2020.
6 | // Copyright © 2020 The Bow Authors. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import NefEditorClient
11 |
12 | class NefEditorClientTests: XCTestCase {
13 |
14 | override func setUp() {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDown() {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | }
26 |
27 | func testPerformanceExample() {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nef Playgrounds
2 |
3 | Welcome to the client-side code of [nef Playgrounds for iPad](https://apps.apple.com/us/app/nef-playgrounds/id1511012848)!
4 |
5 | nef Playgrounds lets you:
6 |
7 | 👨🍳 Create a nef recipe...
8 |
9 | 📦 ... add your favorite Swift dependencies ...
10 |
11 | 📲 ... and create a Swift Playground that you can use on your iPad!
12 |
13 | ## How does it work?
14 |
15 | nef Playgrounds uses GitHub API to search for Swift repositories and select a branch or tag that can be used as a dependency in Swift Package Manager. Then, it communicates with [its backend](https://github.com/bow-swift/nef-editor-server), which is also open source, to generate a Swift Playground that contains the selected dependencies. This Playground is sent back to the client and users can open it in the Playgrounds app. It will let users write Swift code using the Swift Packages of their choice.
16 |
17 | > Unfortunately, this may not always work; your repository must contain only Swift code, have a `Package.swift` manifest file, and be prepared to run on the iPad (the runtime in the iPad is slightly different and there may be parts of your library that do not work properly).
18 |
19 | ## How do I run this project?
20 |
21 | - Open the project on Xcode.
22 | - Run the schemes `GenerateGitHubAPI` and `GenerateNefAPI`. You will need to have [Bow OpenAPI](https://openapi.bow-swift.io) installed on your Mac. These tasks will generate two folders named `GitHub` and `NefAPI` respectively.
23 | - Add the folders to the root of the project.
24 | - You may need to configure your app ID and create the necessary entitlements to handle Apple Sign In and iCloud storage.
25 |
26 | ## License
27 |
28 | Copyright (C) 2020-2021 The nef Authors
29 |
30 | Licensed under the Apache License, Version 2.0 (the "License");
31 | you may not use this file except in compliance with the License.
32 | You may obtain a copy of the License at
33 |
34 | http://www.apache.org/licenses/LICENSE-2.0
35 |
36 | Unless required by applicable law or agreed to in writing, software
37 | distributed under the License is distributed on an "AS IS" BASIS,
38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
39 | See the License for the specific language governing permissions and
40 | limitations under the License.
41 |
--------------------------------------------------------------------------------
/github.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | version: '0.1.0'
4 | title: GitHub API
5 | paths:
6 | '/search/repositories':
7 | get:
8 | operationId: searchRepositories
9 | tags:
10 | - Search
11 | description: Searches for repositories matching the provided query.
12 | parameters:
13 | - in: query
14 | name: q
15 | description: Query for the repository search.
16 | required: true
17 | schema:
18 | type: string
19 | responses:
20 | '200':
21 | description: Returns a list of repositories matching the provided query.
22 | content:
23 | application/json:
24 | schema:
25 | $ref: '#/components/schemas/SearchRepositoriesResult'
26 | '/repos/{full_name}/branches':
27 | get:
28 | operationId: getBranches
29 | tags:
30 | - Repository
31 | description: Obtains all branches of a repository.
32 | parameters:
33 | - in: path
34 | name: full_name
35 | description: Full name (owner + repository) of a repository.
36 | required: true
37 | schema:
38 | type: string
39 | responses:
40 | '200':
41 | description: Returns a list of branches for a given repository.
42 | content:
43 | application/json:
44 | schema:
45 | $ref: '#/components/schemas/Branches'
46 | '/repos/{full_name}/tags':
47 | get:
48 | operationId: getVersions
49 | tags:
50 | - Repository
51 | description: Obtains all tags of a repository.
52 | parameters:
53 | - in: path
54 | name: full_name
55 | description: Full name (owner + repository) of a repository.
56 | required: true
57 | schema:
58 | type: string
59 | responses:
60 | '200':
61 | description: Returns a list of tags for a given repository.
62 | content:
63 | application/json:
64 | schema:
65 | $ref: '#/components/schemas/Tags'
66 | components:
67 | schemas:
68 | Branches:
69 | description: An array of branches.
70 | type: array
71 | items:
72 | $ref: '#/components/schemas/Branch'
73 | Branch:
74 | description: A representation of the metadata of a branch in a GitHub repository.
75 | type: object
76 | properties:
77 | name:
78 | type: string
79 | required:
80 | - name
81 | Owner:
82 | description: A representation of the metadata of the owner of a GitHub repository.
83 | type: object
84 | properties:
85 | login:
86 | type: string
87 | avatar_url:
88 | type: string
89 | required:
90 | - login
91 | - avatar_url
92 | Repositories:
93 | description: An array of repositories.
94 | type: array
95 | items:
96 | $ref: '#/components/schemas/Repository'
97 | Repository:
98 | description: A representation of the metadata of a GitHub repository.
99 | type: object
100 | properties:
101 | name:
102 | type: string
103 | full_name:
104 | type: string
105 | description:
106 | type: string
107 | private:
108 | type: boolean
109 | html_url:
110 | type: string
111 | stargazers_count:
112 | type: integer
113 | owner:
114 | $ref: '#/components/schemas/Owner'
115 | required:
116 | - name
117 | - full_name
118 | - private
119 | - html_url
120 | - stargazers_count
121 | - owner
122 | SearchRepositoriesResult:
123 | description: Describes the results of a repository search.
124 | type: object
125 | properties:
126 | total_count:
127 | type: integer
128 | items:
129 | $ref: '#/components/schemas/Repositories'
130 | required:
131 | - total_count
132 | - items
133 | Tags:
134 | description: An array of tags.
135 | type: array
136 | items:
137 | $ref: '#/components/schemas/Tag'
138 | Tag:
139 | description: A representation of the metadata of a tag in a GitHub repository.
140 | type: object
141 | properties:
142 | name:
143 | type: string
144 | required:
145 | - name
146 |
--------------------------------------------------------------------------------
/nef.yaml:
--------------------------------------------------------------------------------
1 | openapi: "3.0.0"
2 | info:
3 | title: nef Playgrounds - Server
4 | version: "1.0.0"
5 |
6 | paths:
7 | /signin:
8 | post:
9 | summary: Sign in with Apple.
10 | operationId: signin
11 | requestBody:
12 | required: true
13 | content:
14 | application/json:
15 | schema:
16 | $ref: "#/components/schemas/AppleSignInRequest"
17 | responses:
18 | '200':
19 | description: The request was successful.
20 | content:
21 | application/json:
22 | schema:
23 | $ref: '#/components/schemas/AppleSignInResponse'
24 | '500':
25 | description: The request was aborted.
26 | content:
27 | application/json:
28 | schema:
29 | $ref: '#/components/schemas/NefEditorError'
30 |
31 | components:
32 | schemas:
33 |
34 | NefEditorError:
35 | type: object
36 | required:
37 | - error
38 | - reason
39 | properties:
40 | error:
41 | type: boolean
42 | reason:
43 | type: string
44 |
45 | AppleSignInRequest:
46 | type: object
47 | required:
48 | - identityToken
49 | - authorizationCode
50 | properties:
51 | identityToken:
52 | type: string
53 | authorizationCode:
54 | type: string
55 |
56 | AppleSignInResponse:
57 | type: object
58 | required:
59 | - token
60 | properties:
61 | token:
62 | type: string
63 |
--------------------------------------------------------------------------------