-
๊ฐ๋ฐ๊ธฐ๊ฐ : 2022.11.07 ~ 2022.01.11
-
SSAC 2๊ธฐ Service Level Project์ ์ผํ์ผ๋ก 1์ธ iOS ๊ฐ๋ฐ
-
1์ธ iOS ๊ฐ๋ฐ ๋ด๋น / ๊ธฐํ, ๋์์ธ, ์๋ฒ๋ ๊ต์ก๊ณผ์ ์ ํตํ ์ ๊ณต
-
Swagger, Confulence, Figma, Notion, Zep ์ฌ์ฉ
์จ๋ณด๋ฉ | ์ด๊ธฐ์ค์ | ํ | ๊ฒ์ |
---|---|---|---|
์์ฒญ | ์๋ฝ | ์ฑํ | ๋ด์ ๋ณด |
---|---|---|---|
-
๋ฌธ์์ธ์ฆ ๊ธฐ๋ฐ ํ์๊ฐ์ ๋ฐ ํํด
-
์ค์๊ฐ ์์น ๊ธฐ๋ฐ ์ฃผ๋ณ ์คํฐ๋๋ฉ์ดํธ ํ์
-
์คํฐ๋ ๊ธฐ๋ฐ ์คํฐ๋๋ฉ์ดํธ ๊ฒ์ ๊ธฐ๋ฅ
-
์คํฐ๋๋ฉ์ดํธ ์์ฒญ/์๋ฝ ๊ธฐ๋ฅ
-
์ผ๋์ผ ์ฑํ
-
๋ฆฌ๋ทฐ ๋จ๊ธฐ๊ธฐ
-
๋ด ์ ๋ณด ๊ด๋ฆฌ
- ์จ๋ณด๋ฉ์
collectionView + pageControl
๋ก ๊ตฌํํ๋๋ฐRx+main ํ๋ก์ ํธ
๊ฐ ์ ๋ฒ ๊ตฌ๊ธ๋งํ๊ธฐ ์ข์ ํ๋ก์ ํธ๋ค!!
scrollViewWillEndDragging
์ ์์์ค๋ก ๊ตฌํํ๊ธฐ ์ํ ๋ฐฉ๋ฒ :onboardView.collectionView.rx.willEndDragging
onboardView.collectionView.rx.willEndDragging
.withUnretained(self)
.subscribe(onNext: { (vc, arg1) in
let (_, targetContentOffset) = arg1
let page = Int(targetContentOffset.pointee.x / self.onboardView.collectionView.frame.width)
self.onboardView.pageControl.currentPage = page
})
.disposed(by: disposeBag)
- ์จ๋ณด๋ฉ์์ ๋ฌ๋ผ์ง๋ ๋ผ๋ฒจ๊ณผ ํน์ ๋ถ๋ถ์ ์์ด ์ ์ฉ๋๋ ๊ฑด switch๋ฌธ์ผ๋ก ํด๊ฒฐํ๋ค.
- ๋ผ๋ฒจ๋ก ํด์ผ ํ๋ ์ด์ ๋
๋ก์ปฌ๋ผ์ด์ง/๋ณด์ด์ค์ค๋ฒ
๋์ํด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
PlainTextField
๋ง๋ค ๋clearButtonRect
๋ฉ์๋ ์๋กญ๊ฒ ์ ์ฉํ๋ค.editingDidBegin/editingDidEnd
๋ ์ปค์คํ ํ ์คํธ ํ๋์์ ๊ตฌํํด์ ์ ์ฉํ ์ ์๋ค. ๊ทธ๋์ ๋ณ๋๋กisSelected
๊ฐ์ ์ฒ๋ฆฌ๋ฅผ ์ํด๋ ๋๋ค.
private func setupState() {
self.addTarget(self, action: #selector(editingDidBegin), for: .editingDidBegin)
self.addTarget(self, action: #selector(editingDidEnd), for: .editingDidEnd)
}
}
// MARK: - TextField State
extension PlainTextField {
@objc func editingDidBegin() {
lineView.backgroundColor = Color.black
}
@objc func editingDidEnd() {
lineView.backgroundColor = Color.gray3
self.resignFirstResponder()
}
}
- Rx์์ ์ ๊ณตํ๋ ๋ค์ํ๊ณ ์๋ก์ด ์ฐ์ฐ์๋ค์ ์ ์ฉํด๋ณผ ์ ์๋ ๊ฒฝํ ํ๋!
- 1 )
withLatestFrom
์ฐ์ฐ์๋ฅผ ํตํด์ ๋ฒํผ ํญ ์์ ๊ฐ๊ฐ isValid ์ฆ, ํ ์คํธํ๋์ ๊ฐ์ด ์ ํจํ์ง์ ๋ํ Bool๊ฐ์ ๋ฐํํ๋๋ก ํจ. - 2 )
compactMap
์ ์ฌ์ฉํด์ coordinate ๊ฐ์ ๋ฐ์์ค๋ ์ฒ๋ฆฌ๋ฅผ ํด์คฌ์.compactMap
์ 1์ฐจ์ ๋ฐฐ์ด์์ ์ต์ ๋ ๋ฐ์ธ๋ฉ ์ฒ๋ฆฌํด์ฃผ๋ ์ฅ์ ์ด ์์ด์ ๊ฐํธํจ์ ๋๋. - 3 )
throttle
๋ฌธ์ ๋ฉ์์ง ์ธ์ฆ ์ฝ๋๋ฅผ ๋ฐ์ ๋ฒํผ์ ๋๋ฅด๋ ๊ฒฝ์ฐ ์ฌ์ฉ์์ ๊ณผ๋ํ ํญ์ ํตํด API ์๋ฒ ํต์ ์ด ๋๋ ๊ฑธ ๋ง๊ธฐ ์ํด ์ฌ์ฉํจ.
output.tap
.withUnretained(self)
.throttle(.milliseconds(500), scheduler: MainScheduler.instance)
.subscribe(onNext: { (vc, isValid) in
isValid ?
vc.messageViewModel.requestLogin() :
vc.showToast(Toast.phoneTypeError.message)
})
.disposed(by: disposeBag)
- ์ ํ ์ expandable ๋๋ ํ๋ฉด์ ๊ตฌํํ๋ฉด์ cell์๋ ๋ง์ง๊ฐ์ ์ค ์ ์๊ณ , stackView์๋ ๋ง์ง๊ฐ์ ์ค ์ ์๋ค๋ ์ฌ์ค์ ๋ฐฐ์ ๋ค. (inset)
$0.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
$0.isLayoutMarginsRelativeArrangement = true
override func layoutSubviews() {
super.layoutSubviews()
contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16))
}
- ๊ทธ๋์ ์ฝ๋ ๊ธฐ๋ฐ์ผ๋ก ๋ ์ด์์์ ์ก์ ๋ SnapKit์ ์์ฃผ ์ฌ์ฉํ๋๋ฐ ์๋กญ๊ฒ ์๊ฒ ๋ ๋ฉ์๋๋ค. ํด๋น ๋ฉ์๋๋ฅผ ํตํด ์ฑํ ๋ทฐ์์ ์ ๋ ฅ์ฐฝ ๋ ์ด์์๊ณผ ์ต์คํ๋๋ธ UI ๋ฑ์ ๊ตฌํํ๋ค.
priority(.low)
๋ฅผ ํด์ฃผ๋ฉด ์ฐ์ ์์๋ฅผ ์ค์ ํ ์ ์๋ค.updateConstraints
: ์ ์ฝ ๋ณ๊ฒฝ ์remakeConstraints
: ๊ธฐ์กด ์ ์ฝ์ด ๋ค ์ฌ๋ผ์ง๊ณ ๋ค์ ์ค์
RxCoreLocatio
n์ ์จ๋ณด๋ ๊ฒ์ด ์๋์ด ์๊ฒ ๋DelegateProxy
๋ฅผextension
์ผ๋ก ๋ฃ์ด Rx๋ก CoreLocation ๊ตฌํํ๋ค. ์ ๊ธฐํ ์ ์ Rx์์ ์์ฒด์ ์ผ๋ก ์ ๊ณตํ๋ ๊ฒ๋ ์์ฝ ๋ฒ์ ์์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํด์ ์ฌ์ฉํ์ง ๋ชปํด์ ํด๋น ๋ฐฉ๋ฒ์ผ๋ก ํด๊ฒฐํ์
- ๋ธ๋ผ์ผ์ด ์๋ ๋ฐฐ์ด์ ์๋ฒ ํต์ ์ body์ ๋ด์ ๋ณด๋ผ ๋
URLEncoding(arrayEncoding: .noBrackets)
์ ํตํด ๋ธ๋ผ์ผ์ ์ ๊ฑฐํด์ค ์ ์๋ค๋ ๋ฐฉ๋ฒ์ ์์๊ณ , ์๋ผ๋ชจํ์ด์ด์์ ์ ๊ณตํด์ฃผ๋ ์ธ์ฝ๋ฉ ๋ฉ์๋๋ค์ ๊ณต๋ถํ๋ ๊ณ๊ธฐ๊ฐ ๋๋ค.
- ์ ์ ํ๊ฒ ๋ทฐ๋ฅผ ์ปค์คํ ํด ํ์ํ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ ์ ๊ณผ, ํด๋์ค ๋ด๋ถ์ ํ์ํ ์ด๊ฑฐํ์ ํตํด ๋ถํ์ํ ์ฝ๋์ ๋ฐ๋ณต์ ์ค์ธ ์ .
- ์ ๊ทผ์ ์ด์ ์ฌ์ฉํด์ ์ธ๋ถ์์ ์ ๊ทผํ์ง ๋ชปํ๊ฒ ๋ง์๊ณ , ๋ฐํ์ ์ ์ฑ๋ฅ์ ๋์๋ค.
- Input/Output ํจํด์ ๊ณต๋ถํ๊ณ ๋ฐ๋ก ์ ์ฉํ๊ธฐ์ ์ด๋ ์ ๋ ์๊ฐ์ด ๊ฑธ๋ ธ๋ ์ (์ด๊ธฐ์ค์ ์์ ์ ๋๋ก ์ ์ฉ)
- ๋ฐ์ดํฐ์ ํ๋ฆ์ ํ์ ํ ์ ์์ด ์ข์์ง๋ง ํ๋ฐ๋ถ์๋ ๊ณต์์ฐ์ ์คํจ์ ์์ธ์ด ๋์ด ํจํด์ ์ฌ์ฉํ์ง ์๊ฒ ๋๋ค.
- API ๋ณ๋ก ์๋ต๊ฐ๊ณผ ์ํ์ฝ๋๊ฐ ์ค๋ ๊ฒฝ์ฐ or ์ํ์ฝ๋๋ง ์ค๋ ๊ฒฝ์ฐ๊ฐ ์๋๋ฐ ํ๋์ APIManager์ ํ์ ๋ฉ์๋๋ก ๊ด๋ฆฌํด์ฃผ๋ ค๊ณ ์ฌ๋ฌ๋ฒ ์๋ํ๋ค๋ณด๋ ๋๋ ์ด๋๋ค๋ ์ .
final class APIManager {
private init() { }
static let shared = APIManager()
typealias Completions<T> = ((T?, Int?, APIError?) -> Void)
func request<T: Decodable>(_ type: T.Type = T.self,
_ convertible: URLRequestConvertible,
completion: @escaping Completions<T>) {
AF.request(convertible)
.responseDecodable(of: type) { [weak self] response in
guard let self = self else { return }
guard let statusCode = response.response?.statusCode else { return }
completion(nil, statusCode, nil)
print("โ ๏ธ ์ํ์ฝ๋๋ง!!! ===", type, "/", statusCode)
switch response.result {
case .success(let data):
completion(data, statusCode, nil)
print("โ
์ฑ๊ณต!!! ===", data, "/", statusCode)
case .failure(_):
guard let error = APIError(rawValue: statusCode) else { return }
if error.rawValue == 401 {
ErrorManager.refreshToken {
self.request(type, convertible, completion: completion)
}
}
completion(nil, statusCode, error)
print("โ ์คํจ!!! ===", type, "/", error, "/", error.localizedDescription)
}
}
}
}
- ์ด ๊ฒฝ์ฐ, ๊ผญ ํ๋์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ง ์์๋ ๋๋ค๋ ๊ฒ๊ณผ ์ถํ Rx์์ ์ ๊ณตํด์ฃผ๋ Single์ด๋ผ๋ ์ต์ ๋ฒ๋ธ์ ์๊ฒ ๋๋๋ฐ, success, error ์ด๋ฒคํธ๋ง ๋ฐฉ์ถํด ๋คํธ์ํฌ์ ์ ํฉํ๋ค๊ณ ์๊ฐ๋ผ์ ๊ณต๋ถ ํ ์ ์ฉํด๋ณด๊ณ ์ถ๋ค๊ณ ์๊ฐํ๊ฒ ๋๋ค.
- ๋ทฐ๋ชจ๋ธ์์ ๋คํธ์ํฌ ์ฒ๋ฆฌ ํ ํ๋ฉด์ ํ ์ฒ๋ฆฌํด์ค ๋ ์ฝ๋๋ค์ดํฐ ํจํด์ ํ์์ฑ์ ๋๋ผ๊ฒ ๋์์ผ๋, ์ ํ๋ ์๊ฐ์ผ๋ก ์ธํด ๊ณต๋ถํ ์๊ฐ์ด ์์ด ์ ์ฉํ์ง ๋ชปํ๋ค.
- ์ฑํ
์ ๊ตฌํํ ๋ ์ด๋ ค์ ๋ ์ ์ด
BehaviorSubject
๋ฅผ ํตํดchatResponse
๋ฅผ ๋ฐ์์ postChat ์๋ฒํต์ ์ ๋ณด๋ด๋ ์ฑํ ๋ฐ์ดํฐ๋ฅผ onNext๋ฅผ ํตํด ๋ฃ๊ณ , ๋ทฐ์ ์ฑํ ๋ด์ฉ์ ๋ฟ๋ ค์ฃผ๋๋ฐ ์ด๋ ์ฑํ ๋ฐฐ์ด์ด ์๋ ๋ณด๋ธ ์ฑํ ๋ด์ฉ๋ง ํ๋์ฉ ๋ค์ด๊ฐ ์ด๋ ค์์ ๊ฒช์๋ค. ๊ทธ๋์[Chat]
ํ์ ์ ๊ฐ์ง chatList๋ฅผ ๋ฐ๋ก ๋ง๋ค๊ณ ์ดchatList
๋ฅผonNext
๋ฅผ ํตํด ๊ฐ์ ๋ฃ์ด์คฌ๋ค. ์ด ๋ถ๋ถ์์ ๋ฐ์ํ ํ๋ก๊ทธ๋๋ฐ์ ์ ์ฉํจ์๋ ๋ถ๊ตฌํ๊ณ ๋ถํ์ํ ๊ณผ์ ์ด ์ค๊ฐ์ ๋ค์ด๊ฐ ์ ์ด ์์ฝ๊ณ , ๊ฐ์ ์ ์ ๊ณ ๋ฏผํด๋ด์ผ ํ ๊ฒ ๊ฐ๋ค.
/// ChatViewModel
var chatList = [Chat]()
let chatResponse = BehaviorSubject<[Chat]>(value: [])
func postChat(_ chat: String, to: String, completion: @escaping ((Int) -> Void)) {
APIManager.shared.request(Chat.self, ChatRouter.sendChat(chat: chat, to: to)) { [weak self] data, status, error in
guard let self = self else { return }
if let data = data {
self.chatList.append(
Chat(id: data.id,
to: data.to,
from: data.from,
chat: data.chat,
createdAt: data.createdAt.toDate().toString(format: .special)))
RealmRepository.shared.addChatData(
chat: RealmChat(id: data.id,
from: data.from,
to: data.to,
chat: data.chat,
createdAt: data.createdAt.toDate()))
self.chatResponse.onNext(self.chatList)
completion(self.chatList.count)
}
if let error = error {
ErrorManager.handle(with: error, vc: ChatViewController(ChatViewModel()))
}
}
}
- ๊ฒ์๋ทฐ๋ฅผ ๊ตฌํ ์ ๋ณต์กํ ๋ก์ง ์ฒ๋ฆฌ๋ฅผ ์ค๊ณํ๋ ๋ฐ์ ์ด๋ ค์์ ๊ฒช์๊ณ , ๋น์ฆ๋์ค ๋ก์ง์ ์ ๋๋ก ๋ทฐ๋ชจ๋ธ๋ก ๋ถ๋ฆฌ์ํค์ง ๋ชปํ ์ ์ด ๊ฐ์ ํ ๋ถ๋ถ.