-
Notifications
You must be signed in to change notification settings - Fork 1
11 18 팀 현황 공유 및 목표 설정
Yune gim edited this page Dec 2, 2024
·
2 revisions
- 에픽 1, 2 빠르게 완성 후 에픽 3 Task 분류 ✅
- 배포 꼭 하기 ← 안하면 혼남
- 회고록 Wiki 최신화 & 발표 준비
- [지난 금요일 오프라인 미팅 후 개선 사항](https://www.notion.so/95c20a107c1e4aad9f994d2efd528fd1?pvs=21) → 한번 더 리마인드 대화 필요 ✅
- 수요일 학습 공유를 위한 MultipeerConnectivity 학습 ✅
- 빠른 석영님 PR 리뷰 및 머지 ✅
-
DI ✅
-
Core 모듈화 → 차선책: framework / 베스트: Project ✅
-
invite 관련 팝업 및 연결 완료까지 (화면 연결) ✅
-
화면간 연결 + MainViewController
- 위에 GroupInfoViewController로 고정시켜 버리고, 하단만 갈아끼우게
⇒ MainViewController를 두는 작업 필요(하단 VC만 갈아끼울 수 있도록) (navigation) → 건우님
-
GoupInfoView Model 연결?
DI
ConnectionViewController 관련 circleView의 addSubView 순서 변경 필요
```swift
final class DIContainer {
static let shared = DIContainer()
private var services: [String: Any] = [:]
func register<T>(type: T.Type, instance: T) {
let key = String(describing: type)
services[key] = instance
}
func resolve<T>(type: T.Type) -> T {
let key = String(describing: type)
guard let service = services[key] as? T else {
fatalError("\(key) is not registered.")
}
return service
}
}
```
```swift
private extension SceneDelegate {
func registerDependencies() {
// MARK: - Socket Provider
DIContainer.shared.register(
type: SocketProvider.self,
instance: SocketProvider()
)
// MARK: - Repository
DIContainer.shared.register(
type: BrowsingUserRepository.self,
instance: BrowsingUserRepository(
socketProvider: DIContainer.shared.resolve(type: SocketProvider.self)
)
)
DIContainer.shared.register(
type: ConnectedUserRepository.self,
instance: ConnectedUserRepository(
socketProvider: DIContainer.shared.resolve(type: SocketProvider.self)
)
)
// MARK: - UseCase
DIContainer.shared.register(
type: BrowsingUserUseCase.self,
instance: BrowsingUserUseCase(
repository: DIContainer.shared.resolve(type: BrowsingUserRepository.self)
)
)
DIContainer.shared.register(
type: ConnectedUserUseCase.self,
instance: ConnectedUserUseCase(
repository: DIContainer.shared.resolve(type: ConnectedUserRepository.self)
)
)
// MARK: - View Models
DIContainer.shared.register(
type: ConnectionViewModel.self,
instance: ConnectionViewModel(
usecase: DIContainer.shared.resolve(type: BrowsingUserUseCase.self)
)
)
DIContainer.shared.register(
type: GroupInfoViewModel.self,
instance: GroupInfoViewModel()
)
DIContainer.shared.register(
type: VideoListViewModel.self,
instance: MockVideoListViewModel()
)
}
}
```
```swift
## 관련 이슈
- https://github.com/boostcampwm-2024/iOS09-BeStory/issues/19
## ✅ 완료 및 수정 내역
- [x] DIContainer 구현
- [x] 객체를 등록하고 화면 전환을 구현(ConnectionView)
## 🛠️ 테스트 방법
- 직접 실행하여 테스트 가능합니다.
## 📝 리뷰 노트
### 💡 각 모듈을 import 하는 과정에서 발생한 문제
import가 안되는 문제 + Repository와 Repository Interface가 계속 다르다는 문제(같음에도 불구하고)가 발생했습니다.
이를 해결하기 위해 각 레이어의 Framework Dependency를 확인하고 알맞게 수정했습니다.
### 💡 ConnectionViewController에서 CircleView가 addSubView되기 전에 layout을 설정한 문제
해당 문제는 addSubView의 순서가 잘못되어 발생했습니다.
따라서 addSubView 후에 layout을 설정하도록 수정하여 해결했습니다.
### 💡 참고 사항
- DIContainer를 처음 구현해보다보니 부족한 점이 많습니다. 리뷰 부탁드립니다.
- 현재 ConnectionView만 정상적으로 출력이 되게끔 했습니다. 다른 화면은 완성 및 흐름 논의 후 재 설정해야합니다.
```
Core 모듈화 (Project)
- Extensions
- Utilities
-
Dynamic Library 타겟 생성
- File > New > Target에서 Dynamic Library 선택.
- 이름:
Core
. - Language: Swift. -
Extensions 폴더 포함
-
Core
내부에Extensions
디렉토리 생성. -Extensions
관련 파일은 별도로 관리하고,Core
의 소스에는 포함하지 않음. -
Core Module 노출
-
Core
에 필요한 기능과 인터페이스를 Dynamic Library로 구현합니다.
- Xcode를 열고 File → New → Project로 이동합니다.
- Template 선택 화면에서: - Framework & Library → Dynamic Library 선택.
-
Product Name에 라이브러리 이름을 입력합니다.
- 예:
Core
. - Team과 Organization Identifier를 설정한 뒤, Dynamic Library 전용 프로젝트가 생성됩니다.
- 프로젝트 디렉토리에는
Sources
폴더를 생성하고, 라이브러리 코드를 작성합니다. -
Target의 Build Settings를 확인하여 아래 값들을 설정:
- Mach-O Type:
Dynamic Library
. - Build Active Architecture Only:No
(다양한 아키텍처 지원). - Library Search Paths: 다른 프로젝트에서 사용할 경우, 빌드 결과물을 검색할 경로를 지정.
-
Product:
.dylib
또는.framework
형식으로 빌드됩니다. - 결과물은
Build
폴더에 위치합니다. - 기본 경로:DerivedData/<ProjectName>/Build/Products/<Configuration>-<Platform>/
- 다른 프로젝트에서 Dynamic Library를 사용하려면:
- 해당
.dylib
또는.framework
파일을 프로젝트로 가져옵니다. - Build Phases → Link Binary with Libraries에 추가합니다. - Framework Search Paths를 설정하여 경로를 연결합니다.
-
File > New > Target
- Framework 선택.
- 이름:
Extensions
. - Language: Swift. -
Core에 의존성 추가
-
Extensions
에서Core
모듈의 일부를 사용해야 한다면, Link Binary With Libraries에Core.a
를 추가. -
Extensions 파일 작성
-
Extensions
디렉토리에 필요한 Swift Extensions 파일 추가. 예:UIColor+Extension.swift
,Array+Extension.swift
.
1. **Core 연결**
- App Target에서 **Link Binary With Libraries**에 `Core.a`를 추가.
2. **Extensions 연결**
- App Target에서 **Link Binary With Libraries**에 `Extensions.framework`를 추가.
3. **Import**
- 필요한 곳에서 다음과 같이 사용:
```swift
import Core
import Extensions
let color = UIColor.appPrimary
let uniqueArray = [1, 2, 2, 3].unique()
```
Invite 관련 리뷰 (Project)
- 기존 ViewModel 코드
```swift
// MARK: - Transform
extension ConnectionViewModel {
func transform(_ input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
input.sink { [weak self] result in
guard let self else { return }
switch result {
case .fetchUsers:
fetchUsers().forEach({ self.found(user: $0) })
case .invite(let id):
invite(id: id)
}
}
.store(in: &cancellables)
return output.eraseToAnyPublisher()
}
}
// MARK: - UseCase Methods
private extension ConnectionViewModel {
func fetchUsers() -> [BrowsedUser] {
return usecase.fetchBrowsedUsers()
}
func invite(id: String) {
usecase.inviteUser(with: id)
}
}
// MARK: - Binding
private extension ConnectionViewModel {
func setupBind() {
usecase.browsedUser
.sink { [weak self] updatedUser in
guard let self else { return }
switch updatedUser.state {
case .found:
found(user: updatedUser)
case .lost:
lost(user: updatedUser)
default:
break
}
}
.store(in: &cancellables)
}
}
```
- 수정된 ViewModel 코드
```swift
// MARK: - Transform
extension ConnectionViewModel {
func transform(_ input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
input.sink { [weak self] result in
guard let self else { return }
switch result {
// Connection Input
case .fetchUsers:
fetchUsers().forEach({ self.found(user: $0) })
case .invite(let id):
invite(id: id)
// Invitation Input
case .accept(let id):
acceptInvitation(from: id)
case .reject(let id):
rejectInvitation(from: id)
}
}
.store(in: &cancellables)
return output.eraseToAnyPublisher()
}
}
// MARK: - UseCase Methods
private extension ConnectionViewModel {
func fetchUsers() -> [BrowsedUser] {
return usecase.fetchBrowsedUsers()
}
func invite(id: String) {
usecase.inviteUser(with: id)
}
func acceptInvitation(from id: String) {
usecase.acceptInvitation(from: id)
}
func rejectInvitation(from id: String) {
usecase.rejectInvitation(from: id)
}
}
// MARK: - Binding
private extension ConnectionViewModel {
func setupBind() {
// Browsed User (found, lost)
usecase.browsedUser
.sink { [weak self] updatedUser in
guard let self else { return }
switch updatedUser.state {
case .found:
found(user: updatedUser)
case .lost:
lost(user: updatedUser)
default:
break
}
}
.store(in: &cancellables)
// Invitation Received From Who
usecase.invitationReceived
.sink { [weak self] invitationReceived in
guard let self else { return }
invited(from: invitationReceived)
}
.store(in: &cancellables)
// Invitation Result (when I invite other user)
usecase.invitationResult
.sink { [weak self] invitedUser in
guard let self else { return }
switch invitedUser.state {
case .accept:
accepted(by: invitedUser.name)
case .reject:
rejected(by: invitedUser.name)
}
.store(in: &cancellables)
// Invitaion Fired Due to Timeout (invited User receive)
usecase.invitationDidFired
.sink { [weak self] _ in
guard let self else { return }
// TODO: - output for timeout?
}
.store(in: &cancellables)
}
}
// MARK: - Output Methods
private extension ConnectionViewModel {
// Connection Output
func found(user: BrowsedUser) {
guard
self.getCurrentPosition(id: user.id) == nil,
let position = self.getRandomPosition(),
let emoji = self.getRandomEmoji()
else { return }
self.addCurrentPosition(id: user.id, position: position)
self.output.send(
.found(
user: user,
position: position,
emoji: emoji
)
)
}
func lost(user: BrowsedUser) {
guard let position = self.getCurrentPosition(id: user.id) else { return }
self.removeCurrentPosition(id: user.id)
self.output.send(.lost(user: user, position: position))
}
// Invitation Output
func invited(from user: BrowsedUser) {
self.output.send(.invited(from: user)) // ViewController에서 popup
}
func accepted(by name: String) {
self.output.send(.accepted(by: name)) // "\(name)\nInvitation has been Accepted
}
func rejected(by name: String) {
self.output.send(.rejected(by: name)) // "\(name)\nInvitation has been Rejected"
}
}
```
```swift
// MARK: - Input
enum ConnectionViewInput {
// Connection Input
case fetchUsers
case invite(id: String)
// Invitation Input
case accept(from id: String)
case reject(from id: String)
}
// MARK: - Output
enum ConnectionViewOutput {
// Connection Output
case found(user: BrowsedUser, position: (Double, Double), emoji: String)
case lost(user: BrowsedUser, position: (Double, Double))
// Invitation Output
case invited(from: BrowsedUser)
case accepted(by name: String)
case rejected(by name: String)
// case timeout()
}
```
```swift
B가 timeout으로 받는 invitationDidFired 때 팝업을 어떻게 처리할지? 남은 시간 출력 해야하나?
A는 요청 후 어떻게 기다릴지? -> 기다리는 팝업?
ViewController에서 popup과 input 관련 코드 추가 필요
그 후 GroupInfoViewController 고민하기
```
화면 연결 (MainViewController)
```swift
public final class MainViewController: UIViewController {
// MARK: - UI Components
private let groupInfoViewController: UIViewController
private let navigationController: UINavigationController
// MARK: - Initializer
init(
groupInfoViewController: UIViewController,
initialViewController: UIViewController
) {
self.groupInfoViewController = groupInfoViewController
self.navigationViewController = UINavigationController(rootViewController: initialViewController)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
setupViewAttributes()
setupViewHierarchies()
setupViewConstraints()
}
}
// MARK: - UI Configure
private extension MainViewController {
func setupViewAttributes() {
}
func setupViewHierarchies() {
}
func setupViewConstraints() {
}
}
```
```swift
각 ViewController에 Closure를 사용한 화면 전환
Closure는 SceneDelegate에서 DI를 통한 객체 주입 시 함께 주입
ConnectionViewController에 버튼 추가 필요 (연결 상태에 따른 활성화 로직)
```