본문으로 바로가기
반응형

맥용 어플리케이션을 개발하는 데는 여러가지 방법이 있다.

  1. 자바를 이용하는 방법 : jar 파일로 만든 뒤 맥용 wrapper 를 이용해서 맥 어플리케이션의 형태로 묶어준다.
  2. C++ 혹은 Python을 이용하는 방법 : QT 프레임워크를 이용해서 개발한다. 파이썬용으로는 PyQT 프레임워크를 이용한다.
  3. Swift 혹은 Objective-C를 이용하는 방법 Cocoa 프레임워크를 이용해서 개발한다.


1, 2는 지금도 충분히 해볼 수 있는 것들이지만 항상 네이티브 맥용 앱을 만들어보고 싶었기에 3을 시도해보기로 했다. 나는 Objective-C, Swift 둘 다 모르지만 문법이야 비슷하겠지 싶어 몇가지 특징만 공부하고 Swift를 이용해 DB를 연동해보기로 했다.


테스트가 끝나고 느낀 점을 먼저 말해보자면,

Swift가 아직도 계속 변화하고 있는 변화무쌍한 언어이다 보니 Deprecated된 함수가 너무 많다. 문제가 생길 때마다 구글에서 해결책을 검색하는데 대부분의 결과가 Swift 2.0 기준인 때가 많아 그 함수를 그대로 쓸 수 없을 때가 많다. 물론 deprecated된 함수라도 쓸 수는 있지만 warning이 뜨는게 거슬리기도 하고, 최대한 최신 버전에 맞춰서 해보려 하니 애로사항이 많았다.

delegate, outlet, protocol 등의 용어가 뭔지, 어떤 식으로 동작하는건지 이해가 부족해서 리스트 컨트롤 (tableView)에 목록 띄우는데 한참 걸렸다. 기본 개념을 계속 파야할 것 같다.

cocoa 프레임워크에 대한 책은 지인짜 없다. ios개발 책은 그래도 종종 보이는데 맥용 코코아 관련 책은 말도 안되게 없다. 결국 공식 레퍼런스만 죽어라 파야 했다. 영어 싫은데 ㅠㅠ


내 환경은 다음과 같다.

OS : MacOSX El Capitan (10.11.6)

IDE : Xcode 8

Language : Swift 3


구체적인 이론이나 작동원리는 아직 나도 잘 모르고, 어차피 스스로 공부해야 하는 부분이니 제외하고, 테스트용 앱을 만들었던 과정만 작성한다.

1. 프로젝트 생성

새 코코아 프로젝트를 생성한다. 스토리보드를 사용할 것이기 때문에 Use Storyboard 옵션은 체크하고, 언어는 Swift 를 선택한다.


2. SQLite 라이브러리 추가

Xcode의 프로젝트 설정 (왼쪽 내비게이션 패널에서 프로젝트 이름을 클릭) 중 Build Phase 메뉴의 Link Binary With Library 메뉴에서 SQLite 라이브러리를 추가한다. (libsqlite3.tbd)


3. DB 사용을 위한 SQLite Wrapper 로드 

열심히 구글링해보니 SQLite를 Swift로 사용할 때 fmdb 라는 wrapper를 가장 많이 사용하는 것 같다. 깃헙에서 소스를 받아 준비했다.

https://github.com/ccgus/fmdb

다운로드 받은 소스에서 info.plist를 제외한 모든 파일을 프로젝트에 추가한다. (FMDB.h, FMDatabase.h, FMDatabase.m, FMDatabaseAdditions.h, FMDatabaseAddtions.m, FMDatabasePool.h, FMDatabasePool.m, FMDatabaseQueue.h, FMDatabaseQueue.m, FMResultSet.h, FMResultSet.m)



프로젝트 아래로 이 파일들을 끌어다 놓으면 Bridging Header를 추가할 거냐는 메시지가 뜬다. Objective-C 소스를 Swift에서 사용하려는 것이기 때문에 추가해야 한다. 확인을 누르면 (프로젝트명-Bridging-Header.h 파일이 프로젝트에 함께 추가된다.

추가된 Bridging-Header.h 파일을 선택하고 #import "FMDB.h" 를 추가한다. (* #include 가 아니다!)




그러면 준비는 끝났다.


4. 인터페이스 빌더

필요한 화면을 구성한다. 나는 버튼을 하나 만들어 버튼을 클릭하면 sqlite db 파일을 불러오고, 결과를 텍스트뷰와 테이블뷰에 동시에 뿌려주려고 한다.

왼쪽 패널에서 storyboard 파일을 선택하면 UI를 구성할 수 있는 창이 뜬다. 오른쪽 패널에서 push button, text view, table view를 찾아 적당히 배치한다.

패널이 뜨지 않았다면 스크린샷의 파랗게 표시된 버튼들이 활성화 된 버튼들이므로 참고해서 진행하면 된다.


5. 동작 연결

만들어진 컨트롤과 코드를 연결해주어야 한다. ViewController.swift 파일에 아웃렛을 구성한다. 아웃렛을 연결하려면 먼저 UI를 구성하는 패널과 소스 패널을 함께 화면에 표시해야 한다. 4번의 스크린샷의 상단 메뉴 오른쪽을 보면동그라미가 두개 겹쳐져 있는 버튼을 볼 수 있는데, 이 버튼이 두개의 소스를 화면에 함께 표시하는 버튼이다. 왼쪽 창을 선택하고 storyboard파일을 선택하면 로드되고, 같은 방식으로 오른쪽 창을 선택한 뒤 ViewController.swift 를 선택하면 오른쪽에 로드된다.


창을 띄웠으면, 버튼부터 코드의 class 선언 바로 아랫부분까지 마우스 오른쪽 버튼으로 드래그한다. (혹은 ctrl + 마우스 왼쪽 버튼 드래그)

주의할 점은, TableView의 경우 하나의 컨트롤이 아니라 여러개의 컨트롤이 조합된 형태이기 때문에 드래그했을 때 tableview가 맞는지 한번 더 확인해야 한다. 아니면, 아래 두번째 스크린샷처럼 내비게이션 패널과 UI창 사이에 있는 조그만 트리에서 테이블 뷰를 찾아 그쪽에서 드래그를 해도 된다. 사실 이게 훨씬 편하다.



그 다음으로는 동작함수 (Action 이라고 한다)를 설정해야 한다. 이 프로젝트의 경우 버튼 액션만 설정하면 된다.

아웃렛 설정과 같은 방식으로 버튼을 마우스 오른쪽 버튼으로 끌어 코드에 갖다 대면 (보통 class ViewController의 마지막 부분으로) 작은 팝업이 뜨는데, outletaction으로 변경해주면 된다.

6. 이제 본격적으로 버튼을 눌렀을 때 동작을 구현한다.

아까 로드한 SQLite wrapper 를 이용할 시간이다.

파일을 불러오고, 파일이 있으면 쿼리를 날린다.

쿼리 결과값이 구해지면 루프를 돌면서 각 row의 데이터를 불러온다.

텍스트박스에 추가할 내용은 스트링으로, 리스트에 추가할 내용은 배열로 만든다. 코드를 참고. ViewContoller.swiftViewContoller 클래스 부분만 표시했다.

class ViewController: NSViewController {

     // 아웃렛 설정
    @IBOutlet weak var btnLoad: NSButton!
    @IBOutlet var hText: NSTextInputClient!
    @IBOutlet weak var htableView: NSTableView!
    
    // 앱이 실행될때 무조건 한번 실행되는 부분
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 테이블 뷰의 delegate, dataSource를 설정해 준다.
        htableView.delegate = self
        htableView.dataSource = self
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    // 테이블 뷰에 데이터를 표시하기 위한 스트링 배열
    var titleArray = [String]()
    
     // 버튼 동작 함수
    @IBAction func loadData(_ sender: AnyObject) {
        let filemgr = FileManager.default
        let dirPaths = filemgr.urls(for: .documentDirectory, in: .userDomainMask)[0]
        var databasePath = dirPaths.appendingPathComponent("hymn.db").path

        // databasePath 변수에 설정된 파일이 존재하지 않을 때 처리
        if !filemgr.fileExists(atPath: databasePath) {
            let alert = NSAlert()
            alert.messageText = "오류"
            alert.informativeText = "DB가 없어요!"
            alert.beginSheetModal(for: self.view.window!) { (returnCode: NSModalResponse) -> Void in
                print ("returnCode: ", returnCode)
            }
        }else{
        // databasePath 변수에 설정된 파일이 존재할 때 처리
            let myDB = FMDatabase(path: databasePath as String)
            
            if myDB == nil {
                print("Error: \(myDB?.lastErrorMessage())")
            }

            // DB 쿼리 실행 부분            
            if myDB!.open() {
                let sql = "SELECT * FROM hymn;"
                let results:FMResultSet? = myDB!.executeQuery(sql, withArgumentsIn: nil)
                
                if (results == nil) {
                    print("Error: \(myDB!.lastErrorMessage())")
                }else{
                    var resTxt = "" // 텍스트박스에 표시하기 위한 스트링 변수
                    var title = ""
                    titleArray.removeAll()
                    
                    // DB에서 불러온 각 열을 루프를 돌며 처리한다. (텍스트박스용 스트링에 내용 추가, 테이블뷰용 배열에 내용 추가)
                    while(results?.next() == true) {
                        title = (results?.string(forColumn: "title"))!
                        resTxt += title + "\n"
                        titleArray.append(title)
                    }

                    // 텍스트박스에 내용을 표시한다.
                    hText.insertText(resTxt, replacementRange: NSMakeRange(0,hText.attributedString!().length))
                }
            } else {
                print("Error: \(myDB!.lastErrorMessage())")
            }

            // 변경된 배열 데이터를 테이블 뷰에서 다시 불러오는 함수
            htableView.reloadData()
        }
    }
}



7. 텍스트박스에 결과 출력

텍스트박스에 insertText함수를 사용해 결과를 출력했다. 첫번째 파라미터는 표시할 스트링, 두번째 파라미터는 텍스트가 표시될 범위를 의미하는데 이 파라미터로 입력된 NSMakeRange(0,hText.attributedString!().length)는 기존 데이터의 처음부터 끝까지를 범위로 설정한다는 의미다. 더 알고싶다면 공식 레퍼런스를 참조.


이제 그 다음은 테이블 뷰. 여기가 가장 어려웠다.


8. 테이블 뷰에 결과 출력

코코아에서 테이블뷰에 데이터를 띄우는 방식은 좀 새롭다.

테이블 뷰에 addData 같은 함수를 사용해서 하나하나 추가하는 것이 아니라 (이렇게 생각하고 접근했는데 도저히 안되더라) 데이터 소스를 만들어 컨트롤과 연결하면 테이블 뷰의 내용은 자동으로 변경된다. 값을 바꾸려면 데이터 소스만 변경하면 되고, 다만 적절한 타이밍에 ReloadData를 호출해 주어야 한다.


테이블 뷰는 스크롤 뷰 하위에 테이블 뷰, 테이블 뷰 하위에 컬럼뷰가 존재한다. 먼저 컬럼 헤더의 숫자와 캡션을 설정한다. (테이블뷰를 선택하고, 오른쪽의 Inpector 패널에서 변경할 수 있다)

그 다음은 delegatedatasource 를 구현한다. View Controller 아래에 다음과 같은 내용을 추가한다.


그 후 아래 코드처럼 구현된 delegatedatasource를 컨트롤과 연결시켜주어야 하고, 데이터가 변경된 이후 갱신 함수를 실행해야 하는데, 이부분은 이미 6번의 코드에 모두 포함되어 있다. (6번 코드의 viewDidLoad함수와 가장 아래쪽 reloadData함수 실행 부분)

extension ViewController: NSTableViewDataSource {
    
    func numberOfRows(in tableView: NSTableView) -> Int {
        return titleArray.count 
    }
    
}

extension ViewController: NSTableViewDelegate {
    
    fileprivate enum CellIdentifiers {
        static let titleCell = "titleCellID"
    }
    
    func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
        
        var text: String = ""
        var cellIdentifier: String = ""
        
        // 데이터가 없을 경우
        var item : String?
        if titleArray[row] == "" {
            return nil
        } else {
            item = titleArray[row]
        }
        
        // 첫번째 열 처리
        if tableColumn == tableView.tableColumns[0] {
            text = item!
            cellIdentifier = CellIdentifiers.titleCell
        }
        
        // 테이블 뷰 출력
        if let cell = tableView.make(withIdentifier: cellIdentifier, owner: nil) as? NSTableCellView {
            cell.textField?.stringValue = text
            return cell
        }
        return nil
    }
}



9. 결과

sqlite db에 들어있는 데이터가 잘 표시된다.


반응형

'프로그래밍 스터디' 카테고리의 다른 글

자바스크립트 콘솔에 서식 적용하기  (0) 2017.02.19