SwiftUI matchedGeometryEffect combined with NavigationView - ios

I'm trying to used matchedGeometryEffect on a pair of views. It works well until you navigate to a child view and then back, in which case the matchedGeometryEffect seems broken briefly (the red rectangle is instantly visible when I try expanding my view)
Is there something I'm missing?
struct ContentView: View {
#Namespace private var namespace
#State private var expanded = false
var body: some View {
NavigationView {
VStack {
NavigationLink("Click Me") {
Text("Hello, world")
}
Group {
if expanded {
Rectangle()
.foregroundColor(.red)
.matchedGeometryEffect(id: "Rect", in: namespace)
.frame(width: 300, height: 300)
}
else {
Rectangle()
.foregroundColor(.blue)
.matchedGeometryEffect(id: "Rect", in: namespace)
.frame(width: 50, height: 50)
}
}
.onTapGesture {
withAnimation(.linear(duration: 2.0)) {
expanded.toggle()
}
}
}
}
}
}

It seems this was a bug in iOS (I was using 15.5), the latest version (iOS 16.1) works without issue.

Related

How to clip a view while using a SwiftUI .move transition / animation

I'm trying to animate in a view and make it appear as if it's a sort of drawer opening from another view. This is all fine except if the first view is not opaque. It appears that you can see the animating view the moment it begins animating. Is there a way to clip this so it appears that the view is growing from the top of the bottom view?
Even without opacity this is an issue if where you're animating in from isn't a covered (demoed in second gif)
Sample Code:
struct ContentView: View {
#State private var showingSecondView: Bool = false
var body: some View {
VStack(spacing: 0) {
Spacer()
if showingSecondView {
ZStack {
Color.green.opacity(0.25)
Text("Second View")
}
.frame(width: 300, height: 300)
.transition(.move(edge: .bottom))
}
ZStack {
Color.black.opacity(1)
Text("First View")
}
.frame(width: 300, height: 300)
Button("Animate In / Out") {
showingSecondView.toggle()
}
.padding()
}
.animation(.easeInOut, value: showingSecondView)
}
}
It is possible to do by clipping exact container of 'drawer'. Here is a demo of possible approach.
Tested with Xcode 13.2 / iOS 15.2 (Simulator slow animation is ON for better demo)
var body: some View {
VStack(spacing: 0) {
Spacer()
VStack {
if showingSecondView {
ZStack {
Color.green.opacity(0.25)
Text("Second View")
}
.transition(.move(edge: .bottom))
} else {
Color.clear // << replacement for transition visibility
}
}
.frame(width: 300, height: 300)
.animation(.easeInOut, value: showingSecondView) // << animate drawer !!
.clipped() // << clip drawer area
ZStack {
Color.black.opacity(0.2)
Text("First View")
}
.frame(width: 300, height: 300)
Button("Animate In / Out") {
showingSecondView.toggle()
}
.padding()
}
}
Here a way for you:
struct ContentView: View {
#State private var isSecondViewPresented: Bool = false
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
Color.green.opacity(0.25).cornerRadius(20)
Text("Second View")
}
.frame(width: 300, height: 300)
.offset(y: isSecondViewPresented ? 0 : 300)
.clipShape(RoundedRectangle(cornerRadius: 20))
ZStack {
Color.black.opacity(0.1).cornerRadius(20)
Text("First View")
}
.frame(width: 300, height: 150)
Button("Animate In / Out") {
isSecondViewPresented.toggle()
}
.padding()
}
.animation(.easeInOut, value: isSecondViewPresented)
}
}

Showing Overlay Aligning to the Bottom

I want to lay a View over another when I tap a button.
The following is my code:
import SwiftUI
struct SheetView: View {
#State private var showSheet: Bool = false
var body: some View {
NavigationView {
VStack {
ZStack {
Rectangle()
.fill(Color.orange)
.frame(height: 32.0)
Button("Please select a mailing address") {
showSheet.toggle()
}.foregroundColor(Color.black)
}
Spacer()
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())
}
.overlay(popOver)
}
var popOver: some View {
Group {
if showSheet {
ZStack {
Color.black.opacity(0.4).ignoresSafeArea()
ZStack {
Rectangle()
.fill(Color.white)
//.frame(width: UIScreen.main.bounds.width, height: 400)
.frame(maxWidth: .infinity, maxHeight: 320.0, alignment: .bottom)
//.position(x: UIScreen.main.bounds.width / 2.0, y: 600)
}
}.onTapGesture {
showSheet.toggle()
}
}
}
}
}
And it looks like the following picture. I get pretty much what I need except that the overlaid View will appear at the center. How can I make it appear, aligning to the bottom View?
Here is a fix - use alignment for internal ZStack (tested with Xcode 13.2 / iOS 15.2):
ZStack(alignment: .bottom) {
Color.black.opacity(0.4).ignoresSafeArea()
ZStack {
Rectangle()
.fill(Color.white)
.frame(maxWidth: .infinity, maxHeight: 320.0, alignment: .bottom)
}
}
//.ignoresSafeArea() // << probably you also need this
.onTapGesture {
showSheet.toggle()
}

How to show Profile Icon next to Large Navigation Bar Title in SwiftUI?

I am developing an App that supports multiple Profiles. I really like the way Apple displays the Profile Icon next to the Large Navigation Bar Title in all their Apps. See the Screenshot below:
My Question is the following:
Is it possible to achieve this in SwiftUI? And if so, how?
If it's not possible in pure SwiftUI, how can I achieve it including UIKit Code?
Thanks for your help.
I solved this by using SwiftUI-Introspect, to "Introspect underlying UIKit components from SwiftUI".
Here is an example of a view:
struct ContentView: View {
#State private var lastHostingView: UIView!
var body: some View {
NavigationView {
ScrollView {
ForEach(1 ... 50, id: \.self) { index in
Text("Index: \(index)")
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Large title")
.introspectNavigationController { navController in
let bar = navController.navigationBar
let hosting = UIHostingController(rootView: BarContent())
guard let hostingView = hosting.view else { return }
// bar.addSubview(hostingView) // <--- OPTION 1
// bar.subviews.first(where: \.clipsToBounds)?.addSubview(hostingView) // <--- OPTION 2
hostingView.backgroundColor = .clear
lastHostingView?.removeFromSuperview()
lastHostingView = hostingView
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.trailingAnchor.constraint(equalTo: bar.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: bar.bottomAnchor, constant: -8)
])
}
}
}
}
Bar content & profile picture views:
struct BarContent: View {
var body: some View {
Button {
print("Profile tapped")
} label: {
ProfilePicture()
}
}
}
struct ProfilePicture: View {
var body: some View {
Circle()
.fill(
LinearGradient(
gradient: Gradient(colors: [.red, .blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
.padding(.horizontal)
}
}
The .frame(width: 40, height: 40) & hostingView.bottomAnchor constant will need to be adjusted to your needs.
And the results for each option (commented in the code):
Option 1
Option 2
View sticks when scrolled
View disappearing underneath on scroll
Without NavigationView
I done this with pure SwiftUI. You have to replace the Image("Profile") line with your own image (maybe from Assets or from base64 data with UIImage).
HStack {
Text("Apps")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
.padding(.all, 30)
This products following result:
With NavigationView
Let's assume that you have NavigationView and inside that there's only ScrollView and .navigationTitle. You can add that profile image there by using overlay.
NavigationView {
ScrollView {
//your content here
}
.overlay(
ProfileView()
.padding(.trailing, 20)
.offset(x: 0, y: -50)
, alignment: .topTrailing)
.navigationTitle(Text("Apps"))
}
Where ProfileView could be something like this:
struct ProfileView: View {
var body: some View {
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
}
The result will be like this...
...which is pretty close to the App Store:

Unable to change background color of view in SwiftUI

I am trying to change background color main this view but unable to do it. I tried to put background(Color.green) at HStack, VSTack and even on ZStack but it did not work, not sure if i am putting at right place. By default it is taking phone or simulator color which is white but i want to apply custom background color
My Xcode version is 11.5
struct HomePageView: View {
#State var size = UIScreen.main.bounds.width / 1.6
var body: some View {
GeometryReader{_ in
VStack {
HStack {
ZStack{
// main home page components here....
NavigationView{
VStack {
AssignmentDaysView()
}.background(Color.lairBackgroundGray)
.frame(width: 100, height: 100, alignment: .top)
.navigationBarItems(leading: Button(action: {
self.size = 10
}, label: {
Image("menu")
.resizable()
.frame(width: 30, height: 30)
}).foregroundColor(.appHeadingColor), trailing:
Button(action: {
print("profile is pressed")
}) {
HStack {
NavigationLink(destination: ProfileView()) {
LinearGradient.lairHorizontalDark
.frame(width: 30, height: 30)
.mask(
Image(systemName: "person.crop.circle")
.resizable()
.scaledToFit()
)
}
}
}
).navigationBarTitle("Home", displayMode: .inline)
}
HStack{
menu(size: self.$size)
.cornerRadius(20)
.padding(.leading, -self.size)
.offset(x: -self.size)
Spacer()
}
Spacer()
}.animation(.spring()).background(Color.lairBackgroundGray)
Spacer()
}
}
}
}
}
struct HomePageView_Previews: PreviewProvider {
static var previews: some View {
HomePageView()
}
}
In your NavigationView you have a VStack. Instead you can use a ZStack and add a background below your VStack.
Try the following:
NavigationView {
ZStack {
Color.green // <- or any other Color/Gradient/View you want
.edgesIgnoringSafeArea(.all) // <- optionally, if you want to cover the whole screen
VStack {
Text("assignments")
}
.background(Color.gray)
.frame(width: 100, height: 100, alignment: .top)
}
}
Note: you use many stacks wrapped in a GeometryReader which you don't use. Consider simplifying your View by removing unnecessary stacks. Also you may not need a GeometryReader if you use UIScreen.main.bounds (however, GeometryReader is preferred in SwiftUI).
Try removing some layers: you can start with removing the top ones: GeometryReader, VStack, HStack...
Try the following:
Change the view background color especially safe area also
struct SignUpView: View {
var body: some View {
ZStack {
Color.blue //background color
}.edgesIgnoringSafeArea(.all)

NavigationLink on SwiftUI pushes view twice

When adding a NavigationLink in SwiftUI the destination is presented twice, ex: I tap on the NavigationLink and it pushes my destination but when I dismiss the destination, via the back button or the swipe gesture it pushes the destination again without taping on the link.
Here is the part of my code that handles the link:
var body: some View {
HStack(spacing: 8.0) {
ForEach(part.getReference()) { (imageRef: ReferenceImage) in
ZStack(alignment: .topTrailing) {
Image(uiImage: imageRef.getUIImage())
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(6)
.onLongPressGesture {
print("looong")
self.managedObjectContext.delete(imageRef)
do {
try self.managedObjectContext.save()
} catch {
print("error deleting: \(error.localizedDescription)")
}
}
ZStack(alignment: .center) {
Circle()
.foregroundColor(Color.appColors.lightRose)
.opacity(0.7)
.frame(width: 35, height: 35)
Image(systemName: "arkit")
.imageScale(.large)
}
NavigationLink(destination:
ZStack {
Color.appColors.rose
.edgesIgnoringSafeArea(.top)
ReferenceARSwiftUIView(currentImage: imageRef.getUIImage())
.navigationBarTitle("AR Reference")
}
) {
EmptyView()
.frame(width: 90, height: 90)
}
}
}.animation(.interpolatingSpring(stiffness: 0.5, damping: 0.5))
EDIT 01:
As suggested I removed a bit of the noise in the code:
var part: Part
var body: some View {
HStack(spacing: 8.0) {
ForEach(part.getReference()) { (imageRef: ReferenceImage) in
ZStack(alignment: .topTrailing) {
Image(uiImage: imageRef.getUIImage())
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(6)
NavigationLink(destination: ReferenceARSwiftUIView(currentImage: imageRef.getUIImage())) {
EmptyView()
.frame(width: 90, height: 90)
}
}
}.animation(.interpolatingSpring(stiffness: 0.5, damping: 0.5))
EDIT 02:
I think I narrowed down, basically if I remove the ForEach the NavigationLink pushes correctly to the next View. Also depending on the number of itens I have on my array for the ForEach the number of pushes is the same.
I'm sure that Part conforms to Identifiable protocol and has id property.
The destination of the NavigationLink is presented multiple times because you have multiple instances of Part that have the exact same identity id in the forEach loop.
Just make sure that each instance of Part has a unique id and everything will work as expected.
I solved this by setting the NavigationLink's tag to the unique id of the model item.
Assuming you have an id for your imageRef, ReferenceImage class.
var part: Part
var body: some View {
HStack(spacing: 8.0) {
ForEach(part.getReference()) { (imageRef: ReferenceImage) in
ZStack(alignment: .topTrailing) {
Image(uiImage: imageRef.getUIImage())
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(6)
NavigationLink(destination: ReferenceARSwiftUIView(currentImage: imageRef.getUIImage()), tag: imageRef.id) {
EmptyView()
.frame(width: 90, height: 90)
}
}
}.animation(.interpolatingSpring(stiffness: 0.5, damping: 0.5))
NavigationLink(destination: ReferenceARSwiftUIView(currentImage: imageRef.getUIImage()), tag: imageRef.id)
Yeah changing List to ScrollView helped me in a similar situation.
ScrollView {
if !usersViewModel.isLoading {
ForEach(self.usersViewModel.users, id: \.id) { user in
NavigationLink(destination: UserDashboardView(showDashboardDetail: $showDashboardDetail, user: user)) {
VStack(alignment: .leading) {
NetworkCardView(user: user)
}
.frame(width: screen.size.width - 60, height: 250)
.padding(.top)
}
}
.listRowBackground(AppColor())
}
}
I wasn't able to reproduce your issue, because even the stripped down version contains types declared by you, but not included in the question.
A couple of things:
If you want to use NavigationLink, the view needs to be embeded in a NavigationView.
If you are using ForEach inside an HStack there is a chance you are going to run out of space to display all the elements, so it would be a good idea to wrap it in a ScrollView.
Last, but not least: inside ForEach you have to make NavigationLink your top level view and make any other views its content:
var body: some View {
NavigationView {
ScrollView(.horizontal) {
HStack {
ForEach(part.getReference()) { (imageRef: ReferenceImage) in
NavigationLink(destination: ReferenceARSwiftUIView(currentImage: imageRef.getUIImage())) {
Image(uiImage: imageRef.getUIImage())
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(6)
}
}
}
}
}
}

Resources