Skip to content

11 18 팀 현황 공유 및 목표 설정

Yune gim edited this page Dec 2, 2024 · 2 revisions

금주 목표

팀원 별 목표?

건우

  • 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)

Core Layer 하위 Framework

  • Extensions
  • Utilities

Core Layer Project

  1. Dynamic Library 타겟 생성 - File > New > Target에서 Dynamic Library 선택. - 이름: Core. - Language: Swift.
  2. Extensions 폴더 포함 - Core 내부에 Extensions 디렉토리 생성. - Extensions 관련 파일은 별도로 관리하고, Core의 소스에는 포함하지 않음.
  3. Core Module 노출 - Core에 필요한 기능과 인터페이스를 Dynamic Library로 구현합니다.

Xcode에서 새로운 프로젝트 생성

  1. Xcode를 열고 File → New → Project로 이동합니다.
  2. Template 선택 화면에서: - Framework & Library → Dynamic Library 선택.
  3. Product Name에 라이브러리 이름을 입력합니다. - 예: Core.
  4. TeamOrganization Identifier를 설정한 뒤, Dynamic Library 전용 프로젝트가 생성됩니다.

2️⃣ Dynamic Library 프로젝트 구성

  1. 프로젝트 디렉토리에는 Sources 폴더를 생성하고, 라이브러리 코드를 작성합니다.
  2. Target의 Build Settings를 확인하여 아래 값들을 설정: - Mach-O Type: Dynamic Library. - Build Active Architecture Only: No (다양한 아키텍처 지원). - Library Search Paths: 다른 프로젝트에서 사용할 경우, 빌드 결과물을 검색할 경로를 지정.

3️⃣ 빌드 후 결과물 확인

  1. Product: .dylib 또는 .framework 형식으로 빌드됩니다.
  2. 결과물은 Build 폴더에 위치합니다. - 기본 경로: DerivedData/<ProjectName>/Build/Products/<Configuration>-<Platform>/

4️⃣ 다른 프로젝트에서 사용

  1. 다른 프로젝트에서 Dynamic Library를 사용하려면: - 해당 .dylib 또는 .framework 파일을 프로젝트로 가져옵니다. - Build Phases → Link Binary with Libraries에 추가합니다. - Framework Search Paths를 설정하여 경로를 연결합니다.

Extensions Framework 타겟 생성

  1. File > New > Target - Framework 선택. - 이름: Extensions. - Language: Swift.
  2. Core에 의존성 추가 - Extensions에서 Core 모듈의 일부를 사용해야 한다면, Link Binary With LibrariesCore.a를 추가.
  3. Extensions 파일 작성 - Extensions 디렉토리에 필요한 Swift Extensions 파일 추가. 예: UIColor+Extension.swift, Array+Extension.swift.

App Target에서 Core와 Extensions 연결

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에 버튼 추가 필요 (연결 상태에 따른 활성화 로직)
```

차은우원빈현빈장원영의

개발 스토리

✏️ 기획


✔️ 규칙


📌 1주차 회의록

데일리 스크럼

회의록

회고

📌 2주차 회의록

데일리 스크럼

회의록

회고

📌 3주차 회의록

데일리 스크럼

회의록

회고

📌 4주차 회의록

데일리 스크럼

회의록

회고

📌 5주차 회의록

데일리 스크럼

회의록

회고

📌 6주차 회의록

데일리 스크럼

회의록

회고


🔥 트러블슈팅

Clone this wiki locally