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