Post

아이폰 검색앱 생성(6)

엘라스틱 서치에 데이터를 저장했으니 검색 api를 통해서 데이터를 검색할 수 있다 그래서 이제 검색을 할 ios앱을 만들어야한다 앱은 간단히 메인 검색 페이지와 검색결과를 보여주는 리스트 페이지로 구성한다 결과에서 글의 제목을 클릭한다면 해당 공지사항이 있는 웹페이지로 이동한다

아이폰 검색앱 생성(6)

엘라스틱 서치에 데이터를 저장했으니 검색 api를 통해서 데이터를 검색할 수 있다 그래서 이제 검색을 할 ios앱을 만들어야한다 앱은 간단히 메인 검색 페이지와 검색결과를 보여주는 리스트 페이지로 구성한다 결과에서 글의 제목을 클릭한다면 해당 공지사항이 있는 웹페이지로 이동한다

 

1. ContentView.swift 작성

1
2
3
4
5
6
7
8
9
10
11
// 프로젝트의 이름으로 만들어진 swift 파일
import SwiftUI

@main
struct search_app_iosApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
1
2
3
4
5
6
7
8
// ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        MainView()
    }
}

기본적으로 생성되는 파일중 내가 만든 프로젝트의 이름으로 만들어진 swift파일이 존재하고 ContentView.swift 파일이 존재한다 기본적으로 작성되는 코드에 따라서 처음 진입은 @main이 있는 파일에서 시작된다 연결된 ContentView로 연결되고 거기서 메인 페이지로 만든 MainView를 연결했다( 바로 써도 상관 없음 )

 

2. MainView.swift 작성

검색창을 띄울 메인 페이지 일단 간단하게 메인 페이지에 검색창만 만들었다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import SwiftUI

struct MainView: View {
    @State private var searchQuery: String = "" // 입력한 검색어 저장
    @State private var isSearchActive: Bool = false // 입력이 있는지 확인

    var body: some View {
        NavigationView {
            VStack {
                TextField("Search...", text: $searchQuery)
                    .padding()
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(.horizontal)

                Button(action: { // 검색창이 비어있지 않을 때 활성화
                    if !searchQuery.isEmpty {
                        isSearchActive = true
                    }
                }) {
                    Text("Search")
                        .font(.headline)
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(8)
                }
                .padding(.top, 20)

                NavigationLink( // 검색 버튼을 눌렀을때
                    destination: SearchResultsView(searchQuery: searchQuery), // 목적지로 이동
                    isActive: $isSearchActive,
                    label: {
                        EmptyView()
                    }
                )
            }
            .navigationTitle("Main Page")
        }
    }
}

 

3. SearchResultsView.swift 작성

앞에서 입력한 검색어를 이용해서 검색 결과를 출력해주는 페이지이다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import SwiftUI

// 검색 결과를 받고서 이용할 데이터 형태를 정의함 
struct SearchResult: Identifiable {
    let id: String
    let site: String
    let title: String
    let url: String
    let contentPreview: String? // content의 일부를 저장
}

// api 응답 형태 정의
struct ApiResponse: Decodable {
    struct Hits: Decodable {
        struct InnerHits: Decodable {
            struct Source: Decodable {
                let site: String
                let title: String
                let url: String
                let content: String?
            }

            let _id: String
            let _source: Source
        }

        let hits: [InnerHits]
    }

    let hits: Hits
}

struct SearchResultsView: View {
    var searchQuery: String
    @State private var results: [SearchResult] = [] // 검색 경과를 배열로
    @State private var isLoading: Bool = false
    @State private var errorMessage: String? = nil

    var body: some View {
        VStack {
            if isLoading {
                ProgressView("Loading...")
            } else if let errorMessage = errorMessage {
                Text("Error: \(errorMessage)").foregroundColor(.red)
            } else if results.isEmpty {
                Text("No results found")
            } else {
                List(results) { result in
                    VStack(alignment: .leading) {
                        Text(result.site)
                            .font(.headline)
                        
                        // Title as a clickable link
                        Link(destination: URL(string: result.url)!) {
                            Text(result.title)
                                .font(.subheadline)
                                .foregroundColor(.blue)
                        }

                        if let contentPreview = result.contentPreview {
                            Text(contentPreview)
                                .font(.footnote)
                                .foregroundColor(.gray)
                                .lineLimit(2) // Only show two lines of content
                        }
                    }
                }
            }
        }
        .onAppear(perform: fetchSearchResults)
        .navigationTitle("Search Results")
    }

    func fetchSearchResults() {
        guard let url = URL(string: "http://elastic:<es_pw>@localhost:9200/notice_index/_search?q=title:\(searchQuery)") else {
            errorMessage = "Invalid URL"
            return
        }

        isLoading = true
        errorMessage = nil

        URLSession.shared.dataTask(with: url) { data, response, error in
            DispatchQueue.main.async {
                isLoading = false

                if let error = error {
                    errorMessage = "Failed to load data: \(error.localizedDescription)"
                    return
                }

                guard let data = data else {
                    errorMessage = "No data received"
                    return
                }

                do {
                    let apiResponse = try JSONDecoder().decode(ApiResponse.self, from: data)
                    self.results = apiResponse.hits.hits.map { hit in
                        // 옵셔널 체이닝과 nil 병합 연산자를 사용해 안전하게 언래핑
                        let contentPreview = hit._source.content?.trimmingCharacters(in: .whitespacesAndNewlines)
                            .components(separatedBy: .newlines)
                            .joined(separator: " ")
                            .prefix(100)
                        
                        let previewText = contentPreview.map(String.init) ?? "" // 옵셔널 값 변환 및 처리
                        
                        return SearchResult(
                            id: hit._id,
                            site: hit._source.site,
                            title: hit._source.title,
                            url: hit._source.url,
                            contentPreview: previewText.isEmpty ? nil : previewText
                        )
                    }
                } catch {
                    errorMessage = "Failed to parse data: \(error.localizedDescription)"
                }
            }
        }.resume()
    }
}

검색어 ‘공사’를 넣었을 때 결과

This post is licensed under CC BY 4.0 by the author.