SwiftUI: how can I hide scroll indicators? [duplicate] - swiftui

I want to create a SwiftUI List, but not show scroll indicators. ScrollView offers showsIndicators to do this. How can it be done?

Any Indicators (List, scrollView, etc.)
you can get rid of showing indicators for all Lists, but with an API of the UITableView. because SwiftUI List is using UITableView for iOS behind the scene:
struct ContentView: View {
init() {
UITableView.appearance().showsVerticalScrollIndicator = false
}
var body: some View {
List(0...100, id: \.self) { item in
Text("hey")
}
}
}
Note that this will eliminate all TableViews and Lists indicators. You should make it visible again if you need to.
⚠️ Not Yet Important Note
Seems like Apple is removing appearance hacks (but not for this one yet). So you can use LazyVStack inside and ScrollView instead of List and use the available argument for hiding the indicators.
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) { // <- This argument
LazyVStack {
ForEach(1...100, id: \.self) {
Text("\($0)").frame(height: 40)
}
}
}
}
}

It is actually easy to accomplish this without any appearance work arounds in the answer accepted. You just have to use the ScrollView initializer and set the parameter showsIndicators to false.
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}
Inside your ScrollView you could use LazyVStack, if you have a lot of subviews to scroll through. SwiftUI will then render very efficiently your subviews: "lazy" -> only if needed).

Until there is a native SwiftUI way to achieve this, the Introspect library provides a decent solution.
After applying all modifiers to your list just add as a last modifier the following:
List {
...
}
.introspectTableView { tableView in
tableView.showsVerticalScrollIndicator = false // here you can access any other UITableView property
}
I hope there is a native way to do that at some point.

Hide scrolling indicator now became very simple
List {}.scrollIndicators(ScrollIndicatorVisibility.hidden)

List basically creates a tableview (UpdateCoalescingTableView) behind the scenes, and tableview's are scrollable. Unfortunately, however, you can't get to the scrollview attributes in SwiftUI.
You "might" be able to create a UIViewRepresentable that could walk up the view hierarchy until it finds a scrollview, but I wouldn't recommend it.
You could also create your own scrollview, put a vstack inside it, and "fake" a list view, which would probably be the safer approach.

The choosen answer won't work in iOS 16. They released a new viewModifier called .scrollIndicators(.hidden). I created a viewModifier wrapper which you can call like this on your List: .modifier(HideListIndicatorsViewModifier())
struct HideListIndicatorsViewModifier: ViewModifier {
#ViewBuilder
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.scrollIndicators(.hidden)
} else {
content
}
}
}

swift scrollview hide scrollbar
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
hide scroll view indicators bar swiftui
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}

Related

Swiftui: How to Close one Tab in a TabView?

I'm learning SwiftUI and having trouble with closing Each Tab View Element.
My App shows photos from user's album by TabView with pageViewStyle one by one.
And What I want to make is user can click save button in each view, and when button is clicked, save that photo and close only that view while other photos are still displayed. So unless all photos are saved or discarded, if user clicks save button, TabView should automatically move to another one.
However, I don't know how to close only one Tab Element. I've tried to use dismiss() and dynamically changing vm.images element. Latter one actually works, but it displays awkward movement and it also requires quite messy code. How could I solve this issue?
Here is my code.
TabView {
ForEach(vm.images, id: \.self) { image in
TestView(image: image)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
struct TestView: View {
#ObservedObject var vm: TestviewModel
...
var body: some View {
VStack(spacing: 10) {
Image(...)
Spacer()
Button {
...
} label: {
Text("Save")
}
}
You need actually to remove saved image from the viewModel container, and UI will be updated automatically
literally
Button {
vm.images.removeAll { $0.id == image.id } // << here !!
} label: {
Text("Save")
}
You need to use the selection initializer of TabView in order to control what it displays. So replace TabView with:
TabView(selection: $selection)
Than add a new property: #State var selection: YourIdType = someDefaultValue, and in the Button action you set selection to whatever you want to display.
Also add .tag(TheIdTheViewWillUse) remember that whatever Id you use must be the same as your selection variable. I recommend you use Int for the simple use.

Is there a way to hide scroll indicators in a SwiftUI List?

I want to create a SwiftUI List, but not show scroll indicators. ScrollView offers showsIndicators to do this. How can it be done?
Any Indicators (List, scrollView, etc.)
you can get rid of showing indicators for all Lists, but with an API of the UITableView. because SwiftUI List is using UITableView for iOS behind the scene:
struct ContentView: View {
init() {
UITableView.appearance().showsVerticalScrollIndicator = false
}
var body: some View {
List(0...100, id: \.self) { item in
Text("hey")
}
}
}
Note that this will eliminate all TableViews and Lists indicators. You should make it visible again if you need to.
⚠️ Not Yet Important Note
Seems like Apple is removing appearance hacks (but not for this one yet). So you can use LazyVStack inside and ScrollView instead of List and use the available argument for hiding the indicators.
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) { // <- This argument
LazyVStack {
ForEach(1...100, id: \.self) {
Text("\($0)").frame(height: 40)
}
}
}
}
}
It is actually easy to accomplish this without any appearance work arounds in the answer accepted. You just have to use the ScrollView initializer and set the parameter showsIndicators to false.
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}
Inside your ScrollView you could use LazyVStack, if you have a lot of subviews to scroll through. SwiftUI will then render very efficiently your subviews: "lazy" -> only if needed).
Until there is a native SwiftUI way to achieve this, the Introspect library provides a decent solution.
After applying all modifiers to your list just add as a last modifier the following:
List {
...
}
.introspectTableView { tableView in
tableView.showsVerticalScrollIndicator = false // here you can access any other UITableView property
}
I hope there is a native way to do that at some point.
Hide scrolling indicator now became very simple
List {}.scrollIndicators(ScrollIndicatorVisibility.hidden)
List basically creates a tableview (UpdateCoalescingTableView) behind the scenes, and tableview's are scrollable. Unfortunately, however, you can't get to the scrollview attributes in SwiftUI.
You "might" be able to create a UIViewRepresentable that could walk up the view hierarchy until it finds a scrollview, but I wouldn't recommend it.
You could also create your own scrollview, put a vstack inside it, and "fake" a list view, which would probably be the safer approach.
The choosen answer won't work in iOS 16. They released a new viewModifier called .scrollIndicators(.hidden). I created a viewModifier wrapper which you can call like this on your List: .modifier(HideListIndicatorsViewModifier())
struct HideListIndicatorsViewModifier: ViewModifier {
#ViewBuilder
func body(content: Content) -> some View {
if #available(iOS 16.0, *) {
content
.scrollIndicators(.hidden)
} else {
content
}
}
}
swift scrollview hide scrollbar
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
hide scroll view indicators bar swiftui
ScrollView(.vertical, showsIndicators: false) {
// ... your content for scrollView
}

Is it possible to make a modal non-dismissible in SwiftUI?

I am creating an App where the login / register part is inside a modal, which is shown if the user is not logged in.
The problem is, that the user can dismiss the modal by swiping it down...
Is it possible to prevent this?
var body: some View {
TabView(selection: $selection) {
App()
}.sheet(isPresented: self.$showSheet) { // This needs to be non-dismissible
LoginRegister()
}
}
Second example:
I am using a modal to ask for information. The user should not be able to quit this process except by dismissing the modal with save button. The user has to input information before the button works. Unfortunately the modal can be dismissed by swiping it down.
Is it possible to prevent this?
iOS 15 and later:
Use .interactiveDismissDisabled(true) on the sheet, that's all.
Prev iOS 15:
You can try to do this by using a highPriorityGesture. Of course the blue Rectangle is only for demonstration but you would have to use a view which is covering the whole screen.
struct ModalViewNoClose : View {
#Environment(\.presentationMode) var presentationMode
let gesture = DragGesture()
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: 300, height: 600)
.highPriorityGesture(gesture)
.overlay(
VStack{
Button("Close") {
self.presentationMode.value.dismiss()
}.accentColor(.white)
Text("Modal")
.highPriorityGesture(gesture)
TextField("as", text: .constant("sdf"))
.highPriorityGesture(gesture)
} .highPriorityGesture(gesture)
)
.border(Color.green)
}
}
This is a common problem and a "code smell"... well not really code but a "design pattern smell" anyway.
The problem is that you are making your login process part of the rest of the app.
Instead of presenting the LoginRegister over the App you should really be showing either App or LoginRegister.
i.e. you should have some state object like userLoggedIn: Bool or something and depending on that value you should show either App or LoginRegister.
Just don't have both in the view hierarchy at the same time. That way your user won't be able to dismiss the view.
If you dont mind using Introspect:
import Introspect
#available(iOS 13, *)
extension View {
/// A Boolean value indicating whether the view controller enforces a modal behavior.
///
/// The default value of this property is `false`. When you set it to `true`, UIKit ignores events
/// outside the view controller's bounds and prevents the interactive dismissal of the
/// view controller while it is onscreen.
public func isModalInPresentation(_ value: Bool) -> some View {
introspectViewController {
$0.isModalInPresentation = value
}
}
}
Usage:
.sheet {
VStack {
...
}.isModalInPresentation(true)
}
iOS 15+
Starting from iOS 15 you can use interactiveDismissDisabled.
You just need to attach it to the sheet:
var body: some View {
TabView(selection: $selection) {
App()
}.sheet(isPresented: self.$showSheet) {
LoginRegister()
.interactiveDismissDisabled(true)
}
}
Regarding your second example, you can pass a variable to control when the sheet is disabled:
.interactiveDismissDisabled(!isAllInformationProvided)
You can find more information in the documentation.
theoretically this may help you (I didn't tryed it)
private var isDisplayedBind: Binding<Bool>{ Binding(get: { true }, set: { _ = $0 }) }
and usage:
content
.sheet(isPresented: isDisplayedBind) { some sheet }

How Can I Wrap Text in a SwiftUI NavigationButton?

SwiftUI's Text type has a modifier that allows text to wrap if the text is too long to fit horizontally within its container. To achieve text wrapping, simply pass nil as the argument to the lineLimit modifier:
Text("This text is too long to fit horizontally within its non-NavigationButton container. Therefore, it should wrap to fit, and it does.")
.font(.body)
.lineLimit(nil)
The above works as expected, except for when used inside of a SwiftUI NavigationButton. All Text instances that I have nested within NavigationButton instances do not wrap:
NavigationButton(destination: DestinationView()) {
Text("This text is too long to fit horizontally within its NavigationButton container. Therefore, it should wrap to fit, but it does not.")
.font(.body)
.lineLimit(nil)
}
Is there anything that I am missing from the code above that would allow Text instances to wrap within NavigationButton instances?
Edit to add more context:
The initial view is a List that is wrapped in a NavigationView. The List contains instances of MeasurableItemsListItem, which are wrapped in NavigationButton instances that trigger navigation to a secondary view that is added to the navigation stack:
struct MeasurableItemsList : View {
private let measurableItems = MeasurableItem.allCases
var body: some View {
NavigationView {
List(measurableItems.identified(by: \.self)) { measurableItem in
NavigationButton(destination: DosableFormsList(measurableItem: measurableItem)) {
MeasurableItemsListItem(measurableItem: measurableItem)
}
}
}
}
}
Each list item that is wrapped in a NavigationButton is made from the following structure:
struct MeasurableItemsListItem : View {
let measurableItem: MeasurableItem
var body: some View {
Text(measurableItem.name)
.font(.body)
.foregroundColor(.primary)
.lineLimit(nil)
}
}
You might be helped with this answer: https://stackoverflow.com/a/56604599/30602
The summary is that inside other Builders you need to add .fixedSize(horizontal: false, vertical: true) to your Text() to get it to wrap.
You don't need to use NavigationButton to achieve navigation. You can achieve it by using NavigationLink easily, wherein you don't need to wrap your views inside NavigationButton.
Check this ANSWER which is explaining the usage of NavigationLink without wrapping view inside it. I hope it helps.
P.S.- i don't have enough reputation points to add this as a comment. Thanks

Update UIViewRepresentable size from UIKit in SwiftUI

I'm embedding a view controller with variable-height UITextView inside a parent SwiftUI VStack and the view controller sizes it's frame to the whole screen between viewDidLoad and viewDidLayoutSubviews. The UITextView expands only to the size of the text inside itself and centers itself inside the parent view.
I'm trying to add this view controller in a VStack and have it behave externally like other SwiftUI components do - sized exactly to the content it contains - but it wants to be sized to the whole screen minus the other VStack elements.
I can get the correct size of the UITextView in didLayoutSubviews and pass it upwards to SwiftUI where it can be set properly - but where do I do that?
In the example screenshot below, the orange is the embedded UIView background, the green is the UITextView and the VStack looks like this:
VStack {
HighligherVC()
Text("Tap and drag to highlight")
.foregroundColor(.gray)
.font(.caption)
}
Without being able to see more of your code, it's slightly difficult to say what the best solution would be, but based purely on this part of your question...
I can get the correct size of the UITextView in didLayoutSubviews and pass it upwards to SwiftUI where it can be set properly - but where do I do that?
I would suggest that you pass a binding property to your view controller that can be set to the calculated text view height, meaning that the view that contains your VStack would have a #State property like this:
#State private var textViewHeight: CGFloat = 0
You would then declare a #Binding property on your HighlighterVC and add an initializer like this:
#Binding var textViewHeight: CGFloat
init(textViewHeight: Binding<CGFloat>) {
self._textViewHeight = textViewHeight
}
And then you would set textViewHeight to the calculated height in your didLayoutSubviews and add a .frame modifier to your HighlighterVC like this:
VStack {
HighlighterVC(textViewHeight: self.$textViewHeight)
.frame(height: self.textViewHeight)
Text("Tap and drag to highlight")
.foregroundColor(.gray)
.font(.caption)
}
Like I said at the beginning of my answer, this solution (that I believe would work, but since I can't test it, I'm not 100% certain) is based on your thoughts about what it is that you need. Without seeing more code, it's impossible for me to say if this is the best solution.
Add fixedSize may solve this.
.fixedSize(horizontal: false, vertical: true)