호댕의 iOS 개발

[TWL] 22.01.24 ~ 22.01.28 (UIRefreshController, CoreData, HIG Data entry, Subscript, convenience init, animation, pagination 본문

Software Engineering/TIL

[TWL] 22.01.24 ~ 22.01.28 (UIRefreshController, CoreData, HIG Data entry, Subscript, convenience init, animation, pagination

호르댕댕댕 2022. 2. 12. 12:08

어제에 이어 미처 포스팅하지 못했던 TWL을 업로드한다. 

 

이 주는 주로 프로젝트를 진행하며 새롭게 알게 된 내용들이 많았다. 

그럼 어떤 내용들이 있었는지 살펴보자. 


UIRefreshController

 

Apple Developer Documentation

 

developer.apple.com

대부분의 앱들을 보면 화면 가장 상단에서 화면 아래로 드래그를 했다가 손을 떼면 다시 데이터가 리로드되면서 리로드가 되는 표시가 나온다. 

    @objc private func handleRefrashControl() {
        reloadData()
        let scrollView = view as? UIScrollView
        DispatchQueue.main.async {
            scrollView?.refreshControl?.endRefreshing()
        }
    }

refreshControl에 addTarget을 해주기 위해 @objc 메소드로 데이터를 리로드하는 메서드를 만들었고 리로드를 하면 refreshControl이 종료되도록 구현했다. 

 

collectionView.refreshControl = UIRefreshControl()
collectionView.refreshControl?.addTarget(
    self,
    action: #selector(handleRefrashControl),
    for: .valueChanged
)

 

이후 refreshControl에 addTarget으로 action을 설정해주었다. 

 

CoreData

  • 민감한 정보를 저장하는 방식 ⇒ KeyChain
  • 가벼운 정보를 저장하는 방식 ⇒ UserDefault

그렇다면 Core Data는?

  1. 코어데이터는 DB일까? DB는 통합적으로 관리할 수 있는 데이터의 집합을 의미한다. 물론 CoreData가 데이터를 넣고 빼는 것을 관리하지만 데이터의 집합이라고 보긴 어렵다.
  2. 코어데이터는 ORM일까? 일단 ORM은 데이터를 변환해서 연결해주는 다리 역할을 한다. 매핑을 해주는 것이다. 실제로 Core Data에서 사용하는 DB(보통 SQL로 이뤄져 있음)에서 사용하기 위해 변환을 시켜준다. 따라서 어느정도 ORM 역할을 하긴 하지만 ORM이라고 보긴 어렵다.
  3. 그렇다면 코어데이터는 DBMS일까? DBMS는 데이터를 저장 및 관리하는 기능을 가지고 있다. 하지만 Core Data는 DBMS의 역할만 하진 않는다. CoreData는 하나의 정보를 다양한 기기에서 Sync를 맞춰 사용할 수 있도록 하며, 메시지에서 디바이스를 Shake했을 때 실행 취소를 하는 역할을 수행하기도 한다. 즉 더욱 다양한 역할을 하기 때문에 DBMS라고 보긴 어렵다.

따라서 CoreData는 CoreData이다.

 

HIG의 Data Entry

최근들어 HIG 문서를 거의 보지 못했는데 아카페라 커피 덕분에 HIG 문서를 볼 수 있었다. 여기서 프로젝트에 필요한 내용이 있었다! Data Entry로 사용자에게 데이터를 받을 때에 대한 가이드라인이 있었다.

일단 Data를 사용자에게 입력받을 때 너무 많은 시간이 소요되면 사람들이 앱을 포기할 수도 있다고 하고 있다. 그러면서 다음과 같은 조언?을 하고 있다.

  • 가능한 선택을 제시하라. 최대한 효율적으로 Data entry를 만들기 위해 picker나 table을 textfield 대신 사용하라고 권고하고 있다. 앞에 2개가 textfield보다 사용하기 쉽기 때문이다.
  • 가능한 시스템에서 정보를 가져와라. 연락처나 캘린더 같은 경우 사용자의 동의 하에 시스템에서 얻을 수 있다. 따라서 이런 정보는 사람들에게 제공할 것을 강요하지 마라.
  • 합리적인 기본 값을 제공해라. 가능한 가장 가능성이 높은 값을 필드에 미리 채워놔라. 이는 사람들이 결정하는 시간을 최소화한다.
  • 필요한 값을 수집한 후 진행할 수 있도록 해라. Next나 Continue 버튼을 누르기 전 필수 항목을 모두 작성했는지 확인해라. 버튼 활성화를 통해 아직 계속 입력해야된다는 것을 시각적으로 보여줘라.
  • 동적으로 필드의 데이터를 검증하라. 긴 양식을 작성하고 다시 돌아가서 다시 잘못된 부분을 수정하는 것은 매우 답답한 일이다. 가능한 field의 값을 즉각 체크할 수 있도록 해라.
  • field의 값은 정말 반드시 필요한 경우만 요구하라.
  • 값 목록을 통해 쉽게 찾을 수 있도록 해라. 특히 table이나 picker의 경우 값을 선택하는 것이 쉬워야 한다. 값 목록을 알파벳 순으로 정렬하거나, 빠른 검색 및 선택을 용이하게 하는 다른 논리적 방식으로 정렬하는 것을 고려해라.
  • 목적을 전달하는데 도움이 되도록 텍스트 필드에 힌트를 표시하라. textfield의 경우 내부에 텍스트가 없으면 placeholder text를 가질 수 있다. placeholder text를 작성할 자리가 충분하다면 다른 레이블을 통해 textfield를 설명하지 마라.

여기서 직접적으로 프로젝트에 적용시킨 것은 동적으로 필드의 데이터를 검증하라와 목적을 전달하는데 도움이 되도록 텍스트 필드에 힌트를 표시하라였다.

동적으로 필드의 데이터를 검증하기 위해 각각의 텍스트필드에 .addTarget(_:action:for:)를 사용했고 for는 .editingChanged를 통해 값이 변할 때마다 검증할 수 있도록 구현했다.

다만 이렇게 모든 상황을 실시간으로 검증하려 하니 코드가 굉장히 길어지는 문제가 있었다. 그래도 사용자의 사용성을 올려주는 것이 더 나은 것이겠지?

다만 TextView의 경우 .addTarget(_:action:for:)이 존재하지 않아 실시간으로 검증은 할 수 없었다.

 

subscript

프로젝트를 진행하다가 subscript도 함수처럼 작성할 수 있다는 것을 알았다.

기존에 array[index]로 접근할 경우 만약 없는 Index로 접근을 하게 되면 크래쉬가 발생하게 된다. 따라서 안전하게 값에 접근하기 위해 다음과 같이 작성해줄 수 있다.

extension Array {
    subscript(index: Int) -> Int? {
        guard self.count > index else {
            return nil
        }
        return self[index]
    }
}

만약 파라미터에 argument Label도 작성해주면 [] 내에 파라미터처럼 작성해줄 수도 있다.

 

convenience init

참고: https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

convenience init과 지정 init 사이의 관계는 아래의 규칙을 따른다. (여기서 지정 init은 viewController의 init(coder: NSCoder) 같은 것들을 말한다.

  1. 지정된 이니셜라이저는 슈퍼클래스에서 지정된 이니셜라이저를 호출해야 한다.
  2. convenience init은 반드시 동일한 클래스의 다른 이니셜라이저를 호출해야 한다.
  3. convenience init은 반드시 지정된 이니셜라이저를 호출해야 한다.
    convenience init?(
        coder: NSCoder,
        productId: Int,
        networkTask: NetworkTask,
        jsonParser: JSONParser
    ) {
        self.init(coder: coder)
        self.productId = productId
        self.networkTask = networkTask
        self.jsonParser = jsonParser
    }

따라서 여기서도 반드시 self.init(coder:)를 호출하고 프로퍼티를 초기화해야 한다.

animation

애니메이션을 구현할 수 있는 방법으론 크게 2가지가 존재한다.

  • UIView의 animate 메서드 (이는 CoreAnimation을 추상화한 것이다)
  • CoreAnimation을 사용

왜 버튼에 기능을 추가할 때 setTarget이 아니라 addTarget을 사용할까? 버튼에 target을 여러 개로 설정할 수 있기 때문이다.

여러 애니메이션을 묶어서 사용할 경우 어떻게 해야 할까? 이때 사용할 수 있는 메서드가 animateKeyframes이다.

이때 addKeyframe(withRelativeStartTime:relativeDuration:animations) 메서드를 호출해야함

  • withRelativeStartTime (0~1 사이에서 비율을 의미함) 여러 애니메이션에서 어느 정도 비율에서 시작을 할 지 정하게 된다. 만약 0.5를 하면 애니메이션 중 절반이 시작한 후 시작을 하게 된다.
  • relativeDuration 전체 애니메이션 중 어느 정도 비중을 차지하며 지속을 할 것인지 정하게 된다.
  • setNeedsLayout view의 레이아웃을 다시 계산해야 한다는 플래그를 세워두게 된다. 만약 view의 update cycle에 들어갔을 때 값이 바뀌게 된다.
  • layoutIfNeeded 예약된 layout을 바로 동기로 실행시키는 메서드이다. 따라서 setNeedsLayout과는 다르게 즉시 레이아웃을 변경하게 된다.

 

Pagination 구현

기존 토요 스터디 때 minimumLineSpacing이 있을 때 pagination을 구현해야 했었다. 이때는 contentInset을 minimumLineSpacing의 절반만큼 줘서 애초에 minimumLineSpacing만큼 컨텐츠의 인셋이 차지하도록 구현을 했었다.

하지만 이번에는 UIScrollViewDelegate의 scrollViewWillEndDragging 메서드를 사용해 구현을 했다. 일단 scrollViewWillEndDragging 메서드를 먼저 살펴보자.

이 메서드의 경우 사용자가 컨텐츠 스크롤을 마친 경우 알려주는 메서드이다. 파라미터는 아래와 같다.

  • scrollView: 사용자가 터치를 마친 스크롤 뷰 객체
  • velocity: 터치를 하고 손을 뗐을 때 스크롤 뷰의 속도
  • targetContentOffset: 스크롤 액션의 속도가 줄면서 멈춘다고 예상이 되는 offset

이때 targetContentOffset 매개변수 값을 변경해서 scrollView가 스크롤 애니메이션을 완료하는 위치를 조정할 수 있다.

프로젝트에서 사용한 코드

    private func changePageOffset(of targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        guard let layout = imagesCollectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        let cellWidthIncludingSpacing = layout.itemSize.width + layout.minimumLineSpacing
        var offset = targetContentOffset.pointee
        let index = round(offset.x / cellWidthIncludingSpacing)
        if index > imageIndex {
            imageIndex += 1
        } else if index < imageIndex, imageIndex != 0 {
            imageIndex -= 1
        }
        offset = CGPoint(x: imageIndex * cellWidthIncludingSpacing, y: 0)
        targetContentOffset.pointee = offset
    }

일단 위 코드에서 itemSize와 minimumLineSpacing이 UICollectionViewFlowLayout에 정의된 프로퍼티이기 때문에 UICollectionViewFlowLayout로 타입 캐스팅을 해주었다.

pointee: 포인터가 참조하는 인스턴스에 접근하는 인스턴스 프로퍼티이다. pointee 프로퍼티를 읽을 때 이 포인터가 참조하는 인스턴스는 이미 초기화가 되어 있어야 한다.

alert의 textField 추가

alert에는 addAction처럼 addTextField가 존재한다. 이를 통해 alert에 간단하게 텍스트 필드를 추가할 수 있다.

alert.addTextField() { textField in
    textField.placeholder = "암호"
    textField.textContentType = .password
    textField.isSecureTextEntry = true
}

해당 메서드에는 (UITextField) -> Void 클로저를 매개변수로 받고 있다. 따라서 위처럼 textField의 속성을 정해줄 수 있다.

여기서 isSecureTextEntry를 True로 설정할 경우 작성하는 비밀번호가 안보이게 처리가 된다!

Comments