List row separator not hiding when using another view on top - swift

I am using a package that hides the list view separator for iOS 14.
https://github.com/SchmidtyApps/SwiftUIListSeparator
Whenever I add another view on top of the List, like a simple Divider(), the list row separator will appear again for no reason.
var body: some View {
VStack{
Divider() //if I remove this divider, everything works fine
List{
ForEach(1...3, id: \.self){_ in
Text("item")
} .listSeparatorStyle(.none) ///the method to hide the row separator
}
}
}
You can try on your preview by yourself.
I have tried multiple way to hide the separator but just this one worked for me, so please do not duplicate it.
Why whenever I add another view, the separator line appear again?
Also, how I can add the if #available(iOS 15, *)?
var body: some View {
VStack{
Divider()
List{
ForEach(1...3, id: \.self){_ in
Text("fdasf")
}
if #available(iOS 15, *) {
.listRowSeparator(.hidden)
}
else{
.listSeparatorStyle(.none)
}
}
}
}

Per #Yodagama's comment, use .listRowSeparator(.hidden)
To use different modifiers for OS versions, pull your List out into a function (or its own View), and then apply the relevant modifier…
struct ContentView: View {
var body: some View {
if #available(iOS 15, *) {
list()
.listRowSeparator(.hidden)
} else{
list()
.listRowSeparator(.hidden)
}
}
func list() -> some View {
List {
ForEach(1...3, id: \.self){_ in
Text("fdasf")
}
}
}
}

Related

SwiftUI PageView update outside view

I have a PageStyle TabView, and want to update a Text that is outside of it.
I've used onAppear to get the page change event, and looks good at first if I scroll forward, but once I go backwards a few onAppear are missed.
Is this the correct way of doing this. Is is possible to do that?
struct PageView: View {
#State var title: String = "hello"
var body: some View {
VStack {
Text(self.title)
TabView {
ForEach(0..<100) { i in
Text("\(i)").onAppear {
self.title = ("TITLE = \(i)")
}
}
}
.tabViewStyle(PageTabViewStyle())
}
}
}
You can do this by using TabView(selection: and tag modifier on Text items, like below (tested with Xcode 12.1 / iOS 14.1)
var body: some View {
VStack {
Text(self.title)
TabView(selection: $title){ // << here !!
ForEach(0..<100) { i in
Text("\(i)")
.tag(String(i)) // << here !!
}
}
.tabViewStyle(PageTabViewStyle())
}
}

How can I pass the navigation destinations as parameters in SwiftUI?

I am creating a SwiftUI app with a wizard-like navigation. I have already build a WizardView, in which I then embed the single pages. The WizardView should take care of general stuff like displaying everything in a nice looking frame, display navigation buttons, and so on. Basically everything seems to look right, so I started to have a look at how navigation works in SwiftUI.
This is a condensed version of what I ended up with. The problem is, I can't find a possible solution for passing the nextView from the concrete page to the WizardView struct.
import SwiftUI
struct WizardView<Content: View>: View {
private let content: () -> Content
private var canNavigateBack: Bool
private var canNavigateForth: Bool
// TODO
// How do I pass this as parameter, which has to be optional or some default view,
// as the last screen won't have a next one.
private var nextView = EmptyView()
init(canNavigateBack: Bool? = true,
canNavigateForth: Bool? = true,
#ViewBuilder content: #escaping () -> Content) {
self.canNavigateBack = canNavigateBack!;
self.canNavigateForth = canNavigateForth!;
self.content = content
}
var body: some View {
VStack(spacing: 15) {
content()
HStack {
// TODO Implement correct back button
if (canNavigateBack) {
NavigationLink(destination: EmptyView()) {
Text("Back")
}
}
if (canNavigateBack && canNavigateForth) {
Spacer()
}
// Naviate to next screen
if (canNavigateForth) {
NavigationLink(destination: nextView) {
Text("Next")
}
}
}
}
.padding()
}
}
struct DemoView_Previews: PreviewProvider {
static var previews: some View {
VStack {
/**
* The usage within the different screens should be something like this
*/
Divider()
WizardView(canNavigateBack: false) {
Text("This would be the first screen, without back navigation.")
}
Divider()
WizardView() {
Text("This would be another screen.")
}
Divider()
WizardView(canNavigateForth: false) {
Text("This is the last screen, without next button.")
}
Divider()
}
}
}
I am fairly new to Swift and SwiftUI so maybe I just don't understand some basic concepts here.
Update:
Here I found something, that helped me, at least a little. SwiftUI MVVM Coordinator/Router/NavigationLink
I will update this, question as soon, as I made my way through.

SwiftUI List disclosure indicator without NavigationLink

I am searching for a solution to show the disclosure indicator chevron without having the need to wrap my view into an NavigationLink. For example I want to show the indicator but not navigate to a new view but instead show a modal for example.
I have found a lot solutions that hide the indicator button but none which explains how to add one. Is this even possible in the current SwiftUI version ?
struct MyList: View {
var body: some View {
NavigationView {
List {
Section {
Text("Item 1")
Text("Item 2")
Text("Item 3")
Text("Item 4")
}
}
}
}
For example I want to add the disclosure indicator to Item 1 without needing to wrap it into an NavigationLink
I already tried to fake the indicator with the chevron.right SF Symbol, but the symbol does not match 100% the default iOS one. Top is default bottom is chevron.right.
It is definitely possible.
You can use a combination of Button and a non-functional NavigationLink to achieve what you want.
Add the following extension on NavigationLink.
extension NavigationLink where Label == EmptyView, Destination == EmptyView {
/// Useful in cases where a `NavigationLink` is needed but there should not be
/// a destination. e.g. for programmatic navigation.
static var empty: NavigationLink {
self.init(destination: EmptyView(), label: { EmptyView() })
}
}
Then, in your List, you can do something like this for the row:
// ...
ForEach(section.items) { item in
Button(action: {
// your custom navigation / action goes here
}) {
HStack {
Text(item.name)
Spacer()
NavigationLink.empty
}
}
}
// ...
The above produces the same result as if you had used a NavigationLink and also highlights / dehighlights the row as expected on interactions.
Hopefully, this is what you are looking for. You can add the item to a HStack and with a Spacer in between fake it that its a Link:
HStack {
Text("Item 1")
Spacer()
Button(action: {
}){
Image(systemName: "chevron.right")
.font(.body)
}
}
The answers already submitted don't account for one thing: the highlighting of the cell when it is tapped. See the About Peek-a-View cell in the image at the bottom of my answer — it is being highlighted because I was pressing it when the screenshot was taken.
My solution accounts for both this and the chevron:
Button(action: { /* handle the tap here */ }) {
NavigationLink("Cell title", destination: EmptyView())
}
.foregroundColor(Color(uiColor: .label))
The presence of the Button seems to inform SwiftUI when the cell is being tapped; simply adding an onTapGesture() is not enough.
The only downside to this approach is that specifying the .foregroundColor() is required; without it, the button text will be blue instead.
in iOS15 the following is a better match as the other solutions were little too big and not bold enough. it'll also resize better to different Display scales better than specifying font sizes.
HStack {
Text("Label")
Spacer()
Image(systemName: "chevron.forward")
.font(Font.system(.caption).weight(.bold))
.foregroundColor(Color(UIColor.tertiaryLabel))
}
Would be good if there was an offical way of doing this. Updating every OS tweak is annoying.
I found an original looking solution. Inserting the icon by hand does not bring the exact same look.
The trick is to use the initializer with the "isActive" parameter and pass a local binding which is always false. So the NavigationLink waits for a programmatically trigger event which will never occur.
// use this initializer
NavigationLink(isActive: <Binding<Bool>>, destination: <() -> _>, label: <() -> _>)
You can pass an empty closure to the destination parameter. It will never get called anyway. To do some action you put a button on top within a ZStack.
func navigationLinkStyle() -> some View {
let never = Binding<Bool> { false } set: { _ in }
return ZStack {
NavigationLink(isActive: never, destination: { }) {
Text("Item 1") // your list cell view
}
Button {
// do your action on tap gesture
} label: {
EmptyView() // invisible placeholder
}
}
}
For accessibility you might need to mimic UIKit version of disclosure indicator. You don't need to implement it this way per se but if you use e.g. Appium for testing you might want to have it like this to keep tests succeeding
Apparently UIKit's disclosure indicator is a disabled button with some accessibility values so here's the solution:
struct DisclosureIndicator: View {
var body: some View {
Button {
} label: {
Image(systemName: "chevron.right")
.font(.body)
.foregroundColor(Color(UIColor.tertiaryLabel))
}
.disabled(true)
.accessibilityLabel(Text("chevron"))
.accessibilityIdentifier("chevron")
.accessibilityHidden(true)
}
}
Or maybe create a fake one and use it, even if you tap you can call your events.
NavigationLink(destination: EmptyView()) {
HStack {
Circle()
Text("TITLE")
}
}
.contentShape(Rectangle())
.onTapGesture {
print("ALERT MAYBE")
}
I created a custom NavigationLink that:
Adds an action API (instead of having to push a View)
Shows the disclosure indicator
Ensures that List cell selection remains as-is
Usage
MYNavigationLink(action: {
didSelectCell()
}) {
MYCellView()
}
Code
import SwiftUI
struct MYNavigationLink<Label: View>: View {
#Environment(\.colorScheme) var colorScheme
private let action: () -> Void
private let label: () -> Label
init(action: #escaping () -> Void, #ViewBuilder label: #escaping () -> Label) {
self.action = action
self.label = label
}
var body: some View {
Button(action: action) {
HStack(spacing: 0) {
label()
Spacer()
NavigationLink.empty
.layoutPriority(-1) // prioritize `label`
}
}
// Fix the `tint` color that `Button` adds
.tint(colorScheme == .dark ? .white : .black) // TODO: Change this for your app
}
}
// Inspiration:
// - https://stackoverflow.com/a/66891173/826435
private extension NavigationLink where Label == EmptyView, Destination == EmptyView {
static var empty: NavigationLink {
self.init(destination: EmptyView(), label: { EmptyView() })
}
}

Disable Scrolling in SwiftUI List/Form

Lately, I have been working on creating a complex view that allows me to use a Picker below a Form. In every case, the Form will only have two options, thus not enough data to scroll downwards for more data. Being able to scroll this form but not Picker below makes the view feel bad. I can't place the picker inside of the form or else SwiftUI changes the styling on the Picker. And I can't find anywhere whether it is possible to disable scrolling on a List/Form without using:
.disable(condition)
Is there any way to disable scrolling on a List or Form without using the above statement?
Here is my code for reference
VStack{
Form {
Section{
Toggle(isOn: $uNotifs.notificationsEnabled) {
Text("Notifications")
}
}
if(uNotifs.notificationsEnabled){
Section {
Toggle(isOn: $uNotifs.smartNotifications) {
Text("Enable Smart Notifications")
}
}.animation(.easeInOut)
}
} // End Form
.listStyle(GroupedListStyle())
.environment(\.horizontalSizeClass, .regular)
if(!uNotifs.smartNotifications){
GeometryReader{geometry in
HStack{
Picker("",selection: self.$hours){
ForEach(0..<24){
Text("\($0)").tag($0)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width:geometry.size.width / CGFloat(5))
.clipped()
Text("hours")
Picker("",selection: self.$min){
ForEach(0..<61){
Text("\($0)").tag($0)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width:geometry.size.width / CGFloat(5))
.clipped()
Text("min")
}
Here it is
Using approach from my post SwiftUI: How to scroll List programmatically [solution]?, it is possible to add the following extension
extension ListScrollingProxy {
func disableScrolling(_ flag: Bool) {
scrollView?.isScrollEnabled = !flag
}
}
and the use it as in example for above demo
struct DemoDisablingScrolling: View {
private let scrollingProxy = ListScrollingProxy()
#State var scrollingDisabled = false
var body: some View {
VStack {
Button("Scrolling \(scrollingDisabled ? "Off" : "On")") {
self.scrollingDisabled.toggle()
self.scrollingProxy.disableScrolling(self.scrollingDisabled)
}
Divider()
List(0..<50, id: \.self) { i in
Text("Item \(i)")
.background(ListScrollingHelper(proxy: self.scrollingProxy))
}
}
}
}
You can use the .scrollDisabled(true) modifier on the component (Form or List) to accomplish this behavior.

In SwiftUI, where are the control events, i.e. scrollViewDidScroll to detect the bottom of list data

In SwiftUI, does anyone know where are the control events such as scrollViewDidScroll to detect when a user reaches the bottom of a list causing an event to retrieve additional chunks of data? Or is there a new way to do this?
Seems like UIRefreshControl() is not there either...
Plenty of features are missing from SwiftUI - it doesn't seem to be possible at the moment.
But here's a workaround.
TL;DR skip directly at the bottom of the answer
An interesting finding whilst doing some comparisons between ScrollView and List:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1...100) { item in
Text("\(item)")
}
Rectangle()
.onAppear { print("Reached end of scroll view") }
}
}
}
I appended a Rectangle at the end of 100 Text items inside a ScrollView, with a print in onDidAppear.
It fired when the ScrollView appeared, even if it showed the first 20 items.
All views inside a Scrollview are rendered immediately, even if they are offscreen.
I tried the same with List, and the behaviour is different.
struct ContentView: View {
var body: some View {
List {
ForEach(1...100) { item in
Text("\(item)")
}
Rectangle()
.onAppear { print("Reached end of scroll view") }
}
}
}
The print gets executed only when the bottom of the List is reached!
So this is a temporary solution, until SwiftUI API gets better.
Use a List and place a "fake" view at the end of it, and put fetching logic inside onAppear { }
You can to check that the latest element is appeared inside onAppear.
struct ContentView: View {
#State var items = Array(1...30)
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text("\(item)")
.onAppear {
if let last == self.items.last {
print("last item")
self.items += last+1...last+30
}
}
}
}
}
}
In case you need more precise info on how for the scrollView or list has been scrolled, you could use the following extension as a workaround:
extension View {
func onFrameChange(_ frameHandler: #escaping (CGRect)->(),
enabled isEnabled: Bool = true) -> some View {
guard isEnabled else { return AnyView(self) }
return AnyView(self.background(GeometryReader { (geometry: GeometryProxy) in
Color.clear.beforeReturn {
frameHandler(geometry.frame(in: .global))
}
}))
}
private func beforeReturn(_ onBeforeReturn: ()->()) -> Self {
onBeforeReturn()
return self
}
}
The way you can leverage the changed frame like this:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(0..<100) { number in
Text("\(number)").onFrameChange({ (frame) in
print("Origin is now \(frame.origin)")
}, enabled: number == 0)
}
}
}
}
The onFrameChange closure will be called while scrolling. Using a different color than clear might result in better performance.
edit: I've improved the code a little bit by getting the frame outside of the beforeReturn closure. This helps in the cases where the geometryProxy is not available within that closure.
I tried the answer for this question and was getting the error Pattern matching in a condition requires the 'case' keyword like #C.Aglar .
I changed the code to check if the item that appears is the last of the list, it'll print/execute the clause. This condition will be true once you scroll and reach the last element of the list.
struct ContentView: View {
#State var items = Array(1...30)
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text("\(item)")
.onAppear {
if item == self.items.last {
print("last item")
fetchStuff()
}
}
}
}
}
}
The OnAppear workaround works fine on a LazyVStack nested inside of a ScrollView, e.g.:
ScrollView {
LazyVStack (alignment: .leading) {
TextField("comida", text: $controller.searchedText)
switch controller.dataStatus {
case DataRequestStatus.notYetRequested:
typeSomethingView
case DataRequestStatus.done:
bunchOfItems
case DataRequestStatus.waiting:
loadingView
case DataRequestStatus.error:
errorView
}
bottomInvisibleView
.onAppear {
controller.loadNextPage()
}
}
.padding()
}
The LazyVStack is, well, lazy, and so only create the bottom when it's almost on the screen
I've extracted the LazyVStack plus invisible view in a view modifier for ScrollView that can be used like:
ScrollView {
Text("Some long long text")
}.onScrolledToBottom {
...
}
The implementation:
extension ScrollView {
func onScrolledToBottom(perform action: #escaping() -> Void) -> some View {
return ScrollView<LazyVStack> {
LazyVStack {
self.content
Rectangle().size(.zero).onAppear {
action()
}
}
}
}
}