SwiftUI Form Picker only shows once - swiftui

I am playing around with SwiftUI, and am currently building a Form using a Picker.
import SwiftUI
struct ContentView: View {
private let names = ["Bill", "Peter", "Johan", "Kevin"]
#State private var favoritePerson = "Bill"
var body: some View {
NavigationView {
Form {
Picker("Favorite person", selection: $favoritePerson) {
ForEach(names, id: \.self) { name in
Text(name)
}
}
}
.navigationBarTitle("Form", displayMode: .inline)
}
}
}
The first time you tap on the "Favorite person" row, the picker shows up fine, and tapping on one of the names brings you back to the form. But tapping on the form row a second time doesn't do anything: you don't go to the picker, the row stays highlighted but nothing happens. If this is a SwiftUI bug, is there a known workaround? (I already needed to use a small navigation bar title to work around the Picker UI bug where otherwise its content moves up when it's shown ☹️)

this issue is just one with the simulator. If you build the app on a physical iOS device, it no longer becomes an issue. It's like that bug with Navigation Link that would only work once.

I have the same problem in Xcode 11.4 and also in the real device.
Picker change didn't call CreateTab, it only worked in initialize.
Picker("Numbers", selection: $selectorIndex) {
ForEach(0 ..< formData.tabs.count) { index in
Text(formData.tabs[index].name).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
.onReceive([self.selectorIndex].publisher.first()) { (value) in
print(value)
CreateTab(tabs: formData.tabs, index: self.selectorIndex)
}

Related

SwiftUI navigationBarTitle not resetting to .large returning from Toolbar set to .inline

There are a few posts regarding SwiftUI .inline not resetting to .largeTitle when navigation returns to the parent:
For example:
Navigation bar title stays inline in iOS 15
and
Navigationbar title is inline on pushed view, but was set to large
While earlier posts seem to suggest this has been corrected, I'm running into the same problem, even in iOS 16, but I'm not using a < Back button, instead I'm using "Cancel" (and not show, "Save") on my DestinationView. My goal is to mimic Apple's practice of showing a modal view when adding data, but a show-style push on the navigation stack when viewing and editing existing data (e.g. Contacts app, Reminders app, Calendar app). The brief code below illustrates the problem without adding extra code to handle data updating (e.g. #EnviornmentObject).
When I run this in the Live Preview in Xcode 14.0.1, scheme set to iPhone 13 Pro, no problems. Click a NavLink, return from destination, and ContentView shows .large navigationBarTitle. BUT when I run in the simulator or on a 13 Pro device, returning to Home from a NavigationLink remains .inline unless I pull down on the list. If I switch to iPhone 14 Pro, the live preview looks fine, but the simulator shows a short of abrupt switch from inline back to large, not a smooth animation. Am I doing something wrong in the setup here or is there a bug in the implementation, noting that the behavior oddly holds to .inline on return home to ContentView, if I use this in either a simulator or device for iPhone 13 Pro. Thanks for guidance & insight!
struct ContentView: View {
#State private var sheetIsPresented = false
var items = ["Item1", "Item2", "Item3"]
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.self) { item in
NavigationLink(item, destination: DestinationView(item: item))
.padding()
}
}
.navigationBarTitle("Home", displayMode: .large)
.listStyle(.plain)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
sheetIsPresented.toggle()
} label: {
Image(systemName: "plus")
}
}
}
}
.sheet(isPresented: $sheetIsPresented) {
NavigationStack {
DestinationView(item: "New!")
}
}
}
}
struct DestinationView: View {
var item: String
#Environment(.dismiss) private var dismiss
var body: some View {
List {
Text(item)
}
.toolbar {
ToolbarItem (placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
}
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden()
}
}

SwiftUI for macOS: Expand clickable area of a TextField in a ForEach view of a List

As a hobby project, I'm developing a SwiftUI app targeted for macOS.
I have a CoreData entity (let's call it Sample) with a String property called title.
In my main view (SamplesView) I'm displaying a List of Samples, and I want titles be editable directly from the list. For that, I've made a sub-view (SampleRowView) with a TextField, and I'm displaying this sub-view in the List using ForEach.
It works and looks okayish. Though, I can edit the title only if I click directly on the TextField's text (point 1 on the screenshot). If I click on the "empty" part of the TextField (f.e. point 2) it does not respond. I thought that the shape of the TextField is limited somehow by the length of its text, but as visible on the screenshot, TextField occupies the whole row.
Appreciate any help and ideas about how to make the TextField respond to click on its any point, not only on the text.
// "Sample" is a CoreData entity
public class Sample: NSManagedObject {
//...
#NSManaged public var title: String
}
// This is the main view
struct SamplesView: View {
#FetchRequest(...)
var samples: FetchedResults<Sample>
var body: some View {
VStack {
List {
ForEach(samples) { sample in
SampleRowView(sample: sample)
}
.onDelete(perform: deleteSample)
}
}
}
}
// List rows with editable Sample's title
struct SampleRowView: View {
#ObservedObject var sample: Sample
var body: some View {
TextField("", text: $sample.title)
}
}
Update:
The problem is the same even on the fresh project. Also, if I change TextField with TextEditor the behavior is kinda expected.
Digging a bit more into it:
TextField inside a List in SwiftUI on macOS: Editing not working well
Editable TextField in SwiftUI List
SwiftUI make ForEach List row properly clickable for edition in EditMode
I've found that it seems to be a bug in SwiftUI, and for now the only solution is to somehow replace the List with ScrollView with custom item moving and deletion. This is sad.
import SwiftUI
struct Sample: Identifiable {
let id: Int
var title: String
init(id: Int) {
self.id = id
self.title = "Sample \(id)"
}
}
struct TestView: View {
#State var samples = [Sample(id: 1), Sample(id: 2)]
var body: some View {
List {
ForEach($samples) { $sample in
TextField("", text: $sample.title) // .textFieldStyle(.squareBorder) -- doesn't help
// TextEditor(text: $sample.title) // This works as expected
}
}
}
}
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
TestView()
}
}
}
I'm using XCode Version 13.2.1, Swift 5, MacOS deployment target 11.6.
You could try to add
.contentShape(Rectangle())
to your View element.
I use it along with Text()-Instances which allows me to accept clicks not only on the written part of the View element, but everywhere within its bounds.

Handling focus event changes on tvOS in SwiftUI

How do I respond to focus events on tvOS in SwiftUI?
I have the following SwiftUI view:
struct MyView: View {
var body: some View {
VStack {
Button(action: {
print("Button 1 pressed")
}) {
Text("Button 1")
}.focusable(true) { focused in
print("Button 1 focused: \(focused)")
}
Button(action: {
print("Button 2 pressed")
}) {
Text("Button 2")
}.focusable(true) { focused in
print("Button 2 focused: \(focused)")
}
}
}
Clicking either of the buttons prints out correctly. However, changing focus between the two buttons does not print anything.
This guy is doing the same thing with rows in a list & says it started working for him with the Xcode 11 GM, but I'm on 11.5 and it's definitely not working (at least not for Buttons (or Toggles - I tried those too)).
Reading the documentation, this appears to be the correct way to go about this, but it doesn't seem to actually work. Am I missing something, or is this just broken?
In case anybody else stumbles upon this question, the answer is to make your view data-focused
So, if you have a list of movies in a scrollview, you would add this to your outer view:
var myMovieList: [Movie];
#FocusState var selectedMovie: Int?;
And then somewhere in your body property:
ForEach(0..<myMovieList.count) { index in
MovieCard(myMovieList[i])
.focusable(true)
.focused($selectedMovie, equals: index)
}
.focusable() tells the OS that this element is focusable, and .focused tells it to make that view focused when the binding variable ($selected) equals the value passed to equals: ...
For example on tvOS, if you wrap this in a scrollview and press left/right, it will change the selection and update the selected index in the $selectedMovie variable; you can then use that to index into your movie list to display extra info.
Here is a more complete example that will also scale the selected view:
https://pastebin.com/jejwYxMU

SwiftUI on MacOS: Call a function when NavigationLink Clicked

My need is simple but I can't seem to figure a way (and many posts are rather complicated or are old/for iOS).
I have a NavigationView with NavigationLink and it all works fine. For certain reasons, I'd like to know the item the user clicked on because it's used for other actions on the view.
I tried putting a button inside the NavigationView but then only the button action fires, but the link doesn't work.
I tried OnTapGesture - then the function fires but isn't propagated so the detail view never shows up.
What am I missing? Is there some way I can do this without overcomplicating?
ForEach(anArray, id: \.self) { entry in
NavigationLink(destination: SettingsView(s: entry)
{
Text("something")
}.onTapGesture {print("Hello")}
}
Here is the solution, actually use a button that does some action and activates a link
Tested with Xcode 11.5b2
#State private var activatedLink: Int? = nil
// ... other code
ForEach(Array(anArray.enumerated()), id: \.element) { (i, entry) in
Button("something") {
print(">> some action")
self.activatedLink = i // << order might be important !!
}.background(
NavigationLink(destination: SettingsView(s: entry), tag: i, selection: $activatedLink) { EmptyView() }
.buttonStyle(PlainButtonStyle()) // << required !!
)
}

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/