SwiftUI - View disappears if animated - ios

I am building a custom segmented control. This is the code that I have written.
struct SegmentedControl: View {
private var items: [String] = ["One", "Two", "Three"]
#Namespace var animation:Namespace.ID
#State var selected: String = "One"
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(items, id: \.self) { item in
Button(action: {
withAnimation(.spring()){
self.selected = item
}
}) {
Text(item)
.font(Font.subheadline.weight(.medium))
.foregroundColor(selected == item ? .white : .accentColor)
.padding(.horizontal, 25)
.padding(.vertical, 10)
.background(zStack(item: item))
}
}
} .padding()
}
}
private func zStack(item: String) -> some View {
ZStack{
if selected == item {
Color.accentColor
.clipShape(Capsule())
.matchedGeometryEffect(id: "Tab", in: animation)
} else {
Color(.gray)
.clipShape(Capsule())
}}
}
}
A control is Blue when it is selected.
It works as expected most of the time like in the following GIF.
However, sometimes if you navigate back and forth very fast, the Color.accentColor moves off screen and disappears as you see in the following GIF. I have used a lot of time but could not fix it.
Sometimes, I get this error.
Multiple inserted views in matched geometry group Pair<String,
ID>(first: "Tab", second: SwiftUI.Namespace.ID(id: 248)) have `isSource:
true`, results are undefined.
Info, It is easier to test it on a physical device rather than a simulator.
Update
This is my all codde including the ContentView and the Modal.
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
NavigationView {
VStack {
Button(action: {
self.isPresented.toggle()
}, label: {
Text("Button")
})
}
}
.sheet(isPresented: $isPresented, content: {
ModalView()
})
}
}
struct ModalView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: TabbarView(),
label: {
Text("Navigate")
})
}
}
}
struct TabbarView: View {
private var items: [String] = ["One", "Two", "Three"]
#Namespace var animation:Namespace.ID
#State var selected: String = "" // change here
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(items, id: \.self) { item in
Button(action: {
withAnimation{
self.selected = item
}
}) {
Text(item)
.font(Font.subheadline.weight(.medium))
.foregroundColor(selected == item ? .white : .accentColor)
.padding(.horizontal, 25)
.padding(.vertical, 10)
.background(zStack(item: item))
}
}
} .padding()
}
.onAppear { self.selected = "One" } // add this
}
private func zStack(item: String) -> some View {
ZStack{
if selected == item {
Color.accentColor
.clipShape(Capsule())
.matchedGeometryEffect(id: "Tab", in: animation)
} else {
Color(.gray)
.clipShape(Capsule())
}}
}
}

Related

Odd animation with .matchedGeometryEffect and NavigationView

I'm working on creating a view which uses matchedGeometryEffect, embedded within a NavigationView, and when presenting the second view another NavigationView.
The animation "works" and it matches correctly, however when it toggles from view to view it happens to swing as if unwinding from the navigation stack.
However, if I comment out the NavigationView on the secondary view the matched geometry works correctly.
I am working on iOS 14.0 and above.
Sample code
Model / Mock Data
struct Model: Identifiable {
let id = UUID().uuidString
let icon: String
let title: String
let account: String
let colour: Color
}
let mockItems: [Model] = [
Model(title: "Test title 1", colour: .gray),
Model(title: "Test title 2", colour: .blue),
Model(title: "Test title 3", colour: .purple)
...
]
Card View
struct CardView: View {
let item: Model
var body: some View {
VStack {
Text(item.title)
.font(.title3)
.fontWeight(.heavy)
}
.padding()
.frame(maxWidth: .infinity, minHeight: 100, alignment: .leading)
.background(item.colour)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Secondary / Detail View
struct DetailView: View {
#Binding var isShowingDetail: Bool
let item: Model
let animation: Namespace.ID
var body: some View {
NavigationView { // <--- comment out here and it works
VStack {
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
withAnimation { isShowingDetail = false }
}
ScrollView(.vertical, showsIndicators: false) {
Text("Lorem ipsum dolor...")
}
}
.padding(.horizontal)
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(.stack)
}
}
Primary View
struct ListView: View {
#State private var selectedCard: Model?
#State private var isShowingCard: Bool = false
#Namespace var animation
var body: some View {
ZStack {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
ForEach(mockItems) { item in
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
withAnimation {
selectedCard = item
isShowingCard = true
}
}
}
}
.navigationTitle("Test title")
}
.padding(.horizontal)
.navigationViewStyle(.stack)
// show detail view
if let selectedCard = selectedCard, isShowingCard {
DetailView(
isShowingDetail: $isShowingCard,
item: selectedCard,
animation: animation
)
}
}
}
}
Video examples
With NavigationView in DetailView
Without NavigationView in DetailView
Ignore the list view still visible
You don't need second NavigationView (actually I don't see links at all, so necessity of the first one is also under question). Anyway we can just change the layout order and put everything into one NavigationView, like below.
Tested with Xcode 13.4 / iOS 15.5
struct ListView: View {
#State private var selectedCard: Model?
#State private var isShowingCard: Bool = false
#Namespace var animation
var body: some View {
NavigationView {
ZStack { // container !!
if let selectedCard = selectedCard, isShowingCard {
DetailView(
isShowingDetail: $isShowingCard,
item: selectedCard,
animation: animation
)
} else {
ScrollView(.vertical, showsIndicators: false) {
ForEach(mockItems) { item in
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
selectedCard = item
isShowingCard = true
}
}
}
.navigationTitle("Test title")
}
}
.padding(.horizontal)
.navigationViewStyle(.stack)
.animation(.default, value: isShowingCard) // << animated here !!
}
}
}
struct DetailView: View {
#Binding var isShowingDetail: Bool
let item: Model
let animation: Namespace.ID
var body: some View {
VStack {
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
isShowingDetail = false
}
ScrollView(.vertical, showsIndicators: false) {
Text("Lorem ipsum dolor...")
}
}
.padding(.horizontal)
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
}
}
Test module is here

How can I go to a view by clicking on a Button in SwiftUI

If the user clicks on connexionBtnView I want to redirect them to an AdminView or UserView
import SwiftUI
struct ConnexionView: View {
#State var loginId: String = ""
#State var pwd: String = ""
#StateObject private var keyboardHander = KeyBoardHandler()
var body: some View {
NavigationView{
ZStack{
Image("background")
.ignoresSafeArea()
VStack (spacing: 15){
Spacer()
logoView
Spacer()
titleView
loginIdView
loginPwdView
connexionBtnView
Spacer()
NavigationLink {
LostPwdView()
} label: {
lostPwd
}
Spacer()
}.frame(maxHeight: .infinity)
.padding(.bottom,keyboardHander.keyboardHeight)
.animation(.default)
}
}
}
The NavigationLink has the isActive parameter. You can pass it in the init of NavigationLink and when this state variable has the true value you will redirect to another view. Details here.
struct ConnexionView: View {
#State var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $isActive) {
LostPwdView()
} label: {
Text("Some Label")
}
Button("Tap me!") {
isActive = true
}
}
}
}
}
struct LostPwdView: View {
var body: some View {
Text("Hello")
}
}
What you need to do is having a #State variable that would trigger the navigation:
.fullScreenCover(
isPresented: $viewShown
) {
print("View dismissed")
} content: {
NextView()
}
Where NextView() is the View you want to show and the viewShown is your State variable, below a full example:
struct ExampleView: View {
#State var isNextPageOpen = false
var body: some View {
Button("Tap Here To Navigate") {
isNextPageOpen = true
}
.fullScreenCover(
isPresented: $isNextPageOpen
) {
print("View dismissed")
} content: {
NextView()
}
}
}

Presenting Sheet modally in SwiftUI

I am trying to present a modal sheet upon selecting the menu item in the navigation bar. But, the sheet is not displayed. Upon debugging I noticed that the state variable showSheet is not getting updated and I am sort of lost as to why it is not updating.
Any help is very much appreciated. Thank you!
There is another post (#State not updating in SwiftUI 2) that has a similar issue. Is this a bug in SwiftUI?
Below is a full sample
I have a fileprivate enum that defines two cases for the views - add and edit
fileprivate enum SheetView {
case add, edit
}
Below is the ContentView. The ContentView declares two #State variables that are set based on the menu item selected
The menu items (var actionItems) are on the NavigationView and has menu with 2 buttons - Add and Edit. Each button has an action set to toggle the showSheetView and the showSheet variables. The content is presented based on which item is selected. Content is built using #ViewBuilder
struct ContentView: View {
#State private var showSheetView = false
#State private var showSheet: SheetView? = nil
var body: some View {
GeometryReader { g in
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.navigationBarItems(trailing: actionItems)
}
}.sheet(isPresented: $showSheetView) {
content
}
}
var actionItems: some View {
Menu {
Button(action: {
showSheet = .add
showSheetView.toggle()
}) {
Label("Add Asset", systemImage: "plus")
}
Button(action: {
showSheet = .edit
showSheetView.toggle()
}) {
Label("Edit Asset", systemImage: "minus")
}
} label: {
Image(systemName: "dot.circle.and.cursorarrow").resizable()
}
}
#ViewBuilder
var content: some View {
if let currentView = showSheet {
switch currentView {
case .add:
AddAsset(showSheetView: $showSheetView)
case .edit:
EditAsset(showSheetView: $showSheetView)
}
}
}
}
Below are the two Views - AddAsset and EditAsset
struct AddAsset: View {
#Binding var showSheetView: Bool
var body: some View {
NavigationView {
Text("Add Asset")
.navigationBarTitle(Text("Add"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("Dismissing sheet view...")
self.showSheetView = false
}) {
Text("Done").bold()
})
}
}
}
struct EditAsset: View {
#Binding var showSheetView: Bool
var body: some View {
NavigationView {
Text("Edit Asset")
.navigationBarTitle(Text("Edit"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("Dismissing sheet view...")
self.showSheetView = false
}) {
Text("Done").bold()
})
}
}
}
The solution is to use sheet(item: variant.
Here is fixed code (there are many changes so all components included). Tested with Xcode 12.1 / iOS 14.1
enum SheetView: Identifiable {
var id: Self { self }
case add, edit
}
struct ContentView: View {
#State private var showSheet: SheetView? = nil
var body: some View {
GeometryReader { g in
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.navigationBarItems(trailing: actionItems)
}
}.sheet(item: $showSheet) { mode in
content(for: mode)
}
}
var actionItems: some View {
Menu {
Button(action: {
showSheet = .add
}) {
Label("Add Asset", systemImage: "plus")
}
Button(action: {
showSheet = .edit
}) {
Label("Edit Asset", systemImage: "minus")
}
} label: {
Image(systemName: "dot.circle.and.cursorarrow").resizable()
}
}
#ViewBuilder
func content(for mode: SheetView) -> some View {
switch mode {
case .add:
AddAsset(showSheet: $showSheet)
case .edit:
EditAsset(showSheet: $showSheet)
}
}
}
struct AddAsset: View {
#Binding var showSheet: SheetView?
var body: some View {
NavigationView {
Text("Add Asset")
.navigationBarTitle(Text("Add"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("Dismissing sheet view...")
self.showSheet = nil
}) {
Text("Done").bold()
})
}
}
}
struct EditAsset: View {
#Binding var showSheet: SheetView?
var body: some View {
NavigationView {
Text("Edit Asset")
.navigationBarTitle(Text("Edit"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
print("Dismissing sheet view...")
self.showSheet = nil
}) {
Text("Done").bold()
})
}
}
}

Show different views from NavigationBarItem menu in SwiftUI

I am trying to show a different view based on the option chosen in the NavigationBar menu. I am getting stuck on the best way to do this.
First, based on my current approach (I think it is not right!), I get a message in Xcode debugger when I press the menu item:
SideMenu[16587:1131441] [UILog] Called -[UIContextMenuInteraction
updateVisibleMenuWithBlock:] while no context menu is visible. This
won't do anything.
How do I fix this?
Second, When I select an option from the menu, how do I reset the bool so that it does not get executed unless it is chosen from the menu again. Trying to reset as self.showNewView = false within the if condition gives a compiler error
Here is a full executable sample code I am trying to work with. Appreciate any help in resolving this. Thank you!
struct ContentView: View {
#State var showNewView = false
#State var showAddView = false
#State var showEditView = false
#State var showDeleteView = false
var body: some View {
NavigationView {
GeometryReader { g in
VStack {
if self.showAddView {
AddView()
}
if self.showNewView {
NewView()
}
if self.showEditView {
EditView()
}
if self.showDeleteView {
DeleteView()
}
}.frame(width: g.size.width, height: g.size.height)
}
.navigationTitle("Title")
.navigationBarItems(leading: {
Menu {
Button(action: {showNewView.toggle()}) {
Label("New", systemImage: "pencil")
}
Button(action: {showEditView.toggle()}) {
Label("Edit", systemImage: "square.and.pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}(), trailing: {
Menu {
Button(action: {showAddView.toggle()}) {
Label("Add", systemImage: "plus")
}
Button(action: {showDeleteView.toggle()}) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "plus")
}
}())
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct NewView: View {
var body: some View {
GeometryReader { g in
Text("This is New View")
}
.background(Color.red)
}
}
struct EditView: View {
var body: some View {
GeometryReader { g in
Text("This is Edit View")
}
.background(Color.green)
}
}
struct AddView: View {
var body: some View {
GeometryReader { g in
Text("This is Add View")
}
.background(Color.orange)
}
}
struct DeleteView: View {
var body: some View {
GeometryReader { g in
Text("This is Delete View")
}
.background(Color.purple)
}
}
Here is what I get when I select each of the menu items. I would like to be able to show only one menu item. Essentially dismiss the other one when a new menu item is selected
A possible solution is to use a dedicated enum for your current view (instead of four #State properties):
enum CurrentView {
case new, add, edit, delete
}
#State var currentView: CurrentView?
Note that you can also extract parts of code to computed properties.
Here is a full code:
enum CurrentView {
case new, add, edit, delete
}
struct ContentView: View {
#State var currentView: CurrentView?
var body: some View {
NavigationView {
GeometryReader { g in
content
.frame(width: g.size.width, height: g.size.height)
}
.navigationTitle("Title")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: leadingBarItems, trailing: trailingBarItems)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var content: some View {
if let currentView = currentView {
switch currentView {
case .add:
AddView()
case .new:
NewView()
case .edit:
EditView()
case .delete:
DeleteView()
}
}
}
var leadingBarItems: some View {
Menu {
Button(action: { currentView = .new }) {
Label("New", systemImage: "pencil")
}
Button(action: { currentView = .edit }) {
Label("Edit", systemImage: "square.and.pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
var trailingBarItems: some View {
Menu {
Button(action: { currentView = .add }) {
Label("Add", systemImage: "plus")
}
Button(action: { currentView = .delete }) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "plus")
}
}
}

SwiftUI EditButton() bug

I'm presenting a List inside a modal. If I'm inside a NavigationView the EditButton its totally broken.
struct ContentView: View {
#State var showSheetView = false
var body: some View {
NavigationView {
Button(action: {
self.showSheetView.toggle()
}) {
Image(systemName: "bell.circle.fill")
.font(Font.system(.title))
}
.sheet(isPresented: $showSheetView) {
SheetView()
}
}
}
}
struct SheetView: View {
#State private var myArray: [String] = ["One", "Two", "Three"]
var body: some View {
NavigationView {
VStack {
List {
ForEach(myArray, id: \.self) { item in
Text(item)
}.onDelete(perform: { indexSet in
})
}
}
.navigationBarItems(trailing: EditButton())
}
}
}
If I remove the NavigationView where i present from, then at first it seems to work, the second time i present it gets broken again.
struct ContentView: View {
#State var showSheetView = false
var body: some View {
Button(action: {
self.showSheetView.toggle()
}) {
Image(systemName: "bell.circle.fill")
.font(Font.system(.title))
}
.sheet(isPresented: $showSheetView) {
SheetView()
}
}
}
Manually handling the editMode works for me on macOS Big Sur with Xcode 12.1 / iOS 14.1.
I also had a problem of EditButton showing "Edit" again in an edit mode when I rotate the simulator, and the below solution solves it as well.
The following solution uses a custom EditButton struct that handles a manual editMode binding.
First the custom EditButton:
struct EditButton: View {
#Binding var editMode: EditMode
var body: some View {
Button {
switch editMode {
case .active: editMode = .inactive
case .inactive: editMode = .active
default: break
}
} label: {
if let isEditing = editMode.isEditing, isEditing {
Text("Done")
} else {
Text("Edit")
}
}
}
}
Using the above EditButton is straightforward:
struct SheetView: View {
#State private var myArray: [String] = ["One", "Two", "Three"]
#State private var editMode = EditMode.inactive
var body: some View {
NavigationView {
VStack {
List {
ForEach(myArray, id: \.self) { item in
Text(item)
}.onDelete(perform: { indexSet in
})
}
}
.navigationBarItems(trailing: EditButton(editMode: $editMode))
.environment(\.editMode, $editMode)
.animation(.spring(response: 0))
}
}
}
The EditButton in the trailing navigation bar item handles the #State private var editMode kept in SheetView.
This editMode is then injected into the inner views using the environment .environment(\.editMode, $editMode).
For the animation effect of the edit mode transition, I found .spring(response: 0) most appropriate.
Instead of
.navigationBarItems(trailing: EditButton())
you could try:
.toolbar { EditButton() }
I had the same problem and this worked fine for me.
This worked for me, in iOS 16.0
struct MyEditButton: View {
#Binding var editMode: EditMode
var body: some View {
Button {
switch editMode {
case .active: editMode = .inactive
case .inactive: editMode = .active
default: break
}
} label: {
if let isEditing = editMode.isEditing, isEditing {
Text("Done")
} else {
Text("Edit")
}
}
}
}
struct ContentView: View {
#State var editMode: EditMode = .inactive
#State var list = ["A", "B", "C"]
var body: some View {
Form {
Section(header: HStack {
MyEditButton(editMode: $editMode)
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(Text("LIST"), alignment: .leading)
}) {
List {
ForEach(list, id: \.self) { value in
Text(value)
}
.onDelete(perform: deleteSection)
.onMove(perform: moveSection)
}
}
}
.environment(\.editMode, $editMode)
.padding()
}
func deleteSection(at offsets: IndexSet) {
list.remove(atOffsets: offsets)
}
func moveSection(from source: IndexSet, to destination: Int) {
list.move(fromOffsets: source, toOffset: destination)
}
}

Resources