├── FeedRead ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon_16.png │ │ ├── icon_32.png │ │ ├── icon_128.png │ │ ├── icon_256.png │ │ ├── icon_512.png │ │ ├── icon_128@2x.png │ │ ├── icon_16@2x.png │ │ ├── icon_256@2x.png │ │ ├── icon_32@2x.png │ │ ├── icon_512@2x.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── FeedRead.entitlements ├── Views │ ├── ContentView.swift │ ├── ArticleView.swift │ ├── WebView.swift │ ├── AddFeedView.swift │ ├── SettingsView.swift │ ├── ArticleListView.swift │ └── FeedListView.swift ├── Models │ ├── Links.swift │ ├── Feed.swift │ ├── FeedArticle.swift │ ├── FeedReader.swift │ └── FeedList.swift ├── Info.plist └── FeedReadApp.swift ├── FeedRead.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcuserdata │ └── sarah.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ └── xcschemes │ │ └── FeedRead.xcscheme └── project.pbxproj └── README.md /FeedRead/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /FeedRead/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_16.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_32.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_128.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_256.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_512.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trozware/feedread/HEAD/FeedRead/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png -------------------------------------------------------------------------------- /FeedRead.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FeedRead.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FeedRead 2 | 3 | Sample app built during in talk recorded for [Back to the Mac](https://backtomac.org), November 2020. 4 | 5 | --- 6 | 7 | ### Topics covered in the talk: 8 | 9 | - multiple panes 10 | - toolbar 11 | - dialogs 12 | - menus 13 | 14 | --- 15 | 16 | ### Added later due to time constraints: 17 | 18 | - preferences window 19 | - app settings 20 | - tab view 21 | 22 | -------------------------------------------------------------------------------- /FeedRead/FeedRead.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /FeedRead.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "FeedKit", 6 | "repositoryURL": "https://github.com/nmdias/FeedKit", 7 | "state": { 8 | "branch": null, 9 | "revision": "68493a33d862c33c9a9f67ec729b3b7df1b20ade", 10 | "version": "9.1.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /FeedRead.xcodeproj/xcuserdata/sarah.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | FeedRead.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 2697DBDD2543B0D200020318 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /FeedRead/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | // ContentView.swift - FeedRead 2 | // Sarah Reichelt - 24/10/20. 3 | 4 | import SwiftUI 5 | 6 | struct ContentView: View { 7 | var body: some View { 8 | NavigationView { 9 | FeedListView() 10 | ArticleListView(feed: Feed.blankFeed) 11 | ArticleView(article: FeedArticle.blankArticle) 12 | } 13 | .toolbar { 14 | ToolbarItem(placement: .navigation) { 15 | Button(action: toggleSidebar, label: { 16 | Image(systemName: "sidebar.left") 17 | }) 18 | } 19 | } 20 | } 21 | 22 | func toggleSidebar() { 23 | NSApp.keyWindow?.firstResponder?.tryToPerform( 24 | #selector(NSSplitViewController.toggleSidebar(_:)), with: nil) 25 | } 26 | } 27 | 28 | struct ContentView_Previews: PreviewProvider { 29 | static var previews: some View { 30 | ContentView() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /FeedRead/Models/Links.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Links.swift 3 | // FeedRead 4 | // 5 | // Created by Sarah Reichelt on 14/10/20. 6 | // 7 | 8 | import AppKit 9 | 10 | struct Link: Identifiable { 11 | let id = UUID() 12 | let title: String 13 | let address: String 14 | 15 | static var sampleLinks: [Link] { 16 | return [ 17 | Link(title: "TrozWare", address: "https://troz.net"), 18 | Link(title: "Back to the Mac", address: "https://backtomac.org"), 19 | Link(title: "Apple - SwiftUI", address: "https://developer.apple.com/xcode/swiftui/"), 20 | Link(title: "Top 100 World News RSS Feeds", address: "https://blog.feedspot.com/world_news_rss_feeds/"), 21 | Link(title: "Popular RSS Feeds", address: "https://rss.com/blog/popular-rss-feeds/"), 22 | ] 23 | } 24 | 25 | func openLink() { 26 | guard let url = URL(string: address) else { 27 | return 28 | } 29 | NSWorkspace.shared.open(url) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /FeedRead/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSMinimumSystemVersion 22 | $(MACOSX_DEPLOYMENT_TARGET) 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /FeedRead/Views/ArticleView.swift: -------------------------------------------------------------------------------- 1 | // ArticleView.swift - FeedRead 2 | // Sarah Reichelt - 24/10/20. 3 | 4 | import SwiftUI 5 | 6 | struct ArticleView: View { 7 | var article: FeedArticle 8 | 9 | var body: some View { 10 | if article.title.isEmpty { 11 | EmptyView() 12 | } else { 13 | VStack { 14 | Text(article.title) 15 | .font(.title) 16 | .multilineTextAlignment(.center) 17 | .padding(.top) 18 | 19 | WebView(html: article.content) 20 | .padding([.bottom, .horizontal]) 21 | } 22 | .frame(minWidth: 300, idealWidth: 500, maxWidth: .infinity, 23 | minHeight: 300, idealHeight: 400, maxHeight: .infinity, 24 | alignment: .center) 25 | } 26 | } 27 | } 28 | 29 | struct ArticleView_Previews: PreviewProvider { 30 | static var previews: some View { 31 | ArticleView(article: FeedList.sampleArticle) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /FeedRead/Models/Feed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Feed.swift 3 | // FeedRead 4 | // 5 | // Created by Sarah Reichelt on 15/9/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Feed: Identifiable { 11 | var id = UUID() 12 | var feedTitle: String 13 | var feedDescription: String 14 | var feedUrl: URL 15 | var feedDate: Date 16 | var feedArticles: [FeedArticle] 17 | 18 | static var blankFeed: Feed { 19 | let blankUrl = URL(string: "https://www.apple.com")! 20 | return Feed(feedTitle: "", feedDescription: "", feedUrl: blankUrl, feedDate: Date(), feedArticles: []) 21 | } 22 | } 23 | 24 | extension Feed: Codable { 25 | enum CodingKeys: String, CodingKey { 26 | case feedTitle 27 | case feedDescription 28 | case feedUrl 29 | case feedDate 30 | case feedArticles 31 | } 32 | } 33 | 34 | extension Feed: Equatable, Hashable { 35 | static func ==(lhs: Feed, rhs: Feed) -> Bool { 36 | return lhs.feedUrl == rhs.feedUrl 37 | } 38 | 39 | func hash(into hasher: inout Hasher) { 40 | hasher.combine(feedUrl) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /FeedRead/Views/WebView.swift: -------------------------------------------------------------------------------- 1 | // WebView.swift - FeedRead 2 | // Sarah Reichelt - 24/10/20. 3 | 4 | import SwiftUI 5 | import WebKit 6 | 7 | final class WebView: NSViewRepresentable { 8 | var html: String 9 | 10 | init(html: String) { 11 | self.html = html 12 | } 13 | 14 | func makeNSView(context: Context) -> WKWebView { 15 | let webView = WKWebView() 16 | return webView 17 | } 18 | 19 | func updateNSView(_ nsView: WKWebView, context: Context) { 20 | nsView.loadHTMLString(styledHtml, baseURL: nil) 21 | } 22 | 23 | var styledHtml: String { 24 | return """ 25 | 26 | 27 | 39 | 40 | 41 | \(html) 42 | 43 | 44 | """ 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /FeedRead/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /FeedRead/Views/AddFeedView.swift: -------------------------------------------------------------------------------- 1 | // AddFeedView.swift - FeedRead 2 | // Sarah Reichelt - 24/10/20. 3 | 4 | import SwiftUI 5 | 6 | struct AddFeedView: View { 7 | @EnvironmentObject var feedList: FeedList 8 | @Environment(\.presentationMode) var presentationMode 9 | @State private var newFeedAddress = "" 10 | 11 | var body: some View { 12 | VStack { 13 | Text("Adding a New Feed") 14 | .font(.title) 15 | 16 | Text("Enter the URL for the feed to add:") 17 | TextField("Feed URL", text: $newFeedAddress) 18 | .textFieldStyle(RoundedBorderTextFieldStyle()) 19 | .padding(.vertical) 20 | 21 | HStack { 22 | Button(action: { 23 | presentationMode.wrappedValue.dismiss() 24 | 25 | }, label: { 26 | Text("Cancel") 27 | }) 28 | .keyboardShortcut(.cancelAction) 29 | 30 | 31 | Spacer() 32 | 33 | Button(action: { 34 | presentationMode.wrappedValue.dismiss() 35 | feedList.addToUrls(link: newFeedAddress) 36 | 37 | }, label: { 38 | Text("Add Feed") 39 | }) 40 | .keyboardShortcut(.defaultAction) 41 | .disabled(newFeedAddress.isEmpty) 42 | } 43 | } 44 | .padding() 45 | .frame(width: 400) 46 | 47 | } 48 | } 49 | 50 | struct AddFeedView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | AddFeedView() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /FeedRead/Views/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // SettingsView.swift - FeedRead 2 | // Sarah Reichelt - 25/10/20. 3 | 4 | import SwiftUI 5 | 6 | struct SettingsView: View { 7 | @AppStorage("articleLimit") var articleLimit: Int = 0 8 | @AppStorage("appearance") var appearance = AppAppearance.system 9 | 10 | var body: some View { 11 | TabView { 12 | VStack { 13 | Stepper("Show newest \(articleLimit) articles", 14 | value: $articleLimit, 15 | in: 0 ... 300, 16 | step: 5) 17 | Text("Set to 0 to show all available articles.") 18 | .font(.caption) 19 | .foregroundColor(.gray) 20 | } 21 | .tabItem { 22 | Image(systemName: "note.text") 23 | Text("Articles") 24 | } 25 | .navigationTitle("FeedRead Preferences") 26 | 27 | Picker(selection: $appearance, label: Text(""), content: { 28 | Text("Dark mode").tag(AppAppearance.dark) 29 | Text("Light mode").tag(AppAppearance.light) 30 | Text("System mode").tag(AppAppearance.system) 31 | }) 32 | .pickerStyle(RadioGroupPickerStyle()) 33 | .tabItem { 34 | Image(systemName: "moon.circle.fill") 35 | Text("Appearance") 36 | } 37 | .navigationTitle("FeedRead Preferences") 38 | } 39 | .frame(width: 320, height: 120, alignment: .center) 40 | } 41 | } 42 | 43 | struct SettingsView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | SettingsView() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /FeedRead/FeedReadApp.swift: -------------------------------------------------------------------------------- 1 | // FeedReadApp.swift - FeedRead 2 | // Sarah Reichelt - 24/10/20. 3 | 4 | import SwiftUI 5 | 6 | @main 7 | struct FeedReadApp: App { 8 | @StateObject var feedList = FeedList() 9 | @AppStorage("appearance") var appearance = AppAppearance.system 10 | 11 | var body: some Scene { 12 | WindowGroup { 13 | ContentView() 14 | .environmentObject(feedList) 15 | .onAppear { updateMode() } 16 | } 17 | .commands { 18 | CommandGroup(after: CommandGroupPlacement.printItem) { 19 | Button(action: { feedList.refreshAllFeeds() }, label: { 20 | Text("Refresh All") 21 | }) 22 | .keyboardShortcut("r", modifiers: .command) 23 | } 24 | 25 | CommandMenu("Useful Links") { 26 | ForEach(Link.sampleLinks) { link in 27 | Button(action: { link.openLink() }, label: { 28 | Text(link.title) 29 | }) 30 | } 31 | } 32 | } 33 | .onChange(of: appearance) { _ in 34 | updateMode() 35 | } 36 | 37 | Settings { 38 | SettingsView() 39 | .environmentObject(feedList) 40 | } 41 | } 42 | 43 | func updateMode() { 44 | switch appearance { 45 | case .dark: 46 | NSApp.appearance = NSAppearance(named: .darkAqua) 47 | case .light: 48 | NSApp.appearance = NSAppearance(named: .aqua) 49 | case .system: 50 | NSApp.appearance = nil 51 | } 52 | } 53 | } 54 | 55 | enum AppAppearance: String { 56 | case dark 57 | case light 58 | case system 59 | } 60 | -------------------------------------------------------------------------------- /FeedRead/Views/ArticleListView.swift: -------------------------------------------------------------------------------- 1 | // ArticleListView.swift - FeedRead 2 | // Sarah Reichelt - 24/10/20. 3 | 4 | import SwiftUI 5 | 6 | struct ArticleListView: View { 7 | var feed: Feed 8 | @AppStorage("articleLimit") var articleLimit: Int = 0 9 | 10 | var body: some View { 11 | List(trimArticles()) { article in 12 | NavigationLink(destination: ArticleView(article: article)) { 13 | ArticleListRow(article: article) 14 | } 15 | } 16 | } 17 | 18 | func trimArticles() -> [FeedArticle] { 19 | var validArticles = feed.feedArticles 20 | if articleLimit > 0 && validArticles.count > articleLimit { 21 | validArticles = Array(validArticles[0 ..< articleLimit]) 22 | } 23 | return validArticles 24 | } 25 | } 26 | 27 | struct ArticleListView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | ArticleListView(feed: FeedList.sampleFeed) 30 | .previewLayout(.fixed(width: 200, height: 500)) 31 | } 32 | } 33 | 34 | struct ArticleListRow: View { 35 | var article: FeedArticle 36 | 37 | var body: some View { 38 | VStack(alignment: .leading) { 39 | Text(article.title) 40 | .font(.title3) 41 | .fixedSize(horizontal: false, vertical: true) 42 | .lineLimit(3) 43 | .padding(.vertical, 2) 44 | 45 | Text(dateFormatter.string(from: article.date)) 46 | .font(.subheadline) 47 | } 48 | .padding(.bottom, 4) 49 | .padding(.vertical, 4) 50 | } 51 | } 52 | 53 | var dateFormatter: DateFormatter { 54 | let df = DateFormatter() 55 | df.dateStyle = .long 56 | df.timeStyle = .none 57 | return df 58 | } 59 | -------------------------------------------------------------------------------- /FeedRead/Models/FeedArticle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedEntry.swift 3 | // FeedRead 4 | // 5 | // Created by Sarah Reichelt on 15/9/20. 6 | // 7 | 8 | import Foundation 9 | import FeedKit 10 | 11 | struct FeedArticle: Identifiable { 12 | let id = UUID() 13 | let title: String 14 | var content: String 15 | let date: Date 16 | let url: URL 17 | 18 | static var blankArticle: FeedArticle { 19 | let blankUrl = URL(string: "https://www.apple.com")! 20 | return FeedArticle(title: "", content: "", date: Date(), url: blankUrl) 21 | } 22 | } 23 | 24 | extension FeedArticle: Codable { 25 | enum CodingKeys: String, CodingKey { 26 | case title 27 | case content 28 | case date 29 | case url 30 | } 31 | } 32 | 33 | extension FeedArticle { 34 | init?(fromAtom entry: AtomFeedEntry) { 35 | guard let title = entry.title, 36 | let content = entry.content?.value, 37 | let link = entry.links?.first?.attributes?.href, 38 | let url = URL(string: link) else { 39 | return nil 40 | } 41 | let date = entry.updated ?? Date() 42 | 43 | self.title = title 44 | self.content = content 45 | self.date = date 46 | self.url = url 47 | } 48 | 49 | init?(fromRss item: RSSFeedItem) { 50 | guard let title = item.title, 51 | let content = item.description, 52 | let link = item.link, 53 | let url = URL(string: link) else { 54 | return nil 55 | } 56 | let date = item.pubDate ?? Date() 57 | 58 | self.title = title 59 | self.content = content 60 | self.date = date 61 | self.url = url 62 | } 63 | 64 | init?(fromJson item: JSONFeedItem) { 65 | guard let title = item.title, 66 | let content = item.contentHtml, 67 | let link = item.url, 68 | let url = URL(string: link) else { 69 | return nil 70 | } 71 | let date = item.datePublished ?? Date() 72 | 73 | self.title = title 74 | self.content = content 75 | self.date = date 76 | self.url = url 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /FeedRead.xcodeproj/xcshareddata/xcschemes/FeedRead.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /FeedRead/Views/FeedListView.swift: -------------------------------------------------------------------------------- 1 | // FeedListView.swift - FeedRead 2 | // Sarah Reichelt - 24/10/20. 3 | 4 | import SwiftUI 5 | 6 | struct FeedListView: View { 7 | @EnvironmentObject var feedList: FeedList 8 | @State private var selectedFeed: Feed? = nil 9 | @State private var showAddSheet = false 10 | @State private var showDeleteAlert = false 11 | 12 | var body: some View { 13 | VStack { 14 | List(feedList.feeds) { feed in 15 | NavigationLink(destination: ArticleListView(feed: feed), 16 | tag: feed, 17 | selection: $selectedFeed) { 18 | FeedListRow(feed: feed) 19 | } 20 | } 21 | .listStyle(SidebarListStyle()) 22 | 23 | HStack { 24 | Button(action: { showAddSheet = true }, label: { 25 | Label("Add", systemImage: "note.text.badge.plus") 26 | }) 27 | 28 | Spacer() 29 | 30 | Button(action: { showDeleteAlert = true }, label: { 31 | Label("Delete", systemImage: "trash") 32 | .foregroundColor(.red) 33 | }) 34 | } 35 | .padding(.horizontal, 6) 36 | .padding(6) 37 | } 38 | .alert(isPresented: $showDeleteAlert) { 39 | deleteAlert() 40 | } 41 | .sheet(isPresented: $showAddSheet) { 42 | AddFeedView() 43 | } 44 | } 45 | 46 | func deleteAlert() -> Alert { 47 | if let deleteTitle = selectedFeed?.feedTitle { 48 | return Alert(title: Text("Delete"), 49 | message: Text("Really delete the \(deleteTitle) feed?"), 50 | primaryButton: .default(Text("Delete"), action: { 51 | feedList.remove(feedTitle: deleteTitle) 52 | }), 53 | secondaryButton: .cancel({})) 54 | } else { 55 | return Alert(title: Text("Delete Feed"), 56 | message: Text("Select a feed before clicking Delete."), 57 | dismissButton: .default(Text("OK"))) 58 | } 59 | } 60 | } 61 | 62 | struct FeedListView_Previews: PreviewProvider { 63 | static var previews: some View { 64 | FeedListView() 65 | .environmentObject(FeedList()) 66 | .previewLayout(.fixed(width: 200, height: 500)) 67 | } 68 | } 69 | 70 | struct FeedListRow: View { 71 | var feed: Feed 72 | @AppStorage("articleLimit") var articleLimit: Int = 0 73 | 74 | var body: some View { 75 | VStack(alignment: .leading) { 76 | Text(feed.feedTitle) 77 | .font(.title2) 78 | .padding(.top, 4) 79 | if !feed.feedDescription.isEmpty { 80 | Text(feed.feedDescription) 81 | .font(.body) 82 | .fixedSize(horizontal: false, vertical: true) 83 | .lineLimit(2) 84 | } 85 | Text("\(trimArticlesCount()) articles") 86 | .font(.subheadline) 87 | .bold() 88 | } 89 | .padding(.bottom, 4) 90 | .padding(.vertical, 4) 91 | } 92 | 93 | func trimArticlesCount() -> Int { 94 | var validArticles = feed.feedArticles 95 | if articleLimit > 0 && validArticles.count > articleLimit { 96 | validArticles = Array(validArticles[0 ..< articleLimit]) 97 | } 98 | return validArticles.count 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /FeedRead/Models/FeedReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedReader.swift 3 | // FeedRead 4 | // 5 | // Created by Sarah Reichelt on 15/9/20. 6 | // 7 | 8 | import SwiftUI 9 | import FeedKit 10 | 11 | struct FeedReader { 12 | let feedUrl: URL 13 | 14 | func readFeed(callback: @escaping (Feed?) -> ()) { 15 | let parser = FeedParser(URL: feedUrl) 16 | let queue = DispatchQueue.global() 17 | parser.parseAsync(queue: queue) { result in 18 | DispatchQueue.main.async { 19 | switch result { 20 | case .success(let feed): 21 | switch feed { 22 | case let .atom(feed): 23 | let feed = processAtomFeed(feed) 24 | callback(feed) 25 | case let .rss(feed): 26 | let feed = processRssFeed(feed) 27 | callback(feed) 28 | case let .json(feed): 29 | let feed = processJsonFeed(feed) 30 | callback(feed) 31 | } 32 | case .failure(let error): 33 | print(error.localizedDescription) 34 | callback(nil) 35 | } 36 | } 37 | } 38 | } 39 | 40 | func processAtomFeed(_ feed: AtomFeed) -> Feed { 41 | let title = feed.title ?? titleFrom(url: feedUrl) 42 | let description = feed.subtitle?.value ?? "" 43 | let date = feed.updated ?? Date() 44 | 45 | var feedItems: [FeedArticle] = [] 46 | if let entries = feed.entries { 47 | feedItems = entries.compactMap { entry in 48 | if let feedEntry = FeedArticle(fromAtom: entry) { 49 | return feedEntry 50 | } 51 | return nil 52 | } 53 | } 54 | 55 | let newFeed = Feed(feedTitle: title, 56 | feedDescription: description.withoutHtmlTags(), 57 | feedUrl: feedUrl, 58 | feedDate: date, 59 | feedArticles: feedItems) 60 | return newFeed 61 | } 62 | 63 | func processRssFeed(_ feed: RSSFeed) -> Feed { 64 | let title = feed.title ?? titleFrom(url: feedUrl) 65 | let description = feed.description ?? "" 66 | let date = feed.pubDate ?? Date() 67 | 68 | var feedItems: [FeedArticle] = [] 69 | if let items = feed.items { 70 | feedItems = items.compactMap { item in 71 | if let feedEntry = FeedArticle(fromRss: item) { 72 | return feedEntry 73 | } 74 | return nil 75 | } 76 | } 77 | 78 | let newFeed = Feed(feedTitle: title, 79 | feedDescription: description.withoutHtmlTags(), 80 | feedUrl: feedUrl, 81 | feedDate: date, 82 | feedArticles: feedItems) 83 | return newFeed 84 | } 85 | 86 | func processJsonFeed(_ feed: JSONFeed) -> Feed { 87 | let title = feed.title ?? titleFrom(url: feedUrl) 88 | let description = feed.description ?? "" 89 | let date = Date() 90 | 91 | var feedItems: [FeedArticle] = [] 92 | if let items = feed.items { 93 | feedItems = items.compactMap { item in 94 | if let feedEntry = FeedArticle(fromJson: item) { 95 | return feedEntry 96 | } 97 | return nil 98 | } 99 | } 100 | 101 | let newFeed = Feed(feedTitle: title, 102 | feedDescription: description.withoutHtmlTags(), 103 | feedUrl: feedUrl, 104 | feedDate: date, 105 | feedArticles: feedItems) 106 | return newFeed 107 | } 108 | 109 | func titleFrom(url: URL) -> String { 110 | return url.deletingPathExtension().lastPathComponent 111 | } 112 | } 113 | 114 | 115 | extension String { 116 | func withoutHtmlTags() -> String { 117 | return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil) 118 | .trimmingCharacters(in: .whitespacesAndNewlines) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /FeedRead/Models/FeedList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedList.swift 3 | // FeedRead 4 | // 5 | // Created by Sarah Reichelt on 15/9/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | class FeedList: ObservableObject { 11 | @Published var feedsUrls: [URL] = [] 12 | @Published var feeds: [Feed] = [] 13 | 14 | var updateIndex = 0 15 | 16 | init() { 17 | readUrls() 18 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 19 | self.getAllFeeds() 20 | } 21 | } 22 | 23 | func refreshAllFeeds() { 24 | updateIndex = 0 25 | refreshNextFeed() 26 | } 27 | 28 | // MARK: - Updates 29 | 30 | private func getAllFeeds() { 31 | feeds = [] 32 | for url in feedsUrls { 33 | getFeed(for: url) 34 | } 35 | } 36 | 37 | private func refreshNextFeed() { 38 | if updateIndex >= feeds.count { 39 | return 40 | } 41 | getFeed(at: updateIndex) 42 | updateIndex += 1 43 | } 44 | 45 | private func getFeed(for feedUrl: URL) { 46 | let reader = FeedReader(feedUrl: feedUrl) 47 | reader.readFeed { feed in 48 | if let feed = feed { 49 | var feeds = self.feeds 50 | 51 | feeds.append(feed) 52 | feeds.sort { (a, b) -> Bool in 53 | return a.feedTitle.lowercased() < b.feedTitle.lowercased() 54 | } 55 | 56 | self.feeds = feeds 57 | 58 | // un-comment this line to create the sample data for previews 59 | // self.saveTestData() 60 | } 61 | } 62 | } 63 | 64 | private func getFeed(at index: Int) { 65 | let feedToUpdate = feeds[index] 66 | let reader = FeedReader(feedUrl: feedToUpdate.feedUrl) 67 | reader.readFeed { feed in 68 | if let feed = feed { 69 | self.feeds[index] = feed 70 | } 71 | self.refreshNextFeed() 72 | } 73 | } 74 | } 75 | 76 | // MARK: - Data Storage 77 | 78 | extension FeedList { 79 | 80 | func readUrls() { 81 | // un-comment this line to use test URLs only 82 | // useDefaultUrls() 83 | 84 | if let links = UserDefaults.standard.array(forKey: "feedUrls") as? [String], 85 | links.count > 0 { 86 | feedsUrls = links.compactMap { link in 87 | return URL(string: link) 88 | } 89 | } else { 90 | useDefaultUrls() 91 | } 92 | } 93 | 94 | func addToUrls(link: String) { 95 | if let url = URL(string: link), !feedsUrls.contains(url) { 96 | feedsUrls.append(url) 97 | saveUrls() 98 | getFeed(for: url) 99 | } 100 | } 101 | 102 | func remove(feedTitle: String) { 103 | guard let feedIndex = feeds.firstIndex(where: { feed in 104 | feed.feedTitle == feedTitle 105 | }) else { 106 | return 107 | } 108 | 109 | let feed = feeds[feedIndex] 110 | remove(url: feed.feedUrl) 111 | feeds.remove(at: feedIndex) 112 | } 113 | 114 | func remove(url: URL) { 115 | if let index = feedsUrls.firstIndex(of: url) { 116 | feedsUrls.remove(at: index) 117 | saveUrls() 118 | } 119 | } 120 | 121 | private func saveUrls() { 122 | let links = feedsUrls.map { $0.absoluteString } 123 | UserDefaults.standard.setValue(links, forKey: "feedUrls") 124 | } 125 | 126 | } 127 | 128 | // MARK: - Sample Data 129 | 130 | extension FeedList { 131 | private func useDefaultUrls() { 132 | let links = [ 133 | "https://www.apple.com/au/newsroom/rss-feed.rss", 134 | "https://theguardian.com/world/rss", 135 | "https://troz.net/index.xml" 136 | ] 137 | let urls = links.compactMap { link in 138 | return URL(string: link) 139 | } 140 | 141 | feedsUrls = urls 142 | saveUrls() 143 | } 144 | 145 | private func saveTestData() { 146 | do { 147 | let feedJson = try JSONEncoder().encode(feeds) 148 | let fileManager = FileManager.default 149 | let downloadsFolder = try fileManager.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false) 150 | let dataFile = downloadsFolder.appendingPathComponent("sample_feed.json") 151 | try feedJson.write(to: dataFile) 152 | } catch { 153 | print(error) 154 | } 155 | } 156 | 157 | static func useTestData() -> [Feed] { 158 | do { 159 | guard let sampleDataUrl = Bundle.main.url(forResource: "sample_feed", withExtension: "json") else { 160 | return [] 161 | } 162 | let sampleData = try Data(contentsOf: sampleDataUrl) 163 | let sampleFeeds = try JSONDecoder().decode([Feed].self, from: sampleData) 164 | return sampleFeeds 165 | } catch { 166 | print(error) 167 | } 168 | return [] 169 | } 170 | 171 | 172 | static var sampleFeed: Feed { 173 | let sampleFeeds = FeedList.useTestData() 174 | return sampleFeeds[0] 175 | } 176 | 177 | static var sampleArticle: FeedArticle { 178 | let sampleFeeds = FeedList.useTestData() 179 | return sampleFeeds[2].feedArticles[0] 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /FeedRead.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2697DBE22543B0D200020318 /* FeedReadApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2697DBE12543B0D200020318 /* FeedReadApp.swift */; }; 11 | 2697DBE42543B0D200020318 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2697DBE32543B0D200020318 /* ContentView.swift */; }; 12 | 2697DBE62543B0D300020318 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2697DBE52543B0D300020318 /* Assets.xcassets */; }; 13 | 2697DBE92543B0D300020318 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2697DBE82543B0D300020318 /* Preview Assets.xcassets */; }; 14 | 2697DBF92543B0E500020318 /* FeedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2697DBF32543B0E500020318 /* FeedList.swift */; }; 15 | 2697DBFA2543B0E500020318 /* Links.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2697DBF42543B0E500020318 /* Links.swift */; }; 16 | 2697DBFB2543B0E500020318 /* FeedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2697DBF52543B0E500020318 /* FeedReader.swift */; }; 17 | 2697DBFC2543B0E500020318 /* FeedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2697DBF62543B0E500020318 /* FeedArticle.swift */; }; 18 | 2697DBFD2543B0E500020318 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2697DBF72543B0E500020318 /* Feed.swift */; }; 19 | 2697DBFE2543B0E500020318 /* sample_feed.json in Resources */ = {isa = PBXBuildFile; fileRef = 2697DBF82543B0E500020318 /* sample_feed.json */; }; 20 | 2697DC022543B1DE00020318 /* FeedKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2697DC012543B1DE00020318 /* FeedKit */; }; 21 | 26C72E6B2544F4260042D8C3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C72E6A2544F4260042D8C3 /* SettingsView.swift */; }; 22 | 26EEC2952543D9C2003DAA7E /* FeedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEC2942543D9C2003DAA7E /* FeedListView.swift */; }; 23 | 26EEC2A42543DE9F003DAA7E /* ArticleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEC2A32543DE9F003DAA7E /* ArticleListView.swift */; }; 24 | 26EEC2BF2543E5BC003DAA7E /* ArticleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEC2BE2543E5BC003DAA7E /* ArticleView.swift */; }; 25 | 26EEC2C22543E63C003DAA7E /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEC2C12543E63C003DAA7E /* WebView.swift */; }; 26 | 26EEC2C52543E9CA003DAA7E /* AddFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EEC2C42543E9CA003DAA7E /* AddFeedView.swift */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 2697DBDE2543B0D200020318 /* FeedRead.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FeedRead.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 2697DBE12543B0D200020318 /* FeedReadApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedReadApp.swift; sourceTree = ""; }; 32 | 2697DBE32543B0D200020318 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 33 | 2697DBE52543B0D300020318 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 34 | 2697DBE82543B0D300020318 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 35 | 2697DBEA2543B0D300020318 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | 2697DBEB2543B0D300020318 /* FeedRead.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FeedRead.entitlements; sourceTree = ""; }; 37 | 2697DBF32543B0E500020318 /* FeedList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedList.swift; sourceTree = ""; }; 38 | 2697DBF42543B0E500020318 /* Links.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Links.swift; sourceTree = ""; }; 39 | 2697DBF52543B0E500020318 /* FeedReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedReader.swift; sourceTree = ""; }; 40 | 2697DBF62543B0E500020318 /* FeedArticle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedArticle.swift; sourceTree = ""; }; 41 | 2697DBF72543B0E500020318 /* Feed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; 42 | 2697DBF82543B0E500020318 /* sample_feed.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = sample_feed.json; sourceTree = ""; }; 43 | 26C72E6A2544F4260042D8C3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 44 | 26EEC2942543D9C2003DAA7E /* FeedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListView.swift; sourceTree = ""; }; 45 | 26EEC2A32543DE9F003DAA7E /* ArticleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleListView.swift; sourceTree = ""; }; 46 | 26EEC2BE2543E5BC003DAA7E /* ArticleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleView.swift; sourceTree = ""; }; 47 | 26EEC2C12543E63C003DAA7E /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; 48 | 26EEC2C42543E9CA003DAA7E /* AddFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedView.swift; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | 2697DBDB2543B0D200020318 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | 2697DC022543B1DE00020318 /* FeedKit in Frameworks */, 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | 2697DBD52543B0D200020318 = { 64 | isa = PBXGroup; 65 | children = ( 66 | 2697DBE02543B0D200020318 /* FeedRead */, 67 | 2697DBDF2543B0D200020318 /* Products */, 68 | ); 69 | sourceTree = ""; 70 | }; 71 | 2697DBDF2543B0D200020318 /* Products */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 2697DBDE2543B0D200020318 /* FeedRead.app */, 75 | ); 76 | name = Products; 77 | sourceTree = ""; 78 | }; 79 | 2697DBE02543B0D200020318 /* FeedRead */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 2697DBE12543B0D200020318 /* FeedReadApp.swift */, 83 | 26C72E682544F3BA0042D8C3 /* Views */, 84 | 2697DBF22543B0E500020318 /* Models */, 85 | 2697DBE52543B0D300020318 /* Assets.xcassets */, 86 | 2697DBEA2543B0D300020318 /* Info.plist */, 87 | 2697DBEB2543B0D300020318 /* FeedRead.entitlements */, 88 | 2697DBE72543B0D300020318 /* Preview Content */, 89 | ); 90 | path = FeedRead; 91 | sourceTree = ""; 92 | }; 93 | 2697DBE72543B0D300020318 /* Preview Content */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 2697DBE82543B0D300020318 /* Preview Assets.xcassets */, 97 | ); 98 | path = "Preview Content"; 99 | sourceTree = ""; 100 | }; 101 | 2697DBF22543B0E500020318 /* Models */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 2697DBF32543B0E500020318 /* FeedList.swift */, 105 | 2697DBF72543B0E500020318 /* Feed.swift */, 106 | 2697DBF62543B0E500020318 /* FeedArticle.swift */, 107 | 2697DBF52543B0E500020318 /* FeedReader.swift */, 108 | 2697DBF42543B0E500020318 /* Links.swift */, 109 | 2697DBF82543B0E500020318 /* sample_feed.json */, 110 | ); 111 | path = Models; 112 | sourceTree = ""; 113 | }; 114 | 26C72E682544F3BA0042D8C3 /* Views */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 2697DBE32543B0D200020318 /* ContentView.swift */, 118 | 26EEC2942543D9C2003DAA7E /* FeedListView.swift */, 119 | 26EEC2A32543DE9F003DAA7E /* ArticleListView.swift */, 120 | 26EEC2BE2543E5BC003DAA7E /* ArticleView.swift */, 121 | 26EEC2C12543E63C003DAA7E /* WebView.swift */, 122 | 26EEC2C42543E9CA003DAA7E /* AddFeedView.swift */, 123 | 26C72E6A2544F4260042D8C3 /* SettingsView.swift */, 124 | ); 125 | path = Views; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | 2697DBDD2543B0D200020318 /* FeedRead */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 2697DBEE2543B0D300020318 /* Build configuration list for PBXNativeTarget "FeedRead" */; 134 | buildPhases = ( 135 | 2697DBDA2543B0D200020318 /* Sources */, 136 | 2697DBDB2543B0D200020318 /* Frameworks */, 137 | 2697DBDC2543B0D200020318 /* Resources */, 138 | ); 139 | buildRules = ( 140 | ); 141 | dependencies = ( 142 | ); 143 | name = FeedRead; 144 | packageProductDependencies = ( 145 | 2697DC012543B1DE00020318 /* FeedKit */, 146 | ); 147 | productName = FeedRead; 148 | productReference = 2697DBDE2543B0D200020318 /* FeedRead.app */; 149 | productType = "com.apple.product-type.application"; 150 | }; 151 | /* End PBXNativeTarget section */ 152 | 153 | /* Begin PBXProject section */ 154 | 2697DBD62543B0D200020318 /* Project object */ = { 155 | isa = PBXProject; 156 | attributes = { 157 | LastSwiftUpdateCheck = 1220; 158 | LastUpgradeCheck = 1220; 159 | TargetAttributes = { 160 | 2697DBDD2543B0D200020318 = { 161 | CreatedOnToolsVersion = 12.2; 162 | }; 163 | }; 164 | }; 165 | buildConfigurationList = 2697DBD92543B0D200020318 /* Build configuration list for PBXProject "FeedRead" */; 166 | compatibilityVersion = "Xcode 9.3"; 167 | developmentRegion = en; 168 | hasScannedForEncodings = 0; 169 | knownRegions = ( 170 | en, 171 | Base, 172 | ); 173 | mainGroup = 2697DBD52543B0D200020318; 174 | packageReferences = ( 175 | 2697DC002543B1DE00020318 /* XCRemoteSwiftPackageReference "FeedKit" */, 176 | ); 177 | productRefGroup = 2697DBDF2543B0D200020318 /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 2697DBDD2543B0D200020318 /* FeedRead */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 2697DBDC2543B0D200020318 /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 2697DBFE2543B0E500020318 /* sample_feed.json in Resources */, 192 | 2697DBE92543B0D300020318 /* Preview Assets.xcassets in Resources */, 193 | 2697DBE62543B0D300020318 /* Assets.xcassets in Resources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | /* End PBXResourcesBuildPhase section */ 198 | 199 | /* Begin PBXSourcesBuildPhase section */ 200 | 2697DBDA2543B0D200020318 /* Sources */ = { 201 | isa = PBXSourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | 26EEC2952543D9C2003DAA7E /* FeedListView.swift in Sources */, 205 | 2697DBFC2543B0E500020318 /* FeedArticle.swift in Sources */, 206 | 2697DBE42543B0D200020318 /* ContentView.swift in Sources */, 207 | 2697DBF92543B0E500020318 /* FeedList.swift in Sources */, 208 | 26EEC2C52543E9CA003DAA7E /* AddFeedView.swift in Sources */, 209 | 2697DBFD2543B0E500020318 /* Feed.swift in Sources */, 210 | 2697DBE22543B0D200020318 /* FeedReadApp.swift in Sources */, 211 | 2697DBFA2543B0E500020318 /* Links.swift in Sources */, 212 | 26EEC2BF2543E5BC003DAA7E /* ArticleView.swift in Sources */, 213 | 26EEC2A42543DE9F003DAA7E /* ArticleListView.swift in Sources */, 214 | 2697DBFB2543B0E500020318 /* FeedReader.swift in Sources */, 215 | 26EEC2C22543E63C003DAA7E /* WebView.swift in Sources */, 216 | 26C72E6B2544F4260042D8C3 /* SettingsView.swift in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | /* End PBXSourcesBuildPhase section */ 221 | 222 | /* Begin XCBuildConfiguration section */ 223 | 2697DBEC2543B0D300020318 /* Debug */ = { 224 | isa = XCBuildConfiguration; 225 | buildSettings = { 226 | ALWAYS_SEARCH_USER_PATHS = NO; 227 | CLANG_ANALYZER_NONNULL = YES; 228 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 229 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 230 | CLANG_CXX_LIBRARY = "libc++"; 231 | CLANG_ENABLE_MODULES = YES; 232 | CLANG_ENABLE_OBJC_ARC = YES; 233 | CLANG_ENABLE_OBJC_WEAK = YES; 234 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 235 | CLANG_WARN_BOOL_CONVERSION = YES; 236 | CLANG_WARN_COMMA = YES; 237 | CLANG_WARN_CONSTANT_CONVERSION = YES; 238 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 239 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 240 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 241 | CLANG_WARN_EMPTY_BODY = YES; 242 | CLANG_WARN_ENUM_CONVERSION = YES; 243 | CLANG_WARN_INFINITE_RECURSION = YES; 244 | CLANG_WARN_INT_CONVERSION = YES; 245 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 247 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 249 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | COPY_PHASE_STRIP = NO; 257 | DEBUG_INFORMATION_FORMAT = dwarf; 258 | ENABLE_STRICT_OBJC_MSGSEND = YES; 259 | ENABLE_TESTABILITY = YES; 260 | GCC_C_LANGUAGE_STANDARD = gnu11; 261 | GCC_DYNAMIC_NO_PIC = NO; 262 | GCC_NO_COMMON_BLOCKS = YES; 263 | GCC_OPTIMIZATION_LEVEL = 0; 264 | GCC_PREPROCESSOR_DEFINITIONS = ( 265 | "DEBUG=1", 266 | "$(inherited)", 267 | ); 268 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 269 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 270 | GCC_WARN_UNDECLARED_SELECTOR = YES; 271 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 272 | GCC_WARN_UNUSED_FUNCTION = YES; 273 | GCC_WARN_UNUSED_VARIABLE = YES; 274 | MACOSX_DEPLOYMENT_TARGET = 11.0; 275 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 276 | MTL_FAST_MATH = YES; 277 | ONLY_ACTIVE_ARCH = YES; 278 | SDKROOT = macosx; 279 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 280 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 281 | }; 282 | name = Debug; 283 | }; 284 | 2697DBED2543B0D300020318 /* Release */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ALWAYS_SEARCH_USER_PATHS = NO; 288 | CLANG_ANALYZER_NONNULL = YES; 289 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 290 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 291 | CLANG_CXX_LIBRARY = "libc++"; 292 | CLANG_ENABLE_MODULES = YES; 293 | CLANG_ENABLE_OBJC_ARC = YES; 294 | CLANG_ENABLE_OBJC_WEAK = YES; 295 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 296 | CLANG_WARN_BOOL_CONVERSION = YES; 297 | CLANG_WARN_COMMA = YES; 298 | CLANG_WARN_CONSTANT_CONVERSION = YES; 299 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 300 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 301 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 302 | CLANG_WARN_EMPTY_BODY = YES; 303 | CLANG_WARN_ENUM_CONVERSION = YES; 304 | CLANG_WARN_INFINITE_RECURSION = YES; 305 | CLANG_WARN_INT_CONVERSION = YES; 306 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 307 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 308 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 309 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 310 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 311 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 312 | CLANG_WARN_STRICT_PROTOTYPES = YES; 313 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 314 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 315 | CLANG_WARN_UNREACHABLE_CODE = YES; 316 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 317 | COPY_PHASE_STRIP = NO; 318 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 319 | ENABLE_NS_ASSERTIONS = NO; 320 | ENABLE_STRICT_OBJC_MSGSEND = YES; 321 | GCC_C_LANGUAGE_STANDARD = gnu11; 322 | GCC_NO_COMMON_BLOCKS = YES; 323 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 324 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 325 | GCC_WARN_UNDECLARED_SELECTOR = YES; 326 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 327 | GCC_WARN_UNUSED_FUNCTION = YES; 328 | GCC_WARN_UNUSED_VARIABLE = YES; 329 | MACOSX_DEPLOYMENT_TARGET = 11.0; 330 | MTL_ENABLE_DEBUG_INFO = NO; 331 | MTL_FAST_MATH = YES; 332 | SDKROOT = macosx; 333 | SWIFT_COMPILATION_MODE = wholemodule; 334 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 335 | }; 336 | name = Release; 337 | }; 338 | 2697DBEF2543B0D300020318 /* Debug */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 342 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 343 | CODE_SIGN_ENTITLEMENTS = FeedRead/FeedRead.entitlements; 344 | CODE_SIGN_STYLE = Automatic; 345 | COMBINE_HIDPI_IMAGES = YES; 346 | DEVELOPMENT_ASSET_PATHS = "\"FeedRead/Preview Content\""; 347 | ENABLE_PREVIEWS = YES; 348 | INFOPLIST_FILE = FeedRead/Info.plist; 349 | LD_RUNPATH_SEARCH_PATHS = ( 350 | "$(inherited)", 351 | "@executable_path/../Frameworks", 352 | ); 353 | MACOSX_DEPLOYMENT_TARGET = 11.0; 354 | PRODUCT_BUNDLE_IDENTIFIER = net.troz.FeedRead; 355 | PRODUCT_NAME = "$(TARGET_NAME)"; 356 | SWIFT_VERSION = 5.0; 357 | }; 358 | name = Debug; 359 | }; 360 | 2697DBF02543B0D300020318 /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 364 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 365 | CODE_SIGN_ENTITLEMENTS = FeedRead/FeedRead.entitlements; 366 | CODE_SIGN_STYLE = Automatic; 367 | COMBINE_HIDPI_IMAGES = YES; 368 | DEVELOPMENT_ASSET_PATHS = "\"FeedRead/Preview Content\""; 369 | ENABLE_PREVIEWS = YES; 370 | INFOPLIST_FILE = FeedRead/Info.plist; 371 | LD_RUNPATH_SEARCH_PATHS = ( 372 | "$(inherited)", 373 | "@executable_path/../Frameworks", 374 | ); 375 | MACOSX_DEPLOYMENT_TARGET = 11.0; 376 | PRODUCT_BUNDLE_IDENTIFIER = net.troz.FeedRead; 377 | PRODUCT_NAME = "$(TARGET_NAME)"; 378 | SWIFT_VERSION = 5.0; 379 | }; 380 | name = Release; 381 | }; 382 | /* End XCBuildConfiguration section */ 383 | 384 | /* Begin XCConfigurationList section */ 385 | 2697DBD92543B0D200020318 /* Build configuration list for PBXProject "FeedRead" */ = { 386 | isa = XCConfigurationList; 387 | buildConfigurations = ( 388 | 2697DBEC2543B0D300020318 /* Debug */, 389 | 2697DBED2543B0D300020318 /* Release */, 390 | ); 391 | defaultConfigurationIsVisible = 0; 392 | defaultConfigurationName = Release; 393 | }; 394 | 2697DBEE2543B0D300020318 /* Build configuration list for PBXNativeTarget "FeedRead" */ = { 395 | isa = XCConfigurationList; 396 | buildConfigurations = ( 397 | 2697DBEF2543B0D300020318 /* Debug */, 398 | 2697DBF02543B0D300020318 /* Release */, 399 | ); 400 | defaultConfigurationIsVisible = 0; 401 | defaultConfigurationName = Release; 402 | }; 403 | /* End XCConfigurationList section */ 404 | 405 | /* Begin XCRemoteSwiftPackageReference section */ 406 | 2697DC002543B1DE00020318 /* XCRemoteSwiftPackageReference "FeedKit" */ = { 407 | isa = XCRemoteSwiftPackageReference; 408 | repositoryURL = "https://github.com/nmdias/FeedKit"; 409 | requirement = { 410 | kind = upToNextMajorVersion; 411 | minimumVersion = 9.1.2; 412 | }; 413 | }; 414 | /* End XCRemoteSwiftPackageReference section */ 415 | 416 | /* Begin XCSwiftPackageProductDependency section */ 417 | 2697DC012543B1DE00020318 /* FeedKit */ = { 418 | isa = XCSwiftPackageProductDependency; 419 | package = 2697DC002543B1DE00020318 /* XCRemoteSwiftPackageReference "FeedKit" */; 420 | productName = FeedKit; 421 | }; 422 | /* End XCSwiftPackageProductDependency section */ 423 | }; 424 | rootObject = 2697DBD62543B0D200020318 /* Project object */; 425 | } 426 | --------------------------------------------------------------------------------