├── .gitignore
├── HNReader
├── View
│ ├── HNReader
│ │ └── HNReader.app
│ │ │ └── Contents
│ │ │ ├── PkgInfo
│ │ │ ├── MacOS
│ │ │ └── HNReader
│ │ │ ├── Resources
│ │ │ ├── Assets.car
│ │ │ ├── AppIcon.icns
│ │ │ └── HNReader.momd
│ │ │ │ ├── HNReader.mom
│ │ │ │ ├── HNReader.omo
│ │ │ │ └── VersionInfo.plist
│ │ │ ├── Info.plist
│ │ │ └── _CodeSignature
│ │ │ └── CodeResources
│ ├── HomeView.swift
│ ├── ConditionalRedactedModifier.swift
│ ├── Components
│ │ ├── HTMLText.swift
│ │ └── BgButton.swift
│ ├── InfoView.swift
│ ├── ItemList.swift
│ └── ItemCell.swift
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── icon_16x16.png
│ │ ├── icon_32x32.png
│ │ ├── icon_128x128.png
│ │ ├── icon_16x16@2x.png
│ │ ├── icon_256x256.png
│ │ ├── icon_32x32@2x.png
│ │ ├── icon_512x512.png
│ │ ├── icon_128x128@2x.png
│ │ ├── icon_256x256@2x.png
│ │ ├── icon_512x512@2x.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── fonts
│ ├── IBMPlexSerif-Bold.ttf
│ ├── IBMPlexSerif-Light.ttf
│ ├── IBMPlexSerif-Regular.ttf
│ └── IBMPlexSerif-SemiBold.ttf
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── HNReader.xcdatamodeld
│ ├── .xccurrentversion
│ └── HNReader.xcdatamodel
│ │ └── contents
├── Model
│ ├── User.swift
│ └── Item.swift
├── HNReader.entitlements
├── Utils
│ ├── +View.swift
│ └── +Date.swift
├── HNClient
│ ├── ItemCache.swift
│ ├── ItemDownloader.swift
│ ├── HackerNews.swift
│ └── HackerNewsClient.swift
├── ViewModel
│ ├── ItemListViewModel.swift
│ └── AppState.swift
├── Info.plist
├── HNReaderApp.swift
└── Persistence.swift
├── Logo
├── Logo.png
└── HNReader Logo.sketch
├── resources
├── appstore.png
└── screenshot.png
├── HNReader.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── mattrighetti.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── mattrighetti.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── LICENSE
├── HNReaderTests
├── Info.plist
├── ModelTests
│ └── ItemTests.swift
├── UtilsTests
│ └── +DateTests.swift
└── HNClientTests
│ ├── HackerNewsTests.swift
│ └── HackerNewsClientTests.swift
├── CHANGELOG.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | GoogleService-Info.plist
3 |
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/PkgInfo:
--------------------------------------------------------------------------------
1 | APPL????
--------------------------------------------------------------------------------
/Logo/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/Logo/Logo.png
--------------------------------------------------------------------------------
/resources/appstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/resources/appstore.png
--------------------------------------------------------------------------------
/resources/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/resources/screenshot.png
--------------------------------------------------------------------------------
/Logo/HNReader Logo.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/Logo/HNReader Logo.sketch
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/HNReader/fonts/IBMPlexSerif-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/fonts/IBMPlexSerif-Bold.ttf
--------------------------------------------------------------------------------
/HNReader/fonts/IBMPlexSerif-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/fonts/IBMPlexSerif-Light.ttf
--------------------------------------------------------------------------------
/HNReader/fonts/IBMPlexSerif-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/fonts/IBMPlexSerif-Regular.ttf
--------------------------------------------------------------------------------
/HNReader/fonts/IBMPlexSerif-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/fonts/IBMPlexSerif-SemiBold.ttf
--------------------------------------------------------------------------------
/HNReader/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/MacOS/HNReader:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/View/HNReader/HNReader.app/Contents/MacOS/HNReader
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/Resources/Assets.car:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/View/HNReader/HNReader.app/Contents/Resources/Assets.car
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/Resources/AppIcon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/View/HNReader/HNReader.app/Contents/Resources/AppIcon.icns
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/HNReader.mom:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/HNReader.mom
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/HNReader.omo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/HNReader.omo
--------------------------------------------------------------------------------
/HNReader.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/VersionInfo.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader/View/HNReader/HNReader.app/Contents/Resources/HNReader.momd/VersionInfo.plist
--------------------------------------------------------------------------------
/HNReader/HNReader.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/HNReader.xcodeproj/project.xcworkspace/xcuserdata/mattrighetti.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattrighetti/HNReaderApp/HEAD/HNReader.xcodeproj/project.xcworkspace/xcuserdata/mattrighetti.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Mattia Righetti (mattiarighetti@protonmail.com)
2 |
3 | Licensed under either of
4 |
5 | * Apache License, Version 2.0, (http://www.apache.org/license/LICENSE-2.0)
6 | * MIT License (http://opensource.org/licenses/MIT)
7 |
8 | at your option.
--------------------------------------------------------------------------------
/HNReader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/HNReader/HNReader.xcdatamodeld/HNReader.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/HNReader/Model/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct User: Decodable {
11 | public let id: String
12 | public let created: Int
13 | public let karma: Int
14 | public let about: String?
15 | public let submitted: [Int]?
16 | }
17 |
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | },
6 | {
7 | "appearances" : [
8 | {
9 | "appearance" : "luminosity",
10 | "value" : "dark"
11 | }
12 | ],
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/HNReader/HNReader.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 |
--------------------------------------------------------------------------------
/HNReader.xcodeproj/xcuserdata/mattrighetti.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | HNReader.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/HNReader/Utils/+View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // +View.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 05/11/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 | @discardableResult
12 | func openInWindow(title: String, sender: Any?) -> NSWindow {
13 | let controller = NSHostingController(rootView: self)
14 | let win = NSWindow(contentViewController: controller)
15 | win.contentViewController = controller
16 | win.title = title
17 | win.makeKeyAndOrderFront(sender)
18 | return win
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/HNReader/Utils/+Date.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Mattia Righetti on 13/06/21.
3 | //
4 |
5 | import Foundation
6 |
7 | extension Date {
8 | public func timeElapsedStringRepresentation(since: Date) -> String {
9 | let elapsedTime = timeIntervalSince(since)
10 | let years = Int(floor(elapsedTime / 365 / 24 / 60 / 60))
11 | let days = Int(floor(elapsedTime / 24 / 60 / 60))
12 | let hours = Int(floor(elapsedTime / 60 / 60))
13 | let minutes = Int(floor(elapsedTime / 60))
14 |
15 | if years >= 1 {
16 | return "\(years)y"
17 | } else if days >= 1 {
18 | return "\(days)d"
19 | } else if hours >= 1 {
20 | return "\(hours)h"
21 | } else {
22 | return "\(minutes)m"
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/HNReader/HNClient/ItemCache.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Mattia Righetti on 13/06/21.
3 | //
4 |
5 | import Foundation
6 |
7 | class StructWrapper: NSObject {
8 | let value: T
9 |
10 | init(_ _struct: T) {
11 | value = _struct
12 | }
13 | }
14 |
15 |
16 | class ItemCache: NSCache> {
17 | static let shared = ItemCache()
18 |
19 | func cache(_ item: Item, for key: Int) {
20 | let keyString = NSString(format: "%d", key)
21 | let itemWrapper = StructWrapper(item)
22 | self.setObject(itemWrapper, forKey: keyString)
23 | }
24 |
25 | func getItem(for key: Int) -> Item? {
26 | let keyString = NSString(format: "%d", key)
27 | let itemWrapper = self.object(forKey: keyString)
28 | return itemWrapper?.value
29 | }
30 | }
--------------------------------------------------------------------------------
/HNReaderTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
3 |
4 | - - -
5 | ## 1.3.1 - 2023-11-05
6 | #### Bug Fixes
7 | - fix bundle version - (805ebaf) - Mattia Righetti
8 | - fix settings view - (54e1250) - Mattia Righetti
9 | #### Features
10 | - flattened ui and add url/comments link - (0007c9c) - Mattia Righetti
11 | #### Miscellaneous Chores
12 | - **(version)** bump to v1.3 - (f472012) - Mattia Righetti
13 | - xcode stuff - (c1fe436) - Mattia Righetti
14 | #### Refactoring
15 | - misc infoview refactoring - (9bbfb60) - Mattia Righetti
16 | - infoview + bgbutton - (db8b862) - Mattia Righetti
17 | - switch to #Preview - (b4f470e) - Mattia Righetti
18 |
19 | - - -
20 |
21 | Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto).
22 |
--------------------------------------------------------------------------------
/HNReader/View/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeView.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import SwiftUI
9 | import CoreData
10 |
11 | struct HomeView: View {
12 | var body: some View {
13 | NavigationView {
14 | Sidebar()
15 | ItemList()
16 | }
17 | }
18 | }
19 |
20 | struct Sidebar: View {
21 | @EnvironmentObject var appState: AppState
22 |
23 | var body: some View {
24 | List(selection: $appState.sidebarSelection) {
25 | Section(header: Text("Categories")) {
26 | ForEach(AppState.SidebarSelection.allCases, id: \.self) { selectionItem in
27 | Label(selectionItem.rawValue, systemImage: selectionItem.iconName)
28 | .tag(selectionItem)
29 | }
30 | }
31 | }
32 | }
33 | }
34 |
35 | #Preview {
36 | HomeView()
37 | }
38 |
--------------------------------------------------------------------------------
/HNReaderTests/ModelTests/ItemTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Mattia Righetti on 13/06/21.
3 | //
4 |
5 | import XCTest
6 | @testable import HNReader
7 |
8 | class ItemTests: XCTestCase {
9 | func testItemDateIntervalFormatter() {
10 | let yesterday = Date(timeIntervalSinceNow: -(60*60*24))
11 | let item1 = Item(id: 213412,
12 | deleted: false,
13 | type: .story,
14 | by: nil,
15 | time: Int(yesterday.timeIntervalSince1970),
16 | text: nil,
17 | dead: nil,
18 | parent: nil,
19 | poll: nil,
20 | kids: nil,
21 | url: "https://www.reddit.com/r/MechanicalKeyboards",
22 | score: nil,
23 | title: nil,
24 | parts: nil,
25 | descendants: nil)
26 |
27 | XCTAssertEqual("1d", item1.timeStringRepresentation)
28 | XCTAssertEqual("reddit.com", item1.urlHost)
29 | }
30 | }
--------------------------------------------------------------------------------
/HNReader/View/ConditionalRedactedModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConditionalRedacted.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 04/11/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ConditionalRedactedModifier: ViewModifier {
11 | var isRedacted: Bool
12 |
13 | func body(content: Content) -> some View {
14 | if isRedacted {
15 | content.redacted(reason: .placeholder)
16 | } else {
17 | content
18 | }
19 | }
20 | }
21 |
22 | extension View {
23 | func redactIfNull(_ obj: Optional) -> some View {
24 | switch obj {
25 | case .none:
26 | return self.modifier(ConditionalRedactedModifier(isRedacted: true))
27 | case .some(_):
28 | return self.modifier(ConditionalRedactedModifier(isRedacted: false))
29 | }
30 | }
31 | }
32 |
33 | #Preview {
34 | VStack {
35 | Text("Some Text")
36 | .redactIfNull(Optional.none)
37 | }
38 | .frame(width: 200, height: 100)
39 | }
40 |
--------------------------------------------------------------------------------
/HNReader/View/Components/HTMLText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTMLText.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 13/06/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HTMLText: NSViewRepresentable {
11 | var text: String
12 |
13 | func makeNSView(context: Context) -> NSTextField {
14 | let text = NSTextField()
15 | text.isEditable = false
16 | if let attributedString = try? NSAttributedString(
17 | data: self.text.data(using: .utf8)!,
18 | options: [.documentType: NSAttributedString.DocumentType.html],
19 | documentAttributes: nil
20 | ) {
21 | text.attributedStringValue = attributedString
22 | }
23 | text.textColor = .white
24 | return text
25 | }
26 |
27 | func updateNSView(_ nsView: NSViewType, context: Context) {}
28 | }
29 |
30 | #Preview {
31 | HTMLText(text: """
32 | string <h1>Krupal testing <span style="font-weight:
33 | bold;">Customer WYWO</span></h1>
34 | """)
35 | }
36 |
--------------------------------------------------------------------------------
/HNReader/HNClient/ItemDownloader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Mattia Righetti on 13/06/21.
3 | //
4 |
5 | import Foundation
6 |
7 | protocol ItemDownloader {
8 | var cacheKey: Int { get }
9 | func downloadItem(completion: @escaping (Item?) -> Void)
10 | }
11 |
12 | class DefaultItemDownloader: ItemDownloader {
13 | let itemId: Int
14 | var cacheKey: Int {
15 | itemId
16 | }
17 |
18 | init(itemId: Int) {
19 | self.itemId = itemId
20 | }
21 |
22 | func downloadItem(completion: @escaping (Item?) -> ()) {
23 | guard let url = URL(string: HackerNews.API.Item.id(itemId).urlString) else { return }
24 | let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
25 | if let data = data {
26 | var item: Item
27 | do {
28 | item = try JSONDecoder().decode(Item.self, from: data)
29 | completion(item)
30 | } catch {
31 | print("encountered error while downloading item")
32 | completion(nil)
33 | }
34 | }
35 | }
36 | task.resume()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/HNReader/ViewModel/ItemListViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Mattia Righetti on 12/06/21.
3 | //
4 |
5 | import Combine
6 | import SwiftUI
7 | import OSLog
8 |
9 | class ItemListViewModel: ObservableObject {
10 | @Published var currentNewsSelection: HackerNews.API.Stories = .top {
11 | willSet {
12 | fetchStories(by: newValue)
13 | }
14 | }
15 | @Published var storiesIds: [Int] = []
16 | public var subscriptions = Set()
17 |
18 | public func fetchStories(by category: HackerNews.API.Stories) {
19 | HackerNewsClient.shared.getStoriesId(by: category)
20 | .receive(on: DispatchQueue.main)
21 | .sink(receiveCompletion: { completion in
22 | switch completion {
23 | case .failure(let error):
24 | NSLog("encountered error while completing fetch task: \(error)")
25 | case .finished:
26 | break
27 | }
28 | }, receiveValue: { [unowned self] itemIds in
29 | storiesIds = itemIds
30 | })
31 | .store(in: &subscriptions)
32 | }
33 |
34 | public func refreshStories() {
35 | fetchStories(by: currentNewsSelection)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/HNReaderTests/UtilsTests/+DateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Mattia Righetti on 13/06/21.
3 | //
4 |
5 | import XCTest
6 | @testable import HNReader
7 |
8 | class DateTests: XCTestCase {
9 | func testElapsedTimeStringRepresentation() {
10 | let minuteTimeInterval = TimeInterval(60)
11 | let hourTimeInterval = TimeInterval(minuteTimeInterval*60)
12 | let dayTimeInterval = TimeInterval(24*hourTimeInterval)
13 | let yearTimeInterval = TimeInterval(dayTimeInterval*365)
14 | let yesterday = Date(timeIntervalSinceNow: -dayTimeInterval)
15 | let halfHourAgo = Date(timeIntervalSinceNow: -(minuteTimeInterval*30))
16 | let hourAgo = Date(timeIntervalSinceNow: -hourTimeInterval)
17 | let threeYearsAgo = Date(timeIntervalSinceNow: -(3*yearTimeInterval))
18 | let s1 = Date().timeElapsedStringRepresentation(since: yesterday)
19 | let s2 = Date().timeElapsedStringRepresentation(since: halfHourAgo)
20 | let s3 = Date().timeElapsedStringRepresentation(since: hourAgo)
21 | let s4 = Date().timeElapsedStringRepresentation(since: threeYearsAgo)
22 | XCTAssertEqual("1d", s1)
23 | XCTAssertEqual("30m", s2)
24 | XCTAssertEqual("1h", s3)
25 | XCTAssertEqual("3y", s4)
26 | }
27 | }
--------------------------------------------------------------------------------
/HNReader/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | LSApplicationCategoryType
22 | public.app-category.news
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | UIAppFonts
26 |
27 | IBMPlexSerif-Bold.ttf
28 | IBMPlexSerif-SemiBold.ttf
29 | IBMPlexSerif-Regular.ttf
30 | IBMPlexSerif-Light.ttf
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # HNReaderApp
6 | This is the public repository for the HNReader macOS application.
7 |
8 | You can report any issue and suggest/request new features in the [issue](https://github.com/mattrighetti/HNReaderApp/issues) section, you can also use the [discussions](https://github.com/mattrighetti/HNReaderApp/discussions) section to chat with others and me about the application.
9 |
10 | The application is still in beta for the moment and needs optimizations. When the time come I will release it to AppStore and on `brew`.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ## Application preview
19 | ### Dark mode
20 |
21 |
22 | ### Light mode
23 |
24 |
25 | ## License
26 | [MIT](LICENSE) or [APACHE 2.0](LICENSE)
27 |
--------------------------------------------------------------------------------
/HNReader/View/Components/BgButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BgButton.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 05/11/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BgButton: View {
11 | var text: String? = nil
12 | var icon: String
13 | var disabled: Bool = false
14 | var minSize: CGSize
15 | var onHover: ((Bool) -> Void)? = nil
16 | var action: (() -> ())? = nil
17 |
18 | var body: some View {
19 | ZStack {
20 | RoundedRectangle(cornerRadius: 15)
21 | .foregroundStyle(Color.gray.opacity(0.1))
22 | .frame(width: minSize.width, height: minSize.height)
23 |
24 | Label(title: {
25 | if text != nil {
26 | Text(text!)
27 | .font(.custom("IBMPlexSerif-Regular", size: 13))
28 | }
29 | }, icon: {
30 | Image(systemName: icon)
31 | })
32 | .foregroundStyle(disabled ? .tertiary : .primary)
33 | .padding()
34 | }
35 | .frame(width: minSize.width, height: minSize.height)
36 | .onHover {
37 | onHover?($0)
38 | }
39 | .onTapGesture {
40 | action?()
41 | }
42 | }
43 | }
44 |
45 | #Preview {
46 | BgButton(text: "Rate app", icon: "star", minSize: CGSize(width: 150, height: 50)).padding()
47 | }
48 |
--------------------------------------------------------------------------------
/HNReaderTests/HNClientTests/HackerNewsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Mattia Righetti on 12/06/21.
3 | //
4 |
5 | import XCTest
6 | @testable import HNReader
7 |
8 | class HackerNewsTests: XCTestCase {
9 | func testItemApiUrlStrings() throws {
10 | let itemUrlExpected = "https://hacker-news.firebaseio.com/v0/item/39.json"
11 | let itemUrl = HackerNews.API.Item.id(39)
12 | XCTAssertEqual(itemUrlExpected, itemUrl.urlString)
13 | }
14 |
15 | func testUserApiUrlString() throws {
16 | let userUrlExpected = "https://hacker-news.firebaseio.com/v0/user/randomuser.json"
17 | let userUrl = HackerNews.API.User.id("randomuser")
18 | XCTAssertEqual(userUrlExpected, userUrl.urlString)
19 | }
20 |
21 | func testStoriesApiUrlString() throws {
22 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/topstories.json", HackerNews.API.Stories.top.urlString)
23 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/newstories.json", HackerNews.API.Stories.new.urlString)
24 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/beststories.json", HackerNews.API.Stories.best.urlString)
25 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/askstories.json", HackerNews.API.Stories.ask.urlString)
26 | XCTAssertEqual("https://hacker-news.firebaseio.com/v0/showstories.json", HackerNews.API.Stories.show.urlString)
27 | }
28 | }
--------------------------------------------------------------------------------
/HNReader/HNClient/HackerNews.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HackerNes.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import Foundation
9 |
10 | /// HackerNews endpoint data structure
11 | struct HackerNews {
12 | public static let endpoint = "https://hacker-news.firebaseio.com/v0"
13 |
14 | /// HackerNews REST API methods
15 | enum API {
16 | /// HackerNews User REST API methods
17 | enum User {
18 | case id(String)
19 |
20 | public var urlString: String {
21 | switch self {
22 | case .id(let userId):
23 | return "\(HackerNews.endpoint)/user/\(userId).json"
24 | }
25 | }
26 | }
27 |
28 | /// HackerNews Stories REST API methods
29 | enum Stories: String {
30 | case top
31 | case new
32 | case best
33 | case ask
34 | case job
35 | case show
36 |
37 | public var urlString: String {
38 | "\(HackerNews.endpoint)/\(self.rawValue)stories.json"
39 | }
40 | }
41 |
42 | /// HackerNews Item REST API methods
43 | enum Item {
44 | case id(Int)
45 |
46 | public var urlString: String {
47 | switch self {
48 | case .id(let storyId):
49 | return "\(HackerNews.endpoint)/item/\(storyId).json"
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/HNReader/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon_16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "icon_16x16@2x.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "icon_32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon_32x32@2x.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "icon_128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon_128x128@2x.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon_256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "icon_256x256@2x.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "icon_512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon_512x512@2x.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/HNReader/HNReaderApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HNReaderApp.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct HNReaderApp: App {
12 | let persistenceController = PersistenceController.shared
13 | let appState = AppState()
14 |
15 | private var displayModeBind: Binding {
16 | Binding(
17 | get: { appState.getColorScheme() },
18 | set: {
19 | appState.setColorScheme($0)
20 | displayMode = $0
21 | }
22 | )
23 | }
24 | @State var displayMode: ColorScheme?
25 |
26 | var body: some Scene {
27 | WindowGroup {
28 | HomeView()
29 | .frame(minWidth: 800, maxWidth: .infinity, minHeight: 500, maxHeight: .infinity)
30 | .onAppear {
31 | displayMode = appState.getColorScheme()
32 | }
33 | .preferredColorScheme(displayMode)
34 | .environmentObject(appState)
35 |
36 | }
37 |
38 | Settings {
39 | VStack {
40 | Form {
41 | Picker(selection: displayModeBind, label: Text("Theme")) {
42 | Text("Dark").tag(ColorScheme.dark)
43 | Text("Light").tag(ColorScheme.light)
44 | }
45 | .pickerStyle(SegmentedPickerStyle())
46 | .frame(maxWidth: 200)
47 | }
48 | }
49 | .frame(minHeight: 100)
50 | .frame(minWidth: 300)
51 | .preferredColorScheme(displayMode)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildMachineOSBuild
6 | 20F71
7 | CFBundleDevelopmentRegion
8 | en
9 | CFBundleExecutable
10 | HNReader
11 | CFBundleIconFile
12 | AppIcon
13 | CFBundleIconName
14 | AppIcon
15 | CFBundleIdentifier
16 | com.mattrighetti.HNReader
17 | CFBundleInfoDictionaryVersion
18 | 6.0
19 | CFBundleName
20 | HNReader
21 | CFBundlePackageType
22 | APPL
23 | CFBundleShortVersionString
24 | 1.0
25 | CFBundleSupportedPlatforms
26 |
27 | MacOSX
28 |
29 | CFBundleVersion
30 | 1
31 | DTCompiler
32 | com.apple.compilers.llvm.clang.1_0
33 | DTPlatformBuild
34 | 12E262
35 | DTPlatformName
36 | macosx
37 | DTPlatformVersion
38 | 11.3
39 | DTSDKBuild
40 | 20E214
41 | DTSDKName
42 | macosx11.3
43 | DTXcode
44 | 1250
45 | DTXcodeBuild
46 | 12E262
47 | LSApplicationCategoryType
48 | public.app-category.news
49 | LSMinimumSystemVersion
50 | 11.0
51 |
52 |
53 |
--------------------------------------------------------------------------------
/HNReader/Model/Item.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Item.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import Foundation
9 |
10 | /**
11 | Stories, comments, jobs, Ask HNs and even polls are items. They're identified by their ids, which are unique integers.
12 |
13 | For example, a story: https://hacker-news.firebaseio.com/v0/item/8863.json?print=pretty
14 | ```json
15 | {
16 | "by" : "dhouston",
17 | "descendants" : 71,
18 | "id" : 8863,
19 | "kids" : [ 8952, 9224, 8917, 8884, 8887, 8943, 8869, 8958, 9005, 9671, 8940, 9067, 8908, 9055, 8865, 8881, 8872, 8873, 8955, 10403, 8903, 8928, 9125, 8998, 8901, 8902, 8907, 8894, 8878, 8870, 8980, 8934, 8876 ],
20 | "score" : 111,
21 | "time" : 1175714200,
22 | "title" : "My YC app: Dropbox - Throw away your USB drive",
23 | "type" : "story",
24 | "url" : "http://www.getdropbox.com/u/2/screencast.html"
25 | }
26 | ```
27 | */
28 | public struct Item: Decodable {
29 | public let id: Int
30 | public let deleted: Bool?
31 | public let type: ItemType?
32 | public let by: String?
33 | public let time: Int?
34 | public let text: String?
35 | public let dead: Bool?
36 | public let parent: Int?
37 | public let poll: Bool?
38 | public let kids: [Int]?
39 | public let url: String?
40 | public let score: Int?
41 | public let title: String?
42 | public let parts: Int?
43 | public let descendants: Int?
44 |
45 | public var urlHost: String? {
46 | if let url = url {
47 | var hostString = URL(string: url)!.host!
48 | if hostString.contains("www.") {
49 | hostString.removeFirst(4)
50 | }
51 | return hostString
52 | } else {
53 | return nil
54 | }
55 | }
56 |
57 | public var scoreString: String? {
58 | guard let score = score else { return nil }
59 | return "\(score)"
60 | }
61 |
62 | public var timeStringRepresentation: String? {
63 | Date().timeElapsedStringRepresentation(since: Date(timeIntervalSince1970: TimeInterval(time!)))
64 | }
65 | }
66 |
67 | public enum ItemType: String, Decodable {
68 | case poll
69 | case job
70 | case story
71 | case comment
72 | case pollopt
73 | }
74 |
--------------------------------------------------------------------------------
/HNReader/View/InfoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoView.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 05/11/23.
6 | //
7 |
8 | import AppKit
9 | import StoreKit
10 | import SwiftUI
11 |
12 | struct InfoView: View {
13 | var version: String {
14 | let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
15 | /// Build version of the app, i.e. `64`
16 | let appBundleVersion: String = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
17 |
18 | return "\(version) (\(appBundleVersion))"
19 | }
20 |
21 | var body: some View {
22 | VStack {
23 | Text("HNReader v\(version)")
24 | .font(.custom("IBMPlexSerif-SemiBold", size: 15))
25 |
26 | Text("This project is open source, if you like it you can star it on GitHub.")
27 | .lineLimit(4)
28 | .font(.custom("IBMPlexSerif-Regular", size: 13))
29 | .foregroundStyle(.secondary)
30 | .multilineTextAlignment(.center)
31 | .padding(.vertical)
32 |
33 | BgButton(text: "Rate HNReader", icon: "star", minSize: CGSize(width: 250, height: 50), onHover: { h in
34 | DispatchQueue.main.async {
35 | if (h) {
36 | NSCursor.pointingHand.push()
37 | } else {
38 | NSCursor.pop()
39 | }
40 | }
41 | }, action: {
42 | NSWorkspace.shared.open(URL(string: "https://apps.apple.com/it/app/id1572480416?action=write-review")!)
43 | })
44 |
45 | BgButton(text: "Open on GitHub", icon: "arrow.up.right", minSize: CGSize(width: 250, height: 50), onHover: { h in
46 | DispatchQueue.main.async {
47 | if (h) {
48 | NSCursor.pointingHand.push()
49 | } else {
50 | NSCursor.pop()
51 | }
52 | }
53 | }, action: {
54 | NSWorkspace.shared.open(URL(string: "https://github.com/mattrighetti/HNReaderApp.git")!)
55 | })
56 | }
57 | .padding()
58 | .frame(width: 300)
59 | }
60 | }
61 |
62 | #Preview {
63 | InfoView()
64 | }
65 |
--------------------------------------------------------------------------------
/HNReader/View/ItemList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItemList.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import SwiftUI
9 | import OSLog
10 |
11 | struct ItemList: View {
12 | @EnvironmentObject var appState: AppState
13 | @StateObject var viewModel = ItemListViewModel()
14 | @State private var itemLimitSelection: Int = 1
15 | private var itemLimitOptions: [Int] = [25, 50, 100]
16 |
17 | var body: some View {
18 | List {
19 | ForEach(viewModel.storiesIds, id: \.self) { itemId in
20 | ItemCell(itemId: itemId)
21 | .listRowSeparator(.hidden)
22 | }
23 | }
24 | .onAppear {
25 | viewModel.currentNewsSelection = appState.newsSelection
26 | }
27 | .onChange(of: appState.newsSelection, {
28 | fetchItems(by: appState.newsSelection)
29 | })
30 | .toolbar {
31 | MaxItemPicker(enabled: false)
32 | Button(action: viewModel.refreshStories) {
33 | Label("Refresh news", systemImage: "arrow.counterclockwise.circle")
34 | }
35 | Button(action: {
36 | InfoView().openInWindow(title: "Info", sender: nil)
37 | }, label: {
38 | Label("Info", systemImage: "info.circle")
39 | })
40 | }
41 | .navigationTitle("Hacker News")
42 | }
43 |
44 | @ViewBuilder
45 | private func MaxItemPicker(enabled: Bool) -> some View {
46 | if enabled {
47 | Picker("Limit", selection: $itemLimitSelection) {
48 | ForEach(itemLimitOptions.indices, id: \.self) { index in
49 | Text("\(itemLimitOptions[index])")
50 | .tag(index)
51 | }
52 | }
53 | .pickerStyle(SegmentedPickerStyle())
54 | } else {
55 | EmptyView()
56 | }
57 | }
58 |
59 | private func fetchItems(by category: HackerNews.API.Stories) {
60 | if category != viewModel.currentNewsSelection {
61 | NSLog("changing category from \(viewModel.currentNewsSelection) to \(category)")
62 | viewModel.currentNewsSelection = category
63 | }
64 | }
65 | }
66 |
67 | #Preview {
68 | ItemList()
69 | }
70 |
--------------------------------------------------------------------------------
/HNReader/ViewModel/AppState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppState.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import Combine
9 | import SwiftUI
10 |
11 | class AppState: ObservableObject {
12 | @AppStorage("displayMode") var displayMode: DisplayMode = .system
13 | @Published var sidebarSelection: SidebarSelection? = SidebarSelection.top {
14 | willSet {
15 | switch newValue {
16 | case .top:
17 | newsSelection = .top
18 | case .ask:
19 | newsSelection = .ask
20 | case .show:
21 | newsSelection = .show
22 | case .best:
23 | newsSelection = .best
24 | case .new:
25 | newsSelection = .new
26 | case .job:
27 | newsSelection = .job
28 | default:
29 | break
30 | }
31 | }
32 | }
33 | @Published var newsSelection: HackerNews.API.Stories = .top
34 |
35 | func getColorScheme() -> ColorScheme {
36 | switch displayMode {
37 | case .dark: return .dark
38 | case .light: return .light
39 | case .system: return .dark
40 | }
41 | }
42 |
43 | func setColorScheme(_ colorScheme: ColorScheme) {
44 | switch colorScheme {
45 | case .dark:
46 | displayMode = .dark
47 | case .light:
48 | displayMode = .light
49 | @unknown default:
50 | break
51 | }
52 | }
53 |
54 | // Sidebar categories abstraction
55 | enum SidebarSelection: String, Equatable, CaseIterable {
56 | case top = "Top"
57 | case ask = "Ask"
58 | case show = "Show"
59 | case job = "Job"
60 | case best = "Best"
61 | case new = "Newest"
62 |
63 | var iconName: String {
64 | switch self {
65 | case .top: return "flame"
66 | case .ask: return "person.fill.questionmark"
67 | case .new: return "paperplane"
68 | case .show: return "eye.circle"
69 | case .best: return "rosette"
70 | case .job: return "briefcase"
71 | }
72 | }
73 | }
74 |
75 | enum DisplayMode: Int {
76 | case system = 0
77 | case dark = 1
78 | case light = 2
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/HNReader/Persistence.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Persistence.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import CoreData
9 |
10 | struct PersistenceController {
11 | static let shared = PersistenceController()
12 |
13 | static var preview: PersistenceController = {
14 | let result = PersistenceController(inMemory: true)
15 | let viewContext = result.container.viewContext
16 | do {
17 | try viewContext.save()
18 | } catch {
19 | // Replace this implementation with code to handle the error appropriately.
20 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
21 | let nsError = error as NSError
22 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
23 | }
24 | return result
25 | }()
26 |
27 | let container: NSPersistentContainer
28 |
29 | init(inMemory: Bool = false) {
30 | container = NSPersistentContainer(name: "HNReader")
31 | if inMemory {
32 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
33 | }
34 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in
35 | if let error = error as NSError? {
36 | // Replace this implementation with code to handle the error appropriately.
37 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
38 |
39 | /*
40 | Typical reasons for an error here include:
41 | * The parent directory does not exist, cannot be created, or disallows writing.
42 | * The persistent store is not accessible, due to permissions or data protection when the device is locked.
43 | * The device is out of space.
44 | * The store could not be migrated to the current model version.
45 | Check the error message to determine what the actual problem was.
46 | */
47 | fatalError("Unresolved error \(error), \(error.userInfo)")
48 | }
49 | })
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/HNReader/HNClient/HackerNewsClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HackerNewsClient.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | /// HackerNews Client that exposes methods to get users, items and stories from https://news.ycombinator.com
12 | class HackerNewsClient {
13 | public static let shared: HackerNewsClient = HackerNewsClient()
14 | private let session: URLSession = URLSession.shared
15 | private let decoder: JSONDecoder = JSONDecoder()
16 | private var subscriptions = Set()
17 |
18 | /// Retrieves user from HackerNews
19 | public func getUser(withId id: String) -> AnyPublisher {
20 | let url = URL(string: HackerNews.API.User.id(id).urlString)!
21 | return session
22 | .dataTaskPublisher(for: url)
23 | .retry(3)
24 | .map(\.data)
25 | .decode(type: User.self, decoder: decoder)
26 | .receive(on: DispatchQueue.main)
27 | .eraseToAnyPublisher()
28 | }
29 |
30 | /// Retrieves item from HackerNews
31 | public func getItem(withId id: Int) -> AnyPublisher- {
32 | let url = URL(string: HackerNews.API.Item.id(id).urlString)!
33 | return session
34 | .dataTaskPublisher(for: url)
35 | .retry(3)
36 | .map(\.data)
37 | .decode(type: Item.self, decoder: decoder)
38 | .eraseToAnyPublisher()
39 | }
40 |
41 | /// Retrieves array of items from HackerNews
42 | ///
43 | /// You can specify which kind of stories you would like to retrieve:
44 | /// - `top`
45 | /// - `best`
46 | /// - `new`
47 | public func getStoriesId(by api: HackerNews.API.Stories) -> AnyPublisher<[Int], Error> {
48 | let url = URL(string: api.urlString)!
49 | return session
50 | .dataTaskPublisher(for: url)
51 | .map(\.data)
52 | .decode(type: [Int].self, decoder: decoder)
53 | .eraseToAnyPublisher()
54 | }
55 |
56 | public func getStories(withIds ids: [Int]) -> AnyPublisher<[Item], Error> {
57 | ids.publisher
58 | .flatMap(getItem)
59 | .collect()
60 | .eraseToAnyPublisher()
61 | }
62 |
63 | public func getStories(by category: HackerNews.API.Stories, limit: Int? = 50) -> AnyPublisher<[Item], Error> {
64 | getStoriesId(by: category)
65 | .flatMap(getStories)
66 | .eraseToAnyPublisher()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/HNReaderTests/HNClientTests/HackerNewsClientTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Mattia Righetti on 12/06/21.
3 | //
4 |
5 | import XCTest
6 | import Combine
7 | @testable import HNReader
8 |
9 | class HackerNewsClientTests: XCTestCase {
10 | private var cancellables: Set!
11 |
12 | override func setUp() {
13 | super.setUp()
14 | cancellables = []
15 | }
16 |
17 | func testGetUser() throws {
18 | var user: User?
19 | var error: Error?
20 | let expectation = self.expectation(description: "userGet")
21 |
22 | HackerNewsClient.shared
23 | .getUser(withId: "mattrighetti")
24 | .sink(receiveCompletion: { completion in
25 | switch completion {
26 | case .finished:
27 | break
28 | case .failure(let err):
29 | error = err
30 | }
31 |
32 | expectation.fulfill()
33 | }, receiveValue: { usr in
34 | user = usr
35 | })
36 | .store(in: &cancellables)
37 |
38 | waitForExpectations(timeout: 5)
39 |
40 | XCTAssertNil(error)
41 | XCTAssertEqual(user!.id, "mattrighetti")
42 | }
43 |
44 | func testGetItem() throws {
45 | var item: Item?
46 | var error: Error?
47 | let expectation = self.expectation(description: "itemGet")
48 |
49 | HackerNewsClient.shared
50 | .getItem(withId: 27348900)
51 | .sink(receiveCompletion: { completion in
52 | switch completion {
53 | case .finished:
54 | break
55 | case .failure(let err):
56 | error = err
57 | }
58 |
59 | expectation.fulfill()
60 | }, receiveValue: { newItem in
61 | item = newItem
62 | })
63 | .store(in: &cancellables)
64 |
65 | waitForExpectations(timeout: 5)
66 |
67 | XCTAssertNil(error)
68 | XCTAssertNotNil(item)
69 | XCTAssertEqual(item!.id, 27348900)
70 | }
71 |
72 | func testGetTopStories() throws {
73 | var stories: [Item]?
74 | var error: Error?
75 | let expectation = self.expectation(description: "bestStoriesGet")
76 |
77 | HackerNewsClient.shared
78 | .getStories(by: .top, limit: 100)
79 | .sink(receiveCompletion: { completion in
80 | switch completion {
81 | case .finished:
82 | break
83 | case .failure(let err):
84 | error = err
85 | }
86 |
87 | expectation.fulfill()
88 | }, receiveValue: { items in
89 | stories = items
90 | })
91 | .store(in: &cancellables)
92 |
93 | waitForExpectations(timeout: 15)
94 |
95 | XCTAssertNil(error)
96 | XCTAssertNotNil(stories)
97 | print(stories)
98 | XCTAssertFalse(stories!.isEmpty)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/HNReader/View/HNReader/HNReader.app/Contents/_CodeSignature/CodeResources:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | files
6 |
7 | Resources/AppIcon.icns
8 |
9 | XuqNLC4a6bCVfG02ZjyF5oNNFks=
10 |
11 | Resources/Assets.car
12 |
13 | AOjNs8bov+COFYNU1MoTdNBymKY=
14 |
15 | Resources/HNReader.momd/HNReader.mom
16 |
17 | A5i6Glwsluin1g2ltov3tpUjbLM=
18 |
19 | Resources/HNReader.momd/HNReader.omo
20 |
21 | acxzJXINprFYwcy2DBxx01kTGZc=
22 |
23 | Resources/HNReader.momd/VersionInfo.plist
24 |
25 | caVpC1d/jd/uNi/pQzM4WAsxE60=
26 |
27 |
28 | files2
29 |
30 | Resources/AppIcon.icns
31 |
32 | hash2
33 |
34 | KlNaPoIIGS5Drw9lDCdN2L1VcRHQYkG472WCHnauBQg=
35 |
36 |
37 | Resources/Assets.car
38 |
39 | hash2
40 |
41 | ne33KSa9/FQA9oL3k4QVN+nBWIVtij3HzTNmLFnfZSw=
42 |
43 |
44 | Resources/HNReader.momd/HNReader.mom
45 |
46 | hash2
47 |
48 | J2b313nYGtCcc2LpxMYzwmp5/uYjYA2FeaKKTx36wfA=
49 |
50 |
51 | Resources/HNReader.momd/HNReader.omo
52 |
53 | hash2
54 |
55 | F3Dl+udfH8Ihjo0aD1CMms8lVQTUgKuhWDDTIR2HFb4=
56 |
57 |
58 | Resources/HNReader.momd/VersionInfo.plist
59 |
60 | hash2
61 |
62 | Z6nIstUOYh27AEP8TxmKktPZzMhjQfE91eP5zPHPQ8g=
63 |
64 |
65 |
66 | rules
67 |
68 | ^Resources/
69 |
70 | ^Resources/.*\.lproj/
71 |
72 | optional
73 |
74 | weight
75 | 1000
76 |
77 | ^Resources/.*\.lproj/locversion.plist$
78 |
79 | omit
80 |
81 | weight
82 | 1100
83 |
84 | ^Resources/Base\.lproj/
85 |
86 | weight
87 | 1010
88 |
89 | ^version.plist$
90 |
91 |
92 | rules2
93 |
94 | .*\.dSYM($|/)
95 |
96 | weight
97 | 11
98 |
99 | ^(.*/)?\.DS_Store$
100 |
101 | omit
102 |
103 | weight
104 | 2000
105 |
106 | ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/
107 |
108 | nested
109 |
110 | weight
111 | 10
112 |
113 | ^.*
114 |
115 | ^Info\.plist$
116 |
117 | omit
118 |
119 | weight
120 | 20
121 |
122 | ^PkgInfo$
123 |
124 | omit
125 |
126 | weight
127 | 20
128 |
129 | ^Resources/
130 |
131 | weight
132 | 20
133 |
134 | ^Resources/.*\.lproj/
135 |
136 | optional
137 |
138 | weight
139 | 1000
140 |
141 | ^Resources/.*\.lproj/locversion.plist$
142 |
143 | omit
144 |
145 | weight
146 | 1100
147 |
148 | ^Resources/Base\.lproj/
149 |
150 | weight
151 | 1010
152 |
153 | ^[^/]+$
154 |
155 | nested
156 |
157 | weight
158 | 10
159 |
160 | ^embedded\.provisionprofile$
161 |
162 | weight
163 | 20
164 |
165 | ^version\.plist$
166 |
167 | weight
168 | 20
169 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/HNReader/View/ItemCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ItemCell.swift
3 | // HNReader
4 | //
5 | // Created by Mattia Righetti on 12/06/21.
6 | //
7 |
8 | import SwiftUI
9 |
10 | func nullableString(_ s: String?) -> String {
11 | guard let s = s else { return "" }
12 | return s
13 | }
14 |
15 | struct ItemCell: View {
16 | var itemId: Int
17 | let itemDownloader: ItemDownloader
18 |
19 | @Environment(\.colorScheme) var colorScheme
20 | @State var item: Item?
21 |
22 | init(itemId: Int) {
23 | self.itemId = itemId
24 | itemDownloader = DefaultItemDownloader(itemId: itemId)
25 | }
26 |
27 | var body: some View {
28 | HStack {
29 | VStack(alignment: .leading, spacing: 5) {
30 | Text(item?.title ?? String(repeating: "-", count: 30))
31 | .font(.custom("IBMPlexSerif-Bold", size: 17))
32 | .redactIfNull(item)
33 |
34 | if let url = item?.urlHost {
35 | Text(url)
36 | .font(.custom("IBMPlexSerif-Light", size: 12))
37 | .redactIfNull(item)
38 | }
39 |
40 | HStack {
41 | Text(nullableString(item?.scoreString))
42 | .font(.custom("IBMPlexSerif-SemiBold", size: 12))
43 | .redactIfNull(item)
44 |
45 | Text("Posted by \(nullableString(item?.by))")
46 | .font(.custom("IBMPlexSerif-Regular", size: 12))
47 | .redactIfNull(item)
48 |
49 | Text("\(nullableString(item?.timeStringRepresentation))")
50 | .font(.custom("IBMPlexSerif-Regular", size: 12))
51 | .redactIfNull(item)
52 |
53 | Spacer()
54 | }
55 | }
56 |
57 | HStack {
58 | BgButton(icon: "bubble.left", minSize: CGSize(width: 50, height: 50), onHover: { isHovered in
59 | DispatchQueue.main.async {
60 | if (isHovered) {
61 | NSCursor.pointingHand.push()
62 | } else {
63 | NSCursor.pop()
64 | }
65 | }
66 | }, action: {
67 | if let item = item {
68 | guard let url = URL(string: "https://news.ycombinator.com/item?id=\(item.id)") else { return }
69 | NSWorkspace.shared.open(url)
70 | }
71 | })
72 |
73 | BgButton(icon: "arrow.up.right", disabled: item?.url == nil, minSize: CGSize(width: 50, height: 50), onHover: { isHovered in
74 | DispatchQueue.main.async {
75 | if (isHovered) {
76 | if item?.url == nil {
77 | NSCursor.operationNotAllowed.push()
78 | } else {
79 | NSCursor.pointingHand.push()
80 | }
81 | } else {
82 | NSCursor.pop()
83 | }
84 | }
85 | }, action: {
86 | guard let url = item?.url, let url = URL(string: url) else { return }
87 | NSWorkspace.shared.open(url)
88 | })
89 | }
90 | .padding(.leading)
91 | }
92 | .padding()
93 | .background(colorScheme == .dark ? Color.black.opacity(0.3) : Color.gray.opacity(0.1))
94 | .cornerRadius(10)
95 | .onAppear {
96 | if item == nil {
97 | fetchItem()
98 | }
99 | }
100 | }
101 |
102 | private func fetchItem() {
103 | let cacheKey = itemId
104 | if let cachedItem = ItemCache.shared.getItem(for: cacheKey) {
105 | self.item = cachedItem
106 | } else {
107 | itemDownloader.downloadItem(completion: { item in
108 | guard let item = item else { return }
109 | ItemCache.shared.cache(item, for: cacheKey)
110 | DispatchQueue.main.async {
111 | self.item = item
112 | }
113 | })
114 | }
115 | }
116 | }
117 |
118 | #Preview {
119 | ItemCell(itemId: 27492268)
120 | }
121 |
--------------------------------------------------------------------------------
/HNReader.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 3307109CEEE15ACDD7B88CFE /* HackerNewsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071E538EC434DF1A245518 /* HackerNewsClientTests.swift */; };
11 | 330710FDACC9DEEBEC56011F /* ItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071EEBE46634E658582AE3 /* ItemTests.swift */; };
12 | 330711A9216E762026AF98A0 /* +Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3307159309D438EFAA1259C7 /* +Date.swift */; };
13 | 330713D3016ED410AFD53FDF /* ItemListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3307147A30B9051D95FFFB20 /* ItemListViewModel.swift */; };
14 | 3307147AB95F03650FC40B97 /* ItemCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071CD8B451FE700B64F387 /* ItemCache.swift */; };
15 | 330718D415D21296AA14E7CA /* HackerNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071291447141A6D31E671B /* HackerNewsTests.swift */; };
16 | 330719203034BDB177F28C41 /* +DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071B0E5439D8D207CB68F4 /* +DateTests.swift */; };
17 | 33071F1C64D4742E1F947FAA /* ItemDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33071D0E5913DB91DDDBDADB /* ItemDownloader.swift */; };
18 | 5F109D592AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */; };
19 | 5F109D5F2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5F109D5B2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf */; };
20 | 5F109D602AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5F109D5C2AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf */; };
21 | 5F109D612AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5F109D5D2AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf */; };
22 | 5F109D622AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5F109D5E2AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf */; };
23 | 5FCE20A72AF7B25A00BF4097 /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCE20A62AF7B25A00BF4097 /* InfoView.swift */; };
24 | 5FCE20A92AF7B47B00BF4097 /* BgButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCE20A82AF7B47B00BF4097 /* BgButton.swift */; };
25 | 5FCE20AB2AF7B8EE00BF4097 /* +View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCE20AA2AF7B8EE00BF4097 /* +View.swift */; };
26 | C93F99B6267554F00046F870 /* ItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B5267554F00046F870 /* ItemCell.swift */; };
27 | C93F99B8267557FC0046F870 /* ItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B7267557FC0046F870 /* ItemList.swift */; };
28 | C93F99BA267580CE0046F870 /* HTMLText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C93F99B9267580CE0046F870 /* HTMLText.swift */; };
29 | C9D0937726741BBE002CC786 /* HNReaderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0937626741BBE002CC786 /* HNReaderApp.swift */; };
30 | C9D0937926741BBE002CC786 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0937826741BBE002CC786 /* HomeView.swift */; };
31 | C9D0937B26741BBF002CC786 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9D0937A26741BBF002CC786 /* Assets.xcassets */; };
32 | C9D0937E26741BBF002CC786 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9D0937D26741BBF002CC786 /* Preview Assets.xcassets */; };
33 | C9D0938026741BBF002CC786 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0937F26741BBF002CC786 /* Persistence.swift */; };
34 | C9D093AC26741C25002CC786 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D093AB26741C25002CC786 /* Item.swift */; };
35 | C9E9BCFD2674C80E001B4E19 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E9BCFC2674C80E001B4E19 /* AppState.swift */; };
36 | C9E9BCFF2674CB6C001B4E19 /* HackerNewsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E9BCFE2674CB6C001B4E19 /* HackerNewsClient.swift */; };
37 | C9E9BD012674D007001B4E19 /* HackerNews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E9BD002674D007001B4E19 /* HackerNews.swift */; };
38 | C9E9BD032674D095001B4E19 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E9BD022674D095001B4E19 /* User.swift */; };
39 | /* End PBXBuildFile section */
40 |
41 | /* Begin PBXContainerItemProxy section */
42 | C9D0938B26741BC0002CC786 /* PBXContainerItemProxy */ = {
43 | isa = PBXContainerItemProxy;
44 | containerPortal = C9D0936B26741BBE002CC786 /* Project object */;
45 | proxyType = 1;
46 | remoteGlobalIDString = C9D0937226741BBE002CC786;
47 | remoteInfo = HNReader;
48 | };
49 | /* End PBXContainerItemProxy section */
50 |
51 | /* Begin PBXFileReference section */
52 | 33071291447141A6D31E671B /* HackerNewsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HackerNewsTests.swift; sourceTree = ""; };
53 | 3307147A30B9051D95FFFB20 /* ItemListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListViewModel.swift; sourceTree = ""; };
54 | 3307159309D438EFAA1259C7 /* +Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "+Date.swift"; sourceTree = ""; };
55 | 33071B0E5439D8D207CB68F4 /* +DateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "+DateTests.swift"; sourceTree = ""; };
56 | 33071CD8B451FE700B64F387 /* ItemCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCache.swift; sourceTree = ""; };
57 | 33071D0E5913DB91DDDBDADB /* ItemDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemDownloader.swift; sourceTree = ""; };
58 | 33071E538EC434DF1A245518 /* HackerNewsClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HackerNewsClientTests.swift; sourceTree = ""; };
59 | 33071EEBE46634E658582AE3 /* ItemTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemTests.swift; sourceTree = ""; };
60 | 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalRedactedModifier.swift; sourceTree = ""; };
61 | 5F109D5B2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSerif-Light.ttf"; sourceTree = ""; };
62 | 5F109D5C2AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSerif-Bold.ttf"; sourceTree = ""; };
63 | 5F109D5D2AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSerif-Regular.ttf"; sourceTree = ""; };
64 | 5F109D5E2AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSerif-SemiBold.ttf"; sourceTree = ""; };
65 | 5FCE20A62AF7B25A00BF4097 /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; };
66 | 5FCE20A82AF7B47B00BF4097 /* BgButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BgButton.swift; sourceTree = ""; };
67 | 5FCE20AA2AF7B8EE00BF4097 /* +View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "+View.swift"; sourceTree = ""; };
68 | C93F99B5267554F00046F870 /* ItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCell.swift; sourceTree = ""; };
69 | C93F99B7267557FC0046F870 /* ItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemList.swift; sourceTree = ""; };
70 | C93F99B9267580CE0046F870 /* HTMLText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLText.swift; sourceTree = ""; };
71 | C9D0937326741BBE002CC786 /* HNReader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HNReader.app; sourceTree = BUILT_PRODUCTS_DIR; };
72 | C9D0937626741BBE002CC786 /* HNReaderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HNReaderApp.swift; sourceTree = ""; };
73 | C9D0937826741BBE002CC786 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; };
74 | C9D0937A26741BBF002CC786 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
75 | C9D0937D26741BBF002CC786 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
76 | C9D0937F26741BBF002CC786 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; };
77 | C9D0938426741BBF002CC786 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
78 | C9D0938526741BBF002CC786 /* HNReader.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HNReader.entitlements; sourceTree = ""; };
79 | C9D0938A26741BC0002CC786 /* HNReaderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HNReaderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
80 | C9D0939026741BC0002CC786 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
81 | C9D093AB26741C25002CC786 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; };
82 | C9E9BCFC2674C80E001B4E19 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; };
83 | C9E9BCFE2674CB6C001B4E19 /* HackerNewsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackerNewsClient.swift; sourceTree = ""; };
84 | C9E9BD002674D007001B4E19 /* HackerNews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackerNews.swift; sourceTree = ""; };
85 | C9E9BD022674D095001B4E19 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
86 | /* End PBXFileReference section */
87 |
88 | /* Begin PBXFrameworksBuildPhase section */
89 | C9D0937026741BBE002CC786 /* Frameworks */ = {
90 | isa = PBXFrameworksBuildPhase;
91 | buildActionMask = 2147483647;
92 | files = (
93 | );
94 | runOnlyForDeploymentPostprocessing = 0;
95 | };
96 | C9D0938726741BC0002CC786 /* Frameworks */ = {
97 | isa = PBXFrameworksBuildPhase;
98 | buildActionMask = 2147483647;
99 | files = (
100 | );
101 | runOnlyForDeploymentPostprocessing = 0;
102 | };
103 | /* End PBXFrameworksBuildPhase section */
104 |
105 | /* Begin PBXGroup section */
106 | 3307153CA4CBEF847DCF967F /* Utils */ = {
107 | isa = PBXGroup;
108 | children = (
109 | 3307159309D438EFAA1259C7 /* +Date.swift */,
110 | 5FCE20AA2AF7B8EE00BF4097 /* +View.swift */,
111 | );
112 | path = Utils;
113 | sourceTree = "";
114 | };
115 | 330716F7289F75F50DB30273 /* UtilsTests */ = {
116 | isa = PBXGroup;
117 | children = (
118 | 33071B0E5439D8D207CB68F4 /* +DateTests.swift */,
119 | );
120 | path = UtilsTests;
121 | sourceTree = "";
122 | };
123 | 33071AE1286F3A139C23E0A9 /* HNClientTests */ = {
124 | isa = PBXGroup;
125 | children = (
126 | 33071E538EC434DF1A245518 /* HackerNewsClientTests.swift */,
127 | 33071291447141A6D31E671B /* HackerNewsTests.swift */,
128 | );
129 | path = HNClientTests;
130 | sourceTree = "";
131 | };
132 | 33071C911DD94AB17E409FD7 /* ModelTests */ = {
133 | isa = PBXGroup;
134 | children = (
135 | 33071EEBE46634E658582AE3 /* ItemTests.swift */,
136 | );
137 | path = ModelTests;
138 | sourceTree = "";
139 | };
140 | 5F96E4AB2AF70C2C00593AD6 /* fonts */ = {
141 | isa = PBXGroup;
142 | children = (
143 | 5F109D5C2AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf */,
144 | 5F109D5B2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf */,
145 | 5F109D5D2AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf */,
146 | 5F109D5E2AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf */,
147 | );
148 | path = fonts;
149 | sourceTree = "";
150 | };
151 | C9926691267588B80035A88F /* Components */ = {
152 | isa = PBXGroup;
153 | children = (
154 | C93F99B9267580CE0046F870 /* HTMLText.swift */,
155 | 5FCE20A82AF7B47B00BF4097 /* BgButton.swift */,
156 | );
157 | path = Components;
158 | sourceTree = "";
159 | };
160 | C9D0936A26741BBE002CC786 = {
161 | isa = PBXGroup;
162 | children = (
163 | C9D0937526741BBE002CC786 /* HNReader */,
164 | C9D0938D26741BC0002CC786 /* HNReaderTests */,
165 | C9D0937426741BBE002CC786 /* Products */,
166 | );
167 | sourceTree = "";
168 | };
169 | C9D0937426741BBE002CC786 /* Products */ = {
170 | isa = PBXGroup;
171 | children = (
172 | C9D0937326741BBE002CC786 /* HNReader.app */,
173 | C9D0938A26741BC0002CC786 /* HNReaderTests.xctest */,
174 | );
175 | name = Products;
176 | sourceTree = "";
177 | };
178 | C9D0937526741BBE002CC786 /* HNReader */ = {
179 | isa = PBXGroup;
180 | children = (
181 | 5F96E4AB2AF70C2C00593AD6 /* fonts */,
182 | C9D093AA26741BFD002CC786 /* HNClient */,
183 | C9D093A926741BF6002CC786 /* ViewModel */,
184 | C9D093A826741BF0002CC786 /* Model */,
185 | C9D093A726741BE1002CC786 /* View */,
186 | C9D0937626741BBE002CC786 /* HNReaderApp.swift */,
187 | C9D0937A26741BBF002CC786 /* Assets.xcassets */,
188 | C9D0937F26741BBF002CC786 /* Persistence.swift */,
189 | C9D0938426741BBF002CC786 /* Info.plist */,
190 | C9D0938526741BBF002CC786 /* HNReader.entitlements */,
191 | C9D0937C26741BBF002CC786 /* Preview Content */,
192 | 3307153CA4CBEF847DCF967F /* Utils */,
193 | );
194 | path = HNReader;
195 | sourceTree = "";
196 | };
197 | C9D0937C26741BBF002CC786 /* Preview Content */ = {
198 | isa = PBXGroup;
199 | children = (
200 | C9D0937D26741BBF002CC786 /* Preview Assets.xcassets */,
201 | );
202 | path = "Preview Content";
203 | sourceTree = "";
204 | };
205 | C9D0938D26741BC0002CC786 /* HNReaderTests */ = {
206 | isa = PBXGroup;
207 | children = (
208 | C9D0939026741BC0002CC786 /* Info.plist */,
209 | 33071AE1286F3A139C23E0A9 /* HNClientTests */,
210 | 330716F7289F75F50DB30273 /* UtilsTests */,
211 | 33071C911DD94AB17E409FD7 /* ModelTests */,
212 | );
213 | path = HNReaderTests;
214 | sourceTree = "";
215 | };
216 | C9D093A726741BE1002CC786 /* View */ = {
217 | isa = PBXGroup;
218 | children = (
219 | C9D0937826741BBE002CC786 /* HomeView.swift */,
220 | C93F99B5267554F00046F870 /* ItemCell.swift */,
221 | C93F99B7267557FC0046F870 /* ItemList.swift */,
222 | C9926691267588B80035A88F /* Components */,
223 | 5F109D582AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift */,
224 | 5FCE20A62AF7B25A00BF4097 /* InfoView.swift */,
225 | );
226 | path = View;
227 | sourceTree = "";
228 | };
229 | C9D093A826741BF0002CC786 /* Model */ = {
230 | isa = PBXGroup;
231 | children = (
232 | C9D093AB26741C25002CC786 /* Item.swift */,
233 | C9E9BD022674D095001B4E19 /* User.swift */,
234 | );
235 | path = Model;
236 | sourceTree = "";
237 | };
238 | C9D093A926741BF6002CC786 /* ViewModel */ = {
239 | isa = PBXGroup;
240 | children = (
241 | C9E9BCFC2674C80E001B4E19 /* AppState.swift */,
242 | 3307147A30B9051D95FFFB20 /* ItemListViewModel.swift */,
243 | );
244 | path = ViewModel;
245 | sourceTree = "";
246 | };
247 | C9D093AA26741BFD002CC786 /* HNClient */ = {
248 | isa = PBXGroup;
249 | children = (
250 | C9E9BCFE2674CB6C001B4E19 /* HackerNewsClient.swift */,
251 | C9E9BD002674D007001B4E19 /* HackerNews.swift */,
252 | 33071D0E5913DB91DDDBDADB /* ItemDownloader.swift */,
253 | 33071CD8B451FE700B64F387 /* ItemCache.swift */,
254 | );
255 | path = HNClient;
256 | sourceTree = "";
257 | };
258 | /* End PBXGroup section */
259 |
260 | /* Begin PBXNativeTarget section */
261 | C9D0937226741BBE002CC786 /* HNReader */ = {
262 | isa = PBXNativeTarget;
263 | buildConfigurationList = C9D0939E26741BC0002CC786 /* Build configuration list for PBXNativeTarget "HNReader" */;
264 | buildPhases = (
265 | C9D0936F26741BBE002CC786 /* Sources */,
266 | C9D0937026741BBE002CC786 /* Frameworks */,
267 | C9D0937126741BBE002CC786 /* Resources */,
268 | );
269 | buildRules = (
270 | );
271 | dependencies = (
272 | );
273 | name = HNReader;
274 | packageProductDependencies = (
275 | );
276 | productName = HNReader;
277 | productReference = C9D0937326741BBE002CC786 /* HNReader.app */;
278 | productType = "com.apple.product-type.application";
279 | };
280 | C9D0938926741BC0002CC786 /* HNReaderTests */ = {
281 | isa = PBXNativeTarget;
282 | buildConfigurationList = C9D093A126741BC0002CC786 /* Build configuration list for PBXNativeTarget "HNReaderTests" */;
283 | buildPhases = (
284 | C9D0938626741BC0002CC786 /* Sources */,
285 | C9D0938726741BC0002CC786 /* Frameworks */,
286 | C9D0938826741BC0002CC786 /* Resources */,
287 | );
288 | buildRules = (
289 | );
290 | dependencies = (
291 | C9D0938C26741BC0002CC786 /* PBXTargetDependency */,
292 | );
293 | name = HNReaderTests;
294 | productName = HNReaderTests;
295 | productReference = C9D0938A26741BC0002CC786 /* HNReaderTests.xctest */;
296 | productType = "com.apple.product-type.bundle.unit-test";
297 | };
298 | /* End PBXNativeTarget section */
299 |
300 | /* Begin PBXProject section */
301 | C9D0936B26741BBE002CC786 /* Project object */ = {
302 | isa = PBXProject;
303 | attributes = {
304 | BuildIndependentTargetsInParallel = YES;
305 | LastSwiftUpdateCheck = 1250;
306 | LastUpgradeCheck = 1510;
307 | TargetAttributes = {
308 | C9D0937226741BBE002CC786 = {
309 | CreatedOnToolsVersion = 12.5;
310 | };
311 | C9D0938926741BC0002CC786 = {
312 | CreatedOnToolsVersion = 12.5;
313 | TestTargetID = C9D0937226741BBE002CC786;
314 | };
315 | };
316 | };
317 | buildConfigurationList = C9D0936E26741BBE002CC786 /* Build configuration list for PBXProject "HNReader" */;
318 | compatibilityVersion = "Xcode 9.3";
319 | developmentRegion = en;
320 | hasScannedForEncodings = 0;
321 | knownRegions = (
322 | en,
323 | Base,
324 | );
325 | mainGroup = C9D0936A26741BBE002CC786;
326 | packageReferences = (
327 | );
328 | productRefGroup = C9D0937426741BBE002CC786 /* Products */;
329 | projectDirPath = "";
330 | projectRoot = "";
331 | targets = (
332 | C9D0937226741BBE002CC786 /* HNReader */,
333 | C9D0938926741BC0002CC786 /* HNReaderTests */,
334 | );
335 | };
336 | /* End PBXProject section */
337 |
338 | /* Begin PBXResourcesBuildPhase section */
339 | C9D0937126741BBE002CC786 /* Resources */ = {
340 | isa = PBXResourcesBuildPhase;
341 | buildActionMask = 2147483647;
342 | files = (
343 | C9D0937E26741BBF002CC786 /* Preview Assets.xcassets in Resources */,
344 | 5F109D612AF704F700AE6AF3 /* IBMPlexSerif-Regular.ttf in Resources */,
345 | 5F109D602AF704F700AE6AF3 /* IBMPlexSerif-Bold.ttf in Resources */,
346 | 5F109D622AF704F700AE6AF3 /* IBMPlexSerif-SemiBold.ttf in Resources */,
347 | C9D0937B26741BBF002CC786 /* Assets.xcassets in Resources */,
348 | 5F109D5F2AF704F700AE6AF3 /* IBMPlexSerif-Light.ttf in Resources */,
349 | );
350 | runOnlyForDeploymentPostprocessing = 0;
351 | };
352 | C9D0938826741BC0002CC786 /* Resources */ = {
353 | isa = PBXResourcesBuildPhase;
354 | buildActionMask = 2147483647;
355 | files = (
356 | );
357 | runOnlyForDeploymentPostprocessing = 0;
358 | };
359 | /* End PBXResourcesBuildPhase section */
360 |
361 | /* Begin PBXSourcesBuildPhase section */
362 | C9D0936F26741BBE002CC786 /* Sources */ = {
363 | isa = PBXSourcesBuildPhase;
364 | buildActionMask = 2147483647;
365 | files = (
366 | C9D093AC26741C25002CC786 /* Item.swift in Sources */,
367 | C9D0938026741BBF002CC786 /* Persistence.swift in Sources */,
368 | C9D0937926741BBE002CC786 /* HomeView.swift in Sources */,
369 | C93F99B6267554F00046F870 /* ItemCell.swift in Sources */,
370 | 5FCE20A92AF7B47B00BF4097 /* BgButton.swift in Sources */,
371 | 5FCE20A72AF7B25A00BF4097 /* InfoView.swift in Sources */,
372 | 5F109D592AF6F50D00AE6AF3 /* ConditionalRedactedModifier.swift in Sources */,
373 | C93F99B8267557FC0046F870 /* ItemList.swift in Sources */,
374 | C9E9BCFD2674C80E001B4E19 /* AppState.swift in Sources */,
375 | C9E9BD032674D095001B4E19 /* User.swift in Sources */,
376 | C93F99BA267580CE0046F870 /* HTMLText.swift in Sources */,
377 | C9E9BCFF2674CB6C001B4E19 /* HackerNewsClient.swift in Sources */,
378 | C9E9BD012674D007001B4E19 /* HackerNews.swift in Sources */,
379 | C9D0937726741BBE002CC786 /* HNReaderApp.swift in Sources */,
380 | 330713D3016ED410AFD53FDF /* ItemListViewModel.swift in Sources */,
381 | 330711A9216E762026AF98A0 /* +Date.swift in Sources */,
382 | 33071F1C64D4742E1F947FAA /* ItemDownloader.swift in Sources */,
383 | 5FCE20AB2AF7B8EE00BF4097 /* +View.swift in Sources */,
384 | 3307147AB95F03650FC40B97 /* ItemCache.swift in Sources */,
385 | );
386 | runOnlyForDeploymentPostprocessing = 0;
387 | };
388 | C9D0938626741BC0002CC786 /* Sources */ = {
389 | isa = PBXSourcesBuildPhase;
390 | buildActionMask = 2147483647;
391 | files = (
392 | 3307109CEEE15ACDD7B88CFE /* HackerNewsClientTests.swift in Sources */,
393 | 330718D415D21296AA14E7CA /* HackerNewsTests.swift in Sources */,
394 | 330719203034BDB177F28C41 /* +DateTests.swift in Sources */,
395 | 330710FDACC9DEEBEC56011F /* ItemTests.swift in Sources */,
396 | );
397 | runOnlyForDeploymentPostprocessing = 0;
398 | };
399 | /* End PBXSourcesBuildPhase section */
400 |
401 | /* Begin PBXTargetDependency section */
402 | C9D0938C26741BC0002CC786 /* PBXTargetDependency */ = {
403 | isa = PBXTargetDependency;
404 | target = C9D0937226741BBE002CC786 /* HNReader */;
405 | targetProxy = C9D0938B26741BC0002CC786 /* PBXContainerItemProxy */;
406 | };
407 | /* End PBXTargetDependency section */
408 |
409 | /* Begin XCBuildConfiguration section */
410 | C9D0939C26741BC0002CC786 /* Debug */ = {
411 | isa = XCBuildConfiguration;
412 | buildSettings = {
413 | ALWAYS_SEARCH_USER_PATHS = NO;
414 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
415 | CLANG_ANALYZER_NONNULL = YES;
416 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
417 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
418 | CLANG_CXX_LIBRARY = "libc++";
419 | CLANG_ENABLE_MODULES = YES;
420 | CLANG_ENABLE_OBJC_ARC = YES;
421 | CLANG_ENABLE_OBJC_WEAK = YES;
422 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
423 | CLANG_WARN_BOOL_CONVERSION = YES;
424 | CLANG_WARN_COMMA = YES;
425 | CLANG_WARN_CONSTANT_CONVERSION = YES;
426 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
427 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
428 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
429 | CLANG_WARN_EMPTY_BODY = YES;
430 | CLANG_WARN_ENUM_CONVERSION = YES;
431 | CLANG_WARN_INFINITE_RECURSION = YES;
432 | CLANG_WARN_INT_CONVERSION = YES;
433 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
434 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
435 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
436 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
437 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
438 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
439 | CLANG_WARN_STRICT_PROTOTYPES = YES;
440 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
441 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
442 | CLANG_WARN_UNREACHABLE_CODE = YES;
443 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
444 | COPY_PHASE_STRIP = NO;
445 | DEAD_CODE_STRIPPING = YES;
446 | DEBUG_INFORMATION_FORMAT = dwarf;
447 | ENABLE_STRICT_OBJC_MSGSEND = YES;
448 | ENABLE_TESTABILITY = YES;
449 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
450 | GCC_C_LANGUAGE_STANDARD = gnu11;
451 | GCC_DYNAMIC_NO_PIC = NO;
452 | GCC_NO_COMMON_BLOCKS = YES;
453 | GCC_OPTIMIZATION_LEVEL = 0;
454 | GCC_PREPROCESSOR_DEFINITIONS = (
455 | "DEBUG=1",
456 | "$(inherited)",
457 | );
458 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
459 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
460 | GCC_WARN_UNDECLARED_SELECTOR = YES;
461 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
462 | GCC_WARN_UNUSED_FUNCTION = YES;
463 | GCC_WARN_UNUSED_VARIABLE = YES;
464 | MACOSX_DEPLOYMENT_TARGET = 14.0;
465 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
466 | MTL_FAST_MATH = YES;
467 | ONLY_ACTIVE_ARCH = YES;
468 | SDKROOT = macosx;
469 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
470 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
471 | };
472 | name = Debug;
473 | };
474 | C9D0939D26741BC0002CC786 /* Release */ = {
475 | isa = XCBuildConfiguration;
476 | buildSettings = {
477 | ALWAYS_SEARCH_USER_PATHS = NO;
478 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
479 | CLANG_ANALYZER_NONNULL = YES;
480 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
481 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
482 | CLANG_CXX_LIBRARY = "libc++";
483 | CLANG_ENABLE_MODULES = YES;
484 | CLANG_ENABLE_OBJC_ARC = YES;
485 | CLANG_ENABLE_OBJC_WEAK = YES;
486 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
487 | CLANG_WARN_BOOL_CONVERSION = YES;
488 | CLANG_WARN_COMMA = YES;
489 | CLANG_WARN_CONSTANT_CONVERSION = YES;
490 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
491 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
492 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
493 | CLANG_WARN_EMPTY_BODY = YES;
494 | CLANG_WARN_ENUM_CONVERSION = YES;
495 | CLANG_WARN_INFINITE_RECURSION = YES;
496 | CLANG_WARN_INT_CONVERSION = YES;
497 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
498 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
499 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
500 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
501 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
502 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
503 | CLANG_WARN_STRICT_PROTOTYPES = YES;
504 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
505 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
506 | CLANG_WARN_UNREACHABLE_CODE = YES;
507 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
508 | COPY_PHASE_STRIP = NO;
509 | DEAD_CODE_STRIPPING = YES;
510 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
511 | ENABLE_NS_ASSERTIONS = NO;
512 | ENABLE_STRICT_OBJC_MSGSEND = YES;
513 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
514 | GCC_C_LANGUAGE_STANDARD = gnu11;
515 | GCC_NO_COMMON_BLOCKS = YES;
516 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
517 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
518 | GCC_WARN_UNDECLARED_SELECTOR = YES;
519 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
520 | GCC_WARN_UNUSED_FUNCTION = YES;
521 | GCC_WARN_UNUSED_VARIABLE = YES;
522 | MACOSX_DEPLOYMENT_TARGET = 14.0;
523 | MTL_ENABLE_DEBUG_INFO = NO;
524 | MTL_FAST_MATH = YES;
525 | SDKROOT = macosx;
526 | SWIFT_COMPILATION_MODE = wholemodule;
527 | SWIFT_OPTIMIZATION_LEVEL = "-O";
528 | };
529 | name = Release;
530 | };
531 | C9D0939F26741BC0002CC786 /* Debug */ = {
532 | isa = XCBuildConfiguration;
533 | buildSettings = {
534 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
535 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
536 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
537 | CODE_SIGN_ENTITLEMENTS = HNReader/HNReader.entitlements;
538 | CODE_SIGN_IDENTITY = "Apple Development";
539 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
540 | CODE_SIGN_STYLE = Automatic;
541 | COMBINE_HIDPI_IMAGES = YES;
542 | CURRENT_PROJECT_VERSION = 1.3;
543 | DEAD_CODE_STRIPPING = YES;
544 | DEVELOPMENT_ASSET_PATHS = "\"HNReader/Preview Content\"";
545 | DEVELOPMENT_TEAM = H89RFW5UZ6;
546 | ENABLE_HARDENED_RUNTIME = YES;
547 | ENABLE_PREVIEWS = YES;
548 | INFOPLIST_FILE = HNReader/Info.plist;
549 | INFOPLIST_KEY_CFBundleDisplayName = HNReader;
550 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
551 | LD_RUNPATH_SEARCH_PATHS = (
552 | "$(inherited)",
553 | "@executable_path/../Frameworks",
554 | );
555 | MACOSX_DEPLOYMENT_TARGET = 14.0;
556 | MARKETING_VERSION = 1.3;
557 | PRODUCT_BUNDLE_IDENTIFIER = com.mattrighetti.HNReader;
558 | PRODUCT_NAME = "$(TARGET_NAME)";
559 | PROVISIONING_PROFILE_SPECIFIER = "";
560 | SWIFT_VERSION = 5.0;
561 | };
562 | name = Debug;
563 | };
564 | C9D093A026741BC0002CC786 /* Release */ = {
565 | isa = XCBuildConfiguration;
566 | buildSettings = {
567 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
568 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
569 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
570 | CODE_SIGN_ENTITLEMENTS = HNReader/HNReader.entitlements;
571 | CODE_SIGN_IDENTITY = "Apple Development";
572 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
573 | CODE_SIGN_STYLE = Automatic;
574 | COMBINE_HIDPI_IMAGES = YES;
575 | CURRENT_PROJECT_VERSION = 1.3;
576 | DEAD_CODE_STRIPPING = YES;
577 | DEVELOPMENT_ASSET_PATHS = "\"HNReader/Preview Content\"";
578 | DEVELOPMENT_TEAM = H89RFW5UZ6;
579 | ENABLE_HARDENED_RUNTIME = YES;
580 | ENABLE_PREVIEWS = YES;
581 | INFOPLIST_FILE = HNReader/Info.plist;
582 | INFOPLIST_KEY_CFBundleDisplayName = HNReader;
583 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
584 | LD_RUNPATH_SEARCH_PATHS = (
585 | "$(inherited)",
586 | "@executable_path/../Frameworks",
587 | );
588 | MACOSX_DEPLOYMENT_TARGET = 14.0;
589 | MARKETING_VERSION = 1.3;
590 | PRODUCT_BUNDLE_IDENTIFIER = com.mattrighetti.HNReader;
591 | PRODUCT_NAME = "$(TARGET_NAME)";
592 | PROVISIONING_PROFILE_SPECIFIER = "";
593 | SWIFT_VERSION = 5.0;
594 | };
595 | name = Release;
596 | };
597 | C9D093A226741BC0002CC786 /* Debug */ = {
598 | isa = XCBuildConfiguration;
599 | buildSettings = {
600 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
601 | BUNDLE_LOADER = "$(TEST_HOST)";
602 | CODE_SIGN_STYLE = Automatic;
603 | COMBINE_HIDPI_IMAGES = YES;
604 | DEAD_CODE_STRIPPING = YES;
605 | DEVELOPMENT_TEAM = H89RFW5UZ6;
606 | INFOPLIST_FILE = HNReaderTests/Info.plist;
607 | LD_RUNPATH_SEARCH_PATHS = (
608 | "$(inherited)",
609 | "@executable_path/../Frameworks",
610 | "@loader_path/../Frameworks",
611 | );
612 | MACOSX_DEPLOYMENT_TARGET = 14.0;
613 | PRODUCT_BUNDLE_IDENTIFIER = com.mattrighetti.HNReaderTests;
614 | PRODUCT_NAME = "$(TARGET_NAME)";
615 | SWIFT_VERSION = 5.0;
616 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HNReader.app/Contents/MacOS/HNReader";
617 | };
618 | name = Debug;
619 | };
620 | C9D093A326741BC0002CC786 /* Release */ = {
621 | isa = XCBuildConfiguration;
622 | buildSettings = {
623 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
624 | BUNDLE_LOADER = "$(TEST_HOST)";
625 | CODE_SIGN_STYLE = Automatic;
626 | COMBINE_HIDPI_IMAGES = YES;
627 | DEAD_CODE_STRIPPING = YES;
628 | DEVELOPMENT_TEAM = H89RFW5UZ6;
629 | INFOPLIST_FILE = HNReaderTests/Info.plist;
630 | LD_RUNPATH_SEARCH_PATHS = (
631 | "$(inherited)",
632 | "@executable_path/../Frameworks",
633 | "@loader_path/../Frameworks",
634 | );
635 | MACOSX_DEPLOYMENT_TARGET = 14.0;
636 | PRODUCT_BUNDLE_IDENTIFIER = com.mattrighetti.HNReaderTests;
637 | PRODUCT_NAME = "$(TARGET_NAME)";
638 | SWIFT_VERSION = 5.0;
639 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HNReader.app/Contents/MacOS/HNReader";
640 | };
641 | name = Release;
642 | };
643 | /* End XCBuildConfiguration section */
644 |
645 | /* Begin XCConfigurationList section */
646 | C9D0936E26741BBE002CC786 /* Build configuration list for PBXProject "HNReader" */ = {
647 | isa = XCConfigurationList;
648 | buildConfigurations = (
649 | C9D0939C26741BC0002CC786 /* Debug */,
650 | C9D0939D26741BC0002CC786 /* Release */,
651 | );
652 | defaultConfigurationIsVisible = 0;
653 | defaultConfigurationName = Release;
654 | };
655 | C9D0939E26741BC0002CC786 /* Build configuration list for PBXNativeTarget "HNReader" */ = {
656 | isa = XCConfigurationList;
657 | buildConfigurations = (
658 | C9D0939F26741BC0002CC786 /* Debug */,
659 | C9D093A026741BC0002CC786 /* Release */,
660 | );
661 | defaultConfigurationIsVisible = 0;
662 | defaultConfigurationName = Release;
663 | };
664 | C9D093A126741BC0002CC786 /* Build configuration list for PBXNativeTarget "HNReaderTests" */ = {
665 | isa = XCConfigurationList;
666 | buildConfigurations = (
667 | C9D093A226741BC0002CC786 /* Debug */,
668 | C9D093A326741BC0002CC786 /* Release */,
669 | );
670 | defaultConfigurationIsVisible = 0;
671 | defaultConfigurationName = Release;
672 | };
673 | /* End XCConfigurationList section */
674 | };
675 | rootObject = C9D0936B26741BBE002CC786 /* Project object */;
676 | }
677 |
--------------------------------------------------------------------------------