Post

아이폰 검색앱 생성(6)

아이폰 검색앱 생성(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.