Do not bind for not changing value. Use initialState
ViewController
init(reactor: Reactor) {
self._reactor = reactor
super.init(nibName: nil, bundle: nil)
titleLabel.text = reactor.initialState.title
}
iOS Developer
Do not bind for not changing value. Use initialState
ViewController
init(reactor: Reactor) {
self._reactor = reactor
super.init(nibName: nil, bundle: nil)
titleLabel.text = reactor.initialState.title
}
case .tapDeleteButton(let model):
let deleteHandler: () -> Void = { [weak self, weak listAdapter] in
self?.action.onNext(.deletePositionCell(model?.userCash))
listAdapter?.performUpdates(animated: true, completion: { [weak self] _ in // line 1
self?.action.onNext(.delete(model?.userCash))
})
}
self?.action.onNext(.showDeleteAlert(deleteHandler))
}
case .showDeleteAlert(let deleteAction):
guard let deleteAction = deleteAction else { return .empty() }
deleteAction()
return .empty()
case .deletePositionCell(let userCash):
return .just(.deletePositionCell(userCash)) // line 2
line 1 is called before line 2.
override func canMoveItem(at index: Int) -> Bool {
return true
}
2. detect longPressGesture on View Controller
let collectionViewUpdateQueue = BlockableTaskQueue()
func bind() {
collectionView.rx
.longPressGesture(configuration: { _, delegate in
delegate.simultaneousRecognitionPolicy = .never
delegate.beginPolicy = .custom { [weak self] gesture -> Bool in
guard let collectionView = self?.collectionView else { return false }
let touchLocation = gesture.location(in: collectionView)
guard let selectedIndexPath = collectionView.indexPathForItem(at: touchLocation) else { return false }
guard let cell = collectionView.cellForItem(at: selectedIndexPath) as? PositionListPositionCell else { return false }
return cell.isMovable
}
})
.subscribe(onNext: { [weak self] gesture in
self?.handleGesture(gesture)
})
.disposed(by: disposeBag)
}
extension PositionListStockViewController {
func handleGesture(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
let touchLocation = gesture.location(in: collectionView)
guard let selectedIndexPath = collectionView.indexPathForItem(at: touchLocation) else { return }
collectionViewUpdateQueue.block()
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
let position = gesture.location(in: collectionView)
collectionView.updateInteractiveMovementTargetPosition(position)
case .ended:
collectionView.endInteractiveMovement()
collectionViewUpdateQueue.unblock()
default:
collectionView.cancelInteractiveMovement()
collectionViewUpdateQueue.unblock()
}
}
}
3. limit removable index from start to end
init() {
let (collectionView, layout) = ViewFactory.collectionViewAndLayout(isHorizontal: false)
layout.reorderingDelegate = self
}
extension PositionListStockViewController: CollectionViewFlowLayoutWithReorderingDelegate {
func collectionViewFlowLayoutWithReorderingOverrideTargetIndex(moveFrom originalIndexPath: IndexPath, to computedTargetIndexPath: IndexPath) -> IndexPath {
guard let reactor = reactor else { return originalIndexPath }
guard let minIndexPath = reactor.getStartIndexPathOfPositions(),
let maxIndexPath = reactor.getEndIndexPathOfPositions() else { return originalIndexPath }
return computedTargetIndexPath.clamped(min: minIndexPath, max: maxIndexPath)
}
}
extension PositionListStockViewReactor {
func getStartIndexPathOfPositions() -> IndexPath? {
guard let positions = currentState.positions,
positions.isNotEmpty,
let sectionIndex = makeListSection().firstIndex(where: {
guard let model = $0 as? DiffableBox<PositionListPositionModel> else {
return false
}
return true
})
else {
return nil
}
return IndexPath(row: 0, section: sectionIndex)
}
func getEndIndexPathOfPositions() -> IndexPath? {
guard let startIndex = getStartIndexPathOfPositions() else { return nil }
let numberOfPositions = currentState.positions?.count ?? 0
return IndexPath(row: 0, section: startIndex.section + numberOfPositions)
}
}
4. Change list object first and then reorder request to server
init() {
adapter.moveDelegate = self
}
extension PositionListStockViewController: ListAdapterMoveDelegate {
func listAdapter(_ listAdapter: ListAdapter, move object: Any, from previousObjects: [Any], to objects: [Any]) {
guard let object = object as? DiffableBox<PositionListPositionModel> else {
return
}
let previous = previousObjects
.compactMap { object in
return object as? DiffableBox<PositionListPositionModel>
}
let current = objects
.compactMap { object in
return object as? DiffableBox<PositionListPositionModel>
}
reactorNotNil?.action.onNext(.onlyChangePositionsTo(current.compactMap(\.value.userStock)))
reactorNotNil?.action.onNext(.reorder(newList: current.compactMap(\.value.userStock)))
}
}
5. When updating the collection view, enque to the collectionViewUpdateQueue
reactor.state.distinctUntilChanged { $0.listVersion }
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.collectionViewUpdateQueue.enqueue(
BlockableTaskQueue.Mutation(
type: .wholeUpdate,
mutate: { [weak self] in self?.adapter.performUpdates(animated: false, completion: nil) }
)
)
})
.disposed(by: disposeBag)
class ModelUtil {
// 1. 응답이 JSONObject나 JSONArray가 아님
// 2. 응답의 전체를 감싸는 key값으로 parsing이 안됨
// 3. key값인 Array 각각이 JSONObject가 아님
@discardableResult
static func apiFormatError() -> NSError{
let error = NSError(domain:"com.companyname.appname.API JSON Format Error", code:0, userInfo:nil)
Crashlytics.sharedInstance().recordError(error)
return error
}
@discardableResult
static func apiFormatError(userInfo: Dictionary<String, Any>) -> NSError{
let error = NSError(domain:"com.companyname.appname.API JSON Format Error", code:0, userInfo:userInfo)
Crashlytics.sharedInstance().recordError(error)
return error
}
// 필수로 받아와야 하는 값이 parsing되지 않음
@discardableResult static func apiEssentialValueParsingError() -> NSError {
let error = NSError(domain:"com.companyname.appname.API JSON Essential Value Parsing Error", code:0, userInfo:nil)
Crashlytics.sharedInstance().recordError(error)
return error
}
// 1. parsing된 값이 empty임
// 2. parsing된 값이 상황상 null이면 안 되는데 null임
static func apiEmptyValueError() {
let error = NSError(domain:"com.companyname.appname.API JSON Empty Parsed Value Error", code:0, userInfo:nil)
Crashlytics.sharedInstance().recordError(error)
}
// 요구사항에서 받기로 했던 length와 API 결과 length가 다를 때
static func apiLengthError() {
let error = NSError(domain:"com.companyname.appname.API JSON Length is Not Expected", code:0, userInfo:nil)
Crashlytics.sharedInstance().recordError(error)
}
}
enum ModelError: Error {
case invalidFormat(data: [String: Any])
}
Model class
extension IngredientPack {
static func getIngredientPack(_ requestManager: RequestManager, type: PackType, completion: @escaping (RequestResult<IngredientPack>) -> Void) {
let url = CustomRequest.domainAppend("APIPath")!
requestManager.request(url: url, parameters: ["type": type.rawValue]) { (result) in
switch result {
case .success(let data):
do {
let ingredientPacks = try JSONDecoder().decode([IngredientPack].self, from: data)
guard let ingredientPack = ingredientPacks.first else {
throw ModelUtil.apiFormatError()
}
completion(.success(ingredientPack))
} catch {
completion(.failure(.others))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
2. Throw errors when the status code is not normal
func parseResponse(request: URLRequest, _ error: Error?, _ response: URLResponse?, _ data: Data?, completion: @escaping (RequestResult<Data>) -> Void) {
// Network Indicator Control.
self.networkIndicatorCount -= 1
// Handling Error.
if let error = error {
ResponseError.cocoaError(request: request, error: error)
completion(.failure(.others))
// Handling Response Data.
} else {
if let httpURLResponse = response as? HTTPURLResponse {
if httpURLResponse.statusCode != 403
&& (httpURLResponse.statusCode < 200
|| httpURLResponse.statusCode >= 400){
let statusCodeError = ResponseError.statusCodeError(statusCode: httpURLResponse.statusCode, request: request)
Crashlytics.sharedInstance().recordError(statusCodeError)
completion(.failure(.others))
return
}
}
// (1) Data 형식이 JSON형태가 아닐 경우 Error 처리.
// (2) Data 형식이 JSON형태이고 값이 UserAuthError인 경우 Error 처리.
// (3) Data 형식이 JSON형태이고 값이 UserAuthError가 아니면 success처리.
if let data = data, let response = response {
if (data.count > 0) {
// TODO: UserAuthError를 확인하는 방법으로 Result접근이 아니라 StatusCode로 분별하기.
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
// 인증 에러 인 경우 JSON형식으로 UserAuthError라는 Key에 Message값을 보냄.
if let json = jsonObject as? [String: Any],
let authErrorMessage = json["UserAuthError"] as? String {
ResponseError.userAuthError(request: request, response: response, json: json, authErrorMessage: authErrorMessage)
completion(.failure(.authenticationFailed(message: authErrorMessage)))
return // .success가 호출되지 않도록 return
}
} catch {
let nsError = error as NSError
var userInfo = nsError.userInfo
let responseData = String(data: data, encoding: .utf8).or("nil")
userInfo.updateValue(responseData, forKey: "responseData")
let error = NSError(domain: nsError.domain, code: nsError.code, request: request, userInfo: userInfo)
Crashlytics.sharedInstance().recordError(error)
completion(.failure(.others))
return // .success가 호출되지 않도록 return
}
}
completion(.success(data))
// Handling No Data.
} else {
ResponseError.noData(request: request)
completion(.failure(.others))
// TODO: CustomRequest.
// 정상적인 상황에서도 response가 없는 요청도 있기 때문에 completeHandler가 있는 요청에 대해서만 (response를 바라는데 error 가 발생 한 경우) 에러를 리포팅 한다.
// Response가 없는 요청 알아둘 것.
}
}
}
3. Third party login error
@objc func login(on viewController: UIViewController) {
if let currentToken = AccessToken.current {
if let tracker = loginEventTracker {
let loginResult = LoginManagerLoginResult(token: currentToken, isCancelled: false, grantedPermissions: [], declinedPermissions: [])
tracker(loginResult, nil)
} else {
fetchUser(from: viewController)
}
} else {
loginManager.logIn(permissions: MyFacebook.permissions, from: viewController) { (result, error) in
if let error = error {
Alert.showConfirmAlert(withTitle: self.errorTitle, message: error.localizedDescription, viewController: viewController)
Crashlytics.sharedInstance().recordError(error)
self.thirdPartyLoginDelegate?.logFail(registerType: MyFacebook.registerType, message: self.errorMessage)
} else if result?.isCancelled == false {
self.fetchUser(from: viewController)
}
}
}
}
navigationController?.interactivePopGestureRecognizer?.delegate = nil
Method 1: Make mockable annotation to certain files
#!/bin/bash
sed -i '' '12i\
/// @mockable' File.swift
12 is line number
2. edit Makefile
all: mockable
mockable:
./mockable.bash
3.
$PROJECT_DIR/third_party/mockolo -s Pods/Backend -d Neo/Generated/MockAPI.swift
Method 2: Make mockable annotations to all files and that mockolo to read certain files
/// @mockable you need to include the file to ios/scripts/generate-mock.bash to make Mock.
public protocol {{apiPrefix}}{{classname}}Type {
2.
$PROJECT_DIR/third_party/mockolo -srcs Pods/Backend/social/Classes/Swaggers/APIs/CertainAPI.swift -d [project]/Generated/MockAPI.swift
let dateComponents = DateComponents(year: year, quarter: quarter)
let calendar = Calendar.current
let date = calendar.date(from: dateComponents)
I recommend to use SwiftFormat.
https://github.com/nicklockwood/SwiftFormat
echo ""
__system "Run swiftformat.."
Pods/SwiftFormat/CommandLineTool/swiftformat -c .swiftformat .
__system "Done"
2. add bash file command to the pre-commit hook.
./chore.bash
use hidesBottomBarWhenPushed variable.
don’t use “tabBarController?.tabBar.isHidden = true”. Because tabBar does not show when the view controller is popped.
And you need to set hidesBottomBarWhenPushed when the view controller ‘init’. It doesn’t work if it’s set afterwards.