Skip to content

godo129/RIBs-todo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

70 Commits
 
 
 
 
 
 

Repository files navigation

RIBs-todo

앱 소개

RIBs 를 적용한 todo 앱입니다.

Tuist 를 이용한 모듈화를 진행했습니다.

앱 구조

스크린샷 2023-09-22 오전 11 30 43

위와 같은 구조로 앱이 구성되어 있습니다.

Todo 는 UIKit 으로 Profile 은 SwiftUI 로 구성하였습니다.

그리고 RIBs 를 이용했습니다.

RIBs 를 선택한 이유

옵션들

MVC

Untitled (29)

  • MVC 를 사용하면 ViewController 의 크기 비대해지는 문제가 생기게 되고, 논리적인 부분이 너무 걸쳐 있어서 관리하기도 어렵고 중복된 코드가 발생하는 경우가 많았습니다.
  • 물론 크기가 작은 프로젝트에서는 MVC 를 사용해도 괜찮지만 MVC 는 View 와 이를 보여주는 로직이 존재하는 한 적합한 패턴이라고 생각하다고 느끼진 못 했습니다.

MVVM

Untitled (30)

  • MVVM 은 ViewController 에서의 로직에 해당하는 부분을 ViewModel 에 분리한 형태, 그러나 이것은 이렇게 써야 한다라는 특별하게 정의된 패턴이 없다보니 개발자마다 다들 자신만의 방식으로 만들다보니 통합하기 어려운 모습이 보였습니다.
  • 그러다보니 많은 사람들이 코드를 작성할 때 어떤형식으로 만들지에 대한 약속을 해야하고 그렇지 않으면 관리하기 힘든 모습을 보일 수 있다고 생각하게 되었습니다.

TCA

Untitled (31)

  • TCA는 ViewStore 을 이용해서 뷰의 Action 에 따라 Effect 가 발생하게 되고 이에 따라서 State 가 변경 되는 형태로 구성되어 있습니다. 찾아보니 Flux 패턴이 있던데 이와 비슷한 방식인 것 같습니다.
Untitled (32)

위는 제가 TCA 를 적용해서 만든 코드입니다.

  • 프레임워크여서 이미 정해진 규칙이 있기 때문에 MVVM 보다 더 일관적으로 코드를 유지할 수 있다고 생각하게 되었습니다. 하지만

Untitled (33)

ViewStore 이 ObservableObject 라는 프로토콜을 채택하고 있고 이것은 Combine 와 관련이 있는 부분이기 때문에 RxSwift 를 사용하는 프로젝트에서는 TCA 를 적용하면 조금은 생각을 해봐야 될 것 같다고 생각하게 되었습니다.

RIBs

Untitled (34)

  • RIBs 는 Router, Interactor 및 Builder 의 약자로써 Builder 은 RIB 들을 만드는 것을 뜻하고, Router 은 뷰간의 전환을 Interactor 은 View 에서 발생한 이벤트들을 처리하는 부분을 뜻합니다. 그리고 Presenter 와 View 는 각각 ViewModel 과 ViewController 의 역할을 할 수 있는 부부입니다. Component 는 의존선 부분이라고 보면 됩니다.
  • 이 RIBs 또한 프레임워크기 때문에 정해진 패턴이 존재합니다. 그리고 Uber 에서 만든 cross-platform 모바일 아키텍쳐 프레임워크 이기 때문에 다른 모바일 기기에서도 이 패턴을 사용할 수 있다고 합니다. 상당히 많은 회사에서 RIBs 로 전환을 하는 아티클이나 글들이 많아서 자료가 풍부하다고 느꼈습니다.
  • 하지만 기본적으로 Builder, Router, Ineteractor, View, Component 가 무조건 있어야 해서 하나의 뷰를 만드는 데 거의 반강제적으로 4개의 파일이 생겨서 관리하기 매우 어려울 수 있는 단점이 있습니다. 그리고 UIKit 을 기반으로 만들어졌기 때문에 SwiftUI 에선 사용하는 게 큰 이점이 없다고 느꼈습니다.
Untitled (35)

그래서 RIBs 를 선택한 이유

기본적으로 선택지는 4가지가 있었습니다. 그 중 MVC 는 우선적으로 관리측면과 확장성에서 매우 불리하다고 느껴서 제외하였습니다. 그리고 MVVM 은 MVVM-C 과 CleanArchitecture 을 적용한 프로젝트를 진행해보았고, MVVM 의 문제점인 개발자마다 스타이리 달라서 코드의 일관성을 유지하기 어려운 문제점 또 MVVM 에서 TCA 나 RIBs 로 마이그레이션을 시도하는 기업들의 아티클을 보고 MVVM 을 선택지에 제외하였습니다.

그래서 최종적으로 TCA 와 RIBs 라는 두 가지의 선택지가 남아 있었습니다.

TCA 는 SwiftUI 를 사용한 프로젝트를 맡았을 때 MVVM 과 MVC 가 혼합되어있어서 프로젝트가 진행됨에 있어서 관리를 할 수 없게 되어서 찾다보니 SwiftUI 에서는 @State 라는 Binding 기본적으로 제공하는 특성 때문에 MVVM 을 지양하자는 아티클을 보았고 거기서 추천하는 방식으로 TCA 라는 프레임워크를 사용하는 것이였습니다. 그래서 TCA 를 적용해서 앱 구조를 다시 잡은 적이 있습니다. 하지만 뷰의 전환을 관리하는 부분이 존재하지 않아서 SwiftUI 에서 가장 큰 문제인 뷰의 전환을 관리할 수 있는 방법이 iOS16 이후에 NavigationStack 이 나왔지만 그 이전엔 사실상 존재하지 않아서 라이브러리를 이용하던가 자신이 직접 뷰의 전환을 관리하는 객체를 만들어야 하는 단점을 해결할 수 있지 않아서 큰 어려움을 겪었고 저는 개인적으로 이를 관리할 수 있는 객체를 만들어서 해결했습니다.

UIKit을 이용할 때도 MVVM 과 같이 따로 관리할 수 있는 방법을 찾아야 하는 문제점이 있다는 것을 알게 되었고 RIBs 를 이용하면 Router 이 있기 때문에 이를 이용해서 이러한 뷰의 전환을 관리할 수 있는 문제점을 해결할 수 있음을 알게 되어서 RIBs 를 사용하기로 하였습니다.

Tuist 를 이용한 프로젝트 관리

협업시에 .pbxproj 에 충돌이 일어나게 되는 데 이 부분을 Tuist 를 이용하면 줄일 수 있고,

Untitled (36)

외부 라이브러리 사용시 Tuist 를 이용하면 버전 관리하기도 쉽고 모듈간의 의존성을 이미지로 만들어주어서 의존선들을 파악하기 쉬운 장점을 가지고 있어서 사용하였습니다.

Untitled (37)

추상화를 적용한 Provider 생성

MoyaProvider 을 참고하여 Network 통신하는 코드와 Local 에 데이터를 저장하는 코드를 만들었습니다.

NetworkProvider

  • RemoteTargetType
// 정의 
public enum HTTPMethod: String  {
    case get = "GET"
    case post = "POST"
}

public protocol RemoteTargetType {
    var base: String { get }
    var path: String { get }
    var httpMethod: HTTPMethod { get }
    var data: Encodable? { get }
    var headers: [String: String]? { get }
    var paramters: [String: String]? { get }
    func asRequest() -> URLRequest?
}

extension RemoteTargetType {
    public func asRequest() -> URLRequest? {
        guard var urlComponents = URLComponents(string: base + path) else {
            return nil
        }
        if let queries = paramters {
            let queryItems = queries.map { URLQueryItem(name: $0.key, value: $0.value) }
            urlComponents.queryItems = queryItems
        }
        guard let url = urlComponents.url else {
            return nil
        }
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = httpMethod.rawValue
        urlRequest.allHTTPHeaderFields = headers
        if let enocodedData = try? data?.toJson() {
            urlRequest.httpBody = enocodedData
        }
        print("-------")
        print("🚀🚀🚀🚀")
        print("Request")
        print(urlRequest.description)
        print("Query Paramters: \(urlComponents.queryItems ?? [])")
        return urlRequest
    }
}

extension Encodable {
    func toJson() throws -> Data {
        do {
            let encodedData = try JSONEncoder().encode(self)
            return encodedData
        } catch {
            print(error)
            throw error
        }
    }
}

extension Data {
    func toObject<T: Decodable>(_ type: T.Type) throws -> T {
        do {
            let docodedData = try JSONDecoder().decode(type, from: self)
            return docodedData
        } catch {
            print(error)
            throw error
        }
    }
}
  • NetworkProvider
public protocol NetworkProviderProtocol {
    func request<T: RemoteTargetType>(_ type: T) async throws -> Data
}

public struct NetworkProvider<T: RemoteTargetType>: NetworkProviderProtocol {
    
    private enum NetworkProviderError: LocalizedError {
        case urlRequestDoesntExist
        case urlResponseDeosntExist
        case responseFailed
        case responseDataDeosntExist
    }
    
    public init() {}
    
    public func request<T: RemoteTargetType>(_ type: T) async throws -> Data {
        return try await withCheckedThrowingContinuation { continuation in
            guard let urlReqeust = type.asRequest() else {
                continuation.resume(throwing: NetworkProviderError.urlRequestDoesntExist)
                return
            }
            let task = URLSession.shared.dataTask(with: urlReqeust) { data, response, error in
                print("--------")
                print("🪂🪂🪂🪂")
                print("Response")
                print(urlReqeust.description)
                if let error {
                    print("error: \(error)")
                    continuation.resume(throwing: error)
                    return
                }
                guard let responseStatus = response as? HTTPURLResponse else {
                    continuation.resume(throwing: NetworkProviderError.urlResponseDeosntExist)
                    return
                }
                print("reponseStatusCode: \(responseStatus.statusCode)")
                if responseStatus.statusCode != 200 {
                    continuation.resume(throwing: NetworkProviderError.responseFailed)
                    return
                }
                guard let data else {
                    continuation.resume(throwing: NetworkProviderError.responseDataDeosntExist)
                    return
                }
                print("responseData: \(String(data: data, encoding: .utf8) ?? "") - \(data)")
                
                continuation.resume(returning: data)
            }
            task.resume()
        }
    }
}

LocalProvider

  • LocalStorable
public protocol LocalStorable {
    var identifier: String { get }
    var encodeType: Encodable.Type? { get }
    var decodeType: Decodable.Type? { get }
    var enocodeData: Encodable? { get }
}
  • LocalProviderProtocol
public protocol LocalProviderProtocol {
    func create<T: LocalStorable>(_ type: T) throws
    func read<T: LocalStorable>(_ type: T) throws -> Decodable
    func delete<T: LocalStorable>(_ type: T) throws
}

결과 좀 더 깔끔하고 재사용성 높은 코드를 만들 수 있었습니다.

And, NeedleKit

NeedleKit

레이아웃을 쉽게 적용할 수 있는 SnapKit 이라는 라이브러리를 참고하여 NeedleKit 이라는 모듈을 만들어 적용했습니다. 비슷한 인터페이스를 가지도록 설계했습니다.

차이점이 있다면 SnapKit 이라는 라이브러리는 NSLayoutConstraint 를 생성자를 이용해서 만들 었다면 저는 NSLayoutAnchor 을 이용해서 NSLayoutConstraint 를 만든 것이 다른 점입니다.

extension UIView {

    public var ndl: ConstraintDSL {
       return ConstraintDSL(view: self)
    }
    
    @available(*, deprecated, renamed:"ndl.makeConstraints(_:)")
    public func ndl_makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
        self.ndl.makeConstraints(closure)
    }

}

public struct ConstraintDSL {
    
    private let view: UIView

    internal init(view: UIView) {
        view.translatesAutoresizingMaskIntoConstraints = false
        self.view = view
    }
    
    public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
        return ConstraintMaker.makeConstraint(view: self.view, closure: closure)
    }
}

internal enum AnchorAttribute {
    case top
    case bottom
    case left
    case right
    case width
    case heigth
    case centerX
    case CenterY
    case firstBaseLine
    case lastBaseLine
    case leading
    case trailing
    case edges
    case horizontal
    case vertical
}

public class ConstraintMaker {

... 

internal static func makeConstraint(
        view: UIView,
        closure: (_ make: ConstraintMaker) -> Void
    ) {
		let constraints = prepareConstraints(view, closure: closure)
    for constraint in constraints {
					...
		}

}

internal static func prepareConstraints(_ view: UIView, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
        let constraintMaker = ConstraintMaker(view)
        closure(constraintMaker)
        var constraints: [Constraint] = []
        for anchor in constraintMaker.anchors {
            if let to = anchor.to {
                let constraint = Constraint(type: anchor.type, from: constraintMaker.view, to: to, constant: anchor.constant, needSafeAreaLayout: anchor.needSafeAreaLayoutGuide)
                constraints.append(constraint)
            } else if anchor.type == .width || anchor.type == .heigth {
                let constraint = Constraint(type: anchor.type, from: constraintMaker.view, to: empthyView, constant: anchor.constant, needSafeAreaLayout: anchor.needSafeAreaLayoutGuide)
                constraints.append(constraint)
            }
        }
        return constraints
    }
    
    private func anchorAppend(_ constraintAnchor: ConstraintAnchor) -> ConstraintAnchor {
        self.anchors.append(constraintAnchor)
        return constraintAnchor
    }
}

public class ConstraintAnchor {

    internal let type: AnchorAttribute
    internal var needSafeAreaLayoutGuide: Bool = false
    internal var to: UIView?
    internal var constant: CGFloat = 0.0
    
    internal init(type: AnchorAttribute) {
        self.type = type
    }

    @discardableResult
    public func equalTo(_ view: UIView, needSafeAreaLayoutGuide: Bool = false) -> ConstraintRelate {
        self.needSafeAreaLayoutGuide = needSafeAreaLayoutGuide
        self.to = view
        return ConstraintRelate(constraintAnchor: self)
    }
    
    @discardableResult
    public func equalTo(_ constant: CGFloat) -> ConstraintRelate {
        self.constant = constant
        return ConstraintRelate(constraintAnchor: self)
    }
}

public class ConstraintRelate {
    
    private let constraintAnchor: ConstraintAnchor
    
    internal init(constraintAnchor: ConstraintAnchor) {
        self.constraintAnchor = constraintAnchor
    }
    
    @discardableResult
    public func constant(_ constant: CGFloat) -> ConstraintRelate {
        self.constraintAnchor.constant += constant
        return self
    }
}
Untitled (38)

And

인스턴스 정의를 쉽게 할 수 있는 Then 라이브러리를 참고하여 And 라는 모듈을 만들어 보았습니다.

public protocol And {}

extension And where Self: AnyObject {
    @inlinable
    public func and(_ adjust: ((Self) throws -> Void)) rethrows -> Self {
        try adjust(self)
        return self
    }
}

extension NSObject: And {}
Untitled (39)

결과적으로 각각의 프레임워크가 어떻게 구성 되는 지 알게 되었고, 접근 제어자에 대해 이해할 수 있게 되었습니다.

Module 화

  • 네트워크 통신을 담당하는 NetworkProvider 와 Plist, UserDefaults, NsCahce 를 관리할 수 있는 LocalProvider 그리고 레이아웃 설정을 쉽게 해주는 NideelKit, 인스턴스 정의를 쉽게 해주는 And 으로 모듈을 나눴습니다.
  • 모듈화를 한 뒤 각각의 모듈들의 UnitTest 를 진행할 수 있어서 테스트 가능하고, 재사용 가능하도록 만들었습니다.

Untitled (40)

Untitled (41)

환경별 Build 세팅

  • TodoApp-Dog 와 TodoApp-Cat 이라는 두 가지의 Build 환경을 만들었습니다.
Untitled (42) Untitled (45)
  • 두 환경을 구분할 수 있도록 설정을 해주었습니다.
Untitled (43) Untitled (44)
  • 두 빌드 환경에 따라 서로 다른 API 콜을 진행할 수 있도록 만들었고 각각 개와 고양이 사진이 나오도록 하였습니다.

Simulator Screen Recording - iPhone 14 Pro Max - 2023-08-31 at 19 56 44

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages