iOS 13 앱 업데이트 후기

Background

iOS 13에 다크 모드가 추가돼서 개인 앱 보안카드 위젯을 오랜만에 업데이트했다. 이왕 하는 김에 9000줄 정도의 옵젝씨 코드도 전부 스위프트 전환을 했고 기존에 쓰던 C++로 된 SMTP 라이브러리도 더 가벼운 스위프트 라이브러리로 바꿨다. Pod도 전부 제거하고 이제는 카르타고로 파이어베이스만 외부에서 가져와 빌드를 하고 있다. 앱 용량도 15메가에서 6메가 정도로 줄어서 기분이 좋다. 오래된 집을 깔끔하게 리모델링한 기분이다.

1️⃣ .pageSheet 모달

개발자 입장에서 가장 충격적인 변화는 modal presentation의 기본 값이 바뀌었다는 점이다. 기존에는 fullScreen이 기본이었고 화면 전체를 가렸다. iOS 13에서는 pageSheet가 기본이 되었고 이 프레젠테이션은 화면 전체를 가리지 않고 상단에 살짝 걸쳐 있다. 그리고 화면을 드래그해서 뷰를 내릴 수 있다. iOS 13이 나오기 전부터 페이스북 앱에서는 외부 링크용 웹뷰가 이런식으로 되어있었고 지도나 내비게이션 앱, AR 앱 등에서 최근 급격히 많이 보이기 시작했다. 이와 관련된 오픈소스도 굉장히 많이 등장했다.

아이폰의 크기가 많이 커진 상황에서 기존의 모달을 닫으려면 스크린 맨 위에 있는 버튼에 닿기 위해 위태롭게 그립을 바꾸거나 다른 손을 써야 했었다. 하지만 손가락을 멀리 뻗지 않고도 드래그 제스처로 뷰를 내릴 수 있다는 점에서 이런 형태의 모달이 개인적으로 매우 유용했고 앞으로 아이폰 앱에서 쓸 일이 많을 것이라고 생각했었다. 그래서 네이티브로 이런 UI를 사용할 수 있게 돼서 기쁜 한편, 이렇게 하위 호환성을 깨뜨리는 방식으로 급격히 변화를 가져 올 줄은 몰랐다. Modernizing Your UI for iOS 13에서 변경점에 대해 아래처럼 발표를 한다.

‘여러분, 새로운 모달 프레젠테이션 스타일입니다! 사용자에게 더 많은 비주얼 컨텍스트를 제공합니다. 인터랙티브하게 dismiss 할 수도 있습니다. 멋지죠? 어떻게 쓰면 되나고요? 그냥 아무것도 안하면 됩니다. 왜냐면 저희가 기본 값을 이걸로 바꿔버렸기 때문이죠. 예전처럼 동작하게 만들고 싶으시다고요? 코드를 한 줄만 추가해주면 됩니다!’

예전처럼 동작하려면 코드를 추가해야 한다니! 그리고 이걸 이렇게 기쁜 표정으로 자랑하듯이 발표하다니. 이런 태도, 뻔뻔함이 애플의 상징처럼 느껴져서 황당하면서 엄청 재밌었다. Breaking change 까지는 아니지만 이번 모달의 변화는 거의 에러 못지 않게 기존 UI와의 호환성을 깨뜨리거나 유저 플로우에 큰 영향을 준다. 특히 모달을 벗어나는 경로가 새로 생긴 것에 대한 추가 작업이 필수이다.

예전에는 모달 화면을 나갈 수 있는 유일한 방법은 앱 개발자가 추가한 코드를 통해서 밖에 없었다. 하지만 pageSheet 모달을 사용하면 드래그 제스처가 자동으로 추가된다. 여기에 개발자가 관여할 수 있는 방법은 두 가지가 있다. UIViewController의 isModalInPresentation을 true로 하면 아예 유저가 드래그로 dismiss 할 수 없게 된다. 또는 UIAdaptivePresentationControllerDelegate에 새롭게 추가된

func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool

에서 지정할 수도 있다.

마지막으로 유용한 델리게이트 하나는

func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController)

isModalInPresentation이 true거나 presentationControllerShouldDismiss 메서드가 false 를 리턴하면 유저의 드래그 dismiss가 실패하면서 이 메서드가 불린다. 사용자가 작업해 둔 내용물이 있을 때 실수로 날리지 않게 이때 정말 화면을 나갈 것이냐를 확인하는 얼럿이나 액션 시트를 띄워주면 된다.

2️⃣ @propertyWrapper

Swift 5.1에 추가된 프로퍼티 래퍼가 매우 유용했다. 프로퍼티 래퍼는 변수의 저장소, 캐싱 전략, 바운딩 로직 등등 변수의 특성과 관련된 로직을 분리하여 재사용할 수 있게 해주는 기능인데 총 4가지의 프로퍼티 래퍼를 만들어서 썼다.

  • SharedDefaults: UserDefaults에서 값을 가져오는 래퍼.
  • SharedKeychain: Keychain에서 값을 불러오는 래퍼.
  • LateInitialized: 변수를 사용하기 전에 무조건 값을 세팅해줘야 하는 래퍼. IUO를 제거할 수 있다. 전부터 unspecified()로 만들어서 쓰던 것을 대체했다. 굳이 lazy 키워드를 써주지 않아도 돼서 실수 유발 가능성도 없고, 원하면 immutable 하게도 만들 수 있어서 훨씬 사용성이 좋다.
  • ClampedString: 지정해둔 max 길이를 넘어갈 수 없는 String 래퍼

프로퍼티 래퍼는 스위프트의 API 디자인 가이드라인에서 가장 중요시 하는 원칙 Clarity at the point of use에 딱 부합하는 기능인 것 같다.

3️⃣ SF Symbols

정-말 좋다. 특히 사이드 프로젝트 앱 개발을 할 때 무료 아이콘을 쓰겠다고 이곳 저곳에서 원하는 걸 가져오다 보면 통일성도 떨어지고 무엇보다 퀄리티가 너무 아쉽다. SF Symbol은 정말 많고 다양해서 용도가 무궁무진하다. 게다가 이미 시스템 앱들이 쓰고 있다. 보안카드 위젯 앱에서도 기존에 쓰던 아이콘들을 전부 대체했고 텍스트로 쓰던 부분도 적절한 아이콘으로 교체할 수 있었다.

SF Symbol은 두 가지 방법으로 사용할 수 있다. 첫번째는 이미지로 쓰는 것이다. UIImage(systemName:) 생성자를 사용해서 이미지를 만들 수 있다. 두번째는 텍스트로 쓰는 것이다. 원리가 뭔지는 모르겠지만 심볼은 SF(샌프란시스코) 폰트에 포함되어있다. 그래서 SF 폰트로 설정된 UILabel이나 UITextView 등에서 이모티콘처럼 텍스트와 함께 사용할 수 있다. 심지어 폰트 두께와 크기에 따라서 아이콘도 바뀐다. 이미지로 쓸 때도 두께와 크기를 변경할 수 있다.

관련 링크

4️⃣ 다크 모드


사용자에게 이번 업데이트의 최대 하이라이트는 다크 모드겠지만 개발 관점에서 딱히 특별하거나 재밌는 것은 없었다. 속된 말로 노가다지만, 하고 나서 결과물을 보면 뿌듯하고 개운한 느낌이랄까. 어두운 배경의 새로운 색을 조합하고, xcasset에 named color를 만들어서 적용하는 것 뿐이다. 네임드 컬러는 Xcode 9부터 등장했고 iOS 11부터 지원한다. 네임드 컬러를 만들어 놓고 UIColor(named:) 로 생성하거나 스토리보드 컬러 피커에서 고를 수도 있어서 사용이 편리하다.

또한 다크 모드 대응의 일환으로 애플에서 시스템 컬러와 다이나믹 시스템 컬러라는 것을 새로 만들었다. ‘system’ 이라는 접두어가 쓰인 색을 쓰면 자동으로 다크모드와 라이트모드에서 각각 적절한 색이 사용된다. 다이나믹 시스템 컬러라는 것은 배경, 컨텐츠용 텍스트, separator 등등 색의 사용처 기준으로 색의 이름을 지은 것이다. 색 이름이 label, secondaryLabel, tertiaryLabel, separator, link 등등 처럼 되어있어서 의미에 맞춰 사용하면 된다. 또한 다크모드 대응된 회색도 6가지 종류나 생겼다. HIG 설명에 따르면 투명도가 잘 안 먹히는 상황에서 드물게 쓰라고 하는데 이건 디자인쪽 언어인거 같아서 무슨 말인지 잘 이해가 안간다.

개발하다가 공식 문서를 읽지 않아서 디버깅이 오래 걸렸던 이슈 하나는 다크모드 / 라이트모드 전환을 할때 어떤 뷰들만 색이 전환이 안되는 이슈였다. 알고보니 UIColor 객체를 바로 사용하면 자동으로 전환이 되지만 CGColor 값으로 사용하면 개발자가 수동으로 다크 모드 전환에 대응했어야 하는 것이다. (참고: Standard Colors). 그래서 커스텀 뷰의 CAShapeLayer에 적용해놓은 borderColor와 fillColor는 다크 모드 전환이 안되고 있었다. 따라서 다크모드 전환이 될 때 불리는 적절한 메서드 내에서 CGColor 값을 지정해주어야 한다.

종합적으로 느낀 점은 언제나 그랬듯 애플이 기본으로 제공해주는 것들만 적절히 잘 써도 디자이너의 도움 없이도 그럴듯하게 다크모드를 적용할 수 있다.

Tags: iOS 13, dark mode, propertyWrapper, SF symbols  

경험 발표의 가치

컨퍼런스나 밋업의 발표는 세가지로 분류해 볼 수 있다.

  1. 첫번째로는 가장 먼저 떠오르는 기술 발표. 누군가는 어떤 프레임워크를 깊게 파봤다거나, 특정 기술을 한계까지 밀어붙여봤을 것이다. 아예 새로운 걸 만들었을 수도 있다. 이걸로 뭘 할 수 있고 어떻게 잘 쓸 수 있는지 얘기하는 것이 기술 발표의 핵심이다.

  2. 다음으로는 영감을 주는 발표도 있다. 기술적인 내용은 거의 없거나 비중이 낮지만 청중들에게 아이디어를 심어주고 새로운 시도를 해보게끔 독려를 한다. 또는 이미 하고 있던 것을 계속해서 하도록 동기 부여를 할 수 도 있다.

  3. 마지막으로 경험 발표가 있다. 경험 발표는 누구나 시도해볼 수 있다. 어떤 문제가 있었는데 이걸 어떻게 해결했다던지, 아니면 어떻게 개발을 시작하게 되었는지도 정말 좋은 경험 공유가 될 수 있다. 중요한 것은 경험을 통해 자신이 배운 점을 전달하는 것이다.

경험 발표가 값진 이유는 ‘과정’이 녹아있기 때문이다. 모든 사람은 유일무이하다. 같은 일도 각자만의 방식으로 경험하고 같은 지식도 다른 사고를 거쳐 습득한다. 따라서 발표자의 ‘배운 점’이 이미 알려져 있는 것일지라도 어떤 과정과 방식으로 그걸 배웠는가가 듣는 개발자들에게는 특별한 자산이 된다. 프로그래머인 우리는 매일 어려운 문제에 부딪히고 문제를 해결한다. 해답이 아니라 방법을 알고 있으면 처음보는 미래의 문제들도 해결할 능력을 갖추게 되는 것이다.

그래서 하고 싶은 말은,

👉 레츠스위프트 2019 발표자 신청

꼭 iOS 개발자가 아니어도, 스위프트/오브젝티브-씨를 몰라도, 경력의 종류와 양과 상관없이, 그리고 개발자가 아니어도 좋습니다. 당신의 기술과 동력과 경험을 나눠주세요.

Tags: letswift, cfp, presentations  

BookStore iOS 샘플 앱

아래 글은 BookStore-iOS 프로젝트의 소개 글의 한글 버전입니다.

앱 개요

이 샘플 앱은 IT Bookstore API를 사용하여 최신 프로그래밍 책을 검색해볼 수 있는 앱이다.

해당 프로젝트는 Result 타입 활용, 유닛 테스팅, UI 테스팅, 네트워크 stubbing, 프레임워크 분리, 문서화 주석(documentation comment) 작성 등의 예제를 포함하고 있다.

내용

  • Result 타입
  • 유닛 테스트에서 네트워크 stubbing
  • Mock 데이터로 UI 테스팅 하기
  • 앱 로직을 프레임워크로 분리하기
  • 문서화 주석 작성
  • Implicitly Unwrapped Optional 제거하기

Result 타입

보통은 Result 인스턴스에서 Success나 Failure 값에 접근하고 싶을때 스위치문을 쓰게 된다.

switch result {
case .success(let response):
  //response 객체 사용
case .failure(let error):
  //에러 처리
}

하지만 스위치문은 너무 문법이 길고 불필요한 case까지 써줘야하는 단점이 있다. 그래서 successcatch라는 익스텐션 메서드를 추가해서 Result 인스턴스 값을 접근하는 데에 사용하면 매우 용이하다.

searchResult.success { response in
  //response 객체 사용
}.catch { error in
  //에러 처리
}

혹은

result.success(handleSuccess)
      .catch(handleError)
      
func handleSuccess(_ result: SearchResult) { ... }
func handleError(_ error: Error) { ... }

유닛 테스트에서 네트워킹 Stubbing

테스트에서 실제 네트워킹을 하는 것은 바람직한 방식이 아니다. 유닛 테스트에 지나치게 불확실한 의존성이 생기기 때문이다. URLSession을 사용하고 있다면 URLProtocol의 서브클래스를 만들어서 stubbing을 할 수 있다.

1. URLProtocol 서브클래스 생성

MockURLProtocol 참고

2. MockURLProtocolURLSession에 연결

let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]

//해당 session으로 네트워크 요청을 수행
let session = URLSession(configuration: config) 

4. 원래 쓰던 것처럼 URLSession 사용

session.dataTask(with: urlRequest) { (data, response, error) in
  //Mock 데이터가 전달됨
}.resume()

Mock 데이터로 UI 테스팅 하기

유닛 테스트에서 사용하는 위 방식은 UI 테스팅에서는 작동하지 않는다. 테스트 번들과 XCUIApplication이 돌아가는 앱 번들이 서로 다른 프로세스에서 실행되기 때문이다. Swifter라는 라이브러리를 쓰면 간단한 로컬 HTTP 서버를 시뮬레이터 내부에서 돌릴 수 있어서 앱 네트워킹을 localhost 서버로 보낼 수 있게 된다.

먼저 launchArguments로 값을 전달하여 UI 테스팅 중에만 host를 바꿔준다.

//XCTestCase 내부
override func setUp() {
  app = XCUIApplication()
  app.launchArguments = ["-uitesting"]
}

//AppDelegate의 application(_:didFinishLaunchingWithOptions:) 메서드 내부
if ProcessInfo.processInfo.arguments.contains("-uitesting") {
  BookStoreConfiguration.shared.setBaseURL(URL(string: "http://localhost:8080")!)
}

아래처럼 Swifter 서버 인스턴스에 mock 데이터를 세팅해준다.

let server = HttpServer()

func testNewBooksNormal() {
  do {
    let path = try TestUtil.path(for: normalResponseJSONFilename, in: type(of: self))
    server[newBooksPath] = shareFile(path)
    try server.start()
    app.launch()
  } catch {
    XCTAssert(false, "Swifter Server failed to start.")
  }
        
  XCTContext.runActivity(named: "Test Successful TableView Screen") { _ in
    XCTAssert(app.tables[tableViewIdentifier].waitForExistence(timeout: 3))
    XCTAssert(app.tables[tableViewIdentifier].cells.count > 0)
    XCTAssert(app.staticTexts["9781788476249"].exists)
    XCTAssert(app.staticTexts["$44.99"].exists)
  }
}

앱 로직을 프레임워크로 분리

앱의 기능 로직을 별도 타겟 프레임워크로 분리하면 여러 장점이 있다. 유닛 테스트의 기준이 명확히 분리되고 디펜던시에 대해 고려할 수 밖에 없게 된다. 반면에 프레임워크 로딩 때문에 앱 실행이 조금 느려질 수 있다.

BookStoreKitIT Bookstore API와 통신하여 도서 정보와 검색하는 기능을 책임지는 프레임워크이다. Networking은 URLSession을 기반으로 HTTP 통신과 응답 데이터 파싱을 간편하게 해주는 프레임워크이다.

문서화 주식 작성하기

스위프트 API 디자인 가이드라인에서는 모든 선언문에 문서화 주석을 달기를 추천하고 있고, 이해하기 쉬운 설명을 다는 행위만으로도 설계에 좋은 영향을 준다고 한다.

1. 열심히 작성

문서에서 마크업 문법을 참고하여 작성한다.

예)

2. 결과물을 확인/활용 한다.

Xcode 자동완성 기능

도움말 보기 기능 (option + click)

Implicitly Unwrapped Optional 제거하기

스위프트에서 !는 경고 표시이다. 크래시가 발생할 수 있는 가능성을 내포하는 코드이기 때문에 최대한 쓰지 않는 것이 좋다. 아래 두가지 방식으로 iOS 개발에서 종종 사용되는 IUO를 줄일 수 있다.

IBOutlet을 옵셔널로 바꾸기

스토리보드에서 뷰컨트롤러로 IBOutlet을 연결하면 기본적으로 IUO로 생성된다. 하지만 아울렛이 반드시 IUO일 이유는 전혀 없다. 보통 아울렛으로 연결된 뷰들은 값을 넣는 작업이 많기 때문에 옵셔널로 바꾸더라도 if let이나 guard가 생기지 않는다.

lazy 초기화 활용

UIViewController에 프로퍼티를 추가할때 종종 IUO를 사용한다. 뷰컨트롤러의 생성자를 오버라이딩해서 값을 넣어줄 수 없기 때문인데 이 경우에도 약간의 코드로 IUO를 없애줄 수 있다. unspecified라는 함수를 하나 만들어서 아래처럼 lazy var로 선언을 해주면 된다. 이렇게 하면 위험한 IUO나 언래핑하기 번거로운 옵셔널을 쓰지 않아도 된다.

lazy var bookStore: BookStoreService = unspecified()

Tags: ios, swift, unit testing, ui testing, network mocking, frameworks, optionals