[TIL]06.06

rbw·2022년 6월 6일
0

TIL

목록 보기
24/98
post-thumbnail

중괄호 뒤 소괄호의 의미

인터넷을 돌아댕기며 코드를 보던 중에, 닫힌 중괄호 뒤 갑자기 소괄호가 오는 경우가 있어서 이건 무슨 코드이지 하고 알아보았다. 클로저 뒤에 소괄호를 작성하면 선언하자마자 호출이 되나 싶었는데 결론부터 말하자면 이 생각이 맞았는데, 맞는지 찾아보다가 더 헷갈려 버린 일이 있어서 적어보려고 한다.

읽기 전용 계산 프로퍼티와 착각

  • 읽기 전용 계산 프로퍼티의 중괄호 뒤에 소괄호를 작성한 것으로 착각을 함
// 예시 코드
var testread: Int {
    let num = 1
    if num == 1 {
        print("hi")
        return num
    }
    else {
        return 0
    }
}() // error : Consecutive statements on a line must be separated by ';'

좀 자세히 보지 않아서 읽기 전용 계산 프로퍼티에 작성을 한 줄 알고 있었는데, 좀 더 알아보니까 표현식에 작성한 것을 알게 되었다.

이 부분에서 삽질을 좀 하게 되었다..,. 하지만, 결론적으로 아래에서 보이는 코드도 읽기 전용(let)으로 만드는 동작 자체는 비슷한듯 하다.

일반 표현식을 클로저로 래핑하여 선언한 코드

클로저의 공식문서에서의 예제로 설명을 하면, 클로저로 래핑을 하면 따로 호출 될 때까지 실행이 안되므로, 뒤에 괄호를 붙여줌으로써 바로 호출을 하는 코드를 작성이 가능하다.

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// 클로저를 호출하지 않았으므로, count는 5
// 이때 타입은 () -> String
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count) // 5

// 만약 표현식 뒤에 소괄호를 붙인다면, 실행이 되는것을 볼 수 있다.
// 이때 타입은 String 
let customerProvider = { customersInLine.remove(at: 0) }()
print(customersInLine.count) // 4

자동클로저에 설명에서 비슷한 예제를 찾게 되었다.

이 부분을 UIKit에서 활용하는 코드는 다음과 같다. 선언과 동시에 호출을 하여 초기화를 해주는 경우에 좋은 방법으로 사용된다.

// UIKit에서의 활용
let loginRegisterButton:UIButton = {
    let button = UIButton(type: .system)
    button.backgroundColor = .white
    button.setTitle("Register", for: .normal)
    button.translatesAutoresizingMaskIntoConstraints = false
    button.setTitleColor(.white, for: .normal)
    return button
}()

좀 더 시각화한다면 동일한 작업을 수행하는, 이름 있는 함수를 작성하면 된다.

func makeWhiteButton() -> UIButton {
    let button = UIButton(type: .system)
    button.backgroundColor = UIColor.White
    button.setTitle("Register", for: .normal)
    button.translatesAutoresizingMaskIntoConstraints = false
    button.setTitleColor(.white, for: .normal)
    return button
}

// 아래와 같이 초기화 한다. 
// 위에 클로저 뒤 소괄호는 아래 코드의 소괄호의 역할과 동일하다.
let loginRegisterButton:UIButton = makeWhiteButton()

좀 더 프로퍼티와 클로저의 동작에 대해 알아 보아서 좋은 기회였던것 같다. 삽질한 것은 마음이 좀 아프지만,,


Router

검색 중에 이런 키워드를 알게되어서 적어보려고 한다.

보통 SwiftUI 에서는 NavigationView, Link 를 해당 뷰에 넣어서 목적지인 다른 뷰로 이동을 하는데, 이는 뷰 끼리 의존성의 문제가 발생 할 수 있다. 이를 Router 를 만들어서 Router View 에서 해결할 수 있다. 또헌 A view에서 B view로만 가던것을, 인자만 바꿔서 다른 View로도 자유롭게 이동을 가능하게 만들어준다.

먼저 주로 사용하던 방식에 대한 문제를 살펴보면 SwiftUI에서의 접근 방식은 더 높은 수준의 예측 가능성이 있지만, View 재사용성 및 테스트 가능성 측면에서의 단점들이 존재한다.

struct ViewA: View {
 
  @State var navigateToViewB: Bool = false
 
  var body: some View {
    NavigationView {
      VStack {
        Button("Go to ViewB") {
          doSomethingAsync() {
            self.navigateToViewB = true
          }
        }
 
        NavigationLink(
          destination: ViewB(
            viewModel: .init(
              userRepo: .init()
            )
          ),
          isActive: $navigateToViewB,
          label: {
            EmptyView()
          }
        )
      }
    }
  }
}

위 방식에 대한 단점으로는

  • 종속성의 문제, View A는 종속성과 함께 ViewB를 구성함
  • ViewA는 ViewB로만 이동해야 하며, 다른 컨텍스트에서 재사용이 불가하다
  • ViewA가 탐색 대상 상태를 관리하고 있음

이제 UIKit과 유사한 방식을 제공하기 위해, SwiftUIRouter (경로 기반 라우팅)을 활용해서 SwiftUI의 선언적 접근 방식을 유지하면서 다음과 같은 패턴도 작성해보겠습니다.

  • ViewA에서 ViewB의 구성 및 종속성을 제거
  • 탐색이 실행될 때, ViewA에 라우팅 개체를 주입함
  • 라우팅 개체가 앱/글로벌 도메인에서 로컬 도메인으로 범위를 좁혀 모듈성을 활성화 하도록 허용
  • 탐색 흐름을 지시하는 비즈니스 논리 계층 활성화

먼저 Routing 프로토콜을 만들어서, ViewA에서 ViewB의 종속성을 제거하고, 이 작업을 다른 객체(Router)에 위임한다.

// 연관타입을 지정해서, 다른 유형에서도 라우터를 생성할 수 있게함
protocol Routing {
  associatedtype Route
  associatedtype View: SwiftUI.View
 
  @ViewBuilder func view(for route: Route) -> Self.View
}

// 이번 예시에서는 두 화면의 열거형으로 진행
enum AppRoute {
  case viewA
  case viewB
}

AppRouter 라는 구체적인 구현에는 환경 개체를 제공해야 한다. 이는 View를 빌드하는데 필요한 종속성도 포함한다. 그리고 Router 프로퍼티가 필요하게끔 View를 업데이트 해야할 필요가 있다.

struct AppRouter: Routing {
  let environment: Environment
 
  func view(for route: AppRoute) -> some View {
    switch route {
    case .viewA:
      ViewA(router: self)
 
    case .viewB:
      ViewB(
        router: self,
        viewModel: .init(
          userRepo: environment.userRepo
        )
      )
    }
  }
}

라우터가 생성되었고, 이를 ViewA에 주입을 하고, ViewB 구성을 위임할 수 있습니다. 라우팅이 연관타입을 포함되어 있다는 점에서, 우리의 뷰를 프로토콜로 제약을 주는 제네릭으로 만들 필요가 있고, 라우터의 타입을 AppRoute 으로 제한해야한다.

struct ViewA<Router: Routing>: View where Router.Route == AppRoute {
  let router: Router
  @State var navigateToViewB: Bool = false
 
  var body: some View {
    NavigationView {
      VStack {
        Button("Go to ViewB") {
          doSomethingAsync() {
            self.navigateToViewB = true
          }
        }
 
        NavigationLink(
          destination: router.view(for: .viewB),
          isActive: $navigateToViewB,
          label: {
            EmptyView()
          }
        )
      }
    }
  }
}

비록 ViewA의 변경사항은 많지는 않지만, 이 변경사항의 장점은 다음과 같다.

  • ViewA는 종속하지않고, ViewB의 구성하는 방법도 알 필요가 없다.
  • ViewA는 ViewB 뿐만아니라, 다른 View들도 나타낼 수 있다.
  • 라우터는 제네릭이므로, ViewA는 다른 라우터 구현으로도 초기화가 가능하다.
  • ViewA는 다른 컨텍스트에서 사용가능 하며, doSomethingAsync 완료시, 다른 뷰를 표시 가능하다.

실제로 앱 개발시에는 이런 라우터를 활용하는 듯 하다. 알아두면 좋은 개념인 듯 하다.


참조

https://stackoverflow.com/questions/39612964/how-does-a-block-with-curly-braces-and-parentheses-work

https://shinesolutions.com/2021/08/20/swiftui-navigation/

profile
hi there 👋

0개의 댓글