SwiftUI Can't Remove Space Above Picker - Form Version
I'm struggling mightily with the formatting of pickers in SwiftUI. I built a simple
picker with a few other views in a single view app. There is a space between the Header
and the picker that I can't remove. I'd settle for setting the gray color to white. If
I change the frame of the picker it just scrunches the picker - the gray space remains
untouched. I'm not sure what to even call that space - it does not appear to be part
of the Header, nor the Picker, nor the Form nor the Section.
The only reference to this issue that I found was article 57851878 which suggests putting
the view in the header itself. That does not work and would be a really bad idea anyway.
The space in the image outlined is red is the subject:
And this is the code:
struct ContentView: View {
#State private var thing: String = ""
#State private var enableSaveButton = false
#State private var selection = 0
#State private var selection2 = 0
var things = ["books","desks","chairs","lamps","couches","shelves"]
var body: some View {
NavigationView {
//ScrollView{
VStack {
Group {//first group
VStack {
Text("Text at the Top")
TextField("enter something here", text: $thing)
.frame(width:350, height: 50)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.gray, lineWidth: 2))
.padding(.leading, 5)
.padding(.bottom, 20)
Section(header: HStack {
Text("This is the Header")
.font(.headline)
.foregroundColor(.blue)
.padding(.bottom, 0)
.padding(.leading, 30)
Spacer()
}
.background(Color.white)
.listRowInsets(EdgeInsets(
top: 0,
leading: 0,
bottom: 0,
trailing: 0))
) {
Form {
Text("This is the Chosen Thing: \(self.things[selection2])")
Picker(selection: self.$selection2, label: Text("Choose Thing").foregroundColor(.blue)) {
ForEach(0 ..< things.count) {
Text(self.things[$0])
}
}//picker
}//form
.frame(width:350, height: 115)
.padding(.top, 0)
}//first picker section
}//vstack
}//first group
Spacer()
Group {//second Group
Text("Enable and Disable this button")
Button(action: {
print("whatever")
} ) {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.frame(width: 100, height: 40)
Text("Save").font(.headline)
}
}
.shadow(radius: 12, x: 10, y: 10)
//.disabled(!enableSaveButton)
}//second Group
}//outer VStack
//}//Scrollview
.navigationBarTitle("Things")
}//nav view
}//body
}
Any guidance would be appreciated. Xcode 11.2.1 (11B500)
You can remove upper and lower space of Form by adding below code
struct ContentView: View {
init() {
UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
UITableView.appearance().tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
}
//your code .....
}
Another possibility is to set the background color for the table view and table view cell to clear in init()
Init() {
UiTableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundcolor = .clear
}
This will not get rid of the space, but it will make the underlying table transparent to the window background color
it looks like it is a section header of the form...
if you change your code like this you will see, you have a section header text. Why SwiftUI does this by itself ...i don't know....unfortunately it even does not disappear with frame set to height 0....
Form {
Section(header: Text("General Settings")){
Text("This is the Chosen Thing: \(self.things[selection2])")
}
Picker(selection: self.$selection2, label: Text("Choose Thing").foregroundColor(.blue)) {
ForEach(0 ..< things.count) {
Text(self.things[$0])
}
}//picker
}//form
.frame(width:350, height: 115)
.padding(.top, 0)
Related
I attempted to follow SwiftUI - Placing two pickers side-by-side in HStack does not resize pickers to resize my pickers which works fine without applying styling. But when I apply the WheelPickerStyle the frames seem to overlap and incrementing where one picker should be increments a different picker to the right. For example, incrementing what I think is red would increment blue.
Here is what it looks like with a width defined on the frame and the code below.
struct DatePickerView: View {
var hoursArray = [Int](0...23)
var minsArray = [Int](0...60)
var secondsArray = [Int](0...60)
#State var hoursSelection: Int = 0
#State var minsSelection: Int = 0
#State var secsSelection: Int = 0
var body: some View {
VStack {
Text("Select time.").font(.headline)
HStack(spacing: 0) {
Picker(selection: $hoursSelection, label: Text("Hours")) {
ForEach(0..<hoursArray.count) {
idx in Text("\(hoursArray[idx])")
}
}
.frame(width: 50, height: 50, alignment: .center)
.border(Color.red)
.pickerStyle(WheelPickerStyle())
.clipped()
Picker(selection: $minsSelection, label: Text("Minutes")) {
ForEach(0..<minsArray.count) {
idx in Text("\(minsArray[idx])")
}
}
.frame(width: 50, height: 50, alignment: .center)
.border(Color.green)
.pickerStyle(WheelPickerStyle())
.clipped()
Picker(selection: $secsSelection, label: Text("Seconds")) {
ForEach(0..<secondsArray.count) {
idx in Text("\(secondsArray[idx])")
}
}
.frame(width: 50, height: 50, alignment: .center)
.border(Color.blue)
.pickerStyle(WheelPickerStyle())
.clipped()
}
}
}
}
How would you resize the pickers so that when a user clicks the corresponding Picker that Picker increments and not the one to the right?
I am developing an App that supports multiple Profiles. I really like the way Apple displays the Profile Icon next to the Large Navigation Bar Title in all their Apps. See the Screenshot below:
My Question is the following:
Is it possible to achieve this in SwiftUI? And if so, how?
If it's not possible in pure SwiftUI, how can I achieve it including UIKit Code?
Thanks for your help.
I solved this by using SwiftUI-Introspect, to "Introspect underlying UIKit components from SwiftUI".
Here is an example of a view:
struct ContentView: View {
#State private var lastHostingView: UIView!
var body: some View {
NavigationView {
ScrollView {
ForEach(1 ... 50, id: \.self) { index in
Text("Index: \(index)")
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Large title")
.introspectNavigationController { navController in
let bar = navController.navigationBar
let hosting = UIHostingController(rootView: BarContent())
guard let hostingView = hosting.view else { return }
// bar.addSubview(hostingView) // <--- OPTION 1
// bar.subviews.first(where: \.clipsToBounds)?.addSubview(hostingView) // <--- OPTION 2
hostingView.backgroundColor = .clear
lastHostingView?.removeFromSuperview()
lastHostingView = hostingView
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.trailingAnchor.constraint(equalTo: bar.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: bar.bottomAnchor, constant: -8)
])
}
}
}
}
Bar content & profile picture views:
struct BarContent: View {
var body: some View {
Button {
print("Profile tapped")
} label: {
ProfilePicture()
}
}
}
struct ProfilePicture: View {
var body: some View {
Circle()
.fill(
LinearGradient(
gradient: Gradient(colors: [.red, .blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
.padding(.horizontal)
}
}
The .frame(width: 40, height: 40) & hostingView.bottomAnchor constant will need to be adjusted to your needs.
And the results for each option (commented in the code):
Option 1
Option 2
View sticks when scrolled
View disappearing underneath on scroll
Without NavigationView
I done this with pure SwiftUI. You have to replace the Image("Profile") line with your own image (maybe from Assets or from base64 data with UIImage).
HStack {
Text("Apps")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
.padding(.all, 30)
This products following result:
With NavigationView
Let's assume that you have NavigationView and inside that there's only ScrollView and .navigationTitle. You can add that profile image there by using overlay.
NavigationView {
ScrollView {
//your content here
}
.overlay(
ProfileView()
.padding(.trailing, 20)
.offset(x: 0, y: -50)
, alignment: .topTrailing)
.navigationTitle(Text("Apps"))
}
Where ProfileView could be something like this:
struct ProfileView: View {
var body: some View {
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
}
The result will be like this...
...which is pretty close to the App Store:
I can't figure out how to change the width of Picker() in SwiftUI 2, iOS 14. Is this a bug or am I missing out something? Seems like the width modifier only gets applied onto the Text but not onto the grey bar.
struct ContentView: View {
#State private var position = 0
var body: some View {
VStack {
Spacer()
Picker(selection: self.$position, label: Text("")){
ForEach(0..<50){ i in
Text("\(i)")
}
}
.frame(width: 10, height: 50, alignment: .center)
Spacer()
}
}
}
The width: 10 is too small... anyway, use .clipped (or .clipShape) to restrict drawing outside bounds
Picker(selection: self.$position, label: Text("")){
ForEach(0..<50){ i in
Text("\(i)")
}
}
.frame(width: 50, height: 150, alignment: .center)
.clipped() // << here !!
My goal is to make sure Text in a container to scale according to its parent. It works well when the container only contains one Text view, as following:
import SwiftUI
struct FontScalingExperiment: View {
var body: some View {
Text("Hello World ~!")
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.scaledToFill()
)
}
}
struct FontScalingExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
FontScalingExperiment()
.previewLayout(.fixed(width: 100, height: 100))
FontScalingExperiment()
.previewLayout(.fixed(width: 200, height: 200))
FontScalingExperiment()
.previewLayout(.fixed(width: 300, height: 300))
FontScalingExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
the result:
However, when we have more complex View, we cant use same approach to automatically scale the text based on its parent size, for example:
import SwiftUI
struct IndicatorExperiment: View {
var body: some View {
VStack {
HStack {
Text("Line 1")
Spacer()
}
Spacer()
VStack {
Text("Line 2")
Text("Line 3")
}
Spacer()
Text("Line 4")
}
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
}
}
struct IndicatorExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
IndicatorExperiment()
.previewLayout(.fixed(width: 100, height: 100))
IndicatorExperiment()
.previewLayout(.fixed(width: 200, height: 200))
IndicatorExperiment()
.previewLayout(.fixed(width: 300, height: 300))
IndicatorExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
Simply adding these 3 modifiers:
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
wont produce result like the first example; Text enlarged beyond the frame.
I did successfully, produce the result that I want by using GeometryReader then scale the font size based on geometry.size.width. Is this the only approach for achieving the desired result in SwiftUI?
You can try make all the Texts the same height. To do this you will need to set the padding and spacing explicitly, so this will scale rather than the fixed default values.
Also, the Spacer() didn't make much sense here - if the requirement was that all the Text stay the same size, the Spacer would just make all the text small. For Text to scale based on space, and where Spacer tries to use as much space as possible, it's a contradiction. Instead, I decided to just set the VStack's spacing in the initializer.
Working code:
struct IndicatorExperiment: View {
private let size: CGFloat
private let padding: CGFloat
private let primarySpacing: CGFloat
private let secondarySpacing: CGFloat
private let textHeight: CGFloat
init(size: CGFloat) {
self.size = size
padding = size / 10
primarySpacing = size / 15
secondarySpacing = size / 40
let totalHeights = size - padding * 2 - primarySpacing * 2 - secondarySpacing
textHeight = totalHeights / 4
}
var body: some View {
VStack(spacing: primarySpacing) {
HStack {
scaledText("Line 1")
Spacer()
}
.frame(height: textHeight)
VStack(spacing: secondarySpacing) {
scaledText("Line 2")
scaledText("Line 3")
}
.frame(height: textHeight * 2 + secondarySpacing)
scaledText("Line 4")
}
.padding(padding)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
}
private func scaledText(_ content: String) -> some View {
Text(content)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(height: textHeight)
}
}
Code to test with:
struct ContentView: View {
var body: some View {
ScrollView {
VStack(spacing: 50) {
IndicatorExperiment(size: 100)
IndicatorExperiment(size: 200)
IndicatorExperiment(size: 300)
IndicatorExperiment(size: 400)
}
}
}
}
Result:
Using GeometryReader and a .minimumScaleFactor modifier would probably the only way to scale text in a view. To have more control on sizing, one possible way is to provde the .frame size from the parent view.
Scalable Text View
GeometryReader { geo in
Text("Foo")
.font(
.system(size: min(geo.size.height, geo.size.width) * 0.95))
.minimumScaleFactor(0.05)
.lineLimit(1)
}
Parent View that uses the Scalable Text View
GeometryReader { geo in
ScaleableText()
.frame(width: geo.size.width, height: geo.size.height)
}
I wanna eliminate the space above the first Section in a Form
var body: some View {
VStack {
Text("Text 1")
Form {
Section {
Text("Text 2")
}
}
}
}
I tried to set the frame of the Section's header to 0, but it does not work
The solution is to use a Section with an EmptyView() and place the view you want to be at the top in the header of this Section
var body: some View {
VStack {
Text("Text 1")
Form {
Section(header: VStack(alignment: .center, spacing: 0) {
Text("Text 2").padding(.all, 16)
.frame(width: UIScreen.main.bounds.width, alignment: .leading)
.background(Color.white)
}) {
EmptyView()
}.padding([.top], -6)
}
}
}
I'd supply a dummy header view and set its size to be zero:
Section(header: Color.clear
.frame(width: 0, height: 0)
.accessibilityHidden(true)) {
Text("Text 2")
}
The best solution may depend on your requirements. What worked for me was an empty Text view as in:
Section(header: Text("")) {
MySectionView()
}
BTW, I tried EmptyView() but that is ignored completely; as in the space still remains.
This is much simpler:
struct ContentView: View
{
init()
{
UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
}
var body: some View
{
}
}