'Swift와 iOS'에 해당하는 글 94건

목차

  • libxlsxwriter 소개

  • Swift 프로젝트에서 c라이브러리 pod 세팅하기

  • libxlsxwriter 사용법

  • 마치며


libxlsxwriter 소개

libxlsxwriter는 C라이브러리입니다. (벌써부터 머리가 지끈..) Objective-C를 사용할때는 편하게 썼었는데 요즘은 Swift만 쓰다보니 C를 쓰는게 여간 부담스러운 일이 아닐 수 없네요. 여러가지 라이브러리를 써봤지만 실패를 하곤 결국 C 라이브러리까지 찾게 되었습니다. 그래도 한가지 다행인 소식은 이 라이브러리가 CocoaPods을 지원한다는 것입니다.

아래는 여러가지 엑셀 편집 라이브러리입니다. 저는 결국 libxlsxwriter를 사용하기로 했습니다.

Swift 프로젝트에서 c라이브러리 pod 세팅하기

libxlsxwriter를 사용하려면 pod으로 워크스페이스를 만들어줘야하는데요. 그밖에도 몇가지 세팅이 더 필요합니다. C라이브러리라그런지 프레임워크로 인식을 못하더라구요. 그래서 bridge-header.h를 만들고 #include로 라이브러리의 파일을 불러오면 프로젝트의 Swift에서 사용할 수 있습니다.

Podfile 만들기

Podfile을 만듭니다. 확장자가 따로 없는 텍스트파일입니다. Podfile.xcodeproj 와 같은 경로(프로젝트 경로라 부르겠습니다)에 저장해둡니다.

libxlsx_구조.png

Podfile 의 내용물입니다. CocoaPods은 Objective-C나 Swift 언어의 프로젝트에서 사용하는 의존성 관리 툴인데, 사용하는 방법은 따로 찾아보시길 바랍니다.

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

def my_pods
  pod 'libxlsxwriter', '~> 0.7.7'
end

target 'excel-example' do
  my_pods
end
target 'excel-exampleUITests' do
  my_pods
end

여기서 중요한 부분은 pod 'libxlsxwriter', '~> 0.7.7'입니다. 0.7.7버전의 libxlsxwriter 라이브러리를 설치하라는 부분이죠. (참고로 excel-example는 제 프로젝트 이름입니다.)

pod 설치

cd ~/{프로젝트경로}
pod install
excel-example-Bridging-Header.h 만들어서 설정

저는 use_frameworks!를 사용했었는데, libxlsxwriter는 C 라이브러리라 그런지 읽어오질 못했습니다. 그래서 따로 excel-example-Bridging-Header.h를 만들고 프로젝트에 명시해주었습니다(Build Setting에 들어가서 Swift Compiler - Code Generation > Objective-C Bridging headerexcel-example/excel-example-Bridging-Header.h으로 설정해주시면 됩니다).

C 라이브러리 불러올 수 있게 Pod 경로 설정

이번에는 헤더파일을 불러올 수 있게 Pod 경로를 명시해줍니다(마찬가지로 Build Setting에 들어가서 Search Paths > User Header Search PathsPods를 추가하고 recursive를 선택해줍니다).

여기까지 잘 따라오셨다면 이제 excel-example-Bridging-Header.h를 통해 C라이브러리를 불러와서 사용할 수 있습니다!

libxlsxwriter 사용법

excel-example-Bridging-Header.h에서 xlsxwriter.h 불러오기

#include "xlsxwriter.h"
저장할 경로 정하기

let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let url = documentsURL.appendingPathComponent("filename").appendingPathExtension("xlsx")
let path = NSString(string: url.path).fileSystemRepresentation
print("path:", url.path)
초기 설정

/* 새 워크북을 만들고 워크시트를 하나 추가하기 */
let workbook = new_workbook(path)
let worksheet = workbook_add_worksheet(workbook, nil);

아래같은 새 엑셀 파일을 하나 만들고 Sheet1 시트를 하나 만든 것입니다. 시트 이름을 바꾸려면 nil대신에 문자열을 넣어줍니다.

new_xlsx.png

초기 설정

/* 0컬럼부터 5컬럼까지 13.7 너비 적용 */
worksheet_set_column(worksheet, 0, 5, 13.7, nil);
셀 생성

/* 0 row, 4 column에 "이름" 문자열 넣기 */
worksheet_write_string(worksheet, 0, 4, "이름", nil);
/* 1 row, 2 column에 1.25123 숫자 넣기 */
worksheet_write_number(worksheet, 1, 2, 1.25123, nil);


/* 포멧 생성 */
let my_format = workbook_add_format(workbook);
format_set_align(my_format, UInt8(LXW_ALIGN_RIGHT.rawValue));
format_set_align(my_format, UInt8(LXW_ALIGN_VERTICAL_CENTER.rawValue));
format_set_bold(my_format);
format_set_font_size(my_format, 20)
format_set_font_name(my_format, "Arial")
/* 포멧이 적용된 셀 생성 */
worksheet_write_string(worksheet, 0, 1, "타이틀", my_format);
셀 합치기

worksheet_merge_range(worksheet, 3, 0, 3, 5, "합쳐진 셀", my_format);
파일 닫기

workbook_close(workbook);

모든 작업이 끝났으면 파일을 닫아주셔야합니다. 그리고 닫은 파일을 사용하려하면 에러가 발생합니다.

전체코드

https://github.com/tucan9389/excel-example-ios

위 코드로 만들어진 엑셀파일 예제코드 실행 ê²°ê³¼ 엑셀파일.png

마치며

코드 분할

C 인터페이스를 사용하다보니 Swift 코드와 안 어울리는것 같습니다. 저는 ExcelMaker.swift 파일로 빼내서 ExcelMaker 클래스를 만든 다음 클래스 메소드 static func createXLSX(filename: String, info: MyInfo) { ... }를 만들어서 사용했습니다.

Excel 포멧의 한계

엑셀파일 만들기가 csv파일 만들기와 비슷할 줄 알고 쉽게봤다가 고생 좀 하면서 느낀점은, MS는 엑셀파일을 자기네 프로그램 안에서만 쓰게 하려고 이렇게 사용하기 어렵게 만들었나 싶습니다. 아마도 엑셀파일 용량 줄이고 최적화시킨다고 이렇게 된게 아닐까 생각이 드는데, 라이브러리를 쓰실때도 오피셜 라이브러리가 따로 없기때문에 문제가 생겼을때 처리하기도 매우 곤란한 상황입니다. 여하튼 제3자 개발자가 엑셀파일을 직접 건드리는 작업은 피하는걸 권장하고싶습니다.

엑셀파일 만들기가 csv파일 만들기와 비슷할거라 생각하고 덤볐다가 고생했습니다. 그리고 고생한 이유를 찾아보았습니다.

  1. 엑셀파일 포멧이 표준을 지키지 않습니다.마이크로소프트 자신만의 포멧을 만들어 엑셀을 지원하고 있는데, 아마도 용량을 줄이거나 최적화시키려고 그랬겠지요..

  2. 공식라이브러리는 당연히 없고, 그러다보니 엑셀파일을 만들 수 있게 지원해주는 비공식(이지만 거의 공식처럼 쓰이는)라이브러리도 약한 편입니다.물론 Swift 라이브러리를 찾아보려다보니 범위가 좁긴 했지만, 엑셀 정도로 자주쓰이는 포멧은 Swift용 라이브러리가 있을법도한데 Star가 100개를 넘는 저장소를 찾기 힘들었습니다.

참고



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret

원문: https://medium.com/developerinsider/best-ios-development-tips-and-tricks-6c42c1d208c1

이 글은 먼저 DeveloperInsider에 발행됩니다. 여기(원문)서 확인할 수 있습니다.

1. Xcode에서 빌드시간 확인하기

프로젝트의 정확한 빌드 시간을 모른다면 Xcode에서 아래 옵션을 켜자.

defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES

2. Swift 프로젝트 빌드시간 단축시키기

Xcode 9.2 릴리즈 노트에서는 BuildSystemScheduleInherentlyParallelCommandsExclusively 사용자 디폴트를 켜서 스위프트 빌드시간을 단축시켜주는 실험단계의 기능을 언급했습니다.

defaults write com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively -bool NO

주의: 릴리즈 노트에 따르면 "빌드시간동안 메모리 사용을 증가시킬 수 있는 실험적 기능"이라고 했습니다.

3. Xcode에서 전체화면 모드로 시뮬레이터 사용하기

나는 Xcode 9 기능중에 전체화면으로 iOS 시뮬레이터와 Xcode를 실행시키는 것을 좋아한다. 이 기능을 사용하려면 그냥 터미널을 켜서 아래 명령을 실행시키면 된다.

defaults write com.apple.iphonesimulator AllowFullscreenMode -bool YES

시뮬레이터에 숨겨진 더 많은 기능을 사용하고싶으면 애플의 숨겨진 Internals 메뉴를 활성시키면 된다. 이렇게 하기 위해서는 루트 폴더에 AppleInternal이라는 빈 폴더를 만들자. 아래 명령을 실행시키고 시뮬레이터를 재시작하면 된다.

sudo mkdir /AppleInternal

4. iOS 시뮬레이터 화면 녹화하기

xcrun 명령어 유틸리티를 이용하면 시뮬레이터 창을 스크린샷 찍거나 비디오로 저장할 수 있습니다. 비디오를 찍기 위해서는 아래 명령을 실행시키세요.

xcrun simctl io booted recordVideo <filename>.<file extension>

예시:

xcrun simctl io booted recordVideo appvideo.mov

녹화를 멈추려면 control + c를 누르십시오. 파일이 만들어지면 현재 폴더에 저장됩니다.

5. 파인더에서 시뮬레이터에 파일 공유하기

Xcode 9부터는 시뮬레이터가 파인더 확장을 가지고 있습니다. 이것은 파이더 창에서 바로 파일을 공유할 수 있게 해줍니다. 그래도 파일을 시뮬레이터로 드레그&드롭해 주는게 더 빠른거 같네요.

아니면 아래 simctl 명령으로 이미지/비디오 파일을 전송할 수 있습니다.

xcrun simctl addmedia booted <PATH TO FILE>

6. sudo 인증시 지문 사용하기

맥북프로의 지문을 sudo 인증의 비밀번호로 사용하고 싶다면 /etc/pam.d/sudo를 수정하고 상단에 아래줄을 넣습니다.

auth sufficient pam_tid.so

이제 sudo로 지문을 사용할 수 있습니다.

7. 소리 알림과 함께 AutoLayout 컨스트레인트 디버깅하기

이번 방법은 AutoLayout 컨스트레인트를 디버깅하는데 훌륭한 벙법입니다. 그냥 실행시 UIConstraintBasedLayoutPlaySoundOnUnsatisfiable 인자를 넘겨주면 런타임시 컨스트레인트가 어긋날을 경우 소리가 납니다.

-_UIConstraintBasedLayoutPlaySoundOnUnsatisfiable YES

8. Xcode에서 사용할 수 없는 시뮬레이터 제거하기

이 조그만 명령어는 Xcode에서 사용할 수 없는 모든 시뮬레이터를 제거해 줍니다. 여기서 "unavailable"은 Xcode의 xcode-select 버전에서 사용불가능한 것을 의미합니다.

xcrun simctl delete unavailable

재밌었나요? 아래에 댓글을 달아주시면 고맙겠습니다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 - 블로그 댓글 - 페이스북 페이지(@나는한다번역-ios) - 트위터(@tucan9389) 으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.

원문: https://iosdevweekly.com/issues/351참고: https://pilgwon.github.io/blog/2018/05/14/iOS-Dev-Weekly-351.html
pilgwon님 번역글을 보다가 빠진 부분이 있는것 같아서 제가 번역해봤습니다. 피드백은 댓글로 남겨주세요.



구글 I/O 컨퍼런스가 이번주에 열렸습니다. 여러분은 대부분 Google Duplex의 데모를 봤을 텐데요. 자세히 알아볼 예정은 아니지만, 제 생각을 정리하자면 다음과 같을 것입니다:

  1. 어시스턴트는 아래 몇가지 이유때문에라도 "이 전화는 구글 어시스턴트가 거는 것입니다.."같은 이야기로 시작해서 구별할 수 있어야합니다.

  2. 이런 세부 유스케이스에대한 기술 개념이나 윤리에대해서는 문제가 없습니다. 만약 이런 전화를 받는 사업자가 이런 종류의 전화를 원치 않으면 호환가능한 온라인 체계를 만들고 구글은 전화 대신에 이것을 사용할 수 있을 것입니다. 물론 우리는 미래에 이런 종류의 기술을 어떻게 다룰지 굉장히 조심스럽게 접근할 필요가 있지만, 아직 이게 종말의 시작까지는 아닌거 같습니다.

  3. 네, 수많은 어시스턴트는 단도직입적이고 짧았었습니다만 이것이 어쩌면 사람 반응을 줄이려고 했을지 궁금합니다. 만약 직접 대화를 시작하면 여러 대화가 정해지며 어시스턴트는 더 유연한 대화의 복잡한 대답을 시도하거나 다룰 필요가 없습니다. 이는 1번에서 언급한 것처럼 조수 자신을 확인한 경우에도 일어납니다.

  4. 항상 현실 세계에서는 잘못될 수 있으며, 이런 일이 일어났을때 어떻게 대처할지 저는 매우 궁금합니다. 어떤 부분에서 이게 컴퓨터라는것을 인정할 수 있을까요? 첫번째에서 이야기한것처럼 하면 훨씬 쉬워질 수 있을 것 같습니다.

구글도 모든 것을 생각했을 거라 확신하지만, 이것은 저도 처음에 생각했던 것입니다.

아무튼, 이 모든 것은 iOS(혹은 모바일)로 조금만 작업하면 할 수 있는 것들입니다. WWDC의 키노트는 일반 대중뿐만 아니라 개발자를 위한 키노트이기 때문에 우리를 위한 정말 유익한 뉴스와 눈에 띌만한 무언가가 몇 가지 있을 것입니다:

  1. 구글 어시스턴트는 엄청나게 많은 제3자앱을 지원하며, 이번 키노트에서 어마어마하게 많이 발표되었습니다. SiriKit은 지원된 도매인을 매우 잘 이해했야하므로 굉장히 한정적인 통합 가능성으로 출발했었습니다. 구글은 더 많이 개방된 API로 시작하였으며 이제는 그 도매인을 더 이해하기위해 모인 자료들을 이용하려하고 있습니다. 둘은 굉장히 다른 접근법입니다.

  2. AR과 ML 프레임워크 이름 짓기가 웃긴 상황(원문 트위터)이 되고있습니다만 이런 기술들은 키노트에서 구글이 이야기한 거의 모든 주제였습니다. MLKit은 Firebase의 일부(원문)로써 iOS에서도 사용할 수 있으며 매우 흥미롭습니다.

  3. App slices는 매우 멋지며 나는 사용자가 앱과 어떻게 상효작용하는지가 더 큰 부분이 되어간다는것을 확인할 수 있었습니다. 이것은 iOS 앱 확장같은 느낌이 났지만 다른 접근법을 사용합니다.

  4. 안드로이드 스튜디오(그리고 다른 리눅스 앱들)는 이제 크롬OS에서 돌릴 수 있습니다. 이건 데스크탑 리눅스의 해입니다!! 😂

여튼 구글 I/O에서 나온 것들은 정말 흥미로웠지만 우린 iOS 얘기를 하려고 모였으니 iOS 얘기를 해봅시다.

Dave Verwer


이하, 아티클에대한 번역은 pilgwon 블로그 글을 참고해주세요.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.

출처: [애플 공식 문서] Integrating a Core ML Model into Your App(원문)

앱에 간단한 모델을 추가하고, 모델에 입력 데이터를 넣은 뒤, 모델 예측을 실행시키는 예제입니다.

프로젝트 다운로드

SDKs

iOS 11.0+

Xcode 9.2+

Framework

Core ML

On This Page

개요

함께 보기

개요

이번 간단한 앱은 학습된 간단한 모델 (MarsHabitatPricer.mlmodel)을 사용하여 화성거주지 가격을 예측할 것이다.

여러분의 Xcode 프로젝트에 모델 넣기

모델을 Xcode 프로젝트에 넣기 위해서는 모델을 드레그해서 프로젝트 네비게이터에 넣어야한다.

Xcode에서 모델을 열면 모델 타입, 예상되는 입출력등 모델에대한 정보를 확인할 수 있다. 여기서 모델 입력값은 태양열 전지판과 온실 갯수, 그리고 거주지 크기(에이커 단위)이다.

코드에서 모델 생성하기

Xcode는 모델의 입출력 정보를 이용하여 자동으로 모델의 인터페이스를 생성하는데, 이 인터페이스는 여러분의 코드상에서 모델과 상호작용하는데 쓰일 것이다. MarsHabitatPricer.mlmodel의 경우 Xcode는 MarsHabitatPricer모델을 표현하는 인터페이스와 모델의 입력(MarsHabitatPricerInput), 모델의 출력(MarsHabitatPricerOutput)을 생성한다.

모델을 생성하려면 만들어진 MarsHabitatPricer 클래스 생성자를 사용하자.


let model = MarsHabitatPricer()

모델에 넣을 입력값 가져오기

이번의 간단한 앱에서는 사용자로부터 모델의 입력값을 가져오기위해 UIPickerView를 사용한다.


func selectedRow(for feature: Feature) -> Int {
   return pickerView.selectedRow(inComponent: feature.rawValue)
}

let solarPanels = pickerDataSource.value(for: selectedRow(for: .solarPanels), feature: .solarPanels)
let greenhouses = pickerDataSource.value(for: selectedRow(for: .greenhouses), feature: .greenhouses)
let size = pickerDataSource.value(for: selectedRow(for: .size), feature: .size)

예측을위해 모델 사용하기

MarsHabitatPricer 클래스는 자동으로 생성된 prediction(solarPanels:greenhouses:size:) 메소드를 가지는데, 이 메소드는 모델 입력값으로부터 가격을 예측하는데 쓰인다. 이번의경우 태양열 전지판과 온실 갯수, 그리고 거주지 크기(에이커 단위)를 넣는다. 이 메소드는 MarsHabitatPricerOutput 인스턴스를 결과로 내뱉는다.


guard let marsHabitatPricerOutput = try? model.prediction(solarPanels: solarPanels, greenhouses: greenhouses, size: size) else {
   fatalError("Unexpected runtime error.")
}

예측된 가격을 가져오기위해 marsHabitatPricerOutputprice프로퍼티에 접근하여 앱 UI에 표시하자.


let price = marsHabitatPricerOutput.price
priceLabel.text = priceFormatter.string(for: price)
주의

자동으로 생성된 prediction(solarPanels:greenhouses:size:) 메소드에서 에러를 던질수 있다. Core ML으로 작업하면서 만나게될 가장 일반적인 에러 종류는 입력 데이터의 내용과 모델이 기대하는 내용이 서로 일치하지 않는 경우입니다(가령 잘못된 이미지 포멧이라던지).

Core ML 앱을 빌드하고 실행하기

Xcode는 Core ML 모델을 디바이스에 최적화시켜 컴파일한 후 리소스 안에 넣어둘 것입니다. 최적화된 모델은 앱번들안에 들어가며, 이 모델은 앱이 디바이스에서 실행되는동안 예측을 만드는데 쓰입니다.

함께 보기

첫번째 단계



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.

1. Tensorflow mobile for iOS

  • pod 'TensorFlow-experimental'

  • C++ 인터페이스

  • pod(의존성 관리 툴)으로 프로젝트 환경을 구축하면 450MB가 넘음. 여러 플랫폼을 묶다보니 용량이 큼. 예제를 앱으로 말면 약 25MB정도로 만들어짐.

  • Tensorflow Lite 사용 X.

2. Tensorflow Lite

iOS 기기에서 TensorFlowLite 로 .tflite 모델을 사용할 수 있음

  • pod 'TensorFlowLite'

  • C++ 인터페이스

  • TensorFlow Mobile의 진화형태.

  • 더 작은 바이너리 크기

  • 적은 의존성

  • 더 나은 퍼포먼스

Tensorflow Lite for iOSiOS 기기에서 .tflite 모델을 실행시킨 결과



3. CoreML with tf-coreml/tf-coreml

iOS 기기에서 Core ML 로 .mlmodel 모델을 사용할 수 있음

  • Swift/Objective-C 인터페이스(Core ML)

  • 텐서플로우(.pb)를 CoreML(.mlmodel)로 변환하는 컨버터

  • 애플이나 구글의 공식 프로젝트는 아니지만 스타가 많은 프로젝트.

4. MLKit with Firebase

Firebase 프레임워크에 들어있는 MLKit 로 .tflite 모델을 사용할 수 있음

  • Swift 인터페이스

  • 18.05.09 구글I/O에서 발표

  • 기본 ML 기능 제공

    • Text Recognition (OCR)

    • Face Detection

    • Label Detection

    • Cloud Landmark Detection

    • Cloud Text Recognition

    • Cloud Label Detection

    • Custom Model Object Detection(.tflite 모델 사용)

MLKit for iOSiOS 기기에서 MLKit으로 .tflite 모델을 실행시킨 결과


+. tensorflow + swift

기계학습 모델을 만드는 새로운 방법.18년5월에 처음 공개되었고 이제 개발중...



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
제목: iOS11: Machine Learning for everyone

WWDC 2017에서 한가지는 확실하게 만들었다. 애플은 기기에서의 기계학습을 확실히 밀고 있다.

그리고 앱 개발자들이 여기에 조인할 수 있도록 가능한 쉽게 만들고 싶어한다.

작년 애플은 기본 컨볼루셔널 신경망(basic convolutional networks)을 위한 Metal CNN and BNNS frameworks를 발표했다. 올해는 Metal, 새로운 컴퓨터 비젼 프레임워크, Core ML의 수많은 수가사항이 있다. 툴킷은 ML 모델을 여러분의 앱에 아주 쉽게 넣을 수 있게 해준다.


이 블로그 포스팅에서는, iOS11와 macOS10.13에서 새로운 기계학습에대한 내 생각(과 내 경험)을 공유하려고 한다.

Core ML
Core ML은 WWDC에서 많은 주목을 받았고, 그 이유는 알기 쉽다. 바로 많은 개발자들이 그들의 앱에 넣으려고 했던 프레임워크이기 때문이다.

이 API는 꽤 간단하다. 당신이 할 수 있는 것은 다음과 같다.
  1. 학습된 모델을 불러오기
  2. 예측 만들기
  3. 개이득!!!
이 말이 제한적으로 보일 수 있지만, 실제로 모델을 불러와서 예측을 하는 것은 보통 여러분의 앱에서 하고 싶었던 모든 것일 수 있다.

예전에는 학습된 모델을 불러오는 것이 굉장히 힘들었다(사실 library to take away some of the pain에대한 글을 썼었다). 그러니 이제 단지 두단계로 간단해져버려 굉장히 행복하다.

모델은 .mlmodel 파일안에 담겨있다. 이것은 새로나온 공개형 파일 포멧(open file format)으로 여러분의 모델, 입력과 출력, 클래스 레이블 그리고 데이터에 일어나는데 필요한 전처리에서 그 레이어를 표현한다. 또한 학습된 모든 파라미터들을 담고있다(가중치와 기저).

모델을 사용하는데 필요한 모든것이 이 한 파일 안에 들어 있는 것이다.

여러분은 간단하게 여러분의 프로젝트에다 이 mlmodel 파일을 떨어뜨리면 Xcode가 자동으로 스위프트(혹은 Objective-C)를 감싸는 클래스로 생성하며, 이런 과정이 모델을 사용하기 굉장히 쉽게 만들어준다.

예를들어, 여러분의 Xcode 프로젝트에 ResNet50.mlmodel 파일을 추가했으면, 이렇게 작성할 수 있겠다.
let model = ResNet50()
모델을 인스턴트화한다. 그리고 아래에는 예측을 한다.
let pixelBuffer: CVPixelBuffer = /* your image */

if let prediction = try? model.prediction(image: pixelBuffer) {
  print(prediction.classLabel)
}
그리고 이게 전부이다. 모델을 불러오거나 스위프트에서 그 결과물을 변환하기위해 다른 코드를 짤 필요가 없다. 이 모든것을 Core ML과 Xcode가 관리해준다. 멋지다!
주의: 화면뒤에서 무슨일이 일어나는지 알고싶으면, Project Navigator에 mlmodel 파일을 선택하여 버튼을 클릭하면 생성된 헬퍼 코드로 파일을 볼 수 있다.
Core ML은 모델을 CPU에서 돌릴지 GPU에서 돌릴지 스스로 결정할 것이다. 그리하여 가용의 리소스 사용을 최적화할 수 있게 해준다. 또한 Core ML은 특정 부분만 GPU에서 동작하고(많은 계산이 많이 필요한 작업) 다른 부분은 CPU(메모리가 많이 필요한 작업)에서 동작하도록 쪼갤 수 있다.

Core ML이 CPU를 사용할 수 있는 능력은 개발자들에게 또다른 큰 이점을 준다. iOS 시뮬레이터에서 돌려볼 수 있다는 점이다(어떤것은 Metal에서 불가능한데, 이것 또한 유닛테스트로 잘 돌아가지 않는다).

Core ML이 지원하는 모델은 무엇인가?
위의 ResNet50 예제는 이미지 분류기이지만, Core ML은 여러 타입의 모델을 다룰 수 있다.
  • support vector machines(SVM)
  • tree ensembles such as forests and boosted trees
  • linear regression and locgistic regression
  • neural networks: feed-forward, convolutional, recurrent
이 모든 것들은 회기(regression)뿐만 아니라 분류(classification)에도 사용될 수 있다. 추가로 여러분의 모델은 one-hot encoding, feature scaling, imputation of missing values등과같은 전형적인 ML 전처리 과정을 담고 있을 수 있다.

애플은 학습시킨 여러 모델을 다운로드 받을 수 있게 해놓았다. Inception v3, ResNet50, VGG16같은 것이 있는데, 파이썬 라이브러리 Core ML Tools로 자신만의 모델을 변환할 수도 있다.

현재 여러분은 Keras, Caffe, scikit-learn, XGBoost, libSVM로 학습한 데이터를 변환할 수 있다. 이 변환 툴은 지원하는 버전에대한 약간의 명세이다. 예를들어 Keras 1.2.2는 동작하지만 2.0은 아닌. 다행인점은 이 도구가 오픈소스여서 미래에는 더 많은 학습 툴킷을 지원할 부분에는 확신할 수 있다.

그리고 다른 모든것이 실패하면 언제나 여러분만의 변환기를 만들 수 있다. mlmodel 파일 포멧은 공개형이고 꽤 직관적으로 사용할 수 있다(photobuf 포멧 내에 있고, 스팩은 애플이 명세해놓음).

한계
Core ML은 모델을 빨리 불러와서 여러분의 앱에서 실행하기에는 훌륭하다. 그러나 이런 간단한 API로는 그 한계를 가지고 있다.
  • 지원하는 모델 타입들은 감독 기계학습(supervised machine learning)이다. 비감독 학습 알고리즘이나 강화 학습은 안된다. (비록 "제네릭" 뉴럴 네트워크 타입을 지원하는데, 이것을 이용할 수는 있을것 같다)
  • 기기에서 학습은 지원하지 않는다. 당신은 오프라인 툴킷을 사용하여 모델을 학습시키고 모델을 Core ML 포멧으로 변환해야한다.
  • Core ML이 특정 레이어 타입을 지원하지 않는다면, 사용할 수 없다. 이 점에서 여러분이 직접 계산한 커널로 Core ML을 확장시키는게 불가능하다. TensorFlow같은 툴들이 질반 목적 컴퓨테이셔널 그래프를 만들기위해 사용되는 곳은, mlmodel 파일 포멧은 유연성과는 좀 멀다.
  • Core ML 변환 툴은 제한된 수의 학습 툴과 명세한 버전만 지원한다. 예를들어 TensorFlow에서 모델을 학습했으면, 이 툴을 사용할 수 없고 여러분의 변환 스크립트를 작성해야할 것이다. 그리고 내가 덫붙이자면, 여러분의 TensorFlow 모델이 mlmodel에서 지원하지 않는 것을 하고 있다면, Core ML에서 모델을 사용할 수 없다.
  • 중간 레이어(intermediate layer)에서 만들어진 아웃풋을 볼 수 없다. 네트워크의 마지막 레이어에서 나온 예측만 볼 수 있다.
  • 100% 확신이 되진 않지만, 모델 업데이트를 다운받는 것이 문제가 있어 보인다. 여러분이 다시 학습시키고 싶은데, 바뀐 모델바다 앱의 새로운 버전을 내고 싶지 않으면 Core ML은 여러분에게 적절하지 않을 것이다.
  • Core ML은 (편의를위해) CPU에서 실행하는지 GPU에서 실행하는지 숨기는데, 이것이 앱을 위해 잘 동작할거라 믿어야만 한다. 당신이 아무리 아무리 원한다고 해도 Core ML에게 강제로 GPU에서 실행하라고 할 수 없다.
당신이 이런 한계들과 공존할 수 있다면, Core ML은 당신에게 맞는 프레임워크이다.

그렇지않거나, 전체를 컨트롤 하고 싶다면, Metal Performance Shaders나 Accelerate 프레임워크로 여러분만의 것을 만들어야할 것이다.
물론 실제 마법이 Core ML에 있진 않지만, 여러분의 모델에 들어있다. 여러분이 시작할때 적절한 모델을 가지고 있지 않다면 Core ML은 소용없을 것이다. 그리고 모델을 설계하고 학습시키는것은 기계학습에서 어려운 부분이다.

간단한 데모 앱
Core ML을 가지고 놀아보려고 간단한 데모를 만들었다. 언제나처럼 깃헙에서 소스를 확인할 수 있다.


이 데모 앱은 고양이 사진을 분류하기위해 MobileNet architecture을 사용했다.

원래 이 모델은 Caffe에서 학습되었다. 이것이 mlmodel 파일로 변환하는데 약간의 노력이 들게 만들었으나, 한번 변환한 모델로 만들고나니 앱에서 빌드하기 굉장히 쉬었다. (변환 스크립트는 깃헙 저장소에 들어있다)

앱은 아직 멋지지 않지만(단순히 정적인 이미지에대해 상위 5가지 예측의 아웃풋을 낸다) Core ML을 어떻게 쉽게 사용했는지 보여준다. 그냥 코드 몇줄만이 필요할 뿐이었다.
주의: 데모앱이 시뮬레이터에서는 잘 동작하지만 기기에서는 크래쉬가 난다. 읽어내려가서 왜 그런지 찾아내보자 ;-)
물론 안에서 무슨일이 일어나는지 알고싶었다. 실제로 mlmodel 파일이 mlmodelc 폴더에 컴파일되었으며 이 폴더는 여러분의 앱 번들 안에 있다. 이 폴더는 여러 다른 파일들, 몇몇 바이너리, 몇몇 JSON들을 가진다. 따라서 여러분의 앱에 실제로 넣기전에 Core ML이 어떻게 mlmodel을 변환하는지 볼 수 있다.

예를들어 MobileNet Caffe 모델은 소위 Batch Normalization 레이어라 불리는 것을 사용하며, 이것들이 변환된 mlmodel 파일안에 들어있다는 것을 확인했다. 그러나 컴파일된 mlmodelc에서는 Batch Normalization 레이어가 보이지 않았다. 이것은 Core ML이 모델을 최적화시켰다는 좋은 소식이다.

mlmodelc는 여전히 스케일링 레이어(scaling layer)를 포함하며 반드시 필요하지 않아 보이므로 좀 더 모델의 구조를 최적화할 수 있을것 같아 보인다.

물론 아직 iOS11 베타1버전이고 Core ML은 아마 더 개선될 것이다. 그 말은, Core ML에 넘겨주기전에 여러분의 모델을 최적화할 필요가 있다는 뜻이다. (“folding” the Batch Normaliza) 그러나 그렇게하면 특정 모델을위해 측정하고 비교해야할 것이다.


여러분의 모델이 CPU에서 실행될때와 GPU에서 실행될때 같은지도 확인해야할 것이다. 내가 언급한것처럼 Core ML은 모델을 CPU에서 돌릴지(Accelerate 프레임워크를 사용하여) GPU에서 돌릴지(Metal을 사용하여) 정한다. 결국 이 두 구현은 다르게 동작할 것이다. 그러니 둘 다 테스트해봐야한다!

예를들어 MobileNet은 "depthwise" 컨볼루션 레이어라 불리는 것을 사용한다. 이 원래 모델은 Caffe에서 학습되었고, 정규 컨볼루션의 groups 프로퍼티와 아웃풋 채널 수를 같게 만들어 depthwise 컨볼루션을 지원한다. 결과로나온 MobileNet.mlmodel 파일은 동일하지 않다. 이것이 iOS 시뮬레이터 안에서는 잘 동작하겠지만 실제 디바이스 위에서는 크래쉬가 난다!

시뮬레이터는 Accelerate 프레임워크를 사용하지만 실제 디바이스는 Metal Performance Shaders를 사용하여 생긴 문제이다. 그리고 Metal이 데이터를 인코딩하는 방법때문에 MPSCNNConvolution 커널은 제한되어 그룹의 수와 아웃풋 채널의 수를 같게 만들 수 없게 되었다. 아이고!

나는 애플에 버그리포팅을 제출했지만 내 요점은 이렇다. 시뮬레이터에서 모델이 잘 동작한다는게 디바이스에서 잘 동작할거라는 의미는 아니다. 테스트를 해보아라!

얼마나 빠른가?
나의 새로운 10.5" iPad Pro가 다음주까지 도착하지 않기 때문에(역자: 부럽습니다) Core ML의 속도를 시험해보지 못했다

나는 특별히 MobileNets을 실행시키는데 나의 Forge library를 이용할때와 Core ML을 이용할때 그 속도 차이가 어떻게 되는지 관심이 갔다(아직 초기 베타 단계에 있다).

채널을 고정하라! 공유할 데이터가 생기면 이 섹션을 업데이트 할 것이다.

Vision
다음으로 이야기할것은 새로나온 Vision 프레임워크이다.

여러분도 이름에서 추측했을지 모르겠지만, Vision은 컴퓨터비젼(computer vision) 테스크를 실행시켜준다. 과거에는 이를위해 OpenCV를 사용해야했었는데, 이제 iOS는 자신만의 API를 가지게 되었다.

Vision이 수행할 수 있는 일의 종류들이다
  • 이미지안에서 얼굴 찾아내기. 각 얼굴에대해 사각형을 만들어준다.
  • 안면의 세부적인 특징 찾아내기. 눈이나 입의 위치나, 머리의 모양 등.
  • 이미지에서 사각형모양으로된 것을 찾아내기. 표지판같은것들.
  • 비디오에서 움직이는 물체 추적하기.
  • 수평성 각도 알아내기
  • 두 이미지를 변형하여 그 내용을 정렬하기. 사진들을 합성할때 유용하다.
  • 이미지안에 포함된 텍스트 영역 감지하기.
  • 바코드 감지와 인지.
이 작업들중 몇몇은 이미 Core Image와 AVFoundation으로 가능하지만 이제 일관된 API로 한 프레임워크안에 들어왔다.

여러분의 앱이 이 컴퓨터비젼 작업중 하나가 필요할때, 더이상 여러분이 직접 구현하거나 다른 라이브러리를 사용할 필요가 없다. 그냥 Vision 프레임워크를 사용하면 된다. 더욱 강력한 이미지 처리를위해 Core Image 프레임워크와 합쳐서 사용할 수도 있다.

더 나은 것은, Core ML을 작동시키기위해 Vision을 사용할 수도 있다는 점이다. 그리하여 뉴럴 네트워크를 위한 전처리 단계로서 이런 컴퓨터 비젼 기술을 사용할 수 있게 해준다. 예를들어, 사람 얼굴의 위치와 크기를 감지하는데, 그 영역에 비디오 프레임을 자르는데, 이미지에서 얼굴이 있는 부분에 뉴럴럴 네트워크를 실행시키는데 Vision을 사용할 수 있다.

사실 이미지나 비디오와 함께 Core ML을 사용하면 항상 Vision을통해 가는것이 알맞은 방법이다. 가공되지않은 Core ML로는 여러분의 입력 이미지는 모델이 예상하는 포멧안에 있도록 해야하지만, Vision으로는 프레임워크가 이미지 리사이징 등에 주의해야한다. 이것으로 여러분의 추가 노력을 조금 절약해줄 것이다.

코드에서 Core ML을 작동시키기위한 Vision 사용은 다음과같이 생겼다.
// the Core ML machine learning model
let modelCoreML = ResNet50()

// link the Core ML model to Vision
let visionModel = try? VNCoreMLModel(for: modelCoreML.model)

let classificationRequest = VNCoreMLRequest(model: visionModel) {
  request, error in
  if let observations = request.results as? [VNClassificationObservation] {
   /* do something with the prediction */
  }
}

let handler = VNImageRequestHandler(cgImage: yourImage)
try? handler.perform([classificationRequest])
VNImageReuestHandler가 요청하는 오브젝트의 배열을 받아서, 아래처럼 여러 컴퓨터 비젼 작업을 함께 연결하여 할 수 있음을 인지하자.
try? handler.perform([faceDetectionRequest, classificationRequest])
Vision은 컴퓨터 비젼을 사용하기 아주 쉽게 만들어준다. 그러나 기계학습 사람들에게 멋진 일은 컴퓨터 비젼 작업의 아웃풋을 받아서 Core ML 모델에 넣을 수 있다는 점이다. Core Image의 파워와 합쳐지면 이미지 처리 파이프라인을 하나로 만들게된다!

Metal Performance Shaders
내가 말하고싶은 마지막 주제는 Metal이다. 이것은 애플의 GPU 프로그래밍 API이다.

올해 클라이언트를위한 많은 내 일거리는 Metal Performance Shaders (MPS)로 뉴럴 네트워크 구축과 최적화된 퍼포먼스로 맞추는 작업이 포함되있었다. 그러나 iOS10은 컨볼루션 네트워크를 생성하기위한 기본적인 몇 커널만을 제공했었다. 종종 이 갭을 채우기위해 커스터마이징한 커널을 짜야했다.

그러니 나는 iOS11에서 여러 이용가능한 커널 수가 늘었을때 행복했고, 그 이상의 기분이었다. 이제 그래프를 구축하는(building graphs) API를 가진다.

주의: 왜 Core ML 대신에 MPS를 사용할까? 좋은 질문이다! 가장 큰 이유는 Core ML이 여러분이 원하는 것을 지원하지 않거나, 프로세스 전체를 컨트롤 하고싶고 가능한 최대의 속도를 짜내고 싶을때 사용한다.

MPS에서 기계학습을위한 큰 변화들은 다음과 같다.
  • Recurrent neural networks. 이제 RNN, LSTM, GRU, MGU 레이어를 생성할 수 있다. 이것들이 MPSImage 오브젝트의 시퀀스에도 동작하고, MPSMatrix 오브젝트 시퀀스에도 동작한다. 이것이 흥미로운 이유는, 다른 모든 MPS 레이어들이 이미지만 다른다는 것이다(그러나 확실히 텍스트나 이미지가아닌 데이터와 작업할때는 매우 불편하다).
  • 더 많은 데이터 타입들. 이전의 가중치는 32비트 부동소수라고 가정했었는데, 이제 16비트 소수, 8비트 정수, 심지어 바이너리까지 될 수 있다. 컨볼루션과 완전히 연결된 레이어들은 바이너리 가중치와 바이너리화된 인풋으로 할 수 있다.
  • 더 많은 레이어들. 지금까지 우리는 plain-pld convolution과 max/average pooling으로 만들어야 했었다. 그러나 iOS11 MPS는 dilated convolution, subpixel convolution, transposed convolution, upsampling과 resampling, L2-norm pooling, dilated max pooling, 게다가 몇몇 새로운 활성 함수들(activation functions)을 가능하게 했다. 아직 MPS는 Keras나 Caffe 레이어 타입처럼 모든 타입을 가지진 않지만, 그 갭은 줄어들고 있다...
  • 더욱 편리함. Metal은 항번에 채널 4개의 분할로 데이터를 구성하는데(이미지가 MTLTextrue 오브젝트에의해 돌아오기 때문에),  그것때문에 MPSImage으로 작업하는것은 항상 좀 이상하다. 그러나 이제 MPSImage는 데이터를 읽고 쓰는 메소드를 가지므로 한결 편해질것이다.또다른 편리함은 레이어에 batch normalization 파라미터를 설정하게 해주는 새로운 메소드를 가지는 것이다. 이 말은 더이상 여러분이 컨볼루션 레이어 가중치에 batch normalization을 접지 않아도 MPS가 알아서 다 해줄것이라는 의미다. 매우 편리하다!


  • 성능 개선. 기존에 있던 커널들이 더 빨라졌다. 이 소식은 항상 좋다.
  • 그래프 API. 내 생각에는 이것이 큰 소식이다. 모든 레이어와 (임시의) 이미지를 직접 생성하는것은 항상 성가신다. 이제 Keras에서처럼 그래프를 표현할 수 있다. MPS는 이미지가 얼마나 커져야하는지, 패딩을 어떻게 다뤄야하는지, MPS 커널의 offset을 어떻게 설정할지 등을 자동으로 계산한다. 뒷편에서는 퓨징 레이어(fusing layers)로 그래프까지도 최적화시킬 수 있다.
이제 모든 커널들이 NSSecureCoding으로 시리얼라이즈 가능해보이는데, 이 의미는 그래프를 파일로 저장하여 나중에 복구시킬 수 있다는 의미이다. 그리고 이 그래프로 인터페이스를 사용하면 이제 그냥 한 메소드 호출만 하면 된다. 아직 Core ML만큼 쉽지는 않지만, MPS 사용이 확실히 이전보다 작업이 많이 줄었다.

내가 생각하기에 아직 분명하지 않은 것은 자신만의 컴퓨트 커널을 작성할 수 있는지, 그래프에 이것을 붙일 수 있는지이다. 내 클라이언트 작업에서 나는 전처리 과정에서 종종 필요했었고, Metal Shading Language로 작성된 커스텀 shader를 필요로 했다. 내가 말할 수 있는 부분은, "MPSNNCustomKerneNode" 클래스가 될것 같진 않다. 더 조사할 필요가 있어보인다!

결론: 기계학습을 위한 Metal Performance Shaders은 iOS 11과함께 더 강력해졌지만, 아마 많은 개발자들이 (내부적으로 MPS를 사용해가며) Core ML와 붙여 사용할 수 있다.
주의: 여러분의 앱이 계속 iOS 10을 지원하지 않는한, 새로나온 그래프 API는 내 Forge library를 쓸모없게 만들었다. 곧 예제 앱을 새로나온 그래프 API로 포팅할 것이고, 그것에대한 세부적인 내용을 블로그에 포스팅할 예정이다.

남은것들
발표된 것 중에 다른 부분이다.
  • Accelerate: Accelerate 프레임워크에서 BNNS는 기능적 업데이트가 크게 일어나진 않았다. 결국 Softmax 레이어가 나왔지만, MPS가 얻은 새로운 레이어 타입은 없었다. 아마 맞을것이다. 딥 뉴럴 네트워크를위한 CPU 사용은 어쨌든 좋은 아이디어가 아닌것같다. 이 말은, 나는 Accelerate를 사랑하고, 이것으로 많은 즐거움이 있었다. 그리고 이번년도에 나는 스파스 메트릭스를 더 지원했었는데, 꽤 멋졌다.
  • 자연어 처리(Natural Language Processing): Core ML은 이미지만을 위한게 아니라 텍스트를 포함한 수많은 종류의 데이터를 다룰 수 있다. 이 API에는 NSLinguisticTagger 클래스를 사용하는데, 얼마간 사용해봤지만 iOS 11이 나오면서 더욱 효과적게 되었다. 이제 NSLinguisticTagger는 언어 식별, 토큰화, part-of-speech tagging, lemmatization, Named Entity Recognition을 한다.
나는 NLP에 많은 경험이 없으므로 다른 NLP 프레임워크에대해 어떤식으로 stack up 되었는지 말할순 없을것 같지만, NSLinguisticTagger는 보기에 꽤 강력해 보인다. 여러분의 앱에 NLP를 넣고 싶으면 이 API로 시작하기에 좋아보인다.

이 모든게 좋은 소식인가?
애플이 개발자를위해 이 모든 새로운 툴을 제공하는것은 훌륭한 일이지만, 애플의 많은 API에는 중요한 "문제"가 있다.
  1. 오픈소스가 아니다
  2. 제한을 가진다
  3. 새로운 OS가 배포될때만 업데이트를 한다
이 세가지가 함게 있으면 애플의 API는 항상 다른 툴들에비해 뒤떨어질것이다. 만약 Keras가 멋진 새로운 레이어 타입을 추가하면 애플이 그 프레임워크와 OS를 업데이트 하기 전까지 Core ML로는 사용할 수 없을 것이다.

그리고 API의 어떤 부분은 여러분이 원하는대로 동작하지 않을때, 내부로 들어가서 고칠 수 없다. 여러분은 이것으로 작업해야하거나(항상 가능하진 않다) 다음 OS 배포까지 기다려야한다(모든 사용자들이 업그레이드하도록 해야한다).

물론 나는 애플이 비밀 소스를 줄거라 생각하진 않지만, 많은 다른 기계학습 툴킷들이 오픈소스니 Core ML도 오픈소스로 만드는 건 어떨까?🙏

애플이 이것을 아는것은 아마 빠른 시일안에 일어나진 않을 것이지만, 여러분의 앱에 기계학습을 넣기로 했을때는 적어도 위의 내용들을 마음에 담아주자.

더 읽을거리...



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Catching Leaky View Controllers Without Instruments

레테인 사이클(retain cycles)에의해 생긴 메모리 누수를 찾아내는 방법 중 잘 알려진 방법은, 더이상 화면에 있지 않을때 모든 뷰컨트롤러들이 디얼록되었는지 확인하는 것이다. 이 과정은 각각이 할당해제되기 전에 직접 반복해서 시행해보아야하는데, 재밌지도 않을 뿐더러 여기서 에러를 만들어내기도 한다. 만약 좀 더 일찍 UIViewController 누수에대한 
과정을 알 수 있었다면, 매일 개발하는동안 훨씬 낫지 않겠는가?

UIViewController의 잘 알려지지 않은 두개 프로퍼티에대해 감사하게 될 수도 있다.
  • isBeingDismissed: 모달로 나타난 뷰컨트롤러가 dismiss될 때, 이 프로퍼티는 true이다.
  • isMovingFromParentViewController: 부모 뷰컨트롤러로부터 이 뷰컨트롤러가 제거될 때, true가 된다. 이것은 UINavigationController 스택에서 뷰컨트롤러가 pop될 때와 같은, 시스템 컨테이너에서 제거되는 것도 포함된다.
이 프로퍼티 중 하나가 true면, 곧 뷰컨트롤러가 디얼록될 수 있다는 것을 알 수 있다. 정확히 얼마나 있어야 내부적으로 정리된 상태를 만들고 ARC가 디얼록할지는 모른다. 간단하게 생각해서 2초보다 더 길지는 않을 것이라 가정하자.

우리가 생각한 것을 코드로 만들어보자.
extension UIViewController {
   public func dch_checkDeallocation(afterDelay delay: TimeInterval = 2.0) {
       let rootParentViewController = dch_rootParentViewController

       // We don’t check isBeingDismissed simply on this view controller because it’s common

       // to wrap a view controller in another view controller (e.g. in UINavigationController)

       // and present the wrapping view controller instead.

       if isMovingFromParentViewController || rootParentViewController.isBeingDismissed {
           let type = type(of: self)
           let disappearanceSource: String = isMovingFromParentViewController ? "removed from its parent" : "dismissed"

           DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { [weak self] in

               assert(self == nil, "\(type) not deallocated after being \(disappearanceSource)")
           })
       }
   }

   private var dch_rootParentViewController: UIViewController {
       var root = self

       while let parent = root.parent {
           root = parent
       }

       return root
   }
}
흥미로운 부분은 asyncAfter(deadline:execute:) 호출에서 일어난다. 먼저 sefl를 weak로 만들면([weak self]), 나중에 호출할 클로저에의해 리테인되지 않는다. 그리고 self가 nil이면 assert를 건다(여기서 self는 UIViewController). 뷰컨트롤러가 살아있게 잡아두는 리테인 사이클을 가질때만 nil이 아니다.

이제 마지막으로 해야할 일은 모든 뷰컨트롤러의 viewDidDisappear(_:)에서 dch_checkDeallocation()을 호출하는 것이다(부모로부터 제거되거나 dismiss된 후에도 살아있도록 잡아두는 것을 제외하고)
override func viewDidDisappear(_ animated: Bool) {
   super.viewDidDisappear(animated)

   dch_checkDeallocation()
}
만약 누수가 일어나고 있으면, assert에서 실패나 나올 것이다(-Onone 빌드에서만 가능).

여기서 우리는 그냥 메모리 그래프 디버거(Memory Graph Debugger)를 열어 사이클의 원인을 찾고 고치면 된다.

이 방법을 통해 새로 소개된 리테인 사이클을 빠르게 배우는데 도움이 될거라 생각한다. 여러분도 이 방법이 즐거웠길 바란다! production-ready 코드(더 많은 주석과 if DEBUG 확인으로)는 GitHub의 DeallocationChecker에서 확인해볼 수 있다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: iOS — Identifying Memory Leaks using the Xcode Memory Graph Debugger

이 짧은 포스팅에서 내가 설명할 것은 아래와 같다
  • Xcode의 메모리 그래프 디버거란
  • 이것을 어떻게 사용하고, 몇가지 팁들
  • 장/단점들

이것이 무엇인가
짧게말해 메모리 그래프 디버거는 다음 질문의 답변에 도움을 준다. 한 오브젝트가 왜 메모리에 남아있는가?

Xcode의 메모리 그래프 디버거는 레티인 사이클과 메모리 누수를 찾아내고 고치는데 도움을 준다. 그 일이 발생하면, 앱 실행이 일시정지되고 현재 힙에서 오브젝트를 보여주는데, 이 오브젝트가 살아있게 하는 참조들이 무엇인지 그 관계와 함께 나타난다.


어떻게 사용하는가
리테인 사이클과 메모리 누수를 식별하는데 3가지 간략한 단계가 있다.
  • 1. 아래처럼 Xcode scheme editor로 stack logging에 체크한다.

live allocations을 위해 Malloc stack logging을 켜기live allocations을 위해 Malloc stack logging을 켜기


'Live Allocation'의 logging만 켰다. 이것은 디버깅할때 'All Allocations'를 선택하는것보다 오버헤드가 적고, 레테인 사이클과 누수를 식별하는데 필요한 것이다.
  • 2. 분석하고 싶은 앱을 실행시키고(리테인 사이클이나 누수가 의심되는 행동), 그 디버그 바 버튼을 선택하여 메모리 그래프 디버깅 모드로 들어가자.

메모리 그래프 디버깅 버튼메모리 그래프 디버깅 버튼

  • 3 .메모리 그래프 디버거는 앱 실행을 일시정지하고 아래를 보여준다.

Xcode의 메모리 그래프 디버거 모드Xcode의 메모리 그래프 디버거 모드

왼편에 디버그 네비게이터가 앱의 힙 내용을 보여준다.

디버그 네비게이터에 type/instance를 선택하면 가운데 패널에서 인스턴스 참조들을 보여준다.

가운데 참조 패널에 인스턴스를 선택하면 오른편의 인스팩터 페널에서 일반적인 오브젝트 메모리 정보와 allocation backtrace를 보여준다.

누수는 아래처럼 디버그 네비게이터에서 볼 수 있다.

디버그 네비게이터에 나타난 누수디버그 네비게이터에 나타난 누수


  • 1. 메모리 누수를 식별하는데 도움이 되기위해서, 아래처럼 누수만 보이도록 힙 내용을 필터링할 수 있다.

메모리 누수를위한 필터링메모리 누수를위한 필터링

  • 2. 런타임 이슈 네비게이터도 유용한데, 식별된 모든 누수의 숫자를 표시한다.

수많은 메모리 누수들!수많은 메모리 누수들!


좋은점과 나쁜점
  • 좋은점: 운좋게 누수를 쉽게 찾아낼 수 있다(간단한 리테인 사이클). 예를들어 한 오브젝트가 클로저 프로퍼티 안에서 자신을 붙잡고 있을때. 이건 붙잡고 있는 참조를 weak로하여 쉽게 고쳐진다.
  • 나쁜점: 잘못 알려주는 경우. 예를들어 UIButton 오브젝트를 만들고 UIToolBars 아이템 배열에 추가했는데, 메모리 누수로 나왔지만 그 이유는 볼 수가 없었다.


유용한 링크들

이게다다! 📱🚀👍🏽


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Ultimate Guide to JSON Parsing With Swift 4

스위프트4와 Foundation은 마침내 스위프트에서 JSON을 파싱하는 질문에대한 대답을 내놓았다.

파싱을 위한 훌륭한 라이브러리들이 많이 있었지만, 이렇게 적용하기 쉬울뿐 아니라 복잡한 시나리오에따라 커스터마이징이 가능한 솔루션은 꽤나 신선하다.

여기서 말하는 모든 것들을 어떤 Encode/Decoder 구현에 적용해보는 것은 의미없는 일이며, PropertyListEncoder도 마찬가지이다. 여러분이 XML같은 다른게 필요하면 커스텀 구현을 만들수도 있다. 이 블로그 포스트에서는 JSON 파싱에대해 초점을 맞추게 될것인데, 이것이 가장 많은 iOS 개발자와 연관되었기 때문이다.

기본
여러분의 JSON 구조와 오브젝트가 비슷한 구조를 가진다면 일은 쉽게 풀린다.

아래에는 맥주에대한 JSON 문서 예시이다.
{
   "name": "Endeavor",
   "abv": 8.9,
   "brewery": "Saint Arnold",
   "style": "ipa"
}
우리의 스위프트 자료구조는 이렇게 생길 수 있다.
enum BeerStyle : String {
   case ipa
   case stout
   case kolsch
   // ...

}


struct Beer {
   let name: String
   let brewery: String
   let style: BeerStyle
}
이 JSON 문자열을 Beer 인스턴스로 변환하기위해서, 우리는 타입에다 Codable을 넣을 것이다.

Codable은 사실 Encodable & Decoable로 구성된 유니온 타입(union type)으로, 만약 한방향으로 변환하는 기능만 원하면 적절한 프로토콜을 적용시키면 된다. 이것이 스위프트4의 새로운 기능이다.

Codable은 디폴트 구현이 따라오는데, 대부분의 경우 여러분은 그냥 이 프로토콜을 적용시키고 공짜로 유용한 디폴트 동작을 만끽하면 된다.
enum BeerStyle : String, Codable {
  // ...

}


struct Beer : Codable {
  // ...

}

다음으로 그냥 디코더를 만들어야한다.
let jsonData = jsonString.data(encoding: .utf8)!


let decoder = JSONDecoder()

let beer = try! decoder.decode(Beer.self, for: jsonData)
이게 다다!  우리의 JSON 문서를 beer 인스턴스에 파싱하였다. 키의 이름과 타입이 서로 일치하기 때문에 다른 커스터마이징이 필요없었다.

여기서 try!를 사용하는 것은 의미가 없겠지만, 여러분의 앱에서는 똑똑하게 에러를 캐치하여 다뤄줘야한다. 나중에 에러 핸들링에대해 더 다뤄볼 것이다...

이제 우리가 인위적으로 만든 예제에서는 완벽하게 정돈되었다. 그러나 만약 타입이 일치하지 않는다면 어떨까?

키 이름을 커스터마이징하기
API에서 키 이름을 snake-case로 하는 경우는 종종 있고, 이 스타일은 스위프트 속성에대한 네이밍 가이드라인과는 맞지 않는다.

이 부분을 커스터마이징하려면 잠시 Codable 디폴트 구현을 맞춰줘야한다.

키는 컴파일러가 자동으로 생성한 "CodingKeys" 열거형에의해 처리된다. 이 열거형은 CodingKey를 따르는데, 이것은 인코딩된 양식으로 속성을 값에 연결할 수 있게 정의한다.

키를 커스터마이징하기위해 우리는 이것에대한 우리만의 구현을 작성할 것이다. 이 경우, 스위프트 네이밍과는 다르며, 우리는 키에대한 문자열 값을 제공할 수 있다.
struct Beer : Codable {
     // ...

     enum CodingKeys : String, CodingKey {
         case name
         case abv = "alcohol_by_volume"
         case brewery = "brewery_name"
         case style
   }
}
만약 beer 인스턴스를 받아서 JSON으로 인코딩하려하면, 새롱누 포맷으로 동작하는 것을 볼 수 있다.
let encoder = JSONEncoder()

let data = try! encoder.encode(beer)
print(String(data: data, encoding: .utf8)!)
이것은 아웃풋이다.
{"style":"ipa","name":"Endeavor","alcohol_by_volume":8.8999996185302734,"brewery_name":"Saint Arnold"}
여기서 표현된 양식은 매우 인간-친화적이지않다. 더 보기좋게 만들기위해 outputFormatting 프로퍼티로 JSONEncoder 아웃풋 포맷을 커스터마이징 할 수 있다.

디폴트값은 .compact인데, 이것은 위의 결과처럼 만들어준다. 이것을 .prettyPintted로 바꿔서 더 가독성좋은 아웃풋으로 만들 수 있다.
encoder.outputFormatting = .prettyPrinted

{
  "style" : "ipa",
  "name" : "Endeavor",
  "alcohol_by_volume" : 8.8999996185302734,
  "brewery_name" : "Saint Arnold"
}
JSONEncoderJSONDecoder 둘 다 그 동작을 커스터마이징하기위한 더 많은 옵션들이 있다. 더 일반적으로 해야하는 것중 하나는 날짜 파싱을 어떻게 커스터마이징할것인지이다.

날짜 다루기
JSON은 날짜를 표현하는데 데이터 타입이 없으므로 클라이언트와 서버가 동의한 방법대로 표현하여 시리얼라이즈하게된다. 일반적으로 ISO 8601 날짜 포멧팅 방식으로 처리하며 문자열로 시리얼라이즈한다.
프로 팁: ISO 8601을 포함한 다양한 포멧으로 변환해보려면 nsdateformatter.com에서 해볼 수 있다.
다른 포멧들은 아마 레퍼런스 날짜로부터 샌 초단위(혹은 밀리초단위)로, JSON 문서에서 Number로 시리얼라이즈 될 수 있다.

예전에는 이것을 우리 스스로 처리하여야 했었는데, 아마 우리 데이터 타입에 문자열 칸을 제공하고, 문자열 값에서 날짜를 marshal하기위해 DateFormatter를 사용한다. 그 반대도 마찬가지이다.

JSONEncoderJSONDecoder로 모두 해결할 수 있다. 한번 확이해보자. 날짜를 다루는 스타일로 디폴트로 .deferToDate를 사용할 것이다. 이것은 아래처럼 생겼다.
struct Foo : Encodable {
    let date: Date
}


let foo = Foo(date: Date())
try! encoder.encode(foo)

{
  "date" : 519751611.12542897

}
이것을 .iso8601 포멧으로 바꿀 수 있다.
encoder.dateEncodingStrategy = .iso8601

{
  "date" : "2017-06-21T15:29:32Z"
}
다른 JSON 인코딩 전략도 가능하다.
  • .formatted(DateFormatter): 지원하고자하는 표준 날짜 포멧 문자열이 없을때. 여러분의 date formatter 인스턴스를 제공한다.
  • .custom( (Date, encoder) throws -> Void ): 아주 커스텀하고 싶을때, 여기 블럭을 전달하여 제공된 인코더로 데이터를 인코딩 할 것이다.
  • .millisecondsSince1970.secondsSince1970: API에서 아주 일반적인 양식은 아니다. 이것은 인코딩된 표현에서 타임 지역 정보를 완전히 손실해버리기 때문에 별로 추천하지 않는다. 그런 이유로 누군가가 잘못된 가정을 생각하기 쉽게 만든다.
날짜를 디코딩하는 것은 필수적으로 같은 옵션을 가지지만, .custom에대해서는 .custom( (Decoder) throws -> Date )의 모양을 받는다. 따라서 우리는 디코더를 받고, 디코더 안에서 된 것이 무엇이든 그것에서 날짜로 만들 책임을 가진다.

Float 다루기
Float은 스위프트의 Float 타입과 JSON과는 꽤 맞지 않는 또다른 부분이다. 만약 서버가 유효하지 않은 "NaN"을 문자열로 보내면 어떻게 될까? 양수나 음수 Infinity는 어떨까? 이것들은 스위프트의 어떤 값에도 매칭되지 않는다.

디폴트로 구현된 것은 .throw이다. 디코더가 이 값들을 만나면 에러가 나타날 것이며, 우리가 처리하고 싶은대로 맵핑할 것을 제공할 수 있다.
{
  "a": "NaN",
  "b": "+Infinity",
  "c": "-Infinity"
}

struct Numbers : Decodable {
  let a: Float
  let b: Float
  let c: Float
}
decoder.nonConformingFloatDecodingStrategy =  .convertFromString(
     positiveInfinity: "+Infinity",
     negativeInfinity: "-Infinity",
     nan: "NaN"
)


let numbers = try! decoder.decode(Numbers.self, from: jsonData)dump(numbers)
이렇게 나타난다.
▿ __lldb_expr_71.Numbers

  -a: inf


  -b: -inf


  -c: nan
JSONEncodernonConformingFloatEncodingStrategy로 반대로도 할 수 있다.

주로 일어나는 경우의 그런것은 아니지만 어느날 유용하게 다가올 것이다.

데이터 다루기
가끔 base64로 인코딩된 문자열의 작은 비트의 데이터를 보내는 API를 마주칠 수도 있겠다.

이것을 자동으로 다루려면 이런 인코딩 전략중 하나를 JSONEncoder에 넣어준다.
  • .base64
  • .custom( (Data, Encoder) throws -> Void)
디코딩하기 위해서 JSONDecoder에 디코딩 전략을 넣어준다.
  • .base64
  • .custom( (Decoder) throws -> Data)
당연하게도 여기서는 .base64가 일반적인 선택일 될 것이지만, 커스텀화된 것이 필요하다면 블럭기반 전략에 사용하면 된다.

랩퍼 키(Wrapper Keys)
종종 API는 랩퍼 키를 포함할 것인데, 최상위 JSON 엔티티는 항상 오브젝트이다.

이런식으로 생겼을 것이다.
{
  "beers": [ {...} ]
}
스위프트에서 표현하기위해 우리는 이 응답에대해 새로운 타입을 만든다.
struct BeerList : Codable {
   let beers: [Beer]
}
실제로 이게 다다! 우리 키 이름이 일치하고 Beer이 이미 Codable이므로 그냥 동작한다.

루트 수준 배열
만약 API가 루트 엘리먼트로 배열을 반환하고 있다면 이런식으로 응답을 파싱한다.
let decoder = JSONDecoder()

let beers = try decoder.decode([Beer].self, from: data)
여기 타입으로 Array를 사용하고 있다는 점을 주목하자. Array<T>T가 디코딩가능할때만 디코딩 가능하다.

랩핑키 오브젝트(Object Wrapping Keys) 다루기
여기에는 여러분이 만날 수 있는 또다른 시나리오가 있다. 배열안에 각 오브젝트가 있는 한 배열 응답이 키로 랩핑되있다.
[
  {
   "beer" : {
     "id": "uuid12459078214",
     "name": "Endeavor",
     "abv": 8.9,
     "brewery": "Saint Arnold",
     "style": "ipa"
   }
  }
]
이 키를 붙잡아두기위해 위의 랩핑 타입 방법을 쓸 수도 있지만, 이 구조는 이미 강타입으로 디코딩가능하게 구현되있음을 인지하는 것이 더 쉬우 방법일 수 있다.

보이는가?
[[String: Beer]]
혹은 이런 경우가 더 읽기 쉬울지도 모르겠다.
Array<Dictionary<String, Beer>>
Array<T>이 디코딩가능한것처럼, KT가 둘 다 디코딩가능하면 Dictionary<K, T>도 그렇다.
let decoder = JSONDecoder()

let beers = try decoder.decode([[String:Beer]].self, from: data)
dump(beers)

▿ 1 element
  ▿ 1 key/value pair
    ▿ (2 elements)
      - key: "beer"
      ▿ value: __lldb_expr_37.Beer
        - name: "Endeavor"
        - brewery: "Saint Arnold"
        - abv: 8.89999962


       - style: __lldb_expr_37.BeerStyle.ipa

더 복잡하게 네스티드된 응답(Nested Response)
때론 우리 API 응답은 간단하지 않다. 아마 제일 상위에는 응답의 오브젝트를 정의하는 키가 단순히 하나가 아니고, 여러 컬랙션을 받거나 페이지로 된 정보를 받을 것이다.

예를 들어보자.
{
   "meta": {
       "page": 1,
       "total_pages": 4,
       "per_page": 10,
       "total_records": 38
   },
   "breweries": [
       {
           "id": 1234,
           "name": "Saint Arnold"
       },
       {
           "id": 52892,
           "name": "Buffalo Bayou"
       }
   ]
}
json을 인코딩/디코딩 할때, 스위프트에서 실제로 타입을 중첩할 수 있고 구조체 표현을 가질 수 있다.
struct PagedBreweries : Codable {
   struct Meta : Codable {
       let page: Int
       let totalPages: Int
       let perPage: Int
       let totalRecords: Int
       enum CodingKeys : String, CodingKey {
           case page
           case totalPages = "total_pages"
           case perPage = "per_page"
           case totalRecords = "total_records"
       }
   }

   struct Brewery : Codable {
       let id: Int
       let name: String
   }

   let meta: Meta
   let breweries: [Brewery]
}
이런 방법의 커다란 이점은 다른 응답을 같은 타입의 오브젝트로 가질 수 있다는 점이다(이 경우 아마 보이는것처럼 응답 리스트에서 "brewery"idname만 가지지만, brewery를 선택하면 더 많은 속성을 가진다.(but has more attributes if you select the brewery by itself)) 여기서 Brewery 타입은 중첩되있으므로 다른 Brewery타입을 만들어서 다른 구조체를 디코딩/인코딩할 수 있다.

더 깊은 커스터마이징
지금까지 무거운 작업도 디폴트 EncodableDecoable 구현에 의존해왔다.

대부분 이렇게 처리하겠지만, 결국 우리는 인코딩과 디코딩을 더 컨트롤하기위해 깊이 들어가야 할것이다.

커스텀 인코딩
시작하면서, 컴파일러가 공짜루 우리에게 제공해줬던 것을 커스텀하는 버전을 만들어 볼 것이다. 인코딩으로 시작해보자.
extension Beer {
  func encode(to encoder: Encoder) throws {

   }
}
그리고 이 예제를 좀 더 다루기위해 beer 타입에 새로운 필드 몇개도 추가하고 싶다.
struct Beer : Coding {
   // ...

   let createdAt: Date
   let bottleSizes: [Float]
   let comments: String?


   enum CodingKeys: String, CodingKey {
       // ...

       case createdAt = "created_at",
       case bottleSizes = "bottle_sizes"
       case comments
   }
}
이 메소드에는 인코더를 넣고, "컨테이너(container)"를 받아서, 여기에 값을 인코딩한다.

컨테이너가 무엇일까?
컨테이너는 몇가지 타입중에 하나가 될 수 있다.
  • Keyed Container: 키로 값을 제공함. 이것은 원래 딕셔너리임.
  • Unkeyed Container: 키 없이 정렬된 값을 제공함. JSONEncoder에서는 배열을 의미함.
  • Single Value Container: 담겨진 엘리먼트의 어떤 종류도 없이 가공되지 않은 값을 만듦.
우리의 모든 프로퍼티를 인코딩하기위해 우리는 먼저 컨테이너를 받아야한다. 이 포스트 처음에 나왔던 JSON 구조체를 보면, keyed container가 필요해보인다.
var container = encoder.container(keyedBy: CodingKeys.self)
여기서 2가지를 짚어보자.
  • 컨테이너는 우리가 변경할 수 있도록 반드시 mutable 프로퍼티여야하는데, 변수를 var로 선언해야한다.
  • 키들을 지정해야하는데(그리하여 프로퍼티와 키를 매핑시킨다) 이 컨테이너로 인코딩시킬 수 있는 키가 무엇인지 안다.
후자는 아주 강력한 점이 될 것이다.

다음으로 컨테이너에 값을 인코딩한다. 이 호출들은 모두 에러를 던지므로, 각 줄마다 try로 시작할 것이다.
try container.encode(name, forKey: .name)
try container.encode(abv, forKey: .abv)
try container.encode(brewery, forKey: .brewery)
try container.encode(style, forKey: .style)
try container.encode(createdAt, forKey: .createdAt)
try container.encode(comments, forKey: .comments)
try container.encode(bottleSizes, forKey: .bottleSizes)
comments 필드에서, Encodable의 디폴트 구현은 옵션 값에 encodeIfPresent를 사용한다. 만약 nil이면 인코딩된 표현에서 키는 잃어버릴 것이다는 의미이다. 이것은 API에대해 일반적으로 좋은 해결책이 아니므로, 여기에 null 겂아 있다해도 키를 가지도록 하는 방법이 좋은 방법이다. 여기서 우리는 encodeIfPresent(_:forKey:) 대신 encode(_:forKey:)를 사용하여 이 키를 포함하는 아웃풋으로 만든다.

bottoleSizes 값은 자동으로 인코딩되있지만, 어떤 이유로 커스터바이징이 필요하다면 우리만의 컨테이너를 만들어야한다. 여기서 우리는 각 항목별로 처리하고(부동소숫점을 반올림하여) 순서대로 컨테이너에 추가한다.
var sizes = container.nestedUnkeyedContainer(
     forKey: .bottleSizes)

try bottleSizes.forEach {
     try sizes.encode($0.rounded())
}
그리고 끝났다! 여기에는 부동소수점이 따르는 전략이나 날짜 포멧팅에대한 얘기는 없다는 점을 인지하자. 사실 이 메소드는 전적으로 JSON agnostic인데, 이것이 설계의 부분이다. 인코딩과 디코딩 타입은 제네릭 기능이고, 포멧은 필요한 사람에의해 쉽게 명세된다.

이제 우리가 인코딩한 JSON은 이렇게 생겼다.
{
  "comments" : null,
  "style" : "ipa",
  "brewery_name" : "Saint Arnold",
  "created_at" : "2016-05-01T12:00:00Z",
  "alcohol_by_volume" : 8.8999996185302734,
  "bottle_sizes" : [
   12,
   16
  ],
  "name" : "Endeavor"
}
여기서 부동소수점 값이 원래 JSON 문서에서는 8.9였는데 메모리에 표현되면서 같지 않은 숫자로 되버린것은 의미없다. 만약 숫자의 정확한 표현이 필요하다면, NumberFormater로 매번 손수 포멧팅하길 원할지도 모르겠다. 특별히 통화를 다루는 API는 종종 정수형 값으로 센트 숫자를 보낸고(안전하게 반올림될 수 있) 100.0으로 나눠서 달러 값을 얻어낸다.있다

이제 반대로도 할수 있다. 다음으로 Decodable 프로토콜 요구사항에대한 구현을 작성해보자.

커스텀 디코딩
디코딩은 원래 다른 생성자를 작성하는 것이다.
extension Beer {
   init(from decoder: Decoder) throws {

   }
}
이번에도 디코더에서 컨테이너를 만들어야한다.
let container = try decoder.container(keyedBy: CodingKeys.self)
모든 기본 프로퍼티를 디코딩할 수 있다. 각각의 경우에서 우리는 기대하는대로 타입을 지정해야한다. 만약 타입이 매치가 안된다면 DecodingError.TypeMismatch가 던져지며 무슨 문제인지 알려주는 정보를 받는다.
let name = try container.decode(String.self, forKey: .name)

let abv = try container.decode(Float.self, forKey: .abv)

let brewery = try container.decode(String.self,
     forKey: .brewery)

let style = try container.decode(BeerStyle.self,
     forKey: .style)

let createdAt = try container.decode(Date.self,
     forKey: .createdAt)

let comments = try container.decodeIfPresent(String.self,
     forKey: .comments)
같은 메소드를 bottleSizes 배열에도 상ㅇ할 수 있는데, 비슷한 방법으로 각 값을 처리할 수 있다. 여기, 새로운 인스턴스에 저장하기 전에 값을 반올림한다.
var bottleSizesArray = try container.nestedUnkeyedContainer(forKey: .bottleSizes)

var bottleSizes: [Float] = []

while (!bottleSizesArray.isAtEnd) {
   let size = try bottleSizesArray.decode(Float.self)
   bottleSizes.append(size.rounded())
}
컨테이너에 더이상 엘리먼트가 없을때까지 계속 값을 디코딩할 것이다.

이제 모든 이 변수들이 정의되고, 디폴트 생성자 호출에대한 모든 대답을 가지게 되었다.
self.init(name: name,
             brewery: brewery,
             abv: abv,
             style: style,
             createdAt: createdAt,
             bottleSizes: bottleSizes,
             comments: comments)
encode(to encoder:)init(from decoder:)의 커스텀 구현으로, JSON 결과를 우리 타입에 맵핑시키는데 더욱 컨트롤할 수 있게 되었다.

오브젝트 평평하게 만들기(Flattening Objects)
JSON이 우리가 고려하지 못했던 중첩 수준을 가진다면 어떻게 될까? 위 예제를 수정하여 abvstyle이 이렇게 표현되도록 해보자.
{
  "name": "Lawnmower",
  "info": {
    "style": "kolsch",
    "abv": 4.9
  }
  // ...
}
이 구조로 동작하려면 인코딩 디코딩 구현을 모두 커스터마이징 해야한다.

중첩된 키를 위해 열거형을 정의(하고 메인 CodingKeys 열거형으로부터 제거)하면서 시작해 볼 것이다.
struct Beer : Codable {
  enum CodingKeys: String, CodingKey {
     case name
     case brewery
     case createdAt = "created_at"
     case bottleSizes = "bottle_sizes"
     case comments
     case info // <-- NEW

  }

  case InfoCodingKeys: String, CodingKey {
     case abv
     case style
  }
}
우리가 값을 인코딩할때 제일 먼저 info 컨테이너에 참조를 넣어야한다. (which if you recall is a keyed container)
func encode(to encoder: Encoder) throws {


     var container = encoder.container(
         keyedBy: CodingKeys.self)
     var info = try encoder.nestedContainer(
         keyedBy: InfoCodingKeys.self)
     try info.encode(abv, forKey: .abv)
     try info.encode(style, forKey: .style)

     // ...

}
디코딩가능한 구현을 만들기 위해 반대로도 할 수 있다.
init(from decoder: Decoder) throws {
   let container = try decoder.container(
         keyedBy: CodingKeys.self)

   let info = try decoder.nestedContainer(
         keyedBy: InfoCodingKeys.self)
   let abv = try info.decode(Float.self, forKey: .abv)
   let style = try info.decode(BeerStyle.self,
         forKey: .style)

   // ...

}
이제 인코딩된 포멧에 중첩된 구조를가 가질 수 있게 되었으나 우리 오브젝트에서는 평평하게(flatten) 만들었다.

자식 오브젝트 만들기
brewery가 간단한 문자열로 전달되고 분리된 Brewery 타입을 유지하고 싶다고 해보자.
{
  "name": "Endeavor",
  "brewery": "Saint Arnold",
  // ...
}
이 경우에는 다시 encode(to encoder:)init(from decoder:) 의 커스텀 구현을 해줘야한다.
func encode(to encoder: Encoder) throws {
     var container = encoder.container(keyedBy:
         CodingKeys.self)

     try encoder.encode(brewery.name, forKey: .brewery)

     // ...


}


init(from decoder: Decoder) throws {
     let container = try decoder.container(keyedBy:
         CodingKeys.self)
     let breweryName = try decoder.decode(String.self,
         forKey: .brewery)
     let brewery = Brewery(name: breweryName)

     // ...

}

상속
아래 클래스들을 가진다고 생각해보자.
class Person : Codable {
   var name: String?

}


class Employee : Person {
   var employeeID: String?

}
Person 클레스를 상속하여 Codable을 따르게 되었으나, Employee 인스턴스를 인코딩하려면 무슨일이 일어날까?
let employee = Employee()
employee.employeeID = "emp123"
employee.name = "Joe"


let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try! encoder.encode(employee)
print(String(data: data, encoding: .utf8)!)

{
  "name" : "Joe"
}
우리가 원하던 결과는 아니다. 자동으로 생성된 구현이 자식클래스에는 잘 동작하지 않는다. 따라서 다시 인코드/디코드 메소드를 커스터마이징 해야한다.
class Person : Codable {
   var name: String?



   private enum CodingKeys : String, CodingKey {
       case name
   }

   func encode(to encoder: Encoder) throws {
       var container = encoder.container(keyedBy: CodingKeys.self)
       try container.encode(name, forKey: .name)
   }
}
자식클래스에도 같은 일을 할 것이다.
class Employee : Person {
   var employeeID: String?


   private enum CodingKeys : String, CodingKey {
       case employeeID = "emp_id"
   }

   override func encode(to encoder: Encoder) throws {
       var container = encoder.container(keyedBy: CodingKeys.self)
       try container.encode(employeeID, forKey: .employeeID)
   }
}
이렇게 된다.
{
  "emp_id" : "emp123"
}
흠, 이것도 원하던 결과는 아니다. 우리는 부모클래스 구현의 encode(to:)으로 흘러가야한다.

당신은 그냥 부모클래스를 호출하여 인코더에서 넘겨줄 생각일 수 있다. 이것도 동작은 하지만, 현재 스냅샷은 EXC_BAD_ACCESS을 낸다. 내 생각엔 버그같고, 나중에 고쳐질 것이다.

만약 위에처럼 했다면, 같은 컨테이너하에 합쳐진 속성들을 얻을 수 있다. 그러나 스위프트팀은 여러 타입에대해 같은 컨테이너를 재사용하는 것에대해 이야기한다.
만약 공유되는 컨테이너가 필요하면 여전히 super.encode(to: encoder)과 super.init(from: decoder)을 호출할 수 있지만, 우리는 더 안전한 컨테이너화시킨 방법을 추천한다.
그 이유로는, 부모클래스는 우리가 설정한 값을 덮어쓸 수 있지만 우리는 그것에대해 모를 수 있다는 것이다.

대신 우리는 부모클래스의 인코더를 얻기위해 특별한 메소드를 사용할 수 있다. 이 인코더는 이미 컨테이너에서 가지고 있다.
try super.encode(to: container.superEncoder())
이렇게 된다.
{
  "super" : {
   "name" : "Joe"
  },
  "emp_id" : "emp123"
}
이렇게하면 "super"라는 새로운 키에 인코딩된 부모클래스를 만들어낸다. 필요하면 이 키 이름을 커스터마이징 할 수 있다.
enum CodingKeys : String, CodingKey {
  case employeeID = "emp_id"
  case person
}


override func encode(to encoder: Encoder) throws {
  // ...

  try super.encode(to:
     container.superEncoder(forKey: .person))
}
아래의 결과가 나온다.
{
  "person" : {
   "name" : "Joe"
  },
  "emp_id" : "emp123"
}
부모클래스에서 일반적인 구조에 접근하는것은 JSON 파싱을 간단화시킬 수 있고, 어던 경우에는 코드 중복을 줄일 수 있다.

UserInfo
인코딩 디코딩 중에 동작을 변경하거나 오브젝트에 컨텍스트를 제공해야하기위해 커스텀 데이터로 표현해야 한다면, 인코딩 디코딩 중에 사용자 정보(User Info)를 전달할 수 있다.

예를들어 고객을위한 이런 JSON을 만들어주는 버전1의 API를 물려받았다고 해보자.
{
  "customer_name": "Acme, Inc",  // old key name
  "migration_date": "Oct-24-1995", // different date format?
  "created_at": "1991-05-12T12:00:00Z"
}
여기서 우리는 created_at 필드와 다른 날짜 양식을 가지는 migration_date 필드를 가지고 있다. 그리고 이름 프로퍼티가 그냥 name으로 바뀌었다고 가정하자.

이 상황은 이상적이지 않은 상황이 분명하지만, 실제로 일어나고, 종종 더러운 API를 물려받기도 한다.

우리를위해 중요한 값들을 담아둘 특별한 사용자 정보 구조체를 정의하자.
struct CustomerCodingOptions {
  enum ApiVersion {
     case v1
     case v2
  }
  let apiVersion = ApiVersion.v2
  let legacyDateFormatter: DateFormatter

  static let key = CodingUserInfoKey(rawValue: "com.mycompany.customercodingoptions")!

}
이제 이 구조체의 인스턴스를 만들어서 인코더와 디코더에 보낼 수 있다.
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy"

let options = CustomerCodingOptions(apiVersion: .v1, legacyDateFormatter: formatter)

encoder.userInfo = [ CustomerCodingOptions.key : options ]


// ...
encode 메소드 안의 모습이다.
func encode(to encoder: Encoder) throws {    var container = encoder.container(keyedBy: CodingKeys.self)


    // here we can require this be present...


    if let options = encoder.userInfo[CustomerCodingOptions.key] as? CustomerCodingOptions {



        // encode the right key for the customer name


        switch options.apiVersion {


        case .v1:


            try container.encode(name, forKey: .legacyCustomerName)


        case .v2:


            try container.encode(name, forKey: .name)

        }


        // use the provided formatter for the date


        if let migrationDate = legacyMigrationDate {


            let legacyDateString = options.legacyDateFormatter.string(from: migrationDate)


            try container.encode(legacyDateString, forKey: .legacyMigrationDate)

        }


    } else {

        fatalError("We require options")

    }



    try container.encode(createdAt, forKey: .createdAt)

}
디코드 생성자에도 정확하게 같은 것을 할 수 있다.

바깥으로부터 옵션을 제공받아 파싱을 더욱 컨트롤할 수 있는 좋은 방법이다. 게다가 DateFormatter같이 생성할때 비싼 오브젝트를 재사용할 수 있다.

다이나믹 코딩 키(Dynamic Coding Keys)
지금까지 이 가이드에서는 스위프트 네이밍과 다를때 코딩키를 표현하려고 enum을 사용했었다. 가끔 이것이 불가능할 수도 있다. 아래의 경우를 생각해보자.
{
  "kolsh" : {
   "description" : "First only brewed in Köln, Germany, now many American brewpubs..."
  },
  "stout" : {
   "description" : "As mysterious as they look, stouts are typically dark brown to pitch black in color..."
  }
}
beer 스타일의 리스트이나, 키들은 실제로 스타일의 이름이다. API는 시간이 지나면서 바뀌고 커질 수 있기 때문에 모든 가능한 상황을 표현할 수 없을 수 있다.

대신에 우리는 CodingKey의 더 다이나믹한 구현을 만들 수 있다.
struct BeerStyles : Codable {
  struct BeerStyleKey : CodingKey {
   var stringValue: String
   init?(stringValue: String)? {
     self.stringValue = stringValue
   }
   var intValue: Int? { return nil }
   init?(intValue: Int) { return nil }

   static let description = BeerStyleKey(stringValue: "description")!

  }

  struct BeerStyle : Codable {
   let name: String
   let description: String
  }

  let beerStyles : [BeerStyle]
}
CodingKeyStirngInt 값 프로퍼티를 둘 다 필요로하고 생성자를 필요로한다. 그러나 이 경우 정수 키를 지원하지 않아도 된다. 또한 스태틱 "destcription 속성을 위한 스태틱 키를 정의했었는데, 바뀌지 않을 것이다.

디코딩을 하며 시작해보자.
init(from decoder: Decoder) throws {
   let container = try decoder.container(keyedBy: BeerStyleKey.self)

   var styles: [BeerStyle] = []
   for key in container.allKeys {
       let nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
           forKey: key)
       let description = try nested.decode(String.self,
           forKey: .description)
       styles.append(BeerStyle(name: key.stringValue,
           description: description))
   }

   self.beerStyles = styles
}
여기서 우리는 컨테이너 안에서 찾은 모든 키들을 다이나믹하게 돌고, 그 키에대한 컨테이너 참조를 잡아둔다. 그리고 여기서 description을 뽑아낸다.

namedescription을 사용하여 직접 BeeryStyle 인스턴스를 생성하고 배열에 추가할 수 있다.

인코딩의 경우는 어떨까?
func encode(to encoder: Encoder) throws {
   var container = try encoder.container(keyedBy: BeerStyleKey.self)
   for style in beerStyles {
       let key = BeerStyleKey(stringValue: style.name)!

       var nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
           forKey: key)
       try nested.encode(style.description, forKey: .description)
   }
}
여기서 우리는 배열에 있는 모든 스타일들을 돌고, 스타일 이름을 위해 키를 만들어, 키에 컨테이너를 만든다. 그런다음 그 컨테이너에 description을 인코딩하기만 하면 끝난다.

우리가 볼 수 있듯, 커스텀 CodingKey를 만드는 것으로 우리가 다룰 수 있는 응답들의 타입에 많은 유연함을 제공해준다.

에러 처리
지금까지 우리는 어떠한 에러도 다루지 않았다. 이것들은 우리가 실행시킬지도 모르는 에러들이 몇개 있다. 각각은 연관된 값을 제공한다(DecodingError.Context는 언제, 어떤  문제가 생겼는지 알려주는 디버깅 설명을 제공해준다).
  • DecodingError.dataCorrupted(Context): 이 데이타는 오염되었다(즉, 우리가 생각한대로 생기지 않았다). 이것은 여러분이 디코더에 제공한 data가 JSON이 전혀 아니지만, 아마도 실패한 API 콜에서 HTML 에러 페이지일 수 있다.
  • DecodingError.keyNotFound(CodingKey, Context): 필요한 키가 발견되지 않았다. 이것은 질문에서 키를 보냈고 컨텍스트는 어디서, 왜 이런 일이 일어났는지에대한 정보를 제공한다. 이것을 받아다가 몇몇 키를 위한 fallback value를 적절하게 줄 수 있다.
  • DecodingError.typeMismatch(Any.Type, Context): 한 타입을 기대했지만 다른것을 찾았다. 아마도 그 데이터 포멧이 첫번째 버전의 API에서 변경되었을 것이다. 이 에러를 잡고 다른 타입을 사용한 데이터 찾아볼 수 있다.
인코더와 디코더에서 나온 에러들은 문제를 진단하는데 굉장히 유용하며, 특정 상황에 다이나믹하게 적용시킬 수 있는 유연함을 제공하여 적절하게 이것들을 다룰 수 있다.

한가지 이런 예로서 옛날 버전의 API 응답을 마이그레이션하는 것이다. 예를들어 디스크 어딘가에 영속의 캐시에 넣으려고 한 버전의 오브젝트를 인코딩했다고 하자. 나중에 포멧이 바뀌었지만 디스크의 자료는 그대로 있다. 이것을 로드해오려할때 에러가 발생할 수 있는데, 깔끔하게 새로운 데이터 포멧에 마이그레이션하기위해 처리할 수 있다.

더 읽을거리
  • Codable.swift: 스위프트가 오픈소스화되어서 좋은 점 중 하나는 이것이 어덯게 구현되었는지 그냥 볼 수 있다는 점이다. 꼭 한번 보자!
  • Using JSON with Custom Types: 애플이 제공하는 playground 샘플인데, 더 복잡한 JSON 파싱 시나리오를 볼 수 있다.

결론
여기까지 새로운 스위프트 4의 Codable API를 어떻게 사용하는지에대해 빠르게 훑어보았다. 추가하고 싶은게 있나? 아래에 댓글을 달아달라.

이 작업이 마음에 들었는가? 그렇다면 NSScreencast도 마음에 들것이라 생각된다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: All about Concurrency in Swift - Part 1: The Present

역자: 이 시리즈물의 2편이 나왔습니다! 조만간 번역할 예정이에요.

현재 배포된 스위프트 언어에서는 Go나 Rust가 한것 처럼 아직 네이티브 동시성 기능을 가지지 않는다.

작업들을 동시에 실행시키고 싶을때 경쟁상태의 결과를 다뤄야 한다면, 여러분이 할 수 있는 선택지는 몇개가 없다. libDispatch같은 외부 라이브러리를 사용하던지, 아니면 Foundation이나 OS가 제공하는 동기화 프리미티브(primitives)를 사용하는 것이다.

이 시리즈물의 첫번째 파트는, 스위프트3에서 우리가 처한 상황을 보고, Foundation의 락, 스레드, 타이머부터 언어의 게런티에대한 모든것과 최근에 만들어진 Grand Central Dispatch와 Operation Queues를 다룬다.

몇가지 기본 동시성 이론과 일반적인 동시성 패턴도 설명하게 될 것이다.

크리티컬 섹션과 동시 실행크리티컬 섹션과 동시 실행


스위프트가 돌아가는 모든 플랫폼에서 pthread를 사용할 수 있을지라도 이 라이브러이의 기능과 프리미티브는 이 글에서 설명하지 않을 것이며, 그것보다 더 높은 수준의 대안에대해 이야기할 것이다. NSTimer 클래스도 역서 이야기 하지 않으니 스위프트 3에서 어떻게 이것을 사용하는지 여기서 확인하자.

이미 여러번 발표했듯, 스위프트 4 이후의 주요 배포중 하나(꼭 스위프트 5는 아닐 것임)에서 더 나은 메모리 모델(memory model)을 정의하고, 새로운 네이티브 동시성 기능을 넣기위해 이 언어를 확장할 것이다. 새로운 동시성 기능은 외부 라이브러리없이 동시성 및 병렬처리를 다룰 수 있게 해주며, 동시성에대해 스위프트스러운 이상적인 방법을 정의할 것이다.

이것은 이 시리즈물의 다음 글의 주제가 될 것인데, 다른 언어에서 구현한 몇가지 대안의 방법과 패러다임을 토론하고, 이것들이 어떻게 스위프트로 구현될 수 있는지 이야기하게 된다. 그리고 오늘달에 이미 사용할 수 있는 몇가지 오픈소스의 구현을 분석하여 현재 배포된 스위프트로 Actors 패러다임, Go의 CSP 채널, Software Transactional Memory등을 이용할 수 있게 해줄 것이다.

이 두번째 글은 완전히 추측적인 것이다. 글의 주된 목표는, 이 주제에대해 소개해주어서 당신이 동시성을 어떻게 다룰지 정의하는 미래의 스위프트 배포에서 토론에 참여할 수 있게 해주는 것이다.

이글이나 나른 글의 Playground는 GitHub 나 Zipped에서 이용할 수 있다.

목차


멀티스레딩과 동시성 입문
오늘날 어떤 어플리케이션을 만들든 상관없이, 곧(혹은 훗날) 당신의 앱은 멀티스레드 실행의 환경에서 동작할 것이라는 사실을 고려해주어야한다.

하나 이상의 프로세서를 가진 컴퓨팅 플랫폼. 혹은 하나 이상의 하드웨어 실행 코어를 가진 프로세서는 10여년동안 우리 주변에 바짝 다가왔고 스레드프로세스 같은 개념은 나이를 먹어버렸다.

운영체제는 다양한 방법으로 사용자 프로그램에게 이 기능들을 제공해왔고, 모든 현대의 프레임워크나 앱은 유연성과 성능을 높히기위해 몇가지 잘 알려진 디자인 패턴들을 구현할 것이다. 그 중에는 다중 스레드도 포함되있다.

스위프트에서 어떻게 동시성을 다루는지 구체적으로 들어가보기전에, Dispatch QueuesOperation Queues를 사용할 때 필요한 기본 개념을 간단하게 설명하려 한다.

먼저 애플 플랫폼과 프레임워크가 스레드를 사용할지라도 왜 이것을 여러분의 어플리케이션에 넣으려하는지 먼저 질문해보아야한다.

일반적인 상황에서 다중스레드가 해결책이 될 수 있는 몇가지가 있다.
  • 작업 그룹 분리: 스레드는 실행 플로우의 관점에서 여러분의 어플리케이션을 모듈화하는데 사용할 수 있고, 각 스레드들은 예측할 수 있는 방법으로 같은 타입의 작업 그룹을 실행시키는데 사용할 수 있다. 여러분의 프로그램을 다른 실행 플로우로부터 고립시켜 앱의 현재 상태에대해 더 쉽게 만든다.
  • 데이터-독립의 컴포넌트들의 병렬화: 하드웨어 스레드를 지원받거나 아닌(다음에 보자) 다중 소프트웨어 스레드는 원래 입력 데이터 구조의 하위집합에서 작동하는 여러 동일한 작업 본사본들을 병렬화하는데 사용될 수 있다.
  • 조건이나 I/O를 기다리는데 깔끔한 방법: I/O를 블럭킹하거나 다른 종류의 오퍼레이션 블럭할때, 백그라운드 스레드는 이 오퍼레이션을 완료하기까지 깔끔하게 기다리는데 사용될 수 있다. 스레드의 이런 사용은 앱의 전반적인 설계를 증진하고 블럭된 호출 trivial을 다룰 수 있게 한다.

그러나 여러분의 코드를 단일 스레드의 관점에서 볼 때 이해했던 몇가지 가정이 다중 스레드가 실행있을때는 더이상 유효하지 않을 것이다.

각 스레드의 실행이 독립적으로 이루어지고, 데이터공유가 없는 이상적인 세계라면 단일 스레드에서 실행되는 코드처럼 그렇게까지 복잡하지는 않을 것이다. 그러나 보통의 경우처럼 같은 데이터에 동작하는 다중 스레드를 가진다면 이런 자료구조에 접근을 규제해야하고, 이 데이터에대한 모든 오퍼레이션이 다른 스레드의 오퍼레이션과 원치않은 인터렉션이 없도록 만드는 방법이 필요할 것이다.

동시성 프로그래밍은 그 언어나 운영체제로부터 추가적인 보증이 필요한데, 여러 스레드가 동시에 접근하려할때 변수("자원")는 어떻게 행동할지 명시적인 지정이 필요하다.

이런 언어는 메모리모델(Memory Model)을 정의해야한다. 메모리모델의 기본 진술서(basic statments)에는 동시성 스레드에서 어떻게 행동할지 명시적으로 지정해놓은 규칙들을 담아야하고. 메모리가 어떻게 공유될 수 있고 어던 종류의 메모리 접근이 유효한지 정의해야한다.

덕분에 사용자는 예상한대로 동작하는 언어를 가지게 되며, 컴파일러는 메모리 모델에 정의된 것만 반영하여 최적화를 수행할 것이라는 점을 우리는 알 것이다.

너무 엄격한 모델은 컴파일러가 발전할 것을 제안하기 때문에 메모리 모델을 정의하는 것은 언어의 발전에서 정교하게 해야한다. 독창적인 최적화는 메모리모델에서 과거의 결정에 유효하지 않을 수도 있다.

메모리모델을 정의하는 예시이다.
  • 어떤 언어의 진술서에는 atomic이 고려될 수 있는데, 어떤 스레드도 부분적인 결과를 결과를 내지 않는 완전한 곳에서만 오퍼레이션을 실행시킬 수 있다. 예를들어 필수적으로 변수들이 atomic하게 초기화될 수 있는지 없는지 알아야한다.
  • 공유된 변수를 어떻게 스레드에의해 다룰지, 디폴트로 캐싱을 할지, 특정 언어 변경자로 캐시 동작에 영향을 줄 수 있게 할지
  • 크리티컬 섹션(critical section, 공유된 자원에서 동작하는 코드 영역)에 접근을 표시하고 규제하는데 사용되는 동시성 연산자가 있다. 예로서 이것은 한번에 특정 한 코드 패스를 따르기위해 한 스레드만 허용한다.
이제 여러분의 프로그램의 동시성 사용 이야기로 돌아가자.

동시성을 올바르게 다루기위해 여러분 프로그램에서 크리티컬 섹션을 판단해야하고, 다른 스레드간에 공유된 데이터의 접근을 규제하기위해 동시성 프리미티브나 동시성을 인지하는 자료구조를 사용해야 할 것이다.

코드나 자료구조의 이런 영역에 접근 규칙을 만들면 또다른 문제들을 만들게된다. 모든 스레드가 실행하여 공유된 데이터를 수정할 기회를 제공하는 것이 바라는 결과겠지만, 어떤 환경아래 어떤 것들은 아예 실행되지 않을 수도 있고, 그 데이터는 예상하지 못했던 방법으로 변경될지도 모른다.

당신은 추가적인 과제들을 직면하게 될 것이고 어떤 일반적인 문제들과 함께 작업해야 할 것이다.
  • Race Conditions: 같은 데이터에 실행되는 여러 스레드(예를들면 동시에 읽기, 쓰기를 하는)는 오퍼레이션 시리즈의 실행 결과를 예측하기 힘들거나 스레드 실행 순서에 따라 다른 결과가 나올 수 있다.
  • Resources Contention: 다른 작업들을 실행시킬 수 있는 멀티 스레드가 같은 자원에 접근하려고하면, 요청했던 자원을 안전하게 얻는데 시간이 더 많이 요구될 것이다. 여러분이 필요한 자원을 얻는데 이런 지연은 기대하지 않았던 동작이 되버리거나, 아니면 이런 자원 접근을 규제하는 구조를 짜야한다.
  • Deadlocks: 여러 스레드에서 자원에 락을 걸었는데 서로 그 락이 풀리기를 기다리게된다. 이 스레드 그룹은 영원히 실행을 블락시킨다.
  • Starvation: 한 스레드가 절때 특정 순서에서 자원들을 얻지 못할 수 있다. 다양한 이유가 필요하며 영원히 성공하지 못할 자원 취득을 계속해서 시도한다.
  • Priority Inversion: 시스템에의해 할당된 우선순위 전환으로 높은 우선순위의 스레드가 필요로하는 자원을 낮은 우선순위의 스레드가 계속해서 취득하고 있을 수 있다.
  • Non-determinism과 Fairness: 우리는 언제 어느때의 순서에따라 스레드가 공유된 자원을 취득할 수 있을지 가정할 수 없다. 이런 지연은 우선순위를 결정할 수 없고 경쟁의 양에 크게 영향을 받는다. 그러나 크리티컬 섹션을 보호하는데 사용되는 동시성 프리미티브는 공평하게 만들어지거나, 공평을 지원하게 만들 수도 있다(used to guard a critical section can also be built to be fair or to support fairness). 기다리고 있는 모든 스레드가 크리티컬 섹션에 접근할 수 있게 보장하면서, 요청했던 명령을 침해하지 않는다.

언어 게런티
당장 스위프트 자체가 동시성과 관련된 기능을 가지고 있지 않더라도, 스위프트는 프로퍼티를 어떻게 접근할지와 관련된 몇가지 게런티를 제공한다.

예를들어 전역변수는 atomic하게 초기화되므로, 여러 스레드가 한 전역변수를 동시에 초기화하려는 상황을 직접 처리하지 않아도 되고, 초기화가 여전히 진행중일때 누군가 부분적으로 초기화된 모습을 볼 걱정을 할 필요가 없다.

아래에 싱글톤 구현을 이야기할때 이 동작에대해 다시 생각해볼 것이다.

그러나 레이지 프로퍼티(lazy property) 초기화는 atomic하게 수행되지 않는다는 것을 꼭 기억해줘야한다. 게다가 스위프트는 이제 이것을 바꾸기위한 지시자나 변경자를 제공하지 않는다.

클래스 프로퍼티에 접근도 atomic이 아니다. 만약 그렇게 만들어야 한다면, 락이나 다른 비슷한 메커니즘을 사용해서 직접 독점적 접근을 구현해야한다.

스레드
Foundation은 Thread 클래스를 제공하는데, 이 클래스는 내부적으로 pthread를 기반으로 하며, 새로운 스레드를 생성하고 클로저를 실행시키는데 사용할 수 있다.

Thread 클래스의 detachNewThreadSelector:toTarget:withObject: 메소드를 이용하여 스레드를. 생성하거나, 커스텀 Thread 클래스를 선언하고 main() 메소드를 오버리아딩하여 새로운 스레드를 만들 수도 있다.
class MyThread : Thread {
   override func main() {
       print("Thread started, sleep for 2 seconds...")
       sleep(2)
       print("Done sleeping, exiting thread")
   }
}
그러나 iOS10과 macOS Sierra부터는 마침내 모든 플랫폼에서 스레드가 실행시킬 클로저를 생성자뒤에 붙여 새로운 스레드를 생성할 수 있다. 이 글의 모든 예제는 기본 Thread 클래스를 확장한 것이므로 다른 OS에서 테스트해보지 않아도 된다.
var t = Thread {
   print("Started!")
}

t.stackSize = 1024 * 16
t.start()              //Time needed to spawn a thread around 100us
우리가 직접 시작시키기위해 필요한 스레드 인스턴스를 만들어보자. 부가적인 단계로 새로운 스레드를 위한 맞춤형 스택 크기도 지정할 수 있다.

exit()를 호출하여 갑자기 스레드를 중단시킬 수 있지만, 현재 작업들을 깔끔하게 끝낼 기회를 잃어버리므로 절때로 추천하지 않는다. 필요에따라 중단 로직을 스스로 구현하거나, cancel() 메소드를 사용하고 스레드가 자연스럽게 현재 작업을 끝내기전에 중단을 요청을 받았는지 메인 클로저 내에서 알기위해 isCancelled 프로퍼티를 확인할 수 있다.

동기화 프리미티브
공유된 데이터를 변경하고 싶은 다른 스레드들이 있을때는, 데이터 오염이나 결정되지 않은 동작을 막기위해 반드시 이런 스레드들을 어떤 방법으로 동기화해주어야한다.

스레드 동기화에 기본적으로 사용되는 것은 락(lock), 세마포어(semaphore), 모니터(monitor)이다.

Foundation은 이 모든것을 제공한다.

곧 보게 될것인데, 이런 구성들을 구현하는 클래스들(그렇다 모두 참조 타입이다)은 스위프트 3에서 접두를 빼진 않았지만 다음 스위프트 배포판 중 하나에서 빠질 수 있다.

NSLock
NSLock은 Foundation이 제공하는 락(lock)의 기본 타입이다.

스레드가 이 오브젝트에 락을 걸려고하면 두가지 일이 일어날 수 있다. 이전 스레드가 락을 걸지 않았다면 이 스레드는 락을 취득할 것이다. 혹은 락이 이미 걸려있다면 락을 건 소유자가 락을 풀때까지 스레드는 실행을 블락하고 기다릴 것이다. 즉 락은 한번에 한번에 한 스레드만 취득할 수 있는 오브젝트이며 이것이 크리티컬 섹션 접근을 완벽하게 감시할 수 있게 만들어준다.

NSLock과 Foundation의 다른 락은 공평하지 않다(unfair). 스레드의 시리즈가 락을 취득하기위해 기다릴때 원래 락은 시도했던 순서대로 락을 취득하지 않을 것이다.

스레드 경쟁이 커지는 경우에는 실행 순서를 예상할 수 없다. 많은 스레드가 자원을 취득하려 할때, 여러분의 스레드는 starvation을 겪을 수 있고, 아무리 기다려도 절때 락을 취득할 수 없을 수도 있다(혹은 적절한 시간안에 취득할 수 없을 것이다).

경쟁 없이 락을 취득하는데 필요한 시간은 100ns로 예상할 수 있겠지만, 하나 이상의 스레드가 락이 걸린 자원을 취득하려고 할때, 그 시간은 급격하게 증가한다. 따라서 성능의 관점에서 볼때 락은 자원 할당을 다루기에 최고의 해결책은 아니다.

두 스레드가 있는 예제를 보자. 락을 취득될 순서가 정해져있지 않으므로 T1이 한 row에 두번 락을 취득하는 일이 일어날 수 있다(일반적인 상황은 아니다).
let lock = NSLock()
class LThread : Thread {
   var id:Int = 0

   convenience init(id: Int) {
       self.init()
       self.id = id
   }

   override func main() {
       lock.lock()
       print(String(id)+" acquired lock.")
       lock.unlock()
       if lock.try() {
           print(String(id)+" acquired lock again.")
           lock.unlock()
       } else {  // If already locked move along.
           print(String(id)+" couldn't acquire lock.")
       }

       print(String(id)+" exiting.")




    }
}

var t1 = LThread(id:1)
var t2 = LThread(id:2)
t1.start()
t2.start()
락을 사용하기로 했을때 한가지 경고하고 싶은게 있다. 나중에 동시성 이슈를 디버깅해야할 것이다. 항상 어떤 종류의 자료구조 범위 안으로 락 사용을 제한하려 하고, 여러분의 코드베이스 여러곳에서 하나의 락 오브젝트를 직접 참조하지 않도록 노력해야한다.

동시성 문제를 디버깅하는동안, 여러분의 코드 어느 부분이 락을 잡고있는지 계속 추적해가면서 여러 함수들의 로컬 상태를 기억하는것보다는 몇가지 입장 지점으로 동기화된 자료구조의 상태를 확인하는 것이 더 좋은 방법이다. 남은 글로 가서(go the extra mile) 여러분의 동시적인 코드 구조를 잘 짜자.

NSRecursiveLock
재귀적인 락(recursive lock)은 이미 락을 건 스레드에서 여러번 락을 취득할 수 있는데, 재귀함수나 시퀀스에서 동일한 락을 확인하는 여러 함수를 호출할 시 유용하다. 이것은 기본 NSLock과는 함께 동작하지 않을 수 있다.
let rlock = NSRecursiveLock()

class RThread : Thread {

   override func main() {
       rlock.lock()
       print("Thread acquired lock")
       callMe()
       rlock.unlock()
       print("Exiting main")
   }

   func callMe() {
       rlock.lock()
       print("Thread acquired lock")
       rlock.unlock()
       print("Exiting callMe")
   }
}

var tr = RThread()
tr.start()

NSConditionLock
조건락(condition lock)은 더 복잡한 락 설정(소비자-생산자 시나리오)을 지원하는데, 각자 독립적으로 락과 언락될 수 있도록 추가적인 하위락을 제공한다.

하나의 전역의 락(특정 조건에 상관없이 락을 건다)도 사용할 수 있으며 원래의 NSLock처럼 동작한다.

공유하는 정수를 보호하는 락 예제를 보자. 소비자는 출력하고 생산자는 화면에 나타날 때마다 업데이트한다.
let NO_DATA = 1
let GOT_DATA = 2
let clock = NSConditionLock(condition: NO_DATA)
var SharedInt = 0

class ProducerThread : Thread {

   override func main() {
       for i in 0..<5 {
            clock.lock(whenCondition: NO_DATA) //Acquire the lock when NO_DATA
            //If we don't have to wait for consumers we could have just done clock.lock()
            SharedInt = i
           clock.unlock(withCondition: GOT_DATA) //Unlock and set as GOT_DATA
        }
   }
}

class ConsumerThread : Thread {

   override func main() {
       for i in 0..<5 {
            clock.lock(whenCondition: GOT_DATA) //Acquire the lock when GOT_DATA
            print(i)
            clock.unlock(withCondition: NO_DATA) //Unlock and set as NO_DATA
        }
   }
}

let pt = ProducerThread()
let ct = ConsumerThread()
ct.start()
pt.start()
락을 만들때 시작 조건을 지정해주어야하는데, 정수로 표현한다.

lock(whenCondition:) 메소드는 조건이 만족될때 락을 취득하거나 다른 스레드가 unlock(withCondition:)을 이용해서 값을 세팅할때까지 기다릴 것이다.

기본 락보다 조금 개선된 점은 좀 더 복잡한 시나리오를 만들 수 있게 해준다는 점이다.

NSCondition
조건락과 헷갈리지 말자. 한 조건(condition)은 발생 조건을 기다리기위한 명확한 방법을 제공한다.

락을 취득했던 스레드가 동작을 수행하는데 필요한 추가조건이 아직 만족되지 않았다면, 잠시 잡아두고 조건이 만족할때 작업을 계속하게 하는 방법이 필요하다.

끊임없이나 주기적으로 조건을 확인하도록 구현할 수도 있지만(busy waiting), 그렇게하면 스레드가 잡고있는 락에서 무슨일이 일어날까? 조건이 만족할때 다시 이들을 취득하길 바라면서 기다리거나 풀어주는 동안 잡아둬야 하는가(Should we keep them while we wait or release them hoping that we’ll be able to acquire them again when the condition is met)?

조건은 이 문제에대해 명확한 솔루션을 제공한다. 한번 취득한 스레드는 그 조건에대해 기다리고 있는 목록에 들어갈 수 있고, 한번 깨어난 다른 스레드는 조건이 만족했다고 신호를 보낸다.

예제를 보자.
let cond = NSCondition()
var available = false
var SharedString = ""

class WriterThread : Thread {
       override func main() {
       for _ in 0..<5 {
           cond.lock()
           SharedString = "😅"
           available = true
           cond.signal() // Notify and wake up the waiting thread/s
           cond.unlock()
       }
   }
}

class PrinterThread : Thread {
       override func main() {
       for _ in 0..<5 { //Just do it 5 times
           cond.lock()
           while(!available) {  //Protect from spurious signals
                cond.wait()
           }
           print(SharedString)
           SharedString = ""
           available = false
           cond.unlock()
       }
   }
}

let writet = WriterThread()
let printt = PrinterThread()
printt.start()
writet.start()

NSDistributedLock
분산된 락(distributed lock)은 지금까지 우리가 봤던 것과는 꽤 다르고, 이것이 자주 필요해보이진 않는다.

이것은 여러 어플리케이션 간에 공유되도록 만들어졌고 파일시스템 출입을 지원한다. 이 파일시스템은 이것을 취득해야하는 모든 앱이 분명하게 접근할 수 있어야 할것이다.

이런 종류의 락은 try() 메소드를 사용하여 취득될 수 있는데, 이 논-블락킹 메소드는 락이 취득되있는지 아닌지를 알려주는 boolean을 바로 반환한다. 락을 얻으려면 보통 한번 이상 시도해야 할것이다. 직접 실행시키거나 적절한 딜레이를 두고 연속적으로 시도할 수 있다.

분산될 락은 보통 unlock() 메소드를 사용하여 락을 푼다.

아래 기본 예제를 보자.
var dlock = NSDistributedLock(path: "/tmp/MYAPP.lock")

if let dlock = dlock {
   var acquired = false
   while(!acquired) {
       print("Trying to acquire the lock...")
       usleep(1000)
       acquired = dlock.try()
   }

   // Do something...
   dlock.unlock()
}

OSAtomic 어디있는가(Where Art Thou)?
OSAtomic가 제공하는 것과 비슷한 atomic 오퍼레이션들은 기존의 락 로직을 사용하지 않고 변수를 set, get, compare-and set 할 수 있게 해주는 간단한 오퍼레이션이다. 이들은 CPU의 특정 기능(종종 네이티브 atomic 인스트럭션)을 이용하여 앞에서 설명했던 락보다 더 좋은 성능을 낸다.

동시성을 다루는데 필요한 오버헤드가 최소한으로 줄기 때문에, 동시성 자료구조를 만들때 극도로 편리하다.

OSAtomic은 macOS 10.12부터 디프리케이트되었고 리눅스에서는 아예 사용할 수 없으나, 이것처럼 스위프트의 유용한 익스텐션을 사용한 오픈소스 프로젝트나 이것은 비슷한 기능을 제공한다.

synchronized 블럭에서
Objective-C에서 했던것처럼 @synchronized 블럭은 스위프트에서는 만들 수가 없는데, 동일한 키워드가 없다.

다윈에서는 objc_sync_enter(OBJ)objc_sync_exit(OBJ)를 직접 사용하여, 비슷한 어떤것을 준비할 수 있고, 내부적으로 @synchronized와 비슷하게 동작하는 @objc 오브젝트 모니터도 있다. 그러나 별로 의미는 없고, 이런것이 필요할때는 간단하게 락을 쓰는게 더 낫다.

그리고 Dispatch Queues를 설명할때 보게될 것인데, 동기화 호출을 수행하는 작은 코드로 이 기능을 큐로 이용할 수 있다.
var count: Int {
   queue.sync {self.count}
}

이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

GCD: Grand Central Dispatch
이 API에 친숙하지 않은 이들을 위해 Grand Central Dispatch(GCD)를 설명하자면, 이것은 큐 기반 API로 작업자 풀(worker pools)에서 클로저를 실행할 수 있게 해준ㄷ.

실행되야하는 작업을 담은 클로저는 이것을 실행시킨 큐에 담을 수 있는데, 큐의 구성 옵션에따라 순차적으로 할지, 병렬적으로 할지 정한 스레드 시리즈를 이용한다. 그러나 큐의 타입에 상관없이 작업은 항상 먼저 들어온 것이 먼저 나가는(FIFO, First-in First-out) 순서로 시작될 것이다. 즉, 작업은 항상 들어온 순서대로 시작할 것이다. 완료 순서는 각 작업의 지속시간에따라 다르다.

이것은 상대적으로 현대의 언어 런타임이 동시성을 처리할때 일반적으로 발견할 수 있는 패턴이다. 스레드 풀(thread pool)은 일련의 프리 스레드(free thread)나 연결되지 않은 스레드보다 더 쉽게 관리하고 조사하며 컨트롤 할 수 있는 방법이다.

스위프트 3에서 GCD API는 조금 바뀌었다. SE-0088는 설계를 현대화시키고 더 객체지향적으로 만들었다.

Dispatch Queues
GCD는 커스텀 큐를 생성할 수 있을 뿐만 아니라, 몇몇 미리 선언된 시스템 큐에 접근하게도 해준다.

일련의 기본 큐(이 큐는 여러분의 클로저를 차례로 실행시킬 것이다)를 생성하기 위해서는 큐를 식별하는데 쓰이는 문자열 레이블을 제공해야하며, 스택 트레이스(stack trace)에서 큐의 소유자를 간단히 추적하기위해 이 레이블은 도메인 앞부분을 뒤집어 사용하는 것을 추천한다.
let serialQueue = DispatchQueue(label: "com.uraimo.Serial1")  //attributes: .serial
let concurrentQueue = DispatchQueue(label: "com.uraimo.Concurrent1", attributes: .concurrent)
우리가 생성한 두번째 큐는 동시에 된다. 큐는 작업이 실행될때 스레드 풀에 있는 모든 사용가능한 스레드를 사용할 것이다. 이 경우에 실행 순서는 예측할 수 없고, 여러분의 큐를 추가한 순서와 어떤 방법으로도 연관시켜 완료 순서를 가정해서는 안된다.

디폴트 큐는 DispatchQueue 오브젝트에서 찾아볼 수 있다.
let mainQueue = DispatchQueue.main
let globalDefault = DispatchQueue.global()
main 큐는 순차적인 메인 큐인데, 이 큐는 iOS나 macOS에서 그래픽적인 어플리케이션을 위한 메인 이벤트 루프를 처리한다. 이 큐는 이벤트에 응답하고 사용자 인터페이스를 업데이트한다. 우리가 알고 있듯, 사용자 인터페이스에서 일어나는 모든 변경은 이 큐에서 실행되고, 이 스레드에서 오퍼레이션이 길어지면 둔감한 사용자 인터페이스로 렌더링하게 될 것이다.

이 런타임은 다른 프로퍼티로 다른 전역의 큐에 접근할 수 있게 해주는데, Quality of Service(QoS)라는 파라미터로 식별된다.

높은 우선순위부터 낮은 우선순위까지 다양한 우선순위가 DispatchQoS 클래스에 정의되있다.
  • .userInteractive
  • .userInitiated
  • .default
  • .utility
  • .background
  • .unspecified
모바일기기에서 저전력모드로 해놨을때 베터리양이 작으면 background 큐는 중단될 것이다. 이 점을 기억하자.

특정 디폴트 큐를 얻기위해 원하는 우선순위를 지정하는 global(qos:) 게터를 사용하자.
let backgroundQueue = DispatchQueue.global(qos: .background)
동일한 우선순위 명시는 커스텀 큐를 생성할때 다른 속성들과 함께(혹은 없이) 사용될 수 있다.
let serialQueueHighPriority = DispatchQueue(label: "com.uraimo.SerialH", qos: .userInteractive)

Queue 사용하기
클로저 형태의 작업들은 두가지 방법으로 큐에 담긴다. 동기적으로 sync 메소드를 사용하거나 비동기적으로 async 메소드를 사용할 수 있다.

전자를 사용하면 sync 호출은 블락되며 즉 이 클로저가 완료될 때 sync 메소드를 호출할 수 있는 반면(클로저가 끝날때까지 기다려야할 때는 유용하지만, 더 나은 방법이 있다), 후자는 클로저를 큐에 넣고 완료되는대로 계속해서 실행할 수 있게 해준다.

짧은 예제를 보자.
globalDefault.async {
   print("Async on MainQ, first?")
}

globalDefault.sync {
   print("Sync in MainQ, second?")
}
예제처럼 여러 디스패치 호출은 중첩될 수 있는데, background(낮은 우선순위)가 끝나고 사용자 인터페이스를 갱신하는 오퍼레이션이다.
DispatchQueue.global(qos: .background).async {
    // Some background work here
    DispatchQueue.main.async {
        // It's time to update the UI
        print("UI updated on main queue")
   }
}
클로저는 지정된 지연 이후에 실행될 수도 있는데, 스위프트 3은 마침내 더 편리한 방법으로 지정할 수 있게 되었다. .seconds(Int), milliseconds(Int), microseconds, nanoseconds(Int)의 네가지 시간 단위를 사용하여 인터벌을 구상할 수 있는 DispatchTimeInterval 열거형을 사용할 수 있다. 이 열거형으로 원하는 인터벌을 지정할 수 있다.

나중에 실행될 클로저의 스케줄을 짜기위해, 시간 인터벌과함께 asyncAfter(deadline:excute:) 메소드를 사용하자.
globalDefault.asyncAfter(deadline: .now() + .seconds(5)) {
   print("After 5 seconds")
}
같은 클로저를 여러번 순회하면서 실행시켜야 한다면(dispatch_apply를 사용하는 것과 같이), concurrentPerform(iterations:execute:) 메소드를 사용할 수 있다. 그러나 현재 큐의 맥락중에 가능하다면 이 클로저는 동시에 실행된다는 것을 알고 있어야 한다. 그러니 동시성을 지원하는 큐에서동작하는 sync 호출이나 async 호출에 항상 concurrentPerform(iterations:execute:) 호출을 넣어야 함을 기억하자.
globalDefault.sync {
     DispatchQueue.concurrentPerform(iterations: 5) {
       print("\($0) times")
   }
}
큐가 정상적으로 생성되는데 클로저를 처리할 준비를 하는동안 ,필요한 것을 할 수 있게 설정할 수 있다.
let inactiveQueue = DispatchQueue(label: "com.uraimo.inactiveQueue", attributes: [.concurrent, .initiallyInactive])
inactiveQueue.async {
   print("Done!")
}

print("Not yet...")
inactiveQueue.activate()
print("Gone!")
하나 이상의 속성을 지정하는 것은 처음이지만, 여러분도 볼 수 있듯, 필요하면 배열로 여러 속성들을 추가할 수 있다.

작업의 실행은 DispatchObject에서 상속한 메소드로 중단되거나 잠시 멈출 수 있다.
inactiveQueue.suspend()
inactiveQueue.resume()
setTarget(queue:)는 비활성 큐의 우선순위 구성에만 쓰이는데(이것을 활성 큐에 사용하면 크레쉬가 난다), 이 메소드도 사용할 수 있다. 이 메소드를 호출하면, 큐의 우선순위가 파라미터로 주어진 큐의 우선순위와 같아진다. 

Barriers
특정 큐에 (서로다른 지속시관과 함께) 일련의 클로저를 넣었지만, 이제 당신은 이전의 비동기 작업이 모두 완료되고나서 그 작업을 실행시키고 싶다. 이를위해 barriers를 사용할 수 있다.

우리가 이전에 만들었던 동시적 큐에 5개의 테스크(1초에서 5초까지 sleep하는 테스크)를 추가하고, 다른 작업이 완료되면 뭔가를 출력하기위해 barrier를 사용해보자. 마지막 async 호출에 DispatchWorkItemFlags.barrier를 지정할 것이다.
globalDefault.sync {
    DispatchQueue.concurrentPerform(iterations: 5) { (id:Int) in
       sleep(UInt32(id) + 1)
       print("Async on globalDefault, 5 times: " + String(id))
    }
}

globalDefault.async (flags: .barrier) {
   print("All 5 concurrent tasks completed")
}

싱글톤과 Dispatch_once
이미 알고 있을지도 모르겠지만 스위프트 3은 dispatch_once와 동일한 함수가 없다. 이 함수는 싱글톤을 스레드-세이프하게 만드는데 많이 사용되었었다.

운좋게도 스위프트는 전역변수가 atomic하게 초기화됨을 보장한다. 그리고 상수가 한번 초기화되고나면 값을 바꿀 수 없다는 점을 생각해본다면, 이 두 프로퍼티를 이용한 전역 상수는 싱글톤을 쉽게 구현하기에 좋은 후보자가 될 것이다.
final class Singleton {

   public static let sharedInstance: Singleton = Singleton()

   private init() { }

   // ...
}
우리 클래스를 final로 선언하여 상속할 수 없게 만들고, 지정된 생성자를 private으로 만들어서 이 오브젝트의 인스턴스를 추가로 생성하지 못하게 만든다. public static 상수는 싱글톤에서 들어가는 부분이고, 이것은 단 하나의 공유된 인스턴스를 찾는데 사용될 것이다.

한번만 실행될 코드블럭을 만들때도 똑같이하면 된다.
func runMe() {
   struct Inner {
       staticlet i: () = {
           print("Once!")
       }()
   }
   Inner.i
}

runMe()
runMe() // Constant already initialized
runMe() // Constant already initialized
가독성은 떨어지지만 동작은 한다. 한번만 쓰이기위한 코드이면 수용할 수 있는 구현이다.

그러나 만약 그 기능과 dispatch_once API를 정확하게 복제해야한다면, 동기화된 블럭 섹션에서 한 익스텐션으로 표현하듯 처음부터 구현해야한다(But if we need to replicate exactly the functionality and API of dispatch_once we need to implement it from scratch, as described in the synchronized blocks section with an extension).
import Foundation public extension DispatchQueue {

   private static var onceTokens = [Int] ()
   private static var internalQueue = DispatchQueue(label: "dispatchqueue.once")

   public class func once(token: Int, closure: (Void) -> Void) {
       internalQueue.sync {
           if onceTokens.contains(token) {
               return
           } else {
               onceTokens.append(token)
           }
           closure()
       }
   }
}

let t = 1
DispatchQueue.once(token: t) {
   print("only once!")
}
DispatchQueue.once(token: t) {
   print("Two times!?")
}
DispatchQueue.once(token: t) {
   print("Three times!!?")
}
예상한대로 세개중 첫번째 클로저만 실제 호출될 것이다.

대신 여러분의 플랫폼에서 쓸 수 있다면 objc_sync_enterobjc_sync_exit를 사용하여 약간 더 좋은 성능을 낼 수도 있다.
import Foundation

public extension DispatchQueue {

   private static var _onceTokens = [Int] ()

   public class func once(token: Int, closure: (Void) -> Void) {
       objc_sync_enter(self);
       defer { objc_sync_exit(self) }
       if _onceTokens.contains(token) {
           return
       } else {
           _onceTokens.append(token)
       }
       closure()
   }
}

Dispatch Groups
(다른 큐에 추가할지라도) 여러 테스크를 가지고 있으면서 그것들의 완료를 기다린다면 dispatch group으로 그룹화할 수 있다.

예제를 보면 syncasync 호출로 한 테스크를 직접 특정 그룹에 추가할 수 있다.
let mygroup = DispatchGroup()

for i in 0..<5 {
   globalDefault.async(group: mygroup) {
       sleep(UInt32(i))
       print("Group async on globalDefault:" + String(i))
   }
}
이 테스크는 globalDefault에서 실행되지만 mygroup 완료를위한 핸들러를 등록할 수 있다. 이 핸들러는 모든 테스크가 완료되면 우리가 원하는 큐에서 한 클로저를 실행시킬 것이다. wait() 메소드는 블럭킹 지연(blocking wait)을 실행시키는데 사용할 수 있다.
print("Waiting for completion...")
mygroup.notify(queue: globalDefault) {
   print("Notify received, done waiting.")
}
mygroup.wait()
print("Done waiting.")
그룹으로 작업을 추적할 수 있는 또다른 방법이 있다. 큐에서 호출할 때 그룹을 지정하는 것 대신, 직전 그룹을 들어가고(enter) 나오도록(leave) 설정하는 것이다.
for i in 0..<5 {
   mygroup.enter()
   sleep(UInt32(i))
   print("Group sync on MAINQ:" + String(i))
   mygroup.leave()
}

Dispatch Work Items
클로저는 큐에서 실행될 작업을 지정해주는 일만 하는것이 아니라, 그것의 실행 상태를 계속 추적할 수 있도록 컨테이너 타입도 필요하다. 이를위해 DispatchWorkItem이 사용된다. 클로저를 받는 모든 메소드는 work item 종류를 가진다.

work item은 스레드풀의 큐에의해 실행될 클로저를 캡슐화하는데, perform() 메소드를 호출한다.
let workItem = DispatchWorkItem {
   print("Done!")
}
workItem.perform()
그리고 WorkItems은 다른 유용한 메소드를 제공한다. notify는 특정 큐에서 완료될때 클로저를 실행시킨다.
workItem.notify(queue: DispatchQueue.main) {
   print("Notify on Main Queue!")
}
defaultQueue.async(execute: workItem)
클로저가 실행될때까지 기다리게 할 수도 있고, cancel 메소드를 써서 큐가 실행하려하기 전에 제거하라고 알릴 수도 있다.
print("Waiting for work item...")
workItem.wait()
print("Done waiting.")
workItem.cancel()
여기서 한가지 알아야할 중요한 사실은 wait()는 완료를 기다리기위해 현재 스래드를 블럭하지 않고 큐 안에서 바로 이전의 work item들의 우선순위를 올려, 이 특정 item을 가능한 빨리 완료시키려 한다는 점이다.

Dispatch Semaphores
dispatch semaphore는 락이다. 이것은 카운터의 현재 값에따라 하나 이상의 스레드가 락을 취득할 수 있게 해준다.

카운터(세마포어를 취득할대마다 감소시킴)가 0이 될때 스레드는 세마포어를 wait한다.

세마포어에 접근하기위한 슬롯은 대기중인 스레드 호출 signal에대해 열리는데, 이 signal은 카운터를 증가시킨다.

예제를 보자.
let sem = DispatchSemaphore(value: 2)
// The semaphore will be held by groups of two pool threads
globalDefault.sync {
   DispatchQueue.concurrentPerform(iterations: 10) { (id:Int) in
       sem.wait(timeout: DispatchTime.distantFuture)
       sleep(1)
       print(String(id) + " acquired semaphore.")
       sem.signal()
   }
}

Dispatch Assertions
스위프트 3은 현재 실행 맥락안에서 assertion을 실행시킬 수 있는 함수를 소개했다. 이것은 원했던 큐에서 크롤저가 실행되고 있는지 확인할 수 있게 한다. 우리는 DispatchPredicate 열거형의 3가지 case로 만들 수 있다. .onQuquq는 특정 큐에 있는지 확인한다. .notOnQueue는 그 반대를 확인한다. .onQueueAsBarrier는 현재 클로저나 work item이 한 큐의 barrier로 동작하고 있는지 확인한다.
dispatchPrecondition(condition: .notOnQueue(mainQueue))
dispatchPrecondition(condition: .onQueue(queue))
이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

Dispatch Sources
dispatch sources는 이벤트 핸들러를 사용하는 이벤트(커널 시그널이나 시스템, 파일, 소켓)와 관련된 시스템단의 비동기 이벤트를 편리하게 처리할 수 있게 해준다.

몇가지 종류의 Dispatch Sources를 사용할 수 있는데, 아래처럼 묶을 수 있다.
  • Timer Dispatch Sources: 시간내에 혹은 주기적인 이벤트에서 특정 시점에 이벤트를 만드는데 사용된다(DispatchSouceTimer).
  • Signal Dispatch Souces: UNIX 시그널을 다루는데 사용된다(DispatchSourceSignal).
  • Memory Dispatch Sources: 메모리 사용 상태 관련 알림을 등록하는데 사용된다(DispatchSourceMemoryPressure).
  • Discriptor Dispatch Sources: 파일이나 소캣 관련 여러 이벤트를 등록하는데 사용된다(DispatchSourceFileSystemObject, DispatchSourceRead, DispatchSourceWrite).
  • Process Dispatch Sources: 어떤 이벤트에대해 그들의 실행 상태 관련 외부 프로세스를 모니터링하는데 사용된다(DispatchSourceProcess)
  • Mach related Dispatch Source: Mach 커널의 IPC 기능과 관련된 이벤트를 처리하는데 사용된다(DispatchSourceMachReceive, DispatchSourceMachSend).
그리고 필요시 여러분만의 Dispatch Sources를 만들 수도 있다. 모든 Dispatch Sources는 DispatchSourceProtocol을 따르며, 이 프로토콜은 핸들러를 등록하고 Dispatch Sources의 활성 상태를 수정하는데 필요한 기본 오퍼레이션을 정의한다.

이 오브젝트를 어떻게 사용하는지에대한 이해를 돕기위해 DispatchSourceTimer 예제를 한번 보자.

Sources는 DispatchSource가 제공하는 실용적인 메소드로 생성할 수 있다. 이 단편코드에서는 makeTimerSource를 사용하여 핸들러 실행하는데 사용하고 싶은 dispatch 큐를 지정할 것이다.

Timer Sources는 다른 파라미터가 없으므로 큐만 지정하면 source를 생성할 수 있다. 곧 보겠지만, 여러 이벤트를 처리할 수 있는 dispatch source는 항상 이벤트 식별자를 지정해야 할 것이다.
let t = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
t.setEventHandler{ print("!") }
t.scheduleOneshot(deadline: .now() + .seconds(5), leeway: .nanoseconds(0))
t.activate()
source가 생성되면 setEventHandler(closure:)로 이벤트 핸들러를 등록하고, 다른 설정이 필요없으면 active()로 dispatch source를 켜자(이전에 libDispatch는 resume()을 사용해야 했다).

Timer Sources는 오브젝트가 전달할 이벤트에 어떤 종류의 시간을 설정할지에대한 추가적인 단계가 필요하다. 위 예제에서 우리는 엄격한 시간제한 등록 후 5초 딜레이될 이벤트를 정의하고 있다.

이벤트를 전달하기위한 오브젝트도 설정할 수 있는데, Timer 오브젝트로 하는 것처럼 가능하다.
t.scheduleRepeating(deadline: .now(), interval: .seconds(5), leeway: .seconds(1))
dispatch source로 작업을 끝내고나서 이벤트 전달을 완전히 멈추려면, 이밴트 소스를 중지시키는 cancel()을 호출하고, 핸들러를 설정했다면 취소를 호출한 뒤, 핸들러를 등록 해제하는것처럼 최종 정리 작업을 실행한다.
t.cancel()
handleRead() 함수는 소켓에 들어오는 데이터 버퍼에서 새로운 바이트를 사용할 수 있게 되면 전용 큐에서 호출하게 될 것이다. Kitura도 버퍼로된 쓰기를 위해 WriteSource를 사용하는데 쓰기 속도(pace)를 효율적으로 맞추기위해 dispatch source 이벤트를 사용하며, 소켓 채널이 보낼 준비가 되는대로 새로운 바이트를 쓴다. I/O를 할때 read/write dispatch sources는 보통 *NIX 플랫폼에서 사용하는 저수준 API보다 더 좋은 고수준의 대안이 될 수 있다.

나머지 source 타입들도 비슷하게 동작한다. 사용할 수 있는 모든 항목들은 libDispatch의 문서에서 확인할 수 있지만, 그 중 Mach source나 memory pressure source 같은 몇몇은 다윈 플랫폼에서만 동작한다는 것을 기억하자.

Operations과 OperationQueues
Operation Queues에대해 간단히 이야기해보자. 이것은 GCD의 상위에 탑재된 추가 API이다. 이것은 동시적인 큐와 모델 테스크를 오퍼레이션으로 사용하고, 취소하기도 쉬우며, 다른 오퍼레이션이 완료됨에따라 그들의 실행을 가질 수 있다.

Operations은 실행 순서를 정하는 우선순위를 가질 수 있다. 그리고 이것은 OperationQueues에 추가되어 비동기적으로 실행된다.

기본적인 예제를 보자.
var queue = OperationQueue()
queue.name = "My Custom Queue"
queue.maxConcurrentOperationCount = 2

var mainqueue = OperationQueue.main //Refers to the queue of the main thread

queue.addOperation{
   print("Op1")
}
queue.addOperation{
   print("Op2")
}
Block Operation 오브젝트를 생성하여 큐에 넣기전에 설절할 수도 있고, 필요하면 이런 종류의 오퍼레이션에 한개 이상의 클로저를 넣을 수도 있다.

target과 selector로 오퍼레이션을 생성하는 NSInvocationOperation은 스위프트에서는 사용할 수 없다.
var op3 = BlockOperation(block: {
   print("Op3")
})
op3.queuePriority = .veryHigh
op3.completionBlock = {
   if op3.isCancelled {
       print("Someone cancelled me.")
   }
   print("Completed Op3")
}

var op4 = BlockOperation {
   print("Op4 always after Op3")
   OperationQueue.main.addOperation {
       print("I'm on main queue!")
   }
}
Operation은 우선순위(priority)와 완료 클로저를 가질 수 있다. 여기서 이 클로저는 메인 클로저가 완료되면 실행될 것이다.

op4에서 op3까지 의존성(dependency)을 추가할 수 있으므로 op4op3의 완료를 기다렸다가 실행될 것이다.
op4.addDependency(op3)

queue.addOperation(op4)  // op3 will complete before op4, always
queue.addOperation(op3)
removeDependency(operation:)으로 의존성을 제거할 수 있고, 이 의존성들은 public으로 접근할 수 있는 dependencies 배열에 담겨있다.

한 오퍼레이션의 현재 상태는 특정 프로퍼티를 이용해서 알 수 있다.
op3.isReady       //Ready for execution?
op3.isExecuting   //Executing now?
op3.isFinished    //Finished naturally or cancelled?
op3.isCancelled    //Manually cancelled?
cancelAllOperations 메소드를 호출하여 한 큐에 있는 모든 오퍼레이션 프레젠트를 취소할 수 있다. 이 메소드는 큐에 남아있는 오퍼레이션의 isCancelled 플레그를 on으로 설정한다. 한 오퍼레이션을 취소할때는 그것의 cancel 메소드를 호출하면 된다.
queue.cancelAllOperations()

op3.cancel()
어느 큐에서 실행될지 스케줄이 잡힌 뒤에 오퍼레이션이 취소되었다면, 그 오퍼레이션 안에서 실행을 스킵하기위해 isCancelled 프로퍼티를 확인할 것을 추천한다.

이제 마침내 당신은 한 오퍼레이션 큐에서 새로운 오퍼레이션 실행도 멈출 수 있게 되었다(현재 실행되고 있는 오퍼레이션에는 영향을 주지 않는다).
queue.isSuspended = true
이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

마지막 생각들
이 글은 오늘날 스위프트에서 사용할 수 있는 외브 프레임워크를 이용하여, 동시성 관점에서 무엇을 할 수 있는지 좋은 정리를 제공할 것이다.

Part2는 언어 기능의 관점에서 외부 라이브러리에 의존하지 않고 "네이티브한" 동시성을 처리하는 것에 대해 초점을 맞출 것이다. 오늘날 이미 있는 몇가지 오픈소스 구현의 도움을 받아 몇가지 흥미로운 패러다임을 설명할 것이다.

이 두개의 글이 동시성의 세계에 입문하기 좋게 만들어주고, 이것이 스위프트 5(희망하길)에서 고려되기 시작할때 swift-evolution 메일링 리스트의 토론을 이해하고 참여하는데 도움이 될 것이다.

동시성이나 스위프트에 더 흥미로운 자료는 Cocoa With Love 블로그에서 확인할 수 있다.

이 글이 마음에 든다면 나에게 윗해달라!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Isolating tasks in Swift, or how to create a testable networking layer.

최근 몇년동안 더욱더 멋져지고있는 iOS 아키텍처가 많다. 모두 유효하고 부분적으로 장단점을 가진다. 이들은 모두 같은 것을 다룬다. 바로 프레젠테이션으로부터 비지니스 로직을 분리하는 것이다. 오늘 나는 여러분이 다음 프로젝트의 아키텍처에 적용시킬 수 있지만 어떤 아키텍처를 사용하더라해도 쓸 수 있는, 간단한 개념을 적어보려 한다.

꽤 일반적인 네트워크 레이어
내 관점을 설명하기 위해, 나는 먼저 보통 네트워크 레이어를 어떻게 구현하는지에대해 이야기할 것이다.

나는 각기 다른 여러 네트워크 레이어를 보아왔다. 대부분 NetworkManager, ConnectionManager 이런 모습이다. 앱에서 한 클래스안에 있는 모든 API 호출을 담고있다. 이게 유효하고 동작할지라도, 소프트웨어 설계에서 핵심 개념인 단일책임(Single Resoponsibility)은 실패한 것이다.

ConnectionManager에는 좋다고 생각되는 책임들을 너무 많이 담고있다. 게다가 종종 싱글톤으로 구현된다. 그리고 나는 싱글톤이 필연적으로 나쁘다고 말하진 않겠지만, 싱글톤은 의존성으로서 주입될 수 없고 테스팅에 쉽게 목(mock)이 될 수도 없다.

네트워크 레이어는 일반적으로 싱글톤으로 구현된다네트워크 레이어는 일반적으로 싱글톤으로 구현된다


이것은 매우 자주있는 방법이다. 나는 이것을 MVVM나 MVP 아키텍처에서도 보았다.

다른 방법
데이터 접근 레이어는 다른 방법으로 구현될 수 있다. 네트워크 패칭에서 그 프로세스를 표현해보자.

네트워크 호출에 포함된 단계네트워크 호출에 포함된 단계


이 방법을 넣어 세단계로 네트워크 호출을 함축한다.
  1. 요청 만들기: 요청 만들기에서는 URL, method, 파라미터(URL 경로든 http 바디에든), HTTP 헤더를 설정한다.
  2. 요청 디스패치하기: 이것은 매우매우 중요한 단계이다. 이전단계에서 만들어지고 설정된 이 요청은 반드시 URLSession이나 이걸 덮는 레이어(예를들면 Alamofire)를 사용하여 디스패치 되야한다.
  3. 응답을 받고 파싱하기: 이 부분은 이전 두 단계와 분리되어 구현되야할 중요한 단계이다. 이것은 JSON이나 XML 응답을 검증하고 유효한 Entity(혹은 Model)에 파싱하는 곳이다.

여러분의 아키텍처가 깔끔해지고 테스트하기 좋아지길 원한다면, 이 세단계는 다른 오브젝트에서 구현되어야한다.

네트워크 레이어는 적어도 세 컴포넌트를 사용하여 구현되어야함네트워크 레이어는 적어도 세 컴포넌트를 사용하여 구현되어야함


  1. Request: Request 오브젝트는 네트워크 요청을 구성하는데 필요한 모든 것들을 가진다. 이 구조체(혹은 클래스)는 하나의 네트워크 요청을 구성하고있는 책임을 갖는다. 그리고 한 네트워크 요청에 한 Request 오브젝트굉장히 중요하다.
  2. NetworkDispatcher: NetworkDispatcherRequest를 받아 Response를 반환하는 역할의 오브젝트이다. 이것은 프로토콜로 구현될 수 있다. 구체적인 클래스(혹은 구조체)가 아닌 프로토콜로 코드를 짤 수 있지만, 절때 싱글톤으로 구현하지 말아야한다. 그렇게 한다면, 이 NetworkDispatcherMockNetworkDispatcher과 대체될 수 잇고, 이 목은 실제 네트워크 요청을 날리지 않는 대신에 JSON 파일로부터 응답을 받아준다. 이것이 자연스럽게 테스트하기 좋은 아키텍처를 만든다.
  3. NetworkTask: NetworkTaskTask라는 제네릭 클래스의 자식클래스이다. 이 Task(좀 있다가 더 설명할것이지만)는 비동기적으로든 동기적으로든 Input 타입을 받아서 Output 타입을 반환하는 책임을 가지는 제네릭 클래스이다. TaskRxSwiftReactiveCocoaHydraMicrofuturesFOTask나 아니면 간단하게 클로저를 이용해서 구현할 수 있다. 여러분에 달려있다. 여기서 중요한 부분은 세부적인 구현이 아니라 개념이다.

요청 만들기
RequestURLRequest를 만드는데 필요한 모든 구성에대한 책임을 가진 오브젝트이다.

네트워크 요청에대한 예시는 다음과같이 생겼을 것이다.
//
//  Request.swift
//
//  Created by Fernando Ortiz on 2/12/17.
//

import Foundation

enum HTTPMethod: String {
    case get, post, put, patch, delete
}

protocol Request {
    var path        : String            { get }
    var method      : HTTPMethod        { get }
    var bodyParams  : [String: Any]?    { get }
    var headers    : [String: String]? { get }
}

extension Request {
    var method      : HTTPMethod        { return .get }
    var bodyParams  : [String: Any]?    { return nil }
    var headers    : [String: String]? { return nil }
}
여기서 중요한 부분은 Request가 분리된 오브젝트로 구현되었다는 점이다. 물론 Moya promotes처럼 열거형으로 구현될 수도 있는데, 여러분이 원하는 것에 달렸다. 나는 개인적으로 객체지향 스타일을 선호하고, BaseRequest 클래스와 AuthenticatedRequest같은 자식클래스나 GetAllUsersRequest, LoginRequest같은 세부적인 요청을 구현하길 선호한다.



NetworkDispatcher 구현하기
NetworkDispatcher는 네트워크 요청을 디스패치하는 책임을 가지는 컴포넌트이다.

주의: 여기서부터 내 예제에는 RxSwift를 사용할 것이지만, 여러분은 여러분이 좋아하는 방법으로 구현하면 된다.
//
//  NetworkDispatcher.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//  Copyright © 2017 Fernando Martín Ortiz. All rights reserved.
//

import Foundation
import RxSwift

protocol NetworkDispatcher {
    func execute(request: Request) -> Observable<Any>
}
NetworkDispatcherRequest 오브젝트를 디스패치하고 응답을 반환하는 단일 책임을 가진다.
"
구체적인 구현대신 이 프로토콜을 사용하는것에서 멋진 점은 프로토콜 기반 구현은 쉽게 교체가능하게 만들어준다. MockNeoworkDispatcher은 실제 "네트워크" 작업을 실항하지는 한고 대신 JSON 파일에서 응답을 반환해주도록 하여 더욱 테스트하기 쉽게 만들어준다.

Task 고립시키기
Task는 하나의 로직 오퍼레이션을 실행시키는 책임의 간단한 오브젝트이다. 뒷단에서 사용자를 받아오거나, 로그인하기, 사용자 등록하기등이 있을 것이다. Task는 동기적으로나 비동기적으로 일어날 수 있지만, 클라이언트단에서 투명해야한다. 나는 편리한 추상화인 RxSwift의 Observable을 사용하길 좋아하는데, Promise, Signal, Future, 혹은 간단한 콜백이 충분할 수 있다.

Task의 간단한 구현은 아래처럼 될 수 있다.
//
//  Task.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//

import Foundation
import RxSwift

class Task<Input, Output> {
    func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}
나는 객체지향 스타일을 사용했지만, 연관 타입이나 타입 erasure같은 것도 좋은 방법으로 사용할 수 있다. 이것도 잘 동작할 것이다. 내가 이런 객체지향 스타일을 좋아하는 이유는 덜 조잡하고 구현하기 단순해 보이기 때문이다.

모든 TaskInput 타입과 Output 타입이라는 두 제네릭 파라미터를 필요로한다. TaskInput 오브젝트를 받아 Output을 반환하는 일을 포함한 작업을 수행하는데, Observable처럼 이것을 추상화하여 사용할 것이다.

Task를 네트워크 작업을 실행시키기위해 특별하게 만들어줘야한다.
//
//  NetworkTask.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//

import Foundation
import RxSwift

class NetworkTask<Input: Request, Output>: Task<Input, Output> {
    let dispatcher: NetworkDispatcher

    init(dispatcher: NetworkDispatcher) {
        self.dispatcher = dispatcher
    }

    override func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}
위에서 볼 수 있듯 ,NetworkTask는 두가지 제네릭 타입이 필요한데, InputOutput이다. Input이 반드시 Request 오브젝트이여야한다는 것은 당연한 일이다. NetworkTaskNetworkDispatcher로만 인스턴트화되어야 하므로 테스트하고 싶을때 MockNetworkDispatcher를 쉽게 보낼 수 있다.

아키텍처 검토하기
이 방법으로 비지니스 로직을 구현하면 복잡성을 설명하거나 테스트 용이함을 증가시키지 않고 결합력을 줄이는데 도움이 된다.

이 방법은 아래처럼 다이어그램으로 표현될 수 있다.

Task 기반 네트워크 레이어Task 기반 네트워크 레이어


결론
분리된 오브젝트에서 비지니스 로직 오퍼레이션을 고립시키는 것은 더욱 테스트하기 좋은 아키텍처로 만들기 때문에 좋은 방법이다. 복잡성을 줄여주고, 여러분이 사용하는 아키텍처를 전반적으로 독립시킨다. 이것은 ViewModel, Presenter, Interactor, Store 혹은 프레젠테이션 로직에서 비지니스 로직을 분리하는데 사용하기위한 어떤것 뒤에서든 사용될 수 있다.

이것이 나만큼 도움이 되었길 바란다. 뭔가 의심스러운 점이 있거나, 더 좋은 방법을 안다면 커멘트를 남겨달라.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: New stuff from WWDC 2017

올해 그 시기가 다시 돌아왔다. 여러분도 나와 비슷하다면 아마 최근에 몇주동안 많은 시간을 WWDC 발표에 쏟아 부었을 것이다. 릴리즈 노트를 파보고, WWDC 발표를 보고, cool new stuff으로 가지고 놀아보면서 말이다. 그리고 보통 새로운것을 처리하는 그 양이 어마어마하게 많다.

그래서 지난 2년처럼 나는 다양한 소스로부터 찾을 수 있었던 모든 것들을 리스트로 조직화하여 준비했었다. macOS/iOS/watchOS/tvOS에서 사용자가 직면하는 모든 기능들, 모든 새로운 프레임워크, API, 그리고 각각 SDK의 개선점들과 애플의 개발자 도구들이 있다.

여기있는 모든 것들의 더 많은 정보를 위해서는 섹션 위에있는 링크를 따라가서 애플의 개발자 사이트나 "What's New" 문서로 가고 developer.apple.com에 릴리즈 노트를 다운받자. 그리고 물론 새롭게 리디자인된  비공식 WWDC Mac 앱에서 여러분이 관심있는 발표들도 보자.

지난 최근년도에 노트를 확인하고싶으면(예를들어 여러분의 앱에서 iOS9 지원을 멈추려할때 확인하고 싶은 것들), 아래 포스트를 확인해보자.

macOS 10.13 High Sierra
  • Mail
    • 중요한 이메일의 스마트 하이라이트
    • 작성창(compose window)을 위한 뷰 분리
    • 적은 디스크 공간 사용하기
  • 메시지 앱: iOS/macOS간에 iCloud로 대화 동기화하기(end-to-end 암호화)
  • 노트 앱
    • 노트 핀하기
    • 테이블 지원
  • 사진 앱
    • 퍼시스턴트 사이드바
    • 모든 불러온 자료를 연대순으로 보여주기(shows all imports chronologically)
    • 증진된 얼굴 인식
    • 기기간의 사람들 동기화
    • 칼라 곡선과 칼라 선택 편집 도구
    • 사진 인쇄를 위해 더 많은 파트너
    • 메모리즈를 위한 더 많은 토픽들
    • 제3자 앱에서 사진을 편집하여 사진 앱에 다시 저장할 수 있음
    • 애니메이션이 있는 GIF를 실행하기
    • 라이브 사진 효과
  • 사파리
    • 비디오 자동재생 막기
    • 지능적 추적 보호(intelligent tracking prevention)
    • 사이트별 세팅(예를들어 어떤 사이트에대해 자동재생 활성화나 내용 차단기 비활성)
    • 자동으로 읽기 모드
  • 스포트라이트는 더 많은 종류를 찾아줌(예를들어 비행기 정보나 위키피디아에서 나온 여러 결과들)
  • 자연스러워짐, 더 자연스러워진 음향의 Siri 목소리
  • 윈도우 서버(미션 컨트롤 등) Metal 2의 상위에 탑재됨
  • APFS는 새로운 설치에대해 새로운 디폴트 파일 시스템
  • H.265(HEVC) 비디오 지원, 최신 맥에대해 하드웨어 가속이 탑재
  • 접근성
    • 새로운 화면위 접근성 키보드
    • Siri에게 타이핑하기(목소리 인터페이스를 비활성하기)
    • 이미지의 보이스오버 설명(voiceover descriptions)은 이제 이미지 내용의 설명이나 인식된 텍스트를 자동으로 포함할 것임

iOS11
  • 리디자인된 컨트롤 센터
    • 단일 페이지, 애니메이션
    • 3D 터치로 보여지는 더 많은 컨트롤들
    • 몇 영역은 커스터마이징 가능함
    • 토클이 가능함(예를들어 셀룰러 데이터, 핫스팟, 에어드랍)
  • 앱 변경의 새로운 디자인
    • 새로운 애니메이션
    • 더이상 첫번째 앱으로 홈 화면을 보지 않아도 됨
  • 락스크린에서 지나간 노티피케이션 접근가능(상단의 노티피케이션 패널은 이제 락스크린처럼 동일함)
  • 더이상 홈스크린의 독에 있는 앱들은 이름을 보이지 않음
  • 비밀번호 화면, 사진 앱 숫자 입력 화면, 숫자 키보드의 디자인이 바뀜
  • 이제 탭바는 약간 더 볼드된 문자와 채워진 기호(glyph)를 사용함
  • 가로 모드에서 탭바는 좁아지고 레이블은 오른쪽에 나타남
  • 다양한 앱에서 큰 볼드 헤딩으로 새로운 헤더 디자인(iOS10의 음악 앱같은)
  • 멀티터치로 디래그 앤 드롭 지원
    • iPhone과 iPad앱 안에서나 혹은 앱 간의
  • 파일 앱 – 파인더 앱처럼 태그, 좋아요, 중첩된 폴더, 클라우드 저장소 등. (iCloud Drive 앱을 대체하게됨)
  • 애플 뮤직
    • 공유된 "up next"
    • 여러분의 친구들이 무엇을 듣는지 보여줌
    • 여러분의 프로필을 구성하고 다른사람들과 공유되고 싶은 것을 정하기
  • 앱스토어: 완전히 리디자인된 앱
  • 캘린더: 새로운 디자인
  • 카메라: QR 코드 지원
  • 홈: NFC & QR코드를 이용한 페어링으로 스피커(여러 방의 오디오 – AirPlay 2), 스프링클러, 수도꼭지를 지원
  • 지도
    • 쇼핑몰이나 공항에서 층 도면
    • 제한 속도 표시
    • 차선 안내
    • 한손 줌(구글 맵처럼 더블탭+드래그업/다운)
    • 모의-AR 비행 모드 (소스)
  • 메시지
    • 리디자인된 앱 서랍
    • 앱 서랍으로 넘어간 Digital Touch
    • iOS/macOS간의 대화를 자동으로 동기화하기(end-to-end 암호화)
    • 개인 대 개인 지불을 위한 애플 패이
    • 새로운 "Echo"와 "스포트라이트" 화면 효과
    • 옛날 대화를 자동으로 지우는 것을 선택함
  • 노트
    • 노트를 핀하기
    • 목록 화면에서 노트를 잠그기(스와이프 액션)
    • 테이블을 지원
    • 선/그리드 스타일을 선택하기 (source)
    • 한 라인내 그리기
  • 사진
    • 메모리즈를 위한 더 많은 주제
    • 메모리즈를 세로로 재생 가능
    • 애니메이션된 GIF 재생
    • 라이브 포토를 자르기
    • 라이브 포토를 위해 키프레임 선택하기
    • 라이브 포토 효과: 루프, 바운싱, 뮤드, 긴 노출
    • iOS의 사진으로부터 쉽게 와치 페이스(photo or kaleidoscope)를 만들 수 있는 옵션
    • 비디오는 HEVC 포맷(2배까지 압축됨)으로 저장되고, 사진은 HEVC 기반의 HEIF 포맷으로 저장됨
    • iOS9에서처럼 "편집" 버튼이 우측 상단으로 돌아감
  • 팟캐스트: 새로운 디자인
  • 사파리
    • 지능적 추적 보호(intelligent tracking prevention)
    • 세팅에서 "경험적 피처드" 메뉴는 완성되지 않은 새로운 API 가능
    • 추적 보호와 카메라/마이크 접근을 비활성 가능
  • 설정
    • 백그라운드 앱 갱신은 WiFi 연결에서만 동작되도록 세팅할 수 있음
    • 새로운 "계정과 비밀번호" 패널("메일"과 "사파리"에서 확장됨)
    • 리디자인된 "저장공간과 iCloud 사용" 패널("iPhone 저장공간으로)
    • "일반/스포트라이트 검색"이 "Siri" 패널로 합쳐짐
    • 재구성된 "알림" 섹션
    • 이제 모든 앱에대해 항상 "사용하는 동안" 위치 허가를 선택할 수 있음
    • "끄기" 버튼이 일반 메뉴에 있음
    • 시스템 수준에서의 소셜 네트워크 로그인이 제거됨(Twitter, Facebook 등)
  • Siri
    • 자연스러워짐, 더 자연스러워진 음향의 목소리
    • 여러 결과, 추가의 질문들
    • 번역(베타) –영어에서 프랑스어, 독일어, 이탈리아어, 스페인어, 중국어
  • Siri 지능
    • 더 많은 곳에서 다 많은 종류의 제안
    • 기기간에 동기화된 당신의 정보 (end-to-end 암호화)
  • TV 앱: Amazon Prime은 내년에 추가될 것임
  • 접근성
    • "스마트 뒤집기(smart invert)" 모드
    • "Siri에게 타이핑하기" 옵션(목소리 인터페이스를 비활성)
    • 자동으로 호출 답변을 선택하기
    • 이미지의 보이스오버 설명(voiceover descriptions)은 이제 이미지 내용의 설명이나 인식된 텍스트를 자동으로 포함할 것임
    • 추가로 큰 텍스트 모드에서, 탭바의 탭을 길게 누르면 탭 이미지와 레이블이 커져서 팝업되어 표시됨
  • 운전중에는 방해하지 마라(Do Not Disturb while Driving) – 운전을 자동으로 감지하여 알림을 끔
  • 긴급 SOS 기능(전원 버튼을 5번 누르고 슬라이더를 슬라이드 하기)
  • 기존의 기기에서 새 아이폰을 자동으로 설정
  • AirPods을 위한 커스텀 액션 구성
  • 가까이에 다른 기기로부터 WiFi 비밀번호 요청하기
  • QR 코드를 스캔하여 WiFi 로그인하기
  • 내장된 문서 스캐너
  • 화면 녹화(컨트롤 센터로부터 접근)
  • 프린트를 지원하는 앱에서 PDF 생성하기
  • PDF, 스크린샷, 스캔한 문서에 표시하기(그리기)
  • 스크린샷을 찍고나서 구석에 썸네일이 나타나고, 그것을 탭하여 편집, 주석, 공유할 수 있음
  • 새로운 비디오 플레이 컨트롤
  • 셀 시그널에대해 점 대신 바
  • 한손 키보드 옵션 (더 좁아진 키들과 한쪽에 비워진 공간을 남김)
  • 적게 사용된 앱을 자동으로 지우고 필요하면 다시 복구하는 옵션
  • health 데이터는 iCloud에 동기화될 수 있음
  • FLAC 오디오 지원
  • 앱스토어, iTunse Store, 캘린더의 새로운 아이콘
  • 앱스토어는 iPad와 Mac 앱을 위해 정보 페이지를 보여줌(아마 버그같다?)
  • 앱스토어는 유료 앱을 사려할때 애플패이 스타일의 창을 보여줌
  • 홈 스크린에서 앱을 실행할때 다른 애니메이션
  • 기기를 잠금해제할때 다른 애니메이션
  • 알림 텍스트에서 하이라이트된 링크? (source)
  • 3D 터치 없는 기기에서 쉽게 알림 열기? (source)
  • iPhone5 & 5c, iPad4를 위한 드롭 지원
  • 옛날 32비트 앱을 위한 드롭 지원

iPad
  • 리디자인된 앱 독
    • 더 Mac스러운 디자인
    • 앱 안에 있을때 밀어올릴 수 있음
    • 앱은 독에서부터 슬라이드오버(slide-over)까지 드래그될 수 있음
    • 한 앱에서 최근에 사용한 파일에 접근
    • 오른쪽에 핸드오프/제안된 앱을 보여줌(비활성가능)
  • 슬라이드오버를 위한 새로운 디자인
  • 더 새로운 iPad는 분할 뷰에서 2개의 앱을 보여줄 수 있고, 세번째는 슬라이드 오버에서 보여줄 수 있음(그럼 네번째는 PiP에서?)
  • PiP 창은 유연하게 크기조절가능함 (source)
  • 앱 전환자를 위한 새로운 디자인
    • 분할 뷰 앱 페어링을 보존하고 미션 컨트롤 종류처럼 보임
    • 컨트롤 센터와 합쳐짐
  • 키보드자판 위에서 쓸어올려 구두점/숫자에 접근하기
  • 키보드 단축키(Cmd+Shift+3/Cmd+Shift+4)로 스크린샷 찍기
  • 화면에서 팬슬을 팹하여 새로운 노트와함게 노트앱 열기, 혹은 어느 기간안에 이전에 열린 노트 열기
  • 손글씨 인식 & 식별 (확실하진 않지만 iPad만?)
  • 컨트롤 센터에서 손전등 (source)

watchOS4
  • Siri 와치 페이스(Siri watch face)
  • Kaleidoscope 와치 페이스
  • 토이스토리 와치 페이스
  • 뉴스 앱
  • 독 증진
    • 새로운 수직의 디자인
    • 좋아요 모드(이전처럼)이나 최근 모드(iOS처럼) 사이에서 선택할 수 있음
    • iPhone에서 음악을 켜면 자동으로 가장 앞단의 앱으로서 '지금 재생중'을 보여줌
  • 홈 화면을 hex 그리드 대신 가나다 순으로 정렬된 앱 리스트로 보이게 할 수 있음
  • 이제 편집 모드 동안에 crown을 이용하여 구성된 와치 페이스을 스크롤 할 수 있음
  • 컨트롤 센터에서 손전등/안전 라이트
  • iPhone과 페어링하기위한 새로운 UI
  • 바뀐 비밀번호 화면 (굵어진 숫자, 애니메이션, 클릭)
  • 설정에서 위치 서비스 활성화
  • 몇몇 앱에서 다른 누군가에게 시계를 보여줄때 화면은 위아래로 자동으로 회전할 것임
  • 액티비티: 도전, 목표, 축하 등으로 개별화된 알림
  • 심박수: 이력 차트
  • 메일: 제스처(?)
  • 지도: 제안, 최근
  • 음악
    • 리디자인된 UI
    • 당신이 좋아하는 음악을 자동으로 동기화
    • 음악의 위치를 보여줌(iPhone/watch)
    • 시계에 특정 음악을 업로드 할 수 있음
  • 폰: 손수 숫자를 입력하기위한 키패드 (전화받는 도중에도 물론)
  • 타이머: 타이머가 끝나면 다시하기 버튼
  • 날씨: 공기 상태
  • 워크아웃(workout)
    • 새로운 UI – 워크아웃 시작하기 더 쉬워짐
    • 각 타입의 워크아웃은 마지막 목표 설정을 기억함
    • 한 세션에서 여러 워크아웃을 쉽게 할 수 있음
    • 앱으로부터 음악을 컨트롤 할 수 있는 기능
    • 자동으로 방해하지 마시오로 전환
    • 자동으로 재생목록을 재생하는 옵션
    • 개선된 풀장 수영(Pool Swim) 워크아웃
    • 높은 밀도의 인터벌 훈련(High Intensity Interval Training)
  • NFC를 통해 몇몇 운동 장비와 연동
  • 여러 센서나 스포츠 기기와 소통하기위한 코어 블루투스(예를들어 포도당 센서)
  • 개인 대 개인 지불을 위한 애플패이
  • 더 능력있는 카메라 컨트롤 (?)

tvOS4
  • 홈 화면 동기화 – 여러 애플 TV에 설치된 앱과 홈 화면 레이아웃을 동기화하기
  • 자동 어둡게 모드 – 시간에따라 자동으로 모드를 변경
  • AirPlay2 – 여러 방에 오디오 지원
  • AirPods은 이제 자동으로 여러분의 애플 TV와 페어된다.
  • TV 앱: Amazon Prime은 올해 후반기쯤에 추가될 것임

Foundation & Core frameworks
  • Core Data
    • 코어 스포트라이트와 더 통합됨 (NSCoreDataSpotlightDelegate)
    • 새로운 인덱싱 API(NSFetchIndexDescription, NSFetchIndexElementDescription, R-tree 인덱스)
    • com.apple.CoreData.SQLDebug 환경 변수는 4로 설정할때 SQL 쿼리를 디버깅한다고 할 수 있음
    • NSURLNSUUID 속성 타입
    • 다른 프로세스(예를들면 앱 익스텐션)에서의 변화를 추적하기위한 영속 이력 API(NSPersistentHistoryTrackingKey)
  • Core Location
    • CLGeocoder: CNPostalAddress와 통합됨
    • 쇼핑몰이나 공항에서 정밀한 인도어 위치가 가능함
    • 더 정확한 방향의 계산
  • NSArchiverNSUnarchiver는 디프리케이트됨
  • NSDistributedNotificationCenter: 옵저버들이 구독 취소를 할 필요가 없어짐
  • NSError: 새로운 NSLocalizedFailureErrorKey 정보 키
  • NSJSONSerialization: JSON 더미 속에서 알파뱃 순으로 정렬된 키들을 출력할 수 있음(NSJSONWritingSortedKeys)
  • NSLinguisticTagger: 증진된 API로서, 유력한 언어를 인지함
  • NSURL: 얼마나 빈 공간을 사용가능하게 만들 수 있는지 확인함(volumeAvailableCapacityForImportant/OpportunisticUsage)
  • NSURLSession
    • 적용가능한 연결성 API(waitsForConnectivity)
    • 스케줄링 API(earliestBeginDate)
    • 진행 추적(ProgressReporting)은 UIProgressViewNSProgressIndicator에서 사용할 수 있음
    • 증진된 스케줄링을위해 크기 힌트를 요청하기: countOfBytesClientExpectsToSend/Receive
    • Brotli 컨텐트 인코딩을 지원
  • NSXPCConnection: NSProgress 지원
  • 기존에있던 임의의 몇 API는 이제 에러 처리에대해 바뀌기도하고 문자열 대신에 URL을 받음

macOS SDK
  • High Sierra는 32비트 앱을 지원하는 마지막 배포판임
    • 2018년 1월부터 MAS에 제출되는 새로운 앱은 64비트 지원을 필요로함
    • 2018년 6월부터 모든 앱(새것&업데이트)은 64비트만 될 것임
  • AirPlay2 – 여러 방에 오디오 지원
  • Core ML 프레임워크 – 기계 학습
  • 이제 iOS에서 가능하던 Core Spotlight를 Mac에서도 가능함
  • Metal 2
    • 더 나은 성능과 새로운 API
    • 디버깅과 프로파일링 도구
    • 기계학습을 지원
    • 외부 그래픽 카드 접근을 위한 API
    • VR 지원(스팀, 유니티, 언리얼 VR SDK가 Mac으로 옴)
  • 사진 프로젝트 확장(PHProjectExtensionController) – 커스텀 인쇄 프로젝트나 웹 겔러리를 만들기 등
  • Vision 프레임워크 – 이미지 분석과 인식
  • NSAccessibilityCustomRotor – 사용자를 위한 선택 리스트를 제공(예를들면 섹션이나 링크들)
  • NSApplication: 파일이나 URL을 열기위해 나온 새로운 application(_:open:) 콜백(이전의 NSAppleEventManager기반 API를 대체함)
  • NSButton: 내용물은 애니메이터 프록시를통해 애니메이션될 수 있음
  • NSColor
    • systemBlue와같이 새로운 시스템 칼라 셋
    • NSColor.red와같은 표준 칼라들은 이제 sRGB 프로파일을 사용함
    • 어셋 카탈로그로부터 칼라를 불러오기위한 NSColor(named: ...)
  • NSCollectionView: iOS에서처럼 프리패칭하기
  • NSDrawer: 디프리케이트됨 :(
  • NSFontAssetRequest 잃어버린 시스템 폰트를 비동기로 다운로드하기위함
  • NSLevelIndicator: 새로운 모습, 스타일, 구성 옵션들. 값은 애니메이션 될 수 있음
  • NSMenuItem: allowsKeyEquivalentWhenHidden
  • NSSegmentedControl: 정돈(alignment)과 배치(distribution) 속성
  • NSTableView: 디폴트로 스스로 크기조절하는 행 사용(오토레이아웃을 사용하여)
  • NSWindowTab: title, tooltip, accessory view같은 탭들을 더욱 커스터마이징할 수 있는 옵션들
  • Core Image: 찍힌 이미지의 depth map 접근하기
  • MapKit
    • 새로운 "muted" 맵 타입
    • MKAnnotationView.displayPriority는 어떤 지도 주석이 더 중요한지 정해줌
    • 주석 모으기(MKAnnotationView.clusteringIdentifier, .collisionMode, MKClusterAnnotation)
    • MKMapView.dequeueReusableAnnotationView(withIdentifier:for:): UITableView에서처럼 항상 주석을 반환하는 변형
  • WKWebView: 커스텀 URL 스킴, 쿠키 관리, 스넵샷, 내용 필터링을 위한 새로운 API
  • 스위프트 호환성을 위한 정재된 API
    • 스위프트의 전역에 print()와 충돌을 피하기 위해 print라 정의된 몇몇 메소드들 이름 변경(예를들어 printView, printWindow)
    • 몇몇 전역 상수는 강타입 클래스 속성으로 이동됨(예를들어 NSSharingService.composeEmail)
    • 몇 클래스에서 nullability 수정
    • NSApplication.shared같은 ObjC 클래스 속성들
    • contentViewfirstResponder같은 몇몇 속성들이 이전에는 assign(스위프트에서는 unowned(unsafe))이었는데 이제 weak로 됨
  • iCloud 문서 공유하기
    • NSDocument 앱을위해 무료로 됨
    • NSDocument.share, allowsDocumentSharing
  • 파일 메뉴에서 볼 수 있는 새로운 "Share" 서브메뉴(NSDocumentController.allowsAutomaticShareMenu, standardShareMenuItem)
  • 복구가능한 상태를 인코딩하는것을 비동기로 완료할 수 있음(encodeRestorableState(with:backgroundQueue:))
  • 터치바 API 개선
    • 새로운 칼라 피커 컨트롤
    • NSGroupTouchBarItem은 시스템 경고로서 같은 버튼을 사용하는 옵션
    • NSGroupTouchBarItem은 공간이 충분하지 않을때 압축 설정을 하는 옵션
    • RTL 언어 지원
    • playground에서 터치바 랜더링
  • HEVC와 HEIF 지원
  • 시스템 수준의 소셜 네트워크 로그인이 디프리케이트됨(Twitter Facebook등)

iOS SDK
  • 뷰컨트롤러는 이제 상단에 선택적으로 검색 필드와함께 표준의 큰 타이틀 헤딩을 표시할 수 있음
    • 헤딩을 다루기위한 prefersLargeTitles, largeTitleDisplayMode
    • 검색필트를 다루기위한 searchControllerhidesSearchBarWhenScrolling
  • 레이아웃 시스템 변화
    • UIView
      • 뷰는 이제 "safe area insets"을 가진다(safeAreaInsets, safeAreaLayoutGuide, insetsLayoutMarginsFromSafeArea)
      • RTL 언어를 위해 추가된 directionalLayoutMargins
    • UIViewController
      • 추가된 safeAreaLayoutGuideadditionalSafeAreaInsets
      • 추가된 systemMinimumLayoutMarginsviewRespectsSystemMinimumLayoutMargins
      • preferredScreenEdgesDeferringSystemGestures는 시스템 패널에서 사용자가 스와이프한 엣지가 어딘지 정의함
      • topLayoutGuide, bottomLayoutGuide, automaticallyAdjustsScrollViewInsets는 디프리케이트됨
    • UIScollView
      • 추가된 adjustedContentInsetcontentInsetAdjustmentBehavior
      • 추가된 frameLayoutGuidecontentLayoutGuide 레이아웃 가이드
    • UITableView
      • separatorInsetReference(.fromCellEdges.fromAutomaticInsets)
  • 다이나믹 타입 개선
    • UIFontMetrics는 커스텀 폰트의 크기를 조정할 수 있게 해줌(예를들어 다이나믹 타입 설정에따른 버튼 사이즈)
    • constraintEqualToSystemSpacing(below/after:)는 정확하게 스케일한 거리 제약을 정의함
    • spacingBaselineToBaseline은 시각적 포멧 언어를위한 옵션임
    • UIStackView를 위한 spacingUseSystem
  • 드래그 앤 드롭을 위한 새로운 API
  • AirPlay2 – 여러 방에 오디오 지원
  • AR이 가능한 앱을 쉽게 만들수 있는 ARKit(iPhone 6s나 더 최신/iPad Pro나 더 최신)
  • Core ML프레임워크 – 기계 학습
  • Core NFC – NFC 태그를 읽기위한
  • DeviceCheck 프레임워크 – 시스템 재설치후에 보존하는 기기를 위한 플래그 세팅
  • FileProvider 프레임워크 – 파일 프로바이더 익스텐션을 위한 새로운 API
  • IdentityLookup – 알수없는 사람이 보낸 SMS을 걸러내는 Message Filter 익스텐션에서 사용됨
  • IOSurface – macOS에서나온 프레임버퍼 프레임워크
  • Metal 2 – 더 나은 성능, 기계학습 지원, 디버깅 & 프로파일링 툴, 더욱 통합된 API
  • MusicKit – 사용자의 애플 뮤닉 라이브러리에 접근하기위함
  • macOS에서 온 NSXPConnection
  • 앱내에서 PDF를 보여주기위한 PDFKit
  • macOS에서 온 ProcessInfo.ThermalState API
  • Quick Look Preview 익스텐션(QLPreviewingController)
  • 썸네일 익스텐션(QLThumbnailProvider)
  • Vision 프레임워크 – 이미지 분석과 인식
  • UIBarItem.largeContentSizeImage – 탭 바 아이콘을 길게 누를때 접근성 HUD에서 보여지는 이미지
  • UIDocumentBrowserViewController – 앱 파일들을 브라우징하기위한 새로운 UI(로컬이나 클라우드에서)
  • UINavigationItem.titleViewUIBarButtonItem.customView는 이제 오토레이아웃을 사용하여 그들의 크기를 정할 수 있음
  • UIScreen.maximumFramesPerSecond는 새로운 iPad를 위한 ProMotion 지원
  • UIStackView.setCustomSpacing(after:)
  • UITabBar
    • 이제 탭바는 약간 더 볼드된 문자와 채워진 기호(glyph)를 사용함
    • 가로 모드에서 탭바는 좁아지고 레이블은 오른쪽에 나타남
    • 추가로 큰 텍스트 모드에서, 탭바의 탭을 길게 누르면 탭 이미지와 레이블이 커져서 팝업되어 표시됨(큰 버전의 아이콘이나 벡터 이미지를 추천함)
  • UITableView
    • 시작/끝 스와이프 액션이 구성가능함(UISwipeActionsConfiguration, UIContextualAction)
    • 디폴트로 셀과 헤더/풋터에대해 스스로 크기조절하는 오토레이아웃 사용
    • UICollectionView에서처럼 performBatchUpdates(_:completion:)
  • HealthKit
    • 체성분분석기(waist circumference)나 인슐린 레벨같은 새로운 헬스 데이터 타입(HKQuantityTypeIdentifier)
    • tai chi나 mixed cardio같은 새로운 워크아웃 타입(HKWorkoutActivityType)
    • 워크아웃 앱은 워크아웃 세그먼트를 표시할 수 있음(HKWorkoutEventType.segment)
    • 워크아웃 앱은 사용자의 워크아웃 루틴을 읽고 작성할 수 있음(HKWorkoutRoute)
  • MapKit
    • 새로운 "muted" 맵 타입
    • 맵 컨트롤은 이제 일반 뷰처럼 맵으로부터 분리되어 사용할 수 있고, RTL을 인지함
    • MKMarkerAnnotationView: "표시" 모양과함께 새로운 디폴트 주석 뷰
    • MKAnnotationView.displayPriority는 더 중요한 지도 주석을 명시함
    • 주석 모으기(MKAnnotationView.clusteringIdentifier, .collisionMode, MKClusterAnnotation)
    • MKMapView.dequeueReusableAnnotationView(withIdentifier:for:): UITableView에서처럼 항상 주석을 반환하는 것
  • SFSafariViewController
    • 몇몇 추가적인 구성 옵션들
    • 사파리와 다른 앱들은 이제 쿠키들과 웹사이트 데이터에대해 다른 컨테이너를 얻음
    • 사파리에서 웹사이트 데이터를 지우는것은 앱에서의 것도 지운다
  • SiriKit
    • 새로운 목록들과 노트 도메인(TODO 리스트, 노트, 할일 목록를 생성하고 관리하기)
    • 새로운 시각적 코드 도메인(지불을위해 QR 코드를 표시하고 연락처 정보를 교환하기)
    • Ride Booking과 지불을위한 추가사항
  • WKWebView: 커스텀 URL 스킴, 쿠키 관리, 스넵샷, 내용 필터링을위한 새로운 API
  • 앱을위한 iCloud 키체인 패스워드 자동완성
  • iCloud 문서 공유
  • 새로운 NFC 접근 허가(+ NFCReaderUsageDescription)
  • 위치 접근
    • 위치 접근이 어떻게 사용자에게 표시될지에대한 몇가지 변경(화살표 아이콘과 파란색 바)
    • 사용자는 이제 "사용하는 동안"을 항상 선택할 수 있고, 이것을 다룰 수 있어야 함
    • 앱들은 반드시 NSLocationWhenInUseUsageDescription을 포함하고 선택적으로 NSLocationAlwaysAndWhenInUseUsageDescription을 포함함
  • 사진 접근
    • 사진을 저장만 하기위해 write-only 지원을 물어볼 수 있음(NSPhotoLibraryAddUsageDescription)
    • 사용자에게 사진 라이브러리를 모두 접근하지 않고 한 사진만 선택하도록 요구하려면 UIImagePickerController를 사용할 수 있음
  • HEVC와 HEIF를 지원
  • 캡처된 이미지(CIImage에서 kCIImageAuxiliaryDepth)와 비디오(AVDepthData)의 depth map에 접근하기
  • 시스템 수준에서의 소셜 네트워크 로그인이 제거됨(Twitter, Facebook 등)
  • 32비트 앱 지원이 누락됨(iOS11 개발 타겟을 타겟으로하는 앱은 64비트만 지원가능)

watchOS SDK
  • Core Bluetooth
  • Core ML 프레임워크 – 기계학습
  • macOS로부터 온 ProcessInfo.ThermalState API
  • macOS로부터 온 NSXPCConnection
  • WKExtension
    • enableWaterLock – 앱 내 워터 락 버튼(water lock button)을 구현하기 위함(워크아웃 앱과 위치 추적 앱만을 위함)
    • isAutorotating – 자동 회전 기능은 사용자가 손목을 바깥쪽으로 틀면 나옴
    • isFrontmostTimeoutExtended – 어떤 앱이 화면상에서 8분까지 잡혀있으면 그 시간을 연장할지 시스템에게 요청하는 것(watchOS 3.2에서는 디폴트 타임아웃이 2분으로 바뀌었었음)
  • WKInterfaceController
    • 수직으로 스크롤되는 페이지 지원(WKPageOrientation.vertical)
    • 선택된 페이지에서 페이지된 인터페이스를 시작하기(pageIndex 파라미터)
    • 특정 포지션에 리스트를 스크롤하기(scroll(to:at:animated:))
    • 스크롤 이벤트에대한 콜백(interfaceOffsetDidScrollToTop/Bottom, interfaceDidScrollToTop)
  • WKInterfaceGroup: 이제 그룹은 오버랩' 레이아웃을 가질 수 있음
  • WKSnapshopRefreshBackgroundTask: reasonForSnapshop – 왜 스넵샷이 필요했는지 알려줌
  • HealthKit
    • 체성분분석기(waist circumference)나 인슐린 레벨같은 새로운 헬스 데이터 타입(HKQuantityTypeIdentifier)
    • tai chi나 mixed cardio같은 새로운 워크아웃 타입(HKWorkoutActivityType)
    • 워크아웃 앱은 워크아웃 세그먼트를 표시할 수 있음(HKWorkoutEventType.segment)
    • 워크아웃 앱은 사용자의 워크아웃 루틴을 읽고 작성할 수 있음(HKWorkoutRoute)
    • 워크아웃 앱은 crown + 사이드 버튼을 함께 눌러서 "하드웨어 일시정지" 제스처에 응답할 수 있음(HKWorkoutEventType.pauseOrResumeRequest)
  • SiriKit
    • 단일-프로세스 런타임 – UI와 앱 코드는 이제 단일 프로세스에서 돌아갈 것이고 성능을 증진함
    • 위치를 사용하는 앱은 이제 와치에서 직접 위치 접근을 물어볼 수 있음
    • SpriteKit과 SceneKit 뷰들은 전체 화면 모드에서 보여질 수 있음(구석에 시간이 계속 나타나겠지만)
    • 한줄의 오디오 녹음 – 커스텀 컨트롤을 사용하며, 시스템 다이얼로그는 아님
    • 백그라운드 오디오 녹음 모드(마이크 지시자가 보임)
    • 워크아웃 앱이 아닌 것들을 위한 백그라운드 위치 추적 모드(위치 아이콘이 보임)

tvOS SDK
  • AirPlay2 – 여러 방에 오디오 지원
  • Core ML 프레임워크 – 기계 학습
  • iOSSurface – macOS로부터 온 프레임버퍼 프레임워크
  • Metal 2 – 더 나은 성능, 기계학습 지원, 디버깅 & 프로파일링 툴, 더욱 통합된 API
  • macOS에서 온 NSXPConnection
  • macOS에서 온 ProcessInfo.ThermalState API
  • Vision 프레임워크 – 이미지 분석과 인식
  • Core Image: 찍힌 이미지의 depth map접근하기
  • MapKit
    • 새로운 "muted" 맵 타입
    • 맵 컨트롤은 이제 일반 뷰처럼 맵으로부터 분리되어 사용할 수 있음
    • MKMarkerAnnotationView: "표시" 모양과함께 새로운 디폴트 주석 뷰
    • MKAnnotationView.displayPriority는 더 중요한 지도 주석을 명시함
    • 주석 모으기(MKAnnotationView.clusteringIdentifier, .collisionMode, MKClusterAnnotation)
    • MKMapView.dequeueReusableAnnotationView(withIdentifier:for:): UITableView에서처럼 항상 주석을 반환하는 것
  • 더 나은 폰트 크기조절을 위한 UIFontMetrics와 오토레이아웃 추가사항 (iOS를 보라)
  • TVML 템플릿
  • 웹 인스팩터에서 TVML 지원
  • RTL 언어 지원
  • 앱은 조용한 알림을 사용해 백그라운드에서 깨어날 수 있음
  • 리소스 번들 사이즈 제한이 4GB로 늘어남
  • HEVC와 HEIF 지원

개발자 툴
Xcode9
  • 완벽하게 다시작성된 소스 에디터
    • 스위프트로 작성됨
    • 더 빨라짐
    • 더 많은 폰트 구성 옵션들
    • 3가지 커서중 하나 선택하기(수직, 수평, 블락)
    • Cmd-+ 텍스트 크기
    • 코드를 침범하지 않은채 이슈 풍선이 나타나며, 원클릭으로 여러 이슈를 고칠 수 있음
  • 완전히 새로운 리팩토링 시스템
    • C, ObjC, C++, 스위프트를 지원
    • 코드위에 올리면 부분을 하이라이트하고, 클릭하여 다양한 추출(extractions)처럼 변형/리팩토링을 적용할 수 있음
    • 모든 파일에서 이름 재정의
    • 키보드 단축키를 정할 수 있음
    • 엔진은 오픈소스화 될 것이며, Xcode는 자동으로 여러분에의해 만들어진 변형을 불러올 것임
  • 더 빨라진 인텍서(indexer)
    • 최대 35배까지 "빠르게 열림"
    • 최대 50배까지 빠르게 검색됨
  • 새로운 빌드 시스템(옵트-인 미리보기)
    • 스위프트로 쓰여짐
    • 2.5배까지 빨라진 빌드
    • ObjC/스위프트가 섞인 프로젝트의 빌드가 빨라짐
    • 코드 커버리지 테스팅이 빨라짐
    • 빌드동안 인덱싱을 계속 할 수 있음
  • 스위프트 3.2와 4.0을 포함함
    • 스위프트 버전은 타겟별로 선택할 수 있으며, 한 앱은 두 버전으로 빌드된 모듈을 포함할 수 있음
  • 마크다운 에디터가 통합됨
  • Github이 통합됨
    • 저장소 검색 & 워크플로우 클론
    • 새 저장소를 생성하기위해 프로젝트를 푸시하기
    • github.com에서 "open in Xcode"
    • 새로운 소스 컨트롤 네비게이터
  • 설정에서 계정 패널은 더이상 알고있는 git 저장소 목록을 보여주지 않음
  • 시뮬레이터 증진
    • 동시에 여러 시뮬레이터가 돌아감
    • 이제 시뮬레이터는 디바이스 형태를 다시 가지게됨(Xcode 6에서 없어졌음?)
    • 전원버튼같은 하드웨어 버튼을 누를 수 있음
    • 모서리 스와이프를 지원
    • 유연하게 스케일가능함(해상도가 아니라 크기를 변경)
    • 시뮬레이터의 비디오를 녹화하기
    • 시뮬레이터에 어떤 것을 보내기위해 시스템-와이드 공유 익스텐션(예를들어 사진/비디오, 지도 위치, URL, 앱 등)
  • 무선 디버깅 – WiFi나 이더넷을 통해 원격으로 기기에 앱을 실행하기
    • Quicktime, Safari 인스팩터, 콘솔 등에서도 지원함
  • Xcode는 이제 파일을 이동할때 폴더-그룹 동기화를 유지할 것임
  • 런타임 세니타이저(runtime sanitizers)
    • Main Thread Checker는 백그라운드 스레드에서 UI에 접근하는지 감지함
    • Undefined Behavior Sanitizer는 C/ObjC에서 정의되지 않은 행동 버그를 감지함
    • Address와 Thread Sanitizer의 증진
  • 뷰 디버거(view debugger) 증진
    • 계층의 부분으로 뷰컨트롤러를 보여줌
    • SceneKit과 SpriteKit 화면을 이해함
  • Crashes Organizer는 Spotlight에의해 인덱스된 로컬 .dSYMs 사용을 선택할때 심볼화되지 않은 로그를 심볼화할 것임
  • 문서 브라우저에서 검색이 증진됨
  • Xcode server(CI)는 이제 Xcode에 내장됨
  • Xcode의 여러 복사본은 동시에 돌아감(그러나 9개 이하)
  • 테스트 개선점
    • UI 테스트는 이제 여러 앱으로 뻗어나갈 수 있음
    • 3배에서 10개까지 빠른 UI 테스트
    • 새로운 UI 테스팅 API(예를들어 XCUIScreen.screenshot())
    • 테스트 빌드는 동시에 여러 기기에서 돌아감(xcodebuild를 사용하여)
  • 배포를 위한 서명: 자동 서명하기와 더 커스터마이징가능한 서명하기
  • 어셋 카탈로그(asset catalogs)
    • 어셋 카탈로그에 칼라를 추가할 수 있음
    • 앱 아이콘을 위한 앱 얇게하기(app thining)
    • 어셋 카탈로그는 "preserve vector data" 옵션으로 PDF 포멧의 이미지를 담을 수 있음. 이것은 앱에 벡터 양식을 올릴 수 있으며, 다양한 문맥에서 아이콘을 크게 키울때 사용될 수 있음
    • HEVC와 HEIF 어셋을 지원
  • 스위프트에서 프로토콜에의해 요구되는 모든 메소드 추가를 수정
  • 300개 이상의 새로운 진단, 분석, 수정이 있음
  • 새로운 "Strip Swift Symbols"는 바이너리에서 스위프트 심볼을 strip하는 설정을 만들어, 그 크기를 줄임(디폴트로 활성화됨)됨
  • IB에서 constraint가 선택되면 그 엘리먼트의 위치를 변경할 수 있고, 그 상수는 업데이트 될 것임
  • 개선된 템플릿 메크로 시스템(?)

Swift 4
  • 줄어든 바이너리 사이즈
  • 더 나아지고 예측가증한 성능
  • 마이그레이션이 필요하지만, 아주 조금
  • String 개선
    • 사용하기 더 쉬워짐(예를들어 다시 컬랙션이 됨 .characters를 호출할 필요가 없음, 쉬워진 슬라이싱)
    • 더 나은 성능
    • 더 올바른 유니코드 지원(예를들어 이모티콘 다룰때)
    • 다열- 문자열 리터럴("""...""")
    • CharacterunicodeScalars를 가짐
    • 문자열 슬라이스는 이제 새로운 타입인 Substring을 가짐(StringProtocol을 통해 String과함께 더욱 기능을 공유함)
  • DictionarySet 개선점
    • 키-값 쌍의 시퀀스로부터 딕셔너리를 생성하기
    • 깃셔너리와 셋을 같은 타입의 값으로 필터링하기
    • 딕셔너리 값들을 새로운 딕셔너리로 맵핑하기
    • 그룹화된 시퀀스를 딕셔너리로
    • 디폴트 값과 함께 Dictionary 서브스크립팅 하기
  • Codable – 인코딩/디코딩을 위한 새로운 API(예를들면 JSON에서)
  • \Person.name – 운 강타입 키패스
  • NSRange에서/으로부터 Swift.Range 변환
  • observe(keyPath) { observed, change in ... } – 블럭기반 KVO
  • private 속성/메소드는 같은 파일내의 익스텐션에서 접근가능
  • associatedtype을위한 where
  • @objc는 이제 대붑ㄴ의 경우에 명시적으로 추가될 필요가 있음
  • MutableCollection.swapAt(x, y)
  • 일부분 범위(5..., ...5)
  • 제네릭 서브스크립트(파라미터나 리턴값도 제네릭이 될 수 있음)
  • 클래스와 프로토콜 요구사항 합치기: var x: UIView & Animatable
  • 독점적 메모리 접근(exclusive access to memory) – 다른 곳에서 수정되는 값에 올바르게 접근하는지 체크하는 컴파일러 체크와 런타임 체크
  • Swift Package Manager의 다양한 개선사항들

ObjC
  • 스위프트에서처럼 배포타겟보다 최신의 API 사용을 감지함(@available)

App Store & iTunes Connect
  • 앱스토어를 우한 앱 아이콘은 이제 바이너리에서 어셋 캍탈로그안에서 배포될 것임
  • 앱 이름은 30자까지 제한됨
  • 앱 이름 아래 새로운 부제목 필드가 생김(또한 30자)
  • 설명은 이제 새 버전을 제출할때만 편집할 수 있게될것임
  • 새로운 프로모셔널 텍스트 필드(170자) – 설명의 시작부분에 보여지며, 언제든지 편집할 수 있음
  • 앱의 프리뷰 비디오를 3개까지 올릴 수 있고, 자동으로 재생될 것임(이때는 소리없이)
  • 단계별 배포(phased releases) – 자동 다운로드으로 설정한 사용자들은 한주마다 점차 앱을 받음
  • 인앱구매는 앱과는별개로 앱스토어에서 판촉될 수 있음
  • 평점은 더이상 모든 업데이트마다 자동으로 리셋되지 않음(여러분이 직접 리셋할 수 있음)
  • (아직 엄격하게 제약하진 않았지만)리뷰를 요구할때 SKStoreReviewController만 사용할것을 강력하게 추천함 (1.1.7)
  • 실행가능한 코드를 다운받는 프로그래밍 IDE는 이제 몇 조건과 함께 앱스토어에서 가능함 (2.5.2)
  • TestFlight 외부 테스터 제한이 1만명으로 올라갈 것임

Safari
  • CSS stroke 지원
  • variable fonts 지원
  • 드래그 앤 드롭 이벤트
  • WebCrypto API 업데이트
  • 컨텐트 블락커(content blockers)에서 if-top-urlunless--top-url 규칙
  • Brotli 컨텐트 인코딩을 지원

다른것들
  • Swift Playgrounds 2.0
    • 구독 모델로 플레이그라운드 컨텐트의 제공 호스팅
    • 스위프트4와 iOS11 SDK, 카메라, AR API 지원
    • 통합된 문서
    • 8가지 새로운 지역화
  • Business Chat (preview) – iMessage로 여러분의 고객이 여러분에게 연락할 수 있도록 해주는 회사를 위한 서비스
  • HomeKit 프로토콜은 이제 모두에게 무료로 공개됨(비영리 목적)
  • iCloud 저장소 플랜은 가족 모두와 공유될 수 있음


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: WWDC 2017 Viewing Guide

다른 년도, 다른 WWDC 그리고 더욱 많은 세션들을 이해하기위해서 준비했다. 이 글에서는 이번년도 가장 흥미있는 세션을 위한 나의 가이드를 공유한다.

기계학습, 드레그 앤 드롭, Xcode9
제일 먼저 Platform state of the union이 좋다.
  • Platforms State of the Union 아직 이 세션을 보지 않았다면, 여기에는 iOS, macOS, tvOS, watchOS의 전반적인 변화들을 발표한다.

이것들은 넘겨보지 말자
개인적으로 내가 가장 좋아하는 것들인데, 여러분은 그냥 넘겨볼 수도 있겠지만 시간을 투자해보는 것도 의미있는 일이 될 것이다.

앱 프레임워크
수많은 좋은 세션들이 여기에 있다. 나는 watchOS, tvOS, macOS 세션까지 넣지는 않았다. 여기에 내가 뽑은 리스트이다.
  • Session 201 What’s New in Cocoa Touch WWDC의 스타같은 팀, Eliza Block와 Josh Shaffer이 돌아와 Cocoa Touch의 바뀐점을 요약해준다.
  • Session 204 Updating Your App for iOS 11 통합된 서치바, 리프레시 컨트롤, 오토레이아웃 지원과함께 새로나온 큰 타이틀 네비게이션 바(large title Navigation bar)를 다룬다. 새로운 레이아웃 마진(layout margin)은 뷰컨트롤러의 루트뷰 세팅에대한 가이드를 하고(이제 시스템 최솟값을 바꿀 수 있다) 세이프 영역(safe area)는 상단, 하단 레이아웃 가이드를 대신한다. 또한 뷰컨트롤러는 더이상 스크롤뷰의 contentInset으로 더러워지지 않는다. 테이블뷰 셀은 이제 디폴트로 스스로 크기변경이 가능하고 더 쉽게 스와이프 액션을 넣을 수 있다.
  • Session 206 Introducing Password AutoFill for Apps 여러분의 웹 서비스에 로그인 기능이 있다면 쉽게 구현 가능해 보인다. 여러분의 사용자는 iOS 키체인에 증명서를 저장하게 된다.
  • Session 210 What’s New in Core Data 이번년도의 커다란 코어데이터 뉴스는 Core Spotlight 통합과 Persistent History Tracking을 지원한다는 점이다. 후자는 영속 저장소를 갱신하고있는 여러 컨텍스트와 여러 익스텐션의 이슈를 대상으로 한다. 포그라운드로 돌아오는 앱은 일부로 다시 불러오게 하지 않고서 변경사항들을 패치할 수 있다.
  • Session 214 What’s New in SiriKit 아마 예상한것처럼 극적인 변화는 아닐 것이나, 리스트와 노트를 위한 새로운 계획을 받았고 비주얼 (QR) 코드를 보여주는 것도 곧 나올 것이다.
  • Session 219 Modern User Interaction on iOS 애플에서는 이것을 지향하지는 않을 수 있지만, 여러분은 이제 컨트롤센터, 노티피케이션, 액션을 넘은 슬라이드가 발생하기 전에 스크린의 모서리에서 일어나는 시스템 스와이프를 가로챌 수 있다.
  • Session 225 What’s New in Safari View Controller 여러분의 앱에 더 매칭시키기위해 바(bar)와 틴트 칼라(tint color)를 커스터마이징 한다. 드레그 앤 드롭에대한 지원을 탑재한다. 이제 각 앱은 사파리 브라우저와 별개로된 고유의 쿠키 저장소(jar)를 얻었다. 취소나 닫기를 위해 완료(done) 버튼을 변경하고, 공유 시트로부터 활성화를 배제한다.
  • Session 230 Advanced Animations with UIKit 프로퍼티 애니메이터를 사용하는 것에대한 좋은 요약이고, CALayer의 cornerRadius 속성은 이제 애니메이션 가능한 점에대한 좋은 팁이다.
  • Session 235 Building Visually Rich User Experiences 세션 230를 이은 코어 애니메이션의 좋은 팁과 트릭이다.
  • Session 237 What’s New in MapKit 지도에서 여러분의 데이터를 표현할 수 있는 새로운 맵 타입. 새로운 MKMarkerAnnotationView는 주석들이 겹칠때 이것을 묶어준다.
  • Session 241 Introducing PDFKit on the iOS CoreGraphics 위에 만들었지만 현대의 스위프트와 Objective-C API를 지원한다. PDF 문서를 더 쉽게 보고 주석을 달기위해 만들어졌다.
  • Session 242 The Keys to a Better Text Input Experience 높이를 변경하거나 커스텀 인풋 뷰를 넣은 키보드를 다루는 여러 방법을 소개한다. 사용자가 선택한 키보드가 어떤것인지 앱이 기억하게 만들 수 있다. iOS는 똑똑하게도 자동으로 따옴표와 대쉬를 바꿔준다.
  • 개발바들을위해 애플은 동적 타입을 지원하여 계속해서 밀고있다. iOS11에서 큰 변화로는, 모든 스타일이 이제 여분의 큰 접근성 크기로 커진다(예전에는 바디 스타일만 가능했다). 또한 애플은 UIFontMetrics를 사용하여 커스텀 폰트도 쉽게 지원할 수 있게 만들었다. 어셋(asset) 카탈로그에서 PDF vector asset은 이제 벡터 데이터를 저장할 수 있게 설정할 수 있다.

드래그 앤 드롭
이번년도의 커다란 UIKit 증진사항은 앱간의 드래그 앤 드롭을 지원한다는 점이다.

여러분이 시간이 있다면 아래 세션은 더 깊은 내용을 담고 있다.
자연어 처리(NLP, Natural Language Processing), 기계 학습(ML, Machine Learning), 증강 현슬(AR, Augmented Reality)
애플이 밀고 있는 이번년도의 또다른 큰 테마는, 우리 앱에서 NLP, ML, AR을 쉽게 사용할 수 있게 만들고 있다는 점이다. 또한 애플은 개인정보, 속도, 낮은 지연율에 대한 이점을 강조했는데, 클라우드 기반 서비스 없이 기기에서 가능하기 때문이다.

스위프트
스위프트 에볼루션 과정이 공개로 됨으로서 새로운 스위프트4를 보기위해 WWDC까지 기다릴 필요가 없어졌다. 그럼에도 잘 정리해 놓았다.

개발 
Xcode9는 스위프트를 크게 돕기위해 리팩토링 지원에대한 업데이트를 보고 있는것 같다.

디자인과 접근성
애플이 계속해서 개선하고 강조하는 부분으로, 모두를 위한 앱 디자인과 제작에서 필요한 점을 볼 수 있다.
  • Session 110 Convenience for You is Independence for me Todd Stabelfeldt (aka the Quadfather)는 8살때부터 사지마비가 왔는데, 접근성 제작으로 다른 앱 디자인을 만들어 그의 발표에서 영감을 얻자. 만약 여러분 앱의 접근가능함에 의심이 된다면 그가 손으로 사용하는 스위치 컨트롤 시연을 보자 (가끔 혀를 쓰기도 한다).
  • Session 215 What’s New in Accessibility iOS11 사용자를 위해 나온 일반적인 접근성 기능 몇개가 크게 개선되었다. 여러분은 이제 텍스트로 시리와 인터렉트할 수 있다. 모든 시스템은 동적 타입을 위해 감사(audit)할수 있게 되는데, 이제 모든 스타일에 최고로 큰 접근성 크기를 사용할 수 있다(여러분의 UI에서 테스트해보라!). 이제 VoiceOver은 이미지 안에 있는 텍스트를 인지해서 말할 수 있고, 컨테이너 타입(container type) 지원이 개선되었다. 마지막으로 당신이 드래그 앤 드롭 지원을 고려하고 있다면 접근성을 고려하는 것도 잊지 말자.
  • Session 803 Designing Sound Highly entertaining talk on using sound including how some of the familiar Apple system sounds are created.

보안과 네트워킹

앱 스토어
여러분이 앱내구매(IAP, In App Purchase)를 사용한다면 앱스토어에서 바뀌게될 부분을 주목하자. 아래 두 세션을 추천하고 싶다.
  • 몇몇 멋진 개선이다: 올해 후반기에 TestFlight 제한이 10,000명 사용자로 올라갈 것이다. 더이상 iTunes Connect에서 앱 아이콘을 관리할 필요가 없다. 앱 부제목과 프로모셔널 텍스트 필드가 있다. 20 API까지 올렸다. 단계별 배포(phased release).

미디어
요점은 MusicKit이 Apple Music API에 접근할 수 있게 된 것과 Photos API가 바뀐 부분이다. 이미지 피커(image picker)는 과정에서 사라졌으며, 따라서 사용자는 허가를 묻지 않고 이미지를 선택할 수 있다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Vapor and its callback and throwing stacks

이 글은 스위프트로 쓰여진 서버 프레임워크, Vapor에 관한 이야기이다. 나는 iOS 개발자이며, 스위프트를 사랑하기때문에 내 개인 블로그를 이것으로 바꾸려고 하였다. 그렇게 하면서 내가 발견한 몇몇가지 특징들(미들웨어, 콜백체인, 에러핸들링)을 소개해주고 싶었다. 우리는 계층을 만들고 기능을 확장하고 프로젝트를 더 구조화하기위해 프로토콜과 익스텐션 사용을 할 것이다.

다소 간단한 서버로 시작해보자.
let drop = Droplet()  // 1



try? addProvider(VaporPostgreSQL.Provider.self) // 2

drop.preparations += Model.self // 3


drop.middleware.insert(RedirectMiddleWare(), at: 0) // 4

drop.middleware.append(GzipMiddleWare()) // 5

drop.middleware.append(HeadersMiddleware())

drop.get("/test", handler: {request in

    print("4")
    return try JSON(node: "This is a test page.")
})
Droplet 설정

droplet을 만듦으로서 시작하고(1), 데이터베이스 프로바이더를 추가하며(2) 함께 사용될 Model 엔티티를 추가한다. 그런다음 우리의 미들웨어를 추가한다. 미들웨어의 순서는 보통 상관으므로 append하면 된다. 그러나 가끔 구체적인 요구나 미들웨어중 하나를 가장 먼저 실행해야하는 경우가 있을 수 있다. 이 경우는 insert하여 스택의 제일 첫번째에 넣을 수 있다(4). 그리하여 내부에 다른 것들 이전에 첫번째것이 호출된다.

이제 미들웨어로가서, 이것이 어떻게 동작하는지 그리고 왜 이렇게 추가했는지 보자. Vapor는 Middleware라는 프로토콜을 제공하는데, 이것은 그냥 하나의 메소드를 필요로 한다.
func respond(to request: Request, chainingTo next: Responder) throws -> Response
미들웨어의 유일한 메소드


이 메소드를 통해 미들웨어는 요청을 받고, 적절하게 수정하여(혹은 그대로두어), 그것을 다음 응답자(next responder)에 보낸다. 이 응답자는 또다른 미들웨어일 수도 있고 엔드포인트의 핸들러일 수도 있다. 이 응답자는 응답을 반환하고, 적절하게 수정하여(혹은 그대로두어), 직접 응답을 생성하고 반환한다. 아래 예제에서 그것에대한 각각의 상황을 확인할 수 있다.

먼저 스택에서 RedirectMiddleware이다.
struct RedirectMiddleware: Middleware {
    func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        print("1")
        guard request.uri.scheme != "https" else { // 1

            let response = try next.respond(to:request) // 2

            print("7")
            return response // 3

        }

        let uri = uriWithSecureScheme

        print("alternate")
        return Response(redirect:uri.description) // 4

    }
}
RedirectMiddleware

이 미들웨어의 유일한 목표는 현재 요청이 안전한지 확인하는 것이다(1). 안전하지 않으면 요청 URIhttps 스킴을 넣어 새로운 응답을 만들어 반환한다. 만약 안전하면 다음 응답자에게 보내고(2), 끝난다. 우리에게 반환된것이 어떤것이든 반환한다(3). 모든 요청이 안전하고싶기 때문에 모든 다른 미들웨어를 통해 요청이 통과될 필요가 없다. 그러므로 0번째 인덱스에 위치시킨다.

다음 미들웨어는 HeaderMiddleware이다.
struct HeadersMiddleware: Middleware {

    funcrespond(torequest:Request,chainingTonext:Responder)throws->Response{
        print("2")
        let response=try next.respond(to:request) // 1

        print("6")

        response.headers["Cache-Control"] = "public, max-age=86400" // 2


        // Disable the embedding of the site in an external one.

        response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
        response.headers["X-Frame-Options"]  ="DENY"

        return response// 3

    }

}
HeadersMiddleware

이것은 요청을 바로 다음 응답자에게 보내고(1), 응답이 리턴된 후에 여기에 몇 헤더를 설정하고(2), 반환한다(3).

이 스택의 다음으로 다음 응답자인 GzipMiddleware이다.
struct GzipMiddleware: Middleware {
    func respond(torequest: Request, chainingTo next: Responder) throws->Response {
        print("3")
        let response = try next.respond(to: request) // 1

        print("5")

        response.body = gzippedBody // 2

        response.headers["Content-Encoding"] = "gzip"// 3

        response.headers["Content-Length"] = gzippedBody.length

        returnresponse // 4

    }

}
GZipMiddleware

여기서도 바로 다음 리스폰더에게 응답을 보내고(1), 몇몇 헤더를 추가한다(3). 그러나 다른점은 이것을 리턴하기 전에(4) 바디를 바꾼다.

내부적으로 미들웨어는 어떻게 동작할까? 기본적으로 우리가 이것을 사용한 것처럼 된다. DropletResoonder 프로토콜을 따르는데, 이것은 그냥 한 메소드를 가지고 있다.
func respond(to request: Request) throws -> Response
Droplet의 응답 메소드

Middleware 프로토콜과 비교해보면, 이것은 아무거나 연결(chain)하여 호출될 수 없고 그 구현에서 모든 미들웨어를 연결한다.
extension Droplet: Responder {

    public func respond(to request: Request) throws -> Response {

        [...]

        print("0")
        let mainResponder = middleware.chain(to: routerResponder) // 1

        var response: Response


        do {
            response = try mainResponder.respond(to: request) // 2

        }
        catch {
            return Response(status: .internalServerError, headers: [:], body: "Error message".bytes)// 3

        }

        print("10")
        returnresponse// 4

    }

}
Droplet의 respond 메소드 구현

모든 미들웨어를 연결하여 만들어진 응답자(1)를 볼 수 있다(아직 호출되진 않았다). 그리고 요청의 바깥에 응답을 만드는데 사용되며(2), 모든게 괜찮으면 리턴될 것이다(4). 만약 실패하면 새로운 응답이 만들어지고 반환된다(3). 여기에는 에러메시지와 상태코드가 담겨있다. 미들웨어는 어떻게 연결되고(1) 호출될까? Collection을 익스텐션하고 Responder를 상속하여 이런 한 목표는 클로저를 잡아두고 호출하는 것이다.
extension Request {
    public struct Handler: Responder {
        public typealias Closure = (Request) throws -> Response


        private let closure: Closure


        public init(_c losure: @escaping Closure) {
            self.closure = closure // 1

        }

        public func respond(to request: Request) throws -> Response {
            return try closure(request) // 2

        }
    }
}


extension Collection where Iterator.Element == Middleware {

    func chain(to responder: Responder) -> Responder {
        return reversed().reduce(responder) { nextResponder, nextMiddleware in // 3

            return Request.Handler { request in

                return try nextMiddleware.respond(to: request, chainingTo: nextResponder) // 4

            }
        }
    }

}
Request와 Collection을 익스텐션하기

chain 메소드는 모든 미들웨어를 뒤집고(3) 각 단계에 새로운 Handler를 생성하고 반환한다. 이것은 요청과 현재 응답을 보냄으로서 앞에서 말한 Middleware 프로토콜의 메소드를 호출한다. 이렇게하여 우리의 미들웨어 스택(쉽게 설명하기위해 줄인말)을 기억한다면 [Redirect, Headers, Gzip]는 아래처럼 chain메소드를 통해 갈것이다.
reverse ->
[Gzip, Headers, Redirect] ->
create and return a Handler, that in its closure calls Gzip's respond(to: request, chainingTo: mainResponder) ->
create and return a Handler, that in its closure calls Headers' respond(to: request, chainingTo: gzipResponder) ->
create and return a Handler, that in its closure calls Redirect's respond(to: request, chainingTo: headersResponder) ->
return the Redirect Handler
미들웨어의 클로저를 연결하기

Dropletrespond 메소드는 반환된 Handlerclosure가 호출된 곳에 위치한다(2). 이것은 다음 그리고 다음을 호출할 것이다. 마지막으로 호출될 것은 get 메소드에서 전달된 Handler 클로저 이고, 이것은 응답/에러를 다시 연결한 처음의 것이다.
drop.get("/test", handler: { request in

    print("4")
    return try JSON(node: "This is a test page.")
})
get의 핸들러

마지막으로 모두 합치고 그 chain을 따라가보자. 안전한 요청은 이런식으로 보내진다.
create the main responder ("0") ->
RequestMiddleware ("1") ->
Other, internal middleware ->
HeadersMiddleware ("2") ->
GZipMiddleware ("3") ->
MiscMiddleware1 ->
MiscMiddleware2
안전한 요청 chain

그리고 응답은 이런식으로 보내진다.
get's handler ("4") ->
GZipMiddleware ("5") ->
HeadersMiddleware ("6") ->
Other, internal middleware ->
RequestMiddleware ("7") ->
response is sent to client ("10")
응답 chain

안전하지않은 요청은 아래처럼 약간 긴 경로를 따라간다.
get("/test") ->
RequestMiddleware ("1") ->
RequestMiddleware ("alternate") ->
start over ->
RequestMiddleware ("1") ->
[continue with the rest of the secure path]
안전하지 않은 요청 chain

여러분도 보았듯, 미들웨어의 주된 장점은 특화된 클래스로 더 작게 쪼개어 코드를 모듈화 시킬 수 있다는 점이다. 각각이 보통 하나의 작은 목적을 제공하는 덕분에, 더 쉬워지고 더 표현력있고 테스트/변경/제고하기 더 쉬워질 수 있다. 이것은 서로 전혀 모르며 며,이것들은 모두 체인의 위 아래에서 무슨일이 일어나는지 모른체 독립적으로 요청/응답이 동작한다.

체인을 넘기는 것은 정확히 같은 경로를 따르며, 굉장히 직관적이다. 만약 무엇이든 에러를 던졌는데 처리하지 않았다면 헨들러가 발견될때까지 요청/응답 체인에 돌려 보내거나 제네릭 에러 메시지와 함께 만들어진 Response에 내부 캐치(catch) 블럭(extension Droplet: Responder 예제코드에 있는 3)에 도달하게 될 것이다.

어떻게 에럴를 처리할 수 있는지 한번 보자. 먼저, 가장 직관적인 그 자리에서 처리하는 방법이다.
extension String: Error { } // 1


drop.get("/handle", handler: { request in

    do {
        let answer = try findAnswer(in: request)
        returnJSON(answer) // 2

    }
    catch {
       return JSON("Answer couldn't be computed.") // 3

    }
})

drop.get("/pass-along", handler: { request in

   let answer = try findAnswer(in: request) // 4

   return JSON(answer) // 5

})


func findAnswer(for request: Request) throws->String {
   guard let answer = request.extract("answer") as? String else {
       throw "Answer parameter is not present." // 6

   }

  guard computingFinishedFast else {
       throw "Computing took too long to finish."
   }

   return answer
}
문자열 던지기

StringError를 따르게 하여(1) , 연관 값으로 만든 열거형을 만들어서 Error를 따르게 할 수 있으면 문자열을 던질 수 있다.

handle 엔드포인트 경우에, findAnswer에서 잘 처리했다면 answer로 만들어낸 JSON을 반환한다(2). 뭔가 문제가 있다면 에러를 캐치하고 에러로 만들어낸 JSON을 반환한다.

pass-along 엔드포인트의 경우,  모든것이 잘 처리되었다면 같은 시나리오대로 (5)를 적용하고, 에러가 던져졌다면 여기서 처리하지 않으므로 앞에서 언급한대로 콜백체인으로 보내질 것이다.

앞에서 본 것 처럼, findAnswer 메소드의 사용할 다른 엔드포인트를 추가하면 계속계속 에러를 다뤄야 할 것이다.

Vapor의 문서에서 추천해주는 다른 방법으로는, 모든 에러를 처리해주는 에러 처리 미들웨어를 만드는 것이다. AppError 엔티티를 만들고 findAnswer 메소드를 바꿈으로서 시작해보자.
enum AppError: Error {
    case argumentNotPresent(name: String)
    case computingTimedOut
}

drop.get("/handle", handler: { request in

    do {
        let answer = try findAnswer(in: request)
        return JSON(answer) // 1

    }
    catch {
        return JSON("Answer couldn't be computed.") // 2

    }
})

drop.get("/pass-along", handler: { request in

    let answer = try findAnswer(in: request) // 3

    return JSON(answer) // 4

})


func findAnswer(for request: Request) throws -> String {
    guard let answer = request.extract("answer") as? String else {
        throw AppError.argumentNotPresent(name:"answer") // 5

    }

    guard computingFinishedFast else {
        throw AppError.computingTimedOut // 6

    }

    return answer
}
커스텀 에러 던지기

여전히 그 자리에서 에러를 다룰 수 있지만(1)(2), 에러를 다루기위한 미들웨어를 추가할 것이기 때문에 이것들을 함께 던져(3)(4) 어디로 갈지 정할 수 있다. 이제 findAnswer메소드는 기대하는 파라미터를 위해 연관 값과함께 AppError를 던지며(5), 타임아웃을 위한 에러도 던진다(6).

이미 알고 있듯, 한 메소드만 구현하면 되는데, 아래에서 어떻게 하는지 확인해보자.
struct ErrorHandlingMiddleware: Middleware {

    func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        do {
            return try next.respond(to: request) // 1

        }
        catch AppError.argumentNotPresent(let name) { // 2

            throw Abort.custom( // 3

                status: .badRequest,
                message: "Argument \(name) was not found." // 4

            )
        }
        catch AppError.computingTimedOut { // 5

              throw Abort.custom(
                status: .requestTimeout, // 6

                message: "Computing an answer has timed out."
            )
         }
         catch { // 7

                return Response( // 8

                    status: .serverError,
                    message: "Something unexpected happened."
                )
          }
    }
}
ErrorHandlingMiddleware

우리가 해야할 일은 do-catch 블럭 안에서 다음 응답자에 요청을 보내주는 일만 하면 된다. 만약 모든 처리가 잘 되었다면(1), 요청은 get 헨들러에 도달할 것이고 응답은 우리에게 돌아온다(1). 그리고 체인을 따라 간 것을 반환할 수 있다(1).

findAnswer 메소드에서 뭔가 잘못되었다면 우리 스스로 캐치할 수 있게 만들수 있고 특정의 Abort 에러들을 만들어서 던질 수 있다(3). Abort는 Vapor에서 제공하는 열거형일 수도 있고 Response를 생성해서 반환할 수도 있다(8).

연관 값과함께 에러를 캐치하는 것은 우리에게 어떤 유연함을 제공해 주는데, 보이지 않는 파라미터를 추출해내거나(4) 다른 파라미터에 같은 에러를 던질 수 있는 가능성을 열어준다.
func findMeaningOfLife(for request: Request) throws -> Int {
    guard let answer = request.extract("meaningOfLife") as? Int else {
        throw AppError.argumentNotPresent(name: "meaningOfLife")
    }

    return 42
}
연관 값과 함께 에러 던지기

이제 적절한 상태와 함께(6) 개별적인 타임아웃 에러를 다룰 수 있게 되었다(5). 그리고 serverError를 반환하는 곳에 나머지 모든 것을 위한 catch 콜백도 가질 수 있다. 이 serverError는 제네릭 메시지이다.

마지막으로 droplet에  우리의 새 미들웨어를 추가한다.
// [...]
drop.middleware.append(ErrorHandlingMiddleware())
// [...]
에러 처리 미들웨어 추가하기

한가지만 더, 미들웨어의 괜찮은 기능을 소개하고 싶다. 서버당 설정(per-server configuration)이다. ErrorHandlingMiddleware를 제품판(production)에서만 동작하게 하고 싶다고 가정하자. 어떤 이유로 우리앱에서 부분적으로 크래쉬를 내고 싶다고 하자. 우리가 지금까지 봐온 방식대로 미들웨어를 붙이는 것 대신에, droplets은 설정파일을 제공하며, 아래와같이 사용할 수 있다.

먼저, 미들웨어(middleware)로 붙이는 것 대신 컨피겨레이블(configurable)로 미들에웨를 추가하자.
// [...]
// drop.middleware.append(ErrorHandlingMiddleware()) -> replaced with:
drop.addConfigurable(middleware: ErrorHandlingMiddleware(), name: "error-handling")
// [...]
컨피겨레이블 미들웨어 추가하기

Config/production/droplet.jsonConfig/staging/droplet.json 파일서 적절한 키를 추가하자.
// production/droplet.json
{
   ...
    "middleware": {
        "server": [
            ...
            "error-handling", // 1
            ...
        ],
        "client": [
            ...
        ]
    },
    ...
}

// staging/droplet.json
{
    ...
    "middleware": {
        "server": [
            ... // 2
        ],
        "client": [
            ...
        ]
    },
    ...
}
droplet.json

이제 앱 실행때 ErrorHandlingMiddleware는 제품판 서버의 미들웨어에 추가될 것이지만(1), (2) 단계에서는 아니다. 서버와 클라이언트 두가지 다에 미들웨어를 추가할 수 있고, Config/server-type/droplet.json에도 추가할 수 있다. 또한 그 배열의 순서에 따라 된다.
이 글의 끝에 도달하고 있는 것처럼 스위프트는 서버 세팅을 위한 실용적인 솔루션임을 볼 수 있다. 우리의 요청/응답을 수정하기 쉽게 하기 위해 프로토콜은 Middleware를 정의하게 해주고, 체인된 스택에 핸들러를 추가하는 다양한 방법을 제공한다. 또한 기본 에러 처리를 위해 문자열을 던지는 용도로 익스텐션 하는 것 뿐만 아니라, 프로젝트의 더 나은 구조를 만들기 위해 메인 스트럭쳐에서 Request.Handler를 분리해내는 것도 하였다.

서버와 클라이언트에서 둘 다 스위프트를 사용하면, 중복을 피하면서 모델, 기능, 핼퍼/유틸리티를 공유할 수 있고,  서버와 클라이언트를 전체적인 하나로 봄으로서 모든것을 더 쉽게 만들 수 있다.(역자: 이 문장은 조금 더 의논해볼 필요가 있는 것 같습니다.)

Vapor 그 자체로서, 프레임워크로서 선택인가?(As for Vapor itself, as the framework of choice?) 이 에 따르면 최고도 아니고 최악도 아니다. Vapor는 Express.js와 Sinatra와 비슷한 성격을 제공하며(이것들은 이전에 내 블로그에 쓰인 두 방법이었다.), 내 요구에 잘 들어 맞았다(블로그는 너무 많은 요청이 없으며, API를 필요로 하지 않는다).


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: What's new in Swift 4.0

This article was translated with permission from the English original, What's new in Swift 4.0? on Hacking with Swift .

새로운 스위프트4의 새로운 점을 배우기위한 실제 예제 코드: 새로운 인코딩과 디코딩, 똑똑해진 키패스(keypaths), 다열 문자열(multi-line string) 등이 있습니다!

스위프트4.0이 모두의 인기있는 앱 개발 언어로 새로 배포되었고, 간단하고 안전한 코드를 작성할 수 있는 다양한 기능들을 소개했다. 스위프트 3.0이 발표되었을때 그 극적인 변화에 비하면 즐겁게 받아드릴 수 있을 것인데, 실제로 대부분 변화는 현재 존재하는 스위프트 코드와 완전히 호환이 되게 만든 것이다. 따라서 여러분이 약간의 변경사항을 원한다면, 그렇게 오래 걸리지 않을 것이다.

주의: 스위프트4는 아직 활발히 개발중이다. 나는 여기서 몇가지 유용한 새 기능을 골랐고, 이것들은 현재 모두 구현되었으며 시도해볼 수 있다. 마지막 배포가 있기 전의 그 달에 더 많은 기능이 나올 것임을 기억하라.

이 글이 마음에 들었다면, 아래의 것들도 흥미있을 수 있을 것이다.

스위프트스러운 인코딩과 디코딩
값 타입이 멋지다는 사실은 알고있지만, NSCoding과같은 Objective-C API와 인터렉트하기는 힘들다는 것도 알고있다. 둘을 이어주는 레이어를 만들거나 클래스에 넣어 사용해야한다. 두가지 방법 다 좋지만은 않다. 더욱 나쁜것은, 클래스에 넣어 변경할지라도 직접 인코딩과 디코딩 메소드를 작성해야하며, 이것은 고통스러운 에러를 야기할 수 있다.

스위프트4는 특수한 코드 없이 커스텀 데이터 타입을 시리얼라이즈하고 디-시리얼라이즈하게해주는 Codable 프로토콜을 소개했다. 여러분의 값 타입 손실에대해 더이상 걱정하지 않아도 된다. 더 나은점은 어떻게 데이터를 시리얼라이즈 할지 정할 수 있다는 것이다. 기존의 프로퍼티 리스트 양식을 사용할 수도 있고 JSON도 가능하다.

여러분이 읽고 있는 것이 맞는 말이다: 스위프트4는 특별한 코드 없이 여러분의 커스텀 데이터 타입을 JSON으로 시리얼라이즈 해준다.

이 얼마나 아름다운 광경인지 지켜보자. 먼저, 여기에 커스텀 데이터 타입이 있고, 그 인스턴스가 있다.
struct Language: Codable {
   var name: String
   var version: Int
}

let swift = Language(name: "Swift", version: 4)
let php = Language(name: "PHP", version: 7)
let perl = Language(name: "Perl", version: 6)
Language 구조체에 Codable 프로토콜을 따르게 한 것을 볼 수 있을 것이다. 이 간단한 추가사항만으로 아래처럼 JSON의 Data 표현으로 변환할 수 있다.
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(swift) {
   // save encoded somewhere
}
스위프트는 자동으로 여러분의 데이터 타입 안에있는 모든 프로퍼티들을 인코딩할 것이다. 여러분은 딱히 할 일이 없다.

이제 여러분도 나와같이 오랫동안 NSCoding을 사용해왔다면 뭔가 의심스러울 것이다. 정말로 제대로 동작할까? 그리고 이게 동작한다는 것을 어떻게 확신할 수 있을까? 음, Data 오브젝트를 문자열로 만드는 코드를 추가해서 한번 출력해보자. 그리고 다시 Language 인스턴스로 디코딩 시켜, 읽을 수 있는지 보자.
if let encoded = try? encoder.encode(swift) {
   if let json = String(data: encoded, encoding: .utf8) {
       print(json)
   }

   let decoder = JSONDecoder()
   if let decoded = try? decoder.decode(Language.self, from: encoded) {
       print(decoded.name)
   }
}
디코딩이 타입캐스팅을 필요로 하지 않는다는 점을 인지하자. 첫번째 파라미터로 데이터 타입 이름을 주면, 스위프트는 여기서 리턴 타입을 추론한다.

JSONEncoder와 그 프로퍼티 리스트에 짝인 PropertyListEncoder는 이 부분이 어떻게 동작할지 수많은 커스터마이징 옵션을 가진다. 빽빽한(compact) JSON을 원하는지, 출력하지 좋은(pretty-printed) JSON을 원하는지, ISO8601 데이터를 원하는지, 유닉스 epoch 데이터를 원하는지, 바이너리 프로퍼티 리스트를 사용하고 싶은지 XML을 사용하고 싶은지 정할 수 있다. 다른 옵션들에대한 더 많은 정보를 원한다면  the Swift Evolution proposal for this new feature을 확인해보자.

다열 문자열 리터럴
스위프트에서 다열 문자열로 쓰려면 항상 문자열 안에 \n을 써서 개행을 했었어야 했다. 코드상에서 보기에 좋아보이지 않지만 사용자에게는 올바르게 보여진다. 다행히 스위프트4에서 새로 소개된 다열 문자열 리터럴 문법은 문자열 보간법같은 기능도 쓸 수 있는 채로, 자유롭게 개행을 할 수 있게하고 이스케이핑 없이 따옴표를 사용할 수 있다.

문자열 리터럴을 시작하기 위해서는 쌍따옴표를 3개(""") 적고 리턴키를 누른다. 그 다음 여러분이 원하는 만큼 문자열을 쳐내는데, 변수나 개행도 가능하며, 문자열이 끝날때는 리턴키를 누르고 쌍따옴표를 세개 치면 된다.

문자열 리터럴에는 두가지 중요한 규칙이 있기 때문에 리턴키를 누르는 것에대해 이야기하고 싶다. """으로 문자열을 열때, 여러분의 문자열 내용은 반드시 새로운 라인에서 시작해야한다. 그리고 """로 다열 문자열을 끝낼때는 새로운 줄에서 """을 쳐야한다.

여기에 예시가 있다.
let longString = """
When you write a string that spans multiple
lines make sure you start its content on a
line all of its own, and end it with three
quotes also on a line of their own.
Multi-line strings also let you write "quote marks"
freely inside your strings, which is great!
"""
이것은 정의에서 몇몇 개행이 들어있는 새로운 문자열을 만든 것이다. 훨씬 읽고 쓰기 쉽다.

더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

키-밸류 코딩을위한 키패스 증진(Improved keypaths for key-value coding)
Objective-C에서 사랑했던 기능 중 하나는 직접 접근하는 것이 아닌 동적으로 프로퍼티를 참조하는 능력이다. 이것은 "주어진 오브젝트 X에 내가 읽고 싶은 프로퍼티가 있어"라고 말할 수 있다. 이런 참조 방법을 키패스(keypath)라 부르고 이것은 직접 프로퍼티에 접근하는 것과 구별되는데, 여기서는 실제로 값을 읽고 쓰는 것이 아니라 슬며시 감춰놨다가 나중에 사용하는 것이다.

여러분이 아직 키패스를 사용해보지 않았다면 기존의 스위프트 메소드를 이용해서 어떤식으로 동작했는지 보여주겠다. Starship이라는 구조체를 하나 정의하고 Crew라는 구조체를 정의하자. 그리고 각각의 인스턴스를 하나씩 만든다.
// an example struct
struct Crew {
   var name: String
   var rank: String
}

// another example struct, this time with a method
struct Starship {
   var name: String
   var maxWarp: Double
   var captain: Crew

   func goToMaximumWarp() {
       print("\(name) is now travelling at warp \(maxWarp)")
   }
}

// create instances of those two structs
let janeway = Crew(name: "Kathryn Janeway", rank: "Captain")
let voyager = Starship(name: "Voyager", maxWarp: 9.975, captain: janeway)

// grab a reference to the goToMaximumWarp() method
let enterWarp = voyager.goToMaximumWarp

// call that reference
enterWarp()
스위프트에서는 함수가 일급 타입이므로 마지막 두 줄에서 goToMaximumWarp() 메소드를 enterWarp으로 참조하게 만들 수 있다. 그리고 나중에 우리가 원할때 언제든지 사용할 수 있다. 여기서 문제는 이와같은 것을 프로퍼티에는 못한다는 것이다. "불가피한 상황이 생겼을때 확인할 수 있도록 captain의 이름 프로퍼티를 참조하는 것을 만들어라"고 말할 수 없다. 스위프트는 단지 직접 프로퍼티를 읽어서 원래의 값을 얻어내가 때문이다.

이것은 키패스로 고칠 수 있는데, 우리의 enterWarp() 코드같은 프러퍼티를 참조하며 호출되지 않은 것(uninvoked references to properties)이다. 이제 참조를 호출하면 현재 값을 얻어낸다. 그러나 나중에 참조를 호출하면 나중에 값을 얻어낸다. 프로퍼티의 수를 통해 파헤쳐볼 수 있고, 스위프트는 올바른 타입을 받나냈는지 확인하기위해 타입 추론을 이용한다.

스위프트 에볼루션 커뮤니티는 이 키패스 문법이 올바른지에대해 꽤 오랫동안 토론해왔었는데, 이것이 스위프트 코드와 시각적으로 달라 보일 필요가 있었기 때문이다. 결국 백슬래시를 사용한 문법이 되었다. \Starship.name, \Starship.maxWarp, \Starship.captain.name. 이 두가지를 변수에 할당하여 여러분이 원하는 언제나 어떤 Starship 인스턴스에서도 사용할 수 있다. 예제를 보자.
let nameKeyPath = \Starship.name
let maxWarpKeyPath = \Starship.maxWarp
let captainName = \Starship.captain.name

let starshipName = voyager[keyPath: nameKeyPath]
let starshipMaxWarp = voyager[keyPath: maxWarpKeyPath]
let starshipCaptain = voyager[keyPath: captainName]
스위프트는 타입 추론이 가능하기 때문에 starshipName 문자열과 starshipMaxWarp 더블형을 만들 것이다. 이 예제의 세번째처럼 프로퍼티의 프로퍼티라도 스위프트는 역시 올바르게 이해할 것이다.

나중에는 런타임중에도 배열 인덱스를 접근할 수 있거나 문자열로부터 키패스를 만들어낼 수 있는 계획이 있다. 더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

약간의 광고
If you're enjoying this article, you might like my free Natural Swift video. It gives you 75 minutes of hands-on coding that teaches functional programming, protocol-oriented programming, and value types, and you can download it for free with no obligation or catches – just click here.

And now back to your regularly scheduled broadcast…

딕셔너리 기능 증진
스위프트4에서 한가지 더 흥미로운 프로퍼절은 딕셔너리를 더욱 강력하게 만들고 특정 상황에 여러분이 예상한대로 동작하게 만드는 기능이었다.

간단한 예제로 시작해보자. 스위프트3에서 딕셔너리를 필터링 하는 것은 새로운 딕셔너리를 반환하지 았다. 대신 키/값 래이블과 함께 튜플 배열을 반환했었다. 예제이다.
let cities = ["Shanghai": 24_256_800, "Karachi": 23_500_000, "Beijing": 21_516_000, "Seoul": 9_995_000];
let massiveCities = cities.filter { $0.value > 10_000_000 }
이후에는 더이상 딕셔너리가 아니므로 massiveCities["Shanghai"]로 읽을 수 없다. 대신 massiveCities[0].value를 사용해야했는데, 별로 좋아보이지 않는다.

스위프트4부터는 더욱 여러분이 생각한대로 동작한다. 여기서는 새로운 딕셔너리를 반환해준다. 명백하게 이것은 튜플-배열 리턴 타입에 의존하고있는 현재 코드를 변경해야 할 것이다.

비슷하게, 딕셔너리에서 map() 메소드도 많은 사람들이 원하던 방식대로 되지 않았었다. 전달되면 키-값 튜플을 받고, 배열에 추가하기위해 한 값을 반환할 수 있다. 예제를 보자.
let populations = cities.map { $0.value * 2 }
이것은 스위프트4에서 바뀌지 않았지만, mapValues()라 불리는 새 메소드가 추가되었다. 이것은 더욱더 유용할 것으로 보이는데, 값을 변경하고 기존의 키를 이용해 딕셔너리에 다시 값을 넣어둘 수 있게 해준다.

예를들어, 이 코드는 모든 도시 인구를 100만으로 나누고 문자열로 만든다. 그리고 다시 Shanghai, Karachi, Seoul의 같은 키로 새로운 딕셔너리에 넣는다.
let roundedCities = cities.mapValues { "\($0 / 1_000_000) million people" }
(여기서 여러분은 실수로 중복할 수도 있기 때문에 딕셔너리 키를 맵핑시키는게 안전하지 않다고 생각할 수 있다.)

쉽게, 내가 좋아하는 새로운 딕셔너리 추가사항은 grouping 생성자이다. 이것은 시퀀스를 여러분이 원하는대로 그룹화한 시퀀스의 딕셔너리로 만들어준다. 계속해서 cities 예제로가서, 도시 이름의 배열을 얻어내기위해 cities.keys를 사용할 수 있으며, 첫글자로 그룹화한다.
let groupedCities = Dictionary(grouping: cities.keys) { $0.characters.first! }
print(groupedCities)
// ["B": ["Beijing"], "S": ["Shanghai", "Seoul"], "K": ["Karachi"]]
대신 아래처럼 그 이름의 길이에따라 도시들을 그룹화 시킬 수 있다.
let groupedCities = Dictionary(grouping: cities.keys) { $0.count }
print(groupedCities)
// [5: ["Seoul"], 7: ["Karachi", "Beijing"], 8: ["Shanghai"]]
마지막으로, 딕셔너리 키에 접근할 수 있고 키에 해당하는 값이 없다면 디폴트 값을 제공할 수 있다.
let person = ["name": "Taylor", "city": "Nashville"]
let name = person["name", default: "Anonymous"]
이제 경험이 좀 있는 개발자들은 nil-coalescing으로 사용하는게 더 났다고 주장할 것이다. 나도 동의한다. 현재 버전의 스위프트것을 사용하는 것 대신에 아래처럼도 쓸 수 있다.
let name = person["name"] ?? "Anonymous"
그러나 그냥 읽는데는 문제가 없지만, 딕셔너리 값을 수정할때는 동작하지 않는다. 적절하게 딕셔너리 값을 수정할 수 없는데, 그 키로 접근하면 옵셔널을 반환하기 때문이다. 키가 존재하지 않을 수도 있다. 스위프트4의 디폴트 딕셔너리 값으로 여러분은 더욱 적합한 코드를 작성할 수 있다. 아래처럼 말이다.
var favoriteTVShows = ["Red Dwarf", "Blackadder", "Fawlty Towers", "Red Dwarf"]
var favoriteCounts = [String: Int]()

for show in favoriteTVShows {
   favoriteCounts[show, default: 0] += 1
}
이 코드는 favoriteTVShows에 있는 모든 문자열을 돌면서, favoriteCounts라는 딕셔너리를 사용하여 각 항목이 나타날때마다 카운팅을 한다. 코드 한줄에서 딕셔너리를 수정할 수 있는데, 딕셔너리에는 항상 값을 가질거라는 것을 알기 때문이다. 디폴트값의 0이든 이전에 카운팅되었던 것에따라 더 높은 숫자든.

더 많은 정보를 원한다면 the Swift Evolution proposal for these new features을 확인해보자.

문자열은 다시 컬랙션이다!
작은 변화지만 많은 사람들을 행복하게 만든다. 문자열이 다시 컬랙션이 되었다. 이 의미는, 이것을 뒤집거나, 문자를 하나씩 돌거나, map()하거나 flatMap()하는 등이 가능하다. 예제를 보자.
let quote = "It is a truth universally acknowledged that new Swift versions bring new features."
let reversed = quote.reversed()

for letter in quote {
   print(letter)
}
이 변경은 String Manifesto라 불리는 다양한 개정사항의 부분에 소개되어 있다.

한쪽만의 범위(One-sided ranges)
가장 최신은 아니지만 최근에, 스위프트4는 파이썬같은 한쪽만 컬랙션 자르기를 소개했다. 한쪽이 빠지면 자동으로 컬랙션의 시작이나 끝을 추론한다. 이 변경은 현재 코드에 영향을 주지 않는다. 현재 연산자에 새로운 부분이기 때문에 잠재적인 손상을 걱정하지 않아도 된다.

아래에 예제가 있다.
let characters = ["Dr Horrible", "Captain Hammer", "Penny", "Bad Horse", "Moist"]
let bigParts = characters[..<3]
let smallParts = characters[3...]
print(bigParts)
// ["Dr Horrible", "Captain Hammer", "Penny"]
print(smallParts)
// ["Bad Horse", "Moist"]
더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

아직 더 남은게 있다...
스위프트4를 탑재한 Xcode의 첫번째 배포판은 6월에 iOS11, tvOS11, watchOS4, macOS와함께 나올것으로 보인다(역자: 이미 나왔습니다). 지금까지 우리가 본 것은 특히 명확하게 이미 나오기로 약속되었고, 팀은 가능한 추가할 수 있도록 스위프트4를 만들려고 노력하고 있다. 주로 기존에 있는 것을 고치거나 수정하지 않도록 새로운 기능을 추가하는 것이 업그레이드하기 편하기 해줄 수 있고, 고맙게도 이 언어를 위한 새로운 안정성의 시작의 신호가 있다.

스위프트 에볼루션이 때론 혼돈속에 빠지기도 했지만(접근 수준같은 부분), 스위프트4는 애플의 커뮤니티 방법을 다시 검증했다. 나는 위에 몇몇 스위프트 에볼루션을 링크 걸어두었는데, 각각의 것들은 커뮤니티 여론의 도움으로 광범위하게 토론되었다. 애플 엔지니어가 변경을 강행하지 않고 분별력있게 하였으며, 무엇이 이미 스마트하고 엘레강트한 언어인지 다듬기위해 고려했다.

한가지 지연된 기능은 ABI 호환성인데, 이것은 개발자들에게 컴파일된 라이브러리를 배포할 수 있게 해줄 것이다. 오늘 스위프트에 몇가지 핵심 기능이 구현되지 않고 남아있다. 다행히도 스위프트5가 되기 전에 만나볼 수 있을 것이다...


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: What's New in Swift 4

주의: 이 튜토리얼은 Xcode9 beta 1에 들어있는 스위프트 4버전을 사용함.

스위프트4는 애플이 2017년 가을에 낼 가장 최신의 메이저 배포이다. 그 메인 초점은 스위프트3 코드와 소스 호환성 제공이며, ABI 안정성에 맞춰 작업하고 있다. 이 글에서는 스위프트가 바뀌면서 여러분의 코드에 중요하게 영향을 줄 것을 짚어 줄 것이다. 그리고 이제 시작해보자!


Swift 4Swift 4

시작하며
스위프트4는 Xcode9에 포함되있다. 여러분은 애플의 developer portal에서 최신버전의 Xcode9를 다운받을 수 있다(반드시 활성화된 개발자 계정이 있어야한다). 각 Xcode beta는 배포될 시점의 가장 최신의 스위프트4를 탑재할 것이다.

읽어내려가다보면 [SE-xxxx] 양식의 링크를 볼 수 있을 것이다. 이 링크들은 관련된 스위프트 에볼루션 프로포절로 연결해준다. 어떤 주제든 더 배우고 싶으면 이 링크를 확인해보자.

나는 여러분이 스위프트4의 각 기능을 시도해보길 추천하며 플레이그라운드에서 돌려보길 바란다. 그리하여 지식을 여러분 머릿속에 확립시킬 수 있고 각 주제마다 더 깊게 들어가볼 수 있게 해줄 것이다. 각 예제를 고쳐보고 추가해보면서 가지고 놀아보자. 잘 즐겨보길 바란다!

노트: 이 글은 각 Xcode beta에대해 갱신될 것이다. 여러분이 다른 스위프트 스넵샷을 사용하면 여기 코드가 동작할것이라는 보장은 할 수 없다.

스위프트4로 마이그레이션하기
스위프트3에서 4로 마이그레이션하는 것은 2.2에서 3으로 가는 것보다 덜 힘들 것이다. 보통 많은 변화가 추가되고 개별적으로 건드릴 필요가 없다. 그덕에 스위프트 마이그레이션 툴은 주요 변화만 처리해 줄 것이다.

Xcode9는 동시에 스위프트4와 스위프트3.2안에 스위프트3도 가능하게 지원한다. 여러분 프로젝트에서 각 타겟은 스위프트3.2나 스위프트4가 될 수 있고, 필요에따라 조각조각 마이그레이션 할 수 있다. 그래도 스위프트3.2로 변환하는 것은 완전히 자유롭진 않다. 새로운 SDK에 맞추기위해 여러분의 코드 부분을 업데이트 해야할 것이다. 그리고 스위프트는 아직 ABI 안정이 되지 않았기 때문에 Xcode9로 여러분의 의존성들을 다시 컴파일 해야할 것이다.

스위프트4로 마이그레이션할 준비가 되었다면 Xcode는 다시한번 마이그레이션 툴로 여러분을 도와줄 것이다. Xcode에서 Edit/Convert/To Current Swift Syntax...로 가서 변환 툴을 실행시킨다.

변환하고 싶은 타겟을 선택한 후, Xcode는 Objective-C 추론에대한 설정을 물어볼 것이다. 추론을 제한하여 바이너리 크기를 줄이는 추천 옵션을 선택하자(이 토픽에대해 더 알고 싶으면 아래에 Limiting @objc Inference를 확인해보자).

여러분의 코드가 의도한대로 바뀌었는지 잘 이해하기위해 우리는 제일 먼저 스위프트4의 API 변경사항들을 다룰 것이다.

API 변경사항
스위프트4에서 소개된 추가사항에 들어기보기 전에, 기존의 API를 변경하거나 증진시킨것이 무엇이 있는지 먼저 살펴보자.

문자열(String)
스위프트4에서 String은 상당히 많은 사랑을 받고 있다. 이 프로포절은 많은 변경사항을 가지며, 큰것부터 하나씩 보자. [SE-0163]

여러분은 이 부분에서 옛날 기분이 들것이다. 스위프트가 2.0버전으로 돌아와서 다시 컬랙션이 되었다. String에서 characters 배열이 필요하다는 점을 없앴다. 이제 String 오브젝트를 직접 이터레이트(iterate) 할 수 있다.
let galaxy = "Milky Way 🐮"
for char in galaxy {
  print(char)
}

YesYes


String으로 논리적인 이터레이트만 가능한게 아니라, SequenceCollection의 모든 기능을 사용할 수 있다.
galaxy.count       // 11

galaxy.isEmpty     // false

galaxy.dropFirst() // "ilky Way 🐮"

String(galaxy.reversed()) // "🐮 yaW ykliM"


// Filter out any none ASCII characters
galaxy.filter { char in
  let isASCII = char.unicodeScalars.reduce(true, { $0 && $1.isASCII })
  return isASCII
} // "Milky Way "
위의 ASCII 예제는 Character의 작은 개선사항을 설명해준다. 이제 Character에서 직접 UnicodeScalarView에 접근할 수 있다. 이전에는 새 String을 만들어야 했었다 [SE-0178].

다른 추가사항은 StringProtocol이다. 이전에 String에 정의되있던 많은 기능들이 이 프로토콜에 정의되있다. 이렇게 변경하게된 이유는 자르기(slice) 작업을 개선하기 위함이다. 스위프트4는 String에서 서브시퀀스를 참조하기위해 Substring 타입을 추가했다.

StringSubstring은 둘 다 StringProtocol에 주어진 기능을 대부분 비슷하게 구현하였다.
// Grab a subsequence of String
let endIndex = galaxy.index(galaxy.startIndex, offsetBy: 3)
var milkSubstring = galaxy[galaxy.startIndex...endIndex]   // "Milk"

type(of: milkSubstring)   // Substring.Type


// Concatenate a String onto a Substring
milkSubstring += "🥛"     // "Milk🥛"


// Create a String from a Substring
let milkString = String(milkSubstring) // "Milk🥛"
또다른 멋진 개선사항은 String이 grapheme cluster를 해석하는 방식이다. 이 해결방안은 유니코드9 각색한 것으로부터 나왔다. 이전에는 유니코드 문자들이 여러 코드 포인트로 만들어져있었는데, 이때문에 count는 1로 합산되었다. 피부톤을 선택하고 이모티콘을 넣으면 이런 일이 발생했다. 아래에 몇가지 예제가 있는데, 예전의 동작방식과 이후의 동작방식을 보여준다.
"👩💻".count // Now: 1, Before: 2

"👍🏽".count // Now: 1, Before: 2

"👨❤️💋👨".count // Now: 1, Before, 4
이것은 String Manifesto에 언급된 변화들중 일부분이다. 나중에는 여러분이 예상했던대로 이것에대한 모든 동기와 제안된 솔루션에대해 읽을 수 있다.

딕서녀리와 셋(Dictionary and Set)
Collection 타입이 가버린다면(As far as Collection types go), SetDictionary은 항상 직관적이지 않았다. 운좋게도 스위프트 팀은 여기에 애정을 가져주었다 [SE-0165].

생성자 기반의 시퀀스
그것들 중 첫번째는 키-값 쌍(튜플)의 시퀀스에서 딕셔너리를 만들 수 있게 해주었다는 점이다.
let nearestStarNames = ["Proxima Centauri", "Alpha Centauri A", "Alpha Centauri B", "Barnard's Star", "Wolf 359"]
let nearestStarDistances = [4.24, 4.37, 4.37, 5.96, 7.78]

// Dictionary from sequence of keys-values
let starDistanceDict = Dictionary(uniqueKeysWithValues: zip(nearestStarNames, nearestStarDistances))
// ["Wolf 359": 7.78, "Alpha Centauri B": 4.37, "Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Barnard's Star": 5.96]

중복 키 해결법
이제 여러분이 원하는 방식으로 중복키로 딕셔너리를 초기화할 수 있게 되었다. 이렇게하여 이 문제에대해 별 말 없이도 키-값 쌍을 덮어쓰는 것을 막을 수 있다.
// Random vote of people's favorite stars
let favoriteStarVotes = ["Alpha Centauri A", "Wolf 359", "Alpha Centauri A", "Barnard's Star"]

// Merging keys with closure for conflicts
let mergedKeysAndValues = Dictionary(zip(favoriteStarVotes, repeatElement(1, count: favoriteStarVotes.count)), uniquingKeysWith: +) // ["Barnard's Star": 1, "Alpha Centauri A": 2, "Wolf 359": 1]
위 코드에서는 충돌하는 두 값을 더하여 중복 키 문제를 해결하기위해 축약된 +와 함께 zip을 사용했다.

노트: zip이 생소하다면 애플의 Swift Documentation를 빠르게 읽어보자.

필터링
이제 DictionarySet 모두 필터하였을때 원래 타입의 새 오브젝트를 결과로 할 수 있게 되었다.
// Filtering results into dictionary rather than array of tuples
let closeStars = starDistanceDict.filter { $0.value < 5.0 }
closeStars // Dictionary: ["Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Alpha Centauri B": 4.37]

딕셔너리 맵핑
Dictionary는 그 값을 바로 맵핑할 수 있는 유용한 메소드를 얻어냈다.
// Mapping values directly resulting in a dictionary
let mappedCloseStars = closeStars.mapValues { "\($0)" }
mappedCloseStars // ["Proxima Centauri": "4.24", "Alpha Centauri A": "4.37", "Alpha Centauri B": "4.37"]

딕셔너리의 디폴트 값
일반적으로 Dictionary의 값에 접근할 때, 값이 nil인 경우 디폴트 값을 지정하기위해 nil-coalescing 연산자를 사용한다. 스위프트4에서는 그 줄에서 변경하여 더 깔끔하고 멋진 방법으로 되었다.
// Subscript with a default value
let siriusDistance = mappedCloseStars["Wolf 359", default: "unknown"] // "unknown"


// Subscript with a default value used for mutating
var starWordsCount: [String: Int] = [:]
for starName in nearestStarNames {
  let numWords = starName.split(separator: " ").count
  starWordsCount[starName, default: 0] += numWords // Amazing

}
starWordsCount // ["Wolf 359": 2, "Alpha Centauri B": 3, "Proxima Centauri": 2, "Alpha Centauri A": 3, "Barnard's Star": 2]
이전에 이 타입을 변경하려면 커다란 if let문으로 감싸야 했다. 스위프트4는 한줄에 된다!

딕서녀리 그룹핑
또다른 멋진 추가사항은 Dictionary를 Sequence로부터 초기화할 수 있는 기능과 대괄호로 그룹화할 수 있는 기능이다.
// Grouping sequences by computed key
let starsByFirstLetter = Dictionary(grouping: nearestStarNames) { $0.first! }

// ["B": ["Barnard's Star"], "A": ["Alpha Centauri A", "Alpha Centauri B"], "W": ["Wolf 359"], "P": ["Proxima Centauri"]]
이렇게하면 데이터를 특정 패턴으로 그룹화할때 편하게 다가올 것이다.

용량 잡아놓기(Reserving Capacity)
SequenceDictionary 둘 다 이제 명시적으로 용량을 정해놓을 수 있다.
// Improved Set/Dictionary capacity reservation
starWordsCount.capacity  // 6

starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity

starWordsCount.capacity // 24
이 타입에서 재할당은 부담되는 작업일 수 있다. 저장되는데 필요한 데이터가 얼마인지 알때 reserveCapacity(_:)을 사용하여 퍼포먼스를 쉽게 개선할 수 있다.

이것은 엄청난 양의 정보이니, 그 타입들에대해 명확하게 확인해보고 추가적으로 여러분의 코드에 사용할 방법이 있는지 찾아보자.

private 접근 수정자
스위프트3애서 그렇게 반갑지 않던 부분은 fileprivate 였을 것이다. 이론적으로는 좋지만, 실제로는 사용하기 혼란스러울 수 있었다. private의 목표는 맴버 자신에서만 사용하는 것이었는데, fileprivate은 같은 파일안에 맴버들사이에서 접근을 공유하려는 경우에는 거의 사용하지 않는다.

이 이슈는 스위프트가 익스텐션을 사용하여 논리적인 그룹으로 코드를 쪼개도록 지향하기 때문에 생긴다. 익스텐션은 원래 맴버가 선언된 범위 밖에서 사용할때가 많다. 그렇기때문에 fileprivate에대한 확장이 필요하게 되었다.

스위프트4는 한 타입과 다른 익스텐션 사이에 같은 접근 제어 범위를 공유하여 원래 의도를 이해한다. 이것은 오직 같은 소스파일 안에서만 잡아둔다 [SE-0169].

struct SpaceCraft {
  private let warpCode: String

  init(warpCode: String) {
   self.warpCode = warpCode
  }
}

extension SpaceCraft {
  func goToWarpSpeed(warpCode: String) {
   if warpCode == self.warpCode { // Error in Swift 3 unless warpCode is fileprivate

     print("Do it Scotty!")
   }
  }
}

let enterprise = SpaceCraft(warpCode: "KirkIsCool")
//enterprise.warpCode  // error: 'warpCode' is inaccessible due to 'private' protection level
enterprise.goToWarpSpeed(warpCode: "KirkIsCool") // "Do it Scotty!"
그리하여 fileprivate는 코드 구조를 위한 임시목적이라기보단 의도한 목적에 맞게 사용할 수 있다.

API 추가사항들
이제 스위프트4의 반짝반짝 빛나는 새 기능을 살펴보자. 이 변경사항들은 현재 코드를 망가뜨리진 않고 간단하게 추가할 수 있다.

아키브와 시리얼라이즈


지금까지 스위프트는, 커스텀타입을 시리얼라이즈와 아키브하기위해서 여러노력을 했었어야 했다. class 타입의 경우 NSObject를 상속하고 NSCoding 프로토콜을 구형해야했다.

structenum같은 값 타입은 꼼수처럼 NSObjectNSCoding을 확장한 보조 오브젝트를 만들어야 했다.

스위프트4는 세가지 타입 모두 시리얼라이즈 할 수 있게하여 문제를 해결해 주었다 [SE-0166].

struct CuriosityLog: Codable {
  enum Discovery: String, Codable {
   case rock, water, martian
  }

  var sol: Int
  var discoveries: [Discovery]
}

// Create a log entry for Mars sol 42
let logSol42 = CuriosityLog(sol: 42, discoveries: [.rock, .rock, .rock, .rock])
이 예제에서 볼 수 있듯, EncodableDecodable을 위해 Codable 프로토콜만 구현하면 된다. 모든 프로퍼티가 Codable이면 그 프로토콜은 컴파일러예의햬 자동으로 생성된다.

실제로 오브젝트를 인코드하기 위해, 이것을 인코더에 보내야 할 것이다. 스위프트 인코더는 스위프트4에서 활발하게 구현되고있다. 각각의 다른 스킴에따라 오브젝트를 인코딩할 수 있다 [SE-0167]. (주의: 이 제안중 일부는 아직 개발중에 있다)

let jsonEncoder = JSONEncoder() // One currently available encoder


// Encode the data
let jsonData = try jsonEncoder.encode(logSol42)
// Create a String from the data
let jsonString = String(data: jsonData, encoding: .utf8) // "{"sol":42,"discoveries":["rock","rock","rock","rock"]}"
한 오브젝트를 받아서 자동으로 JSON 오브젝트로 인코딩 시킨다. 속성들을 확인하기위해 JSONEncoder를 커스터마이징된 아웃풋으로 만든다.

이 프로세스의 마지막 과정은 데이터를 구체적인 오브젝트로 디코딩하는 것이다.
let jsonDecoder = JSONDecoder() // Pair decoder to JSONEncoder


// Attempt to decode the data to a CuriosityLog object
let decodedLog = try jsonDecoder.decode(CuriosityLog.self, from: jsonData)
decodedLog.sol         // 42

decodedLog.discoveries // [rock, rock, rock, rock]
스위프트4에서의 인코딩과 디코딩은 @objc 프로토콜의 오버헤드나 한계에 의존하지 않고서 스위프트로부터 기대되는 타입 세이프티를 취할 수 있다.

키-값 코딩
지금까지 스위프트에서 함수는 클로저이기 때문에 함수를 호출하지 않은채 참조하여 붙잡아둘 수 있었다. 그렇지만 프로퍼티가 데이터를 잡아두어 실제로 접근하고 있지 않는한 함수를 프로퍼티에 참조하여 잡아둘 수 없었다.

스위프트4에서 아주 흥미로운 추가사항은 인스턴스의 값에서도 get/set으로 타입의 키패스를 참조할 수 있다는 점이다.
struct Lightsaber {
  enum Color {
   case blue, green, red
  }
  let color: Color
}

class ForceUser {
  var name: String
  var lightsaber: Lightsaber
  var master: ForceUser?

  init(name: String, lightsaber: Lightsaber, master: ForceUser? = nil) {
   self.name = name
   self.lightsaber = lightsaber
   self.master = master
  }
}

let sidious = ForceUser(name: "Darth Sidious", lightsaber: Lightsaber(color: .red))
let obiwan = ForceUser(name: "Obi-Wan Kenobi", lightsaber: Lightsaber(color: .blue))
let anakin = ForceUser(name: "Anakin Skywalker", lightsaber: Lightsaber(color: .blue), master: obiwan)
이것은 몇 ForceUser 인스턴스를 그 name, lightsaber, master로 설정하여 생성하는 코드이다. 키패스를 만들기위해서는 필요한 프로퍼티 앞에 백슬래시(\)만 넣어주면 된다.
// Create reference to the ForceUser.name key path
let nameKeyPath = \ForceUser.name

// Access the value from key path on instance
let obiwanName = obiwan[keyPath: nameKeyPath]  // "Obi-Wan Kenobi"
이 예제에서는 ForceUsername 프로퍼티 키패스를 생성했다. 그리고 keyPath라는 새로운 서브스크립트에 이 키패스를 담아서 사용한다. 이제 디폴트에의해 이 서브스크립트는 모든 타입에서 사용할 수 있다.

아래에는 하위 오브젝트 예제, 프로퍼티 설정, 키패스 참조 만들기에대한 여러가지 키패스 사용법 예시가 있다.
// Use keypath directly inline and to drill down to sub objects
let anakinSaberColor = anakin[keyPath: \ForceUser.lightsaber.color]  // blue


// Access a property on the object returned by key path
let masterKeyPath = \ForceUser.master
let anakinMasterName = anakin[keyPath: masterKeyPath]?.name  // "Obi-Wan Kenobi"


// Change Anakin to the dark side using key path as a setter
anakin[keyPath: masterKeyPath] = sidiousan
akin.master?.name // Darth Sidious


// Note: not currently working, but works in some situations
// Append a key path to an existing path
//let masterNameKeyPath = masterKeyPath.appending(path: \ForceUser.name)
//anakin[keyPath: masterKeyPath] // "Darth Sidious"
스위프트의 키패스에서 장점은 강타입이라는 점이다! 더이상 Objective-C의 문자열 스타일로 지저분할 필요가 없다!

다열 문자열 리터럴(Multi-line String Literals)
많은 프로그래밍 언어에서는 일반적인 기능인 다열 문자열 리터럴이다. 스위프트4에서 이것을 추가했으며 세개의 쌍따옴표로 텍스트를 감싸는 형태의 문법이다 [SE-0168].

let star = "⭐️"let introString = """
  A long time ago in a galaxy far,
  far away....

  You could write multi-lined strings
  without "escaping" single quotes.

  The indentation of the closing quotes
      below deside where the text line
  begins.

  You can even dynamically add values
  from properties: \(star)
  """

print(introString) // prints the string exactly as written above with the value of star
XML/JSON을 만들떄나 UI에 보여줄 긴 양식의 텍스트를 만들때 매우 유용하다.

한쪽만의 범위(One-Sided Ranges)
장황한 표현을 줄이고 가독성을 개선하기위해 표준 라이브러리는 한쪽만 표현된 범위로 시작과 끝을 추론할 수 있다 [SE-0172].

한 인덱스에서 시작이나 끝 인덱스까지 범위의 컬랙션을 만들어보면 유용하다는 점을 바로 인지할 수 있다.
// Collection Subscript
var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let outsideAsteroidBelt = planets[4...] // Before: planets[4..<planets.endIndex]

let firstThree = planets[..<4]          // Before: planets[planets.startIndex..<4]
보이는바와 같이, 한쪽만의 범위는 명시적으로 시작이나 끝 인덱스를 지정해줘야하는 필요를 줄여준다.

무한 시퀀스
시작이나 끝 인덱스가 셀 수 없는 타입일때 무한 Sequence를 정의할 도 있다.
// Infinite range: 1...infinity
var numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (8, "Neptune")]

planets.append("Pluto")
numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (9, "Pluto")]

패턴 매칭
한쪽만의 범위를 사용할 때 또다른 좋은 사용 방법은 패턴 매칭이다.
// Pattern matching

func temperature(planetNumber: Int) {
  switch planetNumber {
  case ...2: // anything less than or equal to 2

   print("Too hot")
  case 4...: // anything greater than or equal to 4

   print("Too cold")
  default:
   print("Justtttt right")
 }
}

temperature(planetNumber: 3) // Earth

제네릭 서브스크립트
서브스크립트는 데이터 타입의 접근성을 직관적이게 만드는데 중요한 역할을 한다. 이런 유용함을 증진하기 위해 서브스크립트는 이제 제네릭으로 만들 수 있다 [SE-0148].

struct GenericDictionary<Key: Hashable, Value> {
  private var data: [Key: Value]

  init(data: [Key: Value]) {
   self.data = data
  }

  subscript<T>(key: Key) -> T? {
   return data[key] as? T
  }
}
이 예제에서는 리턴 타입이 제네릭이다. 그리하여 아래처럼 제네릭 서브스크립트를 사용할 수도 있다.
// Dictionary of type: [String: Any]
var earthData = GenericDictionary(data: ["name": "Earth", "population": 7500000000, "moons": 1])

// Automatically infers return type without "as? String"
let name: String? = earthData["name"]

// Automatically infers return type without "as? Int"
let population: Int? = earthData["population"]
리턴 타입을 제네릭으로 하는것 뿐만 아니라 실제 서브스크립트 타입을 제네릭으로 할 수도 있다.
extension GenericDictionary {
  subscript<Keys: Sequence>(keys: Keys) -> [Value] where Keys.Iterator.Element == Key {
   var values: [Value] = []
   for key in keys {
     if let value = data[key] {
       values.append(value)
     }
   }
   return values
  }
}

// Array subscript value
let nameAndMoons = earthData[["moons", "name"]]        // [1, "Earth"]

// Set subscript value
let nameAndMoons2 = earthData[Set(["moons", "name"])]  // [1, "Earth"]
이 예제에서는 두가지 다른 Sequence 타입(ArraySet)을 넣어 각각 값의 배열을 내뱉는다.

잡다한 것
여기까지는 스위프트4에서 큼직한 변화들을 다뤄봤다. 이제는 비교적 작은 부분들을 빠르게 볼 것이다.

MutableCollection.swapAt(_: _:)
MutableCollection은 이제 변경가능한 swapAt(_:_:) 메소드를 가진다. 이 메소드는 이름대로 동작하는데, 주어진 인덱스에 값을 스왑한다 [SE-0173].

// Very basic bubble sort with an in-place swap
func bubbleSort<T: Comparable>(_ array: [T]) -> [T] {
  var sortedArray = array
  for i in 0..<sortedArray.count - 1 {
   for j in 1..<sortedArray.count {
     if sortedArray[j-1] > sortedArray[j] {
       sortedArray.swapAt(j-1, j) // New MutableCollection method

     }
   }
  }
  return sortedArray
}

bubbleSort([4, 3, 2, 1, 0]) // [0, 1, 2, 3, 4]

연관 타입 제약(Associated Type Constraints)
이제 where을 사용하여 연관타입에 제약을 둘 수 있다 [SE-0142].

protocol MyProtocol {
  associatedtype Element
  associatedtype SubSequence : Sequence where SubSequence.Iterator.Element == Iterator.Element
}
프로토콜 제약을 사용하여 많은 associatedtype 선언이 그 값을 제약할 수 있다.

클래스와 프로토콜의 존재
결국 Objective-C에서 스위프트로 만들어진 이 기능은 한 타입이 클래스를 따를 수 있을 뿐만 아니라 여러 프로토콜도 따르면서 정의할 수 있는 기능이다 [SE-0156].

protocol MyProtocol { }
class View { }
class ViewSubclass: View, MyProtocol { }

class MyClass {
  var delegate: (View & MyProtocol)?
}

let myClass = MyClass()
//myClass.delegate = View() // error: cannot assign value of type 'View' to type '(View & MyProtocol)?'
myClass.delegate = ViewSubclass()

@objc 추론을 제한하기
스위프트 API를 Objective-C에서 쓸 수 있게 만들기 위해서는 @objc라는 컴파일러 속성을 사용한다. 대부분 스위프트 컴파일러는 여러분을 위해 추론해준다. 이 추론에서 커다란 세가지 이슈는 다음과 같다.
  1. 바이너리 크기를 상당하게 증가시킬 가능성을 가진다.
  2. @objc가 추론될 때 알고있는 것이 명확하지 않다.
  3. 무심결에 Objective-C selector 충돌을 만들어낼 가능성을 증가시킨다.
스위프트4는 @objc 추론을 제한하여 이 문제를 해결하였다 [SE-0160]. 이 말은 Objective-C의 모든 동적 디스패치 능력을 원할때는 명시적으로 @objc를 사용해야 함을 의미한다.

이런 변화를 만들어야하는 몇가지 예제는 다음과 같다. private 메소드, dynacmic 선언, NSObject 자식 클래스의 메소드.

NSNumber 연결하기
NSNumber와 스위프트의 넘버 사이에는 여러 답답한 동작들이 존재해왔는데, 이 언어에 너무 오랫동안 몰해왔다. 다행히 스위프트4는 이 버그를 잡았다 [SE-0170].

아래에는 그 동작들의 예제를 설명해준다.
let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231
스위프트3에서 일어나는 이상한 이 동작은 넘버가 오버플로우되면 0에서 시작하게된다. 이 예제에선 999 % 2^8 = 231으로 나온다.

스위프트4는 숫자가 담겨진 타입안에 세이프하게 표현될 수 있을 때만 값을 반환하는 옵셔널으로 만들었다.

스위프트 패키지 매니저
지난 몇달동안 스위프트 패키지 매니저에 여러 업데이트가 있었는데, 가장 큰 변화는 다음과 같다.
  • 브랜치나 커밋 해시에서 의존성을 제공한다(Sourcing dependencies from a branch or commit hash)
  • 더 많은 수용할 수 있는 패키지 버전의 조작(More control of acceptable package versions)
  • 더 일반화하여 해결한 패턴으로 직관적이지 않은 핀닝 명령을 교체()
  • 컴펄레이션에 사용될 스위프트 버전 정의 기능(Ability to define the Swift version used for compilation)
  • 각 타겟마다 소스파일의 위치를 지정함(Specify the location of source files for each target)
이것들은 모두 SPM이 되어야할 모습의 큰 발자국이다.SPM은 아직 긴 여정이 남았지만 프로포절에 활발하게 남아서 윤곽을 잡는 일을 도와줄 수 있다.

최근에 다뤄지는 프로포절의 개괄적인 내용을 보려면 Swift 4 Package Manager Update을 확인해보자.

아직 진행중인 것
이 글을 쓰고 있는 시점에서 15가지 수용된 프로포절이 큐에 있다. 무엇이 어떻게 되가고 있는지 슬쩍 보고 싶으면 Swift Evolution Proposals에 들어가서 Accepted로 필터링해보자.

이제 모두 하나하나 따라가보는것 보단, Xcode9의 베타버전에서 업데이트 된 것을 포스팅 할 것이다.

여기서 어디로 까?
스위프트 언어는 지난 몇년간 매우 커지고 성숙해왔다. 프로포절 과정과 커뮤니티 관계는 이 파이프라인을 따라가면 쉽게 변경사항을 확인할 수 있게 만들어왔다. 또한 누구나 쉽게 직접 에볼루션에 영향을 줄 수 있게 해놓았다.쉽게

스위프트4에서 이런 변화들로, 우리는 마침내 ABI 안정성의 모퉁이에 위치하고 있다. 스위프트 버전을 업그레이드하는데 좀 덜 고통스러워지고 있다. 빌드 퍼포먼스와 툴의 기능은 광대하게 개선되고 있다. 애플의 생태계 바깥에서 사용하는 스위프트는 더욱 실용적이게 되어가고 있다. 그리고 생각하건데, 우리는 아마 오직 몇개만 직관적인 구현에서 벗어나는 String의 완전한 재작성일 것이다(And to think, we're probably only a few full rewrites of String away from an intuitive implementation). ;]

스위프트에 더 많은 것들이 들어왔다. 모든 변경사항들이 어떻게 되가는지 최신 정보를 유지하기 위해서, 아래 자료를 확인하면 된다.
스위프트4에대한 여러분의 생각은 어떤가? 여러분이 제일 좋아하는 변경사항은 무엇인가? 아직도 이 언어 바깥에서 미련이 있는 기능은 무엇인가? 여기서 다루지 않은 새롭고 멋진 무언가를 발견하였는가? 아래 주석에 알려주기 바란다!


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Protocols and MVVM in Swift to avoid repetition

우리가 Viable을 최신 iOS 앱의 토대를 만들어갈때, 이전 iOS 앱으로부터 배우려 했다. 우리는 2가지 목표를 정했다.
  • Massive View Controller(MVC를 비꼬는 약자) 증후군 피하기
  • 가능한 적은 중복
초기에 디자인팀이 만든 Viable 화면에는 수많은 비슷한 화면이었다. 아래에 간단하게만든 예시를 한번 보자. 두 화면은 모두 상단에 UILabel이 있고 검색 결과를 보여주는 UITableView가 있다. 각각의 결과에대한 UITableViewCell도 매우 비슷했다. 이들은 다소 레이아웃을 공유했고 데이터만 달랐다.


Viable은 화면에 표시되는 6가지 타입의 데이터가 있었으며, 각 타입마다 새로운 뷰 컨트롤러를 만들어서 코드 중복이 많았다. 그리하여 우리는 6개의 데이터 타입을 모두 표시할 수 있는 SearchResultsViewcController를 만들었다.
데이터 타입에따라 다르게 렌더링하기위해 제일 처음 떠오른 방법으로는, tableView:cellForRowAtIndexPath:에 거대한 if/else문이었는데, 코드 규모가 잘 정연되지 못했고 결국 길고 못난 메소드가 되버렸다.

MVVM와 프로토콜을 사용하여 해결하기
테일러 구이던(Taylor Guidon)은 MVVM(Model-View-ViewModel) 패턴에대한 입문의 글을 포스팅했는데, 여기서 확인할 수 있다. 이 글은 그 요약 버전인데, 데모 프로젝트에 적용한 것을 깃헙에서 확인할 수 있다.

모델(Models)
모델 그룹에서의 모델은 데이터를 담고 있는다. 우리는 DomainModelProductModel을 가지는데, 둘 다 구조체이다. DomainModel은 이름(name)과 그 상태 도메인을 가질것이고, ProductModel은 제품이름(product name), 제품평점(product rating), 제품로고(product logo), 제품가격(product price)을 가진다.
struct Product {
    var name: String

    var rating: Double

    var price: Double?
}

뷰모델(View Models)
모든 데이터 모델은 해당되는 뷰모델을 가진다. 그 말은, 우리 예제에서는 DomainViewModelProductViewModel을 가진다는 뜻이다. 뷰모델은 모델로부터 데이터를 받아서 사용자에게 보여주기전에 뷰에 적용시킨다. 예를들어 ProductViewModel4.99 가격의 부동소수점을 받아서 $4.99라 읽히는 문자열로 변형한다.
class ProductViewModel: CellRepresentable {
    var product: Product

    var rowHeight: CGFloat = 80

    var price: String {
        guard let price = product.price else {
            return "free"
        }

        return "$\(price)"
    }

    init(product: Product) {
        self.product = product
    }

    func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
        // Dequeue a cell

        let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath) as! ProductTableViewCell


        // Pass ourselves (the view model) to setup the cell

        cell.setup(vm: self)

        // Return the cell

        return cell
    }
}

뷰(Views)
우리 예제에서 뷰는 두가지 UITableViewCell이다. DomainTableViewCellProductTableViewCell를 가진다. 레이아웃은 앱의 스토리보드에 만들어놓았따. 두 클래스 모두 간단한데, 뷰모델을 인자로 받는 setup 메소드 하나만 가지고 있다. 뷰모델은 셀에 정보를 옮길때 사용되는데, 예를들자면 읽을 수 있는 가격($4.99)을 받아서 UILabel의 테스트 프로퍼티에 할당한다.
class ProductTableViewCell: UITableViewCell {
    func setup(vm: ProductViewModel) {
        self.textLabel?.text = vm.product.name
        self.detailTextLabel?.text = vm.price
    }
}

합쳐보기
3가지 큰 기둥을 만들었으니 합쳐보자. 뷰 컨트롤러와 뷰모델을 합치기위해 프로토콜을 사용할 것이다. 프로토콜은 이것을 따르는 클래스나 구조체가 어떤 변수와 메소드를 가질지 정의한다. 계약서를 생각해보자. 여러분이 X라는 프로토콜을 따르고 싶다면, 여기에 명시된 모든것을 구현해야한다. 간결하게 만들기위해 한 프로퍼티와 한 메소드만 넣어놨다. DomainViewModelProductViewModel 둘 다 이 프로토콜을 따른다.
protocol CellRepresentable {
    var rowHeight: CGFloat { get }
    func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell

}
스위프트에서 프로토콜은 일급 객체(first class citizen)이므로 SearchResultsViewController 파일은 화면에 표시할때 필요한 뷰모델 배열을 가진다. [DomainViewModel]()이나 [ProductViewModel]()처럼 배열을 초기화하는것 대신, 프로토콜을 사용하여 뷰모델을 담아둘 수 있다. var data = [CellRepresentable](). DomainViewModelProductViewModelCellRepresentable을 따르기 때문에 배열은 둘 다 담아둘 수 있다.

이제 배열에 있는 모든 요소를 CellRepresentable을 따르게하여 UITableViewCell을 반환하는 cellInstance(_ tableView: UITableView, indexPath: IndexPath) 메소드를 가진다고 확신하게 만들자. 고맙게도 tableView:cellForRowAtIndexPath:cellInstance 메소드만 호출하면 된다.
extension SearchresultsViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return data[indexPath.row].cellInstance(tableView, indexPath: indexPath)
    }
}

extension SearchresultsViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return data[indexPath.row].rowHeight
    }
}
이게 전부다. 우리는 다양한 셀의 다양한 열 높이로 표시해주는 작은 뷰컨트롤러를 가지게 되었다! ISL의 깃헙 페이지에서 데모 프로젝트를 확인해볼 수 있다. 제안이나 질문이 있다면 주저하지말고 @thomasdegry에 트윗해달라.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Optimizing your Swift Codebase with Attirbutes

편집자의 노트: 우리 커뮤니티 블로그 포스트 시리즈는 buddybuild 유저와 훌륭한 모바일 개발자들에의해 작성되었습니다. 이 포스트는 Jordan Morgan에의해 작성되었는데, Buffer의 iOS 개발자이다.

스위프트 + 속성들
모든 iOS 개발자들이 모두 여기에 있을거라 생각한다: 우리는 그냥 프로그래밍을 고르거나 어떤것이 이색적인 새로운 언어를 배우기 시작했다. 그리고 몇 코드를 만날 수 있다. 우리가 이해할 수 없을 수도 있지만, 이것이 동작한다고 생각한다. 따라서 그것에대해 좋은 확신을 가지고 계속하면 나아지지 않는다.

이 과정은 정확히 내가 어떻게 내 코드베이스에 스위프트 속성을 넣는것을 시작할수 있는지에 관한 이야기이다. 스위프트는 탄탄하고 다양한 속성들을 지원하는데, 깃헙 저장소를 검색해볼때 우리가 모르는 한두개를 볼 수 있을지도 모르겠다. 나의 경험과 비슷하다면 나중에 구글에 적어두면 당신만의 방법이 된다.

왜 스위프트 속성들을 이용할까?
속성은 우리 코드베이스의 코드 질을 희생시키지 않은채 효율성을 증진시키는데 도움을 준다. 그 코드는 더 읽기 쉬워지고, 컴파일하기 쉬워지고, 궁극적으로 유지보수하기 쉽고 사용하기에 더 안전하게 된다.

개인적인 프로젝트에서나 Buffer에서나 효율성은 항상 나의 iOS 개발의 중심에 있다. 이 포스트는 스위프트 개발자로서 내 효율성을 증진시킬 수 있는 몇가지 스위프트 속성들을 열거한다. 안으로 들어가보자...

기초
수많은 정의를 보기 전에, 속성이 정확하게 무엇인지 빠르게 짚고 넘어가야한다고 생각한다. 그리고 매우, 모든 스위프트의 속성은 타입이나 선언에대한 더맣은 정보를 준다. 이 정보는 어떤것이 메모리에서 어떻게 다뤄질지 컴파일러 경고로부터 모든것을 지시할 수 있다.

어떻게 생겼든, 모두 "@" 표시를 앞에 붙인다. 또한, 속성 선언은 파라미터로 감싸진것 안에 인자를 받을 수 있다.

여기 간단한 것이 있다.
@attributeName

//Or with arguments…
@attributeName(arguments)
아래 몇가지 예시를 보자.

속성들
@available(args) 속성
스위프트 속성중에 맥가이버칼은 @available()이다. 강력한만큼 유연하고, 매번 API를 관리하고 배포하는 입장이라면 반드시 필요한 것임을 발견할 수 있다. 이것으로는 API 네이밍이 바뀌었다는 것을 지시할 수 있고, 가능한 플랫폼을 지시하는 등을 할 수 있다.

블로그 포스트를 올리는 은유적인 API로부터 나온 오브젝트를 생각해보자.
class BasicPost {}
우리 API를 쓰는 사람들은 오랫동안 BasicPost 클래스를 즐겨 사용한다. 그러나 기술 블로그 포스트를 표현하는 오브젝트에서 더 원하는 몇몇 요청을 처리한다고 생각해보자. 여러분이 읽고 있는 것과 대체로 같다. 그러니 버전1.2에서 이것을 소개하였다.
class TechnicalPost {}
이제 우리 문서를 완성하기위해서,  이것이 존재한다는것을 알리기위해 @available() 이점을 취함으로서 API 사용자에게 정보를 제공하고 코드는 분별력있게된다.
@available(*, introduced: 1.2)
class TechnicalPost {}
이 특정 속성은 몇몇 이자를 받을 수 있지만, 첫번째 인자는 항상 의도하는 플랫폼을 나타낸다. 나머지 인자들은 그 순서에따라 공급될수 있게 지원한다.

와일드카드의 이점을 얻을 수도 있다. 이 경우, 와일드카드는 첫번째 인자로 들어가있는 별표이다. 이것은 사용하는 모든 플랫폼의 API에서 소통한다. 이 클래스는 버전1.2에서 처음 소개되었다(두번째 인자가 설명하는 바).

깔끔하지만, 꽤 길다. 고맙게도 더 단축된 문법으로 초점을 맞출 수 있다.
@available(iOS 10.0, macOS 10.12)
class TechnicalPost {}
더 낫다! 이 속성에대해 잘 알지 못하더라도, 이제 우리가 만들어낸 API는 이 클래스가 사용될때 명확하게 볼 수 있다.

그러나 그대로 두면 컴파일타임 에러가 나올 것이다. 왜일까?

애플은 새로운 플랫폼을 소개하는 경향이 있는데, 우리는 우리 코드에 이것을 설명해야한다. 그러기위해서 이 코드는 제공된 플랫폼과 다른 잠재적인 미래의 플랫폼에 사용가능하다는 마지막 인자로서 와일드카드를 넣는다.
@available(iOS 10.0, macOS 10.12, *)
class TechnicalPost {}
이 방법은 여러분의 코드가 다음 애플의 계획에대해 이미 준비해 놓았다는 뜻이다. For now though, 애플은 오늘날에 존재하는 각 플랫폼을 표현한 것들을 나열하여 제공하고 있따.
  • iOS
  • iOSApplicationExtension
  • macOS
  • macOSApplicationExtension
  • watchOS
  • watchOSApplicationExtension
  • tvOS
  • tvOSApplicationExtension

다음 속성으로 가보기 전에, 다른 공통성을 생각해보자. 최근의 변경에서, 우리 API가 쓰지않게 되었고, 세상에있는 iOS 개발자들은 기술 블로그 포스트를 표현하는 사랑스러운 약간의 JSON으로 제공하도록 책임을 맡게되었다.

이처럼, 원래 클래스는 더이상 필요없게 되었다. 그리고 디프리케이트시킬 시간이 된 것이다.
@available(*, deprecated: 1.3)
class BasicPost {}
한 스위프트에서 모든 플랫폼에서 BasicPost를 디프리케이스 시켜, 와일드카드 인자가 유용해질 수 있게 명확해졌다.

게다가 이것으로 계속하면서 조금씩 리팩토링 하고 싶으면, API 네이밍 변경사항을 알도록 제공해야 할것이다. 내가 애플로부터 발견한 기술의 예의는, ㅈ사용자들이 사용하기 더 쉽게 만들기 위해 사용불가능한 인자와 타입에일리어스로 짝을 지을 수 있다는 것이다.
//From an earlier API version
class BasicPost {}

//From a new API version, where we renamed it for whatever reason
class BaseTechnicalPost {}

@available(*, unavailable, renamed: “BaseTechnicalPost”)
typealias BasicPost = BaseTechnicalPost
개인적으로 이것을 좋아하는데, 효율성 관점에서나 비용적 관점에서, 가장 명확한 코드는 항상 최고의 코드이다.

이 속성은 워닝, 에러 등을 연결하여 보다 트릭적인 메시지를 제공한다(여기서 '트릭적인'은 옛 코드를 인자로 지정하는 것을 의미함).

@discardableResult 속성
성숙하고 물려받은 코드베이스에서 작업하고 있다면, 몇몇 함수가 약간 장황하게 되있는것이 놀랍지 않을 수 있다.

아마 90년대 초반부터 만들어진 기능이 지금까지 계속 추가되고 바뀐 코드를 물려받았으며 기능 가체에 결함이 없었을 것이다. (여기엔 소프트웨어가 켜질때 일어나는 124가지의 중요한 작업들이 있을 수 있다. 데이터베이스 접근, 케스 설정, 프로세스에서 몇몇 사인을 초기화하기 - 시나리오는 끝도 없다)

그 전까지, 당신이 돌아와서 필요한 리팩토링을 할 수 있게 프로덕트 매니저를 설득하고 있다.

그렇다. 그런가?! 그렇다.
let someUnusedVarBecauseIHaveToCallThisOldInsaneFunction = anOldInsaneFunction()
그러나 이상한 커플링 이유를 위해 이 함수를 실행하게 되버려서 clang은 이것들을 더 컴파일할 것이다. 이제 우리는 이것을 위해 보여주기위해 사용하지 않는 변수도 가진다. 아래로 내려가면서 이야기해보자.

필요 없을것같은 함수의 결과를 컴파일러에게 말하여 @discardableResult 속성이 도움이 될 수 있다. 컴파일타임에 경고도 없애준다.
@discardableResult func anOldInsaneFunction() -> String{
   //Bunch of business logic occurs
   return “”
}
이제 불러진 함수를 호출한 저 코드는 과거의 소프트웨어 공학적 실수처럼만 남아있을 것이다. 그러나 에러 없이 되고 있을 것이다.

명확성을 추가하기위해 간단하게 _를 할당하여 좀 더 명확한 상태로 만들 수 있다.
_ = anOldInsaneFunction()
때론 소프트웨어 개발에선 우리가 직접 제어하거나 고칠 수 없는 함수나 아키텍처가 있으며, 이 속성이 그런 상황을 조금이라도 더 낫게 해준다.

@autoclosure 속성
@autoclosure는 여러분의 코드베이스에 손쉽게 명확성을 추가할 수 있는 또다른 속성이다. 이것은 인자로 공급된 클로저를 자동으로 감싼다. 클로저가 아무 인자도 스스로 받으면 이것으로 감싸인 표현식의 실제 값을 반환할 것이다.

보기에 좀 혼란스러울 수 있는데, 한번 만나보면 쉽게 이해된다. 고수준 관점에서, 자동으로 클로저로 만들기위해 표현식을 얻는 능력에대해 이야기하고 있다. 여러분이 만약 프로젝트에 유닛테스트를 넣으려 해보았다면, 이미 이 속성들을 몇번 만나보았을지도 모르겠다.

아래처럼 클래스에 간단한 테스트를 넣고 싶다고 가정해보자.
class Programmer  {
   var pay:Int
   init(withPay pay:Int)
   {
       self.pay = pay
   }

   func applyRaise(by amount:Int)
   {
       self.pay += amount
   }
}

class ProgrammerTests: XCTestCase
{
   func testPayRaise()
   {
       let devsPay = 50000
       let raiseAmount = 25000
       let expectedSalaryPostRaise = devsPay + raiseAmount

       let aDev = Programmer(withPay: devsPay)
       aDev.applyRaise(by: raiseAmount)

       XCTAssertEqual(expectedSalaryPostRaise, aDev.pay, "Unexpected salary after raise was applied.")
    }
}
XCAssertEqual의 앞에 두 파라미터는 둘다 클로저인데, 이 클로저는 제네릭 표현식으로 받는다. 함수의 시그니처가 약간 위협적으로 보이지만, 처음 두 파라미터는 @autoclosure의 이점을 취하였다는 것을 인지하자.
func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> T?, _ expression2: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = default, file: StaticString = file, line: UInt = line) where T : Equatable
@autoclosure 속성이 제공되었기 때문에, 함수를 호출하는 것이 꽤 가독성 좋고 우리에게도 쉬워졌다. 클로저를 값처럼 무언가를 간단하게 전달하거나(이전 예제에서 했던 것처럼) 약간의 로직을 더 전달할 수 있고, 각각은 가볍게 사용할 수 있다.
class ProgrammerTests: XCTestCase
{
   func testPayRaise()
   {
       let devsPay = 50000
       let raiseAmount = 25000

       let aDev = Programmer(withPay: devsPay)
       aDev.applyRaise(by: raiseAmount)

       XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
   }
}
첫번째 인자가 제공될때, 클로저라기보단 추가적인 연산처럼 읽힌다.
XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
반대로 @autoclosure 속성이 없을때는 어떻게 생겼을지 보자.
XCTAssertEqual({
     return aDev.pay + raiseAmount
,}, {
   return 75000}
, "Unexpected salary after raise was applied.")
여러분도 볼 수 있듯, (문법대로) 완전한 클로저를 전달한다. 읽고 쓰기에 좀 많은 편이다. 하나가 마지막 인자로 후행 클로저(trailing closure)를 사용할 수 없으면, 복합적인 문제가 될 수 있다.

@autoclosure를 사용하면 그 안에 랩핑된 실제값을 반환한다. 아마 자동으로 파라미터가 클로저가 된다고까지 말할 수 있으므로... @autoclosure이다!

이 코드는 상속적으로도 지연된다. 실제 클로저가 결국 무거운 작업을 하고 있거나 의도치않은 사이트 이팩트를 낳았다면 추가적인 이점이 있다. 제공된 코드는 안에 감싸인 클로저가 되기 전까지 절때 실행되지 않는다.

더 나아가서, 여러분의 최근 iOS 작업물의 어디에서 보았을까? assert()는 어떨까?
struct Programmer
{
   var isSenior:Bool
   var appsShipped:Int
}

let aSeniorDev = Programmer(isSenior: true, appsShipped: 13)

assert(aSeniorDev.isSenior, “This dev isn’t a senior!”)
제공된 첫번째 인자는 @autoclosure를 사용한다. 그렇지 않았다면 이렇게 호출해야 했을 것이다.
assert({
     return aSeniorDev.isSenior
}, {
    return “This dev isn’t a senior!”
})
@autoclosure를 사용하면, 코드는 작성할때 좀 더 쉬워지고, 즐겁게 코드를 읽게 만드는 경험을 제공한다고도 말할 수 있겠다.

그리고 assert() 시그니처가 어떻게 생겼는지 궁금하다면, 이렇게 생겼을 것이다.
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = file, line: UInt = line)
우리는 이 시그니처에서 두가지 파라미터로부터 벗어나면, 여기에 할당된 디폴트 값이 있기 때문에 예제에서 이것들을 생략한다. 여러분도 알다시피 그렇지 않을까?

여러 속성사용하기
각 속성은 그들이 가지고 있는 것으로 도와줄 수 있는 이점을 가지지만, 어떤 시나리오에서는 이것을 같이 사용하는 것도 도움이 된다.

예를들어, @escaping 속성을 보자. 이 @escaping 속성은 클로저에 들어온 것이 보냈던 함수보다 오래 살 수 있게 해준다.
//A property on a view controller
var onFakeCompletions:[()->()] = []

func fakeNetworkOp(_ completion:@escaping ()->())

{
   //Network stuff happens
   //The closure is appended to an external array outside of the function's scope. This implies it could be invoked outside of the function - i.e., it could "escape" it
   onFakeCompletions.append(completion)
}
이것을 고려하여 같은 파라미티터에 @escaping@autoclosure를 둘다 쓸 수 있다. 예제로 인사부(H.R.)를 생각해보자.  예를들어 임금 인상을위해 직급이 "Senio"인 개발자이면서 적어도 세개의 앱을 출시한 개발자가 누구인지 알려주는 인사부(H.R.)를 생각해보자. 이 개발자는 이력적인 이유로 각각의 평가를 계속 추적해야한다(As an example, let's imagine H.R. let us know that any developer who is both a "Senior" in title and has shipped at least three apps is due for a raise, but we also need to keep track of each evaluation for historical purposes).
class Programmer
{
   var previousPayRaiseEvaluations:[()->Bool] = []
   var isSenior:Bool = false
   var appsShipped:Int = 0

   func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
   {
       if raiseEvaluation()
       {
           //Give them a raise, and then save it to their records
           previousPayRaiseEvaluations.append(raiseEvaluation)
       }
   }
}

let aProgrammer = Programmer()
aProgrammer.isSenior = true
aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1

   func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
   {
       if raiseEvaluation()
       {
           //Give them a raise, and then save it to their records
           previousPayRaiseEvaluations.append(raiseEvaluation)
       }
   }
}

let aProgrammer = Programmer()
aProgrammer.isSenior = true

aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1
확실히 속성들을 함께 "채이닝(chaining)"하는 것을 금지하지 않는다. 그리고 그 상황을 위해 호출할때 꽤 매끄럽게 동작한다.

마지막 생각
속성은 항상 내 코드에서 사용에대해 특별히 열정적으로 해왔던 것이다. 나는 명확하고 정확하고 간단한 방법으로 강력하게 무거운것을 들어올리는 아이디어를 좋아한다. 그리고 이것이 정말로 속성이 하는 일이다. 물론 스위프트와 Objective-C 프로젝트 어디에나 있는 @objc 속성같은 알만한 가치가 있는 것들도 있다.

이것을 고려하여, 한 인자는 속성이 nicety 보다는 necessity에 더 가깝게 만들어질 수 있다. 끝으로 개발자로서 이 모든것들은 여러분의 작업 플로우의 최적화에대한 것이다. 여러분의 코드베이스나 다른 곳에 이렇게 만들어라. 속성은 이런 최적화를 달성하기위한 하나의 방법에 불가하다.

앱이 완성되고나면, buddybuild 같은 서비스를 사용하여 여러분의 지속적 통합(CI, Countinuous Integration)과 배포 과정같은 부담되는 일을 자동화할 수 있겠다.

Buffer에서는 이미 우리 개발 작업 플로우를 최적화하기위한 방법을 항상 찾아보고있고, 이것들은 우리를 도와줄 수 있는 방법 중 우리가 찾은 것이다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: On Comments

코드를 작성할때 가장 큰 적은 복잡성이다. 추상화 수준(level of abstraction)을 유지하고, 코드베이스에서 작업하는 개발자들은 그 개념으로 동작하게 하는 것은 큰 소프트웨어 프로젝트에서 필수적이다.

주석는 복잡성을 유지보수하는 도구이기도하지만, 때로는 돕는게 아니라 미묘하게 코드베이스를 상하게 만들기도 한다.

주석의 내 견해는 두가지로부터 온다. 1) 주석은 컴파일하지 않는다. 2) 주석은 연하게 문법 하이라이트된다. 이것은 컴파일 되지 않고 시아에서 흐릿하게 보이기 때문에 코드에 변경사항이 생길때 무시되기 쉽다. 코드는 변경했는데 주석은 바뀌지 않는다면, 코드의 내용을 정확하게 반영하지 않은 주석과 함께해야한다.

이상적인 세상에서는 코드 리뷰때 이것을 발견할 수도 있지만, 실제로는 절때 발견하지 못할때도 있다. 코드 리뷰 툴이 문법 하이라이트 기능을 가지고 있더라도 주석은 그 문맥에서 백그라운드로 사라지게된다. 또한, 코드 리뷰는 변경된 부분의 주변만 보여주기 때문에, 오래된 주석은 그 자체가 변경될 쯤에만 발견될 수 있을 것이다. 만약 메소드의 전제조건을 변경하여 한 라인을 바꿨다면, 리뷰어는 주석을 보지 못한채로 바꿔라고 말하지 못할 것이다.

일반적인 주석을 피하는 몇가지 방법이 있다. 그 중 몇개는 a blog post by an old manager of mine에서 다루고, 나머지는 Andrew에서 나온 것인데, 루비에서 나온 것이므로 다이나믹 타입 언어에대한 이야기여서, 적용시킬 수 없다.

  1. 이름을 잘 정하자! 주석을 피하는 첫번째 방법은 다음과 같다. 한 단어 이름, 추상적인 이름, 모호한 이름을 피하자. 여러분이 할 수 있는 만큼 더 정확하게 이름을 정할수록 주석을 달 필요가 줄어든다.
  2. 메소드에 전제조건이 있으면 유효하지 않은 값이 들어올때 앱이 크래쉬내는 assertion를 넣자(항상 그러지는 말고 디버깅때만!). 만약 양수지만 0이아닌 정수만 받는다면 코드에 precondition(int>0)으로 작성할 수 있다.
  3. 런타인 assertion보다 나은 것이 컴파일타임의 것이다. 만약 메소드가 비어있지 않는 배열만 받는다면 precondition(!array.isEmpty)를 쓸 수 있다. 그러나 절때 빈 배열이 되지 않는 타입을 사용하는 것도 하나의 방법이다. 여러분의 API의 사용자들은 이제 절때 파라미터에 빈 배열을 보낼 수 없을 것이다.같은 성질에서, 두 이름을 case를 가진 열거형으로 표현된 것보다 더 잘 표현된 불(bool) 파리미터를 가져본 적이 있는가? 다른 열거형처럼 여러분의 옵셔널이 더 잘 표현되는가?  여러분의 의도를 네이밍에 드러내라.
  4. 임시 코드, 프로토타입 코드에 표시를 하자. 나는 종종 이 함수가 이상적으로 짜지지 않았으면 hack_를 표시한다. 스위프트에서 메소드의 언더바는 내 코드베이스를 좋지않게 만들어 괴롭히므로 보기에 거슬린다.그리고 이것을 상기시켜 고치게 만든다. 우리는 최근에 shouldReallyBeInTheCoordinator_를 접두에 붙인 함수를 만들었는데, 코드리뷰를 받아야야할때 코드가 올바른 클래스에 있지 않았기 때문이다. 올바르지 않은 코드가 거슬리게 생긴다면, 코드베이스 요구와 여러분의 감정이 드러맞게 된다. 또다른 좋은 접두에는 perf_temp_ 같은 것들이 있다.
  5. Mark Sands에 의하면, ID를 버그 추적기에서 메소드 이름으로 인코드 할 수 있고, 이것은 스택 트레이스(stack traces)에서 나타날 것이다. UIKit은 몇 케이스에서 레이더 넘버(Radar Number)를 참조한다. 이것은 현실이다.
    -[UIViewController _hackFor11408026_beginAppearanceTransition:animated:]
  6. 함수의 이름이 "왜" 이것인지 설명하는 것을 두려워하지마라. updateFrameOnNextTickBecauseAutoLayoutHasntCompletedYet(frame: CGRect)처럼 함수를 만들 수 있다. 컴파일러는 메소드 길이를 신경쓰지 않으며 코드는 작성하는 수보다 읽히는 횟수가 더 많다. 주석은 단지 단어들이며 메소드 이름이다. 그 코드베이스의 미래 유지보수 담당자는 여러분의 장황한 설명에 감사할 것이다.
  7. TODO(date: Date, message: String) 같은 보조함수(helper function)을 만들어라. 여기서 TODO는 어떤 날짜를 정하지 않으면 에러를 출력한다. (혹은 디버깅에서 크래쉬를 내는게 더 나을 수도 있다) Jordan Rose의 또다른 예제이다.
  8. 테스트에 어떤 알고리즘 요구사항을 인코드하라. 만약 위의 모든것이 실패하고, 특정 문제를 풀기에 precondition, 타입, 메소드이름에 의존할 수 없다면, 테스트를 작성하라. 이것은 특히 엣지 케이스에 좋다. 누군가 코드를 다시 작성했는데, 테스트에 실패한다면 새로운 코드안에 다뤄줘야할 케스이가 있다는 사실을 알게될 것이다.
기억하기: 받아드리기 힘든 코드를 작성하거나 주석이 없는 코드는 변명할 수 없다! 주석을 스킵한 코드는 반드시 명확해야한다. 결국에는 나를 위한 주석이다. 다음 프로그래머에게 내 의도를 표현할 수 있는 또다른 방법을 찾을 수 있다면 주석을 달지 않을 것이다.

더 읽을거리


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Swift: UserDefaults Protocol


스위프트3은 언어뿐만아니라 우리 코드베이스까지도 쓰나미같은 변경이 생겼는데, 이 글을 읽는 몇몇은 아직도 마이그레이션과 투쟁하고 있을지 모르겠다. 그러나 이런 모든 변경에도 stringly typed의 Foundation으로된 몇몇 API들이 남을 것인데, 이는 꽤 괜찮아 보이지만... 그렇지 않을 수도 있다.

이것은 일종의 애증관계인데, API에서 문자열의 유연성은 '애'이나, 그들이 가져오는 상속적인 이유때문에 이것을 사용해야 하는 것을 '증'한다. 신경쓰지 않으면 위험하게 작업하고 있는 것과 동일하다.

Foundation 프레임워크를 만드는 사람들은, 우리가 의도한대로 정확하게 미리 정의할 수 없게 해놓아서 우리는 stringly typed API를 쓸 수 있게 되었다. 그래서 그들의 모든 지혜로움, 능력, 지식으로 개발자로서 무한한 가능성으로 만들 수 있게 하려고 몇몇 API에서는 문자열을 사용하도록 해놓았다. 이것은 어둠의 비밀의 마법이다. (So in all their wisdom, power and knowledge, they decided to use strings in some of the APIs because of the unlimited possibilities it creates for us as developers. It’s either that or some type of dark arcane magic.)
(역자: stringly 타입의 API로 유연하게 사용할 수 있게 해놓은 장점을 말하는 중입니다.)

UserDefaults
오늘의 주제는 내가 iOS 개발을 하면서 배울때 처음으로 친숙해진 API 중 하나이다. 이것이 익숙하지 않는 사람들을 위해 설명하자면 UserDefaults는 한 이미지나 어플리케이션 세팅같은 작은 정보를 저장하지위한 간단한 영속 데이터 저장소이다. 어떤 사람들은 이것을 "다이어트한 코어데이터"라고 생각하기도하지만, 사람들이 그것의 대체물로 만드려 아무리 노력해도 이것은 견고하지 않다.

Stringly Typed API
UserDefaults.standard.set(true, forKey: “isUserLoggedIn”)
UserDefaults.standard.bool(forKey: "isUserLoggedIn")
일반적으로 앱에서 UserDefaults는 앱의 어디에서라도 값을 간단하게 영속적으로 저장(set)하고, 검색(retrieve)하며, 덮어쓰거나(override), 제거하는(remove) 할 수 있다. 그러나 조심하지 않으면 균일성이나 문맥이 없는채로 해볼 순 있겠지만 오타를 칠 가능성이 높아질 것이다. 이 포스팅에서는 UserDefaults의 일반적인 특징을 변형하여 커스터마이징 할 것이다.

상수를 이용하기
let key = "isUserLoggedIn"

UserDefaults.standard.set(true, forKey: key)

UserDefaults.standard.bool(forKey: key)
이런 이상한 트릭을 따라하면 일시적으로는 더 나은 코드를 작성할 수 있을 것이다. 문자열을 한번 이상 쓴다면 상수로 바꿔서 쓰는 규칙을 적용시켜보자. 아마 나에게 고마워 할지도 모르겠다.

그룹 상수
struct Constants {
    let isUserLoggedIn = "isUserLoggedIn"
}
...
UserDefaults.standard
   .set(true, forKey: Constants().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants().isUserLoggedIn)
균일성을 유지하기에 더 도움이 되는 방법은, 한곳에 중요한 디폴트 상수를 모아놓는 것이다. 그래서 여기에 디폴트를 저장하고 참조할 수 있는 Constants 구조체를 만들었다.

또다른 좋은 팁에는, 디폴트로 작업할 때 특히 프로퍼티 이름에 그 값을 반영해놓는 것이다. 이렇게하면 코드를 단순화 시켜주고 전반적인 속성을 더 균일화시켜줄 것이다. 프로퍼티 이름을 복사하고 문자열 안에 붙여넣으면 타이핑을 줄일 수 있을 것이다.
let isUserLoggedIn = "isUserLoggedIn"

문맥 추가하기
struct Constants {
    struct Account

        let isUserLoggedIn = "isUserLoggedIn"
    }
}
...

UserDefaults.standard
  .set(true, forKey: Constants.Account().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account().isUserLoggedIn)
단지 Constants 구조체를 가지는 것만으로도 괜찮겠지만, 코드를 작성할 때 문맥을 제공해야함을 잊어서는 안된다. 여러분 자신을 포함한)함께 작업할 누군가에게 더 읽기 좋은 코드를 만들어야함을 목표로 하는것이 좋다.
Constants().token // Huh?
token의 의미가 무엇일까? 문맥에서 네임스페이스가 없기때문에, 이 코드베이스에 익숙하지 않은 누군가(혹은 미래의 이 코드를 관리하는 사람)가 token이 무엇인지 알아내려할때 고생할 것이다.
Constants.Authentication().token // better

초기화 피하기
struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }

    private init() { }
}
절때로 우리가 의도하지도 않았고 우리 Constants 구조체를 초기화시키고 싶지 않기 때문에, 생성자는 private로 선언되어야한다. 이거은 좀 더 예방적인 단계이지만 계속 추진하고 있는 방법이다. 최소한 적어도 static만 원할때도 실수로 인스턴스 프로퍼티를 선언하는 것을 막아줄 것이다. 그러나 static에 관해 말하자면... 다음을 보자.

static 변수들
struct Constants {
    struct Account
        static let isUserLoggedIn = "isUserLoggedIn"
    }
    ...
}
...

UserDefaults.standard
  .set(true, forKey: Constants.Account.isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account.isUserLoggedIn)
키에 접근할때마다 주의해야하는 것이 있는데, 접근할때마다 이것이 속한 구조체를 초기화해야할 수 있다. 그러지말고 static 선언을 사용하면 한번만 초기화한다.

구조체를 저장 타입으로 정했기 때문에 class대신 static을 사용한다. 스위프트 컴파일러 법에 따르면 구조체는 class 프로퍼티 정의를 사용할 수 없다고 한다. 또한 class 프로퍼티에 static 선언을 사용하면 그 프로퍼티는 final class로 선언한 것과 같다.
final class name: String

static name: String
// final class == static

열거형 케이스로 더 적게 타이핑하기
enum Constants {
    enum Account : String {
        case isUserLoggedIn
    }
    ...
}
...

UserDefaults.standard
    .set(true, forKey: Constants.Account.isUserLoggedIn.rawValue)
UserDefaults.standard
    .bool(forKey: Constants.Account.isUserLoggedIn.rawValue)
이 포스트의 초반부에서 말했듯, 균일성을 위해 프로퍼티는 그 값을 반영해야한다고 했었다. 여기에 static let 대신 enum case를 써서 그 과정을 자동화하여 한걸을 더 나가볼 것이다.

여러분도 인지했듯, 우리는 String을 따르는 Account열거형을 만들었는데, 이것은 RawRepresentable 프로토콜을 따른다. 이렇게 한 이유는, case를 위한 rawValue를 제공하지 않으려면 디폴트로 케이스가 반영될 것이기 때문에 이 작업을 한다. 우리가 해야할 타이핑이나 복사/붙여넣기를 줄이면 더 편해질 것이다.
// Constants.Account.isUserLoggedIn.rawValue == "isUserLoggedIn"

위에는 지금까지 UserDefaults로 꽤 괜찮은 것들을 달성했지만, 멋진것을 했다고 하기엔 좀 부족해 보인다. 가장 큰 문제는 문자열을 입고 있을지라도 여전히 stringly typed API로 작업하고 있어서 여전히 우리 프로젝트에 문제가 생길 수 있다는 점이다.

우리는 주어진 것으로만 작업할 수 있다는 마음을 가진다. 스위프트는 아주 많이 멋진 언어이고, 우리가 배워왔던, 그리고 Objective-C를 작성해가면서 알고 있는 많은 것들을 도전해볼 수 있다. 이 API에 문법 슈거를 만들어보자.

API 목표
UserDefaults.standard.set(true, forKey: .isUserLoggedIn)
// APIGoals
남은 이야기에서는 일반적인 populus 대신, UserDefaults와 소통할때 더 괜찮게 작업할 수 있는 API를 우리 필요에 맞게 만들어보려 할 것이다. and what better way than to do so than making extensions with protocols.

BoolUserDefaultable
protocol BoolUserDefaultable {
    associatedType BoolDefaultKey : RawRepresentable
}
불리언 UserDefaults를 위한 프로토콜을 만들면서 시작해보자. 변수나 함수가 없는 간단한 프로토콜이다. 그러나 RawRepresentable을 따르는 BoolDefaultKey라는 associatedType을 지원하는데 왜 이렇게 했는지는 바로 다음에 이해할 수 있을 것이다.

익스텐션
extension BoolUserDefaultable
    where BoolDefaultKey.RawValue == String { ... }
만약 Crusty's Laws 프로토콜을 따르려는 계획이라면 프로토콜 익스텐션을 선언할 수 있다. 그러나 associatedTyperawValueString 타입이라는 익스텐션에만 제약하는 where절을 적용시켰다.
모든 프로토콜로, 동등하고 해당되는 프로토콜 익스텐션이 있다 - Crusty's Third Law
With every protocol, there is an equal and corresponding protocol extension

UserDefaults 세터
// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
}

static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
}
그렇다. 이것은 표준 UserDefaults를 감싼 간단한 API이다. 이렇게 하는 이유는 Key-Path로된 문자열을 보내는것 보다 간략한 enum case를 보내는게 가독성면에서 더 좋기 때문이다.
UserDefaults.set(false,
    forKey: Aint.Nobody.Got.Time.For.this.rawValue)

프로토콜 따르기
extension UserDefaults : BoolUserDefaultable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
}
우리는 BoolDefaultable을 따르게 하기 위해 UserDefaults를 익스텐션하고 RawRepresentable (String)을 따르는 BoolDefaultKey라는 연관타입을 지원했다.
// Setter

UserDefaults.set(true, forKey: .isUserLoggedIn)

// Getter

UserDefaults.bool(forKey: .isUserLoggedIn)
다시 말하자면, 작업하는 표준에 우리것을 정의하는 것 대신 우리가 지원한 API로 도전하는 중이다. UserDefaults를 익스텐션하면서 우리 API와함께 문맥을 잃어버리기 때문이다. 만약 .isUserLoggedIn 말고 다른 키 였다면 무엇과 관련되었는지 이해할 수 있었을까?
UserDefaults.set(true, forKey: .isAccepted)
// Huh? isAccepted for what?
이 키는 매우 모호해서, 모든 범주의 어떤것이든 될 수 있다. 이것처럼 보이지 않더라도 문맥을 제공하는 것은 항상 유익할 것이다.
필요하지만 가지지 못한것 보다는, 필요없더라도 가지고 있는 편이 낫다.
어렵게 생각하지 말자. 문맥을 추가하는 것은 쉬운 일이다. 간단하게 키를 위한 네임스페이스를 만든다. 이 경우, isUserLoggedIn 키가 있는 곳인 Account 네임스페이스를 만들었다.
struct Account : BoolUserDefaultable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn

    }
    ...
}
...
Account.set(true, forKey: .isUserLoggedIn)

충돌
let account = Account.BoolDefaultKey.isUserLoggedIn.rawValue
let default = UserDefaults.BoolDefaultKey.isUserLoggedIn.rawValue
// account == default
// "isUserLoggedIn" == "isUserLoggedIn"
같은 프로토콜을 따르고 같은 키 케이스를 제공하는 서로다른 두 타입을 가지는 것이 가능하다. 이걸 출시하기 전까지 해결하지 못하면 분명 이것이 새벽에 우리를 깨우는 버그가 될것이다. 다른 값을 바꾸는 키를 가지는 위험을 안고 갈 수 없다. 그러니 우리 키를 네임스페이스한 것으로 만들자.

네임스페이스로 만들기
protocol KeyNamespaceable { }
물론 우리는 스위프트 개발자니까 프로토콜을 만든다. 프로토콜은 우리가 직면한 문제를 풀때 제일 먼저 하게되는 시도일 것이다. 만약 프로토콜이 초콜릿 소스라면, 심지어 스테이크에까지도 어디든지 올려놓을 수 있다(역자: 왜하필 초콜릿 소스에 비유를 했는지.. 다목적 소스라면 역시 굴소스 아닌가요?). 이것이 우리가 프로토콜을 만들어가며 개발하는게 얼마나 좋은지 보여준다.
extension KeyNamespaceable {
    static func namespace<T>(_ key: T) -> String

    where T: RawRepresentable {
          return "\(Self.self).\(key.rawValue)"
    }
}
이 간단한 함수는 두 오젝트를 합친 문자열 보간법을 쓰고, 그 사이에 마침표로 구분했다. 클래스의 이름과 그 키의 rawValue이다. 이 함수는 RawRepresentable을 따르면 key 인자로 받을 수 있게 제네릭을 인자로 받도록 해놓았다.
// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = namespace(key)

    UserDefaults.standard.set(value, forKey: key)
}

static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = namespace(key)

    return UserDefaults.standard.bool(forKey: key)
}
...

let account = namespace(Account.BoolDefaultKey.isUserLoggedIn)

let default = namespace(UserDefaults.BoolDefaultKey.isUserLoggedIn)


// account != default

// "Account.isUserLoggedIn" != "UserDefaults.isUserLoggedIn"

문맥
우리가 이 프로토콜을 만들었기 때문에, UserDefaults API 사용으로부터 해방된 느낌을 받고, 아마 프로토콜의 힘에 취했을 것이다. 이렇게하여 우리은 키를 우리가 원하는 곳으로 옮겨 문맥을 만듦으로서 코드를 읽을때 이해할 수 있게 되었다.
Account.set(true, forKey: .isUserLoggedIn)
그러나 API가 완전히 이해되지 않게 문맥을 잃어버리기도 했다. 처음 보면 이 코드가, 불리언을 영속적으로 저장시키는지 아니면 UserDefaults에 넣는지에대한 아무런 정보도 주지 않는다. 따라서 모든 사이클을 보여주기위해 UserDefaults를 익스텐션하여 우리의 디폴트 타입을 그 안에 넣을 것이다.
extension UserDefaults {
    struct Account : BoolUserDefaultable { ... }
}
...

UserDefaults.Account.set(true, forKey: .isUserLoggedIn)
UserDefaults.Account.bool(forKey: .isUserLoggedIn)


NatashaTheRobot에게 감사의 말을 전한다. 9월에 try! Swift NYC에서 발표할 기회를 얻었었다. 내 발표가 녹화되어 Realm에서 이것을 남겨두었고 Speaker Deck에 슬라이드 자료가 있으니 확인해보자. 발표를 한 이례로 몇가지 배운점을 이 글에 반영했으며, 샘플코드는 Gist나 Playground
에 있다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
제목: Fun with String Interpolation

Update: 2017.02.02 API 설계 가이에따라 escape(unsafe:)가 escaping(unsafe:)로 바뀌었다.

스위프트 프로그래머가 제일 처음 배우는것 중 하나가 문자열 보간(string interpolation)이거나, 혹은 변수와 수식을 리터럴 문자열로 만드는 것이다.
let a = 6
let b = 12
let message = "\(a) × \(b) = \(a * b)"
// → "6 × 12 = 72"
보관된 문자열로 여러분의 커스텀 타입을 초기화할때, 문자열 보간이 하는 일을 커스텀할 수 있다는 사실은 잘 모를것이다. 이 글이 그것에 대한 내용이다.

이스케이핑 언세이프 문자열(Escaping unsafe strings)
나는 (아마) 언세이프한 사용자 입력값을 세니타이즈(sanitize: 문자열을 안전하게 만들기위해 처리하는 과정) 하기위해서 이스케이프된 문자열 작업을 하려 한다.

동기
사용자로부터 받은 문자처럼 외부로부터 받은 데이터를 다루는 프로그램이 있는데, 보장된 프로그램을 만들기 위해 외부 데이터가 공격경로(attack vector)로 사용될 수 있는 부분을 반드시 안전에 대비되어 있엉 한다. 예를들어 공격자가 계정을 등록할때 사용자 이름란에 <script>를 넣을 수 있다. 만약 웹앱이 그대로 텍스트를 렌더링하면 공격자는 제어불능의 스크립트를 실행시킬 수 있게 해주는 셈이 되는데, 공격자가 다른 사용자의 쿠키를 훔칠 수 있게 할 수도 있다. 이스케이프하는 HTML 태그처럼 외부로부터 받은 모든 인풋들을 세니타이즈 해야하는 이유이다.

나는 몇주전에 Joel Spolsky가 쓴 트윗를 발견했는데, 여기에는 2005년도에 그가 쓴 Making Wrong Code Look Wrong 글이 링크되있었다. Spolsky는 이 글에서 변수 네이밍의 특정 스타일에대해 이야기하며, 한 변수가 세이프 혹은 언세이프(이케이프 되지 않은) 문자를 가진다면, 프로그래머들이 따라오기 더 쉬워진다고 말한다. 좋은 읽을거리이고 Hyngrian Notation의 역사 이야기도 담고있다.

모든 문자열이 동등하지 않다.
그러나 네이밍 규약을 따르려고 하는것보다는 스위프트처럼 강타입 언어에서 이 문제를 해결하는게 (더 안전하고) 더 나은 것이며, 이것이 타입 시스템의 이점을 취할 수 있는 것이다.

"언세이프 문자열"과 "세이프 문자열"은 근본적으로 너무 달라서 이것들을 서로 다르게 다루어야한다. 그러나 이 둘은 같은 String 타입으로 사용하는 경향이 있는데, 문제의 근원지는 바로 이것이다. 이제 이 개념을 배우기위해 타입을 분리시켜보자. 나는 이것들을 UnsafeString 그리고 SantinizedHTML이라 부르고 있다. 각각은 내부 저장소로 String을 사용한다.
/// An unescaped string from a potentially unsafe
/// source (such as user input)
struct UnsafeString {
    var value: String
}

/// A string that either comes from a safe source
/// (e.g. a string literal in the source code)
/// or has been escaped.
struct SanitizedHTML {
    fileprivate(set) var value: String

    init(unsafe input: UnsafeString) {
        value = SanitizedHTML.escaping(unsafe: input.value)
    }
}
또한 생성자가 UnsafeString으로부터 SantinizedHTML을 만들도록 하고, 이 과정에서 인풋을 이스케이핑한다. 이 escape 메소드는 모든 꺾쇠를 해당하는 HTML 요소로 대치한다. 아주 간단해 보이는 예제이지만 실상은 조금 복잡할 수도 있다.
import Foundation // required for String.replacingOccurrences(of:with:)

extension SanitizedHTML {
    /// Escapes a string.
    fileprivate static func escaping(unsafe input: String) -> String {
        return input
            .replacingOccurrences(of: "<", with: "&lt;")
            .replacingOccurrences(of: ">", with: "&gt;")
    }
}
두 타입의 value 프로퍼티에대한 선언의 차이를 주목하자. UnsafeString의경우 종종 값타입으로 필요할 것이기 때문에 valuevar이다. 이렇게하여 세이프티(단순 소유 모델(ownership model)은 값이 대입될때 복사가 일어남) 값 타입들은 포기하지 않는다. 반면 SantinizedHTMLvalue 프로퍼티는 fileprivate(set)으로 수정되는데, 모든 제3자는 그 타입의 공식적인 API를 우회하여 수정할 수 없고, 언이스케이프된 문자열 값을 주입하기 위함이다. 이것은 타입의 그 구현으로부터 계속 변경을 허락받아야한다.

세니타이즈한 문자열에 새로운 것을 붙일 수 있는 방법을 만들어보자. append(_:) 메소드를위해 두가지 오버로드를 제공한다. 하나는 UnsafeString을 받고, 다른 하나는 SantinizedHTML을 받는다. 나중에는 이것이 이미 세니타이즈함을 보장할 것이라서 이것을 다시 이스케이프할 필요가 없다.
extension SanitizedHTML {
    mutating func append(_ other: SanitizedHTML) {
        // other is already safe
        value.append(other.value)
    }

    mutating func append(_ other: UnsafeString) {
        let sanitized = SanitizedHTML(unsafe: other)
        append(sanitized)
    }
}

안전한 자료로부터 이스케이프하지 않은 입력 받기
또한 우리는 이미 안전하다고 알고있는 것을 SantinizedHTML에 추가하는 방법도 필요하다. <h1>태그나 <p>태그(혹은 <script>태그까지도)를 HTML 템플릿에서 이스케이프되도록 원하지 않을 수도 있다. 문자열 리터럴(literals), 즉 상수 문자열을 통해 그렇게 할 수 있다. 코드에서의 문자열 리터럴은 항상 안전하다고 가정할 수 있다. 여러분의 소스코드가 보장되있다면 어떤 경우라도 장담할 수 있다(all bets are off in any case).

여러분의 타입을 문자열 리터럴과 함께 초기화시키는 기능을 넣기위해, ExpressibleByStringLiteral 프로토콜을 따르게 한다. 이 프로토콜은 3개의 생성자를 필요로한다. 그래도 제네럴하게 각각에게 전달할 수 있어서 생각보다 쉽게 만들 수 있다.
// Initialization with a string literal should not escape the input.

extension SanitizedHTML: ExpressibleByStringLiteral {

    init(stringLiteral value: String) {

        self.value = value

    }

    init(unicodeScalarLiteral value: String) {

        self.init(stringLiteral: value)

    }

    init(extendedGraphemeClusterLiteral value: String) {

        self.init(stringLiteral: value)

    }

}
우리가 여기에 있는 동안 UnsafeString의 능력과 같게 만드는 것이 좋아보인다. 이 구현은 SantinizedHTML의 것과 동일하다.
extension UnsafeString: ExpressibleByStringLiteral {

    // Same implementation, see above

}
이제 문자열 리터럴로 UnsafeString 값과 SantinizedHTML 값을 생성할 수 있다. 여기에는 타입을 지시해주어야하는데, String값을 받을 수도 있기 때문이다.
let userInput: UnsafeString = "<script>alert('P0wn3d');</script>"

var sanitized: SanitizedHTML = "<strong>Name:</strong> "

sanitized.append(userInput)

sanitized.value

// → "<strong>Name:</strong> &lt;script&gt;alert('P0wn3d');&lt;/script&gt;"
이제 됐다! 세이프 문자열 리터럴은 이스케이프하지 않았지만 언세이프한 사용자 입력이 있다.

문자열 보간법
이제 기본적인 것들이 해결되었다. 모든 렌더링 API가 입력받을때, SantinizedHTML만 받을 수 있어야 한다면, 그 새로운 타입을 언이스케이프된 문자열을 렌더링하지 못하게 만들어야한다.

그러나 아래처럼 문자열 보간법을 통해 초기화할 수 있다면 SantinizedHTML을 사용하여 편리하게 만들 수 있다.
let sanitized2: SanitizedHTML = "<strong>Name:</strong> \(userInput)"

// error: cannot convert value of type 'String' to specified type 'SanitizedHTML'
현재, 이것은 컴파일타임 에러의 결과를 내뱉는다. 유용하게 만드려면 바로 잡아야하는데 보간법 문자열 부분은 안전하고 \()안의 부분은 안전하지 않게 하는 그런 작업을 수행해야한다.

이상한 ExpressibleByStringInterpolation 프로토콜
우리가 다음에 보려하는, 이것이 동작할 수 있다. 표준 라이브러리에는 우리가 따라야할 ExpressibleByStringInterpolation이라는 프로토콜을 제공하고 있다. 현재(Swift3에서) 이 프로토콜은 디프리케이트 되었다. 스위프트 팀이 이것을 "잘못된 설계한계가있다"고 인지했기 때문이다. 이 말은, 우리가 이것을 사용하면 나중에 경고를 보게될 것이고, 스위프트4나 그 후에 어떤 새로운 API(더 강력해 질 것으로 보인다)로 대체되어 우리 코드를 고칠 준비를 해야한다. 그전까지 현재 API가 직관적이진 않지만 놀랍도록 유용하게 쓰일것이다.

이 프로토콜은 두가지 생성자를 필요로한다. 문자열 보간법은 두가지 단계로 처리된다.
1. 먼저 첫번째 단계에서는, 컴파일러가 보간법 문자열을 문자열 리터럴의 세그먼트와 변수 표현식으로 분디한다. 그리고 세그먼트들은 init<T>(stringInterpolationSegment:) 생성자로 보내진다.

그 세그먼트들은 항상 문자열 리터럴 과변수 표현식을 번갈아간다. 그리고 첫번째 세그먼트는 항상 리터럴(보간법 문자열이 한 변수로 시작하면 아마 비어있을 것이다)이다. 두 변수 표현식이 보간법 문자열 안에서 직접 붙어있으면 다시 빈 리터럴 세그먼트가 그 사이에 들어갈 것이다.

보간법 문자를 위한 세그먼트들에대한 예시이다.
"\(name) says \(greeting1)\(greeting2)!"
이것이 아래처럼 된다.
""

name

" says "

greeting1

""

greeting2

"!"
이런 동작이 현재에 공식적으로 문서에 나와있진 않을거라 생각된다.

2. 그 두번째 보간법 단계는, 보간법 문자열에서 나타난 그 요구대로 첫번째 생성자의 결과물을 두번째 생성자인 init(stringInterpolation:)으로 보낸다.

이런 세그먼트들의 순서의 특징을 이용하여, 짝수번째(0을 포함한)의 세그먼트들은 항상 문자열 리터럴이니 세이프하고, 반면 홀수번째의 세그먼트들은 언세이프하니 반드시 이스케이프 해주어야한다.

ExpressibleByStringInterpolation을 따르기
이 API에서 이상한 점은 보간법 과정에서 첫 단계에서 타입을 따르는 생성자를 사용하는 것이다. 이 말은 각기 다른 보간법 세그먼트에서 유효한 SantinizedHTML 값을 만들어서 오직 두번째 단계에서 이 세그먼트들을 완성된 값으로 합쳐야만한다. 우리는 SantinizedHTML 안에 각 세그먼트를 담아두어야하니, 타입 정의에따라 새로운 프로퍼티를 추가해보자.
struct SanitizedHTML {

    fileprivate(set) var value: String

    // Required for string interpolation processing

    fileprivate var interpolationSegment: Any? = nil

    ...

}
 이 프로퍼티의 타입은 Optional<Any>이다. Any인 이유는 보간법 문자열로 들어온 어떤 값이라도 변환하지 않은채 가지고 있기 위함이고, Optional의 이유는 문자열 보간법 처리중에 이것이 필요하기 때문이다. 모든 예외는 nil이 될것이다.

아래 보간법의 모든 단계를 담은 전체 구현이 있다.
extension SanitizedHTML: ExpressibleByStringInterpolation {

    // Step 1

    public init<T>(stringInterpolationSegment expr: T) {

        // Store the segment

        interpolationSegment = expr

        // Dummy initialization, this is never used

        value = ""

    }


    // Step 2

    public init(stringInterpolation segments: SanitizedHTML...) {

        let stringSegments = segments.enumerated()

            .map { index, segment -> String in

                guard let segment = segment.interpolationSegment else {

                    fatalError("Invalid interpolation sequence")

                }

                if index % 2 == 0 {

                    // Even indices are literal segments

                    // and thus already safe.

                    if let string = segment as? String {

                        return string

                    } else {

                        return String(describing: segment)

                    }

                } else {

                    // Odd indices are variable expressions

                    switch segment {

                    case let safe as SanitizedHTML:

                        // Already safe

                        return safe.value

                    case let unsafe as UnsafeString:

                        return SanitizedHTML.escaping(unsafe: unsafe.value)

                    default:

                        // All other types are treated as unsafe too.

                        let unsafe = UnsafeString(value: String(describing: segment))

                        return SanitizedHTML(unsafe: unsafe).value

                    }

                }

        }

        value = stringSegments.joined()

    }

}

1단계 생성자는 받은 값을 저장하고 실제 변환인 2단계로 넘어간다. 생성자는 반드시 모든 프로퍼티를 초기화해주어야 하므로, 2단계까지 계속 그 인스턴스가 살아있더라해도 value 프로퍼티를 위한 더미 값을 제공해주어야한다.

2단계에서는 SantinizedHTML값의 배열로 세그먼트를 받는다. 우리의 목적은 이 값들을 문자열로 변환하는 것이기 때문에, 이 과정에서 변수 표현식 세그먼트들을 이스케이프한다. 그리고 문자열들을 하나로 합쳐 우리 value 프로퍼티에 결과를 저장한다. 배열과 그것의 인덱스들을 연결하고 짝수 인덱스가 세이프하다는 정보를 이용한다. 또한 우리는 이미 짝수 인덱스들이 문자열이라는 것을 알지만, 안전을 위해 필요에따라 세그먼트를 확인하여 문자열로 변환하게 만든다.

홀수 인덱스의 경우, 3가지 상황으로 나뉜다. 첫번째, 세그먼트가 이미 SantinizedHTML 값이라면 다시 이스케이프하지 말고 바로 그 값을 반환한다. 두번째, 세그먼트가 UnsafeString이면 이스케이프하고 그 결과를 반환한다. 세번째, 세그먼트가 그 밖의 타입(String이나 Int)이면 먼저 UnsafeString으로 만들어서 이스케이프한다.

더 확장하여, 연속된 SantinizedHTML 값을 위한 커스텀 로직을 추가할 수도 있지만, 이것만으로도 충분히 강력해 보인다. 위의 것으로 예제를 시험해보자.
let sanitized2: SanitizedHTML = "<strong>Name:</strong> \(userInput)"

sanitized2.value

// → "<strong>Name:</strong> &lt;script&gt;alert('P0wn3d');&lt;/script&gt;"
우리가 원하는데로, 문자열 리터럴은 그대로 보내지고 변수 표현식은 이스케이프될 것이다. 멋지다!

이게다다. 새로운 타입을 더 편하게 사용하고 싶으면, 다음 단계로 CustomStringConvertible 혹은/그리고 CustomDebugStringConvertible를 따르게 할 수 있는데, 이것은 여러분에게 남겨두겠다.

모든 코드는 Gist에 올려놓았다. 플레이그라운드에 붙여넣어서 가지고 놀아보자.

결론
여러분의 타입을 어떻게 보간법 문자열로 번역하는지는 특히 DSLs에게 강력한 기능이다. 이번에 이 기술로 SQL 쿼리를 만들거나 다국어 문자열을 만드는데 적용해볼 수 있겠다(Brent Royal-Gordon이 다국어 문자열을 만든 구현 예시이다). 구성요소로 만든 문자열을 쓰는 모든 작업은 아마 이것이 도움이 되리라 믿는다.

이 API가 완벽하진 않아도 스위프트에서 이것이 가능하게 만들어 놓았다는 점이 매우 멋지다. 문자열 보간법 API가 나중 스위프트 버전에서 바뀌게되면 더 표현력 있고 사용하기 쉬워질 것이라 생각된다.

이 글에 아이디어를 제공해준 Bandes-Storch에게 특별히 감사하다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret

제목: Swift: UIView Animation Syntax Sugar
클로저가 못생기게 엮어버리기 때문에..


들어보았을지도 모르겠지만, 여러분의 스위프트 코드에서 클로저는 유용하게 쓰인다. 이것은 일급(first-class) 종류이고, API의 끝에 있을때는 후행 클로저(trailing closure)로 만들 수 있다. 그리고 이제는 디폴트 @noescape이며 참조 사이클 싸움에서 엄청난 승리이다(now they’re @noescape by default which is a massive win in the fight against reference cycles).

그러나 우리는 이따금씩 한개 이상의 클로저를 전달해야하는 API들과 함께 작업하게 되는데, 한개 이상의 클로저는 클로저의 아름다운 기능을 덜 매력적인 기능으로 만들어버리기도 한다. UIView를 한번 보자,
class func animate(withDuration duration: TimeInterval,
    animations: @escaping () -> Void,
    completion: ((Bool) -> Void)? = nil)

후행 클로저
UIView.animate(withDuration: 0.3, animations: {
    // Animations
}) { finished in
    // Compeleted
}
우리는 기존의 클로저와 후행 클로저를 함께 쓸 수 있다. animations:는 여전히 그 파라미터의 타이틀을 가지지만, completion:후행 클로저로 만들어 파라미터 타이틀을 없앴다. 나는 후행 클로저가 이 타입의 문맥에서 API로부터 동떨어진 느낌을 받았다. 아마 그 이유는 API의 닫힘 괄호와 뒤에 따르는 열림 괄호의 내부 클로저 때문이라 생각된다.
}) { finished in // yuck
Note: 후행 클로저가 무엇인지 확신하기 힘들다면, 그것이 무엇이고 어떻게 쓰이는지 설명해놓은 Swift: Syntax Cheat Codes라는 글을 보자.

가독성을 위한 들여쓰기
애니메이션 클로저들이 기본 선언과 같은 선상의 들여쓰기를 하기 때문에 그것에대해 이야기해보자. 최근에 나는 함수형 프로그래밍 쿨피스(kool-aid) 많이 마셨고, 함수형 코드를 작성하는 것에대해 내가 완전히 좋아하는 방법은 뷸렛 포인트(bullet point) 형식으로, 이것은 명령을 열로 나타내는 것이다.
[0, 1, 2, 4, 5, 6]
    .sorted { $0 < $1 }
    .map { $0 * 2 }
    .forEach { print($0) }
두 클로저 API도 당연히 이렇게 된다.
Note: $0 문법이 이해가 안된다면, 그것이 무엇이고 어떻게 쓰이는지 설명해놓은 Swift: Syntax Cheat Codes라는 글을 보자.

못생긴 것을 강제로 아름답게 만들기
UIView.animate(withDuration: 0.3,
    animations: {
        // Animations
    },
    completion: { finished in
        // Compeleted
    })
나는 Xcode의 자동완성에 맞춰서 내 스스로 UIView 애니메이션 API를 이런식으로 배치하는 방법을 함수형 프로그래밍 문법에서 찾아냈고 사용해보기로 했다. 내 개인적인 의견은, 이런 배치가 이전 것보다 더 읽기 좋은데, 많이 귀찮아진다. 이 코드를 복사 붙여넣기 할때마다 들여쓰기는 헝클어지겠지만 스위프트 문제라기 보단 Xcode의 문제로 생각된다.

클로저 전달하기
let animations = {
    // Animate
}
let completion = { (finished: Bool) in
    // Completion
}

UIView.animate(withDuration: 0.3,
               animations: animations,
               completion: completion)
포스팅의 시작 부분에서 말했듯이 스위프트-토피아(스위프트 세상)에서 클로저는 일급(first-class) 종류이다. 이 의미는 클로저를 변수에 할당할수도 있거니와 당연히 전달도 할 수 있다는 뜻이다. 그러나 이 코드는 이전 코드만큼 읽기 좋은지 납득하기는 어렵고, 다른 오브젝트들이 다른 목적으로 이 클로저에 접근할 수 있어서 이 방법은 주저하게 된다. 결국엔 전자의 선택을 할것이다.

해결책
많은 프로그래머들이 그렇게 하듯, "장기적으로 시간을 절약"하고 싶은 타협 아래 현실적인 문제와 관련하여 해결책을 만들고자 한다.
UIView.Animator(duration: 0.3)
    .animations {
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()
위에서 볼 수 있듯, 스위프트의 함수형 프로그래밍 API에서 나온 방식에서 영감을 받는 문법과 구조이다. 우리는 두 클로저의 API를 일련의 고차함수(higher-order fuction)로 바꾸었고, 이제 우리 코드는 훨씬 더 읽기 쉬워졌으며, 우리 코드를 복사/붙여넣기 할 때 컴파일러가 들여쓰기를 도와줄 것이다.
"장기적으로 시간을 단축시킬 것이다"

Animator
UIView.Animator(duration: 0.3)
    .animations {
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()
우리 Animator 타입은 꽤 간단하게도 3가지 프로퍼티를 가진다. duration, 두 클로저, 한 생성자. 그리고 곧 익숙해질 몇몇 함수들이 있다. 반드시 필요한 것은 아니지만 우리 코드의 가독성을 증진하고, 우리가 구현한 후, 여러곳에서 클로저 시그니처를 변경하고자할때 에러를 줄여주는 typealias를 사용하는데, 우리 클로저의 시그니처를 미리 정의하기위해 두 typealias 선언한다.

클로저 프로퍼티들은 가변(mutable)인데, 어디선가 그것을 저장하고 인스턴스로 만들 뒤에 그 값을 바꿀 수 있다. 그러나 외부에서 변경가능한 상황을 막기위해 private로 하였다. 기존의 UIView API처럼 만들기 위해 completion은 옵셔널이지만 animations는 옵셔널이 아니다. 생성자 구현에서 컴파일러가 불평하는것을 막기 위해 클로저 프로퍼티에 디폴트값을 넣었다.
func animations(_ animations: @escaping Animations) -> Self {
    self.animations = animations
    return self
}

func completion(_ completion: @escaping Completion) -> Self {
    self.completion = completion
    return self
}
클로저 행렬 구현은 놀랍도록 간단하다. 하는 일이라곤 특정 클로저 인자를 받아서 그것을 해당 클로저 값에 넣는 것이다.

자기자신을 반환하기
멋진 점은 이 API들이 Self의 인스턴스를 반환한다는 점인데, 이 부분이 바로 마법같은 부분이다. 우리가 Self라 쓰면 행렬방식(sequence-style) API로 만들 수 있기 때문이다.

함수에서 Self를 반환할때, 그 자리에 다른 함수들이 다시 실행될 수 해준다.
let numbers =
    [0, 1, 2, 4, 5, 6]  // Returns Array
    .sorted { $0 < $1 } // Returns Array
    .map { $0 * 2 }     // Returns Array
그러나 행렬에서 마지막 함수가 오브젝트를 반환하면 반드시 어디 변수에 할당하여야한다. 위의 numbers 상수에 할당한 이유이다.

마지막 함수가 Void를 반환하면 실행시에 아무것도 할당하지 않아도 된다.

[0, 1, 2, 4, 5, 6]         // Returns Array
    .sorted { $0 < $1 }    // Returns Array
    .map { $0 * 2 }        // Returns Array
    .forEach { print($0) } // Returns Void

애니메이션하기
func animate() {
    UIView.animate(withDuration: duration,
        animations: animations,
        completion: completion)
}
나의 수많은 방법처럼, 원래있던 API를 감싸는 것으로 깔끔하게 끝난다. 그러나 이것이 나쁜 방법은 아니다. 스위프트는 우리를 '생각하는자(thinker)', '고쳐쓰는자(tinkerer)'라 생각하고 있으며, 우리에게 제공된 툴을 다시 생각해보고 다시 가공하는 프로그래머로서 가능하게 해준다고 나는 확고히 믿고있다.

UIView를 익스텐션하기
extension UIView {
    class Animator { ... }
}
마지막으로 두가지 이유로서 우리 Animator 클래스를 잡아다가 UIView의 익스텐션에 놓는다. 그 이유는 첫째로 UIView의 네임스페이스를 원하기 때문이다. 따라서 우리가 만든 API에 문맥을 만들어준다. 두번째로는 기능이 UIView와 직접적으로 연관되어 홀로 클래스로 존재하는 것은 의미가 없게된다.

옵션
UIView.Animator(duration: 0.3, delay: 0, options: [.autoreverse])
UIView.SpringAnimator(duration: 0.3, delay: 0.2, damping: 0.2, velocity: 0.2, options: [.autoreverse, .curveEaseIn])
애니메이션 API와 작업할때, 여러 옵션 선택사항이 있으니 문서를 확인해보자. 함수에서 디폴트값과 클래스 상속을 통해, SpringAnimator 클래스만큼 Animator는 여러분이 일반적으로 사용할 수 있는 많은 애니메이션 타입을 이제 커버한다.

언제나처럼 여러분이 확인해볼 수 있게 GitHub에 플레이그라운드를 만들어 놓았고, Xcode가 없는 사람들을 위해  Gist에도 담아두었다.

오늘 읽은 글이 마음에 든다면 나의 다른 글 도 확인해보고, 혹은 여러분 프로젝트에 이 방식을 적용시켜보고자 한다면, 나에게 트윗 해주거나 Twitter에서 나를 팔로우해달라. 매우 기분이 좋은 하루가 될것이다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: That One Optional Property

때론 새로운 것을 위해 여러분의 뷰컨트롤러를 수정해야할 필요가 있다. 한가지는 다른것을 야기하는데, 뷰컨트롤러에 옵셔널 변수를 추가하는 자신을 발견할 것이다. 이것은 몇몇 케이스에서 설정될 것이고 몇몇은 아닐 것이다.

내 생각엔 더 자주 일어나고 이것은 결점이있는 방법이다. 여기에는 몇가지 이유가 있다. 첫째로 어떨때만 쓰이는 옵셔널 프로퍼티를가진 클래스는 정체성이 약한 느낌을 가진다. 바꿔 말해보면, 옵셔널 프로퍼티를 추가하면 그 타입의 근본적인 의미가 흐려진다. 첫번째로 이 옵셔널 프로퍼티는 어떤 시멘틱 의미를 고려하지 않는다. 이 타입이 nil이라면 이 오브젝트의 상태가 무엇이라 할 수 있을까? 여러분의 코드를 가볍게 읽고있는 사람들은 어떤 경우에 프로퍼티가 nil이될지, 혹은 그 오브젝트에서 가지는 분기가 무엇인지 말할 수 없을 것이다. 세번째로 코드는 가변으로 계속 될것이다. 한 옵셔널 프로퍼티는 누가지 선택적 프로퍼티로 될것이고, 이것은 세개로 되고, 당신이 알기 전에 미끄러운 경사의 아래에 가있을 것이다. 이 값이 반드시 존재하는지 이 값이 nil인지 표현하고 싶으면 간단한 옵셔널로는 그렇게 할 수 없다.

내가본 모든 코드베이스에서는, 여러분의 뷰컨트롤러에 왜 옵셔널 프로퍼티가 필요한지에대한 두가지 주된 이유를 발견했다. 나는 이 두가지 다를 탐험할 것이고 각 문제를 해결하기위한 더 나은 패턴을 제안할 것이다.

문제의 옵셔널 프로퍼티를 가지는것에대한 첫번째 이유는 이 뷰컨트롤러가 쓰일때마다 오브젝트나 몇 데이터를 반드시 가질 필요가 없을때이다.

최근에 만난 이것에대한 예시는 뷰컨트롤러가 어떤 경우 푸시 노티피케이션으로부터 표시되는 상황이다. 이때 노티피케이션에서 나온 특정 메시지를 보여줘야한다. 이 문제를 해결하기위한 가장 간단한 방법은 옵셔널 문자열 프로퍼티를 추가하는 것이었다.
class LocationViewController: UIViewController {
     //...
     var notificationMessage: String?
     //...
}
뷰컨트롤러에 다른 코드는 어떤 뷰에 할당할지, 어떻게 배치할지등을 결정하기위해 메시지가 있다고 전환했다. 프로퍼티의 선택성은 그냥 문자열의 존재보다 더 표현한다. It had implications in the rest of the view controller and the rest of the view layer.


여기서 더 중요한 점은 문자열이 더이상 거기에 있을지 아니면 없을지 표현하는게 아니게 된다. 이제 뷰컨트롤러 안에있는 표현 스타일이나 모드를 표현한다. 이것이 푸시 노티피케이션의 문맥에서 만들어진 것인가 혹은 일반 브라우징을 통해 만들어진 것인가? 답은 가까스로 관련된 프로퍼티에 있다.

이 문제를 해결하기위해, 이 모드를 모두 명시적으로 만들어야한다. 이 뷰컨트롤러에서 이것은 일급시민이면, 뷰컨트롤러의 그 영향은 더 분명해질것이다.
class LocationViewController: UIViewController {
     //...
     enum Mode {
          case fromNotification(message: String)
          case normal
     }

     var mode: Mode
     //...
}
Sandi Metz가 말한것처럼, 한 특수화는 절때 없다(there's never one specialization). 옵셔널 프로퍼티를 쓰면 그 코드는 이 프로퍼티의 nil 상태에대한 의미를 가지거나 고유의 의미를 가진다고 할 수 없다. 열거형을 사용하면 코드에서 구체화되고 형식화된다.

새로운 열겨형이 Optional 타입 정의의 열거형 형태와 아주 비슷하다는 점을 인지하자.
enum Optional<Wrapped> {
     case some(Wrapped)
     case none
}
그러나 여기에는 몇가지 분명히 다른 유용한점이 존재한다.

첫째로 시멘틱이다. somenone은 추상적이다. normalfromNotification은 그것과 관련된 의미를 가진다. 여러분의 코드를 읽는 사람들은 여러분에게 감사할 것이다.

두전째로 확장성이다. 뷰 컨트롤러에 다른 모드가 추가되면 그것을 완전히 설명하는 더나은 의미를 가진다. 만약 새로운 모드가 두 모드와 절때 겹치지 않으면, 필요한 연관된 데이터가 어떻든 새로운 열거형 케이스를 추가할 수 있다. 새로운 모드가 현재 모드들과 겹친다면, 새로운 열거형이되어 새로운 프로퍼티를 함께할 수 있다. 오브젝트의 상태는 읽기에 더욱 설명가능한 것이 된다. 어떤 선택이든 새 모드를 위해 또다른 옵셔널을 추가하는 것보다는 낫다.

세번째도 확장성이다. LaunchViewController.Mode가 일급 타입이기 때문에 우리는 함수와 계산된 프로퍼티를 추가할 수 있다. 예를들어 노티피케이션 메시지를 잡아두는 레이블의 높이는 아마 그 메시지의 존재에 달렸을 것이다. 따라서 코드를 열거형으로 옮길 수 있다.
extension Mode {
     var notificationLabelHeight: CGFloat {
          switch self {
          case .normal: return 0
          case .fromNotification(let message): return message.size().height
          }
     }
}
더 풍요로운 타입으로 데이터를 옮기는 것은 적은 코드를 투자하여 이 모든 앞단에서의 이득을 제공한다.

옵셔널 프로퍼티를 사용할지도 모르는 두번째 이유는 첫번째 이유의 부분집합이다. 어떤 경우, 옵셔널 프로퍼티는 뷰컨트롤러의 다른 모드를 표현하지 않는데, 코드에서 임시적인 특징을 표현한다. 아직 값을 가지고 있지 않기 때문에 값으로 프로퍼티를 초기화할 수 없다. 이것의 일반적인 예시는 네트워크로부터 뭔가 패치를 하거나, 시스템으로부터 뭔가 긴 검색시간이 걸려 비동기가 필요해서 기다려야 할때이다.
class UserViewController: UIViewController {
     //...

     // will be loaded asynchronously
     var user: User?
     //...
}
이런 종류의 문제는 어떨때 데이터가 배열형식으로 들어오면 준비될 수 있다. 빈 배열로 존재하지 않는 상태를 표현할 수 있기 때문에, 옵셔널을 사용하지 않더라도 이 문제는 여전히 숨어있다. 테이블 뷰 컨트롤러에 빈 상태를 추가하는 힘든 상황의 횟수가 바로 이런 문제의 정도를 나타낸다.

이 문제는 첫번째 문제의 부분집합이기 때문에, 열거형으로 같은 방법으로 해결할 수 있다.
enum LoadingState<Wrapped> {
     case initialcase loading
     case loaded(Wrapped)
     case error(Error)
}
이 솔루션이 동작하는동안, 비동기 상태를 관리하기위한 더 나은 추상화가 있다. 아직 존재하진 않지만 미래의 어느 시점엔 있을 한 값은 Promise에의해 잘 표현된다. 프로미스는 여러분의 유스케이스에따라 여러 이점을 가진다.

먼저, 프로미스는 가변을 허락하지만, 오직 한번만 그리고 오직 결정되지 않을때까지만이다. 프로미스가 한번 변경되면, 다시는 변경할 수 없다. (여러번에걸처 변경해야한다면, Signal이나 Observable이 여러분이 찾던 것일 것이다.) 이 말은 let과같은 시멘틱을 가지지만 여전히 비동기 데이터를 관리한다.

다음으로, 프로미스는 값이 들어왔을때, 없어질 블럭을 추가하는 기능을 가진다. 만약 프로퍼티가 간단한 옵셔널 프로퍼티를 남긴다면, 추가된 블럭들은 didSet 프로퍼티 옵저버에서 코드와 동일하다. 그러나 프로미스 블럭은 하나 이상 추가할 수 있고, 클래스 어디에서든 추가될 수 있기 때문에 어욱 강력하다. 게다가 프로미스가 이미 값으로 채워져 있을때 추가한다면 그들은 즉시 실행될것이다.

마짐가으로 필요에따라 프로미스는 에러도 처리한다. 특정 종류의 비동기 데이터에대해, 이것이 중요하며, 이것으로부터 자유로울 것이다.

Promise 라이브러리를 사용하고 있다면 빈 생성자로 미결정된 Promise를 생성할 수 있고, fulfill 메소드로 언제든지 채울 수 있다.

옵셔널 프로퍼티를 사용할 때는, 가끔 예전에 보이지 않던 것들이 드러낸다. 뷰컨트롤러에 옵셔널 파라미터를 추가하는 자신을 발견할 때 스스로에게 물어보자. 이 옵셔널이 정말로 의미하는바는 무엇인가? 이 데이터를 표현할 더 좋은 방법은 없는가?



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Keeping XCTest in sync on Linux

갱신이력:
2017.03.30 이 이슈에대해 프로세스를 추적하는 버그를 참조한 꼬릿말 추가
2017.03.30 코드 생성을 사용하는 솔루션을 대신해주는 appendix 를 추가
2017.03.31 생성된 파일의 타겟 패스를 명세하는 Sourcery 양식을 수정. 이것은 수공업의 리네이밍 단계를 절약해준다.

스위프트는 크로스-플랫폼이지만, 애플 플랫폼과 다른 OS에서 다르게 동작하는데, 주로 두가지 이유가 꼽힌다.
  • Objective-C 런타은 애플 플랫폼에서만 가능하다.
  • Foundation과 다른 core library들은 애플OS가 아닌 것의 구현이 따로 되어있다. 이 의미는 Foundation API는 macOS/iOS와 리눅스에서 다른 결과를 만들수도 있고, 혹은 그냥 아직 완전히 구현되지 않았을 수도 있다.
그러므로 라이브러리를 어떤 애플 플랫폼의 특정 기능에 의존하지 않게 짜면, macOS/iOS 그리고 리눅스의 코드를 테스트하기 좋은 전략이 된다.

리눅스에서 테스트 디스커버리
어떤 유닛 테스트 프레임워크는 실행해야 테스트를 찾을수 있게 해놓았따. 애플 플랫폼에는 XCTest 프레임워크가 있는데, 이것은 모든 테스트 수트와 테스트 타겟에있는 메소드를 돌기위해 Objective-C 런타임을 사용한다. Objective-C 런타임은 리눅스에서 사용할 수 없고 스위프트 런타임은 최근에 동등한 기능이 부족하기 때문에, 리눅스의 XCTest는 실행하고자하는 테스트 목록을 명시저으로 제공하도록 개발자들에게 요구한다.

allTests 프로퍼티
이 방식으로 실행하는 방법(스위프트 패키지 매니저에서 만들어진 컨밴션)은 여러분의 각 XCTestCase 서브클래스에 allTests라는 이름의 프로퍼티를 추가하는 방법이다. 이것은 테스트 함수와 그 이름들의 배열을 반환한다. 예를들어 한 테스트를 가지고있는 클래스는 아래처럼 생겼을 것이다.
// Tests/BananaKitTests/BananaTests.swift
import XCTest
import BananaKit
class BananaTests: XCTestCase {
   static var allTests = [
       ("testYellowBananaIsRipe", testYellowBananaIsRipe),
   ]
   func testYellowBananaIsRipe() {
       let banana = Banana(color: .yellow)
       XCTAssertTrue(banana.isRipe)
   }
}

LinuxMain.swift
이 패키지 매니저는 LinuxMain.swift라는 이름의 또다른 파일을 만드는데, 이것은 애플 플랫폼이 아닌 곳에서 동작시키는 테스트 실행자처럼 행동한다. 여기에는 XCTMain(_ :)을 호출하는데 이것은 모든 테스트 수트들의 리스트가 있다.
// Tests/LinuxMain.swift
import XCTest
@testable import BananaKitTests

XCTMain([
   testCase(BananaTests.allTests),
])

수작업의 유지보수는 잊어버리기 쉽다
이 방법은 두곳에서 수작업의 유지보수가 필요하기 때문에 분명 이상적이지 않다.
  1. 새로운 테스트를 추가할때마나 그 클래스의 allTests를 반드시 추가해야한다.
  2. 새로운 테스트 수트를 생성할때마다, LinuxMain.swift에서 XCTMain 호출을 반드시 추가해야한다.
이 두가지 단계 모두 잊어버리기 쉽다. Even worse, 불가피하게 그중 하나를 잊어버릴때, 뭔가 잘못되었음이 조금도 명확하지 않다. 테스트들이 리눅스에서는 통과할 것이고, macOS와 리눅스에서 실행되는 테스트 수를 손수 비교하지 않으면, 몇몇 테스트가 리눅스에서는 돌아가지 않는다는 점을 인지하지 못할수도 있다.

나에게는 이런일이 자주 발생했으므로 이것에대해 뭔가 조처를 취하기로 했다.

리눅스 테스트를 빠트리는 것에대해 보호하기
유지보수 단계중 잊어버릴때 자동으로 테스트 수트를 실패하게 만드는 매커니즘을 만들어보자. 각 XCTesetCase 클래스(와 그들의 allTest 배열)마다 아래 테스트를 추가할 것이다.
class BananaTests: XCTestCase {
   static var allTests = [
       ("testLinuxTestSuiteIncludesAllTests",
        testLinuxTestSuiteIncludesAllTests),
       // Your other tests here...
   ]

  func testLinuxTestSuiteIncludesAllTests() {
       if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
           let thisClass = type(of: self)
           let linuxCount = thisClass.allTests.count
           let darwinCount = Int(thisClass
               .defaultTestSuite().testCaseCount)
           XCTAssertEqual(linuxCount, darwinCount,
               "\(darwinCount - linuxCount) tests are missing from allTests")
       endif
   }
   // Your other tests here...
}
이 테스트는 Objective-C에의해 발견된 테스트 수와 allTest 배열에 항목 수를 비교하여 두 수가 일치하지 않음을 발견하면 실패를 띄워버릴 것이다. 정확히 우리가 원하던 것이다.

(Obj-C 런타임에서의 의존성이라는 의미는 애플 플랫폼에서만 동작하는 테스트라는 뜻이다. 리눅스에서는 컴파일되지 않을 것이고, #if os(macOS) ... 블럭으로 감싼 이유가 바로 그것이다)

allTests에 테스트를 추가하는 것을 잊어버렸을때 실패한 테스트
이것을 테스트 하기위해 다른 테스트를 추가하자, 이 테스트는 allTests 갱신을 깜빡한 경우이다.
import XCTest
import BananaKit

class BananaTests: XCTestCase {
   static var allTests = [
       ("testLinuxTestSuiteIncludesAllTests",
        testLinuxTestSuiteIncludesAllTests),
       ("testYellowBananaIsRipe", testYellowBananaIsRipe),
       // testGreenBananaIsNotRipe is missing!
   ]

   // ...

   func testGreenBananaIsNotRipe() {
       let banana = Banana(color: .green)
       XCTAssertFalse(banana.isRipe)
   }
}
이 테스트을 macOS에서 돌리면 우리의 보호 테스트가 실패할 것이다.

allTests 배열에 테스트를 추가하는 것을 까먹었기 때문에, 보호 테스트가 실패하고있다allTests 배열에 테스트를 추가하는 것을 까먹었기 때문에, 보호 테스트가 실패하고있다


나는 이것을 매우 좋아한다. 분명히, 모든 테스트마다 allTests 배열에 담고 싶을때만 동작할것이다. 즉 위에서 한것처럼 조건부의 컴파일에서 어떤 다윈- 혹은 리눅스 테스트를 감싸야할 것이다. 나는 이것이 많은 코드베이스의 한계를 만족시킬 것이라 믿는다.

LinuxMain.swift를 보호하기
다른 플랫폼은 어떤지 LinuxMain.swift가 완료되었다고 검증할 수 있을까? 이것은 좀 더 힘들다. LinuxMain.swift는 실제 테스트 타겟의 부분이 아니므로(될 수 없다), XCTMain으로 전달되는 내용은 쉽게 확인할 수 없다.

내가본 유일한 솔루션은 Run Script 빌드 단계를 테스트 타겟에 추가하고, LinuxMain.swift에 스크립트 파싱 코드를 넣어, 테스트 타겟에서 테스트 수트의 수에 배열의 항목을 비교한다. 아직 시도해보진 않았지만, 꽤 복잡하게 느껴진다.

업데이트: 코드 생성을 사용한 솔루션으로 appendix를 사용할 수도 있다.

결론
새 테스트에도 불구하고, 여전히 잠재적으로 잊어버릴 수 있는 두가지 때문에 완벽해보이진 않는다. 새로운 XCTestCase 클래스를 생성할때마다 반드시 아래를 행해야한다.
  1. 새 클래스에 testLinuxTestSuiteIncludesAllTests 테스트를 복사하여 붙여넣어야한다.
  2. LinuxMain.swift를 갱신해야한다.
그럼에도 불구하고, 새로운 테스트라는게 가장 일반적인 케이스(현재 테스트 수트를 새로운 테스트를 추가하고는 allTests 배열을 갱신하는 것을 까먹는 케이스)를 커버하므로 현재 상태 보단 상당히 낫다고 생각한다.

스위프트의 반영 능력이 더 강력해지는것을 기다릴 수 없다. 이런 모든것이 불필요하게 되면 좋겠다.


Appendix: Sourcery로 코드 생성
최근에 반복되는 주제로 되는 것처럼 보이는 것에서, Krzysztof Zabłocki가 짚어주었는데, 그의 훌륭한 코드 생성 툴인 Sourcery로 리눅스 테스트 기반을 유지보수할 수도 있다. 이것은 훌륭한 대체물이고 꽤 쉽게 세팅할 수 있다. 아래에 그 방법을 설명해 놓았다.

1. Sourcery 설치하기. 나는 스위프트 패키지 매니저 의존성으로 이것을 추가하는 것에서 동작하지 않았는데(빌드 실패), 스위프트3.1이 새로 나오면서 관련된 일시적인 문제가 아닐까 생각이 된다. 결국 가장 최신 배포을 다운받아서 바이너리를 직접 실행시켰다.

2. LinuxMain.stencil이라는 이름의 파일을 생성하는데, 아래 자료를 함께 넣는다. 여러분의 프로젝트에서 편한 곳에 저장한다. 나는 sourcery/의 하위 폴더에 넣었다.
// sourcery:file:Tests/LinuxMain.swift
import XCTest
{{ argument.testimports }}

{% for type in types.classes|based:"XCTestCase" %}
{% if not type.annotations.disableTests %}extension {{ type.name }} {
  static var allTests = [
  {% for method in type.methods %}{% if method.parameters.count == 0 and method.shortName|hasPrefix:"test" %}  ("{{ method.shortName }}", {{ method.shortName }}),  {% endif %}{% endfor %}]}
{% endif %}{% endfor %}

XCTMain([
{% for type in types.classes|based:"XCTestCase" %}{% if not type.annotations.disableTests %}  testCase({{ type.name }}.allTests),
{% endif %}{% endfor %}])
// sourcery:end
이것은 Ilya Puchka가 작성한 양식을 기반으로 하였다. 나는 그냥 처음과 끝에 //sourcery:... 표시를 추가했다. 이것은 생성된 파일의 경로를 결정한다(Sourcery 0.5.9가 필요함).여러분도 볼 수 있듯, 이것은 양식화 언어(templating language)와 스위프트 코드를 합쳐놓았다. Sourcerey를 호출할때, 여러분의 소스코드에서 타입들을 파싱하여 여러분이 보내온 양식에 맞춰 코드를 생성하는데 쓰일 것이다. 예를들어 {% for type in types.classes|based: "XCTesetCase" %}로 시작하는 루프는 XCTestCase를 상속하는 모든 클래스를 돌면서 allTests 프로퍼티를 담은 익스텐션을 생성할 것이다.

3. 여러분의 테스트 클래스에 있는 allTests 정의를 제거하기. 우리는 다음 단계에서 Sourcery로 이것들을 생성할 것이다. 이미 testLinuxTestSuitIncludesAllTests 메소드를 추가했었다면 이것도 제거하거나 여기서부터 떠나게 만들자. 영향을 주진 않고 여전히 이슈를 찾고 있을 것이다, 예를들어, 테스트를 추가하고나서 Sourcery를 실행시키지 않았을때, 더이상 반드시 엄격하게 하진 않을 것이다.

4. 프로젝트 폴더에서 Sourcery를 실행하기.
$ sourcery --sources Tests/ \
   --templates sourcery/LinuxMain.stencil \
   --args testimports='@testable import BananaKitTests'
Scanning sources...
Found 1 types.
Loading templates...
Loaded 1 templates.Generating code...
Finished.
Processing time 0.0301569700241089 seconds
이것은 현재 있던 Tests/LinuxMain.swift 파일을 생성된 코드로 덮어쓸 것이다.
// Generated using Sourcery 0.5.9 — https://github.com/krzysztofzablocki/Sourcery

// DO NOT EDIT

import XCTest@testable
import BananaKitTests

extension BananaTests {
    static var allTests = [
          ("testYellowBananaIsRipe", testYellowBananaIsRipe),
          ("testGreenBananaIsNotRipe", testGreenBananaIsNotRipe),
    ]
}

XCTMain([
     testCase(BananaTests.allTests),
])
이 작은 예제에선 두 테스트와함께 한 클래스만 있지만, 여러 테스트 클래스에도 역시 동작할 것이다.

5. 마지막(선택적인) 단계에는 더이상 생성된 빌드에서 생겨난 파일이 필요없으므로 그것을 제거한다.
$ rm LinuxMain.generated.swift
그리고 이게 다다. 모든 빌드에 실행되는 스크립트를 위해 두가지 터미널 명령(Sourcery를 호출하고 파일을 제거하는)을 추가하고, 여러분의 리눅스 테스트들은 이제 항상 최신으로 유지될것이다. 매우 멋지다!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Boost Smooth Scrolling with iOS 10 Pre-Fetching API



이전 포스트에서 우리는 iOS 모바일 앱에서 부드러운 스크롤링을 달성하기위한 일반적인 전략을 살펴보았다. 이 전략을 적용시키는 주된 목표는 버벅이는 스크롤링(choppy scrolling)을 피하는 것이고 이런 일반적인 이슈는 사용자 경험에 부정적인 형향을 끼친다. 이런 작업으로 개발자들에게 도움이 되기 위해, 애플은 iOS10에서 UICollectionView를 매우 유용한 변화를 만들었다. 그러나 이렇게 새로이 소개된 기능을 검토하기전에, 우리를 위해 어떤 것이 필요한지 설명하면서 시작해보자.

어떤 이유로 스크롤링이 거칠게 되는가?
이따금씩 앱이 거친 스크롤이 될때를 본적이 있는가? 그 대답이 "그렇다"라면, 스크롤을 빠르게 하려고하는데 앱의 컨텐트가 더듬거리며 나올때 얼마나 실망하는지 경험해보았을 것이다. 여러분은 이런 거친 스크롤 동작이 어떤 이유에서 나왔는지 스스로에게 물어보았을 것이고, 이것이 나쁜 사용자 경험을 따라오게 한다.

짧은 대답은: 앱의 프레임이 떨어질때이다. 그러나 정확히 무슨 뜻일까?

부드러운 스크롤을 유지하려면, 앱은 안정적으로 60 FPS(Frame Per Second)로 표시해야한다. 다른말로 해보자면, 앱은 1초에 60번 컨텐트를 갱신할 필요가 있다. 이 의미는 각 프레임은 대략 16ms만에 렌더링 된다는 의미이다. 운이 나쁠 경우, 할당된 시간보다 더 많이 걸리고, 다음 프레임에 보여줄 데이터가 없어서 앱의 "프레임이 떨어지게" 된다. 이런 불행한 시나리오는 아래 다이어그램으로 표현할 수 있다. 파란색 표시는 그리는 작업을 지시하고 그것들의 생각은 렌더링을 완료하는데 필요한 시간을 표시한다. 여기서 볼 수 있듯, 두번째 프레임에서 우리는 할당된 시간(~16ms)보다 많은 작업이 드는 렌더링 이벤트를 가지는데, 결과로서 세번째 프레임이 드롭된다.


갱신 작업에 쓰인 CPU 시간의 뷰 포인트로부터 같은 시나리오를 보여줄 수 있다. 아래 그래프에서 위로 튀어올라가있는 부분은 앱이 현재 컨텐트를 갱신하기위해 예상했던 16ms보다 더 걸릴때, 프레임이 떨어지는 현상을 나타낸다.


좋은 사용자 경험을 달성하기 위해서는, 갱신 시간을 언제나 16ms 최대치보다 작도록 만들어야한다. 이상적으로는 훌륭한 사용자 경험(한번만이 아닌)을 만들고 싶으므로, 각 갱신 시간은 아래처럼 될 수 있다.
  • 지속적으로 허용된 시간(16ms)보다 아래로한다.
  • 가능한 다른 작업에 사용될 수 있는 CPU 시간을 절약한다.
 프레임을 떨어뜨리는 가장 일반적인 이유는 메인스레드에서 셀의 무거운 데이터 모델을 불러오는 것이다. 이 시나리오의 일반적인 예시는 다음과 같다.
  • URL을통해 이미지를 불러오기.
  • 데이터베이스나 코어데이터로부터 항목에 접근하기.

애플은 iOS10에서 셀들이 불러와지고 표시되는 방법의 최적화를 소개했었다. iOS10으로 증진시킬수 있는 방법을 보고, 부드러운 스크롤링의 사용자 경험을 만들기위해 개발자들이 어떠헥 더 쉽게 됐는지 보다.

iOS9에서의 셀 라이프사이클
UICollectionViewCell의 라이프사이클은 아래처럼 보여줄 수 있다.


컬랙션뷰와 그 셀 사이의 주된 인터렉션은 다음과 같다.
  • 컬랙션뷰는 보여지게될 셀을 위한 컨텐트를 필요로하고 있다. 그 셀은 visible 필드에 들어가는 것에 대한 것이다. collectionView(_ :cellForItemAt:)
  • 컬랙션뷰는 셀을 표시하는것을 요구하고 있다. 그 셀은 단지 visible 필드에 들어가있다. collectionView(_ :willDisplay: forItemAt:)
  • 컬랙션뷰는 셀을 제거하는 중이다. 그 셀은 visible 필드의 바깥에 있다. collectionView(didEndDisplaying: forItemAt:)

iOS10에서의 셀 라이프사이클
iOS10에서 셀의 라이프사이클은 iOS9에서와 거의 비슷하다. 그러나 몇가지 중요한 다른점이 존재한다.

첫번째로 다른점은 OS가 collectionView(_ :cellForItemAt:)를 사용하는것보다 훨씬 쉽게 호출한다는 것이다. 이것은 두가지를 의미한다.
  • 셀을 불러오는 무거운 작업은 셀이 표시되야한다는 필요가 있기 전에 완료될 수 있다.
  • 셀은 결국 표시되지 않을 수 있다. (visible 필드에 들어가지 않을 수 있기 때문에)

두번째로 다른점은 셀이 visible 필드에서 떠날때 그 안에서 일어나는 것이다. iOS10은 보통 collectionView(didEndDisplaying:forItemAt:)이 호출되는데, 셀은 즉시 재활용되지 않는다. OS는 사용자가 스크롤 방향을 뒤집는 경우를 대비해 몇개를 가지고 있는다. 그런 일이 일어나면 셀은 여전히 사용가능하고 컨텐트를 다시 로드할 필요 없이 (collectionView(_ :willDisplay: forItemAt:)가 호출되며) 다시 표시될 수 있다.


iOS9에서 일어나는 것과 비교해보자. 여러분도 알아차릴 것인데, 특정 유스케이스에대해 셀을 불러오는 무거운 작업(collectionView(_ :cellForItemAt:))은 더이상 필요없다. 이 최적화는 사용자가 빠르게 스크롤 할때나 스크롤 방향을 바꿀 때 모두 셀을 빠르게 렌더링할 수 있게 해준다.

세번째로 iOS9와 iOS10간에 중요하게 다른점은 여러행의 레이아웃으로된 컬랙션뷰에서 셀을 로드하는 방법이다.



iOS10은 visible 필드로 들어오는 각 셀마다 분리되어 로드된다(collectionView(_:cellForItemAt:)). 셀 라이프사이클을 설명하는동안 보았듯, 이런 일은 각 셀이 실제로 표시되여햘 때보다 훨씬 쉽다. OS가 다른 요청을 처리하고 셀 로드를 배치하여 최적화의 문을 열게되었다.

셀의 열이 visible 필드로 들어오면, 셀은 한 배치(single batch) (동시에 모든 열을 위해 collectionView(_ :willDisplay: forItemAt:)를 호출한다.)처럼 표시되는데, 이 작업은 CPU 사이클의 주기에서 아주 비싸지 않기 때문이다(적어도 셀 컨텐트를 불러오는 것과 비교해서).

프리-패칭하는 것은, 여러행의 레이아웃을 불러오는 것에 대한 이런 차이점은 iOS10의 UICollectionView 최적화에서 가장 중요한 점이다. 좀 더 세부적인 내용을 살펴보자.

프리-패칭 API
iOS10을 발표할때, 애플은 프리-패칭(Pre-Fetching)을 적용한 기술(Adaptive Technology)로서 소개했었다. 프리-패칭하는것은 스크롤 퍼포먼스 증진의 최적화를 위해 사용자들이 앱과 인터렉션하여 이점을 취하려고 할것이라는 의미다. 예를들어 이런 새로운 기술은 새로운 셀을 불러오기 위해(프리-패칭) 비어있는 시간을 찾아볼 것이다(사용자가 느리게 스크롤할때나 스크롤을 더이상 하지 않을때). 사용자 스크롤 패턴에따라 퍼포먼스같은 것을 실하기위한 프리-패칭의 기회를 더 (혹은 덜) 얻을 수 있다.

가능한 API를 검도하기 전에, 이 기술로 작업할 수 있는 최고의 실행을 살펴보자. 프리-패칭하는 것의 최고의 이점을 얻기위해 셀 컨텐트를 세팅하는 작업들은 반드시 collectionView(_ :cellForItemAt:)에서 실행되어야한다. collectionView(_ :willDisplay:forItemAt:)에서 실행되는 작업들과 collectionView(didEndDisplaying:forItemAt:)에서 실행되는 작업들은 최소화해야하고 CPU가 부담되지 않도록 만들어야한다. 더 나은것은, 이 라이프사이클 이벤트에 아무 작업도 실행시키지 않는게 더 최적화된 것일 것이다! 또한 collectionView(_ :cellForItemAt:)이 셀을 위해 호출될지라도 셀이 절때 표시되지 않을 수 있다는 가능성을 마음에 새겨두자.

어떤 굉장히 좋은 소식은 프리-패칭하는것이 iOS10에서 컴파일된 앱에는 디폴트로 되었다는 것이다. 그러나 UICollectionView의 isPrefetchingEnabled 프로퍼티를 false로 설정하여 이 기능을 끌 수도 있다. 우리가 구현해놓은 컬랙션뷰의 코드를 바꿀 필요가 없다는 뜻이다. 프리-패칭하는 기능의 이점을 얻기위해 취해야할 유일한 옵션은 Pre-Fetching API를 시행하는 것이다.

프리-패칭하는 API와 UICollectionView
UICollectionView를위한 프리-패칭하는 API는 UICollectionViewDataSourcePrefetching 프로토콜에 정의되있다. 이 API는 두가지 메소드를 따르도록 정의되있다.

collectionView(_:prefetchItemsAt:)(필수)—이 메소드는 [IndexPath] 파라미터에의해 지정된 셀의 데이터를 비동기적 로딩을 초기화해준다. 이 비동기 로딩은 Grand Central Dispatch를 통해 실행되거나 OperationQueue를 통해 실행될 수 있다. 이 메소드를 구현할때 중요한 점은 데이터 로딩의 burden을 메인큐에서 백그라운드큐로 이동하는 코드를 작성하는 것이다. 이것의 목표는, 새로운것을 표시하는 중요한 작업을 실행하는 많은 시간을 백그라운드 큐에서 보내도록 하기위해 데이터 로딩 부담을 메인큐에서 백그라운드큐로 넘겨 작업량을 줄이는데 있다.

collectionView(_:cancelPrefetchingForItemsAt:)(선택)—이 메소드는 더이상 필요없는 셀을 [IndexPath] 파라미터로 지정하여 데이터를 소통한다. 이 메소드를 구현하면 필요에따라 데이터 로딩이 되지 못한 것을 취소하는데 사용할 수 있고, 불필요한 작업을 취소하여 CPU 시간을 절감하는 좋은 방법이 된다(일반적으로 사용자가 스크롤 방향을 바꾸기 때문에).

우리가 이전에 말했듯, 프리-패칭하는것은 적용가능한 기술이다. 그러므로 위의 메소드는 앱과 인터렉트하는 사용자의 방법에따라 다르게 유발된다. 이것의 한 결말은 collectionView(_:prefetchItemsAt:)메소드가 컬랙션뷰의 셀마다 호출되지 않는다. 이 말은 collectionView(_:prefetchItemsAt:)를통해 셀을 로딩할때, 앱은 아래의 모든 시나리오를 다룰 수 있다.
  • 데이터가 미리 패치되었고 표시되기위해 준비되었다.
  • 데이터가 현재 패치되는 중이고 표시되기위해 준비되진 않았다.
  • 데이터가 아직 요청되지도 않았다.

프리-패칭하는 API와 UITableView
또한 iOS10은 UITableView를 위한 프리_패칭하는 것도 소개했다. UICollectionView에서 설명했던 모든 주요 개념들이 비슷한 방법으로 UITableView에 적용된다. UITableView를위한 프리-패칭하는 기술은 UITableViewPrefetchingDataSource 프로토콜에 정의되있다. 이 API는 아래 두 메소드를 정의한다.

tableView(_:prefetchRowsAt:)(필수)—이 메소드는 [IndexPath] 파라미터에의해 지정된 셀의 데이터를 비동기적 로딩을 초기화해준다.

tableView(_:cancelPrefetchingForRowsAt:)(선택)—이 메소드는 더이상 필요없는 셀을 [IndexPath] 파라미터로 지정하여 데이터를 소통한다.

UICollectionView 프리-패칭하는 API 메소드에대해 소개한 생각들은 같은 방법으로 비슷한 각 UITableView 프리-패칭하는 API 메소드에 적용된다.

결론
나는 iOS10에서 프리-패칭하는 기능을 적용시키기위해 UITableView와 UICollectionView의 샘플 코드를 수정해놓았다. 여기에서 확인할 수 있다.

이 포스트에서는, UICollectionView와 UITableView에대해 부드러운 스크롤 증진이 가능한 구현을 검토했다. 특히 새로운 OS 최적화에대한 모든 이점을 취할 수 있고 우리의 모바일 앱과 인터렉트할때 최고의 사용자 경험을 제공할 수 있는 특정 프리-패칭하는 API를 구현하여 보았다.

추가적인 링크




이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Picking the right way of failing in Swift

스위프트의 주요 초점중 한가지는 바로 컴파일타임 세이프티이다. 런타임 에러의 경향을 줄여주고 더욱 예상가능한 코드를 작성하는데 개발자들이 집중할 수 있게 해준다. 그러나 때론 여러 이유로 실패한다. 그래서 이번주에는 어떻게 적절하게 실패를 다루는지 보고, 이것을 처분하기위해 어떤 도구를 가져야하는 볼것이다.

몇주전에 우리는 실제 옵셔널이 아닌 옵셔널을 어떻게 다루는지("Handling non-optional optionals in Swift")에대해 보았었다. 지난 포스트에서 나는 강제 언랩핑하는것 대신 guard와함께 preconditionFailure() 사용에대한 경우를 만들었고, 이것을 위해 Require라는 경량의 프레임워크를 소개했었다.

그 포스트 이후로, 많은 사람들이 preconditionFailure()assert()의 차이가 무엇인지, 스위프트의 throwing 기능에 어떻게 연관시킬 수 있는지 물어보았다. 그래서 이번 포스트에서는 각각 그것을 사용할때 그 모든 언어 특징을 좀 더 살펴보자.

한 리스트로 시작해보자.
아래는 스위프트에서 에러를 처리하는 (내가 아는) 모든 것이다.
  • nil을 반환하던지 에러 열거형 케이스를 반환한다. 에러 처리의 가장 간단한 형식은 에러를 만난 함수에서 nil을 반환하는 것이다 (혹은 리턴타입으로 Result 열거형을 사용한다면 .error를 반환한다). 이것은 많은 상황에서 아주 유용할 수 있지만, 모든 에러 처리에대해 과용하면 사용하기 성가셔지고, 로직 결점이 숨어있는 위험을 안게 된다.
  • 에러를 throw한다(throw MyError를 사용하여). 잠재적인 에러 처리를 위해 호출자에서 do, try, catch 패턴을 사용해야한다. 혹은 호출 시점에서 try?를 사용하여 에러를 무시할 수도 있다.
  • assert()assertionFailure()를 사용하여 특정 조건이 참인지 검증한다. 디폴트로, 디버그 빌드에서는 fatal error를 내고, 배포 빌드에서는 무시한다. assert를 유발하면 실행이 멈출것이라는 보장이 없으므로 위험한 런타임 경고처럼 보인다.
  • assert대신 precondition()preconditionFailure()을 사용한다. 핵심적으로 다른점은 배포 빌드일지라도 항상* 판별한다는 점이다. 이 의미는 조건이 성립하지 않으면 절때 계속하지 않음을 보장한다는 뜻이다.
  • fatalError()를 호출한다. 이것은 UIViewController같은 시스템 클래스를 따르는 NSCoding을 상속할때, 아마 Xcode가 생성한 init(coder:) 구현에서 보았을 것이다.
  • exit()를 호출한다. 이것은 코드와함께 여러분의 프로세스를 종료한다. 이것은 전역 범위에서 종료하고 싶을때, 커멘드라인 툴이나 스크립트에서 매우 유용하다(예를들어 main.swift에서)
*Ounchecked 최적화 모드를 사용하여 컴파일을 하지 않는다면

복구가능한 vs 복구불가능한
실패를 올바르게 잡아낼때 생각해야하는 키포인트는 발생한 에러가 복구가능한지 불가능한지 정하는 것이다.

예를들어 우리가 서버에 호출하고있는데 에러를 받았다고 하자. 우리가 얼마나 멋진 프로그래머인지, 서버 기반이 얼마나 탄탄한지 상관없이 종종 이런일이 일아난다. 따라서 fatal과 복구불가능하게 이런 에러 타입을 처리하는것은 종종 실수이다. 대신, 우리가 원하는것은 복구하여 사용자에게 에러 화면의 양식을 보여주는 것이다.

따라서, 이런경우 어떻게 적절한 방법으로 실패를 뽑아낼까? 위의 리스트를 한번 보면, 복구가능한 기술과 복구불가능한 기술로 나눌 수 있다. 아래처럼 말이다.

복구가능한 기술
  • nil을 반환하던지 에러 열거형 케이스를 반환한다.
  • 에러를 throw한다.

복구불가능한 기술
  • assert()를 사용한다.
  • precondition()을 사용한다.
  • fatalError()를 호출한다.
  • exit()를 호출한다.

이 경우 비동기 처리를 다루기 떄문에, 아마 nil을 반환하거나 에러 열거형 케이스를 반환하는게 제일 좋은 방법이다. 아래처럼 말이다.
class DataLoader {
     enum Result {
          case success(Data)
          case failure(Error?)
     }

     func loadData(from url: URL, completionHandler: @escaping (Result) -> Void) {
          let task = urlSession.dataTask(with: url) { data, response, error in
               guard let data = data else {
                    completionHandler(.failure(error))
                    return
               }

               completionHandler(.success(data))
          }

          task.resume()
     }
}
적절한 방법으로 에러를 처리하려고 우리 API를 사용자들에게 강요하는 것은 비동기 API에서, throw는 좋은 선택이다.
class StringFormatter {
     enum Error: Swift.Error {
          case emptyString
     }

     func format(_ string: String) throws -> String {
          guard !string.isEmpty else {
               throw Error.emptyString
          }

          return string.replaceOccurrences(of: "\n", with: " ")
     }
}
그러나 때론 에러가 복구되지 않는다. 예를들어 앱을 실행하는동안 설정파일을 불러와야한다고 하자. 만약 설정파일을 놓힌다면 앱은 정의되지 않은 상태로 갈것이다. 이 경우는 프로그램을 계속 실행하는것 보단 크래쉬를 내는게 낫다. 그러니 더 강한것을 사용하여 실패를 복구하지 않는 방법이 더 적절하다.

이 경우, 설정 파일을 놓혔을 경우에 실행을 멈추기위해 preconditionFailure()을 사용한다.
guard let config = FileLoader().loadFile(named: "Config.json") else {
     preconditionFailture("Failed to load config file")
}

프로그래머 에러 vs 실행 에러
만드는데 중요한 또다른 구별은 결점이있는 로직에의해 에러가 만들어진것인지 잘못된 설정에 의해 에러가 만들어진 것인지이다. 혹은 에러가 앱 플로우의 합법적인 부분으로 고려될 수 있는지로 구별된다. 기본적으로 프로그래머가 만든 것읹 외부 요인이 만든 것인지이다.

프로그래머 에러에대해 대비할 때는 거의 항상 복구하지않는 기술을 사용하고 싶을 것이다. 이런식으로 앱 전체에 걸쳐 특별한 상황을 코딩하지 않아도 된다. 좋은 테스트 수트가 가능한빨리 이런 에러를 잡을 수 있게 해줄 것이다.

예를들어, 한 뷰를 만들것인데 이것을 사용하기전에 바인딩된 ViewModel이 필요하다고 가정하자. 이 ViewModel은 우리 코드에서 옵셔널이지만 사용할때마다 언랩핑하고 싶지 않응ㄹ 것이다. 그러나 제품의 상태에선 ViewModel을 잃어버렸을때 크래쉬를 내고 싶지 않다. 디버그에서 에러를 받는것으로 충분하다. assert를 사용하는 경우가 되겠다.
class DetailDView: UIView {
     struct ViewModel {
          var title: String
          var subtitle: String
          var action: String
     }

     var viewModel: ViewMode?

     override func didMoveToSuperview() {
          super.didMoveToSuperview()

          guard let viewMode = viewModel else {
               assertionFailure("No view model assigned to Detailview.")
               return
          }

          titleLabel.text = viewModel.title
          subtitleLabel.text = viewModel.subtitle
          actionButton.setTitle(viewModel.action, for: .normal)
     }
}
assertionFailure()는 배포빌드에서 묵묵히 실패할것이기 때문에 guard문에서 return해야함을 인지하자.

결론
스위프트에서 가능한 기술을 다루는 여러 에러들 사이에 차이를 명확하게하는데 도움이 되었으면 좋겠다. 내 조언은 한 기술만을 고수하는게 아니라 그 상황에 맞는 가장 적절한 것을 고르는 것이다. 에러가 치명적으로 다룰 수 없어도 사용자 경험을 방해하지 않아야 하기 때문에, 나는 보통 가능한 항상 에러를 복구하려고 노력하는것을 제안하는 편이다.

또한 print(error)는 에러 처리가 아님을 기억하자.

질문이 생기거나 피드백을 주고 싶다면 Twitter로 연락할 수 있다. 또한 나의 다음 주간 플로그 포스트에서 다뤄보고 싶은 주제가 있으면 나에게 알려달라.

읽어주어서 감사하다!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

받은 트랙백이 없고 , 댓글이 없습니다.
secret
제목: Unsafe Swift: Using Pointers and Interacting with C


스위프트는 디폴트로 메모리 세이프하다. 메모리 세이프 하다는 의미는 메모리에 직접 접근하는 것을 막아주고, 당신이 사용하기 전에 모든것이 초기화 되어있음을 보장한다는 뜻이다. 핵심은 "디폴트"이다. 언세이프 스위프트는 여러분이 필요할때 포인터를 이용해서 메모리에 직접 다룰 수 있게 해준다.

이 튜토리얼은 스위프트의 이른바 "언세이프"라 불리는 소용돌이의 여행에 데려다 줄것이다. "언세이프"라는 용어는 종종 혼란을 만든다. 이것이 당신이 도작하지도 않을, 위험하고 나쁜 코드를 작성하고 있다는 의미가 아니라, 오히려 컴파일러가 도와줄 수 있는 부분의 한계를 뛰어넘고 추가적인 주의를 필요로하는 코드를 작성한다는 의미이다.

C같은 언세이프 언어와 함께 작업할때, 추가적인 런타임 성능이 필요할때, 혹은 그냥 그 내부를 살펴보고 싶을때 이러한 기능이 필요함을 발견할 수 있을 것이다. 이 주제가 한걸음 더 나아간 주제이긴 하지만 여러분이 합리적인 스위프트 언어 지식을 가지고 있다면, 따라올 수 있을 것이다. 또한 C언어 경험이 도움이 될것이지만 필수조건은 아니다.

시작하기
이 튜토리얼은 3가지 플레이그라운드로 구성된다. 첫번째 플레이그라운드에서는 메모리 레이아웃(Memory Layout)을 살펴보고 언세이프 포인터를 사용해보는 코드를 몇개 만들어 볼 것이다. 두번째 플레이그라운드에서는 스위프트 인터페이스로 데이터 압축을 스트리밍하는 저수준 C API를 가볍게 다룰것이다. 마지막 플레이그라운드에서는 arc4random에대한 독립적인 대안의 플랫폼을 만들 것인데, 언세이프 스위프트를 사용했지만 사용자들이 그 세부내용은 모르도록 만들것이다.

UnsafeSwift라는 새 플레이그라운드를 생성하면서 시작해보자. 이 튜토리얼의 모든 코드들은 플랫폼에 의존하지 않기 때문에 아무 플랫폼이나 선택해도 된다. Foundation 프레임워크를 불러왔는지 확인하자.

메모리 레이아웃

샘플 메모리샘플 메모리


언세이프 스위프트는 메모리 시스템에 직접적으로 동작한다. 메모리는 일련의 상자라고 생각할 수 있고(실제로 10억개의 상자이다), 각 상자 안에는 숫자가 들어있다. 각 상자는 그것과 연관된 유일한 메모리 주소를 가진다. 저장소의 가장 작은 주소로 가능한 단뒤는 한 바이트이며, 한 바이트는 8비트로 이루어져있다. 8비트는 0에서 255의 값을 저장할 수 있다. 프로세서는 메모리 워드(word)에는 효율적으로 접근할 수 있는데, 워드는 보통 1바이트 이상이다. 64비트 시스템에서는 한 워드가 8바이트 혹은 64비트의 길이이다.

스위프트는 여러분의 프로그램에서 어떤것의 크기나 계열(alignment)에대해 이야기해주는 메모리 레이아웃(Memory Layout) 기능을 가지고 있다.

여러분의 플레이그라운드에 아래의 코드를 추가하자.
MemoryLayout<Int>.size          // returns 8 (on 64-bit)
MemoryLayout<Int>.alignment     // returns 8 (on 64-bit)
MemoryLayout<Int>.stride        // returns 8 (on 64-bit)

MemoryLayout<Int16>.size        // returns 2
MemoryLayout<Int16>.alignment   // returns 2
MemoryLayout<Int16>.stride      // returns 2

MemoryLayout<Bool>.size         // returns 1
MemoryLayout<Bool>.alignment    // returns 1
MemoryLayout<Bool>.stride       // returns 1

MemoryLayout<Float>.size        // returns 4
MemoryLayout<Float>.alignment   // returns 4
MemoryLayout<Float>.stride      // returns 4

MemoryLayout<Double>.size       // returns 8
MemoryLayout<Double>.alignment  // returns 8
MemoryLayout<Double>.stride     // returns 8
MemoryLayout<Type>은 컴파일시간에 특정 Type의 size, alignment, stride를 셜정하는 제네릭 타입이다. 반환된 값은 바이트 단위이다. 예를들어 Int6size에서는 2바이트이고 alignment도 같다. 이것은 2로 나누어 떨어지는 주소에서 시작해야한다는 의미이다.

예를들어 Int16100이라는 주소에 할당할 수 있지만 1이 주소에 할당하는 것은 필요 alignment를 위반하는 것이기 때문에 불가능하다. 만약 Int16들을 함께 모은다면 stride 간격에 모일 것이다. 이 기본 타입을 위해 sizestride와 같다.

다음으로 사용자가 정의한 구조체의 레이아웃을 살펴보고 아래를 플레이그라운드에 추가하자.

struct EmptyStruct {}

MemoryLayout<EmptyStruct>.size      // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride    // returns 1

struct SampleStruct {
  let number: UInt32
  let flag: Bool
}

MemoryLayout<SampleStruct>.size       // returns 5
MemoryLayout<SampleStruct>.alignment  // returns 4
MemoryLayout<SampleStruct>.stride     // returns 8
빈 구조체의 size0이다. 이것은 alignment1이기때문에 어떤 주소에도 가능하다.(모든 자연수는 1로 나누어진다) stride는 특이하게도 1이다. 그 이유는 여러분이 만든 각 EmptyStructsize0이더라도 유용한 메모리 주소를 가져야하기 때문이다.

SampleStruct의 경우는 size5이지만 stride8이다. 이것은 alignment 필요조건의 4바이트 바운더리에의해 그렇게된다. 이것을 고려해볼때 스위프트가 할 수 있는 최고의 일은 8바이트 간격으로 묶는 것이다.

다음을 플레이그라운드에 추가하자.

class EmptyClass {}

MemoryLayout<EmptyClass>.size      // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.stride    // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit)

class SampleClass {
  let number: Int64 = 0
  let flag: Bool = false
}

MemoryLayout<SampleClass>.size      // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.stride    // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.alignment // returns 8 (on 64-bit)
클래스들은 참조 타입이므로 Memory Layout은 참조 크기가 8바이트라고 알려준다.

메모리 레이아웃에대해 더 알고싶다면 Mike Ash의 멋진 글을 보자.

포인터
한 포인터는 한 메모리 주소를 가지고있다. 메모리에 직접 접근하는 타입은 "Unsafe"라는 접두를 가지는데, 따라서 이 포인터 타입은 UnsafePointer라 부른다. 이런식으로 추가적인 타이핑이 성가셔 보일수도 있다. 그러나 이것은 정의되지 않은 행동을 만들 수도 있게 해놓았을때, 컴파일러 없이 메모리 접근의 체크를 하고 있다고 (단지 크레쉬를 예방할 뿐만 아니라) 당신이나 코드를 읽는 사람이 알게 해준다.

스위프트 설계자는 C의 char*와 동일한 UnsafePointer 타입을 만들 수 있게 해놓았다. char*은 구조화되지 않은 방법으로 메모리에 접근할 수 있다. 스위프트는 그렇게 하지 않고, 대게 몇몇개의 포인터 타입을 가지는데, 각각은 다른 기능과 목적을 가진다. 포인터 타입을 가장 알맞게 사용하면, 더욱 의도에 맞게 소통하고, 더 낮은 에러, 그리고 정의되지 않은 동작을 피할 수 있게 도와준다.

언세이프 스위프트 포인터는 그 기능이 예상가능한 네이밍 형식으로, 그 포인터의 특징이 무엇인지 알 수 있게 도와준다. 가변적(mutable)인지 불가변적인지, raw한지 typed인지, 버퍼 스타일인지 말이다. 아래와같이 전체적으로 8가지 조합이 있다.



다음 섹션들에서 우리는 이 포인터 타입들에대해 배워볼 것이다.

Raw 포인터 사용하기
아래 코드를 여러분의 플레이그라운드에 추가하자.
// 1
let count = 2
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let byteCount = stride * count

// 2
do {
  print("Raw pointers")
  // 3
  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  // 4
  defer {
    pointer.deallocate(bytes: byteCount, alignedTo: alignment)
  }
  // 5
  pointer.storeBytes(of: 42, as: Int.self)
  pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
  pointer.load(as: Int.self)
  pointer.advanced(by: stride).load(as: Int.self)
  // 6
  let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
  for (index, byte) in bufferPointer.enumerated() {
    print("byte \(index): \(byte)")
  }
}
이 예제는 두 정수를 저장하고 불러오기 위해 언세이프 스위프트 포인터를 사용한다. 여기서 무슨 일이 일어나는지에대한 설명이다.
  1. 이러한 상수들은 사용된 값들을 종종 들고 있다.
    • count는 저장하기 위해 정수를 가지고 있다.
    • strideInt 타입의 stride를 가지고 있다.
    • alignmentInt 타입의 alignment를 가지고 있다.
    • byteCount는 필요한 모든 바이트 수를 가지고 있다.
  2. 스코프 레벨을 추가하기위해 do 블럭을 추가하였기 때문에, 나중에 예제에서 변수 이름을 재사용할 수 있다.
  3. UnsafeMutableRawPointer.allocate 메소드는 필요한 바이트를 할당하는데 사용한다. 이 메소드는 UnsafeMutableRawPointer를 반환한다. 저 타입의 이름에서 알 수 있듯, 포인터는 (가변의) Raw 바이트를 불러오고 저장하는데 사용될 수 있다.
  4. defer 블럭은 포인터가 적절하게 해제되었는지 확인하기위해 추가된 부분이다. 여기서 ARC는 도움이 되지 않는다. 여러분은 스스로 메모리 관리를 해야한다! 여기에서 defer에대해 더 읽어보길 바란다.
  5. storeBytesload 메소드는 바이트를 저장하고 불러오는데에 쓰인다. 두번째 정수의 메모리 주소는 그 포인터의 stride 바이트를 증가시켜서 계산된디.포인터는 stride 하기때문에, (pointer+stride).storeBytes(of: 6, as: Int:Self)로 포인터 계산 또한 할 수 있다.

  1. UnsafeRawBufferPointer는 메모리가 마치 바이트의 모음인것처럼 접근할 수 있게 해준다. 이 의미는 바이트들을 돌면서 차례로 접근할 수 있게 해주고, 서브스크립트를 이용해 접근하며 filter, map, reduce와같은 멋진 메소드까지 사용할 수 있게 한다. 이 버퍼 포인터는 raw 포인터를 초기화한다.

Typed 포인터 사용하기
typed 포인터를 사용하면 이전의 예제를 간단하게 만들 수 있다. 아래 코드를 플레이그라운드에 추가하자.
do {
  print("Typed pointers")
  let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
  pointer.initialize(to: 0, count: count)
  defer {
    pointer.deinitialize(count: count)
    pointer.deallocate(capacity: count)
  }
  pointer.pointee = 42
  pointer.advanced(by: 1).pointee = 6
  pointer.pointee
  pointer.advanced(by: 1).pointee
  let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
  for (index, value) in bufferPointer.enumerated() {
    print("value \(index): \(value)")
  }
}
아래의 다른 점들을 인지하자.
  • UnsafeMutablePointer.allocate 메소드를 사용하여 메모리를 할당한다. 이 제네릭 파라미터는 포인터로 Int 타입의 값을 불러오고 저장하는데 사용될 것이라는 것을 스위프트에게 알려준다.
  • typed 메모리는 사용하기전과 소멸하기전에 반드시 초기화되어야한다. 이것은 initialize 메소드와 deinitialize 메소드로 할 수 있다. Update: atrick라는 유저가 달아놓은 커멘트처럼 소멸은 non-trivial 타입들만 필요로 한다. 이 말은, 여러분이 non-trivial의 무언가를 바꿀 경우에 소멸을 가지고 있는 것이 여러분 코드의 훗날을 위해 좋은 방법이다. 또한 컴파일러가 이것을 최적화할것이기 때문에 항상 비용이 발생하지 않는다.
  • typed 포인터는 pointee라는 프로퍼티를 가지고 있는데, 이것은 값을 불러오고 저장할때 타입 세이프한 방법을 제공하는 프로퍼티이다.
  • typed 포인터를 증가시키면, 여러분의 숫자의 값을 원하는대로 증가해가며 나타낼 수 있다. 그 포인터는 그 타입의 포인터가 가리키는 값을 기반으로 옳바른 stride를 계산할 수 있다. 다시말해 포인터 계산 또한 가능하다. (pointer+1).pointee=6 이런것 또한 가능하다.
  • typed 버퍼 포인터와 같은 점은, 바이트 대신에 값을 차례로 반복해갈 수 있다.

Raw 포인터를 Typed 포인터로 변환하기
typed 포인터는 항상 직접 초기화 할 필요가 없다. 또한 raw 포인터로부터 만들어질 수 있다.

여러분의 플레이그라운드에 아래 코드를 추가하자.
do {
  print("Converting raw pointers to typed pointers")
  let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  defer {
    rawPointer.deallocate(bytes: byteCount, alignedTo: alignment)
  }
  let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
  typedPointer.initialize(to: 0, count: count)
  defer {
    typedPointer.deinitialize(count: count)
  }

  typedPointer.pointee = 42
  typedPointer.advanced(by: 1).pointee = 6
  typedPointer.pointee
  typedPointer.advanced(by: 1).pointee
  let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
  for (index, value) in bufferPointer.enumerated() {
    print("value \(index): \(value)")
  }
}
이 예제는 이전 것과 비슷한데, 다른점은 처음에 raw 포인터를 생성한다는 것이다. typed 포인터는 필요한 Int 타입으로 메모리를 바인딩하면서 생성된다. 메모리를 바인딩함으로서 타입 세이프한 방법으로 접근할 수 있게 된다. 메모리 바인딩은 typed 포인터를 생성할때 이미 완료된다.

예제에서 남은 부분은 이전것과 동일하다. 한번 typed 포인터의 세계에 발을 드리는 순간 그 예시로 'pointee'를 사용할 수 있다.

한 인스턴스의 바이트를 뽑아내기
종종 현재 있는 인스턴스의 타입에서 바이트를 조사하고 싶을 것이다. 이것은 withUnsafeBytes(of:) 메소드를 호출하여 조사할 수 있다.

아래 코드를 플레이그라운드에 추가하자.
do {
  print("Getting the bytes of an instance")
  var sampleStruct = SampleStruct(number: 25, flag: true)

  withUnsafeBytes(of: &sampleStruct) { bytes in
    for byte in bytes {
      print(byte)
    }
  }
}
이 코드는 SampleStruct 인스턴스의 raw 바이트를 출력해낸다. withUnsafeBytes(of:) 메소드는 UnsafeRawBufferPointer에 접근할 수 있게 해주는데, 클로저 안에서 사용할 수 있다.

withUnsafeBytes 또한 ArrayData의 인스턴스 메모리로서 사용할 수 있다.

체크섬 계산하기
withUnsafeBytes(of:)를 사용하면 결과를 반환할 수 있다. 이것의 사용에대한 예제는 구조체에서 32비트 체크섬의 바이트를 계산하는 것이다.

아래 코드를 플레이그라운드에 추가하자.
do {
  print("Checksum the bytes of a struct")
  var sampleStruct = SampleStruct(number: 25, flag: true)
  let checksum = withUnsafeBytes(of: &sampleStruct) { (bytes) -> UInt32 in
    return ~bytes.reduce(UInt32(0)) { $0 + numericCast($1) }
  }
  print("checksum", checksum) // prints checksum 4294967269

}