Event that fires as Swiftui Picker scrolls through its list - swift

I have a SwiftUI Picker and I want to update another view based on its value as the user scrolls through the list. By passing a #Binding to the picker I can update when the user stops on a selected item... but I haven't been able to find an event that fires as the list scrolls.
With the old UIPickerView you can use titleForRow to hook into this as mentioned in this answer. I'm looking for something similar that works with SwiftUI's Picker.
Here's an extremely simple SwiftUI snippet that demonstrates the problem with the default behavior that I'm trying to find a way to work around:
import SwiftUI
struct ContentView: View {
#State var selected:Int = 0
var toDisplay:String {
get {
return "Selected: \(selected)"
}
}
var body: some View {
return VStack {
Text(toDisplay)
Form {
Section{
Picker(selection: $selected, label: Text("")) {
ForEach(0..<100) {
Text("Item \($0)").tag($0)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: 350, height: 250, alignment: .center)
}
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
If you run this, you can see that the Selected: # text at the top doesn't update until the picker completely stops:

If you’re looking for a way to achieve this using only SwiftUI’s Picker, then I believe you’re out of luck. Because of how SwiftUI is designed, you really don’t have access to the underlying functionality of these views in the same way you would with UIKit. If you think that using titleForRow with a UIPickerView is a viable solution, then your best bet would be to use UIViewRepresentable to port UIPickerView over to use with SwiftUI.

Related

SwiftUI Keyboard dismiss not interactive

Thanks for taking your time to help others :)
Problem description:
App must support iOS 14 (there's no keyboard toolbar), and cannot use Introspect library, sorry.
When using a TextField, I want to dismiss it interactively. It does dismiss the keyboard, but does not take TextField along. And it should.
Simple demo code to replicate what happens:
import SwiftUI
struct ContentView: View {
#State var text: String = ""
init() {
UIScrollView.appearance().keyboardDismissMode = .interactive // To interactively dismiss
}
var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(1...200, id: \.self) { msg in
Text("Message \(msg)")
.padding()
.background(Color.red.cornerRadius(8))
}
}
}
TextField("Hello", text: $text)
.textFieldStyle(.roundedBorder)
.padding()
}
.navigationTitle("Example")
.navigationBarTitleDisplayMode(.inline)
}
}
GIF resources to see behaviour:
Good result:
Bad result:
What I have checked?
Registering the keyboard height through Notifications events, like in: this post and adding that height as offset, bottom padding... nothing works.
Any idea?

Extend SwiftUI's TextEditor within Form to use remaining space

I'm using a TextEditor inside a Form. A minimal playground example would be
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var text = String()
var body: some View {
Form {
Section("Section") {
Toggle("Toggle", isOn: .constant(true))
LabeledContent("TextEditor") {
TextEditor(text: $text)
}
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
This renders into something like:
As TextEditor is a multiline input field, I'd like it to extend to the remaining available space on the screen, so something like
I can achieve this by adding a .frame(height:540) modifier to the TextEditor, however this hardcodes the extend is not very dynamic and thus only works on a specific device.
So the question is, how to to this in a dynamic way which works on all potential devices (different iPhones, iPad, ...).
Note: This question is similar to SwiftUI Texteditor in a form. However this only addresses the issue how to get it to show multiple lines, which can be easily achieved using the above mentioned .frame(height:X) modifier.
I don't think this is the best solution, although it works. If you want something else, try using GeometryReader with a dynamic height value stored in a CGFloat variable. (Tested on iPhone 14 Pro, 14 Pro Max and iPad 12.9 inch)
UISCREEN SOLUTION
import SwiftUI
struct ContentView: View {
#State var text = String()
var body: some View {
if #available(iOS 16, *){
Form {
Section("Section") {
Toggle("Toggle", isOn: .constant(true))
LabeledContent("TextEditor") {
TextEditor(text: $text)
}.frame(minHeight: UIScreen.main.bounds.maxY-190)
}
}
}
}
}
You can also use constant points, as the points reflect different screen sizes and resolutions: https://stackoverflow.com/a/73653966/14856451
GEOMETRY READER SOLUTION
#State var text = String()
#Environment(\.defaultMinListRowHeight) var minH
var body: some View {
if #available(iOS 16, *){
GeometryReader {geometry in
Form {
Section("Section") {
Toggle("Toggle", isOn: .constant(true))
LabeledContent("TextEditor") {
TextEditor(text: $text)
}.frame(minHeight: geometry.size.height-minH-70)
}
}
}
}
}
Notice the blue outline - that's the size of your form. As you can see in all three cases, the TextField reaches the bottom of the form without going into the safe zone, which is used to control gestures on the device (there are no gestures on the iPhone SE, so there is no safe zone either).

How to remove the background box gray of a picker in SwiftUI? [duplicate]

I want to change the color of the selected row.
As you may see, by default it has this light gray color.
I have no idea how to do that since I have found no way to access this row at all.
Is there any way to do that?
Demo code:
struct ContentView: View {
var data = Array(0...20).map { "\($0)" }
#State private var selected = 0
var body: some View {
VStack {
Picker("", selection: $selected) {
ForEach(0 ..< data.count) {
Text(data[$0])
}
}
}
}
}
A UIKit answer would also be welcomed since I have found no way either.
Thank you!
Using the Introspect Swift Package
It is not trivial to do in SwiftUI. I did some research however.
The underlying class behind SwiftUI's Picker goes back to UIKit and it is the UIPickerView. To customise its behaviour you need access then to the UIPickerView class and the documentation is somewhat severely lacking.
And looking at other posts on SO it seems that the way to configure the picker goes all the way to objC for example with the setValue(_ value: Any?, forKey key: String) function. I could not even find a list of those keys! And trying to set up the color of the text did not work however... I found out that I can access the subviews and could solve this background color problem.
This is just to say that I think the best way I found is using this Swift Package called Introspect.
These guys (and girls) did really an amazing job.
First install the Swift Package in your project.
Then add the code to introspect the UIPickerView. This is not part of the defaults so you need to create a custom extension like:
import Introspect
extension View {
public func introspectUIPickerView(customize: #escaping (UIPickerView) -> ()) -> some View {
return inject(UIKitIntrospectionView(
selector: { introspectionView in
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
return nil
}
return Introspect.previousSibling(containing: UIPickerView.self, from: viewHost)
},
customize: customize
))
}
}
In your code you have now access to the UIPickerView, the underlying UIKit class of your SwiftUI's Picker. Add this after the Picker:
Picker("Flavor", selection: $selectedFlavor) {
ForEach(Flavor.allCases) { flavor in
Text(flavor.rawValue.capitalized)
}
}
.pickerStyle(WheelPickerStyle()
.introspectUIPickerView { picker in
picker.subviews[1].backgroundColor = UIColor.clear
}
I used the Picker example from the Apple docs and added the .introspectUIPickerView modifier.
The result: no more grey highlighted color.
Not everything will work because the UIPickerView is really old and some things are not easily customisable. So if you want to customise other things your mileage might vary :)
Here in red with :
.introspectUIPickerView { picker in
picker.subviews[1].backgroundColor = UIColor.red
.withAlphaComponent(0.2)
}
Here would be a way to somewhat customize it.
The workaround is to just simply put a RoundedRectangle with the color of your choice perfectly underneath it.
It's not perfect, since the gray default opacity overlay is on top of that color and one has to figure out the constraints based on the frame.
struct ContentView: View {
var data = Array(0...20).map { "\($0)" }
#State private var selected = 0
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 5)
.frame(width: UIScreen.main.bounds.size.width-18, height: 32)
.foregroundColor(.green)
Picker("", selection: $selected) {
ForEach(0 ..< data.count) {
Text(data[$0])
}
}
}
}
}
More simple use:
.accentColor(your color)
Picker("", selection: $selected) {
ForEach(0 ..< data.count) {
Text(data[$0])
}
}
.accentColor(.black)

Sheet Modal does not present whole View in SwiftUI

I am pretty new to SwiftUI programming and ran into the following problem that I cannot find an answer to.
I want to open a sheet modal from my Main View and want to present a simple View with an Rect on it (for testing purposes).
.sheet(isPresented: $api.showDrawing) {
DrawingViewView()
}
My DrawingViewView looks like:
struct DrawingViewView: View {
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 1500, height: 1000)
}
}
No matter how big I make the Rect it is never shown bigger than:
Is there a way to make the sheet bigger in width?
EDIT:
I also thought of using a fullScreenCover, but if I open a PKCanvasView in a fullScreenCover pencilKit is acting weird. The lines I draw do not correspond with the pencilInput
EDIT: Apperently the problem is the horizontal orientation. If I turn my iPad vertical I have no problems at all!
Thanks a lot!
Jakob
I think this is the easiest way to do it.
import SwiftUI
struct FullScreenModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Color.red.edgesIgnoringSafeArea(.all)
Button("Dismiss Modal") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("Present!") {
isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
}
struct ex_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Hope is what you expected!

Left animation shift when using nested NavigationLink

I am developing an application using SwiftUI (XCode 12.5.1) and every time one of my View appears after exactly two links of "NavigationLink" everything that is inside a Form is shifted slightly to the left, once the appearing animation is over. The following video shows whats going on : the first two times I open the view, everything is fine. The next two times, when the view is accessed from nested NavigationLink, a slight shift to the left is done once the appearing animation is over.
https://www.dropbox.com/s/k3gjc42xlqp2auf/leftShift.mov?dl=0
I have the same problem on both the simulator and a real device (an iPhone). Here is the project: https://www.dropbox.com/s/l8r5hktg6lz69ob/Bug.zip?dl=0 . The main code is available below.
Here is the main view ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: PersonView()) {
Text("Person")
}
NavigationLink(destination: IndirectView()) {
Text("Indirect")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is the indirect view, IndirectView.swift
import SwiftUI
struct IndirectView: View {
var body: some View {
List {
NavigationLink(destination: PersonView()) {
Text("Person")
}
}
}
}
and the person view, PersonView.swift
import SwiftUI
struct PersonView: View {
var body: some View {
Form {
VStack(alignment: .leading, spacing: 5) {
Text("Last Name")
.font(.system(.subheadline))
.foregroundColor(.secondary)
Text("Fayard")
}
}
}
}
Do you have any idea on what's causing this shift?
Thanks for your help
Francois
Frankly saying I have not idea what causes the problem, but here is the fix: add this line of code no your NavigaitonView
NavigationView {
// everything else
}.navigationViewStyle(StackNavigationViewStyle())