Algolia Search's API ๋ฅผ ์ด์ฉํด์ Hacker News ์ฌ์ดํธ์ ๋ฐ์ดํฐ๋ฅผ iOS ์ฑ์์ ๋ฆฌ์คํธ์ ์น๋ทฐ๋ก ๋ณด์ฌ์ฃผ์.
[Hacker News ์ฌ์ดํธ]
[Algolia Search's API]
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 ํ๋กํ ์ฝ์์ 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 ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
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
https://developer.apple.com/documentation/combine/published
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
https://developer.apple.com/documentation/swiftui/navigationlink/init(destination:label:)-27n7s
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) ๊ฒฐ๊ณผํ๋ฉด. ๋.