인터넷을 돌아댕기며 코드를 보던 중에, 닫힌 중괄호 뒤 갑자기 소괄호가 오는 경우가 있어서 이건 무슨 코드이지 하고 알아보았다. 클로저 뒤에 소괄호를 작성하면 선언하자마자 호출이 되나 싶었는데 결론부터 말하자면 이 생각이 맞았는데, 맞는지 찾아보다가 더 헷갈려 버린 일이 있어서 적어보려고 한다.
// 예시 코드
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()
좀 더 프로퍼티와 클로저의 동작에 대해 알아 보아서 좋은 기회였던것 같다. 삽질한 것은 마음이 좀 아프지만,,
검색 중에 이런 키워드를 알게되어서 적어보려고 한다.
보통 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()
}
)
}
}
}
}
위 방식에 대한 단점으로는
이제 UIKit과 유사한 방식을 제공하기 위해, SwiftUIRouter (경로 기반 라우팅)을 활용해서 SwiftUI의 선언적 접근 방식을 유지하면서 다음과 같은 패턴도 작성해보겠습니다.
먼저 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의 변경사항은 많지는 않지만, 이 변경사항의 장점은 다음과 같다.
doSomethingAsync 완료시, 다른 뷰를 표시 가능하다.실제로 앱 개발시에는 이런 라우터를 활용하는 듯 하다. 알아두면 좋은 개념인 듯 하다.
참조
https://stackoverflow.com/questions/39612964/how-does-a-block-with-curly-braces-and-parentheses-work