Algolia Search's API ๋ฅผ ์ด์ฉํด์ Hacker News ์ฌ์ดํธ์ ๋ฐ์ดํฐ๋ฅผ iOS ์ฑ์์ ๋ฆฌ์คํธ์ ์น๋ทฐ๋ก ๋ณด์ฌ์ฃผ์.
[Hacker News ์ฌ์ดํธ]
Hacker News
news.ycombinator.com
[Algolia Search's API]
HN Search powered by Algolia
Hacker News Search, millions articles and comments at your fingertips.
hn.algolia.com
1. Project ์์ฑ
H4XOR News ๋ผ๋ ์ด๋ฆ์ผ๋ก iOS ํ๋ก์ ํธ๋ฅผ ์์ฑํด์ฃผ์. SwiftUI ๋ก ๋ง๋ค ๊ฒ์ด๋ค.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Content")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ContentVIew.swift ํ์ผ์ ๊ธฐ๋ณธ ์ฝ๋
2. List View, NavigationView ๋ง๋ค๊ธฐ
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
List {
Text("Content")
Text("Content")
Text("Content")
}.navigationTitle("H4XOR NEWS")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
List๋ ํฐ์ ์ค ๋ฆฌ์คํธ๊ฐ ๋ง๋ค์ด์ง๊ณ
NavigationView๋ ๋ฆฌ์คํธ ์์ ๊ณต๊ฐ์ด ์๊ธด๋ค. ์ด ๋ navigationTitle์ ์ถ๊ฐํด์ฃผ๋ฉด ๊ตต์ ๊ธ์จ๋ก ์ ๋ชฉ์ฒ๋ฆฌ ๋๋ค.
3. Post ๊ฐ์ฒด ๋ง๋ค๊ธฐ
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
List {
Text("Content")
Text("Content")
Text("Content")
}.navigationTitle("H4XOR NEWS")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Post: Identifiable{
let id: String
let title: String
}
let posts = [
Post(id: "1", title: "Hello"),
Post(id: "2", title: "Bonjour"),
Post(id: "3", title: "Hola")
]
Post ๊ฐ์ฒด์ id์ title ์์ฑ์ ์ถ๊ฐํ๋ค.
Struct, Class ๋ฅผ ์ ์ํ ๋ ID ๊ฐ์ด ํ์ํ ๊ฒฝ์ฐ, Identifiable ํ๋กํ ์ฝ์ ์ถ๊ฐํ๋ฉด ID ๊ฐ์ ๊ธฐ๋ฐํ์ฌ ์์๋ฅผ ์๋ณํ ์ ์๋ค.
https://developer.apple.com/documentation/swift/identifiable
Identifiable | Apple Developer Documentation
A class of types whose instances hold the value of an entity with stable identity.
developer.apple.com
Identifiable ํ๋กํ ์ฝ์์ ID๋ฅผ ์ ๊ณตํ๋ฏ๋ก id ๊ฐ์ ํ์๋ก ์ ์ํด์ค์ผ ํ๋ค.
์ฆ let id: String ๊ตฌ๋ฌธ์ด ์์ผ๋ฉด ์๋ฌ๋๋ค.
4. List์ Identifiable ํ๋กํ ์ฝ์ ์ ์ฉํ Post ๊ฐ์ฒด๋ค ๋์ฐ๊ธฐ
1) List ์์ฑ ์ ํ์์ Identifiable ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ ์ ์ธ ๋ฒ์ ์ ์ ํํ๋ค.
2) ์์ฑ ์ฑ์ฐ๊ธฐ
์์ฑ์ผ๋ก data: RandomAccessCollection ๊ณผ rowContent: (Identifiable) -> View ๊ฐ ํ์ํ๋ค.
3) in ํค์๋ ์ด์ฉํด์ ๋ฆฌ์คํธ ๋ฐ๋ณต๋ฌธ ์ค์ ํ๊ธฐ
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
List(posts) { post in
Text(post.title)
}
.navigationTitle("H4XOR NEWS")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Post: Identifiable{
let id: String
let title: String
}
let posts = [
Post(id: "1", title: "Hello"),
Post(id: "2", title: "Bonjour"),
Post(id: "3", title: "Hola")
]
collection data์๋ ๋ฆฌ์คํธ๋ช posts ๋ฅผ ์ ๋ ฅํด์ฃผ๊ณ , rowContent๋ ๊ดํธ๋ฅผ ๋ซ๊ณ in ํค์๋๋ฅผ ์ฌ์ฉํด์ ๋ฐ๋ณต๋ฌธ ํ์์ผ๋ก ๋ง๋ค์ด์ฃผ์๋ค.
์ฌ์ค ์ด๋ ๊ฒ ๋ณํํ๋ ๋ถ๋ถ์ ์์ ํ ์๋ฟ์ง๋ ์์์ ๊ณ์ ๋ค์ฌ๋ค๋ด์ผ๊ฒ ๋ค.
5. Algolia Search's API์ Json ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
HN Search powered by Algolia
Hacker News Search, millions articles and comments at your fingertips.
hn.algolia.com
API Documentation์ ๋ณด๋ฉด Json ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ URL ๋ฆฌ์คํธ๋ค์ด ์๋ค.
์ฌ๊ธฐ์ ์ฐ์ ์ฒซ ํ์ด์ง์ ๋ชจ๋ ๋ด์ฉ์ ๊ฐ์ ธ์ฌ ์ ์๋ http://hn.algolia.com/api/v1/search?tags=front_page URL์ ์ด์ฉํด๋ณด์.
https://hn.algolia.com/api/v1/search?tags=front_page ๊ฐ ์ ๋ฌํ๋ Json ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ ์๋์ ๊ฐ๋ค.
๋ฐ์ดํฐ ๊ตฌ์กฐ ๋ง๋ค ๋ ์ฌ์ฉํด์ผ ํ๋ ํ๋ฒ ๋ด๋์.
hits ๋ฆฌ์คํธ๋ค์ด ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณ ์๊ณ , hits ๋ฆฌ์คํธ ํ๊ฐ๋ ์ค๋ฅธ์ชฝ๊ณผ ๊ฐ์ด ์๊ฒผ๋ค. title, url, created_at, objectID, points ๋ฑ์ ์ ๋ณด๊ฐ ์ฃผ์ด์ง๋ค.
6. Json ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ Models ํด๋์ค๋ค ๋ง๋ค๊ธฐ
H4XOR ํด๋์ Models ํด๋๋ฅผ ๋ง๋ค๊ณ ๊ทธ ์์ NetworkManager์ PostData swift ํ์ผ์ ์ถ๊ฐํ๋ค.
NetworkManager.swift ๋ API ํธ์ถ ์ฝ๋๋ฅผ ๋ด๋นํ๊ณ , PostData.swift ๋ Data ๊ฐ์ฒด๋ฅผ ๋ด๋นํ ๊ฒ์ด๋ค.
import Foundation
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String{
return objectID
}
let objectID: String
let points: Int
let title: String
let url: String?
}
์์์ ContentView.swift ์ ์ ์ํ Post ๊ตฌ์กฐ์ฒด๋ฅผ ์ง์ฐ๊ณ PostData.swift ํ์ผ์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ ์ ํด์ฃผ์.
๋ํ Decodable ํ๋กํ ์ฝ๋ก Json ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ก ๊ฐ์ฒด๋ก ๋ฐ์์ฌ ์ ์๊ฒ ํด์ค๋ค.
hits ๋ผ๋ ์ด๋ฆ์ ๋ฆฌ์คํธ์ Post ๋ฐ์ดํฐ๊ฐ ์๊ณ , Post ๋ฐ์ดํฐ์์ ์ฌ์ฉํ objectID, points, title, url ์ ์ ์ธํด์ค๋ค.
์ด ๋, Identifiable ํ๋กํ ์ฝ์ ์ฌ์ฉํ๋ ค๋ฉด id ๋ฅผ ์ ์ํด์ค์ผํ๋๋ฐ, objectID ์ ๋ด์ฉ์ด ๊ฒน์น๋๊น id ์์ฑ์ objectID ๊ฐ์ผ๋ก ๋ฐ๋ก ๋ฃ์ด์ฃผ๋๋ก ์์ ๊ฐ์ ๊ตฌ๋ฌธ์ ์ฌ์ฉํ๋ค.
* ๋ค์์ ์คํํ ๋, url ์ด null ์ด๋ผ๋ ์๋ฌ๊ฐ ๋จ๋ฉด let url: String? ์ด๋ ๊ฒ optional ์ ์ถ๊ฐํด์ฃผ๋ฉด ํด๊ฒฐ๋๋ค.
import Foundation
class NetworkManager{
func fetchData(){
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page"){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data {
do{
let results = try decoder.decode(Results.self, from: safeData)
} catch{
print(error)
}
}
}
}
task.resume()
}
}
}
NetworkManager.swift ์ฝ๋๋ ์์ ๊ฐ๋ค. url, session, task ๋ฅผ ์ ์ํ๊ณ json data๋ฅผ decoder๋ก ๊ฐ์ฒด ํํ๋ก ์ ์ฅํ๋ค.
safeData๋ฅผ Results.self ๊ฐ์ฒด๋ก ๋ณ๊ฒฝํ๋ ๊ฒ.
7. JSON ๋ฐ์ดํฐ ํ๋ฉด์ ๋ฟ๋ฆฌ๊ธฐ
1) NetworkManager ํด๋์ค์ ObservableObject ํ๋กํ ์ฝ ์ฑํํ๊ธฐ
import Foundation
class NetworkManager: ObservableObject{
func fetchData(){
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page"){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data {
do{
let results = try decoder.decode(Results.self, from: safeData)
} catch{
print(error)
}
}
}
}
task.resume()
}
}
}
ํด๋์ค์ ObservableObject ํ๋กํ ์ฝ์ ์ฑํํ๋ฉด ํด๋์ค์ ์ธ์คํด์ค๋ฅผ ๊ด์ฐฐํ๊ณ ์๋ค๊ฐ ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ๋ทฐ๋ฅผ ์ ๋ฐ์ดํธ ํ๋ค.
2) ๋ณํ๋ ๋ณ์์ @Published ํค์๋ ๋ถ์ฌ์ฃผ๊ธฐ.
import Foundation
class NetworkManager: ObservableObject{
@Published var posts = [Post]()
func fetchData(){
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page"){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data {
do{
let results = try decoder.decode(Results.self, from: safeData)
self.posts = results.hits
} catch{
print(error)
}
}
}
}
task.resume()
}
}
}
posts ๋ฆฌ์คํธ ๋ณ์๋ฅผ ๋ง๋ค์ด์ results.hits ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ๋๋ง๋ค post ๊ฐ์ฒด๋ฅผ ๋ณ๊ฒฝํ๋ค.
๊ทธ๋ฆฌ๊ณ @Published ํค์๋๋ฅผ ๋ถ์ฌ์ฃผ๋ฉด ์ด ๋ณ์๋ฅผ ๊ตฌ๋ ํ๋ ๊ฐ์ฒด๋ค์ ์ ๋ณ์๊ฐ ๋ณํ ๋๋ง๋ค ์๋์ผ๋ก ์์์ ๋ฐ์ ์ ์๋ค.
https://developer.apple.com/documentation/combine/observableobject
ObservableObject | Apple Developer Documentation
A type of object with a publisher that emits before the object has changed.
developer.apple.com
https://developer.apple.com/documentation/combine/published
Published | Apple Developer Documentation
A type that publishes a property marked with an attribute.
developer.apple.com
3) ContentView.swift ์์ posts ๊ฐ์ฒด ๊ตฌ๋ ํ๊ธฐ
import SwiftUI
struct ContentView: View {
@ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView{
List(networkManager.posts) { post in
Text(post.title)
}
.navigationTitle("H4XOR NEWS")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//let posts = [
// Post(id: "1", title: "Hello"),
// Post(id: "2", title: "Bonjour"),
// Post(id: "3", title: "Hola")
//]
@ObservedObjectํค์๋๋ฅผ ๋ถ์ธ๋ค. NetworkManager๊ฐ ์ ๋ฐ์ดํธํ ๋ ๋ง๋ค ํธ๋ฆฌ๊ฑฐํ๋ค๋ ๋ป์ด๋ค.
List ์์ฑ์ธ data: RandomAccessCollection๋ฅผ posts -> networkManager.posts ๋ก ๋ณ๊ฒฝํด์ค๋ค.
import SwiftUI
struct ContentView: View {
@ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView{
List(networkManager.posts) { post in
Text(post.title)
}
.navigationTitle("H4XOR NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
.onAppear() ๋ body view ๊ฐ ํ๋ฉด์ ๋ํ๋ฌ์ ๋ ์คํ๋๋ ํจ์์ด๋ค.
navigatio view ํธ์ถ -> onAppear() ํธ์ถ๋ก ํ๋ฉด์ ์ค์ง์ ์ผ๋ก ๋ณด์ฌ์ฃผ๋ ์ฝ๋์ด๋ค.
ํ์๋ ๊ณต๊ฐ์ด๋ผ newtworkManager ์์ self ํค์๋๋ฅผ ๋ถ์ฌ์ค์ผ ํ๋ค.
4) ๋น๋๊ธฐ ์ ์ฉํ๊ธฐ
import Foundation
class NetworkManager: ObservableObject{
@Published var posts = [Post]()
func fetchData(){
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page"){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.hits
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
์ฃผ์ํ ์ , @Published ๋ฅผ ์ฌ์ฉํ ๋๋ ๋ฉ์ธ ์ค๋ ๋๋ฅผ ํจ์นํ๋๋ก ๋ ํ์ธํด์ผ ํ๋ค.
self.posts ์ ๊ฐ์ ๋ฃ์ด์ฃผ๋ ๋ถ๋ถ์ DisPatchQueue.main.async ์ฝ๋๋ก ๊ฐ์ธ์ฃผ์.
5) ๊ฒ์๊ธ์ ์ข์์ ์๋ ํ์ํ๊ธฐ
import SwiftUI
struct ContentView: View {
@ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView{
List(networkManager.posts) { post in
HStack{
Text(post.points)
Text(post.title)
}
}
.navigationTitle("H4XOR NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
8. ๋ชฉ๋ก์ ์ธ๋ถ์ฌํญ ๋ทฐ ๋ง๋ค๊ธฐ & ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
1) SwiftUI View ๋ง๋ค๊ธฐ
Views ํด๋๋ฅผ ๋ง๋ค๊ณ ContentView๋ฅผ ๋ฃ์ด์ค๋ค. SwiftUI View ํ ํ๋ฆฟ์ ์ ํํ์ฌ DetailView๋ฅผ ๋ง๋ค์ด์ค๋ค.
2) ContentView ์ NavigationLink ๋ฅผ ์ถ๊ฐํด์ค๋ค.
๋ฆฌ์คํธ์ ์ธ๋ถ์ฌํญ ํ์ด์ง๋ NavigationLink ๋ก ๋ง๋ค ์ ์๋ค.
NavigationLink ์ ์ธ ๋ชฉ๋ก ์ค์์ destination ๊ณผ label ์ ์ธ์๋ก ๊ฐ๋ ์ ์ธ์ ์ ํํ๋ค. destination view๋ฅผ ๋ํ๋ด๋ navigation link ๋ฅผ ๋ง๋๋ ๊ตฌ๋ฌธ์ด๋ค. destination์ ๋ชฉ์ ์ง ๋ทฐ ์ด๋ฆ์ ์ ์ด์ฃผ๋ฉด ๋๋ค.
https://developer.apple.com/documentation/swiftui/navigationlink#Link-to-a-destination-view
NavigationLink | Apple Developer Documentation
A view that controls a navigation presentation.
developer.apple.com
https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:label:)-27n7s
init(destination:label:) | Apple Developer Documentation
Creates a navigation link that presents the destination view.
developer.apple.com
import SwiftUI
struct ContentView: View {
@ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView{
List(networkManager.posts) { post in
NavigationLink (
destination: DetailView(url: post.url)){
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4XOR NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
์ ์ฒด ์ฝ๋๋ ์์ ๊ฐ๋ค.
NavigationLink ( destination: DetailView(url: post.url)) { } ์ด๋ ๊ฒ url ์ DetailView ์ธ์๋ก ๋ฃ์ด์ ์ฌ์ฉํ๋๋ฐ,
์ด๋ ๊ฒ๋ ํ ์ ์๋ค๋๊ฑฐ ๊ธฐ์ตํ์. label ์ธ์๋ฅผ ์์ฐ๊ณ {} ๋ก ๋๋ผ ์ ์๋ ๋ฏ.
๋ฉ์ธ ํ๋ฉด์ Navigation Link ๊ฐ ์๊ฒผ๊ณ , ๋๋ฅด๋ Detail View ๋ก ๋์ด๊ฐ๋ค.
9. Detail View ๋ฅผ ์น๋ทฐ๋ก ํํํ๊ธฐ
1) WebView ๊ตฌ์กฐ ๋ง๋ค๊ธฐ
import SwiftUI
import WebKit
struct DetailView: View {
let url: String?
var body: some View {
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(url: "https://www.google.com")
}
}
struct WebView : UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
<#code#>
}
func updateUIView(_ uiView: UIViewType, context: Context) {
<#code#>
}
}
WebKit๋ฅผ import ํด์ฃผ๊ณ UIViewRepresentable ํ๋กํ ์ฝ์ ์์๋ฐ์ WebView ๊ตฌ์กฐ์ฒด๋ฅผ ๋ง๋ค์ด์ฃผ์ด์ผ ํ๋ค.
UIViewRepresentable์ UIKit view๋ฅผ ๋ํ๋ด๋ SwiftUIView๋ฅผ ๋ง๋ค์ด์ฃผ๋ ํ๋กํ ์ฝ์ด๋ค.
UIViewRepresentable ํ๋กํ ์ฝ์ ํ์ Delegate ํจ์๋ makeUIView(context: Context)์ updateUIView(_ uiView: UIViewType, context: Context) ์ด๋ค.
makeUIView ๋ View ๋ฅผ WebView๋ก ๋ง๋ค์ด์ค๋ค.
updateUIView ๋ View ๋ฅผ ์ ๋ฐ์ดํธํ๋ ํจ์์ด๋ค.
2) DetailView ์ body ์์ WebView์ url ์ ์ ๋ฌํ๋ค.
import SwiftUI
struct DetailView: View {
let url: String?
var body: some View {
WebView(urlString: url)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(url: "https://www.google.com")
}
}
3) DetailView๊ฐ ์ค url ์ urlString ๋ณ์๋ก ๋ฐ์์ ํ๋ฉด์ ๋ฟ๋ ค์ค๋ค.
struct WebView : UIViewRepresentable {
let urlString: String?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if let safeString = urlString{
if let url = URL(string: safeString){
let request = URLRequest(url: url)
uiView.load(request)
}
}
}
}
urlString ์ด ์์ผ๋ฉด URLRequst๋ฅผ ํตํด ํ๋ฉด์ load ํด์ค๋ค.
UIViewType ์ UIWebView ๋์ WKWebView ๋ฅผ ์ฌ์ฉํด์ฃผ์. ์๋ก ๋์จ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๊ธฐ๋ ํ๊ณ ์น ํ์ด์ง์์ ํ ๋น๋๋ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฑ๊ณผ ๋ณ๋์ ์ค๋ ๋์์ ๊ด๋ฆฌํ์ฌ ์น ํ์ด์ง์ ๋ฉ๋ชจ๋ฆฌ๊ฐ ์ปค๋ ์ฑ์ด ์ฃฝ์ง ์๋๋ค.
4) ์ฝ๋ ๋ฆฌํฉํ ๋ง: ์ฝ๋์ ๋ ๋ฆฝ์ฑ๊ณผ ์ ์ฐ์ฑ์ ์ํด WebView.swift ํ์ผ๋ก ๋นผ์ฃผ๊ธฐ.
import Foundation
import WebKit
import SwiftUI
struct WebView : UIViewRepresentable {
let urlString: String?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if let safeString = urlString{
if let url = URL(string: safeString){
let request = URLRequest(url: url)
uiView.load(request)
}
}
}
}
5) ๊ฒฐ๊ณผํ๋ฉด. ๋.