κ°μΈ νλ‘μ νΈ
2024.01.02 ~ 2024.02.19 (7μ£Ό)
μ΅μ μ§μ λ²μ iOS 16.0
κ°μ κ΄μ¬μ¬λ₯Ό 곡μ νλ μ¬μ©μ κ° μν΅ν μ μλ λ©μ μ μ΄ν리μΌμ΄μ
ReactorKit
RxSwift
, RxCocoa
, RxDataSource
, RxGesture
Firebase Cloud Messaging
, iamPort
, KakaoOpenSDK
SoketIO
, Moya
, Kingfisher
, Realm
, Codable
WebKit
, UIKit
, SnapKit
, Then
, AutoLayout
DiffableDataSource
, CompostionalLayout
AnyFormatKit
, SideMenu
, IQKeyboardManager
, Toast
μ ν λ‘κ·ΈμΈ, μΉ΄μΉ΄μ€ λ‘κ·ΈμΈμΌλ‘ μμ
λ‘κ·ΈμΈ κΈ°λ₯ μΆκ°
ReactorKit
μ νμ©νμ¬ λ¨λ°©ν₯ νλ¦μΌλ‘ μ½λ ꡬ쑰ν
Socket.IO
λ₯Ό ν΅ν΄ μ€μκ° μ±ν
κΈ°λ₯ ꡬν
Firebase Cloud Messaging
μλΉμ€λ₯Ό ν΅ν΄ μλ²λ‘λΆν° μ±λ λλ λμ μ±ν
λ°© μλ¦Ό μ€μκ° μμ
PG
μ°λμ ν΅ν κ²°μ μλΉμ€ ꡬν
βοΈ νμ κ°μ
λ° λ‘κ·ΈμΈ
μ ν λ‘κ·ΈμΈ λ° μΉ΄μΉ΄μ€ λ‘κ·ΈμΈμΌλ‘ μμ
λ‘κ·ΈμΈμ μ¬μ©ν μ μλ€.
μ΄λ©μΌ νμκ°μ
μ μ
λ ₯ κ°μ λν μ ν¨μ± λ° μ‘°κ±΄ κ²μ¦ λ‘μ§μ ν΅κ³Όν ν νμκ°μ
μ μλ£νλ€.
βοΈ μν¬μ€νμ΄μ€
λ©μΈ νλ©΄μμ μ¬μ©μμ μ±λ μ±ν
λͺ©λ‘κ³Ό Dm λͺ©λ‘μ μ‘°ννλ€.
NSDiffableDataSourceSectionSnapshot
μ ν΅ν΄ νλμ μΉμ
λ΄μμ κ³μΈ΅μ ꡬ쑰λ₯Ό κ°μ§ μ μλλ‘ νμ¬ μΉμ
λ³ ν κΈμ΄ κ°λ₯νλλ‘ κ΅¬ννμλ€.
μν¬μ€νμ΄μ€ κ΄λ¦¬μ μ¬λΆμ λ°λΌ νΈμ§ λ° μμ κΆνμ΄ μ£Όμ΄μ§λ€.
μν¬μ€νμ΄μ€λ₯Ό λκ° λ μν¬μ€νμ΄μ€ κ΄λ¦¬μ μ¬λΆμ μ±λ κ΄λ¦¬μ μ¬λΆλ₯Ό νμΈν ν ν΄μ₯ λ‘μ§μ μννλ€.
Realm
μ μ¬μ©νμ¬ κ³Όκ±° μ±ν
λ΄μκ³Ό μ΄λ―Έμ§ νμΌμ λ‘컬μ μ μ₯νμ¬ λΉν¨μ¨μ μΈ λ€νΈμν¬ ν΅μ μλ₯Ό μ€μλ€.
DBμ λ§μ§λ§μΌλ‘ μ μ₯λ λ μ§ λ°μ΄ν°λ₯Ό Cursor κ°μΌλ‘ μλ²μ μμ²νμ¬ μ½μ§ μμ λ©μΈμ§λ₯Ό μλ²μκ² μλ΅λ°λλ€.
μ±ν
λ°© μ§μ
ν SocketIO
λ₯Ό ν΅ν΄ μμΌ μ°κ²°μ νμ¬ μ€μκ° μ±ν
κΈ°λ₯μ ꡬννμκ³ , ν΄λΉ νλ©΄μ λκ°κ±°λ μ± μ’
λ£ λλ λ°±κ·ΈλΌμ΄λ λͺ¨λλ‘ μ ν λ κ²½μ° μμΌ μ°κ²°μ ν΄μ νμ¬ λΆνμν μλ² νΈμΆμ λ°©μ§νμλ€.
Firebase Cloud Messaging
μλΉμ€λ₯Ό νμ©νμ¬ μ€μκ°μΌλ‘ μ±ν
Push Notificationμ μμ ν μ μλ€.
νΈμ μλ¦Ό ν μ μμ λ°μ λ°μ΄ν°λ₯Ό λμ½λ©νμ¬ ν΄λΉ μ±ν
λ°©μΌλ‘ νλ©΄μ μ΄λνλλ‘ κ΅¬ννμλ€.
βοΈ νλ‘ν λ° μΈμ±κ²°μ
νλ‘ν μ¬μ§, λλ€μ, μ°λ½μ²λ₯Ό μμ ν μ μλ€.
ν¬νΈμμ μΉλ·° κΈ°λ°μΌλ‘ μ°λνμ¬ μΈμ± κ²°μ λ₯Ό ꡬννμλ€. κ²°μ μμμ¦μ ν΅ν΄ μλ²μ μ ν¨μ± μ²΄ν¬ ν ꡬ맀ν μ½μΈμ λ°μνλ€.
βοΈ DB μ‘°ν λ° λ€νΈμν¬ ν΅μ λΉλκΈ° μ΄μ
μ±ν
λͺ©λ‘ λ° μμ½μ λ©μΈμ§ κ°μ λ°μ΄ν° μμ²μ μν΄ DB μ κ·Όκ³Ό μ¬λ¬ λ²μ μλ² ν΅μ μ ꡬννλ©΄μ λͺ¨λ μμ
μ΄ μλ£λκΈ° μ μ λ°μ΄ν°κ° 리ν΄λμ΄ μμ½μ κ°μ λ°μ΄ν°κ° λλ½λ μ±λ‘ λ·°μ μ±λ λͺ©λ‘μ΄ λ³΄μ¬μ§λ λ¬Έμ κ° λ°μ
μ¬λ¬ λ²μ μλ² ν΅μ μΌλ‘ μ¬λ¬ λ²μ λΉλκΈ° μμ
μ΄ μνλμ΄ μμ
μλ£ μμ μ 체ν¬νμ§ λͺ»νμ¬ λ°μ
DispatchGroup
μ ν΅ν΄ μ¬λ¬ κ°μ λΉλκΈ° μμ
μ κ·Έλ£Ήννκ³ , λͺ¨λ μμ
μ΄ μλ£λμμμ notifyλ‘ μ²΄ν¬νμ¬ λ°μ΄ν°λ₯Ό ν λ²μ 리ν΄νλλ‘ κ΅¬ν
// HomeReactor.swift
private func requestUnreadCnt( data: [ ( Channel , String ? ) ] ) -> Observable < [ Channel ] > {
return Observable . create { observer in
let group = DispatchGroup ( )
var channelItems : [ Channel ] = [ ]
data. forEach {
var channel = $0. 0
let last = $0. 1
group. enter ( )
DispatchQueue . main. async {
self . reqeustUnreadChannel ( wsId: channel. workspaceID, name: channel. name, after: last) { unreadCount in
channel. unread = unreadCount ?? 0
channelItems. append ( channel)
group. leave ( )
}
}
}
group. notify ( queue: DispatchQueue . main) {
observer. onNext ( channelItems)
observer. onCompleted ( )
}
return Disposables . create ( )
}
}
// HomeReactor.swift
private func reqeustUnreadChannel( wsId: Int , name: String , after: String ? , completion: @escaping ( ( Int ? ) -> Void ) ) {
ChannelsAPIManager . shared. request ( api: . unreads( wsId: wsId, name: name, after: after ?? nil ) , responseType: UnreadChannelCntResDTO . self)
. asObservable ( )
. subscribe ( with: self ) { owner, result in
switch result {
case . success( let response) :
cnt = response? . count ?? 0
completion ( cnt)
case . failure( let error) :
completion ( nil )
}
}
. disposed ( by: disposeBag)
}
βοΈ μ±ν
μ΄λ―Έμ§ λ‘컬 μ μ₯ λ° μ‘°ν μμ μ΄μ
λ€νΈμν¬ ν΅μ μλ₯Ό μ€μ΄κΈ° μν΄ μ΄λ―Έμ§λ₯Ό λ‘컬μ μ μ₯νλλ‘ κ΅¬ννμλλ°, μ¬λ¬ μ₯μ μ΄λ―Έμ§λ₯Ό λ‘컬μ μ μ₯ν ν μ΄λ―Έμ§ μ΄λ¦μ DBμ μ μ₯νλ €κ³ ν λ, μ΄λ―Έμ§ λ€μ΄λ‘λ κ³Όμ μ΄ λΉλκΈ°λ‘ λμνλ©΄μ μ΄λ―Έμ§ μ΄λ¦μ΄ DBμ μ μ₯λμ§ μλ λ¬Έμ κ° λ°μ
λͺ¨λ μ΄λ―Έμ§ μ μ₯μ μλ£ν μμ μ κΈ°λ€λ¦° ν DBμ μ μ₯νμ¬ λ·°μ μ΄λ―Έμ§λ₯Ό λμΈ λ λ‘컬μμ μ κ·Όνλλ‘ νλ κ²μ λ°μ΄ν°κ° λ§μ μλ‘ μλκ° λλ €μ§ μ μλ€λ λ¬Έμ μ‘΄μ¬
λλ¬Έμ μ μ₯ μ μ μ ν΄λ κ·μΉμ λ§λ μ΄λ¦μ 미리 μμ±νμ¬ μ μ₯νκ³ , μ΄λ―Έμ§ μ μ₯μ λΉλκΈ°λ‘ λμνλλ‘ νμ¬ μ¬μ©μμκ² λ³΄μ¬μ€ λ λ‘컬μ μ΄λ―Έμ§κ° μμΌλ©΄ λ‘컬μμ, μλ€λ©΄ Kingfisherλ₯Ό μ¬μ©νμ¬ λ€μ΄λ‘λνλ λ°©μμΌλ‘ ꡬν
private func saveChatItems( wsId: Int , data: ChannelDTO , chat: [ ChannelMessage ] ) -> [ ChannelMessage ] {
let recordList = chat. map {
let urls : [ String ] = $0. files. map { url in
// μ΄λ―Έμ§ μ΄λ¦ λ¨Όμ μ μ₯
ImageFileService . getFileName ( type: . channel( wsId: wsId, channelId: data. channelId) , fileURL: url)
}
saveImage ( id: wsId, channelId: $0. channelID, files: $0. files, chatId: $0. chatID, fileNames: urls)
let record = $0. toRecord ( )
record. setImgUrls ( urls: urls)
return record
}
do {
try channelRepository. updateChatItems ( data: data, chat: recordList)
debugPrint ( " [SAVE CHAT ITEMS SUCCESS] " )
return recordList. map { $0. toDomain ( ) }
} catch {
print ( error. localizedDescription)
return chat
}
}
βοΈ μμΌ ν΅μ μ’
λ£ μμ
μ€μκ°μΌλ‘ μ±ν
μ μννκΈ° μν΄ μ±ν
νλ©΄μ λ€μ΄κ°μλ μμ μλ μμΌμ μ°κ²°νκ³ , νλ©΄μ λκ° κ²½μ°μλ μ°κ²°μ μ’
λ£ν΄μΌ νκΈ° λλ¬Έμ viewWillAppear()μ viewDidDisappear()μμ μ μ°κ²°κ³Ό ν΄μ λ₯Ό νμμ§λ§, μ±ν
νλ©΄μμ μ±μ λκ°κ² λμμ κ²½μ°μλ μμΌμ΄ μ’
λ£λμ§ μλ λ¬Έμ κ° λ°μ
SceneDelegateμμ sceneDidDisconnect()μ sceneDidEnterBackground() μμ μ μμΌ μ°κ²°μ ν΄μ νκ³ , λ°±κ·ΈλΌμ΄λ λͺ¨λμ μλ€κ° λ€μ μ±ν
νλ©΄μΌλ‘ λμμ¬ κ²½μ° μ¬μ°κ²°μ μν΄ sceneDidBecomeActive() μμ μ μμΌμ λ€μ μ°κ²°νλλ‘ κ΅¬ν
// SceneDelegate.swift
func sceneDidDisconnect( _ scene: UIScene ) {
if SocketNetworkManager . shared. isConnected {
SocketNetworkManager . shared. disconnect ( )
}
}
func sceneDidBecomeActive( _ scene: UIScene ) {
// μ€λ¨λκ² μλ€λ©΄ λ€μ μ°κ²°
SocketNetworkManager . shared. reconnect ( )
}
func sceneDidEnterBackground( _ scene: UIScene ) {
// μ°κ²°λκ² μλ€λ©΄ μ€λ¨
SocketNetworkManager . shared. pauseConnect ( )
}
// SocketNetworkManager.swift
var flag : Bool = false // true -> μ μ μ€λ¨
func pauseConnect( ) {
if isConnected && !flag {
disconnect ( )
flag = true
}
}
func reconnect( ) {
if flag {
connect ( )
flag = false
}
}
λ°±κ·ΈλΌμ΄λ λͺ¨λ μ μμΌ μ°κ²°μ ν΄μ ν λ λ€μ ν¬κ·ΈλΌμ΄λλ‘ λμμ μμΌμ μ¬μ°κ²°μ ν΄μΌν κ²½μ°λ₯Ό 체ν¬νκΈ° μν΄ flagκ°μ μ€μ
μ΄λ² νλ‘μ νΈμ ReactorKit νλ μμν¬λ₯Ό μ¬μ©ν΄ 보μλ€. μ΄μ μ MVVM + Input-Output ν¨ν΄κ³Ό λ€λ₯΄κ² λ°μ΄ν°κ° λ¨λ°©ν₯μΌλ‘λ§ νλ₯΄λλ‘ κ΅¬μ‘°νκ° λμ΄ μκΈ° λλ¬Έμ μ²μμλ ꡬ쑰λ₯Ό μ΅νκ³ μ΅μν΄μ§λλ° μκ°μ΄ μ’ κ±Έλ Έλ€. μ΅μν΄μ§κ³ λλ λ§μ λ‘μ§λ€μ΄Β μμ±λμ΄ μμ΄λ ꡬ쑰νλμ΄ μκΈ° λλ¬Έμ κ°λ
μ±μ΄ λμμ§κ³ , μ μ§ λ³΄μνκΈ° μ¬μ΄ μ½λλ₯Ό μμ±ν μ μκ² λμλ€.
본격μ μΈ μλ²μμ μμ
μΌλ‘ FCMκ³Ό μΈμ±κ²°μ ꡬνμ κ²½ννκ² λμλ€. μμν¬νΈ λΌμ΄λΈλ¬λ¦¬λ‘ ν¬νΈμ μλΉμ€λ₯Ό μ°λνκ³ , μ€ κ²°μ μ μμμ¦ μ ν¨μ± κ²μ¦ λ‘μ§ νλ‘μ°λ₯Ό κ²½νν΄λ³Ό μ μμ΄μ μ’μλ€.
FCM μλΉμ€λ₯Ό νμ©νμ¬ μλ²λ‘λΆν° μ€μκ°μΌλ‘ μ±ν
μλ¦Όμ λ°μλ³Ό μ μλλ‘ κ΅¬ννμλ€. νΈμ μλ¦Ό ν μ ν΄λΉ μ±ν
λ°©μΌλ‘ μ΄λνλλ‘ κ΅¬ννλ©΄μ Coordinator ν¨ν΄ μ μ©μ νμμ±μ λλΌκ² λμλ€. μ¬λ¬ 쑰건μ λ°λΌ νλ©΄ μ΄λνλλ‘ κ΅¬ννλ©° νλ©΄ μ΄λ λ‘μ§μ΄ ViewControllerμ μμ ν μμ‘΄νκ³ μλ€λ κ²μ κΉ¨λ¬μκ³ , ViewController μΈλΆμμ νλ©΄ μ ν λ‘μ§μ ꡬννλ κ²μ΄ νμνλ€λ κ²μ λκΌλ€.
μ΄ νλ‘μ νΈλ₯Ό νλ©΄μ κΈ°ν, μλ², λμμΈμ΄ μ 곡λκ³ , μ€λ‘μ§ iOS κ°λ°λ§ λ΄λΉνλ©΄ λλ€λ μ μμ μ€μ μ
무 νκ²½κ³Ό λΉμ·ν κ²½νμ ν΄λ³Ό μ μμλ€. μ΄ νλ‘μ νΈλ‘ λΉλκΈ° μμ
μ²λ¦¬μ λν μ€μμ±μ λλΌκ² λμλ€. λ§μ λ€νΈμν¬ ν΅μ μ μ°κ²°νλ©° λΉλκΈ° μμ
μ μ²λ¦¬νλ κ²μ΄ κ°μ₯ μ΄λ ΅κ² λκ»΄μ‘λ€. GCDμ Swift Concurrencyμ λν΄ λ 곡λΆνμ¬ μ΄νμ λΉλκΈ° μμ
μ μμνκ² μ²λ¦¬ν μ μλλ‘ ν΄μΌκ² λ€.