Skip to content

Commit d3874a3

Browse files
committed
Multiplatform App
1 parent e191a0b commit d3874a3

File tree

22 files changed

+1213
-0
lines changed

22 files changed

+1213
-0
lines changed
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/HackerNews.xcodeproj/xcuserdata/
2+
/HackerNews.xcodeproj/project.xcworkspace/contents.xcworkspacedata
3+
/HackerNews.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
4+
/HackerNews.xcodeproj/project.xcworkspace/xcuserdata/rickwierenga.xcuserdatad/UserInterfaceState.xcuserstate

MultiplatformApp/HackerNews/HackerNews.xcodeproj/project.pbxproj

+518
Large diffs are not rendered by default.

MultiplatformApp/HackerNews/HackerNews.xcodeproj/project.xcworkspace/contents.xcworkspacedata

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"colors" : [
3+
{
4+
"color" : {
5+
"platform" : "ios",
6+
"reference" : "systemOrangeColor"
7+
},
8+
"idiom" : "universal"
9+
}
10+
],
11+
"info" : {
12+
"author" : "xcode",
13+
"version" : 1
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "iphone",
5+
"scale" : "2x",
6+
"size" : "20x20"
7+
},
8+
{
9+
"idiom" : "iphone",
10+
"scale" : "3x",
11+
"size" : "20x20"
12+
},
13+
{
14+
"idiom" : "iphone",
15+
"scale" : "2x",
16+
"size" : "29x29"
17+
},
18+
{
19+
"idiom" : "iphone",
20+
"scale" : "3x",
21+
"size" : "29x29"
22+
},
23+
{
24+
"idiom" : "iphone",
25+
"scale" : "2x",
26+
"size" : "40x40"
27+
},
28+
{
29+
"idiom" : "iphone",
30+
"scale" : "3x",
31+
"size" : "40x40"
32+
},
33+
{
34+
"idiom" : "iphone",
35+
"scale" : "2x",
36+
"size" : "60x60"
37+
},
38+
{
39+
"idiom" : "iphone",
40+
"scale" : "3x",
41+
"size" : "60x60"
42+
},
43+
{
44+
"idiom" : "ipad",
45+
"scale" : "1x",
46+
"size" : "20x20"
47+
},
48+
{
49+
"idiom" : "ipad",
50+
"scale" : "2x",
51+
"size" : "20x20"
52+
},
53+
{
54+
"idiom" : "ipad",
55+
"scale" : "1x",
56+
"size" : "29x29"
57+
},
58+
{
59+
"idiom" : "ipad",
60+
"scale" : "2x",
61+
"size" : "29x29"
62+
},
63+
{
64+
"idiom" : "ipad",
65+
"scale" : "1x",
66+
"size" : "40x40"
67+
},
68+
{
69+
"idiom" : "ipad",
70+
"scale" : "2x",
71+
"size" : "40x40"
72+
},
73+
{
74+
"idiom" : "ipad",
75+
"scale" : "1x",
76+
"size" : "76x76"
77+
},
78+
{
79+
"idiom" : "ipad",
80+
"scale" : "2x",
81+
"size" : "76x76"
82+
},
83+
{
84+
"idiom" : "ipad",
85+
"scale" : "2x",
86+
"size" : "83.5x83.5"
87+
},
88+
{
89+
"idiom" : "ios-marketing",
90+
"scale" : "1x",
91+
"size" : "1024x1024"
92+
},
93+
{
94+
"idiom" : "mac",
95+
"scale" : "1x",
96+
"size" : "16x16"
97+
},
98+
{
99+
"idiom" : "mac",
100+
"scale" : "2x",
101+
"size" : "16x16"
102+
},
103+
{
104+
"idiom" : "mac",
105+
"scale" : "1x",
106+
"size" : "32x32"
107+
},
108+
{
109+
"idiom" : "mac",
110+
"scale" : "2x",
111+
"size" : "32x32"
112+
},
113+
{
114+
"idiom" : "mac",
115+
"scale" : "1x",
116+
"size" : "128x128"
117+
},
118+
{
119+
"idiom" : "mac",
120+
"scale" : "2x",
121+
"size" : "128x128"
122+
},
123+
{
124+
"idiom" : "mac",
125+
"scale" : "1x",
126+
"size" : "256x256"
127+
},
128+
{
129+
"idiom" : "mac",
130+
"scale" : "2x",
131+
"size" : "256x256"
132+
},
133+
{
134+
"idiom" : "mac",
135+
"scale" : "1x",
136+
"size" : "512x512"
137+
},
138+
{
139+
"idiom" : "mac",
140+
"scale" : "2x",
141+
"size" : "512x512"
142+
}
143+
],
144+
"info" : {
145+
"author" : "xcode",
146+
"version" : 1
147+
}
148+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// HackerNews.swift
3+
// HackerNews
4+
//
5+
// Created by Rick Wierenga on 14/07/2020.
6+
//
7+
8+
import Combine
9+
import Foundation
10+
11+
final class HackerNews {
12+
private static let baseUrl = URL(string: "https://hacker-news.firebaseio.com/v0/")!
13+
14+
static func loadItems(in category: Category) -> AnyPublisher<[ItemViewModel], HackerNewsError> {
15+
let url = baseUrl.appendingPathComponent("\(category.urlSuffix)stories.json")
16+
return URLSession.shared.dataTaskPublisher(for: url)
17+
.tryMap { response in
18+
if let httpURLResponse = response.response as? HTTPURLResponse,
19+
!(200...299 ~= httpURLResponse.statusCode) {
20+
throw HackerNewsError.message("Got an HTTP \(httpURLResponse.statusCode) error.")
21+
}
22+
return response.data
23+
}
24+
.decode(type: [Int].self, decoder: JSONDecoder())
25+
.map { $0[0..<30] } // Limit the number of items to 30 to limit the load on the API.
26+
.map { items in
27+
items.map { id in
28+
ItemViewModel(id: id)
29+
}
30+
}
31+
.mapError { HackerNewsError.map($0) }
32+
.eraseToAnyPublisher()
33+
}
34+
35+
static func loadItem(withId id: Int) -> AnyPublisher<Item, HackerNewsError> {
36+
let url = baseUrl.appendingPathComponent("item/\(id).json")
37+
return URLSession.shared.dataTaskPublisher(for: url)
38+
.tryMap { response in
39+
if let httpURLResponse = response.response as? HTTPURLResponse,
40+
!(200...299 ~= httpURLResponse.statusCode) {
41+
throw HackerNewsError.message("Got an HTTP \(httpURLResponse.statusCode) error.")
42+
}
43+
return response.data
44+
}
45+
.decode(type: Item.self, decoder: JSONDecoder())
46+
.mapError { HackerNewsError.map($0) }
47+
.eraseToAnyPublisher()
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// HackerNewsApp.swift
3+
// Shared
4+
//
5+
// Created by Rick Wierenga on 14/07/2020.
6+
//
7+
8+
import SwiftUI
9+
10+
@main
11+
struct HackerNewsApp: App {
12+
var body: some Scene {
13+
WindowGroup {
14+
#if os(macOS)
15+
NavigationView {
16+
Sidebar()
17+
ItemsListView(viewModel: ItemsViewModel(category: .top))
18+
}
19+
#else
20+
NavigationView {
21+
TabBar()
22+
.navigationTitle("Hacker News")
23+
}.navigationViewStyle(StackNavigationViewStyle())
24+
#endif
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// Category.swift
3+
// HackerNews
4+
//
5+
// Created by Rick Wierenga on 14/07/2020.
6+
//
7+
8+
import Foundation
9+
10+
enum Category: String, CaseIterable, Identifiable {
11+
var id: String {
12+
return rawValue
13+
}
14+
15+
case top
16+
case best
17+
case new
18+
case ask
19+
case show
20+
21+
var name: String {
22+
switch self {
23+
case .best: return "Best"
24+
case .new: return "New"
25+
case .top: return "Top"
26+
case .ask: return "Ask HN"
27+
case .show: return "Show HN"
28+
}
29+
}
30+
31+
var urlSuffix: String {
32+
return self.rawValue
33+
}
34+
35+
var icon: String {
36+
switch self {
37+
case .best: return "rosette"
38+
case .new: return "clock"
39+
case .top: return "flame.fill"
40+
case .ask: return "questionmark.circle"
41+
case .show: return "eye"
42+
}
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// HackerNewsError.swift
3+
// HackerNews
4+
//
5+
// Created by Rick Wierenga on 14/07/2020.
6+
//
7+
8+
import Foundation
9+
10+
enum HackerNewsError: Error {
11+
case message(String)
12+
case other(Error)
13+
14+
static func map(_ error: Error) -> HackerNewsError {
15+
return (error as? HackerNewsError) ?? .other(error)
16+
}
17+
}
18+
19+
extension HackerNewsError: CustomStringConvertible {
20+
var description: String {
21+
switch self {
22+
case .message(let message):
23+
return message
24+
case .other(let error):
25+
return error.localizedDescription
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Item.swift
3+
// HackerNews
4+
//
5+
// Created by Rick Wierenga on 14/07/2020.
6+
//
7+
8+
import Foundation
9+
10+
struct Item: Codable, Hashable {
11+
let id: Int
12+
let author: String
13+
let score: Int
14+
let date: Double
15+
let title: String
16+
let contentUrl: URL?
17+
18+
private enum CodingKeys : String, CodingKey {
19+
case id
20+
case author = "by"
21+
case score
22+
case date = "time"
23+
case title
24+
case contentUrl = "url"
25+
}
26+
27+
var formattedDate: String {
28+
// See https://nsdateformatter.com
29+
let dateFormatter = DateFormatter()
30+
dateFormatter.dateFormat = "MMM d, h:mm a"
31+
dateFormatter.locale = Locale(identifier: "en_US")
32+
return dateFormatter.string(from: Date(timeIntervalSince1970: date))
33+
}
34+
35+
var url: URL {
36+
contentUrl ?? URL(string: "http://news.ycombinator.com/item?id=\(id)")!
37+
}
38+
}

0 commit comments

Comments
 (0)