SwiftUI-Introspect
안녕하세요. 그린입니다🍏
이번 포스팅은 오랜만에 외부 라이브러리에 대해 학습해보려고 가져왔어요.
많은 분들이 아는 라이브러리는 아니라고 생각들며 개인적으로 SwiftUI로 뷰 드로잉을 하면서 간혹 UIKit의 API들이 필요하고 사용하는것이 더 편할때 별도 UIRepresntable로나 별도 커스텀 뷰로 직접 SwiftUI에서 기능을 힘들게 구현하거나 구성하지 않아도 조금 편리하게 사용할 수 있도록 도와주는 SwiftUI-Introspect 입니다!
이건 사실 딥하게 보지 않아도 될 정도로 취향을 타는 라이브러리이고 저 또한 이제는 거의 사용을 하지 않고 왠만하면 커스텀하게 뷰를 만들어 사용하고 있어요.
그럼에도 UIKit만 접하다가 SwiftUI를 처음 접하는 분들께서 UIKit을 조금 편리하게 SwiftUI에서도 사용하고 싶으실때 빨리 뷰를 쳐내고 싶을때 쓰면 나쁘진 않을것 같고 그냥 소개 해보고 싶어서 가져왔습니다!
서두가 길었네요ㅋㅋ 바로 그럼 파해쳐보죠🙌
SwiftUI-Introspect?
Introspect 단어의 의미를 저는 몰랐어요.
그래서 파파고에서 한번 찾아봤는데..
잉? 이 라이브러리가 뭘 하는건지 의미만 가지고는 더 모르겠네요?ㅋㅋ
유추해보자면 SwiftUI에서 UIKit을 내부로 끌어들인다 이런 느낌일것 같습니다.
뭐 중요한건 아니니 넘어가고 이 라이브러리의 소개는 이렇습니다.
"SwiftUI 뷰에서 UIKit 및 AppKit의 기본 요소들을 가져올 수 있습니다."
이 말이 모든걸 대변해준다고 생각합니다.
그냥 쉽게 SwiftUI에서 UIKit / AppKit의 API들의 기능을 쓸 수 있다 끝!!
예를들어 SwiftUI에서 Grid등을 통한게 아니라 UITableView, UICollectionView등의 기능을 그대로 쓸 수 있고 엑세스 할 수 있다는 소리입니다.
그럼 이것이 어떤 방식으로 작동하는지 조금 더 알아볼께요.
작동 방식
리드미에 나와있는 공식 소개 작동 방식에 대한 그림입니다.
보시면 View 계층에 커스텀한 Introspection 뷰를 추가하고 UIKit 계층에서 관련한 뷰를 찾는 방식입니다.
TextField로 스텝별로 예를 들어보겠습니다.
1. TextField에 IntrospectionVIew에 오버레이로 추가
2. Introspection 뷰의 view host를 가져옴 (UITextField의 view host 옆에 있음)
3. UITextField가 포함된 이전 동일 계층 뷰 가져옴
작동 방식이 아직 이해가 되지 않아도 괜찮습니다.
밑에서 예시를 통해 차근 차근 볼거에요!
공식 문서에서는 이 검사 방식 자체가 추후 SwiftUI 릴리즈에서는 중단될 수도 있다고 주의를 주고 있어요.
향후 구현에서는 동일 계층을 사용하지 않거나 찾고 있는 UIKit 요소를 사용하지 않을 수 있다고 해요.
중단될 가능성의 이유는 라이브러리와 SwiftUI의 충돌로 보고 있습니다.
충돌하게 되면 introspect() 메서드가 호출되지 않을거라고 예상합니다.
(그럼 이거 왜 안고치고 있는거야... 알아보는게 맞나라는 의심이 슬쩍 들기 시작하네요🥲)
실제 프로덕션에서의 사용
실제로 프로덕션 즉 우리의 프로젝트에서 사용할때 Introspect를 사용할 수 있습니다.
다만 애플에서 제공하는 기본적인 공개 API외에 비공개 API는 사용할 수 없어요.
당연한 소리겠지만 공개적인 뷰 계층 구조만 검사하기는 방식이기에 그렇습니다.
만약 찾을 수 없다면 introspect() 메서드는 무시됩니다.
자 그럼 길고 길었던 진짜 서론이 끝났습니다!
이제 설치해보고 직접 간단히 사용해보시죠🕺🏻
설치
SPM과 코코아팟만을 지원하며 카르타고 지원은 하지 않습니다.
// SPM
https://github.com/siteline/SwiftUI-Introspect.git
// CocoaPod
pod 'Introspect'
기능
자 다시 한번 정리해볼께요.
우리가 필요한건 SwiftUI로 만들어진 뷰 요소에서 UIKit / AppKit의 기능을 사용하고 싶은겁니다!
이 부분 유의하시고 아래 사용할 수 있는 뷰 요소와 없는 뷰 요소를 보겠습니다.
사용할 수 있는 요소
SwiftUI의 아래 기본 View 요소에서는 introspect 라이브러리의 각 메서드를 통해 해당 매칭하는 UIKit/AppKit의 기본 뷰 API 기능을 사용할 수 있습니다.
SwiftUI | UIKit | AppKit | Introspect |
NavigationView (StackStyle) |
UINavigationController | N/A | .introspectNavigationController() |
NavigationVIew (DoubleColumnStyle) |
UISplitViewController | N/A | .introspectSplitViewController() |
Any embedded View | UIViewController | N/A | .introspectViewController() |
ScrollView | UIScrollViewController | NSScrollView | .introspectScrollView() |
List | UITableView | NSTableView | .introspectTableView() |
View in List | UITableVIewCell | NSTableCellView | .introspectTableViewCell() |
TabView | UITabBarCOntroller | NSTabView | .introspectTabBarController() // iOS .introspectTabView() // macOS |
TextField | UITextField | NSTextFiled | .introspectTextField() |
Toggle | UISwitch | NSButton | .introspectSwitch() // iOS .introspectButton() // macOS |
Slider | UISlider | NSSlider | .introspectSlider() |
Stepper | UIStepper | NSStepper | .introspectStepper() |
DatePicker | UIDatePicker | NSDatePicker | .introspectDatePicker() |
Picker (SegmentedPickerStyle) |
UISegmentedControl | NSSegmentedControl | .introspectSegmentedControl() |
Button | N/A | NSButton | .introspectButton() |
ColorPicker | UIColorWell | NSColorWell | .introspectColorWell() |
TextEditor | UITextView | NSTextVIew | .introspectTextView() |
자 요렇게 해당하는 뷰 요소들은 맨 우측 메서드를 통해 기존 UIKit / AppKit의 기능 사용하면 됩니다!
아주 간단하죠?ㅎㅎ
사용할 수 없는 요소
아래는 introspect를 사용할 수 없는 SwiftUI의 뷰 요소들입니다.
SwiftUI | Frameworks | Why not? |
Text | UIKit, AppKit | UILabel, NSLabel에 해당하지 않음 |
Image | UIKit, AppKit | UIImageView, NSImageView에 해당하지 않음 |
Button | UIKit | UIButton에 해당하지 않음 |
이유는 매칭되는 요소에 해당하는게 아니라서 그렇습니다.
이 세개에서는 기능을 사용하지 못한다는점 유의하면 됩니다!
자 이제 본격적으로 어떤 메서드를 사용하고 매칭되는지 봤으니 실제로 코드를 통해 사용해보시죠!
사용하기
여기서는 모든 매칭되는 요소들의 사용 예시 중점보다는 공식 문서 토대로 어떤 식으로 사용하는것인지 알아볼께요.
사실 하나의 예시만 봐도 전부 동일하게 사용되는 방식이라 어려울건 없습니다.
List에서 사용하기
import SwiftUI
import Introspect
struct ListView: View {
var body: some View {
List {
Text("GREEN")
Text("BLACK")
}
.introspectTableView { tableView in
tableView.separatorStyle = .none
}
.introspectTableViewCell { cell in
let backgroundView = UIView()
backgroundView.backgroundColor = .green
cell.selectedBackgroundView = backgroundView
}
}
}
SwiftUI에서 List를 통해 테이블뷰를 그릴때 아래와 같이 UIKit의 테이블뷰에 해당하는 뷰 요소 기능들을 사용할 수 있습니다.
ScrollView에서 사용하기
import SwiftUI
import Introspect
struct ScrollView: View {
var body: some View {
ScrollView {
Text("GREEN")
Text("RED")
Text("BLUE")
Text("YELLOW")
}
.introspectScrollView { scrollView in
scrollView.refreshControl = UIRefreshControl()
}
}
}
UIKit의 UIScrollView의 모든 요소 기능들을 사용할 수 있습니다.
NavigationView에서 사용하기
import SwiftUI
import Introspect
struct ScrollView: View {
var body: some View {
NavigationView {
Text("GREEN")
.introspectNavigationController { navigationController in
navigationController.navigationBar.backgroundColor = .green
}
}
}
}
TextField에서 사용하기
import SwiftUI
import Introspect
struct ScrollView: View {
var body: some View {
TextField(
"GREEN Holder",
text: $textFieldValue
)
.introspectTextField { textField in
textField.layer.backgroundColor = UIColor.green.cgColor
}
}
}
자 아주아주 쉽죠?
추가로 하나 제 경험담을 소개해볼까 합니다.
저는 이걸 동아리 프로젝트를 하며 SwiftUI에서 TextField의 포커싱을 잡아주는 부분에서 becomeFirstResponder() 메서드 호출을 UIKit에서는 쉽게 할 수 있는데 SwiftUI와 TCA를 통해서 풀어내려니 조금 복잡해지는 부분이 있었어요.
그때 이걸 접했었어요.
그리고 이렇게 사용했습니다.
import SwiftUI
import Introspect
struct TitleTextView: View {
...
var body: some View {
...
TextField(
text: titleTextViewStore.binding(
get: { $0 },
send: { Action.titletextFieldChanged($0) }
),
label: {}
)
.introspectTextField { textField in
textField.becomeFirstResponder()
}
}
}
다른 구현부는 잔챙이고 핵심은 becomeFirstResponder하는 부분이에요.
저렇게 view modifier를 통해 쉽게 붙여버릴 수 있더라구요.
resignResponder 등도 마찬가지로 쉽게 사용될것 같구요.
커스텀하게 새로 만들어 기여하기
만약 여러분들이 사용하다가 SwiftUI와 UIKit/AppKit의 뷰 요소들끼리 매칭되지 않은 부분이 있어서 커스텀하게 만든다면 아래 방법을 사용하라고 권장하고 있습니다.
extension View {
public func introspectTextField(customize: @escaping (UITextField) -> ()) -> some View {
return inject(UIKitIntrospectionView(
selector: { introspectionView in
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
return nil
}
return Introspect.previousSibling(containing: UITextField.self, from: viewHost)
},
customize: customize
))
}
}
실제로 만들고 PR을 통해 기여할 수도 있겠네요!
앞으로 SwiftUI가 발전함에 따라 더 많은 기능이 생기고 기여할 여지가 큰 부분일것 같습니다.
계층 구조 검사하기
위에서 직접 커스텀하게 새로 만든다면 계층 구조를 검사할 수 있는 메서드를 사용해야해요.
위에서는 findViewHost 메서드와 previousSibling 메서드를 이용했는데 그 외에도 몇가지가 더 있어 어떤것이 있는지만 소개하겠습니다.
Introspect.findChild(ofType:in:)
Introspect.findChildUsingFrame(ofType:in:from:)
Introspect.previousSibling(containing:from:)
Introspect.nextSibling(containing:from:)
Introspect.findAncestor(ofType:from:)
Introspect.findHostingView(from:)
Introspect.findViewHost(from:)
총 7가지가 있습니다.
저도 아직 커스텀하게 새로 만든게 없어 사용해보진 못했는데 여러분들이 사용해볼 수 있는 기회가 되면 좋겠어요🙌
마무리
잡지식 +1이 되어간 느낌이 듭니다.
한편으로 SwiftUI가 조금 더 안정적이고 발전되었다면 없어질것들이지 않을까 미래에는 사라져있을수도.. 생각이 들었습니다.