├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── Reddit-iOS
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ ├── Icon-App-83.5x83.5@2x.png
│ │ └── ItunesArtwork@2x.png
│ └── Contents.json
├── Base.lproj
│ └── LaunchScreen.storyboard
├── ContentView.swift
├── Info.plist
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Reddit.entitlements
├── Representable
│ ├── SpinnerView.swift
│ └── WebView.swift
├── SceneDelegate.swift
└── Views
│ └── PostList.swift
├── Reddit-macOS
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── icon_128@1x.png
│ │ ├── icon_128@2x.png
│ │ ├── icon_16@1x.png
│ │ ├── icon_16@2x.png
│ │ ├── icon_256@1x.png
│ │ ├── icon_256@2x.png
│ │ ├── icon_32@1x.png
│ │ ├── icon_32@2x.png
│ │ ├── icon_512@1x.png
│ │ └── icon_512@2x.png
│ └── Contents.json
├── Base.lproj
│ └── Main.storyboard
├── ContentView.swift
├── ContentViewState.swift
├── Info.plist
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Reddit_macOS.entitlements
├── Representable
│ ├── SpinnerView.swift
│ └── WebView.swift
└── Views
│ ├── Helpers
│ └── DetailWindowController.swift
│ └── PostList.swift
├── Reddit-watchOS WatchKit App
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-24@2x.png
│ │ ├── Icon-27.5@2x.png
│ │ ├── Icon-29@2x.png
│ │ ├── Icon-29@3x.png
│ │ ├── Icon-40@2x.png
│ │ ├── Icon-86@2x.png
│ │ ├── Icon-98@2x.png
│ │ └── ItunesArtwork@2x.png
│ └── Contents.json
├── Base.lproj
│ └── Interface.storyboard
└── Info.plist
├── Reddit-watchOS WatchKit Extension
├── Assets.xcassets
│ ├── Complication.complicationset
│ │ ├── Circular.imageset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── Extra Large.imageset
│ │ │ └── Contents.json
│ │ ├── Graphic Bezel.imageset
│ │ │ └── Contents.json
│ │ ├── Graphic Circular.imageset
│ │ │ └── Contents.json
│ │ ├── Graphic Corner.imageset
│ │ │ └── Contents.json
│ │ ├── Graphic Large Rectangular.imageset
│ │ │ └── Contents.json
│ │ ├── Modular.imageset
│ │ │ └── Contents.json
│ │ └── Utilitarian.imageset
│ │ │ └── Contents.json
│ └── Contents.json
├── ContentView.swift
├── ExtensionDelegate.swift
├── HostingController.swift
├── Info.plist
├── NotificationController.swift
├── NotificationView.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── PushNotificationPayload.apns
├── Representable
│ ├── SpinnerView.swift
│ └── WebView.swift
└── Views
│ ├── CommentsView.swift
│ ├── Compatibility
│ ├── SpinnerView.swift
│ └── WebView.swift
│ ├── PostDetailView.swift
│ ├── PostView.swift
│ └── SettingsView.swift
├── Reddit.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
└── xcshareddata
│ └── xcschemes
│ ├── Reddit-watchOS WatchKit App (Notification).xcscheme
│ └── Reddit-watchOS WatchKit App.xcscheme
├── Resources
└── banner.jpeg
└── Shared
├── API.swift
├── Helpers
└── Helpers.swift
├── Models
├── Comment.swift
├── Listing.swift
├── Post.swift
└── SortBy.swift
├── SharedAssets.xcassets
├── Contents.json
├── popover.colorset
│ └── Contents.json
└── stickied.colorset
│ └── Contents.json
└── Views
├── CommentsView.swift
├── FlairView.swift
├── MetadataView.swift
├── PostDetailView.swift
└── PostView.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: carson-katri
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | .DS_Store
6 |
7 | ## Build generated
8 | build/
9 | DerivedData/
10 |
11 | ## Various settings
12 | *.pbxuser
13 | !default.pbxuser
14 | *.mode1v3
15 | !default.mode1v3
16 | *.mode2v3
17 | !default.mode2v3
18 | *.perspectivev3
19 | !default.perspectivev3
20 | xcuserdata/
21 |
22 | ## Other
23 | *.moved-aside
24 | *.xccheckout
25 | *.xcscmblueprint
26 |
27 | ## Obj-C/Swift specific
28 | *.hmap
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | .build/
44 |
45 | # CocoaPods
46 | #
47 | # We recommend against adding the Pods directory to your .gitignore. However
48 | # you should judge for yourself, the pros and cons are mentioned at:
49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
50 | #
51 | # Pods/
52 | #
53 | # Add this line if you want to avoid checking in source code from the Xcode workspace
54 | # *.xcworkspace
55 |
56 | # Carthage
57 | #
58 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
59 | # Carthage/Checkouts
60 |
61 | Carthage/Build
62 |
63 | # Accio dependency management
64 | Dependencies/
65 | .accio/
66 |
67 | # fastlane
68 | #
69 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
70 | # screenshots whenever they are needed.
71 | # For more information about the recommended setup visit:
72 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
73 |
74 | fastlane/report.xml
75 | fastlane/Preview.html
76 | fastlane/screenshots/**/*.png
77 | fastlane/test_output
78 |
79 | # Code Injection
80 | #
81 | # After new code Injection tools there's a generated folder /iOSInjectionProject
82 | # https://github.com/johnno1962/injectionforxcode
83 |
84 | iOSInjectionProject/
85 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Carson Katri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 | A cross-platform Reddit client created in SwiftUI.
3 | Get the Public Beta
4 |
5 | > *Note:* This project is far from complete. It still lacks many features of your typical Reddit client and has bugs (partly due to SwiftUI, but I'll take credit for some of them)
6 |
7 | To show off SwiftUI's strength in cross-platform development, I did **not** use Mac Catalyst for this project. Instead, common UI code is shared between iOS, macOS, and watchOS.
8 |
9 |
10 | ## Project Structure
11 | * `Shared` - Models, helpers, API, and any shared Views.
12 | * `Reddit-[PLATFORM]` - Each target folder contains a `Views` and `Representable` folder. `Views` holds platform-specific views, and `Representable` contains `UIViewRepresentables` or `NSViewRepresentables`.
13 |
14 | ## macOS Specific Features
15 | I've added several things to make the macOS app stand out:
16 | 1. Double click - You can double click on a post to open a new window for the detail view.
17 | 2. `NSToolbar` - This is implemented entirely in the `AppDelegate`, and uses standard Cocoa code which interfaces with the SwiftUI views.
18 | 3. `TouchBar` - TODO
19 |
20 | ## SF Symbols
21 | Because macOS doesn't support SF Symbols, I have created the following extension to make sure shared code works. I would like to replace this with custom icons for macOS that it loads from `XCAssets` eventually:
22 | ```swift
23 | /// `SwiftUI` compatibility
24 | #if os(macOS)
25 | extension Image {
26 | init(systemName: String) {
27 | self.init(nsImage: NSImage())
28 | }
29 | }
30 | #endif
31 | ```
32 |
--------------------------------------------------------------------------------
/Reddit-iOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/20/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | func applicationWillTerminate(_ application: UIApplication) {
22 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
23 | }
24 |
25 | // MARK: UISceneSession Lifecycle
26 |
27 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
28 | // Called when a new scene session is being created.
29 | // Use this method to select a configuration to create the new scene with.
30 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
31 | }
32 |
33 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
34 | // Called when the user discards a scene session.
35 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
36 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
37 | }
38 |
39 |
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "ItunesArtwork@2x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-iOS/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png
--------------------------------------------------------------------------------
/Reddit-iOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Reddit-iOS/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 |
--------------------------------------------------------------------------------
/Reddit-iOS/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/20/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct ContentView : View {
13 | @State private var subreddit: String = "swift"
14 | @State private var sortBy: SortBy = .hot
15 |
16 | @State private var showSortSheet: Bool = false
17 | @State private var showSubredditSheet: Bool = false
18 |
19 | var body: some View {
20 | NavigationView {
21 | /// Load the posts
22 | PostList(subreddit: subreddit, sortBy: sortBy)
23 | /// Force inline `NavigationBar`
24 | .navigationBarTitle(Text(""), displayMode: .inline)
25 | .navigationBarItems(leading: HStack {
26 | Button(action: {
27 | self.showSubredditSheet.toggle()
28 | }) {
29 | Text("r/\(self.subreddit)")
30 | }
31 | }, trailing: HStack {
32 | Button(action: {
33 | self.showSortSheet.toggle()
34 | }) {
35 | HStack {
36 | Image(systemName: "arrow.up.arrow.down")
37 | Text(self.sortBy.rawValue)
38 | }
39 | }
40 | })
41 | /// Sorting method `ActionSheet`
42 | .actionSheet(isPresented: $showSortSheet) {
43 | ActionSheet(title: Text("Sort By:"), buttons: [SortBy.hot, SortBy.top, SortBy.new, SortBy.controversial, SortBy.rising].map { method in
44 | ActionSheet.Button.default(Text(method.rawValue.prefix(1).uppercased() + method.rawValue.dropFirst())) {
45 | self.sortBy = method
46 | }
47 | })
48 | }
49 | /// Subreddit selection `Popover`
50 | .popover(isPresented: $showSubredditSheet, attachmentAnchor: .point(UnitPoint(x: 20, y: 20))) {
51 | HStack(spacing: 0) {
52 | Text("r/")
53 | TextField("Subreddit", text: self.$subreddit) {
54 | self.showSubredditSheet.toggle()
55 | }
56 | }
57 | .frame(width: 200)
58 | .padding()
59 | .background(Color("popover"))
60 | .cornerRadius(10)
61 | }
62 | Text("Select a post")
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Reddit-iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 | NSAllowsArbitraryLoads
8 |
9 |
10 | CFBundleDevelopmentRegion
11 | $(DEVELOPMENT_LANGUAGE)
12 | CFBundleExecutable
13 | $(EXECUTABLE_NAME)
14 | CFBundleIdentifier
15 | $(PRODUCT_BUNDLE_IDENTIFIER)
16 | CFBundleInfoDictionaryVersion
17 | 6.0
18 | CFBundleName
19 | $(PRODUCT_NAME)
20 | CFBundlePackageType
21 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
22 | CFBundleShortVersionString
23 | 1.0
24 | CFBundleVersion
25 | 1
26 | LSRequiresIPhoneOS
27 |
28 | UIApplicationSceneManifest
29 |
30 | UIApplicationSupportsMultipleScenes
31 |
32 | UISceneConfigurations
33 |
34 | UIWindowSceneSessionRoleApplication
35 |
36 |
37 | UILaunchStoryboardName
38 | LaunchScreen
39 | UISceneConfigurationName
40 | Default Configuration
41 | UISceneDelegateClassName
42 | $(PRODUCT_MODULE_NAME).SceneDelegate
43 |
44 |
45 |
46 |
47 | UILaunchStoryboardName
48 | LaunchScreen
49 | UIRequiredDeviceCapabilities
50 |
51 | armv7
52 |
53 | UISupportedInterfaceOrientations
54 |
55 | UIInterfaceOrientationPortrait
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 | UISupportedInterfaceOrientations~ipad
60 |
61 | UIInterfaceOrientationPortrait
62 | UIInterfaceOrientationPortraitUpsideDown
63 | UIInterfaceOrientationLandscapeLeft
64 | UIInterfaceOrientationLandscapeRight
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Reddit-iOS/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Reddit-iOS/Reddit.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Reddit-iOS/Representable/SpinnerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpinnerView.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/28/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// A circular spinner for SwiftUI
12 | struct SpinnerView: UIViewRepresentable {
13 | func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView {
14 | return UIActivityIndicatorView()
15 | }
16 |
17 | func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) {
18 | uiView.startAnimating()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Reddit-iOS/Representable/WebView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebView.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/28/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import WebKit
11 |
12 | /// `WKWebView` replacement for `SwiftUI`
13 | struct WebView: UIViewRepresentable {
14 | let url: URL
15 |
16 | func makeUIView(context: UIViewRepresentableContext) -> WKWebView {
17 | return WKWebView(frame: .zero)
18 | }
19 |
20 | func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext) {
21 | uiView.load(URLRequest(url: url))
22 | uiView.isOpaque = false
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Reddit-iOS/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/20/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
21 |
22 | // Use a UIHostingController as window root view controller
23 | let window = UIWindow(windowScene: scene as! UIWindowScene)
24 | window.rootViewController = UIHostingController(rootView: ContentView())
25 | self.window = window
26 | window.makeKeyAndVisible()
27 | }
28 |
29 | func sceneDidDisconnect(_ scene: UIScene) {
30 | // Called as the scene is being released by the system.
31 | // This occurs shortly after the scene enters the background, or when its session is discarded.
32 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
33 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
34 | }
35 |
36 | func sceneDidBecomeActive(_ scene: UIScene) {
37 | // Called when the scene has moved from an inactive state to an active state.
38 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
39 | }
40 |
41 | func sceneWillResignActive(_ scene: UIScene) {
42 | // Called when the scene will move from an active state to an inactive state.
43 | // This may occur due to temporary interruptions (ex. an incoming phone call).
44 | }
45 |
46 | func sceneWillEnterForeground(_ scene: UIScene) {
47 | // Called as the scene transitions from the background to the foreground.
48 | // Use this method to undo the changes made on entering the background.
49 | }
50 |
51 | func sceneDidEnterBackground(_ scene: UIScene) {
52 | // Called as the scene transitions from the foreground to the background.
53 | // Use this method to save data, release shared resources, and store enough scene-specific state information
54 | // to restore the scene back to its current state.
55 | }
56 |
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/Reddit-iOS/Views/PostList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostList.swift
3 | // Reddit-macOS
4 | //
5 | // Created by Carson Katri on 7/31/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct PostList: View {
13 | let subreddit: String
14 | let sortBy: SortBy
15 |
16 | var body: some View {
17 | /// Load posts from web and decode as `Listing`
18 | RequestView(Listing.self, Request {
19 | Url(API.subredditURL(subreddit, sortBy))
20 | Query(["raw_json":"1"])
21 | }) { listing in
22 | /// List of `PostView`s when loaded
23 | List(listing != nil ? listing!.data.children.map { $0.data } : []) { post in
24 | NavigationLink(destination: PostDetailView(post: post)) {
25 | PostView(post: post)
26 | }
27 | }
28 | /// Spinner when loading
29 | SpinnerView()
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Reddit-macOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Reddit-macOS
4 | //
5 | // Created by Carson Katri on 7/31/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import SwiftUI
11 |
12 | @NSApplicationMain
13 | class AppDelegate: NSObject, NSApplicationDelegate, NSToolbarDelegate, NSTextFieldDelegate {
14 |
15 | var window: NSWindow!
16 |
17 | var toolbar: NSToolbar!
18 |
19 | @ObservedObject var state = ContentViewState()
20 |
21 | func applicationDidFinishLaunching(_ aNotification: Notification) {
22 | // Insert code here to initialize your application
23 | window = NSWindow(
24 | contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
25 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
26 | backing: .buffered, defer: false)
27 | window.center()
28 | window.setFrameAutosaveName("Main Window")
29 | window.title = "Reddit"
30 |
31 | window.contentView = NSHostingView(rootView: ContentView().environmentObject(state))
32 |
33 | toolbar = NSToolbar(identifier: "reddit.toolbar")
34 | toolbar.allowsUserCustomization = true
35 | toolbar.delegate = self
36 | self.window.toolbar = toolbar
37 |
38 | window.makeKeyAndOrderFront(nil)
39 | }
40 |
41 | func applicationWillTerminate(_ aNotification: Notification) {
42 | // Insert code here to tear down your application
43 | }
44 |
45 | /// Subreddit search and sorting method
46 | func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
47 |
48 | let toolbarItem = NSToolbarItemGroup(itemIdentifier: itemIdentifier)
49 |
50 | let field = NSTextField(string: state.subreddit)
51 | field.placeholderString = "Jump to Subreddit"
52 | field.heightAnchor.constraint(equalToConstant: 22).isActive = true
53 | field.delegate = self
54 | let fieldItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: "subreddit.search.bar"))
55 | fieldItem.view = field
56 |
57 | let text = NSTextField(string: "r/")
58 | text.isBezeled = false
59 | text.backgroundColor = NSColor(hue: 1, saturation: 1, brightness: 1, alpha: 0)
60 | text.isEditable = false
61 | let labelItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: "r.label"))
62 | labelItem.view = text
63 |
64 | let segmentedControl = NSSegmentedControl(labels: SortBy.allCases.map { $0.rawValue.capitalized }, trackingMode: .selectOne, target: self, action: #selector(sortBy(_:)))
65 | segmentedControl.selectedSegment = 0
66 | let segmentedItem = NSToolbarItem()
67 | segmentedItem.view = segmentedControl
68 |
69 | toolbarItem.subitems = [labelItem, fieldItem, segmentedItem]
70 |
71 | return toolbarItem
72 | }
73 |
74 | @objc func sortBy(_ sender: NSSegmentedControl) {
75 | state.sortBy = SortBy.allCases[sender.selectedSegment]
76 | }
77 |
78 | func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
79 | if commandSelector == #selector(NSResponder.insertNewline(_:)) {
80 | state.subreddit = textView.string.replacingOccurrences(of: "r/", with: "").replacingOccurrences(of: " ", with: "")
81 | return true
82 | }
83 | return false
84 | }
85 |
86 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
87 | return [NSToolbarItem.Identifier(rawValue: "subreddit.search")]
88 | }
89 |
90 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
91 | return self.toolbarDefaultItemIdentifiers(toolbar)
92 | }
93 |
94 | func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
95 | return self.toolbarDefaultItemIdentifiers(toolbar)
96 | }
97 | }
98 |
99 |
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "icon_16@1x.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "icon_16@2x.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "icon_32@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "icon_32@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "icon_128@1x.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "icon_128@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "icon_256@1x.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "icon_256@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "icon_512@1x.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "icon_512@2x.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_128@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_128@1x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_16@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_16@1x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_256@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_256@1x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_32@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_32@1x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_512@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_512@1x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-macOS/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png
--------------------------------------------------------------------------------
/Reddit-macOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Reddit-macOS/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
--------------------------------------------------------------------------------
/Reddit-macOS/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Reddit-macOS
4 | //
5 | // Created by Carson Katri on 7/20/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct ContentView : View {
13 | @State private var sortBy: SortBy = .hot
14 |
15 | @State private var showSortSheet: Bool = false
16 | @State private var showSubredditSheet: Bool = false
17 |
18 | @State private var selectedPostId: String? = nil
19 |
20 | @EnvironmentObject private var state: ContentViewState
21 |
22 | var body: some View {
23 | NavigationView {
24 | /// Load the posts
25 | RequestView(Listing.self, Request {
26 | Url(API.subredditURL(state.subreddit, sortBy))
27 | Query(["raw_json":"1"])
28 | }) { listing in
29 | PostList(posts: listing?.posts ?? [], subreddit: self.state.subreddit, sortBy: self.state.sortBy, isLoading: false, selectedPostId: self.$selectedPostId)
30 | .frame(minWidth: 300)
31 | /// Spinner when loading
32 | PostList(posts: [], subreddit: self.state.subreddit, sortBy: self.state.sortBy, isLoading: true, selectedPostId: self.$selectedPostId)
33 | .frame(minWidth: 300)
34 | }
35 | Text("Select a post")
36 | .frame(maxWidth: .infinity, maxHeight: .infinity)
37 | }
38 | .touchBar {
39 | /*Picker("Sort By", selection: $state.sortBy) {
40 | ForEach(SortBy.allCases, id: \.rawValue) { sort in
41 | Text(sort.rawValue)
42 | }
43 | }*/
44 | Text("Hello, World!")
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Reddit-macOS/ContentViewState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentViewState.swift
3 | // Reddit-macOS
4 | //
5 | // Created by Carson Katri on 7/31/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Combine
11 |
12 | /// An `ObservableObject` to store information selected in the `NSToolbar`
13 | final class ContentViewState: ObservableObject {
14 | @Published var subreddit: String = "swift"
15 | @Published var sortBy: SortBy = .hot
16 | }
17 |
--------------------------------------------------------------------------------
/Reddit-macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2019 Carson Katri. All rights reserved.
27 | NSMainStoryboardFile
28 | Main
29 | NSPrincipalClass
30 | NSApplication
31 | NSSupportsAutomaticTermination
32 |
33 | NSSupportsSuddenTermination
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Reddit-macOS/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Reddit-macOS/Reddit_macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Reddit-macOS/Representable/SpinnerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpinnerView.swift
3 | // Reddit-macOS
4 | //
5 | // Created by Carson Katri on 7/31/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct SpinnerView : NSViewRepresentable {
12 | func makeNSView(context: NSViewRepresentableContext) -> NSProgressIndicator {
13 | NSProgressIndicator()
14 | }
15 |
16 | func updateNSView(_ nsView: NSProgressIndicator, context: NSViewRepresentableContext) {
17 | nsView.isIndeterminate = true
18 | nsView.style = .spinning
19 | nsView.startAnimation(self)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Reddit-macOS/Representable/WebView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebView.swift
3 | // Reddit-macOS
4 | //
5 | // Created by Carson Katri on 7/31/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import WebKit
11 |
12 | struct WebView : NSViewRepresentable {
13 | let url: URL
14 |
15 | func makeNSView(context: NSViewRepresentableContext) -> WKWebView {
16 | WKWebView()
17 | }
18 |
19 | func updateNSView(_ nsView: WKWebView, context: NSViewRepresentableContext) {
20 | nsView.load(URLRequest(url: url))
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Reddit-macOS/Views/Helpers/DetailWindowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailWindowController.swift
3 | // Reddit-macOS
4 | //
5 | // Created by Carson Katri on 7/31/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import SwiftUI
11 |
12 | /// A class to handle opening windows for posts when doubling clicking the entry
13 | class DetailWindowController: NSWindowController {
14 | convenience init(rootView: RootView) {
15 | let hostingController = NSHostingController(rootView: rootView.frame(width: 400, height: 500))
16 | let window = NSWindow(contentViewController: hostingController)
17 | window.setContentSize(NSSize(width: 400, height: 500))
18 | self.init(window: window)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Reddit-macOS/Views/PostList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostList.swift
3 | // Reddit-macOS
4 | //
5 | // Created by Carson Katri on 7/31/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct PostList: View {
13 | let posts: [Post]
14 | let subreddit: String
15 | let sortBy: SortBy
16 | let isLoading: Bool
17 |
18 | @Binding var selectedPostId: String?
19 |
20 | private var selectedPostIds: Binding> {
21 | Binding(get: {
22 | if let selectedPostId = self.selectedPostId {
23 | return Set(arrayLiteral: selectedPostId)
24 | }
25 | else {
26 | return Set()
27 | }
28 | }, set: {
29 | self.selectedPostId = $0.first
30 | })
31 | }
32 |
33 | private var selectedNavigationLink: Binding {
34 | Binding(get: {
35 | return self.selectedPostId
36 | }, set: { selectedPostId in
37 | // Absorbing any change that NavigationLink does to its selection property
38 | })
39 | }
40 |
41 | var body: some View {
42 | List(selection: selectedPostIds) {
43 | Section(header: Text("\(subreddit) | \(sortBy.rawValue)")) {
44 | /// List of `PostView`s when loaded
45 | if isLoading {
46 | SpinnerView()
47 | }
48 | else if posts.count > 0 {
49 | ForEach(posts) { post in
50 | NavigationLink(destination: PostDetailView(post: post), tag: post.id, selection: self.selectedNavigationLink) {
51 | PostView(post: post)
52 | }
53 | .tag(post.id)
54 | .padding(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
55 | .contentShape(Rectangle())
56 | /// Double-click to open a new window for the `PostDetailView`
57 | .onTapGesture(count: 2) {
58 | let detailView = PostDetailView(post: post)
59 |
60 | let controller = DetailWindowController(rootView: detailView)
61 | controller.window?.title = post.title
62 | controller.showWindow(nil)
63 | }
64 | /// Adding after the double tap so that double tap takes precedence
65 | .onTapGesture(count: 1) {
66 | self.selectedPostId = post.id
67 | }
68 | }
69 | }
70 | else {
71 | Text("No posts found")
72 | }
73 | }
74 | .collapsible(false)
75 | }
76 | .listStyle(SidebarListStyle())
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "24x24",
5 | "idiom" : "watch",
6 | "filename" : "Icon-24@2x.png",
7 | "scale" : "2x",
8 | "role" : "notificationCenter",
9 | "subtype" : "38mm"
10 | },
11 | {
12 | "size" : "27.5x27.5",
13 | "idiom" : "watch",
14 | "filename" : "Icon-27.5@2x.png",
15 | "scale" : "2x",
16 | "role" : "notificationCenter",
17 | "subtype" : "42mm"
18 | },
19 | {
20 | "size" : "29x29",
21 | "idiom" : "watch",
22 | "filename" : "Icon-29@2x.png",
23 | "role" : "companionSettings",
24 | "scale" : "2x"
25 | },
26 | {
27 | "size" : "29x29",
28 | "idiom" : "watch",
29 | "filename" : "Icon-29@3x.png",
30 | "role" : "companionSettings",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "watch",
36 | "filename" : "Icon-40@2x.png",
37 | "scale" : "2x",
38 | "role" : "appLauncher",
39 | "subtype" : "38mm"
40 | },
41 | {
42 | "size" : "44x44",
43 | "idiom" : "watch",
44 | "scale" : "2x",
45 | "role" : "appLauncher",
46 | "subtype" : "40mm"
47 | },
48 | {
49 | "size" : "50x50",
50 | "idiom" : "watch",
51 | "scale" : "2x",
52 | "role" : "appLauncher",
53 | "subtype" : "44mm"
54 | },
55 | {
56 | "size" : "86x86",
57 | "idiom" : "watch",
58 | "filename" : "Icon-86@2x.png",
59 | "scale" : "2x",
60 | "role" : "quickLook",
61 | "subtype" : "38mm"
62 | },
63 | {
64 | "size" : "98x98",
65 | "idiom" : "watch",
66 | "filename" : "Icon-98@2x.png",
67 | "scale" : "2x",
68 | "role" : "quickLook",
69 | "subtype" : "42mm"
70 | },
71 | {
72 | "size" : "108x108",
73 | "idiom" : "watch",
74 | "scale" : "2x",
75 | "role" : "quickLook",
76 | "subtype" : "44mm"
77 | },
78 | {
79 | "size" : "1024x1024",
80 | "idiom" : "watch-marketing",
81 | "filename" : "ItunesArtwork@2x.png",
82 | "scale" : "1x"
83 | }
84 | ],
85 | "info" : {
86 | "version" : 1,
87 | "author" : "xcode"
88 | }
89 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-24@2x.png
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-27.5@2x.png
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-86@2x.png
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/Icon-98@2x.png
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Reddit-watchOS WatchKit App/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Base.lproj/Interface.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit App/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Reddit
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | UISupportedInterfaceOrientations
24 |
25 | UIInterfaceOrientationPortrait
26 | UIInterfaceOrientationPortraitUpsideDown
27 |
28 | WKWatchKitApp
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets" : [
3 | {
4 | "idiom" : "watch",
5 | "filename" : "Circular.imageset",
6 | "role" : "circular"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "filename" : "Extra Large.imageset",
11 | "role" : "extra-large"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "filename" : "Graphic Bezel.imageset",
16 | "role" : "graphic-bezel"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "filename" : "Graphic Circular.imageset",
21 | "role" : "graphic-circular"
22 | },
23 | {
24 | "idiom" : "watch",
25 | "filename" : "Graphic Corner.imageset",
26 | "role" : "graphic-corner"
27 | },
28 | {
29 | "idiom" : "watch",
30 | "filename" : "Graphic Large Rectangular.imageset",
31 | "role" : "graphic-large-rectangular"
32 | },
33 | {
34 | "idiom" : "watch",
35 | "filename" : "Modular.imageset",
36 | "role" : "modular"
37 | },
38 | {
39 | "idiom" : "watch",
40 | "filename" : "Utilitarian.imageset",
41 | "role" : "utilitarian"
42 | }
43 | ],
44 | "info" : {
45 | "version" : 1,
46 | "author" : "xcode"
47 | }
48 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "watch",
5 | "scale" : "2x",
6 | "screen-width" : "<=145"
7 | },
8 | {
9 | "idiom" : "watch",
10 | "scale" : "2x",
11 | "screen-width" : ">161"
12 | },
13 | {
14 | "idiom" : "watch",
15 | "scale" : "2x",
16 | "screen-width" : ">145"
17 | },
18 | {
19 | "idiom" : "watch",
20 | "scale" : "2x",
21 | "screen-width" : ">183"
22 | }
23 | ],
24 | "info" : {
25 | "version" : 1,
26 | "author" : "xcode"
27 | }
28 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct ContentView: View {
13 | @State private var subreddit: String = "swift"
14 | @State private var sortBy: SortBy = .hot
15 |
16 | @State private var showSettings: Bool = false
17 |
18 | var body: some View {
19 | /// Load the `Post`s
20 | RequestView(Listing.self, Request {
21 | Url(API.subredditURL(subreddit, sortBy))
22 | Query(["raw_json":"1"])
23 | }) { listing in
24 | /// List of `PostView`s when loaded
25 | List {
26 | /// Settings `Button`
27 | Button(action: {
28 | self.showSettings.toggle()
29 | }) {
30 | HStack {
31 | Image(systemName: "gear")
32 | Text("r/\(self.subreddit) | \(self.sortBy.rawValue)")
33 | }
34 | }
35 | /// Post items
36 | ForEach(listing != nil ? listing!.data.children.map { $0.data } : [], id: \.id) { post in
37 | NavigationLink(destination: PostDetailView(post: post)) {
38 | PostView(post: post)
39 | }
40 | }
41 | }
42 | /// Spinner when loading
43 | SpinnerView()
44 | }
45 | .navigationBarTitle("r/\(self.subreddit) | \(self.sortBy.rawValue)")
46 | .sheet(isPresented: $showSettings) {
47 | SettingsView(showSettings: self.$showSettings, subreddit: self.$subreddit, sortBy: self.$sortBy)
48 | }
49 | }
50 | }
51 |
52 | #if DEBUG
53 | struct ContentView_Previews: PreviewProvider {
54 | static var previews: some View {
55 | List([0, 1, 2], id: \.self) { item in
56 | PostView(post: Post(title: "Hello World", name: "hello-world", id: "hw", selftext: "This is some body content. Blah blah\nblah blah blah", selftext_html: nil, thumbnail: "blahblah", url: "", author: "me", subreddit: "swift", score: 1000, num_comments: 50, stickied: true, created_utc: Date().timeIntervalSince1970, preview: nil, link_flair_text: "Hello World", is_original_content: true, spoiler: false, replies: nil))
57 | }
58 | .listStyle(CarouselListStyle())
59 | }
60 | }
61 | #endif
62 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/ExtensionDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExtensionDelegate.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import WatchKit
10 |
11 | class ExtensionDelegate: NSObject, WKExtensionDelegate {
12 |
13 | func applicationDidFinishLaunching() {
14 | // Perform any final initialization of your application.
15 | }
16 |
17 | func applicationDidBecomeActive() {
18 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
19 | }
20 |
21 | func applicationWillResignActive() {
22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
23 | // Use this method to pause ongoing tasks, disable timers, etc.
24 | }
25 |
26 | func handle(_ backgroundTasks: Set) {
27 | // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
28 | for task in backgroundTasks {
29 | // Use a switch statement to check the task type
30 | switch task {
31 | case let backgroundTask as WKApplicationRefreshBackgroundTask:
32 | // Be sure to complete the background task once you’re done.
33 | backgroundTask.setTaskCompletedWithSnapshot(false)
34 | case let snapshotTask as WKSnapshotRefreshBackgroundTask:
35 | // Snapshot tasks have a unique completion call, make sure to set your expiration date
36 | snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
37 | case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
38 | // Be sure to complete the connectivity task once you’re done.
39 | connectivityTask.setTaskCompletedWithSnapshot(false)
40 | case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
41 | // Be sure to complete the URL session task once you’re done.
42 | urlSessionTask.setTaskCompletedWithSnapshot(false)
43 | case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
44 | // Be sure to complete the relevant-shortcut task once you're done.
45 | relevantShortcutTask.setTaskCompletedWithSnapshot(false)
46 | case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
47 | // Be sure to complete the intent-did-run task once you're done.
48 | intentDidRunTask.setTaskCompletedWithSnapshot(false)
49 | default:
50 | // make sure to complete unhandled task types
51 | task.setTaskCompletedWithSnapshot(false)
52 | }
53 | }
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/HostingController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HostingController.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import WatchKit
10 | import Foundation
11 | import SwiftUI
12 |
13 | class HostingController: WKHostingController {
14 | override var body: ContentView {
15 | return ContentView()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 | NSAllowsArbitraryLoads
8 |
9 |
10 | CFBundleDevelopmentRegion
11 | $(DEVELOPMENT_LANGUAGE)
12 | CFBundleDisplayName
13 | Reddit
14 | CFBundleExecutable
15 | $(EXECUTABLE_NAME)
16 | CFBundleIdentifier
17 | $(PRODUCT_BUNDLE_IDENTIFIER)
18 | CFBundleInfoDictionaryVersion
19 | 6.0
20 | CFBundleName
21 | $(PRODUCT_NAME)
22 | CFBundlePackageType
23 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
24 | CFBundleShortVersionString
25 | 1.0
26 | CFBundleVersion
27 | 1
28 | NSExtension
29 |
30 | NSExtensionAttributes
31 |
32 | WKAppBundleIdentifier
33 | com.carsonkatri.Reddit-watchOS.watchkitapp
34 |
35 | NSExtensionPointIdentifier
36 | com.apple.watchkit
37 |
38 | WKExtensionDelegateClassName
39 | $(PRODUCT_MODULE_NAME).ExtensionDelegate
40 | WKWatchOnly
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/NotificationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationController.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import WatchKit
10 | import SwiftUI
11 | import UserNotifications
12 |
13 | class NotificationController: WKUserNotificationHostingController {
14 |
15 | override var body: NotificationView {
16 | return NotificationView()
17 | }
18 |
19 | override func willActivate() {
20 | // This method is called when watch view controller is about to be visible to user
21 | super.willActivate()
22 | }
23 |
24 | override func didDeactivate() {
25 | // This method is called when watch view controller is no longer visible
26 | super.didDeactivate()
27 | }
28 |
29 | override func didReceive(_ notification: UNNotification) {
30 | // This method is called when a notification needs to be presented.
31 | // Implement it if you use a dynamic notification interface.
32 | // Populate your dynamic notification interface as quickly as possible.
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/NotificationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct NotificationView: View {
12 | var body: some View {
13 | Text("Hello World")
14 | }
15 | }
16 |
17 | #if DEBUG
18 | struct NotificationView_Previews: PreviewProvider {
19 | static var previews: some View {
20 | NotificationView()
21 | }
22 | }
23 | #endif
24 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/PushNotificationPayload.apns:
--------------------------------------------------------------------------------
1 | {
2 | "aps": {
3 | "alert": {
4 | "body": "Test message",
5 | "title": "Optional title",
6 | "subtitle": "Optional subtitle"
7 | },
8 | "category": "myCategory",
9 | "thread-id": "5280"
10 | },
11 |
12 | "WatchKit Simulator Actions": [
13 | {
14 | "title": "First Button",
15 | "identifier": "firstButtonAction"
16 | }
17 | ],
18 |
19 | "customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App."
20 | }
21 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Representable/SpinnerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpinnerView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct SpinnerView: View {
12 |
13 | var body: some View {
14 | Text("Loading...")
15 | }
16 | }
17 |
18 | #if DEBUG
19 | struct SpinnerView_Previews: PreviewProvider {
20 | static var previews: some View {
21 | SpinnerView()
22 | }
23 | }
24 | #endif
25 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Representable/WebView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct WebView: View {
12 | let url: URL
13 |
14 | var body: some View {
15 | Text("")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Views/CommentsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentsView.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct CommentsView: View {
13 | let post: Post
14 |
15 | var noComments: some View {
16 | Text("😞 No comments...")
17 | .frame(height: nil)
18 | }
19 |
20 | func commentView(_ comment: Comment) -> some View {
21 | VStack(alignment: .leading) {
22 | Text(comment.author)
23 | .foregroundColor(.gray)
24 | .font(.system(size: 10))
25 | Text(comment.body ?? "")
26 | }
27 | }
28 |
29 | var body: some View {
30 | // Load the comments
31 | RequestView([CommentListing].self, Request {
32 | Url(API.postURL(post.subreddit, post.id))
33 | Header.Accept(.json)
34 | }) { listings in
35 | if listings != nil {
36 | // `dropFirst` because `first` is the actual post
37 | if listings!.dropFirst().map({ $0.data.children }).flatMap({ $0.map { $0.data } }).count > 0 {
38 | ForEach(listings!.dropFirst().map({ $0.data.children }).flatMap { $0.map { $0.data } }, id: \.id) { comment in
39 | self.commentView(comment)
40 | }
41 | } else {
42 | self.noComments
43 | }
44 | } else {
45 | self.noComments
46 | }
47 | SpinnerView()
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Views/Compatibility/SpinnerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpinnerView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct SpinnerView: View {
12 |
13 | var body: some View {
14 | Text("Loading...")
15 | }
16 | }
17 |
18 | #if DEBUG
19 | struct SpinnerView_Previews: PreviewProvider {
20 | static var previews: some View {
21 | SpinnerView()
22 | }
23 | }
24 | #endif
25 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Views/Compatibility/WebView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WebView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct WebView: View {
12 | let url: URL
13 |
14 | var body: some View {
15 | Text("")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Views/PostDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostDetailView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct PostDetailView: View {
13 | let post: Post
14 |
15 | var body: some View {
16 | List {
17 | if post.url.contains(".jpg") || post.url.contains(".png") {
18 | RequestImage(Url(post.url))
19 | }
20 |
21 | Text(post.title)
22 | .font(.system(size: 21))
23 | .bold()
24 | Text(post.selftext)
25 |
26 | MetadataView(post: post, spaced: true)
27 | .font(.system(size: 10))
28 | .foregroundColor(.white)
29 | .lineLimit(1)
30 |
31 | CommentsView(post: post)
32 | }
33 | .navigationBarTitle("r/\(post.subreddit)")
34 | }
35 | }
36 |
37 | #if DEBUG
38 | struct PostDetailView_Previews: PreviewProvider {
39 | static var previews: some View {
40 | PostDetailView(post: Post(title: "Hello World | This is secondary text", name: "hello-world", id: "hw", selftext: "This is some body content. Blah blah\nblah blah blah", selftext_html: nil, thumbnail: "blahblah", url: "", author: "me", subreddit: "swift", score: 1000, num_comments: 50, stickied: true, created_utc: Date().timeIntervalSince1970, preview: nil, link_flair_text: "Hello World", is_original_content: true, spoiler: false, replies: nil))
41 | }
42 | }
43 | #endif
44 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Views/PostView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct PostView: View {
13 | let post: Post
14 | var body: some View {
15 | HStack {
16 | if post.thumbnail != "self" {
17 | RequestImage(Url(post.thumbnail))
18 | .frame(width: 32, height: 32)
19 | .cornerRadius(5)
20 | }
21 | VStack(alignment: .leading, spacing: 0) {
22 | HStack(spacing: 2) {
23 | if post.stickied {
24 | Image(systemName: "pin.fill")
25 | .resizable()
26 | .foregroundColor(.green)
27 | .frame(width: 10, height: 10)
28 | .rotationEffect(Angle(degrees: 45))
29 | }
30 | Text(post.title)
31 | .lineLimit(1)
32 | }
33 | Text(post.selftext)
34 | .foregroundColor(.gray)
35 | .lineLimit(1)
36 | }
37 | }
38 | .padding([.top, .bottom])
39 | }
40 | }
41 |
42 | #if DEBUG
43 | struct PostView_Previews: PreviewProvider {
44 | static var previews: some View {
45 | PostView(post: Post(title: "Hello World | This is secondary text", name: "hello-world", id: "hw", selftext: "This is some body content. Blah blah\nblah blah blah", selftext_html: nil, thumbnail: "blahblah", url: "", author: "me", subreddit: "swift", score: 1000, num_comments: 50, stickied: true, created_utc: Date().timeIntervalSince1970, preview: nil, link_flair_text: "Hello World", is_original_content: true, spoiler: false, replies: nil))
46 | }
47 | }
48 | #endif
49 |
--------------------------------------------------------------------------------
/Reddit-watchOS WatchKit Extension/Views/SettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 8/8/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct SettingsView: View {
12 | @Binding var showSettings: Bool
13 | @Binding var subreddit: String
14 | @Binding var sortBy: SortBy
15 |
16 | var body: some View {
17 | ScrollView {
18 | HStack {
19 | Text("r/")
20 | TextField("Subreddit", text: self.$subreddit)
21 | }
22 | Picker(selection: $sortBy, label: Text("Sort By")) {
23 | ForEach(SortBy.allCases, id: \.rawValue) { sort in
24 | Text(sort.rawValue).tag(sort)
25 | }
26 | }
27 | .frame(height: 100)
28 | Button(action: {
29 | self.showSettings.toggle()
30 | }) { Text("Done") }
31 | .background(Color.white.opacity(0.25))
32 | .cornerRadius(4)
33 | }
34 | }
35 | }
36 |
37 | #if DEBUG
38 | struct SettingsView_Previews: PreviewProvider {
39 | static var previews: some View {
40 | SettingsView(showSettings: .constant(true), subreddit: .constant("swift"), sortBy: .constant(.hot))
41 | }
42 | }
43 | #endif
44 |
--------------------------------------------------------------------------------
/Reddit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Reddit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Reddit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Request",
6 | "repositoryURL": "https://github.com/carson-katri/swift-request",
7 | "state": {
8 | "branch": null,
9 | "revision": "5de85a849f22e56471af6cb0367bd575519b131f",
10 | "version": "1.1.1"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Reddit.xcodeproj/xcshareddata/xcschemes/Reddit-watchOS WatchKit App (Notification).xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
59 |
63 |
69 |
70 |
71 |
72 |
80 |
84 |
90 |
91 |
92 |
93 |
99 |
100 |
101 |
102 |
104 |
105 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/Reddit.xcodeproj/xcshareddata/xcschemes/Reddit-watchOS WatchKit App.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
58 |
62 |
68 |
69 |
70 |
71 |
77 |
81 |
87 |
88 |
89 |
90 |
96 |
97 |
98 |
99 |
101 |
102 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/Resources/banner.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carson-katri/reddit-swiftui/cc71ae8b677c835d098ec0e0dfa293ec5e76b55c/Resources/banner.jpeg
--------------------------------------------------------------------------------
/Shared/API.swift:
--------------------------------------------------------------------------------
1 | //
2 | // API.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 8/1/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct API {
12 | static func subredditURL(_ subreddit: String, _ sortBy: SortBy) -> String {
13 | return "https://www.reddit.com/r/\(subreddit)/\(sortBy.rawValue).json"
14 | }
15 |
16 | static func postURL(_ subreddit: String, _ id: String) -> String {
17 | return "https://www.reddit.com/r/\(subreddit)/\(id).json"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Shared/Helpers/Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Helpers.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/27/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | /// `RelativeDateTimeFormatter` convenience function
13 | func timeSince(_ interval: TimeInterval) -> String {
14 | let formatter = RelativeDateTimeFormatter()
15 | return formatter.localizedString(for: Date(timeIntervalSince1970: interval), relativeTo: Date())
16 | }
17 |
18 | /// `SwiftUI` compatibility
19 | #if os(macOS)
20 | extension Image {
21 | init(systemName: String) {
22 | self.init(nsImage: NSImage())
23 | }
24 | }
25 | #endif
26 |
--------------------------------------------------------------------------------
/Shared/Models/Comment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comment.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/27/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// A comment from the Reddit API
12 | struct Comment: Decodable {
13 | let id: String
14 | let author: String
15 | let score: Int
16 | let body: String?
17 | let replies: CommentListing?
18 |
19 | enum CommentKeys: String, CodingKey {
20 | case id
21 | case author
22 | case score
23 | case body
24 | case replies
25 | }
26 |
27 | init(from decoder: Decoder) throws {
28 | let values = try decoder.container(keyedBy: CommentKeys.self)
29 | id = try values.decode(String.self, forKey: .id)
30 | author = try values.decode(String.self, forKey: .author)
31 | score = try values.decode(Int.self, forKey: .score)
32 | body = try? values.decode(String.self, forKey: .body)
33 |
34 | if let replies = try? values.decode(CommentListing.self, forKey: .replies) {
35 | self.replies = replies
36 | } else {
37 | replies = nil
38 | }
39 | }
40 | }
41 |
42 | #if DEBUG
43 | extension Comment {
44 | /// Used to initialize a Comment for Debug purposes
45 | init(nested: Int) {
46 | id = "123"
47 | author = "sirarkimedes"
48 | score = 123556
49 | body = "This is a body of text that is purely to act as an example!"
50 | if nested != 0 {
51 | replies = CommentListing(data: CommentListing.CommentListingData(children: [CommentListing.CommentListingData.CommentData(data: Comment(nested: nested - 1))]))
52 | } else{
53 | replies = nil
54 | }
55 | }
56 | }
57 | #endif
58 |
--------------------------------------------------------------------------------
/Shared/Models/Listing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Listing.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/21/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Root of Reddit API response
12 | struct Listing: Decodable {
13 | let data: ListingData
14 | var posts: [Post] {
15 | return data.children.map { (postData) -> Post in
16 | postData.data
17 | }
18 | }
19 |
20 | struct ListingData: Decodable {
21 | let children: [PostData]
22 |
23 | struct PostData: Decodable {
24 | let data: Post
25 | }
26 | }
27 | }
28 |
29 | /// Root of Reddit API response for comments
30 | struct CommentListing: Decodable {
31 | let data: CommentListingData
32 |
33 | struct CommentListingData: Decodable {
34 | let children: [CommentData]
35 |
36 | struct CommentData: Decodable {
37 | let data: Comment
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Shared/Models/Post.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Post.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/21/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// A post from the Reddit API
12 | struct Post: Decodable, Identifiable {
13 | let title: String
14 | let name: String
15 | let id: String
16 | /// The body of the post
17 | let selftext: String
18 | let selftext_html: String?
19 | let thumbnail: String
20 | let url: String
21 | let author: String
22 | let subreddit: String
23 | let score: Int
24 | let num_comments: Int
25 | let stickied: Bool
26 | let created_utc: Double
27 | let preview: Preview?
28 |
29 | let link_flair_text: String?
30 | let is_original_content: Bool
31 | let spoiler: Bool
32 |
33 | var flairs: [String] {
34 | var res: [String] = []
35 | if link_flair_text != nil {
36 | res.append(link_flair_text!)
37 | }
38 | if is_original_content {
39 | res.append("OC")
40 | }
41 | if spoiler {
42 | res.append("Spoiler")
43 | }
44 | return res
45 | }
46 |
47 | let replies: [Self]?
48 |
49 | struct Preview: Decodable {
50 | let images: [PreviewImage]
51 | let enabled: Bool
52 |
53 | struct PreviewImage: Decodable {
54 | let source: ImageSource
55 | let resolutions: [ImageSource]
56 | let id: String
57 |
58 | struct ImageSource: Decodable {
59 | let url: String
60 | let width: Int
61 | let height: Int
62 | }
63 | }
64 | }
65 | }
66 |
67 | #if DEBUG
68 | extension Post {
69 | /// Used to create a Post for example Debug purposes
70 | static var example: Self {
71 | return Post(title: "Hello World | This is secondary text", name: "hello-world", id: "hw", selftext: "This is some body content. Blah blah\nblah blah blah", selftext_html: nil, thumbnail: "blahblah", url: "", author: "me", subreddit: "swift", score: 1000, num_comments: 50, stickied: true, created_utc: Date().timeIntervalSince1970 - 100, preview: nil, link_flair_text: "Hello World", is_original_content: true, spoiler: false, replies: nil)
72 | }
73 | }
74 | #endif
75 |
--------------------------------------------------------------------------------
/Shared/Models/SortBy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SortBy.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/21/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Sorting method for Reddit API
12 | enum SortBy: String, CaseIterable {
13 | case hot
14 | case new
15 | case controversial
16 | case top
17 | case rising
18 | }
19 |
--------------------------------------------------------------------------------
/Shared/SharedAssets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Shared/SharedAssets.xcassets/popover.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.900",
13 | "alpha" : "1.000",
14 | "blue" : "0.900",
15 | "green" : "0.900"
16 | }
17 | }
18 | },
19 | {
20 | "idiom" : "universal",
21 | "appearances" : [
22 | {
23 | "appearance" : "luminosity",
24 | "value" : "light"
25 | }
26 | ],
27 | "color" : {
28 | "color-space" : "srgb",
29 | "components" : {
30 | "red" : "0.900",
31 | "alpha" : "1.000",
32 | "blue" : "0.900",
33 | "green" : "0.900"
34 | }
35 | }
36 | },
37 | {
38 | "idiom" : "universal",
39 | "appearances" : [
40 | {
41 | "appearance" : "luminosity",
42 | "value" : "dark"
43 | }
44 | ],
45 | "color" : {
46 | "color-space" : "srgb",
47 | "components" : {
48 | "red" : "0.200",
49 | "alpha" : "1.000",
50 | "blue" : "0.200",
51 | "green" : "0.200"
52 | }
53 | }
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/Shared/SharedAssets.xcassets/stickied.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.000",
13 | "alpha" : "1.000",
14 | "blue" : "0.000",
15 | "green" : "0.680"
16 | }
17 | }
18 | },
19 | {
20 | "idiom" : "universal",
21 | "appearances" : [
22 | {
23 | "appearance" : "luminosity",
24 | "value" : "light"
25 | }
26 | ],
27 | "color" : {
28 | "color-space" : "srgb",
29 | "components" : {
30 | "red" : "0.000",
31 | "alpha" : "1.000",
32 | "blue" : "0.000",
33 | "green" : "0.680"
34 | }
35 | }
36 | },
37 | {
38 | "idiom" : "universal",
39 | "appearances" : [
40 | {
41 | "appearance" : "luminosity",
42 | "value" : "dark"
43 | }
44 | ],
45 | "color" : {
46 | "color-space" : "srgb",
47 | "components" : {
48 | "red" : "0.000",
49 | "alpha" : "1.000",
50 | "blue" : "0.000",
51 | "green" : "1.000"
52 | }
53 | }
54 | }
55 | ]
56 | }
--------------------------------------------------------------------------------
/Shared/Views/CommentsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentsView.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/27/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | // MARK: - CommentsView
13 |
14 | struct CommentsView: View {
15 | let post: Post
16 |
17 | var noComments: some View {
18 | Text("😞 No comments...")
19 | .frame(height: nil)
20 | }
21 |
22 | var body: some View {
23 | // Load the comments
24 | RequestView([CommentListing].self, Request {
25 | Url(API.postURL(post.subreddit, post.id))
26 | Header.Accept(.json)
27 | }) { listings in
28 | if listings != nil {
29 | // `dropFirst` because `first` is the actual post
30 | if listings!.dropFirst().map({ $0.data.children }).flatMap({ $0.map { $0.data } }).count > 0 {
31 | ForEach(listings!.dropFirst().map({ $0.data.children }).flatMap { $0.map { $0.data } }, id: \.id) { comment in
32 | CommentView(comment: comment, postAuthor: self.post.author, nestLevel: 0)
33 | }
34 | } else {
35 | self.noComments
36 | }
37 | } else {
38 | self.noComments
39 | }
40 | SpinnerView()
41 | }
42 | }
43 | }
44 |
45 | // MARK: - CommentView
46 |
47 | struct CommentView: View {
48 | let comment: Comment
49 | let postAuthor: String
50 | let nestLevel: Int
51 |
52 | var authorText: some View {
53 | if comment.author == postAuthor {
54 | return Text(comment.author).foregroundColor(.accentColor).bold()
55 | } else {
56 | return Text(comment.author)
57 | }
58 | }
59 |
60 | var body: some View {
61 | Group {
62 | HStack {
63 | /// Left border for nested comments
64 | if nestLevel > 0 {
65 | RoundedRectangle(cornerRadius: 1.5)
66 | .foregroundColor(Color(hue: 1.0 / Double(nestLevel), saturation: 1.0, brightness: 1.0))
67 | .frame(width: 3)
68 | }
69 | /// Content
70 | VStack(alignment: .leading) {
71 | HStack {
72 | authorText
73 | Image(systemName: "arrow.up")
74 | Text("\(comment.score)")
75 | }
76 | .font(.caption)
77 | .opacity(0.75)
78 | Text(comment.body ?? "")
79 | }
80 | }
81 | .padding(.leading, CGFloat(self.nestLevel * 10))
82 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
83 | /// Recursive comments
84 | if comment.replies != nil {
85 | ForEach(comment.replies!.data.children.map { $0.data }, id: \.id) { reply in
86 | CommentView(comment: reply, postAuthor: self.postAuthor, nestLevel: self.nestLevel + 1)
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | // MARK: - Comment View preview
94 |
95 | #if DEBUG
96 | struct CommentView_Previews: PreviewProvider {
97 | static func example(for author: String, nested: Int = 5) -> some View {
98 | CommentView(comment: Comment(nested: nested), postAuthor: author, nestLevel: 0).frame(width: nil, height: 60)
99 | }
100 |
101 | static var previews: some View {
102 | VStack {
103 | example(for: "not", nested: 2)
104 | example(for: "sirarkimedes")
105 | }
106 | }
107 | }
108 | #endif
109 |
--------------------------------------------------------------------------------
/Shared/Views/FlairView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TagView.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 8/9/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct FlairView: View {
12 | let flairs: [String]
13 |
14 | func flair(_ name: String) -> some View {
15 | Text(name)
16 | .font(.caption)
17 | .foregroundColor(.primary)
18 | .padding(5)
19 | .background(Color.secondary.opacity(0.5))
20 | .cornerRadius(4)
21 | }
22 |
23 | var body: some View {
24 | HStack {
25 | ForEach(flairs, id: \.self) {
26 | self.flair($0)
27 | }
28 | }
29 | }
30 | }
31 |
32 | #if DEBUG
33 | struct FlairView_Previews: PreviewProvider {
34 | static var previews: some View {
35 | FlairView(flairs: ["Hello", "World"])
36 | }
37 | }
38 | #endif
39 |
--------------------------------------------------------------------------------
/Shared/Views/MetadataView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MetadataView.swift
3 | // Reddit-watchOS WatchKit Extension
4 | //
5 | // Created by Carson Katri on 7/29/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct MetadataView: View {
12 | let post: Post
13 | let spaced: Bool
14 |
15 | var stickied: some View {
16 | Group {
17 | /// Pinned icon
18 | if post.stickied {
19 | Image(systemName: "pin.fill")
20 | .rotationEffect(Angle(degrees: 45))
21 | .foregroundColor(Color("stickied"))
22 | }
23 | if spaced {
24 | Spacer()
25 | }
26 | }
27 | }
28 |
29 | var body: some View {
30 | /// Spacers are placed to fill the width of the screen if desired
31 | HStack {
32 | if spaced {
33 | Spacer()
34 | }
35 | stickied
36 | /// Tuples store the SF Symbols, text, and color
37 | ForEach([("arrow.up", "\(post.score)", Color.orange), ("text.bubble", "\(post.num_comments)", Color.primary), ("clock", "\(timeSince(post.created_utc))", Color.primary)], id: \.0) { data in
38 | Group {
39 | Image(systemName: data.0)
40 | Text(data.1)
41 | if self.spaced {
42 | Spacer()
43 | }
44 | }
45 | .foregroundColor(data.2)
46 | }
47 | }
48 | }
49 | }
50 |
51 | #if DEBUG
52 | struct MetadataView_Previews: PreviewProvider {
53 | static var previews: some View {
54 | MetadataView(post: Post.example, spaced: true)
55 | }
56 | }
57 | #endif
58 |
--------------------------------------------------------------------------------
/Shared/Views/PostDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostDetailView.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/22/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct PostDetailView: View {
13 | let post: Post
14 |
15 | var title: some View {
16 | let vstack = VStack(alignment: .leading) {
17 | Text(post.author)
18 | .font(.caption)
19 | .opacity(0.75)
20 | Text(post.title)
21 | .font(.title)
22 | .bold()
23 | }
24 | #if os(iOS)
25 | return vstack
26 | #elseif os(macOS)
27 | /// Fill window width
28 | return vstack.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
29 | #endif
30 | }
31 |
32 | var body: some View {
33 | let list = List {
34 | // Image
35 | if post.url.contains(".jpg") || post.url.contains(".png") {
36 | RequestImage(Url(post.url), contentMode: .fit)
37 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
38 | }
39 | // GIF
40 | if post.url.contains(".gif") {
41 | WebView(url: URL(string: post.url)!)
42 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
43 | }
44 | // Title
45 | if post.selftext == "" {
46 | NavigationLink(destination: WebView(url: URL(string: post.url)!)) {
47 | title
48 | }
49 | } else {
50 | title
51 | }
52 | // Body
53 | if post.selftext != "" {
54 | Text(post.selftext)
55 | }
56 | VStack{
57 | ScrollView(.horizontal) {
58 | HStack{
59 | if post.flairs.count > 0 {
60 | FlairView(flairs: post.flairs)
61 | }
62 | MetadataView(post: post, spaced: true)
63 | }
64 | .padding(.bottom, 10)
65 | }
66 | }
67 | CommentsView(post: post)
68 | }
69 | #if os(iOS)
70 | return list.navigationBarTitle(Text("r/\(post.subreddit)"), displayMode: .inline)
71 | #else
72 | return list
73 | #endif
74 | }
75 | }
76 |
77 | #if DEBUG
78 | struct PostDetailView_Previews: PreviewProvider {
79 | static var previews: some View {
80 | PostDetailView(post: Post.example)
81 | }
82 | }
83 | #endif
84 |
--------------------------------------------------------------------------------
/Shared/Views/PostView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostView.swift
3 | // Reddit
4 | //
5 | // Created by Carson Katri on 7/21/19.
6 | // Copyright © 2019 Carson Katri. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import Request
11 |
12 | struct PostView: View {
13 | let post: Post
14 |
15 | var body: some View {
16 | HStack {
17 | VStack(alignment: .leading) {
18 | #if os(iOS)
19 | Text(post.title)
20 | .font(.headline)
21 | .lineLimit(1)
22 | #elseif os(macOS)
23 | Text(post.title)
24 | .bold()
25 | #endif
26 | /// Body preview
27 | Group {
28 | if post.url.contains("reddit") {
29 | Text(post.selftext != "" ? post.selftext : " ")
30 | } else {
31 | Text(post.url)
32 | }
33 | }
34 | .font(.caption)
35 | .opacity(0.75)
36 | .lineLimit(1)
37 | /// Metadata for the post
38 | MetadataView(post: post, spaced: false)
39 | .font(.caption)
40 | .opacity(0.75)
41 | }
42 | if post.thumbnail != "self" {
43 | Spacer()
44 | RequestImage(Url(post.thumbnail))
45 | .aspectRatio(contentMode: .fill)
46 | .frame(width: 50, height: 50, alignment: .center)
47 | .clipped()
48 | .cornerRadius(5.0)
49 | }
50 | }
51 | }
52 | }
53 |
54 | #if DEBUG
55 | struct PostView_Previews: PreviewProvider {
56 | static var previews: some View {
57 | PostView(post: Post.example)
58 | }
59 | }
60 | #endif
61 |
--------------------------------------------------------------------------------