-
Notifications
You must be signed in to change notification settings - Fork 4
iOS troubleShooting Week 2
시뮬레이터에서 애플 로그인을 시도했지만 무한로딩이 되는 현상이 있었습니다.
찾아보니 iOS14 시뮬레이터의 문제였고, iOS 13.5 시뮬레이터를 다운받아 실행함으로써 해결하였습니다.
참고: https://developer.apple.com/forums/thread/651533
- 문제 1
plist App Transport Security Settings에 Allow Arbitrary Loads = true로 수정
-
문제 2
Content-Type: 컨텐츠의 타입(MIME)과 문자열 인코딩(utf-8 등등)을 명시할 수 있습니다. 이를 제대로 작성해야 데이터가 받아와집니다.
Accept: 이런 타입의 데이터를 보내줬으면 좋겠다고 명시할때 사용합니다. 굳이 안넣어도 작동합니다.
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
Corrdinator Pattern을 사용하면 ViewController는 이전과 이후에 어떤 ViewController를 처리할지 모르게 할 수 있습니다. 따라서 아래와 같은 장점이 있습니다.
- ViewController 재사용성 향상
- 화면간의 연결 의존성 분리
- 단일 책임 원칙 준수
UI를 StoryBoard로 만들었을 시 이에 대응하는 ViewController에 종속성 주입이 어려워집니다. instantiateViewController의 creator를 이용합니다.
func instantiateViewController<ViewController>(identifier: String, creator: ((NSCoder) -> ViewController?)? = nil) -> ViewController where ViewController : UIViewController
참고: instantiateviewcontroller iOS 13.0+을 유의합니다.
init?(coder: NSCoder, useCase: LoginUseCaseType) {
loginUseCase = useCase
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("This viewController must be init with useCase.")
}
이렇게 initalize를 만들어주면 ,StoryBoard로 구현한 IBOutlet을 그대로 가져가면서 종속성 주입을 할 수 있습니다.
dependency injection를 적용하면 아래와 같은 장점이 있습니다.
-
단일 책임 원칙 준수 (Separation of concerns)
-
코드 재사용성 향상
-
코드 가독성 향상
-
목객체를 이용한 단위테스트가 용이
Coordinator Pattern을 이용하면 Inital View Controller를 스토리보드로 하지 않고 코드로 불러와야 합니다.
iOS13이전의 버전은 AppDelegate밖에 없기 때문에 자료를 찾아가면서 헷갈리는 경우가 있어 차이를 정리합니다. iOS13부터는 AppDelegate에서 UI Lifecycle을 관리하는 역할을 Scene Delegate로 빼주었습니다. 이를 이용해 multi window 앱을 만들 수 있습니다.(split View)
기존에 Appdelegate에서 썼던 코드입니다. 이를 SceneDelegate로 옮겨줍니다
//func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: //[UIApplication.LaunchOptionsKey: Any]?) -> Bool 내에 선언
// AppDelegate
window.rootViewController = MyRootViewController()
window = UIWindow(frame: UIScreen.main.bounds)
window.makeKeyAndVisible()
SceneDelegate에서는 반드시 아래와 같이 UIWindowScene
을 이용하여 window를 만들어줘야합니다.
//func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) 내에 선언
// SceneDelegate
guard let windowScene = scene as? UIWindowScene else { return }
window.rootViewController = MyRootViewController()
window = UIWindow(windowScene: windowScene)
window.makeKeyAndVisible()
- 씬이 많아지면서 아래와 같은 패턴을 적용하였습니다.
wwdc의 예제에서는 UICollectionViewListCell의 trailingSwipeActionsConfiguration 프로퍼티에 UISwipeActionsConfiguration을 할당해줌으로써 Swipe Action을 적용했는데, 예제를 따라 구현하려니 UICollectionViewListCell에 trailingSwipeActionsConfiguration 프로퍼티가 없었습니다.
https://developer.apple.com/forums/thread/653542
http://codeworkshop.net/objc-diff/sdkdiffs/ios/14.0b2/UIKit.html
정보를 찾아보니 UICollectionViewListCell에 있던 swipeActionsConfiguration들이 UICollectionLayoutListConfiguration로 옮겨졌었습니다.
var config = UICollectionLayoutListConfiguration(appearance: .plain)
let action = UIContextualAction(style: .destructive, title: "Close", handler: { _, _, _ in })
config.trailingSwipeActionsConfigurationProvider = { indexPath in
return UISwipeActionsConfiguration(actions: [action])
}
변경된 API에 맞게 UICollectionLayoutListConfiguration에서 SwipeAction을 설정해주어 해결하였습니다.
-
git action template 문제
git action에서 iOS를 선택하면 자체적으로 주는 템플릿이 있습니다.
MacOS에 시뮬레이터를 띄우는 방식인 것 같은데 stderr에 무엇인가 출력되면 fail로 처리됩니다. 이러한 stderr출력이 있어서 제대로 안되는 문제가 있어 device를 선택할 때
device=`instruments -s -devices | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}'`
를 아래로 수정하였습니다.
device=`xcrun simctl list devices | grep -oE "iPhone.*?[^\(]+" | head -1 | awk '{$1=$1;print}'`
Swipe 액션으로 이슈를 Close 하는 기능을 서버와 연동하면서 데이터와 CollectionView에 표시되는 데이터가 일치하지 않아 생기는 문제가 있었습니다.
셀을 삭제한 후 데이터가 맞지 않아 빈 셀이 생겨버린 모습
여러명의 유저가 사용하는 앱이라 동시에 데이터에 변경이 생겼을 때를 대비해 Close 액션이 발생하면 Close 요청을 서버에 보내고, 응답으로 성공이 오면 다시 전체 리스트 로드 요청을 보내 전체 데이터를 받아 렌더링하는 방식으로 구현하였었습니다.
이 과정에서 서버와 요청을 두번 주고받게 되는데, 그 찰나의 순간에 CollectionView에서는 SwipeAction이 발생한 채 멈춰있게 되어 셀 렌더링에 문제가 발생했습니다.
이를 해결하기 위해 첫번째 Close 요청에 성공 응답이 오면, 로컬의 데이터를 먼저 Close로 수정한 채 CollectionView를 리로드하고, 전체 데이터 요청에 대한 응답이 오면 다시 변경된 데이터에 맞게 셀을 그리도록 수정하여 해결하였습니다.
CollectionView를 여러번 리로드하여 비효율적이라고 생각할 수 있지만, DiffableDataSource를 적용하였으므로 서버에서 받아온 데이터에 변동이 없다면, 데이터소스의 스냅샷에 변동사항이 없어 CollectionView를 다시 리로드하지 않아 불필요한 리로드는 발생하지 않습니다.
비동기로 동작하는 네트워킹 기능을 테스트하는 코드를 작성하였습니다.
func testSuccess() {
let networkService = NetworkService()
let request = IssueListEndPoint(path: .issues, method: .get)
networkService.userToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwiZW1haWwiOiJBQGIuYyIsIm5pY2tuYW1lIjoiQXNkZiIsImlhdCI6MTYwNDQ3ODg2N30.v0ZSPVEW3IyNMVVHPn2mHGUPGw7VeNpMJ3aechf62k4"
networkService.request(requestType: request) { result in
switch result {
case let .success(data):
let decoder = JSONDecoder()
XCTAssertNoThrow(try? decoder.decode([Issue].self, from: data))
case let .failure(error):
XCTFail("네트워크 서버 연결 실패\(error)")
}
}
}
실제 사용하는 API로 요청하여 알맞은 데이터가 오는지 테스트하려 했으나, 네트워크 통신이 비동기적으로 동작하여 네트워크 통신이 끝나기 전에 테스트케이스가 끝나버려 네트워크 통신의 결과와 관계없이 테스트가 항상 통과하는 문제가 있었습니다.
테스트 케이스가 네트워크 통신이 완료될 때 까지 끝나지 않도록 해주기 위해 XCTestExpectation을 활용하여 해결하였습니다.
func testSuccess() {
let expectation = XCTestExpectation(description: "NetworkTaskExpectation")
defer { wait(for: [expectation], timeout: 5.0)}
let networkService = NetworkService()
let request = IssueListEndPoint(path: .issues, method: .get)
networkService.userToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwiZW1haWwiOiJBQGIuYyIsIm5pY2tuYW1lIjoiQXNkZiIsImlhdCI6MTYwNDQ3ODg2N30.v0ZSPVEW3IyNMVVHPn2mHGUPGw7VeNpMJ3aechf62k4"
networkService.request(requestType: request) { result in
switch result {
case let .success(data):
let decoder = JSONDecoder()
XCTAssertNoThrow(try? decoder.decode([Issue].self, from: data))
expectation.fulfill()
case let .failure(error):
XCTFail("네트워크 서버 연결 실패\(error)")
}
}
}
XCTestExpectation을 만들어주고 wats(for:timeout:)을 활용해 5초동안 expectation을 기다리도록 설정해주었습니다. 네트워킹에 성공하여 원하는 데이터를 받아왔을 때 fullfill을 호출해주어, 네트워크 통신이 완료될 때 까지 5초동안 테스트케이스가 끝나지 않고 기다리도록 작성하였습니다.
기획서의 UI에 맞게 테이블 뷰 셀의 Border를 탑, 바텀에 설정해주려고 했습니다.
private extension IssueDetailTableViewCell {
func configure() {
let topBorder = CALayer()
topBorder.backgroundColor = UIColor.blue.cgColor
topBorder.frame = CGRect(x: contentView.frame.minX,
y: contentView.frame.minY,
width: contentView.frame.width,
height: 1)
contentView.layer.addSublayer(topBorder)
let bottomBorder = CALayer()
bottomBorder.backgroundColor = UIColor.red.cgColor
bottomBorder.frame = CGRect(x: contentView.frame.minX,
y: contentView.frame.maxY,
width: contentView.frame.width,
height: 1)
contentView.layer.addSublayer(bottomBorder)
}
}
레이어를 만들어 위치를 탑, 바텀으로 맞춰주고 레이어 추가하는 방식으로 구현하였는데, 바텀 레이어가 화면에 표시되지 않는 문제가 있었습니다. 확실한 구분을 위해 탑의 레이어는 파랑색, 바텀의 레이어는 빨강색으로 설정해주었습니다.
셀의 탑에 추가한 파랑색 레이어는 잘 보이나, 바텀의 빨강색 레이어는 보이지 않는것을 확인할 수 있습니다.
바텀 레이어의 y좌표를 셀 contentView의 maxY로 설정해주었는데, 레이어가 그 아래 방향으로 그려지다보니 셀의 범위 밖에 추가되어서 나타나는 문제였습니다.
private extension IssueDetailTableViewCell {
func configure() {
let height = CGFloat(1)
let topBorder = CALayer()
topBorder.backgroundColor = UIColor.blue.cgColor
topBorder.frame = CGRect(x: contentView.frame.minX,
y: contentView.frame.minY,
width: contentView.frame.width,
height: height)
contentView.layer.addSublayer(topBorder)
let bottomBorder = CALayer()
bottomBorder.backgroundColor = UIColor.red.cgColor
bottomBorder.frame = CGRect(x: contentView.frame.minX,
y: contentView.frame.maxY - height,
width: contentView.frame.width,
height: height)
contentView.layer.addSublayer(bottomBorder)
}
}
바텀 레이어의 위치를 maxY에서 그려질 바텀 레이어의 height 만큼 빼주니 정상적으로 레이어가 표시되었습니다.
이러한 pullUpView를 구현할 때 프로토타입 separator가 tableView이고, cell은 변화하지 않아 staticTableView로 구현하려 했습니다. 위아래로 끌어 당겨야 하기 때문에 UX측면에서 스크롤 기능이 없어야 하고 Close Button이 맨 아래쪽에 고정되어 있어야 했습니다. 하지만 staticTableView는 UITableViewController로만 사용이 가능했고 아래쪽에 Close Button을 고정하는 작업이 복잡했습니다. 따라서, 보다 직관적이고 가독성있는 코드를 위해 staticTableView가 아닌 UIView로 작업하였습니다.
사용자가 통제하고 있다는 느낌을 주기 위해 터치중일 땐 pullUpView를 위아래로 움직일 수 있으며, 마지막 제스쳐에 따라 pullUpView를 보여줄지 결정합니다.
특정 범위 내에 값이 속한지 알고 싶을때
(minY...maxY).contains(changedY)
minY <= changedY && maxY >= changedY
둘중에 시간복잡도 관련 고민이 되어서 contains의 시간복잡도를 찾아봤습니다.
Array와 Set의 contains는 문서에서 명확히 찾을 수 있었습니다
-
Array
-
Set
-
ClosedRange
하지만 closedrange의 Contains에는 시간복잡도가 나와있지 않았습니다.
결국 apple github에서 구현방식을 찾아 볼 수 있었습니다.
범위의 contians도 시간복잡도가 O(1)로 비교문 그대로 구현된 것을 확인할 수 있었습니다. 효율성이 같으니, 가독성을 위해 contains를 이용하기로 하였습니다.
- ++
~=
연산자도 contains를 그대로 가져다 씁니다.