matchedGeometryEffect doesn't work with PageTabView - swiftui

#Namespace var ns
var body: some View {
ZStack {
Color(.black)
TabView {
ForEach(0..<5) { i in
Text("item \(i)")
.foregroundColor(.white)
.matchedGeometryEffect(id: i, in: ns)
.frame(maxWidth: .infinity)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
}
}
When I tried to give each TabView item an effect id, it complains Multiple inserted views in matched geometry group Pair<Int, ID>(first: 0, second: SwiftUI.Namespace.ID(id: 180)) have isSource: true, results are undefined.
What's the correct way to assign a matchedGeometryEffect id to items in PageTabView so that I can zoom in and out with transition like the iOS photos app?
Or is it a bug of SwiftUI?

Related

Display View on top off bottom sheet

I'm trying to implement a view that displays error message for my whole app.
I want this view to always be above every other view, but I also use sheets in my app and in that case the error message is hidden behind the sheet, since the sheet is displayed above every other view.
Here is a View to reproduce my situation:
struct AppView: View {
#State var isPresentingSheet = false
var body: some View {
ZStack {
VStack {
Button("Toggle sheet") {
isPresentingSheet.toggle()
}
}
.sheet(isPresented: $isPresentingSheet) {
Text("Im above everything else")
}
VStack {
HStack {
Image(systemName: "xclose")
Text("I want to be even above the sheet")
}
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(Padding.l)
.background(Color.red.opacity(0.2))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.red),
alignment: .bottom
)
Spacer()
}
}
}
}
I want to know if it's possible to display a view above a sheet, but to me it looks like the sheet is in a completely different window?
But maybe it's possible to create a custom sheet that moves in from the top and is displayed above other native sheets?
If anybody is interested, I have created a custom bottom sheet with simple controls and snap functionality.
The bottom sheet has a zIndex of 1 so you can easily place views above it with a greater zIndex.
You can find it here: AlternativeSheet.
Here is the code to achieve the view of the image above.
import AlternativeSheet
...
ZStack {
VStack {
Button("Toggle sheet") {
isPresentingSheet.toggle()
}
}
.alternativeSheet(isPresented: $isPresentingSheet, snaps: [0.95]) {
Text("Im above everything else")
}
.isDraggable()
.dampenDrag()
VStack {
HStack {
Image(systemName: "xclose")
Text("I want to be even above the sheet")
}
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(Padding.l)
.background(Color.red.opacity(0.3))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.red),
alignment: .bottom
)
Spacer()
}
}

Some NavigationLink are inaccessible

My SwiftUI TVOS app has two sets of NavigationLink. When both sets are present (not commented out), only one set is accessible to tap on. If I comment out one or the other set, the remaining NavigationLink is accessible to tap on and functions properly.
How can both sets of NavigationLink be accessible (can be interacted with)?
I've tried encapsulating my view in NavigationView and NavigationStack, neither behaved differently.
The view, as shown below, only the NavigationLinks in the ScrollView are accessible to interact with. The "Edit" NavigationLink cannot be selected to tap on. If I comment out the ScrollView NavigationLinks, then the "Edit" NavigationLink becomes accessible and functions correctly.
I've also tried replacing LazyVGrid with VStack to no effect.
import SwiftUI
struct TestSources: Hashable {
let id = UUID()
let name: String
}
struct SourcesView: View {
private var Sources = [TestSources(name: "Computer 1"), TestSources(name: "Computer 2")]
var columns: [GridItem] {
Array(repeating: .init(.adaptive(minimum: 200)), count: 2)
}
var body: some View {
NavigationStack {
VStack(alignment: .center) {
// Header
HStack(alignment: .center){
Label("Sources", systemImage: "externaldrive.connected.to.line.below")
.font(.headline)
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
.padding(.all)
NavigationLink(destination: TestEditView()) {
Text("Edit")
}
}
Divider()
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(Sources.indices, id: \.self) { index in
NavigationLink(Sources[index].name ,value: Sources[index])
}.navigationDestination(for: TestSources.self) { source in
TestShareView(source: source)
}
.accentColor(Color.black)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal)
}
}.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
}
struct TestEditView: View {
var body: some View {
Text("Edit")
}
}
struct TestShareView: View {
let source : TestSources
var body: some View {
Text(source.name)
}
}
I don't see any problem with the navigation links in this code.
I pasted the code into a new project and tweaked it a little to make it compile. As you can see, it just works.
My guess is that it might fail because something outside of this code. Maybe, it is within another NavigationStack or some structure that could increse it's navigation complexity?
Or as Yrb suggests, this force unwrapping could be failing because of null values?

Button Text Out-Of-Sync with Tappable Area

I have a navigation view with tab views inside. The reason for this arrangement is to make the tab views disappear when within the navigation views.
I am trying to place a button at the top right corner of the screen (outside the safe area). If EntryView is called directly, the view correctly places the button ("Save") in the corner without use of offset() or position() view modifiers.
When the tab view is called before EntryView, the Save button appears an inch or so below where I would like it to appear. With offset() or position() I can move the button text back where I would like it to appear, but the tappable area doesn't move to the new location.
I have tried different arrangements of ZStack and VStack, but the arrangement below is the closest I have come to getting the button to work in the upper right corner.
Here is where I would like the button to appear: https://i.stack.imgur.com/AMdKQ.png
Is there any way to move the tappable area up to where the text is located?
Or is there some better way to draw the top part of the view?
This code can be dropped directly into Xcode for analysis.
struct ContentView: View {
#State private var selectedTab: Tabs = .home
var body: some View {
NavigationView {
TabView (selection: $selectedTab) {
EntryView()
.tabItem {
Label("Home", systemImage: "house.circle.fill")
}.tag(Tabs.home)
HistoryView()
.tabItem {
Label("History", systemImage: "clock.fill")
}.tag(Tabs.history)
}
}
}
}
struct EntryView: View {
var body: some View {
GeometryReader { g in
ZStack (alignment: .top) {
Color.blue
.frame(height: (g.safeAreaInsets.top) * 0.6, alignment: .top)
.ignoresSafeArea()
HStack {
Spacer()
Button(action: {
print("Save tapped!")
}) {
Text("Save")
.font(Font.title3.bold())
.foregroundColor(.red)
.offset(y: g.size.height * -0.14)
// .position(x: g.size.width * 0.90, y: g.size.height * -0.115)
}
}
.padding(.trailing, 10)
}
}
}
}
struct HistoryView: View {
var body: some View {
VStack (alignment: .leading) {
Text("History Tab")
.padding(.top)
}
}
}
enum Tabs: String {
case home
case history
}

How to center a second HStack View?

I'm having trouble centering that second "Date" Text within the HStack. As you can see, in the image, it is a bit farther to the left. I want only the second view to be centered in the HStack. I want the first View to be latched to leading.
Here is the code.
import SwiftUI
struct DaySummariesBarChart: View {
var body: some View {
HStack {
Text("Date")
.font(.footnote)
Text("Date")
Spacer()
}
}
}
struct BarChart_Previews: PreviewProvider {
static var previews: some View {
DaySummariesBarChart()
.previewLayout(.sizeThatFits)
}
}
This is a pretty clean way to do it:
var body: some View {
ZStack {
Text("Date")
.font(.footnote)
.frame(maxWidth: .infinity, alignment: .leading)
Text("Date")
}
}
The first Text gets a maxWidth of infinity, so it takes up the whole space, but is aligned to .leading.
The second Text is centered by default in the ZStack.
The Spacer() moves the Views to the left. Your problem should be solved by adding another Spacer() on the other side.
struct DaySummariesBarChart: View {
var body: some View {
HStack {
Spacer()
Text("Date")
.font(.footnote)
Text("Date")
Spacer()
}
}
}
You can use a GeometryReader to make the width of the Views exactly half and therefore center the second one.
struct DaySummariesBarChart: View {
var body: some View {
GeometryReader { geometry in
HStack {
Text("Date")
.font(.footnote)
.frame(width: geometry.size.width / 2)
Text("Date")
.frame(width: geometry.size.width / 2)
}
}
}
}

Square Text using aspectRatio in SwiftUI

I'm trying to achieve a following layout using Swift UI…
struct ContentView : View {
var body: some View {
List(1...5) { index in
HStack {
HStack {
Text("Item number \(index)")
Spacer()
}.padding([.leading, .top, .bottom])
.background(Color.blue)
Text("i")
.font(.title)
.italic()
.padding()
.aspectRatio(1, contentMode: .fill)
.background(Color.pink)
}.background(Color.yellow)
}
}
}
I'd like the Text("i") to be square, but setting the .aspectRatio(1, contentMode: .fill) doesn't seem to do anything…
I could set the frame width and height of the text so it's square, but it seems that setting the aspect ratio should achieve what I want in a more dynamic way.
What am I missing?
I think this is what you're looking for:
List(1..<6) { index in
HStack {
HStack {
Text("Item number \(index)")
Spacer()
}
.padding([.leading, .top, .bottom])
.background(Color.blue)
Text("i")
.font(.title)
.italic()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(1, contentMode: .fill)
.background(Color.pink)
.fixedSize(horizontal: true, vertical: false)
.padding(.leading, 6)
}
.padding(6)
.background(Color.yellow)
}
The answer being said, i don't recommend giving SwiftUI too much freedom to decide the sizings. one of the biggest SwiftUI problems right now is the way it decides how to fit the views into each other. if something goes not-so-good on SwiftUI's side, it can result in too many calls to the UIKit's sizeToFit method which can slowdown the app, or even crash it.
but, if you tried this solution in a few different situations and it worked, you can assume that in your case, giving SwiftUI the choice of deciding the sizings is not problematic.
The issue is due to used different fonts for left/right sides, so paddings generate different resulting area.
Here is possible solution. The idea is to give right side rect based on default view size of left side text (this gives ability to track dynamic fonts sizes as well, automatically).
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var height = CGFloat.zero
var body: some View {
List(1...5, id: \.self) { index in
HStack(spacing: 8) {
HStack {
Text("Item number \(index)")
Spacer()
}
.padding([.leading, .top, .bottom])
.background(GeometryReader {
Color.blue.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
})
Text("i")
.italic()
.font(.title)
.frame(width: height, height: height)
.background(Color.pink)
}
.padding(8)
.background(Color.yellow)
.onPreferenceChange(ViewHeightKey.self) {
self.height = $0
}
}
}
}
struct ViewHeightKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
I managed to recreate the view in your first screenshot in SwiftUI. I wasn't sure on how much padding you wanted so I defined a private immutable variable for this value
The blue view is the one that will have the text content and could change in size so by using a GeometryReader you can get the size of the blue view and then use the height value from the size to set the width and height of the pink view. This means that whatever the height of the blue view is, the pink view will follow keeping an equal aspect ratio
The SizeGetter view below is used to get any views size using a GeometryReader and then binds that value back to a #State variable in the ContentView. Because the #State and #Binding property wrappers are being used, whenever the blueViewSize is updated SwiftUI will automatically refresh the view.
The SizeGetter view can be used for any view and is implemented using the .background() modifier as shown below
struct SizeGetter: View {
#Binding var size: CGSize;
var body: some View {
// Get the size of the view using a GeometryReader
GeometryReader { geometry in
Group { () -> AnyView in
// Get the size from the geometry
let size = geometry.frame(in: .global).size;
// If the size has changed, update the size on the main thread
// Checking if the size has changed stops an infinite layout loop
if (size != self.size) {
DispatchQueue.main.async {
self.size = size;
}
}
// Return an empty view
return AnyView(EmptyView());
}
}
}
}
struct ContentView: View {
private let padding: Length = 10;
#State private var blueViewSize: CGSize = .zero;
var body: some View {
List(1...5) { index in
// The yellow view
HStack(spacing: self.padding) {
// The blue view
HStack(spacing: 0) {
VStack(spacing: 0) {
Text("Item number \(index)")
.padding(self.padding);
}
Spacer();
}
.background(SizeGetter(size: self.$blueViewSize))
.background(Color.blue);
// The pink view
VStack(spacing: 0) {
Text("i")
.font(.title)
.italic();
}
.frame(
width: self.blueViewSize.height,
height: self.blueViewSize.height
)
.background(Color.pink);
}
.padding(self.padding)
.background(Color.yellow);
}
}
}
In my opinion it is better to set the background colour of a VStack or HStack instead of the Text view directly because you can then add more text and other views to the stack and not have to set the background colour for each one
I was searching very similar topic "Square Text in SwiftUI", came across your question and I think I've found quite simple approach to achieve your desired layout, using GeometryProxy to set width and heigh of the square view from offered geometry.size.
Checkout the code below, an example of TableCellView which can be used within List View context:
import SwiftUI
struct TableCellView: View {
var index: Int
var body: some View {
HStack {
HStack {
Text("Item number \(index)")
.padding([.top, .leading, .bottom])
Spacer()
}
.background(Color(.systemBlue))
.layoutPriority(1)
GeometryReader { geometry in
self.squareView(geometry: geometry)
}
.padding(.trailing)
}
.background(Color(.systemYellow))
.padding(.trailing)
}
func squareView(geometry: GeometryProxy) -> some View {
Text("i")
.frame(width: geometry.size.height, height: geometry.size.height)
.background(Color(.systemPink))
}
}