I want to create a List view with SwiftUI with a sticky component on the top that is outside of the List.
An example of what i want to achieve is on the bottom.
I tried placing the component inside the List as first item but it will be put inside a list item. and just hides a big part of the component
var body: some View {
List {
WeekHeader()
.environmentObject(weekStore)
DayView(
date: weekStore.currentDate,
showAddingWorkout: $showAddingWorkout,
isEditing: isEditing
)
}
.listStyle(.insetGrouped)
}
I tried placing a List inside a VStack with the component before the List which just splits the view in 2 with a List really small on the bottom.
var body: some View {
VStack {
WeekHeader()
.environmentObject(weekStore)
List {
DayView(
date: weekStore.currentDate,
showAddingWorkout: $showAddingWorkout,
isEditing: isEditing
)
}
.listStyle(.insetGrouped)
}
}
And my last attempt with 'looks' like what i want to achieve but its placed inside the navigation bar and the height of the bar doesn't grow with it so the bottom row of the component couldn't be interacted with.
var body: some View {
NavigationView {
List {
DayView(
date: weekStore.currentDate,
showAddingWorkout: $showAddingWorkout,
isEditing: isEditing
)
}
.listStyle(.insetGrouped)
.toolbar {
ToolbarItemGroup(placement: .principal) {
WeekHeader()
.environmentObject(weekStore)
}
}
}
}
This is the component i want to add above the list
And i want my list to look like a default list.
Please let me know if i need to elaborate my question or provide more code.
Related
I am building a File Manager app.
In this App the user can select a specific Folder in a list. There is also the possibility to select the option "All Folders" that is the first element in the list.
I want to keep the selected folder in a AppState class, setting it to nil if the user is selecting "All Folders":
class AppState: ObservableObject {
#Published var selectedFolder: Folder?
}
struct FileListView: View {
#EnvironmentObject
var appState: AppState
var body: some View {
List() {
FileRowView(
title: "All Files",
)
.onTapGesture {
appState.selectedFolder = nil
}
ForEach(filesList) {
file in
FileRowView(
title: file.title ?? "File unnamed",
)
.onTapGesture {
appState.selectedFolder = folder
}
}
}
}
}
Even though this code works and selectedFolder is updated, the List does not highlight in the View actual selected element. It does not have this "bluish" look and feel usually applied to selected items in a list.
You need the List version that keeps track of selections: List(selection: ).
Even though the binding selection var is optional, you cannot use nil for a List element. nil to the List means that nothing is selected.
With that List initializer you don't need onTapGesture, just give each row a .tag() of the same type as your selection var.
here is a simplified example:
struct FileListView: View {
#State var selectedFolder: Int?
var body: some View {
List(selection: $selectedFolder) {
Text("All Files")
.tag(999)
ForEach(0..<10) { file in
Text("file \(file)")
.tag(file)
}
}
}
}
Why the presetsList does not appear? No errors were thrown though.
import SwiftUI
struct AddMessagePreset: View {
let presetsList = [
Preset(name: "preset text 1"),
Preset(name: "preset text 2"),
Preset(name: "preset text 3")
]
var body: some View {
List(presetsList) { singlePresetModel in
SinglePresetChild (presetModel: singlePresetModel)
}
}
}
import SwiftUI
struct Preset: Identifiable {
let id = UUID()
let name: String
}
struct SinglePresetChild: View {
var presetModel: Preset
var body: some View {
Text("Preset Name \(presetModel.name)")
}
}
UPDATE: To show a List inside another ScrollView (or List), you have to set a height on the inner list view:
struct Preview: View {
var body: some View {
ScrollView {
AddMessagePreset().frame(height: 200)
// more views ...
}
}
}
But let me advise against doing so. Having nested scroll areas can be very confusing for the user.
As discussed in the comments, your component code is fine. However, the way you integrate it into your app causes a problem. Apparently, nesting a List inside a ScrollView does not work properly (also see this thread).
List is already scrollable vertically, so you won't need the additional ScrollView:
struct Preview: View {
var body: some View {
VStack {
AddMessagePreset()
}
}
}
P.S.: If you only want to show AddMessagePreset and won't add another sibling view, you can remove the wrapping VStack; or even show AddMessagePreset as the main view, without any wrapper.
I'm writing a SwiftUI Mac app that is similar to a kanban board. The app has three lists: Todo, Doing, and Done. At the bottom of each list is a button to move a task to another list. For example the todo list has a Start Doing button. Selecting a task from the todo list and clicking the button should move the task from the todo list to the doing list.
Every SwiftUI list selection example I have seen uses a navigation link. Selecting a list item takes you to another view. But I don't want to want to navigate to another view when selecting a list item. I want the selected task so I can change its status and move it to the correct list when clicking the button.
Here's the code for one of my lists.
struct TodoList: View {
// The board has an array of tasks.
#Binding var board: KanbanBoard
#State private var selection: Task? = nil
#State private var showAddSheet = false
var body: some View {
VStack {
Text("Todo")
.font(.title)
List(todoTasks, selection: $selection) { task in
Text(task.title)
}
HStack {
Button(action: { showAddSheet = true }, label: {
Label("Add", systemImage: "plus.square")
})
Spacer()
Button(action: { selection?.status = .doing}, label: {
Label("Start Doing", systemImage: "play.circle")
})
}
}
.sheet(isPresented: $showAddSheet) {
AddTaskView(board: $board)
}
}
var todoTasks: [Task] {
// Task conforms to Identifiable.
// A task has a status that is an enum: todo, doing, or done.
return board.tasks.filter { $0.status == .todo}
}
}
When I click on a list item, it is not selected.
How do I get the selected item from the list without using a navigation link?
Workaround
Tamas Sengel's answer led me to a workaround. Give each list item a Start Doing button so I don't have to track the selection.
List(todoTasks, id: \.self) { task in
HStack {
Text(task.title)
Button {
task.status = .doing
} label: {
Text("Start Doing")
}
}
}
The workaround helps for my specific case. But I'm going to keep the question open in hopes of an answer that provides a better alternative to using a button for people who want a way to get the selected list item.
Use a Button in the List and in the action, set a #State variable to the current list item.
#State var currentTask: Task?
List(todoTasks, id: \.self) { task in
Button {
currentTask = task
} label: {
Text(task.title)
}
}
Use .environment(\.editMode, .constant(.active)) to turn on selecting capability.
import SwiftUI
struct ContentView: View {
struct Ocean: Identifiable, Hashable {
let name: String
let id = UUID()
}
private var oceans = [
Ocean(name: "Pacific"),
Ocean(name: "Atlantic"),
Ocean(name: "Indian"),
Ocean(name: "Southern"),
Ocean(name: "Arctic")
]
#State private var multiSelection = Set<UUID>()
var body: some View {
NavigationView {
List(oceans, selection: $multiSelection) {
Text($0.name)
}
.navigationTitle("Oceans")
.environment(\.editMode, .constant(.active))
.onTapGesture {
// This is a walk-around: try how it works without `asyncAfter()`
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: {
print(multiSelection)
})
}
}
Text("\(multiSelection.count) selections")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Put your 3 List with same data array but filtering by status on each one something like:
task.filter({ $0.status == .toDo })
Then on your row add the modifier .onTapGesture be sure to cover all the available space.
Inside the code block introduce your logic or func to change the item status. changeTaskStatus(item: task)
I am having a hard time trying to figured out how to focus on a specific cell/row in the list in the SwiftUI 2.0 and tvOS 14. I need to be able to focus and select a specific record when I am navigated to a view. However when the focus is switched to the list, some random row is focused. I've tried ScrollView and List to create a list of items with Buttons as items and with appropriate prefersDefaultFocus. Nothing works. Here's some sample code:
struct ChannelListView: View {
#Namespace private var namespace
#ObservedObject var viewModel : LiveViewModel
#State var selection = Set<ChannelItem>()
var body: some View {
List(viewModel.channels, selection: $selection){ item in
ScrollViewReader { proxy in
Button(action: {
}){
ChannelItemView(item: item, selectedItem: $viewModel.selectedChannel, onSelected: { id in
})
.padding(.vertical, 2)
}
.buttonStyle(ChannelButtonStyle())
.prefersDefaultFocus(item == viewModel.selectedChannel, in: namespace)
}
}
.focusScope(namespace)
}
}
In my use case, I have to put a TextField below the available items in a List and by using that TextField, we can add items to the List.
Initially, there're no list items (items array is empty)
Here's a minimal, reproducible example
import SwiftUI
struct ContentView: View {
#State var itemName = ""
#State var items = [String]()
var body: some View {
NavigationView {
List {
ForEach(self.items, id: \.self) {
Text($0)
}
VStack {
TextField("Item Name", text: $itemName)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.items.append(self.itemName)
self.itemName = ""
}) {
Text("Add Item")
}
}
}
.navigationBarTitle(Text("Title"))
}
}
}
We can add a new item to the list by typing something in the TextField and clicking "Add Item" Button , Every item that we add using TextField appears above the TextField in the List. So the TextField goes down in the List (Just like Apple’s Reminders app).
If the app has many items (more than 7 items), the keyboard covers the TextField when the keyboard appears and we can’t see the TextField.
Check this screenshot:
What I want to know is how to automatically scroll the List (move the view up) to see the TextField when keyboard appears (like in Apple's Reminders app).
I had a similar problem in my recent project, the easiest way for me to solve it was to wrap UITextField in SwiftUI and from my custom wrapper reach to the parent scroll view and tell it to scroll when the keyboard appears. I tried my approach on your project and it seems to work.
If you take my code for the wrapper and other files from this GitHub folder: https://github.com/LostMoa/SwiftUI-Code-Examples/tree/master/ScrollTextFieldIntoVisibleRange and then replace the SwiftUI TextField with my custom view (TextFieldWithKeyboardObserver) then it should scroll.
import SwiftUI
struct ContentView: View {
#State var itemName = ""
#State var items = [String]()
var body: some View {
NavigationView {
List {
ForEach(self.items, id: \.self) {
Text($0)
}
VStack {
TextFieldWithKeyboardObserver(text: $itemName, placeholder: "Item Name")
Button(action: {
self.items.append(self.itemName)
self.itemName = ""
}) {
Text("Add Item")
}
}
}
.navigationBarTitle(Text("Title"))
}
}
}
I recently wrote an article explaining this solution: https://lostmoa.com/blog/ScrollTextFieldIntoVisibleRange/