Carousel/sidebar list styles in SwiftUI - swift

There are two list styles that are inactive and produce no result with SwiftUI. Try the .carousel and .sidebar styles to figure it out.
struct ContentView : View {
var body: some View {
List {
Text("Hello")
}
.listStyle(.carousel) // Or .sidebar
}
}
Is this due to the beta (bug), or did I missed something ? The List doesn't appear on my iPhone.

SidebarListStyle is available on macOS only, as CarouselListStyle is only for watchOS.
Here's what sidebar-style list looks like when running on macOS:
Expanded:
Collapsed:
Here's the code (although I'm not sure that an instance of Range or any other one-dimensional sequence is the right choice for this style):
struct ContentView : View {
var body: some View {
List (0..<10) { number in
self.view(forNumber: number)
}
.listStyle(.sidebar)
}
func view(forNumber number: Int) -> some View {
print("number == \(number)")
let ret = Text("#\(number)")
.foregroundColor(.blue)
return ret
}
}

Like others pointed out, the syntax is changing.
Try it this way:
.listStyle(CarouselListStyle())

You're right.
It looks like both .carousel and .sidebar styles are not working / not implemented on iOS.
Here's my demo code:
struct ContentView : View {
#State var alternateStyle = false
var body: some View {
var list =
AnyView(List(0...100) { item in
Text("\(item)").tapAction { self.alternateStyle.toggle() }
}
.navigationBarTitle(Text("A List")))
if alternateStyle {
list = AnyView(list.listStyle(.carousel))
} else {
list = AnyView(list.listStyle(.default))
}
return list
}
}
If you tap on a Text, thus swapping the style, you get a blank list.
SwiftUI is still in beta, hence a lot of components are broken or missing.

Related

Preselecting a (dynamic) item in NavigationSplitView

I'm using NavigationSplitView to structure the user interface of my (macOS) app like this:
struct NavigationView: View {
#State private var selectedApplication: Application?
var body: some View {
NavigationSplitView {
ApplicationsView(selectedApplication: $selectedApplication)
} detail: {
Text(selectedApplication?.name ?? "Nothing selected")
}
}
}
The sidebar is implemented using ApplicationsView that looks like this:
struct ApplicationsView: View {
#FetchRequest(fetchRequest: Application.all()) private var applications
#Binding var selectedApplication: Application?
var body: some View {
List(applications, selection: $selectedApplication) { application in
NavigationLink(value: application) {
Text(application.name)
}
}
// This works, but looks a bit complicated and... ugly
.onReceive(applications.publisher) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
if selectedApplication == nil {
selectedApplication = applications.first
}
}
}
// This also does not work, as the data is not yet available
.onAppear {
selectedApplication = applications.first
}
}
}
I'm currently preselecting the first Application item (if it exists) using the shown onReceive code, but it looks complicated and a bit ugly. For example, it only works properly when delaying the selection code.
Is there a better way to achieve this?
Thanks.
How about just setting the selectedApplication using the task modifier with an id as follows:
struct ApplicationsView: View {
#FetchRequest(fetchRequest: Application.all()) private var applications
#Binding var selectedApplication: Application?
var body: some View {
List(applications, selection: $selectedApplication) { application in
NavigationLink(value: application) {
Text(application.name!)
}
}
.task(id: applications.first) {
selectedApplication = applications.first
}
}
}
the task is fired when the view is first displayed, and when the id object is updated, so this works without introducing a delay

UITableView.appearance().backgroundColor init based on horizontalSiceClass

I can't get to work UITableView.appearance().backgroundColor init based on horizontalSizeClass -> it doesn't do a thing, even though the same code with UIDevice.current.localizedModel works.
I need to use horizontalSizeClass because I have slightly different UIs for both classes.
You can preview in any iPhone in portrait. When you see pink, you got it (and then please look at iPad if it's still white :-)).
struct testBackground: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
init() {
if horizontalSizeClass == .compact {
UITableView.appearance().backgroundColor = UIColor(Color.pink)
}
// FOLLOWING WORKS:
// if UIDevice.current.localizedModel == "iPhone" {
// UITableView.appearance().backgroundColor = UIColor(Color.pink)
// }
}
var body: some View {
List {
}.overlay(Text("Hi, pink world!"))
}
}
Thank you for your help!
Environment injected after init, so to solve this we need to move List into separated view and pass environment value via view constructor arguments (in body it is already available)
struct TestBackground: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
ListContainerView(horizontalSizeClass: horizontalSizeClass)
}
}
struct ListContainerView: View {
init(horizontalSizeClass: UserInterfaceSizeClass?) {
if horizontalSizeClass == .compact {
UITableView.appearance().backgroundColor = UIColor(Color.pink)
}
}
var body: some View {
List {
}.overlay(Text("Hi, pink world!"))
}
}

SwiftUI: Animate Cells within a Form

I am trying to animate my Form or rather the cells within it. My problem is that the following code give me a nice insertion animation but for the removal the cell is suddenly removed after am ugly looking delay.
import SwiftUI
struct ContentView: View {
#State var toggledValue = false
#State var pickedValue = 0
var body: some View {
NavigationView {
Form {
Section {
Toggle(isOn: $toggledValue) {
Text("Toggled Value")
}
if toggledValue {
Picker(selection: $pickedValue, label: Text("Picked Value")) {
ForEach((0...5).identified(by: \.self)) {
Text("Pick Value \($0)").tag($0)
}
}
}
}
Section {
Text("Some Text")
}
}
.navigationBarTitle("Navigation Bar Title")
}
}
}
What I tried so far is to to wrap the Toggle in a withAnimation closure but this does not change anything. What makes me wondering is that the same code using List instead of Form gives me the expected Animation. Is that a bug or am I overseeing something?
This will probably work (tested in iOS 16 in a similar situation):
Add #State private var isShowingPicker = false
Replace if toggledValue by if isShowingPicker
Under .navigationBarTitle(...) add:
onChange(of: toggledValue)
{ newValue in
withAnimation { isShowingPicker = newValue }
}

How to use #State and #Binding parallel?

My setup:
I am having a ContentView that represents a the length of a final selection (selection.count). Therefore I need a selection variable on my ContentView using a #State propertyWrapper since I want the View to get update as soon as the value changes. The selection is supposed to be made on my SelectionView therefore I am creating a Binding between my selection variables on the ContentView and SelectionView.
My Problem: The UI on my SelectionView is supposed to be updated as well when the selection variable changes but since it is using #Binding and not #State the view does not get updated. So I would need something where I can use a #State and #Binding at the same time or a #Binding which also makes the UI reload.
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(selection.count)")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
Form {
ForEach((0...9).identified(by: \.self)) { i in
Button(action: {
if self.selection.contains(i) {
self.selection = self.selection.filter { !($0 == i) }
} else {
self.selection.append(i)
}
}) {
if self.selection.contains(i) {
Text("Unselect \(i)")
} else {
Text("Select \(i)")
}
}
}
}
}
}
}
Note: If I am using #State on the SelectionView instead of #Binding it works properly (which obviously requires me to not create the binding which I want).
There's nothing wrong with your binding. It is the right thing to do and it works the way you want it to. A binding is the way you pass mutable state around in SwiftUI, and you are doing that, and it works. A change to a binding does make the view reload.
To convince yourself of that, just get rid of all the extra stuff in your example, and concentrate on the heart of the matter, the binding:
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(selection.count)")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
VStack {
Button.init("Append") {
self.selection.append(1)
}
Text("Selection: \(selection.count)")
}
}
}
}
Run the example, tap the link, tap the button repeatedly. The display of selection.count is changed in both views. That's what you wanted and that's what happens.
Here's a variant on your original code that displays selection more explicitly (instead of selection.count), and you can see that the right thing is happening:
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(String(describing:selection))")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
List {
ForEach(0...9, id:\.self) { i in
Button(action: {
if let ix = self.selection.firstIndex(of:i) {
self.selection.remove(at: ix)
} else {
self.selection.append(i)
}
}) {
if self.selection.contains(i) {
Text("Unselect \(i)")
} else {
Text("Select \(i)")
}
}
}
}
}
}
}
The solution to your problem is: upgrade to the latest Xcode.
I tested your code in a playground under Xcode 11 beta 4 and it worked correctly.
I tested your code in a playground under Xcode 11 beta 3 and it failed in the way you describe (I think).
The current version of Xcode 11 as of this answer is beta 5, under which your code doesn't compile because the identified(by:) modifier has been removed. When I changed your code to work under beta 5, by replacing ForEach((0...9).identified(by: \.self)) with ForEach(0...9, id: \.self), it worked correctly.
Therefore I deduce that you are still running beta 2 or beta 3. (Form wasn't available in beta 1 so I know you're not using that version.)
Please understand that, at the moment, SwiftUI is still quite buggy, and also still undergoing incompatible API changes. The bugs are unfortunate, but the API evolution is good. It's better that we suffer a few months of changes now than years of less optimal API later.
This means that you need to try to stay on the latest Xcode 11 beta unless it introduces bugs (like the Path bug in beta 5, if your app uses Path) that prevent you from making any progress.
Thanks to #robmayoff I was able to solve the problem. It was a problem with Xcode 11 Beta 3, installing the newest beta version solved the problem

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()
}
}
}
}
}