How to stop SwiftUI view from getting wider than superview - layout

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

Related

Dynamic sort descriptor FetchRequest SwiftUI

I'm having trouble figuring out how to dynamically update Core Data's FetchRequest's NSSortDescriptor in SwiftUI. I'm not sure if setting it in .onAppear is the correct way which it doesn't as I'm seeing a strange rearranging of my list. I'm using an AppStorage variable to store my sorting then I set the NSSortDescriptor .onAppear, and save when state changes. Also I detect any changes in my Picker selection and apply it to my FetchRequest configuration's sort descriptor like Apple describe's here. If I remove the FetchRequest's animation I don't see the strange rearranging but I also don't get any animations which doesn't look nice. I'm really unsure how to solve this or if I'm even doing this right.
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
// Set default sort descriptor as empty because it's set in onAppear
#FetchRequest(
sortDescriptors: [],
animation: .default) // or set to .none
private var items: FetchedResults<Entity>
#AppStorage("sortby") var sortby: SortBy = .title
var body: some View {
NavigationView {
List {
ForEach(items) { item in
Text(item.title)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
ForEach(sort.allCases) { sort in
Button(sort.title, action: {
sortby = sort
})
}
}
}
// When sort changes set the new value here
.onChange(of: sortby, perform: { value in
items.nsSortDescriptors = //NSSortDescriptor here
})
// Set initial sort descriptor here
.onAppear(perform: {
items.nsSortDescriptors = //NSSortDescriptor here
})
}
}
}
FetchRequest's animation is set to default and rearranging.
FetchRequest's animation is set to none, it works but then no animations become available when sorting.

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.

In SwiftUI Picker expands to occupy entire space inside a HStack

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

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