├── .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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | Default 529 | 530 | 531 | 532 | 533 | 534 | 535 | Left to Right 536 | 537 | 538 | 539 | 540 | 541 | 542 | Right to Left 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | Default 554 | 555 | 556 | 557 | 558 | 559 | 560 | Left to Right 561 | 562 | 563 | 564 | 565 | 566 | 567 | Right to Left 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 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 | 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 | --------------------------------------------------------------------------------