In SwiftUI Picker expands to occupy entire space inside a HStack - text

I have a Text and Picker in HStack separated by Spacer. What I want is that the Spacer should should occupy maximum space between picker and text so that the Text and Picker are not the extreme ends of the HStack but, the picker expands to occupy maximum space. Any clue as to how can this can achieved and that the size of Picker is equivalent to its content?
HStack() {
Text(template.name)
.multilineTextAlignment(.leading)
Spacer()
if template.oneItemOnly == "1" {
if let items = template.items {
Picker(selection: $selectedSegment, label:Text("")){
ForEach( 0..<items.count) { index in
Text(items[index].name)
.tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}

You can set max width of Spacer by adding a .frame modifier:
Spacer().frame(maxWidth: .infinity)
Alternatively you can try using GeometryReader to set the width of the Picker relative to the screen width:
GeometryReader { proxy in
...
Picker(selection: $selectedSegment, label: EmptyView()) {
...
}
.frame(width: proxy.size.width / 3)
}
You may also try to implement your own Picker like presented here: Custom Segmented Picker

Related

Make TextEditor dynamic height SwiftUI

I'm trying to create a growing TextEditor as input for a chat view.
The goal is to have a box which expands until 6 lines are reached for example. After that it should be scrollable.
I already managed to do this with strings, which contain line breaks \n.
TextEditor(text: $composedMessage)
.onChange(of: self.composedMessage, perform: { value in
withAnimation(.easeInOut(duration: 0.1), {
if (value.numberOfLines() < 6) {
height = startHeight + CGFloat((value.numberOfLines() * 20))
}
if value.numberOfLines() == 0 || value.isEmpty {
height = 50
}
})
})
I created a string extension which returns the number of line breaks by calling string.numberOfLines() var startHeight: CGFloat = 50
The problem: If I paste a text which contains a really long text, it's not expanding when this string has no line breaks. The text get's broken in the TextEditor.
How can I count the number of breaks the TextEditor makes and put a new line character at that position?
Here's a solution adapted from question and answer,
struct ChatView: View {
#State var text: String = ""
// initial height
#State var height: CGFloat = 30
var body: some View {
ZStack(alignment: .topLeading) {
Text("Placeholder")
.foregroundColor(.appLightGray)
.font(Font.custom(CustomFont.sofiaProMedium, size: 13.5))
.padding(.horizontal, 4)
.padding(.vertical, 9)
.opacity(text.isEmpty ? 1 : 0)
TextEditor(text: $text)
.foregroundColor(.appBlack)
.font(Font.custom(CustomFont.sofiaProMedium, size: 14))
.frame(height: height)
.opacity(text.isEmpty ? 0.25 : 1)
.onChange(of: self.text, perform: { value in
withAnimation(.easeInOut(duration: 0.1), {
if (value.numberOfLines() < 6) {
// new height
height = 120
}
if value.numberOfLines() == 0 || value.isEmpty {
// initial height
height = 30
}
})
})
}
.padding(4)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.appLightGray, lineWidth: 0.5)
)
}
}
And the extension,
extension String {
func numberOfLines() -> Int {
return self.numberOfOccurrencesOf(string: "\n") + 1
}
func numberOfOccurrencesOf(string: String) -> Int {
return self.components(separatedBy:string).count - 1
}
}
I found a solution!
For everyone else trying to solve this:
I added a Text with the same width of the input field and then used a GeometryReader to calculate the height of the Text which automatically wraps. Then if you divide the height by the font size you get the number of lines.
You can make the text field hidden (tested in iOS 14 and iOS 15 beta 3)
If you're looking for an iOS 15 solution, I spent a while and figured it out. I didn't want to have to resort to UIKit or ZStacks with overlays or duplicative content as a "hack". I wanted it to be pure SwiftUI.
I ended up creating a separate struct that I could reuse anywhere I needed it, as well as add additional parameters in my various views.
Here's the struct:
struct FieldMultiEntryTextDynamic: View {
var text: Binding<String>
var body: some View {
TextEditor(text: text)
.padding(.vertical, -8)
.padding(.horizontal, -4)
.frame(minHeight: 0, maxHeight: 300)
.font(.custom("HelveticaNeue", size: 17, relativeTo: .headline))
.foregroundColor(.primary)
.dynamicTypeSize(.medium ... .xxLarge)
.fixedSize(horizontal: false, vertical: true)
} // End Var Body
} // End Struct
The cool thing about this is that you can have placeholder text via an if statement and it supports dynamic type sizes.
You can implement it as follows:
struct MyView: View {
#FocusState private var isFocused: Bool
#State private var myName: String = ""
var body: some View {
HStack(alignment: .top) {
Text("Name:")
ZStack(alignment: .trailing) {
if myName.isEmpty && !isFocused {
Text("Type Your Name")
.font(.custom("HelveticaNeue", size: 17, relativeTo: .headline))
.foregroundColor(.secondary)
}
HStack {
VStack(alignment: .trailing, spacing: 5) {
FieldMultiEntryTextDynamic(text: $myName)
.multilineTextAlignment(.trailing)
.keyboardType(.alphabet)
.focused($isFocused)
}
}
}
}
.padding()
.background(.blue)
}
}
Hope it helps!

Determine actual height of a SpriteKit SKScene inside a SwiftUI View

I have a SwiftUI View with a VStack of 3 SpriteKit SKScenes inside.
While scoreView and accelerometerView have a fixed height, gameScene does not.
It takes the remaining space and depends somehow on the device.
struct ContentView: View {
#StateObject private var gameScene = GameSKScene()
#StateObject private var scoreView = ScoreSKScene()
#StateObject private var accelerometerView = AccelerometerSKScene()
var body: some View {
VStack {
SpriteView(scene: scoreView)
.frame(height: 50)
SpriteView(scene: gameScene)
SpriteView(scene: accelerometerView)
.frame(height: 50)
}.ignoresSafeArea()
}
}
Now inside gameScene have problems setting up the scene because the scene itself has no information of its size.
I can get the width with UIScreen.main.bounds.width but how do I get the actual height?
If I understand you correctly, you want the gameScene SpriteView to fill the remaining space after accounting for the other two scenes. You can use GeometryReader (documentation here).
Example usage:
var body: some View {
GeometryReader { geo in
VStack {
SpriteView(scene: scoreView)
.frame(height: 50)
SpriteView(scene: gameScene)
.frame(height: geo.size.height - 100)
SpriteView(scene: accelerometerView)
.frame(height: 50)
}
}
}

SwiftUI: height and spacing of Views in GeometryReader within a ScrollView

ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
Text("Hello There")
.font(.largeTitle)
}
}
}
The above code results in this:
When we add a GeometryReader, the views lose their sizing and spacing from one another (the .position modifier is just to center the text views within the GeometryReader):
ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
GeometryReader { geometry in
Text("Hello There")
.font(.largeTitle)
.position(x: UIScreen.main.bounds.size.width/2)
}
}
}
}
Can anybody share some help/advice around why this is the behavior and what I may want to look into in order to use a GeometryReader on center aligned views within a ScrollView as I'm trying to do here?
Adding a .frame(minHeight: 40) modifier to the ForEach is one way to force the views to take the space that the Text needs, but that is not a very nice or scalable solution.
Thank you!
If you need just width of screen then place GeometryReader outside of scrollview:
GeometryReader { geometry in // << here !!
ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
Text("Hello There")
.font(.largeTitle)
.position(x: UIScreen.main.bounds.size.width/2)
}
}
}
}
Can anybody share some help/advice around why this is the behavior
ScrollView does not have own size so GeometryReader cannot read anything from it and provides for own children some minimal dimension.

How to stop SwiftUI view from getting wider than superview

I created a UIHostingController to put a SwiftUI view in my app. The root view contains a VStack, and inside that some HStacks and Pickers. The picker pushes the view wider than the screen by 8 points. It looks bad - text is slightly off screen. Usually I would fix this with auto layout constraints that pin the left and right edges of the view to the superview, with priority 1000 (required). What is the equivalent in SwiftUI?
Some boiled down code that triggers the problem
struct EditSegmentView: View {
#State var seconds: Int = 10
var body: some View {
VStack {
HStack {
Text("Pause after intro")
Spacer()
Text("10 seconds")
}
Picker(selection: $seconds, label: Text("Seconds")) {
ForEach(0..<60) { n in
Text("\(n) sec").tag(n)
}
}
}
}
}
That is displayed from a UIViewController, like this:
let editView = EditSegmentView()
let vc = UIHostingController(rootView: editView)
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)
I can't see anything obviously wrong with the code provided. You could try using a GeometryReader to force the VStack to be the width of the available space. Also add some padding(), because otherwise your Text will start right at the edge
var body: some View {
GeometryReader { proxy in
VStack {
//...
} .padding()
.frame(width: proxy.size.width)
}
}
Are there any clues as to why the view is expanding beyond the edges when you debug its view hierarchy?
This is a little late, but I had the same issue and fixed by making sure the outer VStack had it's alignment set to .leading. The inner Vstack also has alignment: leading and then added a .padding() to the inner VStack

SwiftUI - Difference between Spacer() and Color.clear?

Pretty simple question - I'm just curious if there is any perceivable difference between Spacer() and Color.clear in SwiftUI
If you measure the screen, then yes.
I notice a ~2px difference when using both as in this example:
struct ContentView: View {
var body: some View {
HStack {
Spacer()
Text("First")
Spacer()
Text("Second")
Spacer()
}
}
}
Which generates:
(the middle space takes around 84px)
Now using Color.clear:
struct ContentView: View {
var body: some View {
HStack {
Color.clear
Text("First")
Color.clear
Text("Second")
Color.clear
}
}
}
Output:
Notice that the "First" and "Second" strings don't touch the vertical guide anymore...
(the middle "space" now takes around 86px)
Not a huge difference, but I would stick to Spacer.
(xScope is our friend)

Resources