How to make SwiftUI List scroll on tvOS - swiftui

struct ContentView: View {
private let showList = [
"The Flintstones",
"The Cosby Show",
"The Fresh Prince of Bel Air",
"Full House",
"Seinfeld",
"Friends",
"The Big Bang Theory",
"How I Met Your Mother",
"Grey's Anatomy",
"The Office",
"Law & Order",
"I Love Lucy",
"Family Guy",
"The Simpsons",
"24"
]
var body: some View {
List(showList,id: \.self) { element in
Text(element)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am trying to use List in a tvOS app, but when I run the app, the list does not scroll. This list scrolls fine when the same code is run on iOS.
How can I make this list scroll on tvOS?

In TVOS only focusable Elements can be selected via the remote control. So you need to make your list elements focusable. One way would be to make a list of buttons:
List {
ForEach(showList ?? []) { show in
Button(action: {}) {
Text(show)
}
.buttonStyle(CardButtonStyle())
}
}

Related

How do I make a delete BUTTON to remove an item from a list (macOS app)?

I am creating a Notes style app. I want the user to be able to delete a note without having to "swipe to delete". So I do not want to use .onDelete.
I have a trashcan icon next to the note name that I'd like the user to press to delete. I know this can be done in an iOS app using .onTapGesture, but I'm not sure how to do it in a macOS app.
If this can't be done another option would be to create a delete button on the toolbar. Once again, I am not sure how to implement the code to delete the list when the button is pressed.
Here is what I tried in the list view itself:
import SwiftUI
struct NoteListView: View {
let myNotes: [MyNoteViewModel]
var body: some View {
List(myNotes) {myNote in
HStack{
NoteCellView(noteModel: myNote)
Image(systemName: "trash")
// where to potentially implement deletion when trash icon is pressed.
}
}
}
}
NoteCellView:
struct NoteCellView: View {
let noteModel: MyNoteViewModel
var body: some View {
HStack{
Image(systemName: "folder")
Text(noteModel.name)
Spacer()
}
}
}
MyNoteViewModel:
struct MyNoteViewModel: Identifiable{
private let myNote: MyNote
init(myNote: MyNote){
self.myNote = myNote
}
var id: NSManagedObjectID{
myNote.objectID
}
var name: String {
myNote.name ?? ""
}
}
You can use onTapGestureon macOS too :).
But the list of notes has to be a #State or #StateObject var, otherwise you can't change (or delete from) it.
Here is a simplified example:
struct NoteListView: View {
#State var myNotes = ["note 1", "note 2", "note 3", "another note"]
var body: some View {
List(myNotes, id: \.self) { myNote in
HStack{
Text(myNote)
Image(systemName: "trash")
.onTapGesture {
if let index = myNotes.firstIndex(of: myNote) {
withAnimation {
myNotes.remove(at: index)
}
}
}
}
}
}
}

How to display default detail in NavigationSplitView after deleting an item — on iPad

I have a NavigationSplitView (SwiftUI 4, iOS16), list of items in the left part, detail on right. When I run the application, no item selected, it displays "Select item" on the right (detail part). I select an item and it displays item detail on right. So far so good.
Now I delete the item on the left by swiping to the left... but the right part still displays the deleted item detail.
Is there any way to get right part go back to the not-selected detail?
Please notice this behaviour can be observed on iPad only, not on iPhone, as iPhone does not display both parts of NavigationSplitView together.
import SwiftUI
struct ContentView: View {
#State var items = ["item 1", "item 2", "item 3", "item 4"]
var body: some View {
NavigationSplitView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: Text(item)) {
Text(item)
}
}
.onDelete(perform: { offsets in
items.remove(atOffsets: offsets)
})
}
} detail: {
Text("Select item")
}
}
}
Bind your selection with List using #State property and also you don't require to add NavigationLink, NavigationSplitView automatically show detail for the selected list item.
Now if you remove the selected item from the list then after deleting it from the array simply set nil to the selection binding of List.
struct ContentView: View {
#State var items = ["item 1", "item 2", "item 3", "item 4"]
#State private var selection: String?
var body: some View {
NavigationSplitView {
List(selection: $selection) {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete(perform: { offsets in
items.remove(atOffsets: offsets)
guard selection != nil, !items.contains(selection!) else { return }
selection = nil
})
}
} detail: {
if let selectedItem = selection {
Text(selectedItem)
}
else {
Text("Select item")
}
}
}
}
Suggest you to check the Apple documentation of NavigationSplitView for more details.

Why doesn't clearing the new iOS 16 SwiftUI NavigationPath to "pop to root" animate smoothly back to the root view?

I have a new iOS 16 SwiftUI NavigationStack with navigation determined by the NavigationDestination modifier which works fine.
My question is why doesn't it animate smoothly by sliding back to the root view when clearing the NavigationPath if you are more than one view deep within the stack?
It works if you are only one level deep, but anything lower than that causes "popping to root" to just jump back to the root view without the sliding animation.
Is this a "feature" or bug or am I doing something incorrectly?
Steps to re-create the issue
Run the sample code below.
Click the first navigation link and then click "Pop To Root View" - notice that it "slides smoothly" back to root view.
Click the first or second link - then click the "Navigate to View 3" which shows view 3.
Then click "Pop to Root" and you'll notice that it jumps back to the root view rather than slides. That's my question - should it jump back or slide back?
Demo of Issue
Demo Code (using Xcode 14.0 and iOS 16.0):
import SwiftUI
struct DemoPop: View {
#State private var path = NavigationPath()
var body: some View {
VStack {
NavigationStack(path: $path) {
List {
Section("List One") {
NavigationLink("Navigate to View 1", value: "View 1")
NavigationLink("Navigate to View 2", value: "View 2")
}
}
.navigationDestination(for: String.self) { textDesc in
VStack {
Text(textDesc).padding()
Button("Navigate to View 3") {
path.append("View 3")
}.padding()
Button("Pop to Root View") {
path.removeLast(path.count)
}.padding()
}
}
.navigationTitle("Test Pop To Root")
}
}
}
}
struct DemoPop_Previews: PreviewProvider {
static var previews: some View {
DemoPop()
}
}
Update 1:
Think the code above is correct so possibly a bug as mentioned in comments as I have just seen a YouTube video that exhibits the same behaviour - Youtube tutorial - around time line 19:25 - you will see pop to root just jumps back to start.
Update 2:
This has been fixed in iOS 16.2
Most likely a bug, please file a feedback w/ Apple.
That said, if you're on iOS, you can work around it with the following hack:
import UIKit
let animation = CATransition()
animation.isRemovedOnCompletion = true
animation.type = .moveIn
animation.subtype = .fromLeft
animation.duration = 0.3
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
UIApplication.shared.keyWindow!.layer.add(animation, forKey: nil)
self.navigationPath.removeLast()
This has been fixed by Apple on iOS 16.2
For iOS 16.0 and 16.1 here's a 100% working, one-liner solution. Add this SPM library to your codebase https://github.com/davdroman/swiftui-navigation-transitions (version 0.7.1 or later) and simply:
import NavigationTransitions
import SwiftUI
struct DemoPop: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
Section("List One") {
NavigationLink("Navigate to View 1", value: "View 1")
NavigationLink("Navigate to View 2", value: "View 2")
}
}
.navigationDestination(for: String.self) { textDesc in
VStack {
Text(textDesc).padding()
Button("Navigate to View 3") {
path.append("View 3")
}.padding()
Button("Pop to Root View") {
path.removeLast(path.count)
}.padding()
}
}
.navigationTitle("Test Pop To Root")
}
.navigationTransition(.default) // one-liner ✨
}
}
struct DemoPop_Previews: PreviewProvider {
static var previews: some View {
DemoPop()
}
}

Get selected item in SwiftUI list without using a navigation link

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)

Scroll up to see TextField when the keyboard appears in SwiftUI

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/