SwiftUI picker scale to fit selection - swift

I have a picker like the following that has a frame width of 150. I need the text to fit inside the picker, but the lineLimit(), scaledToFit() and scaledToFill() do not do anything on the picker object.
#State var Status: String = ""
Picker(selection: $Status, label:
Text("Status")) {
Text("yyyyyyyyyyyyyyyyyyyyyyyyyyyyy").tag(0)
.lineLimit(2).scaledToFit().scaledToFill()
Text("zzzzzzzzzzzzzzzzzzzzzzzzzzzzz").tag(1)
.lineLimit(2).scaledToFit().scaledToFill()
}.frame(maxWidth: 150, minHeight: 50, maxHeight: 50, alignment:.center)
.background(Color.gray)
.lineLimit(2)
.scaledToFit()
What is the correct way to scale and fit selected text inside the picker?
Thank you for any help!!

I was unable to figure out how to do it with the picker object in SwiftUI, so I ended up creating a custom picker view that resizes the selected value in the frame.
struct PickerView: View{
#State var picking: Bool = false
#State var Status: String = ""
#State var ValueList = ["yyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]
var body: some View{
CustomPicker(picking: $picking, Status: $Status, ValueList: $ValueList).onTapGesture{
picking = true
}
}
}
struct CustomPicker: View{
#Binding var picking: Bool
#Binding var Status: String
#Binding var ValueList: [String]
var body: some View{
HStack{
HStack{
if Status == "" {
Text("Pick Value")
} else {
Text(Status).lineLimit(2)
}
}.frame(maxWidth: 150, minHeight: 50, maxHeight: 50, alignment:.center)
.background(Color.gray)
}.sheet(isPresented: $picking, content: {
ForEach(ValueList, id: \.self){ item in
Text(item).onTapGesture {
Status = item
picking = false
}.foregroundColor(Color.blue).padding().border(Color.gray)
}
})
}
}
You tap to pick a value
You can then select a value
And it displays correctly within the box

Try rather using scaleToFill with a lineLimit of 1.
Check out Apple's Documentation
Another good StackOverflow answer
Example:
Text("yyyyyyyyyyyyyyyyyyyyyyyyyyyyy").tag(0).lineLimit(1).scaledToFill()

Related

SwiftUI - Button inside a custom Collection view is not tappable completely

I have a set of buttons to show for the user and I used CollectionView to align the buttons. Each button is a Vstack with an Image and Text components. The tap is reactive only on the image but not on Text and the padding space around.
I am looking to solve this to make it reactive all over the button.
I found suggestions
to set ContentShape to rectangle and it didn't work
use Hstack to insert spaces on Left and right of the Text but that didn't work either.
Sample code:
ToolBarItem:
var body: some View {
VStack {
Button(action: {
// Delegate event to caller/parent view
self.onClickAction(self.toolBarItem)
}) {
VStack {
HStack {
Spacer()
Image(self.toolBarItem.selectedBackgroundImage)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.padding(EdgeInsets(top: 5, leading: 3, bottom: 0, trailing: 3))
.frame(width: CGFloat(self.toolBarItem.cellWidth * 0.60),
height: CGFloat(self.toolBarItem.cellHeight * 0.60))
Spacer()
}
.contentShape(Rectangle())
HStack {
Spacer()
Text(self.toolBarMenuInfo.specialSelectedName)
.foregroundColor(Color.red)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 5, trailing: 0))
Spacer()
}
.contentShape(Rectangle())
}
.frame(width: CGFloat(self.toolBarItem.cellWidth),
height: CGFloat(self.toolBarItem.cellHeight))
.background(Color.blue.opacity(0.5))
}
}
}
The above ToolBarItem is placed inside the Collection view (custom Object created by me) for as many items required. Refer attachment and the tap occurs only on the image surrounded by green marking.
has anyone had similar issue? Any inputs is appreciated.
I strongly suspect that you issue has to do with the border. but I don't know exactly because you haven't provided that code.
Here is a version of the button view that would give you the effect you see to want.
struct FloatingToolbarButtonView: View {
#Binding var model: ButtonModel
let size: CGSize
var body: some View {
Button(action: {
//Set the model's variable to selected
model.isSelected.toggle()
//Perform the action
model.onClick()
}, label: {
VStack {
//REMOVE systemName: in your code
Image(systemName: model.imageName)
//.renderingMode(.original)
.resizable()
//Maintains proportions
.scaledToFit()
//Set Image color
.foregroundColor(.white)
//Works with most images to change color
.colorMultiply(model.colorSettings.imageNormal)
.padding(5)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
//Set border color/width
.border(Color.green, width: model.isSelected ? 3:0)
Spacer()
Text(model.label)
//Set Text color
.foregroundColor(model.colorSettings.labelNormal)
}
.padding(EdgeInsets(top: 5, leading: 3, bottom: 0, trailing: 3))
}).frame(width: size.width, height: size.height, alignment: .center)
.background(model.colorSettings.backgroundNormal)
}
}
And this is what the model I used looks like
//Holds Button information
struct ButtonModel: Identifiable{
let id: UUID = UUID()
var label: String
var imageName: String
///Action to be called when the button is pressed
var onClick: () -> Void
///identify if the user has selected this button
var isSelected: Bool = false
var colorSettings: ButtonColorSettings
}
I created the buttons in a view model so I can have an easy to to set the action and access isSelected as needed.
//ViewModel that deals with all the button creation and onClick actions
class FloatingToolbarParentViewModel: ObservableObject{
//Settings the buttons at this level lets you read `isPressed`
#Published var horizontalButtons: [ButtonModel] = []
#Published var moreButton: [ButtonModel] = []
#Published var verticalButtons: [ButtonModel] = []
init(){
horizontalButtons = horizontalSamples
moreButton = [mer]
verticalButtons = veticalSamples
}
}
//MARK: Buttons
extension FloatingToolbarParentViewModel{
//MARK: SAMPLES fill in with your data
var identify:ButtonModel {ButtonModel(label: "Identify", imageName: "arrow.up.backward", onClick: {print(#function + " Identfy")}, colorSettings: .white)}
var tiltak:ButtonModel {ButtonModel(label: "Tiltak", imageName: "scissors", onClick: {print(#function + " Tiltak")}, colorSettings: .white)}
var tegn:ButtonModel { ButtonModel(label: "Tegn", imageName: "pencil", onClick: {print(#function + " Tegn")}, colorSettings: .white)}
var bestand:ButtonModel {ButtonModel(label: "Bestand", imageName: "leaf", onClick: {print(#function + " Identfy")}, colorSettings: .red)}
var mer:ButtonModel {ButtonModel(label: "Mer", imageName: "ellipsis.circle", onClick: {print(#function + " Mer")}, colorSettings: .red)}
var kart:ButtonModel {ButtonModel(label: "Kart", imageName: "map.fill", onClick: {print(#function + " Kart")}, colorSettings: .white)}
var posisjon:ButtonModel {ButtonModel(label: "Posisjon", imageName: "magnifyingglass", onClick: {print(#function + " Posisjon")}, colorSettings: .white)}
var spor:ButtonModel {ButtonModel(label: "Spor", imageName: "circle.fill", onClick: {print(#function + " Spor")}, colorSettings: .red)}
var horizontalSamples :[ButtonModel] {[identify,tiltak,tegn,bestand]}
var veticalSamples :[ButtonModel] {[kart,posisjon,spor]}
}
The rest of the code to get the sample going is below. It isn't really needed but it will give you a working product
struct FloatingToolbarParentView: View {
#State var region: MKCoordinateRegion = .init()
#StateObject var vm: FloatingToolbarParentViewModel = .init()
var body: some View {
ZStack{
Map(coordinateRegion: $region)
ToolbarOverlayView( horizontalButtons: $vm.horizontalButtons, cornerButton: $vm.moreButton, verticalButtons: $vm.verticalButtons)
}
}
}
struct ToolbarOverlayView: View{
#State var buttonSize: CGSize = .zero
#Binding var horizontalButtons: [ButtonModel]
#Binding var cornerButton: [ButtonModel]
#Binding var verticalButtons: [ButtonModel]
var body: some View{
GeometryReader{ geo in
VStack{
HStack{
Spacer()
VStack{
Spacer()
FloatingToolbarView(buttons: $verticalButtons, buttonSize: buttonSize, direction: .vertical)
}
}
Spacer()
HStack{
Spacer()
FloatingToolbarView(buttons: $horizontalButtons, buttonSize: buttonSize)
FloatingToolbarView(buttons: $cornerButton, buttonSize: buttonSize)
}
//Adjust the button size on appear and when the orientation changes
.onAppear(perform: {
setButtonSize(size: geo.size)
})
.onChange(of: geo.size.width, perform: { new in
setButtonSize(size: geo.size)
})
}
}
}
//Sets the button size using and minimum and maximum values accordingly
//landscape and portrait have oppositive min and max
func setButtonSize(size: CGSize){
buttonSize = CGSize(width: min(size.width, size.height) * 0.15, height: max(size.width, size.height) * 0.1)
}
}
//Toolbar group for an array of butons
struct FloatingToolbarView: View {
#Binding var buttons :[ButtonModel]
let buttonSize: CGSize
var direction: Direction = .horizontal
var body: some View {
Group{
switch direction {
case .horizontal:
HStack(spacing: 0){
ForEach($buttons){$button in
FloatingToolbarButtonView(model: $button, size: buttonSize)
}
}
case .vertical:
VStack(spacing: 0){
ForEach($buttons){$button in
FloatingToolbarButtonView(model: $button, size: buttonSize)
}
}
}
}
}
enum Direction{
case horizontal
case vertical
}
}
#available(iOS 15.0, *)
struct FloatingToolbarParentView_Previews: PreviewProvider {
static var previews: some View {
FloatingToolbarParentView()
FloatingToolbarParentView().previewInterfaceOrientation(.landscapeLeft)
}
}
//Holds Button Color information
//You havent provided much info on this so I assume that you are setting the colors somewhere
struct ButtonColorSettings{
var labelNormal: Color
var imageNormal: Color
var backgroundNormal: Color
//Sample Color configuration per image
static var white = ButtonColorSettings(labelNormal: .white, imageNormal: .white, backgroundNormal: .black.opacity(0.5))
static var red = ButtonColorSettings(labelNormal: .black, imageNormal: .red, backgroundNormal: .white)
}
Have you tried putting .contentShape(Rectangle()) on the whole VStack inside the Button or on the button itself? That should probably solve it.

SwiftUI limit tappable area of picker in form

I would like to limit the tappable area of the picker in a form to only it's visible area of text (i.e. black rectangle in the picture below). Currently whole row can be tappable.
Screen capture of the program
Here is the code:
import SwiftUI
struct ContentView: View {
#State var rate: String = ""
#State var units = ["mL/day","mL/hour"]
#State var unit: Int = 0
var body: some View {
NavigationView{
Form{
HStack{
Text("Rate")
TextField("0", text: $rate)
Picker(selection: $unit, label: Text(""), content: {
ForEach(0..<units.count, content: { unit in
Text(units[unit])
})
})
.frame(width: 100.0)
.compositingGroup()
.clipped()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I read through this question and try to add .clipped() and .compositingGroup() but seems not useful.
You are going to have to redesign the UI a bit. I would recommend this:
struct ContentView: View {
#State var rate: String = ""
#State var units = ["mL/day","mL/hour"]
#State var unit: Int = 0
var body: some View {
NavigationView{
Form{
Section(header: Text("Rate")) { // Do This
HStack{
TextField("0", text: $rate)
.background(Color.red)
Picker(selection: $unit, label: Text(""), content: {
ForEach(0..<units.count, content: { unit in
Text(units[unit])
})
})
.frame(width: 100)
.clipped()
.background(Color.green)
}
}
}
}
}
}
I made colored backgrounds for each of the subviews. The issue is not that the whole row is selectable. It is that when you have a Picker() in the row, any view that is not itself selectable, in this case the Text() triggers the Picker(). If you use your code and put a colored background behind the text, you will see what happens. If you tap on the Picker() it triggers. If you tap on the TextField() you get an insertion point there. But if you tap on the Text() it triggers the Picker(). The .compositingGroup doesn't affect this behavior at all.
I have this exact design in an app, and the solution is to put the text into Section with a header, or put the Text() in the row above.

Get a specific id in a modal

I'm still learning on the job and my question may seem stupid.
I've got a list of movies and on the tap I want to show card of the selected movie.
So I've got my ResultsView
var results:[DiscoverResult]
#State private var resultsCount:Int = 0
#State private var isPresented:Bool = false
#EnvironmentObject private var genres:Genres
var body: some View {
ScrollView {
ForEach (results){ result in
Button(action: {
isPresented.toggle()
}, label: {
ZStack {
ZStack {
KFImage(URL (string: baseUrlForThumb + result.posterPath)).resizable().scaledToFill()
.frame( height: 150)
.mask(Rectangle().frame( height: 150))
Rectangle().foregroundColor(.clear) // Making rectangle transparent
.background(LinearGradient(gradient: Gradient(colors: [.clear, .clear, .black]), startPoint: .top, endPoint: .bottom))
}.frame( height: 150)
// Titre du film
VStack(alignment: .center) {
Spacer()
Text(result.title)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
// Genres du film
Text(genres.generateGenresList(genreIDS: result.genreIDS)).font(.caption).foregroundColor(.white).multilineTextAlignment(.center)
} .padding()
}.padding(.horizontal)
})
.sheet(isPresented: $isPresented, content: {
MovieView(isPresented: $isPresented, movieId: result.id)
})
.navigationTitle(result.title)
}
}
}
}
And my MovieView
import SwiftUI
struct MovieView: View {
#Binding var isPresented: Bool
var movieId:Int
var body: some View {
VStack {
Text(String(movieId))
.padding()
Button("Fermer") {
isPresented = false
}
}
}
}
But the movie card still the same even list element selected.
I think that the 'result.id' is overwrite at every loop but i don't know how to fix it.
Sorry for my english mistakes.
thank for your purpose.
Instead of using isPresented for .sheet you can use .sheet(item:, content:) and pass the whole result object
.sheet(item: $selecteditem( { result in
MovieView(item: result)
}
To make this work you need a new property (you can remove isPresented)
#State private var selectedItem: DiscoverResult?
and you need update your MovieView struct
struct MovieView: View {
let result: DiscoverResult
var body: some View {
//...
}
}
or pass only the movie id to your MovieView if you prefer that.

Why the #State value is not updated

I have three buttons, each button corresponds to a value in the data array. When user click a button, the corresponding value will be displayed in a sheet window. Here's my code:
struct TestView: View {
#State var data: [Int] = [100, 200, 300]
#State var presented: Bool = false
#State var index4edit: Int = 1
var body: some View {
HStack{
ForEach(1..<4){ index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index)) }
.frame(width: 30, height: 30)
.padding()
}
}
.sheet(isPresented: $presented, content: {
Text("\(index4edit): \(data[index4edit-1])")
})
}
}
My problem is, the first time I click a button, no matter which button, it's always display the first data 100. The reason is that the data index4edit was not updated on the first click, it is always 1, but why? Thanks.
That's because sheet view is created on View initalization. When you change your State, your view won't be recreated because your State variable is not used in your view.
To solve it, just use your State variable somewhere in the code like this dummy example
HStack{
ForEach(1..<4){ index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index) + (index4edit == 0 ? "" : "")) } //<<here is the State used
.frame(width: 30, height: 30)
.padding()
}
}
Now you view will rerender when the State changes, hence your sheet view will be rerendered.
You should set up index4edit to be the real (0-based) index, not index + 1. You should also use data.indices in the ForEach instead of manually inputting the range.
struct TestView: View {
#State private var data: [Int] = [100, 200, 300]
#State private var presented: Bool = false
#State private var index4edit: Int = 0
var body: some View {
HStack {
ForEach(data.indices) { index in
Button(action: {
index4edit = index
presented.toggle()
}){ Text(String(index)) }
.frame(width: 30, height: 30)
.padding()
}
}
.sheet(isPresented: $presented, content: {
Text("\(index4edit): \(data[index4edit])")
})
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}

Bind to a struct property in a SwiftUI Mac app

I'm building a macOS unit converter app in SwiftUI. In the example below, I have an acceleration view that has many text fields. These text fields represent different units of acceleration.
// AccelerationView.swift
struct AccelerationView: View {
#State private var accel = Acceleration()
#EnvironmentObject var formatter: Formatter
var body: some View {
VStack(alignment: .leading) {
Text("Metric").font(.subheadline)
HStack {
TextField("kilometer per second squared", value: $accel.kilometerPerSecondSquare, formatter: formatter.numberFormatter).frame(width: 120)
Text("km/s²").frame(width: 60, alignment: .leading)
}
HStack {
TextField("meter per second squared", value: $accel.meterPerSecondSquare, formatter: formatter.numberFormatter).frame(width: 120)
Text("m/s²").frame(width: 60, alignment: .leading)
}
HStack {
TextField("millimeter per second squared", value: $accel.millimeterPerSecondSquare, formatter: formatter.numberFormatter).frame(width: 120)
Text("mm/s²").frame(width: 60, alignment: .leading)
}
// and so on ...
}
.padding()
}
}
I would like to place the HStack into its own file to clean up the code in the view. My attempt at this is shown below:
// UnitTextField.swift
struct UnitTextField: View {
#State var value: Double = 0.0
let placeHolder: String
let label: String
var fieldWidth: CGFloat = 120
var labelWidth: CGFloat = 60
#EnvironmentObject private var formatter: Formatter
var body: some View {
HStack {
TextField(placeHolder, value: $value, formatter: formatter.numberFormatter)
.frame(width: fieldWidth)
.multilineTextAlignment(.trailing)
Text(label)
.frame(width: labelWidth, alignment: .leading)
}
}
}
This does not work because the UnitTextField value does not properly bind to the acceleration struct. I'm trying to accomplish something like this that I can use in the AccelerationView:
// AccelerationView.swift
struct AccelerationView: View {
#State private var accel = Acceleration()
#EnvironmentObject var formatter: Formatter
var body: some View {
VStack(alignment: .leading) {
Text("Metric").font(.subheadline)
UnitTextField(value: $accel.kilometerPerSecondSquare, placeHolder: "kilometer per second squared", label: "km/s²")
UnitTextField(value: $accel.meterPerSecondSquare, placeHolder: "meter per second squared", label: "m/s²")
UnitTextField(value: $accel.millimeterPerSecondSquare, placeHolder: "millimeter per second squared", label: "mm/s²")
// and so on ...
}
.padding()
}
}
Any suggestions on how to properly implement this in SwiftUI for a macOS application?
If to go from what you're going to accomplish (last snapshot), then I assume you need
struct UnitTextField: View {
#Binding var value: Double
instead of
struct UnitTextField: View {
#State var value: Double = 0.0
It could be like this with binding and environmentObject :
class MyFormat: Formatter, ObservableObject{
var numberFormat = NumberFormatter()
}
struct UnitTextField: View {
#Binding var value: Double
let placeHolder: String
let label: String
var fieldWidth: CGFloat = 120
var labelWidth: CGFloat = 60
#EnvironmentObject var formatter: MyFormat
var body: some View {
HStack {
TextField(placeHolder, value: $value, formatter: formatter.numberFormat)
.frame(width: fieldWidth)
.multilineTextAlignment(.trailing)
Text(label)
.frame(width: labelWidth, alignment: .leading)
}
}
}
struct Acceleration{
var kilometerPerSecondSquare : Double = 0.0
var meterPerSecondSquare : Double = 1.0
var millimeterPerSecondSquare : Double = 2.0
}
struct AccelerationView: View {
#State private var accel = Acceleration()
#EnvironmentObject var formatter: MyFormat
var body: some View {
VStack(alignment: .leading) {
Text("Metric").font(.subheadline)
UnitTextField(value: $accel.kilometerPerSecondSquare, placeHolder: "kilometer per second squared", label: "km/s²").environmentObject(formatter)
UnitTextField(value: $accel.meterPerSecondSquare, placeHolder: "meter per second squared", label: "m/s²").environmentObject(formatter)
UnitTextField(value: $accel.millimeterPerSecondSquare, placeHolder: "millimeter per second squared", label: "mm/s²").environmentObject(formatter)
// and so on ...
}
.padding()
}
}