Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Clone/Instagram] 새로운 기능 추가, 리펙터링 | 마주한 에러 & 개념 정리(UIResponder) #14 - 1 #28

Open
SHcommit opened this issue Dec 25, 2022 · 0 comments

Comments

@SHcommit
Copy link
Owner

새로운 기능 추가, 리펙터링

  • 특정 사용자 검색 후 해당 프로필 아래의 posts 중 하나 클릭시 상세 페이지로 들어감
  • PostController에서 부분적으로 동시성이 필요한 경우 async let. or TaskGroup로 동시성 적용.
  • API fetch 전부 async await 리펙터링

마주한 문제1

FeedController에서 리프레시할 때 에러를 발견했다. FeedController에서 리프레쉬를 하면 Index out of range가 뜬다.

에러0

에러2

FeedController 변수 posts를 화면에 띄운다. 리프레시할 때 현재 있는 posts변수를 RemoveAll한다.

에러1

그 후 fetchPosts() 함수를 호출해 데이터들을 fetch한 후에 posts(collectionView cell 구성하는 데이터) 값을 갱신한다. 그런데 이때 에러가 발생한다. 짐작되는 가장 큰 이유는 fetchPosts()는 async함수인 점이다. Task()에서 데이터를 fetch해야 posts가 갱신된다. 중요한 것은 비동기 함수라는 것.

refresh 이벤트가 발생 후 handleRefresh()로 포스트는 삭제됬다. 동시에 리프레쉬 동작으로 인해 컬랙션 셀들이 아래서 위로 올라오면서 아래 나올 가능성이 있는 1~2개의 cell을 dequeueReusableCell에서 꺼내온다. 이때 posts는 값이 없으므로 Index out of range 오류가 뜨는 것이다.

해결 방법은 리프레시 이벤트 헨들러에서 posts.removeAll()이후에 바로 컬랙션 뷰를 리로드 하는 것이다. 그래서 collectionView의 cell의 개수를 0으로 설정하는 것이다. 다른 대안이 있긴한데... 그냥 이 방법이 나쁘지 않은것 같다

마주한 문제2

Concurrency로 taskGroup가 아닌 async let 을 사용하려고 한다. In ProfileViewModel.swift

fetchToCheckIfUserIsFollowed() //사용자가 나를 팔로우했는지(bool)

fetchUserStats() //사용자의 팔로우, 팔로잉 상태

fetchImage(profileUrl:) 사용자의 이미지

이 세게를 동시에 fetch 하려고한다.

4

각각의 async 함수들은 안에서 에러를 잡아주기 때문에 async let 을 선언할 때 함수 throw 처리 생각하지 않아도 된다.

5

이렇게 Concurrency를 선언했다면 타입 표현이 ambigous하다는 경고가 나온다. 왜 그럴까

곰곰히 생각해봤는데 249라인에서 결국 배열의 각 원소는 Bool, Userstats, UIImage 원소가 있을 거고 이 세가지 타입을 모두 허용하는 배열은 없다. 그래서 배열의 타입은 Any가 된다. 그래서 프로퍼티 변수에 값을 대입할 때 위와 같은 에러가 발생하는 것 같다.

6

여러가지 종류의 값을 받을 수있는 튜플을 사용해서 해결했다.

만약 각각의 함수안에서 에러 처리를 하지 않은 상태라면

async let 변수 = try async 함수 를 한 후에

let _ = try await [변수1,변수2,...] 이런식으로 할 경우

각각의 에러 처리는 어떻게 해야 좋을지 고민했는데 아직 해결방안을 떠올리진 못했다.

새로 알게된 개념1

  • cell의 특정 객체 이벤트 발생 시 화면 전환

FeedCell에서 대화 버튼 클릭하면 navigation stack을 통해 CommentController Scene을 보여 주려고 한다.

t7

FeedCell의 타입은 UICollectionViewCell이다. 상위 View는 collectionView이다. 그래서 UICollectionReueableView 타입에 속하는데 이는 UIView 타입이다. 즉 UINavigationController를 상속받지 않았기 때문에 pushViewController(:aninamted:) 사용할 수 없다. 또한 UIViewController의 present(:animated:) 메소드도 사용할 수 없다. 결국 FeedCell에서 화면을 전환하는 경우는 FeedCell을 호출하는 FeedController의 힘을 빌려야 한다.

이벤트는 FeedCell의 @objc func didTapCommnet(_:)에서 발생한다. 어떻게 FeedController의 UIViewController나 UINavigationController의 객체를 빌려서 화면 전환을 할 수 있을까?

현재 생각할 수 있는 해결 방법은 두 가지이다.

  1. delegate 채택해서 FeedController에 FeedCell의 델리게이트 채택.

  2. Combine사용해서 publiser의 published 흐름에 따른 화면 전환.

  3. delegate 사용의 경우

  • 프로토콜 추가

8

  • FeedCell에 변수를 추가.

9

  • FeedCell에서 델리게이트 변수를 통한 함수 사용.

10

델리게이트 타입의 의존성을 갖는 delegate변수를 선언한 후 didTapComment(_:) 함수에서 프로토콜의 함수를 사용한다.

  • FeedController에 프로토콜 채택 후 구현.

11

이제 화면 전환 할 로직은 FeedController에서 FeedCellDelegate프로토콜 채택 후 구현하면 된다.

  • FeedController의 collectionView(_:cellForItemAt:)에서 각 셀마다 delegate 자신으로 등록

15@@@@

  1. Combine 사용의 경우
  • FeedCell에서 publiser와 anyCancellable 선언

12

  • FeedCell에서 publisher에게 published.

13

  • FeedController의 collectionView(_:cellForItemAt:)에서 각 셀마다 cell에서 publiser의 input stream 구독.

14

  • 코드정리

FeedController의 collectionView(_:cellForItemAt:) 에서 코드가 길어지기 때문에 cell의 메서드로 뺐다.

17

FeedController에선 그냥 함수 호출만하면된다.

16

컴바인을 사용하는 것과 델리게이트 프로토콜을 채택하는 것 둘다 비슷한 개념인 것 같다. 중요한 것은 cell 내부 클래스에선 화면 전환을 할 수 없다는 개념을 새로 알게 됬다.

근데 컴바인을 사용했을 때 위와 같은 에러가 발생했다. 화면 몇 번 내린 후에 셀의 메세지 버튼을 클릭할 때 재사용큐 보다 빨리 터치가 되는 것 같다.

재사용 큐로 꺼내지기 전에 있는 포스트 cell로 들어 갔다가 뒤늦게 재사용 큐에 의해 꺼내진 cell로 인한 포스트가 갱신되는 것 같다. 그래서 또 대화 창으로 들어가진다. subscribeFromDidTapPublisher에서 들어갈 때마다 기존 didTapPublisher의 값을 다시 nil 처리를 했다.

새로 알게 된 개념2

18

텍스트를 입력하기 위한 TextField와 버튼을 view로 만들었을 때 inputAccessoryView에 내가 커스텀한 UIView(TextField와 버튼이 있는)를 입력한다.
inputAccessoryView는 nil이다. 커스텀으로 정의한 뷰를 system에서 지원해주는 input view로 부착하기 위해서는 inputAccessoryView를 재 정의해서 UIResponder에서 읽고 쓸 수 있게 해야 한다.

Responder

UIResponder는 UIKit app에서 이벤트 처리의 뼈대이다. 많은 키 오브젝트는 responder다.(such as UIApplicaiton, UIViewController, UIView) 이벤트가 발생하면 UIKit는 앱의 responder 인스턴스를 다룰 수 있게 발송해준다.

  • 이벤트?

touch event, motion event, remote- control evetn, press event가 대표적으로 있다. 특정 타입의 이벤트를 다루기 위해선 responder가 특정 메서드와 일치하게 오버라이드 해야 한다. 그 예로 터치 이벤트에선 responder를 구현하기 위해서는 touchesBegan(:with:), touchesEnded(:with:) 등 메서드를 구현해야 한다. 터치를 예로 들자면 responder는 이벤트 정보를 UIKit에서 제공 받고 터치나 앱의 인터페이스를 알맞게 업데이트하기 위해 추적하고 바꾼다.

이벤트 헨들링은 UIKit responder들이 또한 앱에서 앞으로 처리되지 않을 이벤트 또한 관리한다. 주어진 responder가 이벤트를 처리하지 않는다면 responder chain 의 다음 이벤트를 처리할 수 있게 호출한다. UIKit는 responder chain을 동적으로 관리한다. 미리 정의된 규칙들을 사용해 어느 object가 다음 이벤트를 받을 지 결정한다.
그 예로 view는 자신의 superView로 이벤트를 호출하고, 계층구조의 root view 연결된 viewController로 이벤트를 호출한다.

Responder는 UIEvent object를 처리한다. 근데 커스텀 input. input view 또한 처리할 수 있다. system's keyboard가 input view의 가장 명백한 예제이다.

input view란?

사용자가 화면에서 UITextField 또는 UITextView object를 tap할 때, The view becoms the first responder and display its input view. 뷰는 최초 responder가 되고 input view를 화면에 띄운다. 여기서 input view는 system keyboard이다. 유사하게, 커스텀 input view를 만들고 다른 responder가 activate될 수 있을 때 화면에 출력할 수 있다. resopnder를 통한 input view를 커스텀하고 연결시키려면 inputView 프로퍼티의 responder를 뷰에 할당하면 된다.

다시 말해 Responder는 raw event data를 받고 다른 이벤트를 처리하거나 다른 responder object에게 호출한다. 앱에서 이벤트를 받는다면 UIKit 은 자동적으로 가장 적절한 responder object에게 이벤트를 지시한다. 그 예로 first responder가 있다.

20

만약에 UITextField에서 이벤트 처리를 하지 못한다면 UIKit은 이벤트를 text field의 부모인 UIView object에게 전달한다.( UIWindow인 루트 뷰에서 파생된,,) root view에선 responder chain으로 다이렉트로 root view인 UIWindow에게 이벤트를 전달하기 전에!! UIView와 연결된 UIViewController에게 event를 전달한다. 만약 window 또한 이벤트를 처리할 수 없다면 UIKit는 UIApplication object에게 이벤트를 전달하고 아직 responder chain의 일부가 아니라면 app delegate에게 전달한다.

Controls

컨트롤은 action message를 사용해서 target 를 통해 연결된 object와 direct하게 소통한다. 컨트롤을 통해 사용자의 상호작용이 일어날 때, 컨트롤은 action message를 자신의 target object에게 전달한다. action message는 이벤트가 아닌데, 여전히 responder chain을 활용할 수 있다. target object의 control이 nil인 경우에 UIKit는 target object로부터 시작해서 resopnder chain을 통해 적절한 action method를 취할 수 있는 object를 찾을 때까지 chain에 연결된 객체들을 찾아간다.

Gesture

Gesture recognizer는 뷰에서 터치와 입력 이벤트 받을 수 있다. 연속적인 터치로 인해 view's gesture가 인식 실패 한다면, UIKit은 뷰에게 터치를 보낸다. 뷰가 터치에 대한 처리를 하지 못한다면 UIKit는 responder chain으로 통과시킨다.

여러 Response chain 중 현재 상태에서 이벤트를 가장 먼저 받는다면 Frist Responder가 된다. 그 예로 텍스트 필드를 누르고 타이핑 할 경우 First Responder는 Textfiend이다. 만약 textfield에서 이벤트에 대한 처리를 하지 않는다면(못한다면) Resopnder chain에 의해 다른 responder로 넘긴다.

inputView, inputAccessoryView

위에서도 설명했지만 inputView는 Frist Responder가 될 때 인데 대표적으로 system keyboard다. UITextField, TextView가 First Responder가 됬을 때 자동으로 나온다. 그래야 텍스트 필드를 입력할 수 있기 때문,, 근데 inputView는 get-only 프로퍼티인데 오버라이딩한다면 커스텀 input view사용이 가능하고 그렇다는건 텍스트 필드를 클릭한 후 First Responder가 되었을 때 알림을 통한 기능이나 picker 등 다양하게 다른 UIView타입의 object를 호출 할 수 있다는 말이다.


autoresizingMask = .flexibleHeight 이걸 선언할 경우 superview의 크기, 바운스 등에 따라 높이가 자동적으로 조정된다. ( in CommentInputAccessoryView.swift )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant