WiFi를 통한 아두이노 활용 (18) : 아이폰 연동 #2

2017. 8. 29. 09:06

Arduino/Wireless

Arduino로 iphone에 데이터 전송하기 #2

이전 글에 이어서, 간단한 아이폰 앱을 만들어 아두이노가 제공하는 XML 데이터를 처리하도록 하겠습니다.

Xcode 실행 및 새로운 App 생성

아이폰, 아이패드 등 ios app을 개발하기 위해선 두 가지가 있어야 합니다.

하나는 Mac 컴퓨터입니다. 윈도우 PC에서 에뮬레이션 하는 방법도 있다지만 원활한 개발을 위해선 Mac이 있어야 할 겁니다. 비싼 가격이 문제인데, 이전 세대의 맥북이나 맥북에어 중에 화면 사이즈 작은 것을 중고로 구해서 시작하는 것도 좋은 방법입니다. 어차피 처음부터 무거운 작업을 하진 않을 테고, 최적화가 잘 되어 있어 그다지 답답하지 않게 느껴집니다.

또 하나 필요한 건 개발자 라이선스입니다. 1년에 $99인데, 개발자도 아닌데 참 부담되는 금액입니다. 실제로 Device에 App을 내보내기 전까지 시뮬레이션만 돌린 다면 라이선스는 필요없으므로, 우선 맥북부터 구해서 충분히 연습 먼저 하면 될 듯 합니다. 다행히, 애플에서 제공하는 개발툴인 Xcode는 무료입니다.

맥에서 본 Xcode 아이콘입니다. 그리고, 오른쪽에 Simulator가 보이는데, Apple의 모든 Device를 시험해 볼 수 있습니다.

Xcode를 처음 실행하면 보게 되는 Launcher 화면입니다. 화살표가 가리키고 있는 Create a new Xcode project 메뉴를 클릭하여 새로운 프로젝트를 생성하는 과정으로 이동합니다.

새 프로젝트를 생성하는 화면입니다. iOS의 가장 일반적인 프로그램 형태인 Single View Application을 선택하고 Next를 누릅니다. iOS 개발에 대한 글이 아니기 때문에 Xcode에 대한 설명은 간단하게 하겠습니다.

프로그램 이름과 저장할 위치를 지정하고 나면 위와 같이 Xcode UI를 볼 수 있습니다.

Button 추가

가운데 보이는 앱 캔버스에 버튼 하나를 추가하겠습니다. 버튼 이름은 Request로 하고, 클릭할 때마다 아두이노에 XML 데이터를 요청합니다.

Xcode 화면 왼쪽에 회색 바탕의 디렉토리 구조가 있습니다. Navigate라고 하는 부분이고, 그 중 첫 번째 아이콘이 Project Navigator라고 프로젝트와 관련된 파일들을 계층적으로 보여 줍니다. 디렉토리에서 Main.storyboard를 선택합니다. Main.storyboard는 UI 요소를 배치하는 설정하는 내용을 담고 있습니다.

화면에 필요한 UI 요소들을 배치하려면, Xcod 오른쪽 아래 Library 영역에서 세 번째 아이콘을 선택하여 Object Library 화면을 보이게 합니다. 여기에서, 필요한 UI object를 아이폰 모양의 View 영역에 드래그 하면 됩니다.

Button object를 드래그해서 올리면 그림처럼 안내선이 나타나서 쉽게 위치 시킬 수 있습니다. 화면 하단에 배치하고 크기를 조절하였습니다.

배치한 오브젝트에 대한 설정은 Xcode 화면 오른쪽 상단의 Inspector 영역에서 가능합니다. 그림에서 보이는 부분은 Attributes Inspector로 주로 오브젝트의 모습과 관련된 속성들을 설정합니다.

몇 가지 속성을 변경해서 위와 같은 버튼 모양을 만들었습니다.

storyboard에서의 작업은 단지 화면 레이아웃만 변경합니다. 그래서, 위에 만든 버튼은 아직 기능이 없습니다.

Xcode에서 Application은 크게 세 가지 개념으로 구성됩니다. 화면에 보이는 부분인 View, 내부적인 동작과 관련된 코드, 그리고 둘 사이를 연결하여 관리하는 View Controller입니다. 바로 위에서 작업한 내용은 뷰 컨트롤러에 뷰 요소(버튼)를 올린 것이고 아직 코딩을 통한 기능 구현은 안된 상태입니다. 새로운 프로젝트가 생성되면 하나의 뷰 컨트롤러가 기본으로 생성되어 있습니다.

세 가지 개념과 맞물려서, 실제로 작업할 때는, 뷰 컨트롤러에 뷰 오브젝트를 올리고, 그에 맞는 코드를 작성한 후, 반드시 오브젝트와 코드를 적절하게 연결해 줘야 합니다. Connections Inspector라는 곳에서 연결할 수도 있고, 드래그 앤 드롭으로 쉽게 연결할 수도 있습니다.

drag & drop으로 쉽게 연결하려면 우선 storyborad 윈도우와 코딩 파일인 ViewController.swift 윈도우가 같이 열려 있어야 합니다. Main.storyboard가 선택된 상태에서 왼쪽 Project Navigator에서 ViewController.swift를 option key를 누른 채 클릭하면 화면 오른쪽에 해당 코딩 윈도우가 보일 겁니다.

위와 같이 버튼을 control key를 누른 상태에서 코드 창으로 드래그하면 되고, 적절한 위치에 오면 안내 메시지가 나타납니다.

drag & drop을 끝내면 왼쪽과 같이 대화상자가 팝업합니다. 오른쪽처럼 Connection type을 Action으로 변경하고 Type은 UiButton, 그리고 적당한 이름을 입력 후 Connect 버튼을 클릭합니다. 이렇게 하면 Button object가 ViewController.swift의 해당 함수와 연결이 되어 지정한 이벤트가 버튼에 발생하면 함수를 실행합니다.

위와 갈이 ViewController.swift 파일에 해당 코드가 추가되었습니다. @IBAction은 Interface Builder Action의 약자입니다. storyboard에 button object를 배열하던 화면이 바로 UI를 설계하는 Interface Builder입니다. 이 곳의 특정 object와 연결된 함수를 뜻하며, 함수 이름은 request, 오브젝트 타입은 UIButton임을 알 수 있습니다.

drag 방식이 아니라면 위 코드창에 직접 같은 내용을 타이핑 하고, Interface Builder에서 해당 버튼을 선택한 후, Connections Inspector화면에서 필요한 Event와 연결해야 하는데, 위 그림의 오른쪽이 바로 Connections Inspector 화면이고 이벤트와 자동으로 연결되어 있음을 확인할 수 있습니다. 연결된 이벤트는 'Touch Up insdie' event이고 버튼을 클릭할 때 발생합니다.

import UIKit
class ViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    @IBAction func request(_ sender: UIButton) {
        print("push!")
    }
}

여기까지의 코드 내용이고 print("push") 함수외에는 모두 자동으로 생성되었습니다. print()는 Xcode 화면 아래쪽의 Console창에 메시지를 출력해주는 함수인데, 테스트시에 디버깅하기에 좋습니다.

button이 잘 작동하는지 테스트 한번 해보겠습니다. 왼쪽 상단의 play 버튼을 클릭하거나, Product 메뉴에서 Run(command + R)을 선택합니다. 플레이 버튼 오른쪽으로 어떤 장치에서 실행할 지 선택하는 Drop-down list가 있습니다(오른쪽 그림 참고). 실제 아이폰을 USB로 연결했을 경우 리스트 상단에서 선택할 수 있고, 보통은 시뮬레이터에서 확인합니다. 오른쪽 리스트에 보이는 파란색 아이콘이 모두 시뮬레이션할 장치명입니다.

원하는 디바이스를 선택한 후 실행하면 실제 기기처럼 시뮬레이터가 부팅부터 시작합니다. 따라서 처음 실행시에는 시간이 좀 필요하고, 시뮬레이션 장치를 변경하면 역시 해당 기기의 부팅부터 시작합니다.

Simulator에서 iphone 6 plus를 선택하고 실행한 모습입니다. Request 버튼을 클릭하면 오른쪽 그림처럼 print() 함수에서 출력한 결과가 콘솔창에 나타납니다.

XML Request & Parser

이제 아두이노 웹서버에 XML 데이터를 요청하는 client request 명령을 작성하겠습니다.

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    @IBAction func request(_ sender: UIButton) { 
        let urlString = "http://192.168.1.65"
        let url: URL = URL(string: urlString)! 
        print(url)
    }
}

"Request" 버튼을 눌렀을 때 XML request가 발생하도록 위에서 만든 @IBAction func request() 내에 작성합니다. 테스트하기 위해 입력했던 print() 함수를 밀어 내고 두 개의 상수, urlString, url을 선언합니다.

Swift언어에서 상수 선언은 "let" 키워드를 이용해 선언합니다. urlString은 아두이노 WiFi module의 IP주소로 초기화하였습니다. 형식은 IP주소이지만 단순한 문자열이므로 urlString은 String type으로 선언됩니다.

urlString에 저장된 주소를 실제로 사용하기 위해선 URL type으로 변환해야 합니다. 상수 url은 URL type으로 선언하였고, urlString의 값을 URL type으로 변환하여 초기화하였습니다. url에 직접 주소를 이용해 초기화값을 줄 수도 있지만, 주소내용을 변경할 수도 있기 때문에 분리하여 선언하는 게 좋습니다.

let url 선언문 줄 끝에 느낌표(!)가 있습니다. 이는 간단하게 표현하면 URL( )의 주소 변환이 실패할 경우 url에 nill(null)값을 넣으라는 의미입니다. "Optional unwrapping"이라고 하는 Swift 언어의 특징 중 하나입니다. 마지막으로 주소값이 제대로 저장되었는지 확인하기 위해 print(url) 을 이용해 콘솔로 출력하도록 했습니다.

이제 저장한 주소로 아두이노 서버에 XML request를 보내야 하는데 그 전에, XML처리기에 의해 파싱된 XML 문서를 어디로 보내야 할지 알려줘야 합니다. 이런 역할을 하는 것이 XMLParserDelegate Protocol입니다. 프로토콜은 일종의 인터페이스로, 앞서 말한 내용대로 파싱된 문서를 delegate 이름 그대로 누가 맡아서 처리할 것인지 알려주는 역할을 합니다. 프로토콜의 선언은 해당 클래스의 클래스명 오른쪽에 콜론(:)을 쓰고 나열합니다.

class ViewController: UIViewController, XMLParserDelegate  {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    @IBAction func request(_ sender: UIButton) { 
        let urlString = "http://192.168.1.65"
        let url: URL = URL(string: urlString)! 
        print(url)
    }
}

첫째 줄을 보면 XMLParserDelegate protocol이 선언되었습니다. 그리고, XML 문서의 파싱은 자동으로 처리되고 delegate가 받아서 처리하며 이 ViewController class가 delegate 가 됩니다.

import UIKit
class ViewController: UIViewController, XMLParserDelegate {
    var parser = XMLParser()

우선, 클래스 상단에, XML 파서 처리기의 인스턴스를 담을 변수를 하나 선언합니다.

@IBAction func request(_ sender: UIButton) { 
    let urlString = "http://192.168.1.65"
    let url: URL = URL(string: urlString)! 
    print(url) 
    parser = XMLParser(contentsOf: url)!
    parser.delegate = self
}

@IBAction func request () 함수를 계속해서 작성하겠습니다. 요청할 주소를 저장한 후에, 그 주소로 XMLParser 인스턴스를 생성하여 위에서 선언한 parser 변수에 담습니다. 그리고, 델리게이트를 지정해줘야 하는데, 자신이 속한 클래스이므로 self로 지정합니다.

@IBAction func request(_ sender: UIButton) { 
    let urlString = "http://192.168.1.65"
    let url: URL = URL(string: urlString)! 
    print(url) 
    parser = XMLParser(contentsOf: url)!
    parser.delegate = self
    let result: Bool = parser.parse() 
    if result {
        print("Parsing success!")
    } else {
        print("Parsing fail!")
    }
}

parser.parse()는 이미 지정된 주소로 XML문서를 요청하고 전송받은 문서를 Parsing하는 명령입니다. 그리고, 파싱이 성공했는지 실패했는지를 Bool type으로 반환합니다. let result: Bool 상수는 이 반환값을 받기 위해 선언하였고, 이 값을 IF문을 통해 체크하는 코드가 이어서 붙습니다.

이렇게 몇 줄의 코딩으로 특정 서버에 XML request를 보내고 받은 문서를 파싱하는 처리까지 구현할 수 있습니다. 이제 parser가 보내주는 이미 파싱된 데이터를 받아서 처리하는 부분만 작성하면 되는데, 이 때 사용하는 함수도 이미 구현되어 있는 것을 가져다 쓰기만 하면 되기에 어렵지 않게 끝낼수 있습니다.

Parsing된 데이터의 처리

파싱된 데이터는 쉽게 말해서 전송받은 데이터에서 start tag, end tag, contents를 추출하여 보내준 데이터입니다. 전에 openweathermap.org의 날씨 정보를 XML로 받아 처리하는 부분에서, 시작태그인지 종료태그인지 등을 구분하는 코딩을 했었는데, 여기서는 그럴 필요도 없는 것입니다.

func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
    print(elementName)
} 
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
    print(elementName)
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
    print(string)
}

위의 세 가지 함수를 ViewController class 선언 바깥쪽 아래에 붙여 주면 끝입니다. 우선 각 함수마다 들어 있는 print() 함수는 파싱이 제대로 되었는지 확인하기 위해 따로 추가한 부분입니다.

첫 번째 함수의 인수 정의 부분(괄호 안)에 didStartElement elementName: String이라는 부분이 있습니다. XML 처리기가 파싱하여 보내준 데이터가 시작태그라면(didStartElement) 이 함수에서 처리한다는 뜻입니다. 그리고, 그 태그 데이터는 elementName이라는 변수에 String type으로 저장되어 들어 옵니다. 그래서, print(elementName)이라 코딩하면 넘겨 받은 시작태그를 그대로 콘솔에 출력해 줍니다.

두 번째 함수는 didEndElement입니다. 마찬가지로 종료 태그라면 여기서 처리한다는 뜻이고, 마지막 함수는 약간 다른데, foundCharacters는 시작태그와 종료태그 사이의 컨텐츠에 대한 얘기입니다. 컨텐츠 부분에서 문자열을 추출했다면 String type의 변수 string에 저장해서 보내준다는 뜻입니다. 역시 print문을 통해서 콘솔로 확인하도록 했습니다.

여기까지 코딩 몇 줄만으로 XML 요청 및 처리에 대한 코드가 모두 준비되었습니다. 마지막 세 가지 함수는 몇 글자만 치면 자동으로 완성까지 해주므로 생각보단 간단하게 구현이 가능합니다.

//
//  ViewController.swift
//  arduinoWeather
//
//  Created by turtle on 2017. 8. 25..
//  Copyright © 2017년 turtleShell. All rights reserved.
//
import UIKit
class ViewController: UIViewController, XMLParserDelegate {
    var parser = XMLParser() 
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    @IBAction func request(_ sender: UIButton) { 
        let urlString = "http://192.168.1.65"
        let url: URL = URL(string: urlString)! 
        print(url) 
        parser = XMLParser(contentsOf: url)!
        parser.delegate = self 
        let result: Bool = parser.parse() 
        if result {
            print("Parsing success!")
        } else {
            print("Parsing fail!")
        }
    } 
    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        print(elementName)
    } 
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        print(elementName)
    } 
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        print(string)
    }
}

여기까지의 ViewController.swift 전체 소스와 실행한 후의 콘솔 내용입니다. 제대로 처리되는 것을 확인할 수 있습니다. 파싱된 태그는 꺾은 괄호 없이 태그이름만 전송됩니다.

이번 글은 여기서 줄이고, 다음 글에선 마지막으로 파싱된 데이터를 콘솔이 아닌 아이폰 화면에 출력하는 부분과 PickerView를 통해 원하는 도시를 선택하여 요청하는 부분을 작성하겠습니다. 이상입니다.

Comments