출시 기간 : 2024.04.11 - 05.05 (약 3주)
기획/디자인/개발 1인 개발
프로젝트 환경 - iPhone 전용(iOS 16.0+), 라이트 모드 고정
자게?, 아니 자게! - 자유로운 소통을 위한 자유게시판
-
회원가입 / 회원탈퇴 / 로그인 / 로그아웃
-
게시글 / 댓글 / 공감 및 비공감 작성 및 조회
-
프로필 수정 및 조회 / 팔로잉 팔로우
-
실시간 유저 / 게시판의 게시글 순위 조회
-
실시간 1:1 채팅
-
프레임워크
UIKit, SwiftUI
-
아키텍처
MVVM-C
-
오픈 소스(Cocoapods)
RxSwift / Realm
Alamofire / Kinfisher / SocketIO
RxDataSource / Snapkit / Tabman / SideMenu
MVVM-C
-
View 및 Business 로직을 분리하기 위한
MVVM-C
아키텍처를 도입 -
Input-Output 패턴
의 Protocol을 채택함으로써 User Interaction과 View Data 핸들링protocol ViewModelType { var disposeBag : DisposeBag { get } associatedtype Input associatedtype Output func transform(input : Input) -> Output } protocol CombineViewModelType : AnyObject, ObservableObject { associatedtype Input associatedtype Output var cancellables : Set<AnyCancellable> {get set} var input : Input {get set} var output : Output {get set} func transform() }
-
Flow Logic을 관리하고, View 간의 Dependency를 줄이기 위해
Coordinator 패턴
적용protocol Coordinator : AnyObject { var finishDelegate: CoordinatorFinishDelegate? { get set } var childCoordinators: [Coordinator] { get set } var navigationController: UINavigationController { get set } var type: CoordinatorType { get } func start() func finish() } extension Coordinator { func finish() { childCoordinators.removeAll() finishDelegate?.coordinatorDidFinish(childCoordinator: self) } }
Reactive Programming
- 비동기 Event의 관리를 위한
RxSwift
와Combine
를 이용한 Reactive Programming 구현
Alamofire
-
URLRequestConvertible
을 활용한Router 패턴
기반의GET/POST/DEL/PUT
메소드를 활용한RESTful API Network
와의 통신 구현 -
RequestInterceptor Protocol
채택함으로써,JWT(Json Web Token)
갱신 적용
SocketIO + Realm
- 과도한 API 호출을 방지하기 위해,
Realm Table Fetch -> Latest Date를 Server에 요청 -> Realm Table Update -> Connect Socket
의 Logic을 이용하여 사용자간 1:1 채팅 구현
-
문제 상황
- UIImage를 별도의 항목으로 추가하는 것이 아닌, UITextView내의 Text(String)로 추가하는 것을 목표로 구현을 시도
- UIImage를 TextView에 추가하는 것은 가능했지만, 1) 기존 Text가 존재하거나, 2) UIImage 추가 후 Text 또는 UIImage를 추가하는 경우 순서가 보장되지 않는 문제가 발생
-
해결 방법
-
func insertImageIntoTextView
를 통한 UITextView에 특정 UIImage 삽입private func insertImageIntoTextView(image: UIImage) { // TextView의 size let newWidth = textView.bounds.width - 30 let scale = newWidth / image.size.width let newHeight = image.size.height * scale let resizedImage = image.resizeImage(targetSize: CGSize(width: newWidth, height: newHeight)) // 이미지를 삽입할 위치 설정 (기존 텍스트 끝에 삽입) let endPosition = textView.endOfDocument let insertionPoint = textView.offset(from: textView.beginningOfDocument, to: endPosition) let attachment = NSTextAttachment() attachment.image = resizedImage let imageAttributedString = NSAttributedString(attachment: attachment) textView.textStorage.insert(imageAttributedString, at: insertionPoint) }
-
func getImageLocations
를 통한 UITextView에 삽입된 특정 UIImage의 position 추출func getImageLocations() -> [Int] { var imageLocations: [Int] = [] self.attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: self.attributedText.length), options: []) { (value, range, stop) in if let _ = value as? NSTextAttachment { imageLocations.append(range.location) } } return imageLocations }
-
func _addTextViewImage
를 통하여 Text가 순서가 보장되며 이미지를 추가함private func _addTextViewImage(url : URL, location: Int) { KingfisherManager.shared.downloader.downloadImage(with: url, options: [.requestModifier(AuthManager.kingfisherAuth())] ) { [weak self] result in guard let self = self else { return } switch result { case .success(let imageResult): // 이미지 다운로드 성공 시 NSAttributedString을 만들어서 UITextView에 삽입 let newWidth = textView.bounds.width - 15 let scale = newWidth / imageResult.image.size.width let newHeight = imageResult.image.size.height * scale let resizedImage = imageResult.image.resizeImage(targetSize: CGSize(width: newWidth, height: newHeight)) let attachment = NSTextAttachment() attachment.image = resizedImage let imageAttributedString = NSAttributedString(attachment: attachment) // 원하는 위치에 이미지 삽입 let mutableAttributedString = NSMutableAttributedString(attributedString: textView.attributedText) let range = NSRange(location: location, length: 0) // 특정 위치 (예: 10번째 문자 뒤) mutableAttributedString.insert(imageAttributedString, at: range.location) textView.attributedText = mutableAttributedString textViewDidChange(textView) case .failure(let error): print("Image download failed: \(error)") } } }
-
-
문제 상황
- UIKit 프로젝트에서 UIHostringController를 사용하여 SwiftUI의 View를 적용시키고자 하였으며, 복수의 View로 구성되어 Navigation을 이용한 화면 전환이 필요하게 되었음.
- 하지만 Coordinator 패턴의 채택으로 모든 화면 전환은 Coordinator가 관장하는데, SwiftUI View에서 Navigation Stack을 사용하여 화면 전환을 시도할 경우, UIKit의 UINavigationController와 SwiftUI의 Navigation Stack이 충돌되는 현상 발생
-
해결 방법
- SwiftUI의 Navigation Stack을 이용한 화면의 직접 전환이 아닌, UIKit의 Coordinator에 의존하여 화면 전환을 하도록 구성
final class ChatListCoordinator : Coordinator { weak var finishDelegate: CoordinatorFinishDelegate? var childCoordinators: [Coordinator] = [] var navigationController: UINavigationController var parentBoardCoordinator : BoardMainCoordinator? var type: CoordinatorType { .tab } init(navigationController: UINavigationController) { self.navigationController = navigationController } func start() { } func start(chatlist : MyChatResponse) { var childView = ChatListView(chatList: chatlist) childView.parentCoordinator = self let vc = ChatListViewController(contentViewController: UIHostingController(rootView: childView)) self.navigationController.pushViewController(vc, animated: true) } }
- Coordinator를 통해 SwiftUI의 View를 직접 호출하는 것이 아닌, 해당 SwiftUI View의 UIHostringController를 호출함으로써 UIKit과 SwiftUI의 Navigation Stack 중첩 문제를 해결