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