For more content, please follow the public account “Swift Garden”.
Like articles? How about a 🔺💛➕ triple? Follow the column, follow me 🚀🚀🚀
For those of you who have studied and used SwiftUI for a while, you might want to disable the scrolling of a list. How do you do that in SwiftUI? And those of you who are familiar with UIKit know that this is a very simple thing to do in UIScrollView.
Aside from SwiftUI’s incomplete tools, SwiftUI does excite developers with the ease of building UIs. There is some comfort in the fact that many SwiftUI components are actually built on UIKit. In addition to that, SwiftUI and UIKit interoperability allows us to make full use of UIViewRepresentable and UIViewControllerRepresentable — both in order to make you can be transplanted to SwiftUI UIKit component And exist.
But this is something we all already know, so what is the purpose of this article?
In the next few sections, I’ll take you to explore an amazing SwiftUI library called Introspect (github.com/siteline/Sw…) . With it, we can access the UNDERLYING UIKit view of the SwiftUI component.
We will cover the following topics:
- How does the Introspect library base work?
- How to disable a SwiftUI list?
- How to customize the Segmented Picker in SwiftUI?
- How do I change the color of NavigationView and TabView?
- How to make a SwiftUI TextField become a First Responder?
The principle behind it
The Introspect library works by adding a custom overlay to the view hierarchy to view the UIKit hierarchy and find the relevant views.
If my description does not make sense to you, let’s use the following steps to further illustrate the principles behind the IntroSpec library.
- Step 1: Add a UIViewRepresentable Overlay to a SwiftUI List
- Step 2: Find its ViewHost (SwiftUI will wrap each UIView in a ViewHost, and then put it in a HostingView)
- Step 3: Find its sibling view in the view hierarchy (with that, we get the UITableView from the Swiftui. List, and we can customize the SwiftUI List using the UITableView properties).
Basically, we overlay an invisible UIViewRepresentable onto the Top of the SwiftUI view, and then use that view to mine the chain of views inwards until we find the UIHostingView that hosts the SwiftUI view. Once we get this view, we can access the UIKit view from there.
However, not all SwiftUI views can be viewed. SwiftUI’s Text, for example, is not built on UILabel. Similarly, images and buttons are not built on UIImageView and UIButton. As a result, we don’t have access to their underlying UIKit views — so they don’t exist. The table below shows the SwiftUI views that can be viewed.
Next, let’s look at some of the missing features in SwiftUI that can be built by looking at the underlying UIKit view.
Disable SwiftUI list scrolling
SwiftUI’s List does not currently have an isScrollEnabled property that allows us to customize the scrolling behavior. UITableView does. With VStack + ForEach, we can also achieve scrolation-free functionality, but this has a downside: The clickable effect of SwiftUI lists or UITableView rows is missing.
Instead, with the help of the introspectTableView view modifier, we can easily disable scrolling while retaining the native list features, as follows:
Similarly, to hide the separator line between elements in a SwiftUI list, simply call tableView.separatorColor =.None.
Customize the Segmented control in SwiftUI
SwiftUI allows you to set a SegmentedPickerStyle for the Picker, but there are a number of limitations: you can’t customize borders, radius, title, or background.
Again, we had to customize the appearance of the Segmented control in SwiftUI with the help of the underlying view. In the following example, we will remove the rounded corners in the Segmented control and set a border color:
import SwiftUI
import Introspect
struct ContentView: View {@State private var selectedIndex = 0
@State private var numbers = ["One"."Two"."Three"]
var body: some View {
VStack {
Picker("Numbers", selection: $selectedIndex) {
ForEach(0..<numbers.count) { index in
Text(self.numbers[index]).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
.introspectSegmentedControl {
segmentedControl in
segmentedControl.layer.cornerRadius = 0
segmentedControl.layer.borderColor = UIColor.label.cgColor
segmentedControl.layer.borderWidth = 1.0
segmentedControl.selectedSegmentTintColor = .red
segmentedControl.setTitleTextAttributes([.foregroundColor:UIColor.white], for: .selected)
segmentedControl.setTitleTextAttributes([.foregroundColor:UIColor.red], for: .normal)
}
Text("Selected value:\(numbers[selectedIndex])").padding()
}
}
}Copy the code
The preview looks like this:
                 Â
Customize NavigationView and TabView styles
Changing the color of the title text in the NavigationBar is not intuitive, nor is it for the TabView. One might suggest changing the appearance in the init method — like this — but this is not a good solution:
init() {
UINavigationBar.appearance().titleTextAttributes =
[.foregroundColor:UIColor.red]
UINavigationBar.appearance().backgroundColor = .green
UITabBar.appearance().backgroundColor = UIColor.blue
}Copy the code
This implementation does not actually customize NavigationView or TabView. Instead, it is globally overridden by their appearance.
We have a better solution for this requirement. For example, the following code snippet modizes the NavigationBar’s title and background colors in a more concise way.
import SwiftUI
import Introspect
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
Text("Do not use.appearance()")
}
.navigationBarTitle("Title", displayMode: .inline)
.introspectNavigationController{
navController in
navController.navigationBar.barTintColor = .blue
navController.navigationBar.titleTextAttributes = [
.foregroundColor: UIColor.white,
.font : UIFont(name:"Helvetica Neue", size: 20)! ] }}}}Copy the code
By viewing the TabView and NavigationView, we can modify their corresponding UIKit views:
struct ContentView : View {@State private var selection = 1
var body: some View {
NavigationView {
VStack {
Text("Do not use.appearance()")
TabView(selection: $selection) {
Text("First screen")
.tabItem {
Image(systemName: "1.square.fill")
Text("First screen")
}.tag(1)
Text("Second screen")
.tabItem {
Image(systemName: "2.square.fill")
Text("Second screen")
}.tag(2)
}
.accentColor(.white)
.introspectTabBarController { tabController in
tabController.tabBar.barTintColor = .blue
tabController.tabBar.isTranslucent = false
}
}
.navigationBarTitle("Title", displayMode: .inline)
.introspectNavigationController{
navController in
navController.navigationBar.barTintColor = .blue
navController.navigationBar.titleTextAttributes = [
.foregroundColor: UIColor.white,
.font : UIFont(name:"Helvetica Neue", size: 20)! ] }}}}Copy the code
The preview looks like this:
 Â
Let TextField be the First Responder
SwiftUI currently does not provide a way to automatically eject the keyboard. Unless we do something, the user will have to manually retrieve the focus of the TextField. Again, we can optimize the experience by accessing the underlying UITextField and calling the becomeFirstResponder function, as follows:
import SwiftUI
import Introspect
struct ContentView : View {@State var text = ""
var body: some View {
VStack {
TextField("Enter some text", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.introspectTextField{
textField in
textField.becomeFirstResponder()
}
}
}
}Copy the code
conclusion
As we can see, looking at the UNDERLYING UIKit view of SwiftUI allows us to break out of some of the limitations of SwiftUI components. For example, we introduced lists, the segmented style Picker, NavigationView, TabView, and TextField in this article.
Further, you can customize Stepper, Slider and DatePicker in the same way.
Of course, I’m sure Apple will give SwiftUI more power and a more flexible API in future releases. Until then, you can use this idea to free up the customization capabilities of the original UIKit API.
My official account here is Swift and computer programming related articles, as well as excellent foreign article translation, welcome to follow ~