Skip to content

Conversation

@hooni0918
Copy link
Member

@hooni0918 hooni0918 commented Aug 8, 2025

🔗 연결된 이슈

📄 작업 내용

  • UIkit 으로 구현한 Mypage 부분을 SwiftUI로 마이그레이션
  • UIkit 기반의 네비게이션 뷰를 통해 뷰만 SwiftUI로 전환

💻 주요 코드 설명

RxSwift와 SwiftUI의 데이터 바인딩 통합

문제점: RxSwift BehaviorRelay vs SwiftUI @ObservedObject

기존 UIKit + RxSwift 코드

// ViewModel (RxSwift)
class MyPageViewModel {
let userInfo: BehaviorRelay<LoginUserModel?> = BehaviorRelay(value: nil)
let editButtonTapped = PublishRelay<Void>()
}

// ViewController
viewModel.userInfo
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] userInfo in
self?.updateUI(with: userInfo)
})
.disposed(by: disposeBag)

문제 상황

SwiftUI의 @ObservedObject는 Combine의 ObservableObject 프로토콜을 준수해야
하는데, RxSwift의 BehaviorRelay는 준수안함 (당연함)

// ❌ 컴파일 에러 발생
struct MyPageContentSwiftUIView: View {
@ObservedObject var viewModel: MyPageViewModel  // Error: MyPageViewModel은 ObservableObject가 아님
}

해결책: Wrapper 패턴으로 브리징

[ObservableObject](https://developer.apple.com/documentation/combine/observableobject) 를 써서 Wrapper 클래스로 해결핑

// Wrapper 클래스로 RxSwift → Combine 브리징
class MyPageViewModelWrapper: ObservableObject {
@Published var userInfo: LoginUserModel?  // Combine의 @Published
let viewModel: MyPageViewModel            // 기존 RxSwift ViewModel
private let disposeBag = DisposeBag()

  init(viewModel: MyPageViewModel) {
      self.viewModel = viewModel

      // RxSwift Observable을 Combine @Published로 변환
      viewModel.userInfo
          .subscribe(onNext: { [weak self] info in
              DispatchQueue.main.async {
                  self?.userInfo = info  // @Published 프로퍼티 업데이트
              }
          })
          .disposed(by: disposeBag)
  }

}

// SwiftUI View에서 사용
struct MyPageContentSwiftUIView: View {
@StateObject private var viewModelWrapper: MyPageViewModelWrapper

  init(viewModel: MyPageViewModel) {
      _viewModelWrapper = StateObject(wrappedValue:
      MyPageViewModelWrapper(viewModel: viewModel))
}

  var body: some View {
      Text(viewModelWrapper.userInfo?.name ?? "Unknown")
          .onChange(of: viewModelWrapper.userInfo) { newValue in
              // 변경 감지
          }
  }

}

UIHostingController 통합

문제점: 네비게이션 구조 유지

https://developer.apple.com/documentation/swiftui/uihostingcontroller

1. 전체 교체 (했다가 실패함ㅠ)

// ❌ 문제가 있었던 접근
class MyPageViewController: UIHostingController<MyPageSwiftUIView> {
				init() {
					super.init(rootView: MyPageSwiftUIView())
			}
}

문제는 BaseViewController의 기능 상실, 네비게이션 바 커스터마이징 어려워지게 되더라구요

해서 안되니까 Child View Controller 채택

class MyPageViewController: BaseViewController {
private var hostingController: UIHostingController<MyPageSwiftUIView>?

  private func setupSwiftUIView() {
      let swiftUIView = MyPageSwiftUIView(viewModel: viewModel)
      hostingController = UIHostingController(rootView: swiftUIView)

      guard let hostingController = hostingController else { return }

      // Child View Controller로 추가 (Apple 권장 방식)
      addChild(hostingController)
      view.addSubview(hostingController.view)

      // Auto Layout 설정 -> 이건 전체 뷰 바꾸기 이전 상황이라 일단 기본으로..
      hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
...
])
      hostingController.didMove(toParent: self)  //부모-자식 관계 지정

문제점: fillEqually Distribution 구현

https://developer.apple.com/documentation/swiftui/geometryreader

기존코드

class MyPageEtcSettingView: BaseView {
		let stackView = UIStackView(axis: .vertical).then {
				$0.spacing = 12
				$0.distribution = .fillEqually  
}
...

  override func setupAutoLayout() {
      stackView.snp.makeConstraints {
          $0.edges.equalToSuperview().inset(22)
      }
...
  
  

GeometryReader로 바로 정상

struct MyPageEtcSettingSwiftUIView: View {
		var body: some View {
				GeometryReader { geometry in
					VStack(spacing: 12) {
let itemCount = 5
let totalSpacing = 12 * (itemCount - 1)  // 48
let padding = 44  // 상하 패딩 22 * 2
let itemHeight = (geometry.size.height - CGFloat(padding) -
CGFloat(totalSpacing)) / CGFloat(itemCount)

              SettingRow(title: "버전정보", subtitle: "1.0.2")
                  .frame(height: itemHeight)  // fillEqually 효과

              SettingRow(title: "이용약관")
                  .frame(height: itemHeight)

              SettingRow(title: "문의하기")
                  .frame(height: itemHeight)

              SettingRow(title: "로그아웃")
                  .frame(height: itemHeight)

              SettingRow(title: "탈퇴하기")
                  .frame(height: itemHeight)
          }
          .padding(22)
      }
  }

}

네비게이션 로직은 UIkit이 뷰만 SwiftUI에서

SwiftUI에서 UIKit ViewController Push를 유지해야 하는 상황 [참고](https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable)

  • 네비게이션 로직: UIKit (UINavigationController, push/pop 등)

    • 개별 화면(View): SwiftUI
    • 화면 비율 유틸리티: UIKit의 Screen 클래스를 SwiftUI View에서 재사용

    구체적인 구조

    // UIKit - ViewController에서 네비게이션 처리
    class ProfileViewController: UIViewController {
    
    	override func viewDidLoad() {
    	super.viewDidLoad()
    
    // SwiftUI View를 UIHostingController로 감싸서 사용
    let swiftUIView = ProfileView(
         profileImage: userImage,
         name: userName
     )
    
    let hostingController = UIHostingController(rootView: swiftUIView)
    
    // UIKit 네비게이션으로 push
     navigationController?.pushViewController(hostingController, animated: true)
    }
    
    // SwiftUI - View만 담당
    struct ProfileView: View {
    let profileImage: UIImage
    let name: String
    
    var body: some View {
        VStack {
            Image(uiImage: profileImage)
                .frame(
                    width: Screen.height(82),   // UIKit의 Screen 유틸리티 사용
    								height: Screen.height(82)
    )
            Text(name)
                .padding(.top, Screen.height(12))
    	    }
    		}
    
    	}
    

Equatable 프로토콜 추가

문제점: [onChange](https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:)) 모디파이어 사용 시 필요

// ❌ 컴파일 에러
.onChange(of: viewModelWrapper.userInfo) { newValue in
// Error: LoginUserModel이 Equatable이 아님
}

해결책: Model에 Equatable 추가

// Before
struct LoginUserModel: ResponseModelType {
let userID: Int
let name: String?
}

// After
struct LoginUserModel: ResponseModelType, Equatable {  // Equatable 추가
let userID: Int
let name: String?
}

👀 기타 더 이야기해볼 점

마이그레이션은 하나씩 천천히 해야겟어요 간단한뷰라 쉬울줄알앗는데 개헷갈림;;

@hooni0918 hooni0918 self-assigned this Aug 8, 2025
@hooni0918 hooni0918 added ♻️ refactor 기존 코드를 리팩토링하거나 수정하는 등 사용 (생산적인 경우) 🧡 JiHoon 쌈뽕한 플러팅 꿀팁을 듣고 싶다면 labels Aug 8, 2025
Copy link
Contributor

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!
이런 걸 보니, 저도 스유가 해보고 싶어지네요.

Comment on lines +20 to +27
SettingRow(
title: "버전정보",
subtitle: "1.0.2",
action: {
print("버전정보 탭됨")
}
)
.frame(height: (geometry.size.height - 44 - 48) / 5) // padding 44, spacing 48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반복적인 SettingRow를 생성하는 코드들을 줄일 수 있는 방법이 있을까요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧡 JiHoon 쌈뽕한 플러팅 꿀팁을 듣고 싶다면 ♻️ refactor 기존 코드를 리팩토링하거나 수정하는 등 사용 (생산적인 경우)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants