텍스트 필드를 사용해서 검색을 구현할 때!
한글 자음과 모음을 분리해서도, 합쳐서도 필터링이 되게 구현하고 싶다면 어떻게 해야할까요!
- "스위프트" 라는 텍스트를 검색한다 ->
스위프트 라는 글자 + "ㅅ 스 승 스위 스윞 스위프 스위픝 스위프트" 까지도 검색이 되게 하려면?? 🤔
먼저 아래 코드를 복붙 해서 Jamo라는 swift 파일을 만들어줍니다
(한글 자음과 모음의 분리를 처리해줄 친구에요)
//
// Jamo.swift
//
// Created by 우주형 on 2023/06/16.
//
import Foundation
extension CharacterSet {
static var modernHangul: CharacterSet{
return CharacterSet(charactersIn: ("가".unicodeScalars.first!)...("힣".unicodeScalars.first!))
}
}
public class Jamo {
// UTF-8 기준
static let INDEX_HANGUL_START:UInt32 = 44032 // "가"
static let INDEX_HANGUL_END:UInt32 = 55199 // "힣"
static let CYCLE_CHO :UInt32 = 588
static let CYCLE_JUNG :UInt32 = 28
static let CHO = [
"ㄱ","ㄲ","ㄴ","ㄷ","ㄸ","ㄹ","ㅁ","ㅂ","ㅃ","ㅅ",
"ㅆ","ㅇ","ㅈ","ㅉ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"
]
static let JUNG = [
"ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ","ㅕ", "ㅖ", "ㅗ", "ㅘ",
"ㅙ", "ㅚ","ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ",
"ㅣ"
]
static let JONG = [
"","ㄱ","ㄲ","ㄳ","ㄴ","ㄵ","ㄶ","ㄷ","ㄹ","ㄺ",
"ㄻ","ㄼ","ㄽ","ㄾ","ㄿ","ㅀ","ㅁ","ㅂ","ㅄ","ㅅ",
"ㅆ","ㅇ","ㅈ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"
]
static let JONG_DOUBLE = [
"ㄳ":"ㄱㅅ","ㄵ":"ㄴㅈ","ㄶ":"ㄴㅎ","ㄺ":"ㄹㄱ","ㄻ":"ㄹㅁ",
"ㄼ":"ㄹㅂ","ㄽ":"ㄹㅅ","ㄾ":"ㄹㅌ","ㄿ":"ㄹㅍ","ㅀ":"ㄹㅎ",
"ㅄ":"ㅂㅅ"
]
static let MO = [
"ㅘ":"ㅏ", "ㅙ":"ㅐ", "ㅚ":"ㅣ", "ㅝ":"ㅓ", "ㅞ":"ㅔ", "ㅟ":"ㅣ", "ㅢ":"ㅣ"
]
static let MO_LIST = [
"ㅘ", "ㅙ", "ㅚ", "ㅝ", "ㅞ", "ㅟ", "ㅢ"
]
static let JA = [
"ㄱ":2, "ㄲ":4, "ㄴ":2, "ㄷ":3, "ㄸ":6,
"ㄹ":5, "ㅁ":4, "ㅂ":4, "ㅃ":8, "ㅅ":2,
"ㅆ":4, "ㅇ":1, "ㅈ":3, "ㅉ":6, "ㅊ":4,
"ㅋ":3, "ㅌ":4, "ㅍ":4, "ㅎ":3, "ㅏ":2,
"ㅐ":3, "ㅑ":3, "ㅒ":4, "ㅓ":2, "ㅔ":3,
"ㅕ":3, "ㅖ":4, "ㅗ":2, "ㅘ":4, "ㅙ":5,
"ㅚ":3, "ㅛ":3, "ㅜ":2, "ㅝ":4, "ㅞ":5,
"ㅟ":3, "ㅠ":3, "ㅡ":1, "ㅢ":2, "ㅣ":1,
"ㄳ":4, "ㄵ":5, "ㄶ":5, "ㄺ":7, "ㄻ":9,
"ㄼ":9, "ㄽ":7, "ㄾ":9, "ㄿ":9, "ㅀ":8,
"ㅄ":6
]
class func getDanmo(_ input: Character) -> Character {
for (key, value) in MO {
if key == "\(input)" {
return Character(value)
}
}
return input
}
//이전 입력한 내용과 비교해서 삭제인지 추가 입력인지 확인하는 함수
class func isDanmoDelete(preInputList: [String], inputList: [String]) -> Bool {
var preCount = 0
var curCount = 0
for text in preInputList {
for (key, value) in JA {
if text == key {
preCount += value
break
}
}
}
for text in inputList {
for (key, value) in JA {
if text == key {
curCount += value
break
}
}
}
// print(">><< curCount : \(curCount) preCount \(preCount) 비교 결과 : \(curCount < preCount)")
if curCount < preCount {
return true
}
return false
}
// 주어진 "단어"를 초성만 가져와서 리턴하는 함수
class func getCho(_ input: String) -> String {
var jamo = ""
//let word = input.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
for scalar in input.unicodeScalars{
jamo += getChoFromOneSyllable(scalar) ?? ""
}
return jamo
}
// 주어진 "단어"를 자모음으로 분해해서 리턴하는 함수
class func getJamo(_ input: String) -> String {
var jamo = ""
//let word = input.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
for scalar in input.unicodeScalars{
jamo += getJamoFromOneSyllable(scalar) ?? ""
}
return jamo
}
class func getJamoList(_ input: String) -> [String] {
var jamos: [String] = []
//let word = input.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
for scalar in input.unicodeScalars{
jamos.append(getJamoFromOneSyllable(scalar) ?? "")
}
return jamos
}
// 주어진 "코드의 음절"을 자모음으로 분해해서 리턴하는 함수
private class func getJamoFromOneSyllable(_ n: UnicodeScalar) -> String?{
if CharacterSet.modernHangul.contains(n){
let index = n.value - INDEX_HANGUL_START
let cho = CHO[Int(index / CYCLE_CHO)]
let jung = JUNG[Int((index % CYCLE_CHO) / CYCLE_JUNG)]
var jong = JONG[Int(index % CYCLE_JUNG)]
if let disassembledJong = JONG_DOUBLE[jong] {
jong = disassembledJong
}
return cho + jung + jong
} else {
return String(UnicodeScalar(n))
}
}
// 주어진 "코드의 음절"중 초성을 분해해서 리턴하는 함수
private class func getChoFromOneSyllable(_ n: UnicodeScalar) -> String?{
if CharacterSet.modernHangul.contains(n){
let index = n.value - INDEX_HANGUL_START
let cho = CHO[Int(index / CYCLE_CHO)]
return cho
} else {
return String(UnicodeScalar(n))
}
}
}
그리고 검색이 될 모델들을 예시로 표현해볼게요
struct TechStack: Identifiable {
let id = UUID().uuidString
let title: String
let koreanTitle: String
init(title: String, koreanTitle: String) {
self.title = title
self.koreanTitle = Jamo.getJamo(koreanTitle)
}
}
extension TechStack {
static let list: [TechStack] = [
TechStack(title: "Angular", koreanTitle: "앵귤러"),
TechStack(title: "Atlassian", koreanTitle: "아틀라시안"),
TechStack(title: "aws", koreanTitle: "아마존 웹 서비스"),
TechStack(title: "Bitbucket", koreanTitle: "비트버킷"),
TechStack(title: "Bootstrap5", koreanTitle: "부트스트랩5"),
TechStack(title: "Confluence", koreanTitle: "콘플루언스")
//...
]
}
모델이 init될 때 koreanTitle에 Jamo.getJamo
가 호출 되게 해서 자음과 모음을 분리한 String이 들어가게끔 해줍니다
import Combine
class TechStackSearchViewModel: ObservableObject {
@Published var searchTech: String = ""
@Published var selectedTech = [String]()
@Published var filteredTech: [TechStack] = []
private let allTech: [TechStack] = TechStack.list
private var cancellables = Set<AnyCancellable>()
init() {
addSubscriber()
}
private func addSubscriber() {
$searchTech
// .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.sink { [weak self] searchText in
guard let self = self else { return }
if searchText.isEmpty {
self.filteredTech = self.allTech
} else {
self.filteredTech = self.allTech.filter { tech in
let englishMatch = tech.title.localizedCaseInsensitiveContains(searchText)
// let koreanMatch = tech.koreanTitle.contains(searchText)
let jamoMatch = tech.koreanTitle.contains(Jamo.getJamo(searchText))
return englishMatch || jamoMatch
}
}
}
.store(in: &cancellables)
}
}
searchTech가 텍스트 필드에 바인딩 될 String이고, 이 string이 변하게 될 때
combine을 사용해서 원하는 조건들이 필터링이 되게끔 구현해주면 됩니당!
(debounce는 필터링 될 때 키보드 입력이 전부 완료되고 몇초 뒤에 호출해줄 지 정하는 메소드)