iOS 개발을 시작하고나서 가장 궁금한 점이 App 이 백그라운드로 넘어 갔을때의 행위여서 이와 관련된 내용을 질문으로도 iOS 커뮤니티에 올려보았고 대답이 달리기전에  나도 무임승차 할 수는 없어서 조사를 해보았다. 참고로 질문 내용은 아래와 같다.


이번에 iOS 로 처음 앱개발을 해보는 중인데요.  (swift 3.0 기반) 

서버로 부터 메세지를 전달 받아서 (PUSH)  사용자에게 알려 주려고 합니다.

iOS 에서 제공하는 푸쉬서비스를 사용하지 않고 개발하려 하는데요.


일단 DispatchQueue.global 를 하나 만들고 그 안에서 서버와 커넥션을 맺은후 (TCP) 

서버로부터 데이터를 받으면 localnotification 을 통해서 사용자에게 알리는 형태입니다.


이게 앱이 전면에 있을 경우는 아주 잘 되지만 , 백그라운드로 넘어가면 멈추더군요. 
아마 iOS 정책이 그런듯 싶은데.. 


질문1. 백그라운드 넘어가서도 유지되는 쓰레드를  iOS 에서 구현 할 수 있는지요?

질문2. 보통 자신이 push 구현 할 때 어떻게 구현하나요? (백그라운드시에도 계속 서버와 연결되어 있어야 할텐데요)

질문3. 백그라운드 시는 무조건  APNS ,GCM, FCM 같은 것을 이용해야 하나요? 
질문4. 즉 나의 서버에서 직접 보내고 스마트폰에서 직접 받는 방식은 불가능한가요?( 핵심 질문)

질문5. 혹시 이러한 내용을 구현하기 위해 Background Modes 를 활성화 시켜서 사용하는지요? 




이 글은 아래의 글들을 참고,번역하여 작성되었다. (참고로 백그라운드 모드로 푸쉬 서비스를 대체 할 수는  없는 듯) 


백그라운드 실행이란 

스마트폰 사용자가 앱을 사용하지 않을 경우 시스템은 앱을 백그라운드 상태로 이동시킵니다. (홈버튼클릭) 많은 앱의 경우 백그라운드 상태는 일시 중지로 앱에 대한 간단한 중지입니다. 앱 일시 중지는 배터리 수명을 향상시키는 방법이기도 하므로 시스템에서 중요한 시스템 리소스를  사용자가 사용 해야하는 새로운 foreground 앱을 위해 할당합니다.

대부분의 앱은 정지 상태로 이전하면 그만이기도 하지만 앱이 백그라운드에서 계속 실행되는 정당한 이유도 있습니다. 하이킹 앱은 시간 경과에 따라 사용자의 위치를 ​​추적하여 하이킹지도 위에 해당 코스를 표시 할 필요가 있습니다. 오디오 앱은 잠금 화면에서 계속 음악을 재생해야 할 수 있습니다. 어떤 앱은 백그라운드에서 콘텐츠를 다운로드하여 해당 콘텐츠를 사용자에게 제공 할 때 지연을 최소화 할 수 있습니다. 앱을 백그라운드에서 실행하는 것이 필요하다고 판단되면 iOS는 시스템 리소스 또는 사용자 배터리를 소모하지 않고 효율적으로 수행 할 수 있도록 도와줍니다. iOS에서 제공하는 기술은 세 가지 범주로 나뉩니다.

* 포 그라운드에서 짧은 작업을 시작하는 앱은 앱이 백그라운드로 이동할 때 해당 작업을 완료 할 시간을 요청할 수 있습니다.

* 포 그라운드에서 다운로드를 시작하는 앱은 다운로드 관리를 시스템에 전달할 수 있으므로 다운로드가 계속되는 동안 앱이 일시 중지되거나 종료 될 수 있습니다. 

*특정 유형의 작업을 지원하기 위해 백그라운드에서 실행해야하는 앱은 하나 이상의 백그라운드 실행 모드에 대한 지원을 선언 할 수 있습니다.

사용자 경험을 향상 시키기 위해 반드시 필요한 경우가 아니라면 배경 작업을 하지 않도록 하십시오. 사용자가 다른 앱을 실행했거나 사용자가 기기를 잠그면 앱이 백그라운드로 이동할 수 있습니다. 두 경우 모두 사용자가 앱이 의미있는 작업을 수행 할 필요가 없다는 신호입니다. 이러한 조건에서 계속해서 실행되게하면 기기의 배터리가 소모되고 사용자가 앱을 완전히 종료 해버리게 만들 수 있습니다. 따라서 백그라운드에서하는 일에 유의하고 가능하면 피하십시오.

백그라운드 이동시 잠시의 추가시간을 요 하는 경우

앱이 작업 중간에 작업을 완료하는 데 약간의 시간이 필요할 경우 UIApplication 객체의 beginBackgroundTaskWithName : expirationHandler : 또는 beginBackgroundTaskWithExpirationHandler : 메서드를 호출하여 추가 실행 시간을 요청할 수 있습니다. 이 메소드 중 하나를 호출하면 일시적으로 앱 일시 중지가 지연되어 작업을 완료하는 데 약간의 시간이 소요됩니다. 작업이 끝나면 앱은 endBackgroundTask : 메소드를 호출하여 시스템이 작업을 완료하고 일시 중단 될 수 있음을 알립니다.

백그라운드에서 컨텐츠를 다운로드

파일을 다운로드 할 때 앱은 NSURLSession 객체를 사용하여 다운로드를 시작해야 앱이 일시 중지되거나 종료 될 경우 다운로드 프로세스를 제어 할 수 있습니다. 백그라운드 전송을 위해 NSURLSession 객체를 구성하면 시스템은 이러한 전송을 별도의 프로세스로 관리하고 정상적인 방법으로 상태를 다시 앱에보고합니다. 전송이 진행되는 동안 앱이 종료되면 시스템은 백그라운드에서 전송을 계속하고 전송이 완료되거나 하나 이상의 작업에 앱의주의가 필요할 때 앱을 시작합니다 (적절한 경우). 백그라운드 전송을 지원하려면 NSURLSession 객체를 적절하게 구성해야합니다. 세션을 구성하려면 먼저 NSURLSessionConfiguration 객체를 만들고 여러 속성을 적절한 값으로 설정해야합니다. 그런 다음 세션을 만들 때 해당 구성 객체를 NSURLSession의 적절한 초기화 메서드에 전달합니다.

오래동안 작업되는 일을 구현하기 위해 

구현하는 데 더 많은 실행 시간이 필요한 작업의 경우 일시 중단되지 않고 백그라운드에서 실행하도록 특정 권한을 요청해야합니다. iOS에서는 특정 앱 유형 만 백그라운드에서 실행할 수 있습니다.

*음악 플레이어 앱과 같이 백그라운드에서 사용자에게 가청 콘텐츠를 재생하는 앱

*백그라운드에서 오디오 콘텐츠를 기록하는 앱

*내비게이션 앱과 같이 항상 사용자에게 위치 정보를 계속 제공하는 앱

*VoIP (Voice over Internet Protocol)를 지원하는 응용 프로그램

*새 콘텐츠를 정기적으로 다운로드하고 처리해야하는 앱

*외부 액세서리를 정기적으로 업데이트하는 앱

이러한 서비스를 구현하는 앱은 지원하는 서비스를 선언하고 해당 서비스의 관련 측면을 구현하기 위해 시스템 프레임 워크를 사용해야합니다. 서비스를 선언하면 시스템에서 사용하는 서비스를 알 수 있지만 어떤 경우에는 실제로 응용 프로그램이 일시 중단되는 것을 막는 시스템 프레임 워크입니다.


작은 양의 콘텐츠를 기회로 가져 오기

주기적으로 새 콘텐츠를 확인해야하는 앱은 시스템에 깨어나서 해당 콘텐츠에 대한 가져 오기 작업을 시작할 수 있도록 요청할 수 있습니다. 이 모드를 지원하려면 Xcode 프로젝트의 Capabilities 탭에있는 Background modes 섹션에서 Background fetch 옵션을 활성화하십시오. 앱의 Info.plist 파일에 가져 오기 값이있는 UIBackgroundModes 키를 포함시켜이 지원을 활성화 할 수도 있습니다.이 모드를 사용한다고해도 시스템이 앱에 백그라운드 가져 오기를 수행 할 수있는 시간을 보장 할 수는 없습니다. 


푸시 알림을 사용하여 다운로드 시작

앱에서 새 콘텐츠를 사용할 수 있을 때 서버가 사용자의 기기에 푸시 알림을 보내는 경우 백그라운드에서 앱을 실행하도록 시스템에 요청하여 새로운 콘텐츠를 바로 다운로드 할 수 있도록 할 수 있습니다. 이 백그라운드 모드의 의도는 사용자가 푸시 알림을 볼 때와 앱이 관련 콘텐츠를 표시 할 수있을 때까지 경과하는 시간을 최소화하는 것입니다. 앱은 대개 사용자가 알림을 보는 것과 거의 같은 시간에 깨어나지 만, 그렇지 않은 경우보다 많은 시간을 제공합니다.

이 백그라운드 모드를 지원하려면 Xcode 프로젝트의 Capabilities 탭에있는 Background modes 섹션에서 Remote notification 옵션을 활성화하십시오. (앱의 Info.plist 파일에 원격 알림 값이있는 UIBackgroundModes 키를 포함 시켜서이 지원을 사용할 수도 있습니다.)

다운로드 작업을 시작하기위한 푸시 알림의 경우 알림의 페이로드에는 해당 값이 1로 설정된 콘텐츠 사용 가능한 키가 포함되어야합니다. 키가 있으면 시스템이 백그라운드에서 앱을 시작 (또는 백그라운드로 시작)하고 앱 위임자의 애플리케이션 : didReceiveRemoteNotification : fetchCompletionHandler : 메소드를 호출합니다. 해당 메소드를 구현하면 관련 콘텐츠를 다운로드하여 앱에 통합해야합니다.


백그라운드에 있는 동안 사용자의 관심을 끄는 방법 (알림)

알림은 일시 ​​중지되었거나 백그라운드에 있거나 실행 중이 아닌 앱에 대해 사용자의 관심을 끌 수 있는 방법입니다. 앱은 로컬 알림을 사용하여 알림을 표시하고, 소리를 내며, 앱의 아이콘에 배지를 부여하거나, 세 가지를 조합하여 사용할 수 있습니다. 예를 들어 알람 시계 앱은 로컬 알림을 사용하여 알람 소리를 재생하고 알람을 표시하여 알람을 비활성화 할 수 있습니다. 알림이 사용자에게 전달되면 사용자는 정보가 앱을 포 그라운드로 되돌려 놓을지를 결정해야합니다. 앱이 이미 포 그라운드에서 실행중인 경우 로컬 알림은 사용자가 아닌 앱에 조용히 전달됩니다.

로컬 알림 전달을 예약하려면 UILocalNotification 클래스의 인스턴스를 만들고 알림 매개 변수를 구성한 다음 UIApplication 클래스의 메서드를 사용하여 일정을 예약하십시오. 로컬 알림 개체에는 전달할 알림 유형 (사운드, 경고 또는 배지) 및 전달할 시간 (적용 가능한 경우)에 대한 정보가 들어 있습니다. UIApplication 클래스의 메서드는 즉시 또는 예약 된 시간에 알림을 전달하는 옵션을 제공합니다.

백그라운드로 앱이 실행되는 상황 이해하기 

백그라운드 실행을 지원하는 앱은 들어오는 이벤트를 처리하기 위해 시스템에서 다시 시작할 수 있습니다. 사용자 강제 종료 이외의 다른 이유로 앱이 종료 된 경우 다음 이벤트 중 하나가 발생하면 시스템이 앱을 실행합니다.

위치 앱용 :
-시스템은 앱의 구성된 게재 기준을 충족하는 위치 업데이트를 수신합니다.
-장치가 등록 된 영역에 들어가거나 나갔습니다. 지역은 지역 또는 iBeacon 지역 일 수 있습니다.

오디오 응용 프로그램의 경우 오디오 프레임 워크는 일부 데이터를 처리하기 위해 응용 프로그램이 필요합니다. 오디오 앱에는 오디오를 재생하거나 마이크를 사용하는 앱이 포함됩니다.

블루투스 앱의 경우 :
 - 중앙 역할을하는 앱은 연결된 주변 기기에서 데이터를 수신합니다.
 - 주변 역할로 작동하는 앱은 연결된 중앙에서 명령을받습니다.

백그라운드 다운로드 앱 :
 - 푸시 알림이 앱에 도착하고 알림의 페이로드에 값 1이있는 콘텐츠 사용 가능한 키가 포함됩니다.
 - 시스템은 새로운 콘텐츠를 다운로드하기 위해 기회가되는 순간에 앱을 깨 웁니다.
 - NSURLSession 클래스를 사용하여 백그라운드에서 콘텐츠를 다운로드하는 앱의 경우 해당 세션 개체와 관련된 모든 작업이 성공적으로 완료되거나 오류가 발생했습니다.
 - 뉴스 스탠드 앱이 시작한 다운로드가 완료됩니다.

좋은 백그라운드 앱이 되려면 

포 그라운드 앱은 시스템 리소스 및 하드웨어 사용과 관련하여 항상 백그라운드 앱보다 우선합니다. 백그라운드에서 실행중인 앱은 이러한 불일치에 대비하고 백그라운드에서 실행될 때 동작을 조정해야합니다. 특히 배경으로 이동하는 앱은 다음 가이드 라인을 따라야합니다.

* 코드에서 OpenGL ES 호출을하지 마십시오.
* 일시 중지되기 전에 Bonjour 관련 서비스를 취소하십시오.
* 네트워크 기반 소켓의 연결 오류를 처리 할 수 ​​있도록 준비하십시오. 여러 가지 이유로 앱이 일시 중지 된 상태에서 시스템이 소켓 연결을 끊을 수 있습니다.
* 백그라운드로 이동하기 전에 앱 상태를 저장하십시오. 메모리가 부족한 상태에서 배경 응용 프로그램을 메모리에서 제거하여 여유 공간을 확보 할 수 있습니다.
배경으로 이동할 때 불필요한 오브젝트에 대한 강력한 참조를 제거하십시오. .
* 일시 중단되기 전에 공유 시스템 자원 사용을 중지하십시오.
* 외부 액세서리에 대한 연결 알림 및 연결 해제 알림에 응답합니다.
* 백그라운드로 이동할 때 활성 경고에 대한 리소스를 정리합니다.
* 백그라운드로 이동하기 전에보기에서 중요한 정보를 제거하십시오.
* 백그라운드에서 실행하는 동안 최소한의 작업을 수행하십시오.

예제 ) Background Modes Tutorial: Getting Started 




import Foundation



public struct Queue<T> {

    

    private var list = LinkedList<T>()

    

    public var isEmpty: Bool {

        return list.isEmpty

    }

    

    

    // 뒤에 삽입하기  

    public mutating func enqueue(_ element: T) {

        list.append(value: element)

    }

    

    

    // 앞에서 가져오기 (가져오고 삭제)

    public mutating func dequeue() -> T? {

        guard !list.isEmpty, let element = list.first else { return nil }

        

        list.remove(node: element)

        

        return element.value

    }

    

    // 앞에서 가져오기 (가져오고 유지) 

    public func peek() -> T? {

        return list.first?.value

    }

}



extension Queue: CustomStringConvertible {

    

    public var description: String {

        

        return list.description

    }

}


import Foundation



public class Node<T> {

    var value: T

    var next: Node<T>?

    weak var previous: Node<T>?

    

    init(value: T) {

        self.value = value

    }

}


public class LinkedList<T> {


    private var head: Node<T>?

    private var tail: Node<T>?

    

    public var isEmpty: Bool {

        return head == nil

    }

    

    public var first: Node<T>? {

        return head

    }

    

    public var last: Node<T>? {

        return tail

    }

    

    public func append(value: T) {

        let newNode = Node(value: value)

        if let tailNode = tail {

            newNode.previous = tailNode

            tailNode.next = newNode

        } else {

            head = newNode

        }

        tail = newNode

    }

    

    public func nodeAt(index: Int) -> Node<T>? {

        if index >= 0 {

            var node = head

            var i = index

            while node != nil {

                if i == 0 { return node }

                i -= 1

                node = node!.next

            }

        }

        return nil

    }

    

    public func removeAll() {

        head = nil

        tail = nil

    }

    

    public func remove(node: Node<T>) -> T {

        let prev = node.previous

        let next = node.next

        

        if let prev = prev {

            prev.next = next

        } else {

            head = next

        }

        next?.previous = prev

        

        if next == nil {

            tail = prev

        }

        

        node.previous = nil

        node.next = nil

        

        return node.value

    }

}


extension LinkedList: CustomStringConvertible {

    public var description: String {

        var text = "["

        var node = head

        

        while node != nil {

            text += "\(node!.value)"

            node = node!.next

            if node != nil { text += ", " }

        }

        return text + "]"

    }

}



iOS 앱개발과 스위프트라는 언어를  처음 시작하면서 소켓 통신 관련해서 좀 애를 먹고있다. 기능은 인증/푸쉬/전광판/사용히스토리/지도/음성인식/설정 정도인  prototype 앱을 2주안에 개발해야하는 촉박한 일정 탓에 약간 멘붕상태에서 봉주르,코코아,파운데이션,Object-C, Swift 2.0 / 3.0 , NSStream, 게임키트 같이 처음 시작하는 사람이 보기에 너무 많은 정보 속에 과연 어떻게 하면 쉽고 빠르게 개발을 할 수 있을까 하며 이것저것 뒤적뒤적 거렸는데 먼저 NSStream 을 이용해 봤다. 이게 가장 적합해 보였고 대략 코딩은 이러하다. (참고로 가장 쉬운 방법은 글 중간에 있으니..인내를)


let addr = "192.168.1.45" let port = 22090 var inp : InputStream? var out : OutputStream? Stream.getStreamsToHost(withName: addr, port: port, inputStream: &inp, outputStream: &out) let inputstream = inp! let outputstream = out! inputstream.open() outputstream.open() outputstream.write(query,maxLength:query.characters.count) let buffersize = 1024 var buffer = [UInt8](repeating :0, count : buffersize) let bytesRead = inputstream.read(&buffer, maxLength: buffersize) var getString : NSString? if(bytesRead>0){ getString = NSString(bytes: buffer, length: bytesRead, encoding: String.Encoding.utf8.rawValue) return getString as! String }else{ return PKConst.connection_fail } inputstream.close() outputstream.close()

이렇게 자바IO 스타일로 코딩을 했는데 뭐 주고 받는것은 문제가 없으나 문서를 다시 읽어 본 결과  Streme 은 ( 참고로 swift 3.0 에서는 NS 같은 쓸데없는 prefix 가 빠졌다. 속이 다 시원하다.)  아래처럼 RunLoop 등 비동기식으로  써야 맞는거다.

 inputstream.delegate = self

 outputstream.delegate = self

        

 inputstream.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)
 outputstream.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)

NSStreamEventOpenCompleted
NSStreamEventHasSpaceAvailable
NSStreamEventHasBytesAvailable
NSStreamEventEndEncountered
NSStreamEventErrorOccurred

이런게 있는거 봐선 느낌상 Reactor 패턴식 (멀티플렉싱) 으로 통신하는게 정석인듯 싶은데 점점 골치 아파 오기 시작한다. Object-C 는 완전한 예제들이 있는듯한데 개인적으로 Object-C 를 굉장히 싫어하며 , 이 와중에 공부해서 포팅하고 싶지도 않다. 

참고로 swift 3.0 으로 구현된 Stream 예제는 대략 다음과 같으나 확실치 않고 추천하고 싶지 않다.
(동기적,비동기적 각각 완전한 예제를 발견하지 못했다. 좋은 예제를 가지고 계신분은 홍익인간의 정신으로 공유좀 ㅜㅜ ) 


import Foundation class Connection: NSObject, StreamDelegate { var host:String? var port:Int? var inputStream: InputStream? var outputStream: OutputStream? var sneds = Queue<String>() func connect(host: String, port: Int) { self.host = host self.port = port Stream.getStreamsToHost(withName:host, port: port, inputStream: &inputStream, outputStream: &outputStream) if inputStream != nil && outputStream != nil { // Set delegate inputStream!.delegate = self outputStream!.delegate = self // Schedule inputStream!.schedule(in: .main, forMode: RunLoopMode.defaultRunLoopMode) outputStream!.schedule(in: .main, forMode: RunLoopMode.defaultRunLoopMode) print("Start open()") // Open! inputStream!.open() outputStream!.open() } } func stream(aStream: Stream, handleEvent eventCode: Stream.Event) { if aStream === inputStream { switch eventCode { case Stream.Event.errorOccurred: print("input: ErrorOccurred: ") case Stream.Event.openCompleted: print("input: OpenCompleted") case Stream.Event.hasBytesAvailable: print("input: HasBytesAvailable") // Here you can `read()` from `inputStream` default: break } } else if aStream === outputStream { switch eventCode { case Stream.Event.errorOccurred: print("output: ErrorOccurred: ") case Stream.Event.openCompleted: print("output: OpenCompleted") case Stream.Event.hasSpaceAvailable: print("output: HasSpaceAvailable") // Here you can write() to `outputStream` default: break } } } } class SimpleSocketUtil { static func checkID ( id : String) -> Bool { let conn = Connection() conn.connect(host: "192.168.1.45", port: 22090) return true } }

여튼 이러한 와중에  나를 구원해 준것이 있었으니 SwiftSocket!!   아래를 참고하시라. 


SwiftSocket 

https://github.com/swiftsocket/SwiftSocket 

무지무지 하기 쉽다.


1. 디펜던시 추가

ytcpsocket.c / ytcpsocket.swift / ysocket.swift  이 3개를 자신의 프로젝트에 추가시킨다.


2. Connection 예제 

func connect(query : String){

   let addr = "192.168.1.45"
   let port = 22090
        
   let client:TCPClient = TCPClient(addr: "192.168.1.45", port: 22090)
   var (success,errmsg)=client.connect(timeout: 1)
   if success{
            var (success,errmsg)=client.send(str:"GET / HTTP/1.0\n\n" )
            if success{
                let data=client.read(1024*10)
                if let d=data{
                    if let str=String(bytes: d, encoding: String.Encoding.utf8){
                        print(str)
                    }
                }
            }else{
                print(errmsg)
            }
   }else{
     print(errmsg)
   }  
}
 이 보다 더 간단할 수 있겠는가? 

API 예제들을 간단히 살펴보면 아래와 같다.

api usage

create client socket

//create a socket connect to www.apple.com and port at 80
var client:TCPClient = TCPClient(addr: "www.apple.com", port: 80)

connect with timeout

var (success, errmsg) = client.connect(timeout: 10)

send data

var (success, errmsg) = client.send(str:"GET / HTTP/1.0\n\n")
//or you can send binnary data
//socket.send(data:[Int8])

read data

var data = client.read(1024*10) //return optional [Int8]

close socket

var (success, errormsg) = client.close()

create servert socket

var server:TCPServer = TCPServer(addr: "127.0.0.1", port: 8080)

listen

var (success, msg) = server.listen()

accept

var client = server.accept() //now you can use client socket api to read and write

client socket example

//创建socket
var client:TCPClient = TCPClient(addr: "www.apple.com", port: 80)
//连接
var (success, errmsg) = client.connect(timeout: 1)
if success {
    //发送数据
    var (success, errmsg) = client.send(str:"GET / HTTP/1.0\n\n" )
    if success {
        //读取数据
        var data = client.read(1024*10)
        if let d = data {
            if let str = String.stringWithBytes(d, length: d.count, encoding: NSUTF8StringEncoding){
                println(str)
            }
        }
    }else {
        println(errmsg)
    }
} else {
    println(errmsg)
}

server socket example (echo server)

func echoService(client c:TCPClient) {
    println("newclient from:\(c.addr)[\(c.port)]")
    var d = c.read(1024*10)
    c.send(data: d!)
    c.close()
}
func testserver(){
    var server:TCPServer = TCPServer(addr: "127.0.0.1", port: 8080)
    var (success, msg) = server.listen()
    if success {
        while true {
            if var client = server.accept() {
                echoService(client: client)
            } else {
                println("accept error")
            }
        }
    } else {
        println(msg)
    }
}






iOS 개발에서 스토리보드란 무엇인가? 

http://palmettowebdesign.com/blog/storyboards-app-development-ios-developers/ 


스토리 보딩은 소프트웨어 디자인, 특히 iOS 앱 개발의 경우 매우 중요합니다. iOS 개발자중 이 사실을 모르는 사람들이 꽤 많이 있습니다. 그렇기 때문에 앱 개발자가 염두에 두어야 할 다른 중요한 사항에 대해 이야기 하기 전에 먼저 스토리 보드에 대해 이야기 해 보겠습니다.


역사 (http://m.blog.naver.com/codnjs9999/220583427668 참고)

이 스토리보드 파일의 기원을 잠깐 이야기하자면 이렇습니다.

이 스토리보드 포멧의 근원은 현재 Mac OS의 조상격인 NeXTSTEP OS에 처음으로 탑재되었던, 

그래픽 유저 인터페이스를 위한 메뉴구성 툴 '인터페이스 빌더' 에 있습니다. 

현재까지도 이 이름은 Xcode 여기저기에 남아있죠. 프로그래밍을 하다보면 많이 목격하게 될 것입니다.

이 툴을 이용한 ios 인터페이스 구성 파일 확장자는 처음엔 'nib (nextstep interface builder)' 였습니다. 

이따금씩 여전히 objective-c로 앱 만드는 분들 중에는 nib파일을 이용하는 사람들도 꽤 있죠.

나중엔 이 같은 포멧이 'xib (xcode interface builder)' 라는 이름으로 많이 쓰입니다. 

nib, xib, 여전히 꽤 많이 쓰이는 포멧이니 꽤 만나겠지만, 만나도 당황하지 않아도 됩니다. 스토리보드와 거의 비슷하니까요. 스토리보드는 같은 포멧 위에서, iOS SDK 5부터 새롭게 도입된 개념입니다.


스토리보드와 xib의 가장 큰 차이점은, xib와 달리 스토리보드에서는 '실행 흐름' 을 제어할 수 있다는 점입니다. 

실행 흐름이란, 이 화면에서 저 화면으로 넘어가는 것, 즉 어떤 동작에 따라 화면이 전환되는 것을 말합니다. 

이것은 굉장히 중요하므로 잘 기억하고 이해하고 있어야 


Storyboards

일반적으로 스토리 보드는  시각적인 표현으로  말하고자 할때 굉장히 강력한 접근 방식 입니다.  그것은 특정 시퀀스가 특정 화면에 적합한 지 또는 캐릭터가 표현해야 하는 감정의 종류를 결정하기 위해 영화의 시각적 내용을 디자인하는 데 오랫동안 사용되어 왔습니다. 스토리 보딩은 시각적 제작의 상상력을 보다 쉽게 만들어 줍니다. 또한 앱이란 시각적 상호 작용이 매우 중요하므로 개발자는 다양한 화면 간의 전환이 어떻게 작동하는지 이해하면 앱 개발에 있어서 크게  도움이됩니다. 또한 앱 개발에서 수행하고자하는 주요 목표를 실현하는데도 도움이 됩니다. 즉 당신이 다음에 해야 할 일을 명확히 알려줍니다.


실제 모습 (http://m.blog.naver.com/codnjs9999/220583427668 참고)

ios 앱 프로젝트를 처음에 만들면, 왼쪽 Project Navigator 에 다음과 같은 파일들이 만들어집니다. 

(single view application 을 선택해 프로젝트를 만들었다고 가정합니다.)


 


아마 이것저것 건드려 보았던 사람이라면 알겠지만, 저 Main.stroyboard 파일이야말로 앱 제작 시에 중심이 되는 파일 중 하나입니다. 한번 저 파일을 클릭(더블클릭 아닙니다) 해보면, 오른쪽에


 


이처럼, 텅 빈 화면에 일전에 말한 View Controller 하나만 덩그러니 놓여 있죠. 바로 이곳에서 앱의 전체적인 화면 구성이 시작됩니다.앱의 기능적 측면, 즉 여러가지 알고리즘들과 데이터 입출력, 서버통신 등, 통칭 백엔드Back-End 작업들은 우리가 보통 프로그래밍이라고 생각하는 그런 작업들입니다. 파일을 열어 신나게 코드를 치고, 딱 봐서는 뭘 하는건지 이해하기 쉽지 않은 그런 작업들 말이죠.바로 우리가 차후에 swift 파일을 만들어 스위프트 언어로 직접 프로그래밍 하게 될 것들입니다.


스토리 보드 장점

스토리 보드에는 다음과 같은 여러 가지 장점이 있습니다.

- "장면"에서 모든보기 컨트롤러를 시각적으로 배치하고 그 사이의 연결을 설명 할 수 있습니다. 스토리 보드를 사용하면 앱의 모든 장면을 더 잘 개념적으로 볼 수 있습니다.

- 스토리 보드는 다양한 장면 간의 전환을 설명 할 수 있습니다. 이러한 전환을 "단편 (segues)"이라고하며 스토리 보드에 바로보기 컨트롤러를 연결하여 단절을 만듭니다. segues 덕분에 UI를 관리하는 데 필요한 코드가 줄어 듭니다.

- 스토리 보드를 사용하면 프로토 타입 셀 및 정적 셀 기능을 사용하여 테이블 뷰 작업을 훨씬 쉽게 수행 할 수 있습니다. 스토리 보드 편집기에서 작성해야하는 코드의 양을 줄여 테이블 뷰를 거의 완벽하게 디자인 할 수 있습니다.

- 스토리 보드를 사용하면 위치와 크기를 정의하는 요소간에 수학적 관계를 정의 할 수있는 자동 레이아웃을보다 쉽게 ​​사용할 수 있습니다. 이 강력한 기능을 통해 다양한 화면 크기 및 크기의 장치를 훨씬 쉽게 처리 할 수 ​​있습니다.  자동 레이아웃 자습서에서 자세한 내용을 보거나 비디오 시리즈를 시청할 수 있습니다.


스토리보드 vs Xib

XIB 사용

장점:

- UI를 빠르게 조합 할 수 있습니다.

- 최소 화면 수의 소형 응용 프로그램을위한 직선적 인 구현 - 다른 언어 (예 : 언어 또는 국가)에 대해 별도의 XIB를 사용할 수 있습니다.

- 엘리먼트를 레이아웃하고 시각적으로 불균형을 파악하는 데 매우 유용합니다. 레이아웃을 약간 조정하는 것은 쉽습니다.


단점 :

- 팀 환경에서 작업 할 때 충돌을 병합하기가 어렵습니다 (차별화, 병합 및 읽기 어려움).

- 매우 동적 구성인  경우는 XIB로 기술하는 것이 불가능합니다.

- XIB를 디스크에서 읽고 분석 / 구문 분석해야하기 때문에 코드를 통해보기를 만드는 것보다 속도가 느립니다.

-XIB에는 Quartz와 같은 코드에서 할 수있는 사용자 정의가 없습니다 (그림자, 둥근 모서리)

-Harder to debug (즉, Interface Builder에서 연결하거나 잘못된 연결을 잊어 버린 경우)


스토리 보드

장점:

- 스토리 보드는 화면이 작거나 중간 정도 인 앱에 적합하며 탐색을위한 요구 사항은보기간에 비교적 간단합니다.

- 코드를 많이 작성하지 않고 애플리케이션의 흐름을 조롱 할 수 있습니다.

단점 :

- 스토리 보드는 iOS 5 이전 버전과 호환되지 않으므로 iOS 4.3을 지원할 수 없습니다

- 모든 사람들이 동일한 파일을 수정하기 때문에 팀 환경에서 병렬로 작업하기가 어렵습니다.

같은 라인을 따라 GIT의 스토리 보드를 합치면 고통이 될 것입니다.

- 사람들은 스토리 보드를 사용하여 Xcode에서 버그를 경험했습니다 (예 : 불일치로 인해 DerivedData 폴더를 자주 비우는 등)


스토리 보드는 장면(scene) 과 세그(segue) 라는 두 가지로 표현됩니다.

장면은 뷰 컨트롤러 또는 싱글뷰를 나타냅니다. 각 장면은 콘센트를 만들기위한 도크와  뷰와 뷰 컨트롤러 간의 연결로 구성됩니다. 뷰에는 사용자가 응용 프로그램과 상호 작용하는 방법을 설명하는 객체와 컨트롤이 포함되어 있습니다. 장면들 각각은 뷰 컨트롤러들 사이의 전환/이동을 보여주는 segues와 연결됩니다. 시작 뷰와 연결 뷰의 객체 사이에 세그가 생성됩니다.

스토리 보드에서 응용 프로그램의 디자인 화면이 어떻게 보이는지 다음 그림을 참고하면  더 잘 이해할 수 있을 것 입니다.


Source

논의가 계속됨에 따라 iPhone의 경우 스토리 보드의 장면이 장치의 화면 내용과 동일하다는 것을 이해하는 것이 중요합니다. 그러나 iPad의 경우에는 여러 화면이 동시에 나타날 가능성이 있기 때문에이 점이 다릅니다.


Stages of Storyboarding

iOS 앱용 스토리 보드 만들기는 두 단계로 구성됩니다. 스토리 보딩의 첫 번째 단계에는 다음이 포함됩니다.

-응용 프로그램의 기본 화면 및 메뉴에 대한 대략적인 스케치 만들기

-주요 작업을 수행하기 위해 사용자가 애플리케이션을 통해 진행하는 방법 파악

-화면을 매핑하고 이에 따라 작업의 우선 순위 지정

-내용과 도구의 일반적인 시각적 비율 그리기

이 모든 것이 여러분의 응용 프로그램에 대한 큰 그림을 형성하는 데 도움이 될 것입니다. 이제 초기 단계를 마친 후 두 번째 단계로 넘어갑니다. 즉, 앱을 눈에 띄게 만들고 사용자 친화적 인 세부 정보를 추가합니다. 

-앱의 색상이 화면에서 화면으로 일관되게 유지되는지 확인

-탐색, 상태, 탭 표시 줄 및 도구 추가

-테이블 뷰, 미리 정의 된 버튼, 페이징 인디케이터, 슬라이더, 스위치 및 회전 휠

또한 모바일 앱에 깔끔한 탐색 경로가 있어야합니다. 수많은 종소리와 휘파람을 넣으려고 유혹하지 마십시오. 앱 기능이 혼란스러워 앱 포기로 이어질 수 있습니다.스토리 보드를 사용하면 앱 개발자는 코드를 엉망으로 만들지 않고도 훌륭한 사용자 인터페이스를 만들 수 있습니다. 이 점을 염두에 두면서 스토리 보드를 사용할 때 앱 개발자가 기억해야하는 10 가지 실용적인 포인트가 있습니다.


1 – Split Up Your Storyboards 

전체 앱의 개념을 넣은 스토리 보드 일 필요는 없습니다. 스토리 보드를 설정, 온 보딩 및 메인 보드로 분할하십시오. 이렇게하면 iOS 앱 개발 규모가 확대 될 때 여러 가지 골칫거리에서 벗어날 수 있습니다. 게다가 팀 환경에서 프로젝트 진행 상황을 논의 할 때 특히 상호 작용이 쉬워집니다.


2 – Use an Exit Segue in Your UI Storyboard

Exit Segue는 사용자가 스토리 보드에서 UIStoryBoard Segue 액션이 처음 실행 된 장소로 돌아갈 수 있도록 도와줍니다. Exit Segue가 작동하는 방식과 표준 Segue와는 다른 모습이 있습니다.

unwind segue를 만들 때 따라야 할 몇 가지 단계는 다음과 같습니다.

-unwind segue 끝에 화면 상에 나타날 뷰 컨트롤러를 선택하십시오.

-뷰 컨트롤러에 대해 선택해야하는 unwind 액션 메소드를 정의하십시오.

-unwind 동작을 시작하는 데 도움이되도록 뷰 컨트롤러를 탐색합니다.

-컨트롤을 클릭하여 버튼을 클릭하여 풀기 시작

-뷰 컨트롤러 장면 상단에있는 종료 객체로 드래그합니다.

-풀기 동작 방법을 선택하려면 관계 패널을 사용하십시오.


3 – Get a Preview Editor

iOS 앱을 제작하고 실행하면 꽤 지루할 수 있습니다. 자동 레이아웃을 처리해야하는 경우에 해당됩니다. 미리보기 편집기를 사용하면보기를 수정하고 변경 사항에 따라보기가 어떻게 반응하는지 볼 수 있습니다.보조 에디터 패널의 왼쪽 하단에있는 + 버튼을 사용하여 여러 화면 크기에서 인터페이스 디자인을 미리 볼 수 있습니다.


4 – Avoid Using Complex Controllers

컨트롤러는 많은 하위 뷰를 관리하는 데 도움이됩니다. 그러나 복잡한 컨트롤러를 사용하면 완전한 난파선이 될  위험이 있습니다. 이는 앱 개발자에게 어려움을 줄 수 있습니다. 특히 앱이 빌드된 방식을 명확하게 보여주는 시각적 편집기를 사용할 때 특히 그렇습니다. 복잡한 보기 구조가 있는 경우 활용할 수있는 몇 가지 옵션이 있습니다. 예를 들어, Zib 또는 다른 컨테이너 보기 객체와 같습니다. Zib은 가장 일반적인 선택 사항 중 하나이지만 컨테이너 뷰 객체를 사용하면 더 효과적 일 수 있습니다. Zib을 추가하는 것을 고려한다면 Zib을 초기화하기 위해 소스 파일을 동반해야합니다.


결론 

스토리 보드는 앱 개발에있어 매우 중요한 도구이며 시각적인것에 익숙한 사람들에게 매우 효과적입니다. 또한 프로토타이핑 작업 속도를 높일 수 있습니다. 스토리 보드는 코드를 엉망으로 만들지 않고 개발자가 논리를 유지하는 작업이 되도록  할 수 있습니다. 한마디로 스토리 보드는 앱 개발의 원동력입니다.


현재 많은것을 하고 있기에 앱 개발을 해야한다는 부담감에 조금은 피곤함이 몰려온다. 웹디자인,웹프론트엔드,웹벡엔드,기획,데브옵스,클라우드에 마이크로서비스식 서버개발, 데이터 가시화, 데이터 분석등을 병렬적으로 한다는건 사실 그만큼 완성도가 떨어진다는걸 뜻한다. 제품의 품질은 그것을 요구하는 상황에 따라서 다르며 그것에 의존되어 개인이 다루어야할 기술 범위 또한  달라지는데 현재는 뭐 한사람이 다 해도 상관없는 상태이긴 하지만.....ㅠㅠ 빠른 시일내에 사업이 본궤도에 올라서 분야별 전문가가 존재하길 희망해본다.

자!! 이러한 긴박한 상황하에 안드로이드와 iOS 개발을 하게 되었다. 기간은 한달. 

먼저 안드로이드는 개발을 2주동안 했다. 기능은 인증/푸쉬/전광판/사용히스토리/지도/음성인식/설정 정도의 복잡하지 않은 것들로 전광판에 보여줄 IoT 정보는 15분 스케쥴링으로 서버에 요청하여 JSON으로 가지고 오는 방식이었다. 서버는  Akka TCP 서버라 Java 기본 소켓통신 (JAVA IO) 을 이용하여 한번 콜하고 바로 커넥션을 끊는 방식을 택했다. 마찬가지로 푸쉬도 구글에서 제공하는 푸쉬를 사용하지 않고 서비스에서 always 커넥션되어 직접 받도록 하였다. 

이제 iOS 를 개발해야하는데 자료는 Object-C 가 많았지만 개인적으로 Swift 가 맘에 들었기 때문에 이것으로 구현 할 생각이다. UI를 개발하기전에 일단 가장 중요하다고 생각되는 네트워킹 및 쓰레드에 관련된 기능을 확인하기 위해 찾아보았고 그것에 해당하는 안드로이드의 AsyncTask 와 그와 비슷하다고 생각되는 iOS 의  GCD 에 대해 말할 예정이다. 이미 눈치 채셨듯이 두개(iOS 와 안드로이드) 에 대해서 잘 알지는 못하며 나 스스로를 위한 정리쯤으로 남겨놓는 글임을 밝힌다. 틀린 정보가 있을 수 있다는 야그..즉 추후에 업데이트되거나 삭제될수도 있을거 같다..


이만 잡담을 마무리하고 본론으로 들어가보자.


1. 안드로이드  AsyncTask

처음에는  AsycnTask를 사용하진 않고 그냥 자바쓰레드를 만들어서 쓰레드안에서 서버와 커뮤니케이션을 한 후에 받은 데이터는 Handler 를 통해서 UI 업데이트를 했다. 그걸 다시 AsyncTask로 바꾸진 않았지만 앞으로 개발할땐 AsyncTask 를 사용할것 같다. 왜? 조금이라도 더 편하니깐. 

무엇이 편하냐?  일단 안드로이드에서는 Activity 라는 하나의 View/controller 가 있고 (iOS에서는 UIViewController ) 그것을 다루기위한 단 하나의 쓰레드가 있다. 쓰레드가 단 하나이기 때문에 시간이 좀 걸리는 기능 (서버와의 커넥션을 해서 데이터를 가져오는) 을 그 쓰레드에서 그냥 해버리면 UI가 버벅댈 수 있으니 따로 쓰레드를 만들어서 사용해야한다. 그때 그냥 일반적인 자바 쓰레드를 하나 띄워서 거기서 데이터를 받은 후에 그 데이터를 UI 에 업데이트해야하는데 그냥 해버리면 에러가 나며 (두개의 쓰레드가 동시에 하나의 구조에 접근하게 되면 문제가 생길거라는건  유츄가능하죠.) 그때 연결고리 역할을 할 Handler 라는것을 활용한다.

* 참고로 멀티쓰레드 패턴에 대해 공부하고 싶은 분은 이 글을 한번 읽어보시길 -> 생산자 - 소비자 패턴 

서브쓰레드에서 데이터를 메인쓰레드에게 자연스럽게 넘겨주는 방식인데 다음과 같이 코딩되는데

일반쓰레드 와 Handler 예)


public
class SearchActivity extends AppCompatActivity { UpdateHandler updatehandler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); updatehandler = new UpdateHandler(); } public void OnClick(View view) { switch (view.getId()) { case R.id.button: try { ConnectThread cthread = new ConnectThread("192.168.1.x"); cthread.start(); } catch (MalformedURLException e) { e.printStackTrace(); } break; } } class ConnectThread extends Thread { String hostname; public ConnectThread(String addr) { this.hostname = addr; } public void run() { try { String result = SimpleSocketUtil.getGWLocation(....); Message msg = updatehandler.obtainMessage(); Bundle data = new Bundle(); data.putString(PKConst.connection, PKConst.connection_success); data.putString(PKConst.useStatus, result); msg.setData(data); updatehandler.sendMessage(msg); } catch(Exception ex) { ex.printStackTrace(); } } } public class UpdateHandler extends Handler { public void handleMessage(Message msg) { String is_connected = msg.getData().getString(PKConst.connection); String result = msg.getData().getString(PKConst.useStatus); if (is_connected.equals(PKConst.connection_success)){ onUpdateSuccess(result); } else { onConnectionFailed(); } } }

 SearchActivity 라는 메인 쓰레드와 cThread 서브쓰레드가 있으며 cThread 에서 서버측에서 데이터를 얻은후에 그 값을 SearchActivity 로 넘겨줄때 Handler 를 이용해서 넘겨주고 있다.

방식은

1. Message 객체를 만든다.
2. Bundle 객체를 만든다.
3. Bundle 객체에 넘겨줄 데이터 key, value 형식으로 담는다. 
4. Message 객체에 Bundle 를 담은 후에 
5. Handler 의 sendMessage 메소드를 통해 보내준다.
6. Handler 는 그 데이터를 받아서 UI 를 업데이트 해준다. (위 코드에서는 onUpdateSuccess 함수 내에서 업데이트) 

이다. 

근데 이 방식 말고도 다양하게 있는 듯 하고 . 더 자세히 알려면 다른 블로그를 참고하시라.

어쨋든 저렇게 하려면 쓰레드도 따로 만들어야하고 핸들러도 따로 만들어야하고 데이터를 메세지와 번들을 이용하여 따로 만들어서 보내줘야하는등 좀 할것들이 있어 보입니다. 이걸 더 간편하게 만들기 위한것이 AsyncTask  인데 

예를 봅시다.

AsyncTask 예 )


public class SearchActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

 
    public void OnClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                try {
                    new ConnectServerTask().execute("192.168.1.x");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
        }
    }

    private class ConnectServerTask extends AsyncTask<String,String,String> {
        
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected String doInBackground(String... ip) {
            String result = SimpleSocketUtil.getGWLocation(ip[0]);
            return result;  // onPostExecute 인자로 넘어감
        }

        @Override
        protected void onProgressUpdate(String... progress) {
           // doInBackgournd 에서 publishProgress 호출하면 이리로옴
        }

        @Override
        protected void onPostExecute(String result) {   // UI 업데이트용 
          super.onPostExecute()
            String is_connected  = result.getData().getString(PKConst.connection);
            String status  = result.getData().getString(PKConst.useStatus);
            if( is_connected.equals(PKConst.connection_success)){
                onUpdateSuccess(status);
            }
            else {
                onConnectionFailed();
            }
        }
    }
} 

소스 설명   

1. AsyncTask 를 상속받은 클래스를 만든다.
2. AsyncTask 객체를 만들어서 execute 로 실행시킨다. (필요한 인자전달) 
3. doInBackgournd 에서 서버에 접속하여 데이터를 가져온 후에 리턴하면 onPostExecute 의 인자로 넘겨집니다.
4. onPostExecute 메소드에서 메인쓰레드의 UI 를 업데이트 시켜준다. 


위 코드에서 중요한것은 
onPreExecute  :   실행되기전에 처리해야할것들
doInBackground  : 서브쓰레드에서 실행되는 부분 (이외의 메소드는 모두 메인쓰레드에서 사용됨) 
onProgressUpdate :  서브쓰레드에서 실행되는 진행상태에 대해 처리하는 함수
onPostExecute  :  결과에 대해서 메인쓰레드에서 실행될 부분
이 밖에 onCancelled 등 이 있으나 생략함.


또한 AsyncTask의 <> 에 들어갈 3가지 파라미터 타입은 

첫번째인자 =>  doInBackground()의 인자타입이며 execute() 에서 넘겨줄 타입이다.
두번째인자 =>  onProgressUpdate() 의 인자타입
세번째인자=>  onPostExcute() 의 인자타입. 즉 doInBackgournd 의 리턴타입이자 onPostExecute 의 인자타입이다.



2. iOS GCD (Grand Central Dispatch) 

참고 : https://www.appcoda.com/ios-concurrency/ 


GCD 는 가장 일반적으로 사용되는 쓰레딩(NSThread)을 직접구현하지 않기 위한 비동기 매니져이며 C 로 구성되었고 iOS4 부터 지원한다고 한다. GCD 는 2가지 방식으로 비동기 업무에 대한 관리를 해주며 , 내부의 쓰레드풀에서 일감을 큐에서 가져와서 처리하는데 구체적으로 살펴보자.


- serial 큐 

 이름처럼 하나의 시간에 하나의 업무만 실행되는 큐이다. 여러개의 시리얼 큐를 만들면, 동시에 일을 처리하게 만들 수도 있다. 공유리소스에 차례대로 접근하기때문에 race condition 문제가 발생하지 않는다. 또한 a 다음에 b 를 처리 하기때문에 순서가 분명한 일에 적합하다.  GCD 에서 dispatch_get_main_queue 가 순차적큐이며 UI 작업은 메인큐에서만 해야한다. 

- concurrent 큐

하나의 큐에 넣은 업무들이 동시적으로 실행된다. 따라서 순서를 보장 받을 수 없다. a 와 b 를 시작시켰는데 
무엇이 먼저 끝날지 모른다. 


serial  및 concurrent 큐에 대해 설명했는데  이제는 어떻게 사용할 수 있는지 알아보자. 기본적으로 시스템은 각 응용 프로그램에 단일 직렬 대기열과 네 개의 동시 대기열을 제공한다. 주 디스패치 큐는 응용 프로그램의 주 스레드에서 작업을 실행하는 전역적으로 사용 가능한 serial 큐입니다. 앱 UI를 업데이트하고 UIView 업데이트와 관련된 모든 작업을 수행하는 데 사용된다. 한 번에 하나의 작업 만 실행되므로 메인 큐에서 무거운 작업을 실행할 때 UI의 움직임이 멈출 수 있다. 

메인 큐 외에 시스템은 4 개의 concurrent 큐를 제공하는데 그것들을 Global Dispatch 큐 라고 한다. 이 큐는 응용 프로그램에 대해 전역이며 우선 순위 수준에 의해서만 구분됩니다. 전역 concurrent 큐 중 하나를 사용하려면 첫 번째 매개 변수 인 다음 값 중 하나를 취하는 dispatch_get_global_queue 함수를 사용하여 원하는 대기열에 대한 참조를 가져와야합니다.

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

이러한 유형은 실행 우선 순위를 나타낸다. HIGH로 설정된  큐가  가장 높은 우선 순위를 가지며 BACKGROUND가 가장 우선 순위가 낮다.. 따라서 작업의 우선 순위에 따라 사용하는 큐를 결정할 수 있게된다. 마지막으로, 임의의 수의 직렬 또는 concurrent 큐를  작성할 수 있다. concurrent 큐의 경우 네 개의 글로벌 대기열 중 하나를 사용하는 것이 좋지만 직접 만들 수도 있다..

즉 대략 코드의 모형은 이렇다. 

let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT dispatch_async(dispatch_get_global_queue(priority, 0)) { // 서버로 부터 데이터를 가져오는 코드 여기다 넣음 dispatch_async(dispatch_get_main_queue()) { // UI 를 업데이트함. } }


좀 더 구체적인 예를 가지고 말해보자.

Concurrent 큐 사용하기

이제 Xcode 프로젝트의 ViewController.swift 파일의  didClick 메서드에 이미지 다운로드를 처리한다고 하자. 

@IBAction func didClickOnStart(sender: AnyObject) {
    let img1 = Downloader.downloadImageWithURL(imageURLs[0])
    self.imageView1.image = img1
    
    let img2 = Downloader.downloadImageWithURL(imageURLs[1])
    self.imageView2.image = img2
    
    let img3 = Downloader.downloadImageWithURL(imageURLs[2])
    self.imageView3.image = img3
    
    let img4 = Downloader.downloadImageWithURL(imageURLs[3])
    self.imageView4.image = img4
    
}

각 다운로더는 하나의 작업으로 간주되며 모든 작업은 이제 메인 큐에서 수행된다. 메인큐는 알다시피 시리얼 큐이기때문에 망했다고 볼 수 있다. ;;

이제 기본 우선 순위 큐인 글로벌concurrent 큐중 하나에 대한 참조를 얻자.

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        dispatch_async(queue) { () -> Void in
            
            let img1 = Downloader.downloadImageWithURL(imageURLs[0])
            dispatch_async(dispatch_get_main_queue(), {
                
                self.imageView1.image = img1
            })
            
        }

먼저 dispatch_get_global_queue를 사용하여 기본 concurrent 큐에 대한 참조를 가져온 다음 블록 내부에 첫 번째 이미지를 다운로드하는 작업을 제출한다. 이미지 다운로드가 완료되면 메인큐에 작업을 제출하여 다운로드 한 이미지로 업데이트 한다. 즉, 이미지 다운로드 작업을 백그라운드 스레드에 하고  메인 대기열에서 UI 관련 작업을 실행 한다.

이미지의 나머지 부분도 똑같이 하면 요렇게 되겠지.

@IBAction func didClickOnStart(sender: AnyObject) {
    
    let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(queue) { () -> Void in
        
        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}

네 개의 이미지 다운로드를  기본 제공되는 concurrent  에 제출한다.  이렇게 하면 이제 이미지를 다운로드하는 동안 버벅거림 없이 슬라이더를 드래그 할 수 있게 된다.

Serial 큐 사용하기 

지연 문제를 해결하기위한 또 다른 방법은 serial 큐를 사용하는 것 인데 이제 ViewController.swift 파일에서 동일한 didClickOnStart () 메서드로 돌아가서 이미지를 다운로드하기 위한 직렬 큐를 사용해보자. 직렬 큐ㄹ 사용할 때는 참조하는 직렬 큐에 세심한 주의를 기울여야 한다. 각 응용 프로그램에는 실제로 UI의 기본 대기열인 하나의 기본 직렬 큐가 있기때문에 직렬 큐를사용할 때 새로운 큐를  만들어야하며, 그렇지 않으면 앱이 UI 업데이트 작업을 실행하는 동안 작업을 같이 실행하게 된다. 이로 인해 다양한 오류 및 지연이 발생할 수 있다.
dispatch_queue_create 함수를 사용하여 새 큐를 만든 다음 이전과 같은 방식으로 모든 작업을 제출할 수 있다. 변경 후 코드를 살펴보자
@IBAction func didClickOnStart(sender: AnyObject) {
    
    let serialQueue = dispatch_queue_create("com.appcoda.imagesQueue", DISPATCH_QUEUE_SERIAL)
    
    
    dispatch_async(serialQueue) { () -> Void in
        
        let img1 = Downloader .downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}
concurrent  큐와 다른 유일한 점은 직렬 큐 생성이다. 앱을 다시 빌드하고 실행하면 UI에 계속해서 상호 작용할 수 있도록 이미지가 백그라운드에서 다시 다운로드됨을 알 수 있다.

그러나 두 가지 사실을 알게 될 것인데 concurrent 큐의 경우와 비교하여 이미지를 다운로드하는 데 약간 시간이 걸립니다. 그 이유는 한 번에 하나의 이미지 만 로드하기 때문. 각 작업은 이전 작업이 완료되기 전에 대기한다.

이미지는 image1, image2, image3 및 image4 순서로 로드되며  이는 대기열이 한 번에 하나의 작업을 실행하는 시리얼로 작업되는 큐이기 때문이다.


스위프트 초보코너 

코드중에 ()-> Void in 으로 시작하는건 말이지. 스위프트에서 클로저가 저 모냥으로 생겼다. (여기서 클로저는 함수형프로그래밍에서 말하는 그 클로저를 말하는것은 아니다. 람다나 익명함수를 스위프트에서는 클로저로 지칭한다) 

즉  

func something() -> String {
   ... 뭔가 함..

}

이런 함수에서 함수명하고 앞에 func 를 뺀것이다. 그리고 in 을 붙히고 외곽에 {} 로 감싸면 클로저!

{ ()-> String in 

   .... 뭔가 함 ..

}


그리고 어떤 언어에서나 개발자가 사용 하기 편하게 만들어 놓은 희안하게 생긴것들이 있는데 그걸 Syntataic sugar 라고 한다. 위에서는 아래와 같은 코드가 그러한 것이다. 그냥 외워야한다. ;;;

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(queue) {


3. Swift 3.0 에서의 GCD 

swift 2 에서 dispatch_get_global_queue 이렇게 길게 써야 했던게 
swift 3 에서는 DispatchQueue.global 식으로 간단히 바뀌었다.

예를 들면 )

swift 2.0 

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(queue) { () -> Void in
        
    let img1 = Downloader.downloadImageWithURL(imageURLs[0])
    dispatch_async(dispatch_get_main_queue(), {
            
        self.imageView1.image = img1
    })
        
}
swift 3.0

 let queue = DispatchQueue.global()
       
 queue.async { () -> Void in
     let img1 = Downloader.downloadImageWithURL(imageURLs[0])
     DispatchQueue.main.async {
          self.imageView1.image = img1
     }
 }


4. GCD 를 이용한 서비스  

iOS 에는 안드로이드의 서비스 개념이 없는듯 하다. 먼가 계속 돌아가는것에 대한 부정적 입장 때문인듯 싶은데 ,이때 GCD내부에서 while 을 돌면서 먼가를 계속 수행하면 될 듯 싶다.다만 app 이 백그라운드로 가면 멈추더라~ (iOS 는 정해져있는듯)

 let queue = DispatchQueue.global()
        
        queue.async { () -> Void in  
            while true {
                sleep(1)
                // 먼가를 한다. 
                // 결과  a 가 생성 
                DispatchQueue.main.async {
                   // a 를 가지고 먼가를 한다. 
                    
                }
                
            }
            
        }
서버로부터 받은 a에 담겨진 값을 localnotification 으로 날리면 그게 바로 실시간 푸쉬서비스~ 아닐까?


5. SwiftTask  https://github.com/ReactKit/SwiftTask

이것에 대한 설명은 일단 사용해보고 난 후에 하기로 한다. 예만 저 링크 페이지에서 가져왔다.
뭔가 안드로이드의 AsyncTask 같다. -.-a


let task = Task<Float, String, NSError> { progress, fulfill, reject, configure in player.doSomethingWithProgress({ (progressValue: Float) in progress(progressValue) }, completion: { (value: NSData?, error: NSError?) in if error == nil { fulfill("OK") } else { reject(error) } }) // pause/resume/cancel configuration (optional) configure.pause = { [weak player] in player?.pause() } configure.resume = { [weak player] in player?.resume() } configure.cancel = { [weak player] in player?.cancel() } } task.success { (value: String) -> Void in // do something with fulfilled value }.failure { (error: NSError?, isCancelled: Bool) -> Void in // do something with rejected error } task.pause() task.resume() task.cancel()





http://dev.iachieved.it/iachievedit/tcp-sockets-with-swift-on-linux/원본  
(구글번역기로 번역했습니다. 아~주 약간 다듬었을뿐입니다. 엄청나게 발전했군요.) 


TCP Sockets with Swift on Linux

먼 옛날 은하계에서 멀리 떨어져있는 소프트웨어 개발자들은 TCP / IP 소켓으로 클라이언트 - 서버 애플리케이션을 작성했었습니다. 그것은 어둠의 시대 이전, HTTP 이전이었지요.

물론 농담입니다. HTTP는 다양한 클라이언트 - 서버 응용 프로그램을 제공하기 위해 활용 될 수 있으며 REST 응용 프로그램의 기반에 있습니다.  HTTP 가 테이블에 가져오는것은  와이어상에서 패킷을 직접가져오는 작업은 아니지만  합의 된 프로토콜 구조 (그리고 어느 포트가 사용되는지에 대한 표준에 어느 정도의 차이가 있음) 는 패킷에 대한 것입니다.  GET, POST, PUT 등과 같은 동작 동사와 HTTP 헤더 자체가 클라이언트 - 서버 응용 프로그램을 개발하는 데 HTTP 를 이상적으로 만들어 줍니다.

결국 스택 맨 아래에서 운영 체제의 소켓 인터페이스를 통해 비트와 바이트가 마샬링됩니다. 네트워크 소켓과 상호 작용하기 위한 API는 매우 풍부하며 이 주제에 대한 많은 자습서와 서적이 있습니다. C의 IP 네트워킹 루틴은 상당히 장황 할 수 있으며 C ++로 객체 지향 루틴에 캡슐화 된 최초의 "실제"API 중 하나였습니다. 그 전통은 Foundation의 CFStream 클래스와 이제는 Swift  swiftysockets  API로 이어졌습니다.

Swiftychat

Swift에서 TCP / IP 네트워크 소켓을 사용하는 방법을 설명하기 위해 기본 "채팅 시스템" 응용 프로그램 인 Swiftychat 을 개발할 것입니다. 기능면에서 제한적이며 실제로 사용하기에 별다른 의미는 없지만 Swift의 TCP / IP 소켓에서 문자열을 보내고받는 방법의 실제 예입니다.

swiftysockets

Swiftychat는 원래 Zewo 팀이 개발 한 Swift Package Manager-ready TCP / IP 소켓 구현 인 swiftysockets를 사용합니다. 불행히도 패키징 제약으로 인해 우리 시스템에 기본 C 라이브러리 Tide를 설치하기 위해 먼저 약간의 난리법석을 떨어야 합니다. 그럼 이제 해보죠.

$ git clone https://github.com/iachievedit/Tide
Cloning into 'Tide'...
...
$ cd Tide
$ sudo make install
clang -c Tide/tcp.c Tide/ip.c Tide/utils.c
ar -rcs libtide.a *.o
rm *.o
mkdir -p tide/usr/local/lib
mkdir -p tide/usr/local/include/tide
cp Tide/tcp.h Tide/ip.h Tide/utils.h Tide/tide_swift.h tide/usr/local/include/tide
# copy .a
cp libtide.a tide/usr/local/lib/
mkdir -p /usr/local
cp -r tide/usr/local/* /usr/local/

어떤 시점에서 우리는 Swift Package Manager가 나머지 패키지 빌드와 링크 될 수있는 C 라이브러리를 컴파일 할 수 있다고 생각되나  그때까지는 이것이 우리가 할 수 있는 최선의 방법입니다.

Tide가 설치되면 우리는 Swiftychat 앱에서 swiftysockets 을 활용할 수 있습니다.

Start Coding!

main.swift 파일은 매우 간단합니다. ChatterServer를 생성하고 시작하십시오.

main.swift

간략한 main.swift는 우리의 구현이 ChatterServer 클래스에 구현되어 있다는 것을 의미합니다.

ChatterServer.swift:


1. Initialization

 IP와 TCPServerSocket 클래스 (swiftysockets)가 모두 오류를 던질 수 있으므로 nil이 가능한 반환 값임을 알리는 이니셜 라이저. IP는 IP 주소와 포트 정보를 멋지게 캡슐화하고 TCPServerSocket 초기화 프로그램에 인스턴스를 제공합니다. initsucceeds가되면 우리는 주어진 포트에 들어오는 연결을 받아 들일 준비가 된 TCP 소켓을 갖게됩니다.

2. The Main Loop

함수의 이름을 startListening, start, main으로 지정하십시오. 새 클라이언트 연결 (server! .accept ())을 받아들이고 연결된 클라이언트 목록에 추가하는 기본 이벤트 루프입니다. server! .accept ()는  새로운 연결을 기다리고 받아드리는 기능입니다. 꽤 표준적인 것들이죠.

3. Client Management

나머지 ChatterServer에는 모든 "클라이언트 관리"기능이 포함되어 있습니다. 클라이언트를 관리하는 몇 가지 변수와 세 가지 루틴이 있습니다.

우리의 변수는 간단합니다.

  • 연결되어진 클라이언트의 배열 ([TCPClientSocket])
  • 클라이언트를 구분하는데 사용되는 연결 카운트

함수내용은 이렇습니다: 

  • addClient는 TCPClientSocket을 사용하여 연결 수를 증가시키고 클라이언트의 연결에 따라 "관리"되는 NSThread를 설정합니다. 추가 연결이 새로 추가됨에 따라 NSThread가 생성됩니다. 잠시 후에 NSThreadroutine에 대해 이야기하겠습니다. 스레드가 시작되면 addClient는 연결된 클라이언트 배열 끝에 TCPClientSocket을 추가합니다.
  • removeClient는 지정된 클라이언트를 "필터 아웃"하기 위해 필터 함수를 사용하여 연결된 클라이언트 목록에서 클라이언트를 제거합니다. 여기에! == identity 연산자를 사용합니다.
  • broadcastMessage는 ChatterServer를 채팅 서버로 만드는 것입니다. where 키워드를 사용하여 클라이언트가 만든 메시지를 다른 모든 연결된 클라이언트에 브로드 캐스팅하는 필터링 된 배열을 만듭니다. 다시, 우리는! == 연산자를 사용합니다.

스레드는 주 프로세스 내에서 실행되는 별도의 실행 경로임을 상기하십시오. 우리 서버는 연결된 각 클라이언트에 대해 별도의 스레드를 생성합니다. 이제는 그것이 좋은 아이디어인지 아닌지에 대해서는 상황에 따라 달라집니다만 수만 명의 클라이언트를 처리 할 서버를 설계한다면,  말리고 싶습니다.

우리 쓰레드를 한번더 봅시다.

클라이언트 처리 스레드는 또한 TCPClientSocket 클래스의 receiveString 메서드를 통해 입력을 기다리는 루프에 위치합니다. 문자열이 수신되면 서버는 이를 콘솔에 기록한 다음 응답을 브로드 캐스트합니다. try 시도로 인해 연결이 끊어지면 서버는 클라이언트를 제거합니다.

Putting it All Together

우리의 목표는 가능한 한 많이 Swift Package Manager를 사용하여 응용 프로그램을 빌드하는 것입니다. swiftpm에 대한 소개는 자습서를 확인하십시오.

Package.swift에 다음을 추가하십시오.

Sources라는 디렉토리에 main.swift 및 ChatterServer.swift 코드를 추가하십시오.

swifty빌드를 실행하면 두 가지 종속성 (Tide 및 swiftysockets)을 다운로드하여 빌드하고 응용 프로그램 코드를 컴파일해야합니다. 모두 잘되면 .build / debug / 디렉토리에 chatterserver라는 이름의 바이너리가 생깁니다.

Testing it Out

다음 튜토리얼은 멋진 chat 클라이언트를 작성하는 것이지만 지금은 nc (netcat) 명령으로 서버를 테스트 할 수 있습니다. 서버를 시작한 다음 다른 터미널 창에서 nc localhost 5555를 입력하십시오. 연결된 서버 1의 클라이언트 1 창이 표시됩니다. netcat "클라이언트"창에서 CTRL-C를 누르면 서버는 그 이유와 함께 연결 해제 메시지를 인쇄합니다 (예 : 연결 재설정 (연결 재설정)).

실제 테스트를 위해 서버와 연결된 세 개의 클라이언트를 시작합니다.

Chatville
Chatville

왼쪽 터미널에서 채팅 서버가 실행 중입니다. 오른쪽에는 3 개의 클라이언트가 있으며, 각각은 nc localhost 5555 명령을 입력하여 시작됩니다. 각 클라이언트가 연결되면 서버는 연결 메시지를 인쇄합니다.

우리의 broadcastMessage 함수는 브로드 캐스트로부터 메시지의 송신자를 제외시킵니다. 이렇게하면 클라이언트가 자신의 메시지를 다시받지 못하게 되겠지요. 

What’s Next

nc를 클라이언트로 사용하는 것은 약간 지루합니다. 닉네임을 설정할 수없고, 메시지에 "구조"가없고, 타임 스탬프가 없습니다. 위의 예에서 메시지를받는 사람은 누가 작성했는지 전혀 알지 못합니다! swiftysockets에는 이미 TCPClientSocket 클래스가 있으므로 더 강력한 채팅 클라이언트를 만들수 있습니다.

Getting the Code

 here 에 우리의 작은 채팅 서버에 대한 코드를 넣었습니다. 또한 현재 구현되지 않은 채터 클라이언트 프로젝트도 포함되어 있습니다. 다운로드를 하고 최상위 디렉토리에 make를 입력하면 클라이언트와 서버가 모두 빌드됩니다. 주의 사항 : swiftysockets을 사용하기 전에 libtide.a와 관련 헤더를 설치해야합니다!

https://www.zybuluo.com/Xiaojun-Jin/note/29874  원본

참고로 스위프트 3.0 기반으로 처음 개발하시는분들은 이걸 사용하는게 나을거 같습니다.

[Swift 3 ]  가장 쉬운 소켓 (TCP)  통신 방법을 찾고 계신가요? 

소켓 기반  iOS 프로그래밍 

소켓은 양방향으로 데이터를 전송할 수있는 도구입니다. 따라서 소켓은 양면을 가지며, 각면은 IP 주소와 포트라는 두 요소의 조합으로 식별됩니다.

데이터가 전송되는 방식 (프로토콜)이 다른 많은 종류의 소켓이 있습니다. 가장 많이 사용되는 유형은 TCP와 UDP 이며 . 이 튜토리얼에서는 TCP 소켓을 다루겠습니다.

iOS 프로젝트를 작성하기 전에 Python 언어를 사용하여 TCP 서버를 만들 계획입니다. 그러나 특정 서버 구현이 iOS 기술과 관련이 없으므로 여기서 건너 뛰고 서버가 준비 되었다고 가정합니다. 이제 App Client에 초점을 맞추고 소켓 프로그래밍을 해 보겠습니다.  클라이언트는 채팅 룸 가입, 메시지 보내기메시지 수신이라는 세 가지 주요 작업을 합니다.

Stream Programming

iOS에서 소켓 연결을 설정하기 위해 우리는 스트림을 사용합니다. 스트림은 데이터를 송수신하는 메커니즘에 대한 추상화입니다. 또한 스트림에는 "연결이 열려 있습니다", "데이터가 수신되었습니다", "연결이 닫혔습니다"등과 같은 특정 이벤트에 따라 대응할 수있는 대리자가 연결되어 있습니다.

Cocoa Framework에 포함 된 스트림과 관련된 중요한 클래스가 있습니다.

  • NSStream 
    This is the super class which defines some abstract features like open, close and delegate.
  • NSInputStream 
    A subclass of NSStream for reading input.
  • NSOutputStream 
    A subclass of NSSTream for writing output.

여기있는 유일한 문제는 NSStream 클래스가 원격 호스트에 연결할 수없는 반면 CFStream은 능력이 있다는 것입니다. 다행스럽게도 NSStream과 CFStream은 일종의 브리지 연결이므로 NSStream 형식의 CFStream을 쉽게 가져올 수 있습니다.

  1. NSInputStream *inputStream; NSOutputStream *outputStream;
  2. - (void)initNetworkCommunication
  3. {
  4. CFReadStreamRef readStream; CFWriteStreamRef writeStream;
  5. CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)@"localhost",
  6. 80, &readStream, &writeStream);
  7. inputStream = (NSInputStream *)readStream;
  8. outputStream = (NSOutputStream *)writeStream;
  9. [inputStream setDelegate:self]; [outputStream setDelegate:self];
  10. [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  11. [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  12. [inputStream open]; [outputStream open];
  13. }

CFStreamCreatePairWithSocketToHost는 두 스트림을 호스트와 포트에 바인딩하는 데 사용됩니다. 일단 호출하면 CFStream을 NSStream으로 자유롭게 캐스팅 할 수 있습니다.

스트림은 지속적으로 데이터를 보내거나받을 준비가되어 있어야합니다. 이 기능을 사용하려면 실행 루프에서 이벤트를 수신하도록 스트림을 예약해야합니다. 앱은 이벤트를 스트리밍하는 데 반응해야하지만 이벤트의 자비가되지는 않습니다. 실행 루프 스케줄링을 사용하면 다른 코드 (필요한 경우)를 실행할 수 있지만 스트림에서 문제가 발생하면 알림을받을 수 있습니다.

Joining the Chat

서버에 연결되면 채팅에 참여할 준비가되었습니다. 조인 메시지의 형식은 "iam : name"입니다. 그래서 우리는 문자열을 만들어서 outputStream에 써야합니다.

  1. - (IBAction)joinChat:(id)sender
  2. {
  3. NSString *res = [NSString stringWithFormat:@"iam:%@", inputNameField.text];
  4. NSData *dt = [[NSData alloc] initWithData:[res dataUsingEncoding:NSASCIIStringEncoding]];
  5. [outputStream write:[data bytes] maxLength:[dt length]];
  6. }

이벤트를 전달하기 위해 서버는 클라이언트 / 서버가주고받는 데이터의 예상 포맷 / 시퀀스를 정의하는 프로토콜을 정의해야합니다. 이 응용 프로그램에서는 매우 간단한 문자열 기반 프로토콜을 사용합니다. iammeans "사용자가 채팅에 참여했습니다."msg는 "메시지 보내기"를 나타냅니다.

Sending Messages

채팅에 참여하는 것과 매우 유사한 방식으로이를 구현합니다. "iam :"을 "msg :"로 전환하면됩니다.

  1. - (IBAction)sendMessage:(id)sender
  2. {
  3. NSString *res = [NSString stringWithFormat:@"msg:%@", inputMessageField.text];
  4. NSData *dt = [[NSData alloc] initWithData:[res dataUsingEncoding:NSASCIIStringEncoding]];
  5. [outputStream write:[data bytes] maxLength:[dt length]];
  6. }

Receiving Messages

애플리케이션이 서버에서 메시지를 수신하고 있다는 사실을 알리려면 NSStream 대리자 스트림 인 handleEvent를 구현해야합니다. 그러면 스트림에서 발생하는 활동에 Google 애플리케이션이 반응 할 수 있습니다.

  1. - (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent
  2. {
  3. NSLog(@"stream event %i", streamEvent);
  4. switch (streamEvent)
  5. {
  6. case NSStreamEventOpenCompleted:
  7. NSLog(@"Stream opened");
  8. break;
  9. case NSStreamEventHasBytesAvailable: // fundamental to receive messages
  10. if (theStream == inputStream)
  11. {
  12. uint8_t buffer[1024]; int len;
  13. while ([inputStream hasBytesAvailable])
  14. {
  15. len = [inputStream read:buffer maxLength:sizeof(buffer)];
  16. if (len > 0)
  17. {
  18. NSString *output = [[NSString alloc] initWithBytes:buffer
  19. length:len
  20. encoding:NSASCIIStringEncoding];
  21. if (nil != output)
  22. {
  23. NSLog(@"server said: %@", output);
  24. }
  25. }
  26. }
  27. }
  28. break;
  29. case NSStreamEventErrorOccurred:
  30. NSLog(@"Can not connect to the host!");
  31. break;
  32. case NSStreamEventEndEncountered:
  33. [theStream close];
  34. [theStream removeFromRunLoop:[NSRunLoop currentRunLoop]
  35. forMode:NSDefaultRunLoopMode];
  36. [theStream release];
  37. theStream = nil;
  38. break;
  39. default:
  40. NSLog(@"Unknown event");
  41. }
  42. }

while 루프를 사용하여 스트림의 바이트를 수집합니다. 스트림에 아무것도 남아 있지 않으면 read 메소드는 0을 리턴합니다. 결과가 0보다 큰 경우 버퍼를 String으로 변환하고 결과를 출력합니다.

기기에서 앱 실행 :

먼저 "localhost"문자열을 컴퓨터의 ip로 전환하십시오. 현재 IP를 찾으려면 "시스템 환경 설정> 네트워크"로 가십시오. 장치는 무선으로 컴퓨터를 서비스하는 동일한 라우터에 연결해야합니다. 3G 연결을 사용하려면 포트 80에서 네트워크 외부의 연결을 허용하도록 라우터를 구성해야합니다 (권장하지 않음).


+ Recent posts