How to use .firstTextBaseline alignment with a view that contains GeometryReader - swift

I have two views in a HStack - the left one is a simple Text view.
For the right view, i'm experimenting with using GeometryReader. I cannot get the baselines of the text aligned when I use geometry reader, but I can when I don't use it.
Here is some code:
struct CompositeView : View {
var body : some View {
VStack {
Button(action: {
}) {
Text("Another text")
.padding(5)
.overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.blue, lineWidth: 2.0))
}
}
}
}
struct GeometryReaderTest : View {
var tags: [String]
init(tags : [String]) {
self.tags = tags
}
#State private var viewHeight = CGFloat.zero
var body: some View {
VStack {
GeometryReader { geometry in
ZStack {
ForEach(self.tags, id: \.self) { tag in
Button(action: {}) {
Text(tag)
.padding(5)
.overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.blue, lineWidth: 2.0))
}
}
}
.background(getHeight(self.$viewHeight))
}
}
.frame(height: viewHeight)
}
/// Get the height of the Geometry view
///
private func getHeight(_ height: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
height.wrappedValue = rect.size.height
}
return .clear
}
}
}
struct MyTestView : View {
var body : some View {
VStack {
HStack (alignment: .firstTextBaseline) {
ZStack {
Text("Hello")
}
GeometryReaderTest(tags: ["One"])
}
HStack (alignment: .firstTextBaseline) {
ZStack {
Text("Hello")
}
CompositeView()
}
}
}
}
Here is the outcome:
As you can see, the baseline alignment doesn't work when the right hand view is a GeometryReader, but it does work when the right hand view is a simple button.
Does anyone know how I can get the baselines to line up?
I'm using GeometryReader because my right-view will eventually be more complex - I'm reducing the issue I'm facing to as small a repro as possible.
Thank you!

Related

ScrollView stops components from expanding

I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
Here is my code:
import SwiftUI
struct DisciplineView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
Card(cardTitle: "Notes")
Card(cardTitle: "Planner")
Card(cardTitle: "Homeworks / Exams")
}
.ignoresSafeArea()
}
}
}
struct DisciplineV_Previews: PreviewProvider {
static var previews: some View {
DisciplineView()
}
}
import SwiftUI
struct Card: View {
#State var cardTitle = ""
#State private var isTapped = false
var body: some View {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.foregroundColor(.gray.opacity(0.2))
.frame(width: .infinity, height: isTapped ? .infinity : 50)
.background(
VStack {
cardInfo
if(isTapped) { Spacer() }
}
.padding(isTapped ? 10 : 0)
)
}
var cardInfo: some View {
HStack {
Text(cardTitle)
.font(.title).bold()
.foregroundColor(isTapped ? .white : .black)
.padding(.leading, 10)
Spacer()
Image(systemName: isTapped ? "arrowtriangle.up.square.fill" : "arrowtriangle.down.square.fill")
.padding(.trailing, 10)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}
}
struct Card_Previews: PreviewProvider {
static var previews: some View {
Card()
}
}
here is almost the same as I would like to have, but I would like the first one to be on the whole screen and stop the ScrollView while appearing.
Thank you!
Described above:
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
I think this is pretty much what you are trying to achieve.
Basically, you have to scroll to the position of the recently presented view and disable the scroll. The scroll have to be disabled enough time to avoid continuing to the next item but at the same time, it have to be enabled soon enough to give the user the feeling that it is scrolling one item at once.
struct ContentView: View {
#State private var canScroll = true
#State private var itemInScreen = -1
var body: some View {
GeometryReader { geo in
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(0...10, id: \.self) { item in
Text("\(item)")
.onAppear {
withAnimation {
proxy.scrollTo(item)
canScroll = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
canScroll = true
}
}
}
}
.frame(width: geo.size.width, height: geo.size.height)
.background(Color.blue)
}
}
}
.disabled(!canScroll)
}
.ignoresSafeArea()
}
}

How to have a custom alignment guide permeate a ScrollView in SwiftUI

I want to be able to align things in a ZStack. Some of these things are in a ScrollView in the ZStack, and some are not. I can get the things that are in the ScrollView to align with each other, and things outside the ScrollView to align with each other, but I cannot get them to all align. I can do if I just remove the ScrollView, so for instance:
extension VerticalAlignment {
enum MidAccountAndName: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[.top]
}
}
static let midAccountAndName = VerticalAlignment(MidAccountAndName.self)
}
struct ContentView: View {
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .midAccountAndName)) {
Rectangle()
.alignmentGuide(.midAccountAndName) { d in
d[VerticalAlignment.center]
}
.frame(width: 1000, height: 2)
HStack(alignment: .midAccountAndName) {
VStack {
Text("Left-hand text")
.alignmentGuide(.midAccountAndName) { d in
d[VerticalAlignment.center]
}
Text("More text")
}
VStack {
Text("Right-hand text")
Text("Title")
.alignmentGuide(.midAccountAndName) { d in
d[VerticalAlignment.center]
}
.font(.largeTitle)
}
}
}
}
}
Results in this, which is what I want:
But this code:
extension VerticalAlignment {
enum MidAccountAndName: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[.top]
}
}
static let midAccountAndName = VerticalAlignment(MidAccountAndName.self)
}
struct ContentView: View {
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .midAccountAndName)) {
Rectangle()
.alignmentGuide(.midAccountAndName) { d in
d[VerticalAlignment.center]
}
.frame(width: 1000, height: 2)
ScrollView {
HStack(alignment: .midAccountAndName) {
VStack {
Text("Left-hand text")
.alignmentGuide(.midAccountAndName) { d in
d[VerticalAlignment.center]
}
Text("More text")
}
VStack {
Text("Right-hand text")
Text("Title")
.alignmentGuide(.midAccountAndName) { d in
d[VerticalAlignment.center]
}
.font(.largeTitle)
}
}
}
}
}
}
Results in this, which is undesirable:
Can anyone help me get the former behaviour working in concert with the ScrollView?
How about this?
struct ContentView: View {
let largeFont = Font.largeTitle
let smallFont = Font.body
var body: some View {
HStack {
HStack {
VStack {
Text("|")
.font(largeFont)
.frame(width: 0)
Text("Left-hand text")
Text("More Text")
}
.font(smallFont)
}
HStack {
VStack {
Text("Right-hand text")
HStack {
Text("Title")
.font(largeFont)
}
}
.font(smallFont)
}
}
}
}
The trick is to use a placeholder at width: 0 on the other side of the HStack in your VStacks. I made the font variables so that you could change the relative sizes without accidentally messing up the alignment. There is usually a simple way in SwiftUI to achieve your layout, and let the OS do the work for you.

How to animate navigationBarHidden in SwiftUI?

struct ContentView: View {
#State var hideNavigationBar: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
Rectangle().fill(Color.red).frame(height: 50)
.onTapGesture(count: 1, perform: {
withAnimation {
self.hideNavigationBar.toggle()
}
})
VStack {
ForEach(1..<50) { index in
HStack {
Text("Sample Text")
Spacer()
}
}
}
}
}
.navigationBarTitle("Browse")
.navigationBarHidden(hideNavigationBar)
}
}
}
When you tap the red rectangle it snaps the navigation bar away. I thought withAnimation{} would fix this, but it doesn't. In UIKit you would do something like this navigationController?.setNavigationBarHidden(true, animated: true).
Tested in xCode 12 beta 6 and xCode 11.7
You could try using
.navigationBarHidden(hideNavigationBar).animation(.linear(duration: 0.5)) instead of .navigationBarHidden(hideNavigationBar)
and also move self.hideNavigationBar.toggle() out of the animation block. That is not required if you use the above approach for hiding of navigation bar with animation.
I think, the only solution is to use a position function in SwiftUI 2
var body: some View {
GeometryReader { geometry in
NavigationView {
ZStack {
Color("background")
.ignoresSafeArea()
// ContentView
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: logo, trailing: barButtonItems)
.toolbar {
ToolbarItem(placement: .principal) {
SearchBarButton(placeholder: LocalizedStringKey("home_vc.search_bar.placeholder"))
.opacity(isNavigationBarHidden ? 0 : 1)
.animation(.easeInOut(duration: data.duration))
}
}
}
.frame(height: geometry.size.height + (isNavigationBarHidden ? 70 : 0))
// This is the key ⬇
.position(x: geometry.size.width/2, y: geometry.size.height/2 - (isNavigationBarHidden ? 35 : 0))
.animation(.easeInOut(duration: 0.38))
.onTapGesture {
isNavigationBarHidden.toggle()
}
}
}
I'm still learning animation in SwiftUI but at this stage, I understand that you must animate the parent view.
So your code would become...
struct ContentView: View {
#State var hideNavigationBar: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
Rectangle().fill(Color.red).frame(height: 50)
.onTapGesture(count: 1) {
self.hideNavigationBar.toggle()
}
VStack {
ForEach(1..<50) { index in
HStack {
Text("Sample Text")
Spacer()
}
}
}
}
}
.navigationBarTitle("Browse")
.navigationBarHidden(hideNavigationBar)
.animation(.spring()) // for example
}
}
}
Note that the last argument in any function call can be placed into a single closure.
So...
.onTapGesture(count: 1, perform: {
self.hideNavigationBar.toggle()
})
can become...
.onTapGesture(count: 1) {
self.hideNavigationBar.toggle()
}
Simpler syntax in my humble opinion.

How to animate view appearance?

I have a custom view. It used as a cell in a List view. I would like to animate appearance of a Group subview on quote.expanded = true (e.g. fading).
.animation(.default) modifier does not work.
struct QuoteView: View {
var quote : QuoteDataModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(quote.latin)
.font(.title)
if quote.expanded {
Group() {
Divider()
Text(quote.russian).font(.body)
}
}
}
}
}
The following code animates for me. Note that animation inside a list, while still probably better than no animation, can still look kind of weird. This is because the height of the list rows themselves do not animate, and snap to their final height, while the view inside the row does animate. This is a SwiftUI issue, and there's not anything you can do about it for now other than file feedback that this behavior doesn't look great.
struct StackOverflowTests: View {
#State private var array = [QuoteDataModel(), QuoteDataModel(), QuoteDataModel()]
var body: some View {
List {
ForEach(array.indices, id: \.self) { index in
QuoteView(quote: self.array[index])
.onTapGesture { self.array[index].expanded.toggle() }
}
}
}
}
struct QuoteView: View {
var quote : QuoteDataModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(quote.latin)
.font(.title)
if quote.expanded {
Group() {
Divider()
Text(quote.russian).font(.body)
}
}
}
.animation(.default)
}
}
Use a Transition to animate the view Appearance:
https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-and-remove-views-with-a-transition
try this....but you will see, that there are still other problems, because the text is left aligned...
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Button("Tap me") {
withAnimation() {
self.expanded.toggle()
if self.expanded {
self.opacity = 1
} else {
self.opacity = 0
}
}
}
Text("aha")
.font(.title)
if expanded {
Group() {
Divider()
Text("oho").font(.body)
}.opacity(opacity)
}
}
}

Views compressed by other views in SwiftUI VStack and List

In my SwiftUI application, I'm trying to implement a UI similar to this:
I've added the two rows for category 1 and category 2. The result looks like this:
NavigationView {
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
Spacer()
}
.navigationBarTitle(Text("Featured"))
}
Now, when added the view for the third category – an VStack with images – the following happens:
This happened, after I replaced Spacer(), with said VStack:
VStack(alignment: .leading) {
Text("Rivers")
.font(.headline)
ForEach(self.categories["Rivers"]!.identified(by: \.self)) { landmark in
landmark.image(forSize: 200)
}
}
My CategoryRow is implemented as follows:
VStack(alignment: .leading) {
Text(title)
.font(.headline)
ScrollView {
HStack {
ForEach(landmarks) { landmark in
CategoryItem(landmark: landmark, isRounded: self.isRounded)
}
}
}
}
Question
It seems that the views are compressed. I was not able to find any compression resistance or content hugging priority modifiers to fix this.
I also tried to use .fixedSize() and .frame(width:height:) on CategoryRow.
How can I prevent the compression of these views?
Update
I've tried embedding the whole outer stack view in a scroll view:
NavigationView {
ScrollView { // also tried List
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
ForEach(...) { landmark in
landmark.image(forSize: 200)
}
}
.navigationBarTitle(Text("Featured"))
}
}
...and the result is worse:
You might prevent the views in VStack from being compressed by using
.fixedSize(horizontal: false, vertical: true)
For example:
I have the following VStack:
VStack(alignment: .leading){
ForEach(group.items) {
FeedCell(item: $0)
}
}
Which render compressed Text()
When I add .fixedSize(horizontal: false, vertical: true)
it doesn't compress anymore
VStack(alignment: .leading){
ForEach(group.items) {
FeedCell(item: $0)
.fixedSize(horizontal: false, vertical: true)
}
}
You could try to add a layoutPriority()operator to your first VStack. This is what the documentation says about the method:
In a group of sibling views, raising a view’s layout priority encourages that view to shrink later when the group is shrunk and stretch sooner when the group is stretched.
So it's a bit like the content compression resistance priority in Autolayout. But the default value here is 0, so you just have to set it to 1 to get the desired effect, like this:
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
Spacer()
}.layoutPriority(1)
VStack(alignment: .leading) {
...
}
Hope it works!
It looks like is not enough space for all your views in VStack, and it compresses some of them. You can embed it into the ScrollView
NavigationView {
ScrollView {
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
/// you images and so on
}
}
}
struct ContentView1: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
CategoryListView {
CategoryView()
}
CategoryListView {
SquareCategoryView()
}
CategoryListView {
RectangleCategoryView()
}
}
.padding()
}
.navigationTitle("Featured")
}
}
}
struct CategoryListView<Content>: View where Content: View {
private let viewSize: CGFloat = 150
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
HStack {
Text("Category name")
Spacer()
}
ScrollView(.horizontal, showsIndicators: false){
HStack {
ForEach(0..<10) { _ in
content()
}
}
}
}
}
}
struct ContentView1_Previews: PreviewProvider {
static var previews: some View {
ContentView1()
}
}
struct CategoryView: View {
private let viewSize: CGFloat = 150
var body: some View {
Circle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize)
}
}
struct RectangleCategoryView: View {
private let viewSize: CGFloat = 350
var body: some View {
Rectangle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize * 9 / 16)
}
}
struct SquareCategoryView: View {
private let viewSize: CGFloat = 150
var body: some View {
Rectangle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize)
}
}
I think your topmost view (in the NavigationView) needs to be a List, so that it is scrollable:
NavigationView {
List {
...
Or use a ScrollView.
A stack automatically fits within a screen. If you want your content to exceed this, you would have used a ScrollView or a TableView etc i UIKit
EDIT:
Actually, a little Googling brought this result, which seems to be exactly what you are making:
https://developer.apple.com/tutorials/swiftui/composing-complex-interfaces