Swift 한글 자모 분리 검색

Woozoo·2023년 6월 16일
2

[Whatis]

목록 보기
8/8

텍스트 필드를 사용해서 검색을 구현할 때!
한글 자음과 모음을 분리해서도, 합쳐서도 필터링이 되게 구현하고 싶다면 어떻게 해야할까요!

  • "스위프트" 라는 텍스트를 검색한다 ->
    스위프트 라는 글자 + "ㅅ 스 승 스위 스윞 스위프 스위픝 스위프트" 까지도 검색이 되게 하려면?? 🤔

먼저 아래 코드를 복붙 해서 Jamo라는 swift 파일을 만들어줍니다
(한글 자음과 모음의 분리를 처리해줄 친구에요)

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))
        }
    }
}

검색이 될 모델 ex) TechStack

그리고 검색이 될 모델들을 예시로 표현해볼게요

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는 필터링 될 때 키보드 입력이 전부 완료되고 몇초 뒤에 호출해줄 지 정하는 메소드)

profile
우주형

0개의 댓글

관련 채용 정보