I just upgrade to Xcode 11 Beta 5 and update my SwiftUI project.
In previous version I wanted to use PresentationLink component to show up a modal. I had the same problem than now, the modal has only shown once. I thought it was a bug as I saw in other SO posts. So I tried my chance by upgrading to Beta 5 but still no luck.
I noticed that this behaviour seems to be caused by wrapping in a ScrollView component. If I delete the ScrollView component everything works fine as expected.
Here's the code:
struct HomeList : View {
var listViewItems = listViewItemsData
#State var show = false
var body: some View {
VStack {
HStack {
VStack(alignment: .leading) {
Text("Project title").font(.largeTitle).fontWeight(.heavy)
Text("Project subtitle").foregroundColor(Color.gray)
}
Spacer()
}.padding(.top, 78).padding(.leading, 60)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(listViewItems) { item in
GeometryReader { geometry in
Button(action: { self.show.toggle()}) {
ListView(title: item.title, image: item.image, color: item.color, destination: item.destination)
.rotation3DEffect(Angle(degrees: Double((geometry.frame(in: .global).minX - 30) / -30)), axis: (x: 0, y: 10, z: 0))
.sheet(isPresented: self.$show, content: { InformationView() })
}
}.frame(width: 246, height: 360)
}
}.padding(30)
Spacer()
}.frame(width: UIScreen.main.bounds.width, height: 480)
Spacer()
}
}
}
To summarize, without ScrollView wrapper the Modal behaviour works as expected.
I would like to know if there is a solution / workaround ? Or I just have to wait a release :)
Edit from answer:
struct HomeList : View {
var listViewItems = listViewItemsData
#State var show = false
#State var view: AnyView = AnyView(Text(""))
var body: some View {
VStack {
HStack {
VStack(alignment: .leading) {
Text("Project title").font(.largeTitle).fontWeight(.heavy)
Text("Project subtitle").foregroundColor(Color.gray)
}
Spacer()
}.padding(.top, 78).padding(.leading, 60)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(listViewItems) { item in
GeometryReader { geometry in
Button(action: {
self.show.toggle()
self.view = item.destination
}) {
ListView(title: item.title, image: item.image, color: item.color, destination: item.destination)
.rotation3DEffect(Angle(degrees: Double((geometry.frame(in: .global).minX - 30) / -30)), axis: (x: 0, y: 10, z: 0))
}
}.frame(width: 246, height: 360)
}
}.padding(30)
Spacer()
}.frame(width: UIScreen.main.bounds.width, height: 480)
.sheet(isPresented: self.$show, content: { self.view })
Spacer()
}
}
}
This is the same issue as https://stackoverflow.com/a/57087399/3179416
Just move your .sheet outside of your ForEach.
import SwiftUI
struct Testing : View {
var listViewItems: [Int] = [1, 2, 3]
#State var show = false
var body: some View {
VStack {
HStack {
VStack(alignment: .leading) {
Text("Project title").font(.largeTitle).fontWeight(.heavy)
Text("Project subtitle").foregroundColor(Color.gray)
}
Spacer()
}.padding(.top, 78).padding(.leading, 60)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(listViewItems, id: \.self) { item in
GeometryReader { geometry in
Button(action: { self.show.toggle()}) {
Text("Button")
.rotation3DEffect(Angle(degrees: Double((geometry.frame(in: .global).minX - 30) / -30)), axis: (x: 0, y: 10, z: 0))
}
}.frame(width: 246, height: 360)
}
}.padding(30)
Spacer()
}.frame(width: UIScreen.main.bounds.width, height: 480)
.sheet(isPresented: self.$show, content: { Text("Modal") })
Spacer()
}
}
}
Related
I wanted to make a bottomsheet in SwiftUI with my own efforts, I open it using animation, but my animation doesn't work when closing, what is the reason?
I wonder if the offset value is increasing with animation, is there a problem while it is decreasing I am not very good at SwiftUI so I could not fully understand the problem.
struct ContentView: View {
#State var isOpen = false
#State var offset = UIScreen.main.bounds.height / 3
var body: some View {
ZStack {
Color.blue
.ignoresSafeArea()
Button(action: {
self.isOpen.toggle()
}, label: {
ZStack {
RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(.black)
Text("Open")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.white)
}
})
.buttonStyle(DefaultButtonStyle())
.frame(width: 300, height: 50, alignment: .center)
if isOpen {
GeometryReader { geometry in
VStack {
Spacer()
BottomSheet()
.frame(width: geometry.size.width,
height: geometry.size.height / 3,
alignment: .center)
.background(
Color.white
)
.offset(y: offset)
.onAppear(perform: {
withAnimation {
self.offset = 0
}
})
.onDisappear(perform: {
withAnimation {
self.offset = UIScreen.main.bounds.height / 3
}
})
}.ignoresSafeArea()
}
}
}
}
}
BottomSheet
struct BottomSheet: View {
var body: some View {
Text("Hello, World!")
}
}
onDisappear gets called when the view was removed, that's the reason custom animation not working :
struct ContentView: View {
#State var isOpen = false
var offset: CGFloat {
isOpen ? 0 : UIScreen.main.bounds.height / 3
}
var body: some View {
ZStack {
Color.blue
.ignoresSafeArea()
Button(action: {
self.isOpen.toggle()
}, label: {
ZStack {
RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(.black)
Text("Open")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.white)
}
})
.buttonStyle(DefaultButtonStyle())
.frame(width: 300, height: 50, alignment: .center)
GeometryReader { geometry in
VStack {
Spacer()
BottomSheet()
.frame(width:geometry.size.width,
height: geometry.size.height / 3,
alignment: .center)
.background(
Color.white
)
.offset(y: offset)
.animation(.easeInOut(duration: 0.5)) .transition(.move(edge: .bottom))
} .edgesIgnoringSafeArea(.bottom)
}
}
}
}
I hope you are having a more pleasant evening than mine!
So as I mentioned in the title, my for each loop crashes whenever I try to remove an item from the original list with a binding. I did some research and the problem is that for each generates a view with an id but when you delete the item in your child view it can't find the contents and crashes. Returns 'Thread 1: Fatal error: Index out of range'. I can fix the issue by declaring a #State var instead of #Binding, which really works! However, I have more than a delete button in my child view and if I don't use a binding declaration, changes made don't reflect on the main view. I don't wanna give up on neither the delete button nor buttons. Is there way to keep all of them in my childview?
Mainview declarations;
struct ContentView: View {
#ObservedObject var superReminders = SuperReminders()
#State var superReminder = SuperReminder()}
My list View;
List{
ForEach(superReminders.reminderlist.indices, id: \.self) { index in
NavigationLink(destination: DetailedRemView(superReminder : self.$superReminders.reminderlist[index] ).environmentObject(superReminders)) {
squareImageView(superReminder : self.$superReminders.reminderlist[index]).environmentObject(superReminders).environmentObject(superReminders)
}.listRowBackground(Color.clear)
}.onDelete { indexSet in
superReminders.reminderlist.remove(atOffsets: indexSet)}
}
Childview declarations;
import SwiftUI
struct DetailedRemView: View {
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "EE, MMM d, YYYY"
return formatter
}
#State public var showingDetail = false
#Environment(\.colorScheme) var colorScheme: ColorScheme
#State private var deleteReminderAlert = false
#EnvironmentObject var superReminders : SuperReminders
#Environment(\.presentationMode) var presentationMode
#Binding var superReminder : SuperReminder
#State private var showDialog = false
#State var animate = false
var body: some View {
VStack{
HStack(alignment: .center){
Text(superReminder.remdate)
.font(.title)
.multilineTextAlignment(.leading)
.padding(.leading)
.frame(minWidth: 100,maxWidth: .infinity, maxHeight: 50)
Spacer()
Button(action: {
self.showDialog.toggle()
}, label: {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.frame(width: 80, height: 35)
HStack{
Text("Edit")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.cornerRadius(8)
Image(systemName: "pencil")
.foregroundColor(.white)
}
}
.shadow(color:Color.gray.opacity(0.3), radius: 3, x: 3, y: 3)
.padding(.leading)
.alert(isPresented: $showDialog,
TextAlert(title: "Edit reminder title",
message: "Enter a new title or dissmis.", placeholder: superReminder.remdate,
keyboardType: .default) { result in
if let text = result {
if text != "" {
superReminder.remdate = text }
else{}
} else {
}
})
})
.padding(.leading)
}
.frame(minWidth: 100, maxWidth: /*#START_MENU_TOKEN#*/.infinity/*#END_MENU_TOKEN#*/, maxHeight: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.padding(.vertical, -10)
ZStack(alignment: .topTrailing){
Image(superReminder.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0,maxWidth: .infinity,minHeight: 200,maxHeight: .infinity)
.saturation(superReminder.pastreminder ? 0.1 : 1)
.clipShape(Rectangle())
.cornerRadius(10)
.padding(.all)
.pinchToZoom()
HStack{
Text(superReminder.dateactual, formatter: dateFormatter)
.foregroundColor(.white)
.frame(width: 180, height: 30, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.background( superReminder.pastreminder ? Color.gray : Color.lightlygreen)
.cornerRadius(8)
.animation(/*#START_MENU_TOKEN#*/.easeIn/*#END_MENU_TOKEN#*/)
if superReminder.pastreminder == true {
ZStack{
RoundedRectangle(cornerRadius: 8)
.fill(Color.black)
.frame(width: 30, height: 30)
Image(systemName: "moon.zzz")
.foregroundColor(.white)
}.offset(x: animate ? -3 : 0)
.onAppear(perform: {
shake()
})
}
else{}
}
.zIndex(0)
.offset(x: -10, y: 10)
.padding()
}
.zIndex(1)
.shadow(color: Color.gray.opacity(0.4), radius: 3, x: 1, y: 2)
HStack{
Button(action: {
self.showingDetail.toggle()
}){
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color.lightlygreen)
.frame(width: 140, height: 40)
HStack{
Text("Reschedule")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.cornerRadius(8)
Image(systemName: "calendar")
.foregroundColor(.white)
}
}
.shadow(color:Color.gray.opacity(0.3), radius: 3, x: 3, y: 3)
.padding(.all, 4.0)
}
.sheet(isPresented: $showingDetail, content :{
remdatepicker(isPresented: self.$showingDetail, superReminder: $superReminder)})
Button(action: {
if superReminder.pastreminder == true {
superReminder.pastreminder = false
}
else if superReminder.pastreminder == false{
superReminder.pastreminder = true
}
}, label: {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(superReminder.pastreminder == true ? Color.lightlyblue : Color.gray)
.frame(width: 100, height: 40)
HStack{
Text(superReminder.pastreminder == true ? "Activate" : "Silence")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.cornerRadius(8)
Image(systemName: superReminder.pastreminder == true ? "checkmark.circle" : "moon.zzz")
.foregroundColor(.white)
}
}
.shadow(color:Color.gray.opacity(0.3), radius: 3, x: 3, y: 3)
.padding(.all, 4.0)
})
Button(action: {
self.deleteReminderAlert.toggle()
}, label: {
ZStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color(.red))
.frame(width: 40, height: 40)
HStack{
Image(systemName: "trash")
.foregroundColor(.white)
}
}
.shadow(color:Color.gray.opacity(0.3), radius: 3, x: 3, y: 3)
.padding(.all, 4.0)
})
}.padding(.bottom, 20)
.alert(isPresented: $deleteReminderAlert){
Alert(
title: Text("Are you sure?"),
message: Text("Do you want to delete this reminder?"),
primaryButton: .destructive(Text("Yes"), action: {
superReminders.remove(superReminder: superReminder)
self.presentationMode.wrappedValue.dismiss()
}),
secondaryButton: .cancel(Text("No"))
)
}
}
}
func shake() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation(Animation.default.repeatCount(6).speed(7)){
animate.toggle()}}}
}
Class and List;
import SwiftUI
struct SuperReminder: Identifiable, Codable, Equatable {
var id = UUID()
var remdate = ""
var dateactual = Date.init()
var image = "New1"
var pastreminder = false
}
class SuperReminders: ObservableObject {
#Published var reminderlist: [SuperReminder]
init() {
self.reminderlist = [
]
}
func add(superReminder: SuperReminder) {
reminderlist.append(superReminder)
}
func remove(superReminder: SuperReminder) {
if let index = reminderlist.firstIndex(of: superReminder) {
reminderlist.remove(at: index)
}
}
}
This answer is similar to
Accessing and manipulating array item in an EnvironmentObject
Loop over superReminders.reminderlist since SuperReminder: Identifiable, Codable, Equatable.
ForEach(superReminders.reminderlist) { superReminder in
NavigationLink(destination: DetailedRemView(superReminders: superReminders,
superReminder: superReminder)) {
-----
}
}
In DetailedRemView, do the following:
struct DetailedRemView: View {
#ObservedObject var superReminders : SuperReminders
var superReminder : SuperReminder
// find index of current superReminder
var indexOfReminder: Int? {
superReminders.reminderlist.firstIndex {$0 == superReminder}
}
var body: some View {
// Unwrap indexOfReminder
if let index = indexOfReminder {
VStack {
------
}
}
}
----
}
Use superReminders.reminderlist[index] in DetailRemView whereever you need to update superReminder.
superReminders.reminderlist[index].pastreminder = false
I have an app were when I tap on a button to open a new view it shows my view because I am using .sheet(), is there a way to make the .sheet() full screen rather than mid way? I tried .present()
.fullScreenCover() and still not working properly. Can anyone help me solve this issue. thanks for the help.
#State var showingDetail = false
Button(action: {
withAnimation {
self.showingDetail.toggle()
}
}) {
Text("Enter")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 300, height: 50)
.background(Color.accentColor)
.cornerRadius(15.0)
.shadow(radius: 10.0, x: 20, y: 10)
}.padding(.top, 50).sheet(isPresented: $showingDetail) {
MainView()
}
You just nee to reorder your modifiers.
Here is the solution provided it will work in iOS 14 +
#State var showingDetail = false
Button(action: {
withAnimation {
self.showingDetail.toggle()
}
}) {
Text("Enter")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 300, height: 50)
.background(Color.accentColor)
.cornerRadius(15.0)
.shadow(radius: 10.0, x: 20, y: 10)
}.fullScreenCover(isPresented: $showingDetail) {
MainView()
.edgesIngoringSafeArea(.all) // if you need to hide navigating and status bar
}
.padding(.top, 50)
Here is the workaround approach for iOS 13.
#State var showingDetail = false
ZStack {
if (!showingDetail) {
Button(action: {
withAnimation {
self.showingDetail.toggle()
}
}) {
Text("Enter")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 300, height: 50)
.background(Color.accentColor)
.cornerRadius(15.0)
.shadow(radius: 10.0, x: 20, y: 10)
}
} else {
// in main view you need to give a button where value of showing detail changes to false
// so clicking on that button will poppet this view
MainView(back: $showingDetail)
.edgesIngoringSafeArea(.all)
.transition(.move(.bottom))
}
}
struct MainView: some View{
#binding back: Bool
var body ....
.....
.....
}
I have several vertically-stacked two pairs of rectangles (Rectangle()), each of which has a Button. When the user taps a button, the user needs to be forwarded to a specific View. For instance, if they tap Alice, the user needs to be forwarded to AliceView.
If it's just the matter of one button, forwarding the user from the current View to another isn't a problem. Anyway, the following is what I have.
import SwiftUI
struct ContentView: View {
#State var canAlice: Bool = false
#State var canKim: Bool = false
var body: some View {
NavigationView {
List {
HStack(spacing: 0) {
// ZStack: A view that overlays its children, aligning them in both axes.
ZStack {
Rectangle().frame(width: UIScreen.screenWidth / 2.0, height: UIScreen.screenWidth / 2.0, alignment: .topLeading)
.foregroundColor(.orange)
.border(Color.yellow, width: 2)
Button(action: {
self.canAlice = true
}, label: {
Text("Alice")
.font(.largeTitle)
.background(Color.black)
.foregroundColor(.white)
})
NavigationLink(destination: AliceView(), isActive: $canAlice) { EmptyView() }
}
ZStack {
Rectangle().frame(width: UIScreen.screenWidth / 2.0, height: UIScreen.screenWidth / 2.0, alignment: .topLeading)
.foregroundColor(.orange)
.border(Color.yellow, width: 2)
Button(action: {
self.canKim = true
}, label: {
Text("Kim")
.font(.largeTitle)
.background(Color.black)
.foregroundColor(.white)
})
NavigationLink(destination: KimView(), isActive: $canKim) { EmptyView() }
}
}.listRowInsets(EdgeInsets())
HStack(spacing: 0) {
...
...
...
}.listRowInsets(EdgeInsets())
HStack(spacing: 0) {
...
...
...
}.listRowInsets(EdgeInsets())
HStack(spacing: 0) {
...
...
...
}.listRowInsets(EdgeInsets())
}
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
}
}
Now, if I tap the Alice button, the current View transitions to AliceView, comes back to the current View and then transitions to KimView. How can I have multiple buttons within a single View and forward the user to respective Views? I'm new to SwiftUI. Thanks.
UPDATE
import SwiftUI
struct ContentView: View {
#State private var selection: Int? = 0
var body: some View {
NavigationView {
List {
HStack(spacing: 0) {
// ZStack: A view that overlays its children, aligning them in both axes.
ZStack {
Rectangle().frame(width: UIScreen.screenWidth / 2.0, height: UIScreen.screenWidth / 2.0, alignment: .topLeading)
.foregroundColor(.orange)
.border(Color.yellow, width: 2)
Button(action: {
self.selection = 1
}, label: {
Text("Alice")
.font(.largeTitle)
.background(Color.black)
.foregroundColor(.white)
})
NavigationLink(destination: AliceView(), tag: 1, selection: self.$selection) { EmptyView() }
}
ZStack {
Rectangle().frame(width: UIScreen.screenWidth / 2.0, height: UIScreen.screenWidth / 2.0, alignment: .topLeading)
.foregroundColor(.orange)
.border(Color.yellow, width: 2)
Button(action: {
self.selection = 2
}, label: {
Text("Kim")
.font(.largeTitle)
.background(Color.black)
.foregroundColor(.white)
})
NavigationLink(destination: KimView(), tag: 2, selection: self.$selection) { EmptyView() }
}
}.listRowInsets(EdgeInsets())
HStack(spacing: 0) {
...
...
...
}.listRowInsets(EdgeInsets())
HStack(spacing: 0) {
...
...
...
}.listRowInsets(EdgeInsets())
HStack(spacing: 0) {
...
...
...
}.listRowInsets(EdgeInsets())
}
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
}
}
}
Now, when I tap Alice or Kim, I won't transition to another view and then bounce back to the initial view. Whether I tap Alice or Kim, I transition to AliceView.
Here is a solution (by approach referenced in comment). Tested with Xcode 11.4 / iOS 13.4
Button style to hide a link:
struct LinkButtonStyle<D: View>: ButtonStyle {
let destination: D
#Binding var isActive: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(NavigationLink(destination: self.destination, isActive: $isActive) { EmptyView() })
}
}
and an example of modified cell
ZStack {
Rectangle().frame(width: UIScreen.screenWidth / 2.0, height: UIScreen.screenWidth / 2.0, alignment: .topLeading)
.foregroundColor(.orange)
.border(Color.yellow, width: 2)
Button(action: {
self.canAlice = true
}, label: {
Text("Alice")
.font(.largeTitle)
.background(Color.black)
.foregroundColor(.white)
}).buttonStyle(LinkButtonStyle(destination: AliceView(), isActive: $canAlice))
}
You could achieve this using a single Bool for isActice parameter and a single NavigationLink. Also need to add a destination parameter outside the body. And set the destination in the button action. Here's an example:
struct ContentView: View {
#State var showNextView = false
var destination = GeorgeView()
var body: some View {
NavigationView {
List {
HStack(spacing: 0) {
NavigationLink(destination: destination, isActive: $showNextView) { EmptyView() }
// Rest of your implementation...
Button(action: {
self.destination = KimView()
self.showNextView = true
}, label: {
Text("Kim")
.font(.largeTitle)
.background(Color.black)
.foregroundColor(.white)
})
}
}
}
}
}
As continue research of reverted List in SwiftUI How to make List reversed in SwiftUI.
Getting strange spacing in reverted list, which looks like extra UITableView header/footer.
struct ContentView: View {
#State private var ids = ["header", "test2"]
#State private var text = "text"
init() {
UITableView.appearance().tableFooterView = UIView()
UITableView.appearance().separatorStyle = .none
}
var body: some View {
ZStack (alignment: .bottomTrailing) {
VStack {
List {
ForEach(ids, id: \.self) { id in
Group {
if (id == "header") {
VStack {
Text("Test")
.font(.largeTitle)
.fontWeight(.heavy)
Text("header")
.foregroundColor(.gray)
}
.scaleEffect(x: 1, y: -1, anchor: .center)
} else {
Text(id).scaleEffect(x: 1, y: -1, anchor: .center)
}
}
}
}
.scaleEffect(x:
1, y: -1, anchor: .center)
.padding(.bottom, -8)
Divider()
VStack(alignment: .leading) {
HStack {
Button(action: {}) {
Image(systemName: "photo")
.frame(width: 60, height: 40)
}
TextField("Message...", text: $text)
.frame(minHeight: 40)
Button(action: {
self.ids.insert(self.text, at:0 )
}) {
Image(systemName: "paperplane.fill")
.frame(width: 60, height: 40)
}
}
.frame(minHeight: 50)
.padding(.top, -13)
.padding(.bottom, 50)
}
.foregroundColor(.secondary)
}
}
}
}
Looks not critical but in my more complicated code it shows more spacing.
Ok, turns out it is another SwiftUI bug.
To get around it, you should add .offset(x: 0, y: -1) to the List.
Working example:
struct ContentView: View {
#State private var ids = ["header", "test2"]
#State private var text = ""
init() {
UITableView.appearance().tableFooterView = UIView()
UITableView.appearance().separatorStyle = .none
UITableView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack (alignment: .bottomTrailing) {
VStack {
List(ids, id: \.self) { id in
Group {
if (id == "header") {
VStack(alignment: .leading) {
Text("Test")
.font(.largeTitle)
.fontWeight(.heavy)
Text("header").foregroundColor(.gray)
}
} else {
Text(id)
}
}.scaleEffect(x: 1, y: -1, anchor: .center)
}
.offset(x: 0, y: -1)
.scaleEffect(x: 1, y: -1, anchor: .center)
.background(Color.red)
Divider()
VStack(alignment: .leading) {
HStack {
Button(action: {}) {
Image(systemName: "photo").frame(width: 60, height: 40)
}
TextField("Message...", text: $text)
Button(action: {
self.ids.append(self.text)
}) {
Image(systemName: "paperplane.fill").frame(width: 60, height: 40)
}
}
}
.foregroundColor(.secondary)
}
}
}
}
Note that I changed your code a bit to make it more observable and have less code.