Use TabView to implement inifinite scroll like the WeekView of Calendar app - swiftui

I am trying to implement a component with a "Horizontal Scroll View" of weekdays similar to the iOS Calendar App. When the user swipe left/right, the dates in scope will be updated to the previous/next week with animation just like the pagers.
I thought the feature is simple enough so I didn't intend to use any third-party framework. I tried TabView to dynamically append items to the beginning/end of the items array, but the animation seems buggy. The problem shows up when the code inserts a new element at the beginning of the array. I'm confused why everything looks good when the element is appended to the last.
Not sure whether it's the right direction I should dig into for this feature. Any suggestions?
struct ContentView: View {
#State private var selection = "2"
#State var items: [String] = ["0", "1", "2", "3", "4"]
var body: some View {
TabView(selection: $selection) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.tag(item)
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onChange(of: selection, perform: { value in
if items.firstIndex(of: value) == items.count - 2, let last = items.last, let number = Int(last) {
self.items.append("\(number + 1)")
}
if items.firstIndex(of: value) == 1, let first = items.first, let number = Int(first) {
self.items.insert("\(number - 1)", at: 0)
}
})
// .id(items.count) // <-- This will rerender the list so the animation will be interrupted when count is changed.
}
}

Related

How to know the current position in a LazyVGrid in SwiftUI

I'm presenting a dynamic table that can expand from the top or bottom. When presenting data in a combination of ScrollView and LazyVGrid, if I add more data to the bottom the scroll view doesn't change (this is what I want). However when data is added to the top of the list, the scrollview moves to the relative same position as before (but now displaced by the amount of new elements) instead of staying at the same position.
I could easily fix this behaviour if I could know the current visible position so after adding more data I could use .scrollTo (oldPosition + additionalElements). However I can't find a way to get this value.
In the following View you can see the behaviour:
import SwiftUI
struct ContentView: View {
#State var table: [Int] = Array(100...200)
var body: some View {
VStack {
HStack {
Button {
let temp = Array(90...99)
self.table = temp + self.table
} label: {
Text("Add 10 on top. It will displace ScrollView")
}
Button {
let temp = Array(201...210)
self.table = self.table + temp
} label: {
Text("Add 10 to the bottom. It will not move ScrollView")
}
}
ScrollViewReader { reader in
ScrollView {
LazyVGrid(columns: [GridItem()], alignment: .center, spacing: 0.0) {
ForEach(table, id: \.self) { i in
Text("\(i)")
}
}
.onAppear {
reader.scrollTo(150, anchor: .center)
}
}
}
}
.padding()
}
}
Any idea how to get this value?
How to get current position of LazyVGrid?

Picker scroll through one element horizontally

I have a SwiftUI Picker in which an item is selected. The text of one element can be large, so I used UIKit UIPickerView and set the manual height to 100, but at some point it became not enough. Is it possible to make scrolling horizontal for each element?
I want to get something like this:
Picker("Items", select: self._selectItem) {
ForEach(self.items, id: \.self) { item in
ScrollView(.horizontal, showsIndicators: false) {
Text(item.description)
}
.tag(item)
}
}
That should work fine. If you only want to scroll one item, you would have to insert a check of the item length.
let items = [
"A long item text.",
"And a even longer item text which is really going further.",
"Another item text which is really going further."
]
struct ContentView: View {
#State private var select = ""
var body: some View {
VStack {
Text("Make your selection!")
List(items, id: \.self) { item in
ScrollView(.horizontal) {
Text(item)
}
.listRowBackground(item == select ? Color.red : Color.white)
.onTapGesture {
select = item
}
}
}
}
}
I would strongly suggest to separate the picking from the text display and scrolling, e.g. like this:
struct ContentView: View {
#State private var select = items[0]
var body: some View {
VStack {
Text("Make your selection!")
Picker("Items", selection: $select) {
ForEach(items) { item in
Text(item.title)
.tag(item)
}
}
ScrollView {
Text(select.text)
}
.padding()
.frame(height: 200)
}
}
}

Present full screen view on tap on list entry in swiftUI

I'm trying to accomplish what I thought was a relatively simple thing, which is to have a view show fullscreen when you tap on an item in a list.
The code shown here works more or less. However, I have to tap exactly on the text. Tapping the free area does not trigger the onTap event.
Sample code:
struct ItemListView: View {
var items: [Item]
var titel: String
#State private var selectedItem: Item?
var body: some View {
List(items) { item in
ItemListCell(item: item)
.onTapGesture {
selectedItem = item
}
}
.navigationBarTitle(titel)
.fullScreenCover(item: $selectedItem, onDismiss: nil) { item in
ItemDetailView(item: item)
}
}
}
struct ItemListCell: View {
var item: Item
var body: some View {
Text(String(item.id))
Text(item.name)
}
}
struct ItemDetailView: View {
#Environment(\.presentationMode) var presentationMode
var item: Item
var body: some View {
Text(String(item.id))
Text(item.name)
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Label("Close", systemImage: "xmark.circle")
}
}
}
I also looked at the "Supporting Selection in Lists" chapter in the documentation, but that only works when the list is in edit mode.
Is there a swiftUI equivalent of UIKit's tableView(_:didSelectRowAt:)?
Make sure that the frame of the content is taking up the whole cell. I do this in the sample below by using .frame(maxWidth: .infinity, alignment: .leading).
Then, add a contentShape modifier sot that SwiftUI knows to consider the entire shape to be the content -- otherwise, it tends to ignore the whitespace.
struct ItemListCell: View {
var item: Item
var body: some View {
VStack {
Text(item.id)
Text(item.name)
}
.frame(maxWidth: .infinity, alignment: .leading)
.border(Color.green) //just for debugging to show the frame
.contentShape(Rectangle())
}
}
Alternatively, if you use Button instead of onTapGesture, you basically get this all for free:
List(items) { item in
Button(action: {
selectedItem = item
}) {
ItemListCell(item: item)
}
}
Lastly, no, there is no direct equivalent of tableView(_:didSelectRowAt:)

swiftui 2.0 Image Gallery onTapgesture only shows the first image in the array

I am creating a reusable gallery view for an app and am having difficulties when any picture is tapped it suppose to become full screen but only the first picture in the array is shown every time no matter the picture tapped. Below is my code, thanks.
import SwiftUI
struct ReusableGalleryView: View {
let greenappData: GreenAppNews
let gridLayout: [GridItem] = Array(repeating: GridItem(.flexible()), count: 3)
#State private var fullscreen = false
#State private var isPresented = false
var body: some View {
VStack{
ScrollView{
LazyVGrid(columns: gridLayout, spacing: 3) {
ForEach(greenappData.GreenAppGallery, id: \.self) { item in
Image(item)
.resizable()
.frame(width: UIScreen.main.bounds.width/3, height: 150)
.onTapGesture {
self.isPresented.toggle()
print(" tapping number")
}
.fullScreenCover(isPresented: $isPresented) {
FullScreenModalView( imageFiller: item)
}
.background(Color.gray.opacity(0.5))
}
}
.padding(.horizontal)
}
}
}
}
This is an example of the json data:
{
"id" : "1",
"GreenAppGallery" : [
"Picture-1",
"Picture-2",
"Picture-3",
"Picture-4",
"Picture-5",
"Picture-6",
"Picture-7",
"Picture-8",
"Picture-9",
"Picture-10"
]
},
fullScreenCover, like sheet tends to create this type of behavior in iOS 14 when using isPresented:.
To fix it, you can change to the fullScreenCover(item: ) form.
Not having all of your code, I'm not able to give you an exact version of what it'll look like, but the gist is this:
Remove your isPresented variable
Replace it with a presentedItem variable that will be an optional. Probably a datatype that is in your gallery. Note that it has to conform to Identifiable (meaning it has to have an id property).
Instead of toggling isPresented, set presentedItem to item
Use fullScreenCover(item: ) { presentedItem in FullScreenModalView( imageFiller: presentedItem) } and pass it your presentedItem variable
Move the fullScreenCover so that it's attached to the ForEach loop rather than the Image
Using this system, you should see it respond to the correct item.
Here's another one of my answers that covers this with sheet: #State var not updated as expected in LazyVGrid

Editable TextField in SwiftUI List

UPDATE: 14 months later, there is this intriguing note in the AppKit release notes:
A TextField that you are editing inside a selected List row now has correct text foreground colors. (68545878)
Now when placing a TextField in a List, the TextField becomes focused on click the very first time it is selected, but subsequent editing attempts fail: the List row is selected but the TextField does not gain focus.
O/P:
In a beta6 SwiftUI macOS (not iOS) app, I need to have a List with editable text fields, but the TextFields in the list are not editable. It works fine if I swap out List for Form (or just VStack), but I need it working with a List. Is there some trick to tell the list to make the fields editable?
import SwiftUI
struct ContentView: View {
#State var stringField: String = ""
var body: some View {
List { // works if this is VStack or Form
Section(header: Text("Form Header"), footer: Text("Form Footer")) {
TextField("Line 1", text: $stringField).environment(\.isEnabled, true)
TextField("Line 2", text: $stringField)
TextField("Line 3", text: $stringField)
TextField("Line 4", text: $stringField)
TextField("Line 5", text: $stringField)
}
}
}
}
The following code is only an experiment to understand the character of List in SwiftUI and show an alternative. What I understand from observing the output from various combinations is that, List View's style is structured in a way to override the default behaviors of underlying View to become Selectable. This means that TextField does absolutely different. TextField is an focusable element where we can type. This focusing variable is not wired in to List View to work together. Hence, List override default focusable. Hence it is not possible to create List with TextView. But if you need, next best option is ScrollView instead of List and do the styling explicitly. Check the following code and both ways.
import SwiftUI
struct ContentView: View {
#State private var arr = ["1","2","3"]
var body: some View {
HStack {
VStack {
List {
ForEach(self.arr.indices, id:\.self) {
TextField("", text: self.$arr[$0])
}
}
}
.frame(minWidth: 150, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
VStack {
ScrollView {
ForEach(self.arr.indices, id:\.self) {
TextField("", text: self.$arr[$0])
.textFieldStyle(PlainTextFieldStyle())
.padding(2)
}
}
.padding(.leading, 5)
.padding(3)
}
.background(Color(NSColor.alternatingContentBackgroundColors[0]))
.frame(minWidth: 150, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
}
}
}
extension NSTextField {
open override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
}
Bug Report
I updated the project to target a MacOS app and found the bug you are reporting. I've updated Apple with this feedback because it indeed does seem to be a bug (Welcome to Beta).
FB7174245 - SwiftUI Textfields embedded in a List are not editable
when target is macOS
Update
So what's the point of all the focus on state and binding below? One variable should be bound to a single control. Here is a working example. I'll leave up the older answer as it carries the example forward with a full app saving and retrieving data to/from CoreData.
import SwiftUI
struct ContentView: View {
#State var field1: String = ""
#State var field2: String = ""
#State var field3: String = ""
#State var field4: String = ""
#State var field5: String = ""
var body: some View {
List { // works if this is VStack or Form
Section(header: Text("Form Header"), footer: Text("Form Footer")) {
TextField("Line 1", text: $field1).environment(\.isEnabled, true)
TextField("Line 2", text: $field2)
TextField("Line 3", text: $field3)
TextField("Line 4", text: $field4)
TextField("Line 5", text: $field5)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
BTW - This works on Xcode Beta (11M392r) with MacOS Catalina - 10.15
Beta (19A546d).
Sample Project
Check out this sample that includes an editable Textfield that writes to CoreData, which I am building on Github.
Take a look at the difference between #State and #Binding so that you
can manipulate data from outside of the content view. (60-sec
video)
struct ContentView: View {
// 1.
#State var name: String = ""
var body: some View {
VStack {
// 2.
TextField(" Enter some text", text: $name)
.border(Color.black)
Text("Text entered:")
// 3.
Text("\(name)")
}
.padding()
.font(.title)
}
}
source
Check out the following answer for the appropriate use-case of #State
(SO Answer)
Does get focus—if you tap just right
Using Xcode 12.4 and SwiftUI 2 for a macOS app, I seemed to have the same problem: Could not make TextEdit work inside a List. After reading here, and experimenting some more, I realized that in my case the TextField does reliably get the focus, but only if you tap in just the right way. I think and hope this is not how it should be working, so I posted the question TextField inside a List in SwiftUI on macOS: Editing not working well, explaining the details of my observations.
In summary: Single-tap exactly on existing text does give focus (after a small, annoying, delay). Double-tap anywhere does not give focus. Single-tap anywhere in an empty field does give focus.
SwiftUI 2.0
You can use TextEditor instead of TextField in lists in order to get editable text fields.
TextEditor(text: $strings)
.font(.body)
.cornerRadius(5)
.shadow(color: Color.black.opacity(0.18), radius: 0.8, y: 1)
.frame(height: 20)
This way you will get a similar aspect between the TextEditor and TextField.