Ignore horizontal safe area with vertical ScrollView - swiftui

I am trying to create a layout with multiple horizontal ScrollViews inside a vertical ScrollView, similar to the template picker in Apple's Pages app. I would like the content of the horizontal ScrollViews to be visible beyond the safe area. However I seem to be unable to get the content of the vertical ScrollView outside the horizontal safe insets. This is visible when iPhones with a notch are used in landscape orientation.
I have tried adding negative padding to the content of the vertical ScrollView. This kind of works, but creates issues when using the device in portrait mode.
Below example code shows the issue. I would expect the rectangles to be visible in beyond the safe area when scrolling horizontally, but they get clipped. How can I make them visible beyond the safe area?
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
ScrollView(.horizontal) {
HStack {
Rectangle()
.frame(width: 200, height: 300)
Rectangle()
.frame(width: 200, height: 300)
Rectangle()
.frame(width: 200, height: 300)
}
} .edgesIgnoringSafeArea(.horizontal)
} .edgesIgnoringSafeArea(.horizontal)
}
}

You can detect when the device's orientation changes and adapt your view:
struct ContentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
Group {
if verticalSizeClass == .compact {
content.edgesIgnoringSafeArea(.horizontal)
} else {
content
}
}
}
var content: some View {
ScrollView(.vertical) {
ScrollView(.horizontal) {
HStack {
Rectangle()
.frame(width: 200, height: 300)
Rectangle()
.frame(width: 200, height: 300)
Rectangle()
.frame(width: 200, height: 300)
}
}
}
.edgesIgnoringSafeArea(.horizontal)
}
}

Related

Swiftui vertical paging tabview with scrollview inside

My goal is to have a vertical paginated tabview with a scrollview inside. Scrolling as soon as you finish the scrollview you pass to the other tab and if the content of the scrollview has a lower height than the screen, scrolling passes directly to the next tab.
var body: some View {
GeometryReader { proxy in
TabView {
ForEach(colors, id: \.self) { color in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .bottom, spacing: 20) {
ForEach(0..<15) { i in
//GeometryReader { block in
VStack(alignment: .leading) {
Text("Block test test test test test test \(i)")
}
.rotationEffect(.degrees(-90))
.frame(width: 70, height: proxy.size.width, alignment: .leading)
.background(Color.green)
.id(i)
//}
//.offset(y: proxy.size.width / 2)
}
}
.frame(height: proxy.size.height)
.background(Color.blue)
}
.frame(width: proxy.size.height, height: proxy.size.width)
.background(Color.pink)
}
.frame(width: proxy.size.width,height: proxy.size.height)
}
.frame( width: proxy.size.height, height: proxy.size.width)
.rotationEffect(.degrees(90), anchor: .topLeading)
.offset(x: proxy.size.width)
.tabViewStyle(
PageTabViewStyle(indexDisplayMode: .never)
)
}
}
These are the steps I followed:
created a tabview with horizontal scrolls inside
Rotated the tabview by 90°
Rotated the Vstacks inside the scrollview by -90°
The result is exact and the scrolling of the contents is continuous passing smoothly between scroll and tab, but the only problem is that I can't control the dimensions of the Vstacks inside the scrollview and therefore I can't have Vstacks with different heights in based on the content.
I tried to add a GeometryReader { block for the VStacks but besides not giving me the correct measurements of the VStacks it breaks the layout completely.
How can I get the dimensions of each Vstack correctly?

How to make a SwiftUI view "Always on top"?

I'm new to the XCode/SwiftUI scene and have a beginners question.
How can I tell SwiftUI to make a view "Always on top"?
I have a view with two buttons in it:
import SwiftUI
struct ContentView: View {
var body: some View {
HStack{
Button("Button1") {}
.padding(20)
.frame(width: 150, height: 40)
Button("x") {
NSApplication.shared.terminate(nil)
}
.padding(10)
.frame(width: 70, height: 40)
}
}
}
Thanks!
First, here is the code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack { // I put everything in this VStack (vertical stack)
HStack{
Button("Button1") {}
.padding(20)
.frame(width: 150, height: 40)
Button("x") {
//
}
.padding(10)
.frame(width: 70, height: 40)
}
Spacer() // I added this spacer at the bottom to push everything to the top
}
}
}
Explanation
What is a VStack?
A VStack is a view that arranges its children in a vertical line.
What is a Spacer?
Spacer views automatically fill up all available space on their axis of expansion, which is a fancy way of saying they take up as much space as they can either horizontally or vertically, depending on what you put them in.
So basically:
When we put the HStack (horizontal stack) inside the VStack and add the Spacer() function at the bottom we push the HStack all the way to the top of the screen.

In SwiftUI, how to fill Path with text color on top of a material?

I'm drawing icons on a toolbar with a material background. The Text and symbol Images are white, but if I draw my own Path, it's gray.
struct ContentView: View {
var body: some View {
HStack {
Text("Hi")
Image(systemName: "square.and.arrow.up.fill")
Path { p in
p.addRect(CGRect(origin: .zero, size: .init(width: 20, height: 30)))
}.fill()
.frame(width: 20, height: 30)
}
.padding()
.background(.regularMaterial)
}
}
I get the same result with .fill(), .fill(.foreground), or .fill(.primary).
Why is it gray? How do I get it to match the white text color?
I find it weird that .white or .black work, but .primary doesn't.
Upon discovering the Material documentation, I found this interesting snippet:
When you add a material, foreground elements exhibit vibrancy, a context-specific blend of the foreground and background colors that improves contrast. However using foregroundStyle(_:) to set a custom foreground style — excluding the hierarchical styles, like secondary — disables vibrancy.
Seems like you have to force a different color (see previous edit which I used the environment color scheme), since hierarchical styles such as .primary won't work by design.
Luckily there is a way around this - you can use colorMultiply to fix this problem. If you set the rectangle to be .white, then the color multiply will make it the .primary color.
Example:
struct ContentView: View {
var body: some View {
HStack {
Text("Hi")
Image(systemName: "square.and.arrow.up.fill")
Path { p in
p.addRect(CGRect(origin: .zero, size: .init(width: 20, height: 30)))
}
.foregroundColor(.white)
.colorMultiply(.primary)
.frame(width: 20, height: 30)
}
.padding()
.background(.regularMaterial)
}
}
There is no issue with code, your usage or expecting is not correct! Text and Image in that code has default Color.primary with zero code! So this is you, that messing with .fill() you can delete that one!
struct ContentView: View {
var body: some View {
HStack {
Text("Hi")
Image(systemName: "square.and.arrow.up.fill")
Path { p in
p.addRect(CGRect(origin: .zero, size: .init(width: 20, height: 30)))
}
.fill(Color.primary) // You can delete this line of code as well! No issue!
.frame(width: 20, height: 30)
}
.padding()
.background(Color.secondary.cornerRadius(5))
}
}

Dark colors in SwiftUi Mac

Starting a new Mac App project from scratch and assigning Color.red and Color.green to two Rectangles results in the following:
Code:
struct ContentView: View {
var body: some View {
HStack {
Rectangle()
.frame(width: 200, height: 200)
.background(Color.green)
Spacer()
Rectangle()
.frame(width: 200, height: 200)
.background(Color.red)
}
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Where do those very dark colors come from? How to have the interface use "normal" colors? I don't have dark mode activated and I'm on Big Sur.
You need to use fill or foregroundColor instead of background:
struct ContentView: View {
var body: some View {
HStack {
Rectangle()
.fill(Color.green) // first possibility: `fill`
.frame(width: 200, height: 200)
Spacer()
Rectangle()
.foregroundColor(Color.red) // second possibility: `foregroundColor`
.frame(width: 200, height: 200)
}
}
}
It's correct. To fill a shape use .fill(Color.green).

SwiftUI: How to force a specific view in an HStack to be in the centre

Given an HStack like the following:
HStack{
Text("View1")
Text("Centre")
Text("View2")
Text("View3")
}
How can I force the 'Centre' view to be in the centre?
Here is possible simple approach. Tested with Xcode 11.4 / iOS 13.4
struct DemoHStackOneInCenter: View {
var body: some View {
HStack{
Spacer().overlay(Text("View1"))
Text("Centre")
Spacer().overlay(
HStack {
Text("View2")
Text("View3")
}
)
}
}
}
The solution with additional alignments for left/right side views was provided in Position view relative to a another centered view
the answer takes a handful of steps
wrap the HStack in a VStack. The VStack gets to control the
horizontal alignment of it's children
Apply a custom alignment guide to the VStack
Create a subview of the VStack which takes the full width. Pin the custom alignment guide to the centre of this view. (This pins the alignment guide to the centre of the VStack)
align the centre of the 'Centre' view to the alignment guide
For the view which has to fill the VStack, I use a Geometry Reader. This automatically expands to take the size of the parent without otherwise disturbing the layout.
import SwiftUI
//Custom Alignment Guide
extension HorizontalAlignment {
enum SubCenter: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let subCentre = HorizontalAlignment(SubCenter.self)
}
struct CentreSubviewOfHStack: View {
var body: some View {
//VStack Alignment set to the custom alignment
VStack(alignment: .subCentre) {
HStack{
Text("View1")
//Centre view aligned
Text("Centre")
.alignmentGuide(.subCentre) { d in d.width/2 }
Text("View2")
Text("View3")
}
//Geometry reader automatically fills the parent
//this is aligned with the custom guide
GeometryReader { geometry in
EmptyView()
}
.alignmentGuide(.subCentre) { d in d.width/2 }
}
}
}
struct CentreSubviewOfHStack_Previews: PreviewProvider {
static var previews: some View {
CentreSubviewOfHStack()
.previewLayout(CGSize.init(x: 250, y: 100))
}
}
Edit: Note - this answer assumes that you can set a fixed height and width of the containing VStack. That stops the GeometryReader from 'pushing' too far out
In a different situation, I replaced the GeometryReader with a rectangle:
//rectangle fills the width, then provides a centre for things to align to
Rectangle()
.frame(height:0)
.frame(idealWidth:.infinity)
.alignmentGuide(.colonCentre) { d in d.width/2 }
Note - this will still expand to maximum width unless constrained!
Asperis answer is already pretty interesting and inspired me for following approach:
Instead of using Spacers with overlays, you could use containers left and right next to the to-be-centered element with their width set to .infinity to stretch them out just like Spacers would.
HStack {
// Fills the left side
VStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 120, height: 200)
}.frame(maxWidth: .infinity)
// Centered
Rectangle()
.foregroundColor(Color.red)
.frame(width: 50, height: 150)
// Fills the right side
VStack {
HStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 25, height: 100)
Rectangle()
.foregroundColor(Color.red)
.frame(width: 25, height: 100)
}
}.frame(maxWidth: .infinity)
}.border(Color.green, width: 3)
I've put it in a ZStack to overlay a centered Text for demonstration:
Using containers has the advantage, that the height would also translates to the parent to size it up if the left/right section is higher than the centered one (demonstrated in screenshot).