Accessibility for UIKit

접근성 관련해서 잘 지원해주는 플랫폼 중 하나는 애플이다. 아래는 애플의 접근성 관련 설명 페이지

https://developer.apple.com/documentation/uikit/accessibility_for_uikit

 

Supporting VoiceOver in Your App

여러 접근성 중 보이스오버가 가장 많이 적용하는 것 중 하나일 것이다. 아래 페이지는 애플 가이드 페이지

https://developer.apple.com/documentation/uikit/accessibility_for_ios_and_tvos/supporting_voiceover_in_your_app

  • The accessibilityLabel property provides descriptive text that VoiceOver reads when the user selects an element.
  • The accessibilityHint property provides additional context (or actions) for the selected element.

 

Accessibility for iOS Developer Guide 

iOS 접근성 관련해서 정말 잘 설명되어있는 가이드 페이지! 

https://a11y-guidelines.orange.com/en/mobile/ios/development/

 

iOS developer guide - Orange digital accessibility guidelines

VoiceOver must vocalize every informative element with a high degree of quality in order to let the user know and perfectly understand the context of the consulted page. The accuracy of this vocalization is done through four attributes: label, hint, value

a11y-guidelines.orange.com

 

Raywenderlich의 Accessibility 가이드 페이지

https://www.raywenderlich.com/6827616-ios-accessibility-getting-started

 

iOS Accessibility: Getting Started

In this iOS accessibility tutorial, learn how to make apps more accessible using VoiceOver and the Accessibility inspector.

www.raywenderlich.com

https://www.raywenderlich.com/4720178-ios-accessibility-tutorial-making-custom-controls-accessible

 

iOS Accessibility Tutorial: Making Custom Controls Accessible

In this iOS accessibility tutorial, you’ll learn to make custom controls accessible using VoiceOver, elements group, custom action, traits, frame and more.

www.raywenderlich.com

 

시뮬레이터에서 접근성 테스트 방법

XCode > Open Developer Tool > Accessibility Insepector

 

 


목차

  • UNUserNotificationCenter란?
  • Push Notification 사용 권한 요청
    1. UNUserNotificationCenterDelegate 채택
    2. Notification 권한 부여 요청
    3. APNS(Apple Push Notification Service)에 등록

 

UNUserNotificationCenter란?

앱과 앱 익스텐션에 대한 알림 관련 작업을 관리하기 위한 센트럴 오브젝트.
공유된 UNUserNotificationCenter 객체를 사용하여 앱과 앱 확장의 모든 알림 관련 동작을 관리한다.

 

Push Notification 사용 권한 요청 

알림에 응답하여 알럿을 표시하거나, 소리를 재생하거나, 앱 아이콘에 배지를 달 수 있는 권한을 요청하는 방법에 대해 알아보자.

 

1. UNUserNotificationCenterDelegate 채택

알림 관련 작업을 처리하려면 UNUserNotificationCenterDelegate 프로토콜을 채택하는 객체를 생성하여 이 객체의 델리게이트 프로퍼티에 할당한다. 이 작업은 해당 델리게이트와 상호작용을 할 수 있는 다른 작업을 수행 하기 전에 이 델리게이트 프로퍼티에 객체를 항상 할당하자.

class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        return true
    }
}

 

2. Notification 권한 부여 요청

승인을 요청하려면, UNUserNotificationCenter 인스턴스를 가져와서 requestAuthorization(options:completionHandler:) 을 호출하자. (alert, sound, badge 타입 요청 가능)

let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
    
    if let error = error {
        // Handle the error here.
    }
    
    // Enable or disable features based on the authorization.
}

 

3. APNS(Apple Push Notification Service)에 원격 알림을 수신하도록 등록

Apple Push Notification Service에 등록 프로세스를 시작하려면 아래 메소드를 호출하자. (Main Thread에서 진행해야함.)

DispatchQueue.main.async {
    UIApplication.shared.registerForRemoteNotifications()
}

등록이 성공되었다면, 앱은 앱 델리게이트 객체의 application(_:didRegisterForRemoteNotificationsWithDeviceToken:) 메소드를 호출하여 디바이스 토큰을 전달함. (디바이스의 원격 알림을 생성하기 위해서는 서버에 이 토큰을 전달해야함.)

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
}

등록이 실패되었다면, 앱은 앱 델리게이트 객체의 application(_:didFailToRegisterForRemoteNotificationsWithError:) 메소드를 호출한다.

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
}

 

 

Ref.

C언어 기반의 프로그래밍 언어에서는 main이라는 함수가 앱의 시작이 된다.
iOS 앱 또한 Objective-C 기반에서 돌아가기 때문에 앱은 main 함수에서 시작된다.

 

iOS 애플리케이션 실행 순서

  1. main 함수 실행
  2. main 함수에서 UIApplicationMain 함수 실행 (앱의 본체에 해당하는 객체인 UIApplication 객체 생성)
  3. Info.plist 파일을 읽어들여 파일에 기록된 정보를 참고하여 그 외에 필요한 데이터 로드.
    (Main nib 파일을 사용하는 경우 이 과정에서 로드됨.)
  4. Main nib 파일이 없거나 그 속에서 앱 델리게이트가 없는 경우, 앱 델리게이트 객체를 만들고 앱 객체와 연결.
  5. 런루프를 만드는 등 실행에 필요한 준비를 마무리함.
  6. 실행 완료를 앞두고, 앱 객체가 앱 델리게이트에게 application:didFinishLaunchingWithOptions: 메시지를 보냄.

Objective-C 기반 iOS 프로젝트 main.m 파일

 

UIApplicationMain 함수: 앱 로딩 프로세스

파라미터

  • principalClassName: 앱 객체가 될 클래스 이름. nil이면 UIApplication으로 대체
  • appDelegateClassName: 앱 델리게이트 클래스 이름.

기능

  • 앱의 본체에 해당하는 객체인 UIApplication 객체 생성, 이 객체는 앱의 Life Cycle 관리
  • 지정된 클래스(@UIApplicationMain이 표시된)에서 델리게이트를 인스턴스화 하고 이를 앱의 객체에 할당
  • 앱의 Run Loop를 포함한 기본 이벤트처리 루프를 설정하고 이벤트 처리 시작
  • 앱의 info.plist에 불러올 main nib 파일이 명시되어있으면, 해당 nib 파일을 불러옴.

 

 

이제 @main에 대해 알아보자.

 Swift 기반으로된 iOS 프로젝트는 main 파일이 없다. 그이유는?
@main을 선언하여 iOS 앱의 진입점을 컴파일러가 합성하기 때문.

@main을 선언해줌으로써, UIKit 앱의 진입점은 해당 클래스이고, 시스템은 UIApplicationDelegate 프로토콜에 구현되어있는 정적 main() 함수를 호출함.

 

(추가) Swift 5.3부터 프로그램의 entry point를 알려주는 속성이 @UIApplicationMain에서 @main으로 변경됨.

 

Ref.

목차

  1. Subscript Syntax
  2. Subscript Usage
  3. Type Subscripts

Subscripts

 

Classes, structures, and enumerations can define subscripts, which are shortcuts for accessing the member elements of a collection, list, or sequence.  

 

(subscript - 컬렉션, 리스트, 시퀀스의 멤버 엘리먼트에 접근하기 위한 shortcut을 제공)

ex) someArray[index], someDictionary[key], ...

 

 

1. Subscript Syntax

- subscripts can be read-write or read-only.

subscript(index: Int) -> Int {
    get {
        // Return an appropriate subscript value here.
    }
    set(newValue) {
        // Perform a suitable setting action here.
    }
}

 

2. Subscript Usage

struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows * columns)
    }
    func indexIsValid(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0 && column < columns
    }
    subscript(row: Int, column: Int) -> Double {
        get {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            return grid[(row * columns) + column]
        }
        set {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            grid[(row * columns) + column] = newValue
        }
    }
}

 

 

3. Type Subscripts

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
    static subscript(n: Int) -> Planet {
        return Planet(rawValue: n)!
    }
}
let mars = Planet[4]
print(mars)

 

 

 

Ref.

https://docs.swift.org/swift-book/LanguageGuide/Subscripts.html

https://kka7.tistory.com/118?category=919617

Methods

목차

  1. Instance Method
  2. Type Method

Class, Structure, Enumeration 모두 생성 가능

 

1. Instance Method

Modifying Value Types from Within Instance Methods

  • 기본적으로, Value 타입의 property는 인스턴스 메소드 안에서 수정될 수 없음.  (여기서말하는 value 타입은 struct, enum)
  • mutating 키워드 : 메소드에서 property의 수정이 가능하도록 정의하기 위해 추가
struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// Prints "The point is now at (3.0, 4.0)"

 

2. Type Method

  • 타입 자체에서 호출되는 메소드
  • func 키워드 앞에 static 키워드 작성
  • Objective-C에서는 Class에서만 type-level method를 정의할 수 있었다. Swift에서는 모든 클래스, 구조체, 열거형에서 정의 가능
class SomeClass {
    class func someTypeMethod() {
        // type method implementation goes here
    }
}
SomeClass.someTypeMethod()
struct LevelTracker {
    static var highestUnlockedLevel = 1
    var currentLevel = 1

    static func unlock(_ level: Int) {
        if level > highestUnlockedLevel { highestUnlockedLevel = level }
    }

    static func isUnlocked(_ level: Int) -> Bool {
        return level <= highestUnlockedLevel
    }

    @discardableResult
    mutating func advance(to level: Int) -> Bool {
        if LevelTracker.isUnlocked(level) {
            currentLevel = level
            return true
        } else {
            return false
        }
    }
}

 

 

 

 

 

Ref.

https://docs.swift.org/swift-book/LanguageGuide/Methods.html

https://kka7.tistory.com/117?category=919617

https://developer.apple.com/documentation/swift/expressiblebystringliteral

 

ExpressibleByStringLiteral - Swift Standard Library | Apple Developer Documentation

Protocol ExpressibleByStringLiteral A type that can be initialized with a string literal. Declarationprotocol ExpressibleByStringLiteral OverviewThe String and StaticString types conform to the ExpressibleByStringLiteral protocol. You can initialize a vari

developer.apple.com

Overview

The String and StaticString types conform to the ExpressibleByStringLiteralprotocol. You can initialize a variable or constant of either of these types using a string literal of any length.

Conforming to ExpressibleByStringLiteral

To add ExpressibleByStringLiteral conformance to your custom type, implement the required initializer.

 


https://developer.apple.com/documentation/swift/customstringconvertible

 

CustomStringConvertible - Swift Standard Library | Apple Developer Documentation

Types that conform to the CustomStringConvertible protocol can provide their own representation to be used when converting an instance to a string. The String(describing:) initializer is the preferred way to convert an instance of any type to a string. If

developer.apple.com

Overview

Types that conform to the CustomStringConvertible protocol can provide their own representation to be used when converting an instance to a string. The String(describing:) initializer is the preferred way to convert an instance of any type to a string. If the passed instance conforms to CustomStringConvertible, the String(describing:) initializer and the print(_:) function use the instance’s custom description property.

Accessing a type’s description property directly or using CustomStringConvertible as a generic constraint is discouraged.

Conforming to the CustomStringConvertible Protocol

Add CustomStringConvertible conformance to your custom types by defining a description property.

For example, this custom Point struct uses the default representation supplied by the standard library:

 


Example

enum CompassPoint {
    case north
    case south
	case custom(String)
}

extension CompassPoint: ExpressibleByStringLiteral {
	init(stringLiteral value: String) {
        switch value {
        case "north":
            self = .north
        case "south":
            self = .south
		default:
        	self = .custom(value)
		}
	}
}

extension CompassPoint: CustomStringConvertible {
	public var description: String {
        switch self {
        case .north:
	        return "north"
        case .south:
        	return "south"
        case .custom(let type):
            return type
        }
    }
}

 

'iOS > Swift' 카테고리의 다른 글

[Swift 5.2] Subscripts - Apple Documentation  (0) 2020.03.05
[Swift 5.2] Methods - Apple Documentation  (0) 2020.03.05
[Swift4.1] map, flapMap, compactMap  (0) 2019.12.11
[Swift 5.2] Properties - Apple Documentation  (0) 2019.12.08
[Swift5.1] Overview  (0) 2019.12.04

목차

  1. Basic Concept
  2. View
    1) 뷰 정의하기
    2) bind(reactor:) 이란?
  3. Reactor = ViewModel
    1) mutate()
    2) reduce()
    3) transform()
  4. Advanced
    - Global States
    - View Comminucation

 

 

1. Basic Concept

  • ReactorKit is a combination of Flux and Reactive Programming.
  • 유저의 액션과 뷰 상태는 observable stream을 통해 각 레이어로 전달됨
  • 이 스트림들은 한방향이다: 뷰는 오직 액션만 방출할 수 있고 리액터는 상태만 방출할 수 있다.

 

2. View

  • 뷰는 데이터를 보여주는 역할.
  • 뷰는 사용자의 인풋을 액션스트림에 바인딩하고, 뷰 상태를 UI 컴포넌트에 바인딩함.
  • 뷰 레이어에는 비지니스 로직이 없다. 뷰는 단지! 액션 스트림과 상태 스트림을 보여주는 방법을 정의하는 것

1) 뷰 정의하기

  • 뷰를 정의하기 위해서, 존재하는 클래스에 View 프로토콜을 컨펌시키자. 그러면 reactor라는 프로퍼티를 자동으로 가질 수 있다.
  • 이 프로퍼티는 일반적으로 view의 외부에 설정된다. (뷰 외부에서 reator 주입시키는 것이 일반적)
class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // inject reactor(reactor 주입)

 

2) bind(reactor:) 란?

  • => 뷰와 리엑터 사이의 액션 스트림상태 스트림을 바인드하기 위한 메서드
  • reactor 프로퍼티가 변경되면, bind(reacor:)가 호출된다.
func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

 

NOTE
ReatorKit에서 말하는 액션 스트림상태 스트림은 아래 Reactor 클래스의 구현부를 보면 알 수 있다.
뷰에 있는 UI 컴포넌트에 rx를 이용하여 해당 Observable을 reactor의 action이 구독한다. 
그리고, reactor.state인 Observable을 뷰의 UI 컴포넌트가 구독한다.

이와 같이 단방향으로 액션 스트림과 상태스트림으로 Action과 State 데이터가 이동한다.

 

3. Reactor = ViewModel

  • Reactor는 뷰의 상태를 관리하는 UI 독립적인 계층
  • Reactor의 가장 중요한 역할은 뷰로부터 제어 흐름을 분리하는 것
  • 모든 뷰는 해당 Reacor를 가지고 있으며, 모든 로직을 그 Reactor에 위임한다.
  • Reactor는 뷰의 의존성이 없기 때문에 테스트하기가 용이하다.

리엑터를 정의하기 위해서는 Reactor 프로토콜을 준수하자!

세개 타입 Action, Mutation, State. + initialState 프로퍼티 필수!

Action은 사용자의 인터렉션, State는 뷰의 상태

class ProfileViewReactor: Reactor {
  // represent user actions (유저 액션을 나타냄)
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // represent state changes (상태 변화를 나타냄)
  enum Mutation {
    case setFollowing(Bool)
  }

  // represents the current view state (현재 뷰 상태를 나타냄)
  struct State {
    var isFollowing: Bool = false
  }

  let initialState: State = State()
}

 

(중요)

리엑터는 2단계로 액션 스트림을 상태 스트림으로 변경한다:  mutate() reduce().

 

1) mutate()

View로부터 Action을 받고, Observable<Mutation>을 생성한다

func mutate(action: Action) -> Observable<Mutation>

 

비동기 오퍼레이션 또는 API 호출과 같은 모든 사이드 이펙트는 이 메소드 안에서 수행된다

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }

  case let .follow(userID):
    return UserAPI.follow()
      .map { _ -> Mutation in
        return Mutation.setFollowing(true)
      }
  }
}

 

2) reduce()

기존 State과 Mutation으로부터 새로운 State를 생성

func reduce(state: State, mutation: Mutation) -> State

이 메소드는 순수함수(pure function)이므로 동기적으로 새로운 State를 리턴해야한다. 이 함수안에서 어떤 side effect들을 수행하지 말자.

func reduce(state: State, mutation: Mutation) -> State {
	var state = state 	// create a copy of the old state
    switch mutation {
    case let .setFollowing(isFollowing):
    	state.isFollowing = isFollowing // manipulate the state, create a new state
        return state	// return the new state
    }
}

 

3) transform()

transform()은 각 스트림을 변형한다. 세 transform() 함수가 존재한다:

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>

 

다른 observable 스트림을 결합하고 변경하려면 이 함수들을 구현하자. 

예를들어, transform(mutation:)은 global event stream을 mutation stream에 결합하기 위한 좋은 방법이다. Global state는 아래에서 자세히 살펴보자.

 

이 메소드는 디버깅을 위한 목적으로 사용될 수 있다.

func transform(action: Observable<Action>) -> Observable<Action> {
  return action.debug("action") // Use RxSwift's debug() operator
}

 

 

4. Advanced

Global States

  • Reactor의 일반적 Action → Mutation → State 흐름에 global state는 없으므로, global state 변경을 위해서 transform(muation:)을 사용해야 한다.
  • 예) 현재 인증된 유저들을 저장하는 global BehaviorSubject가 있다고 가정. 만약 currentUser가 변경되었을 때 Mutation.setUser(User?)를 방출하기를 원한다면 아래와 같이 작성. 그리고 그 mutation은 뷰가 액션을 리엑터에게 보낼 때 각 시간에 방출될 것이다.
var currentUser: BehaviorSubject<User> // global state

func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
  return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}

 

View Communication

  • ReactorKit은 global app state를 정의하고 있지 않으므로, global state를 관리하기 위해서 BehaviorSubject, PublishSubject, reactor를 사용할 수 있다.
  • 여러개의 뷰들을 커뮤니케이션하기 위해서는 callback 클로저나 delegate 패턴을 사용하는 것이 일반적.
  • ReactorKit은 reactive extension을 사용하는것을 추천한다.
  • 주요 목적은 커스텀한 뷰에 대한 커뮤니케이션을 해결하기 위한 것.

 

예) ChatViewController는 메시지를 보여주는 뷰컨트롤러. ChatViewController는 MessageInputView를 가지고 있다.

유저가 MessageInputView의 보내기 버튼을 탭하면, 그 텍스트는 ChatViewController에 보내질 것이고, ChatViewController는 리엑터의 액션과 바인드될 것이다. MessageView의 reactor extension의 예이다:

extension Reactive where Base: MessageInputView {
  var sendButtonTap: ControlEvent<String> {
    let source = base.sendButton.rx.tap.withLatestFrom(...)
    return ControlEvent(events: source)
  }
}

ChatViewController에서 messageinputView extension를 아래와 같이 사용하자:

messageInputView.rx.sendButtonTap
  .map(Reactor.Action.send)
  .bind(to: reactor.action)

 

 

 

 

 

Ref.

https://github.com/ReactorKit/ReactorKit

결론
iOS는 디바이스 크기 별로 할당되는 content area의 Size classes의 값을 토대로 다이나믹하게 layout 조정을 한다.
- Size classes: 디바이스 별로 시스템이 두개로 정의함 (compact, regular)

 

Adaptivity and Layout

  • In iOS, interface elements and layouts can be configured to automatically change shape and size on different devices, during multitasking on iPad, in split view, when the screen is rotated, and more.
  • It’s essential that you design an adaptable interface that provides a great experience in any environment.

Auto Layout

  • Auto Layout is a development tool for constructing adaptive interfaces.
  • Using Auto Layout, you can define rules (known as constraints) that govern the content in your app.
  • Auto Layout automatically readjusts layouts according to the specified constraints when certain environmental variations (known as traits) are detected. 

Layout Guides and Safe Area

Layout guides define rectangular regions that don’t actually appear visibly onscreen, but aid with the positioning, alignment, and spacing of content. The system includes predefined layout guides that make it easy to apply standard margins around content and restrict the width of text for optimal readability. You can also define custom layout guides.

 

Size Classes

  • In iOS, Size Classes are groups of screen sizes that are applied to the width and height of the device screen
  • The system defines two size classes. The two Size Classes that exist currently are Compact and Regular.
    - The Compact Size Class refers to a constrained space. It is denoted in Xcode as wC (Compact width) and hC (Compact height).
    - The Regular Size Class refers to a non-constrained space. It is denoted in Xcode as wR (Regular width) and hR (Regular height).
  • As with other environmental variations, iOS dynamically makes layout adjustments based on the size classes of a content area.
    (For example, when the vertical size class changes from compact height to regular height, perhaps because the user rotated the device from landscape to portrait orientation, tab bars may become taller.)

 

 

 

Ref.

https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/

https://www.bignerdranch.com/blog/designing-for-size-classes-in-ios/

 

'iOS > iOS 기본기' 카테고리의 다른 글

토큰 기반의 인증  (0) 2019.12.21
[iOS] Delegate, Notification, KVO 비교 및 장단점 정리  (0) 2019.12.10
[iOS] Layout - Safe Area  (0) 2019.12.09
[iOS] frame vs. bounds  (0) 2019.12.09
[iOS] int vs NSInteger vs NSNumber  (0) 2019.12.05

+ Recent posts