SwiftUI List inside ZStack inside NavigationView - swiftui

I am building a screen with list and custom full screen background color. On the top level I have NavigationView. To paint background I am using ZStack. The problem is that when I wrap list in ZStack and scroll it, list items collapse with NavigationView. I can achieve the correct behavior only if the list is direct child of NavView and has no changed background color.
Code:
init() {
// Title text color
let navBarAppearance = UINavigationBar.appearance()
navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
navBarAppearance.backgroundColor = .clear
// Clear list background color
UITableView.appearance().separatorStyle = .none
UITableViewCell.appearance().backgroundColor = .clear
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().selectionStyle = .none
}
var body: some View {
NavigationView {
ZStack {
LinearGradient(gradient: Gradient(colors: [Color("DarkShade"), Color("PrimaryDark")]), startPoint: .top, endPoint: .bottom)
.edgesIgnoringSafeArea(.all)
List(self.viewModel.rooms.enumerated().map({ $0 }), id: \.element.id) { (index, room) in
ZStack {
RoomsItemView(room: room)
.onAppear(perform: {
let count = self.viewModel.rooms.count
if index == count - 1 {
self.viewModel.loadRooms(currentListSize: count)
}
})
NavigationLink(destination: GuestRoomView(room: room)) {
EmptyView()
}
}
}
}
.pullToRefresh(isShowing: self.$isRefreshing, onRefresh: self.onRefresh)
.navigationBarTitle("rooms_title")
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading:
Button(action: {
self.shouldShowSpotifyAuth = false
}) {
VStack {
Spacer()
HStack {
Image(systemName: "chevron.left")
.resizable()
.scaledToFit()
.frame(height: 20.0)
Spacer()
}
Spacer()
}
.frame(width: 40.0, height: 30.0)
},
trailing:
Button(action: {
print("create room")
}) {
VStack {
Spacer()
HStack {
Spacer()
Image(systemName: "plus")
.resizable()
.scaledToFit()
.frame(height: 20.0)
}
Spacer()
}
.frame(width: 40.0, height: 30.0)
}
)
}
.onAppear(perform: {
self.onRefresh()
})
.onReceive(viewModel.onRoomsUpdate) {
self.isRefreshing = false
}
.gesture(DragGesture().updating($dragOffset, body: { (value, state, transaction) in
if(value.startLocation.x < 20 && value.translation.width > 100) {
self.shouldShowSpotifyAuth = false
}
})
)
}
Expected result:
Expected result
Actual result:
Actual result

Related

I applied .onAppear () {UITableView.appearance (). BackgroundColor = UIColor.clear}, but it doesn't make the screen transparent

**
Hello everybody.
i applied the above indicator, but the image but i still see only about half screen. the bottom remains gray. I applied ".onAppear () {
UITableView.appearance (). BackgroundColor = UIColor.clear} "because I would like to have transparency and see the whole image below and leave only the part of the list highlighted, but under the list I have only the gray screen and not the image. Basically no transparencies are applied through .
'''
var body: some View {
NavigationView {
ZStack {
VStack { // DEFINISCO IL NEW TASK
VStack(spacing: 16) {
TextField("New Task", text: $task)
.padding()
.background(
Color(UIColor.systemGray6)
)
.cornerRadius(10)
Button(action: {
addItem()
}, label: {
Spacer()
Text("SAVE")
Spacer()
})
.disabled(isButtonDisabled)
.padding()
.font(.headline)
.foregroundColor(.white)
.background(isButtonDisabled ? Color.gray : Color.pink)
.cornerRadius(10)
} //: VSTACK
.padding()
List {
ForEach(items) { item in
VStack(alignment: .leading) {
Text(item.task ?? "")
.font(.headline)
.fontWeight(.bold)
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
.font(.footnote)
.foregroundColor(.gray)
}
}
.onDelete(perform: deleteItems)
} //: LIST
.listStyle(InsetGroupedListStyle())
.shadow(color: Color(red: 0, green: 0, blue: 0,opacity: 0.3),
radius: 12)
.padding(.vertical, 0)
.frame(maxWidth: 640)
} //: VSTACK
} //: ZSTACK
.onAppear() {
UITableView.appearance().backgroundColor = UIColor.clear
}
.navigationTitle("Daily Tasks")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
} //: TOOLBAR
.background(
BackgroundImageView()
)
.background(
backgroundGradient.ignoresSafeArea(.all)
)
Text("Select an item")
} //: NAVIGATION VIEW
}

resizable header in SwiftUI

I was trying to make resizable header but it breaks.
I am trying to clone twitter profile,
I think logic is right but can I know why this one is not working?
I made HStack and try to hide it but when I scroll back it can't come back.
Tried with GeometryReader
Please help me, thanks
import SwiftUI
struct ProfileView: View {
// change views
#State var isHide = false
#State private var selectionFilter: TweetFilterViewModel = .tweets
#Namespace var animation
var body: some View {
VStack(alignment: .leading) {
//hiding
if isHide == false {
headerView
actionButtons
userInfoDetails
}
tweetFilterBar
.padding(0)
ScrollView(showsIndicators: false) {
LazyVStack {
GeometryReader { reader -> AnyView in
let yAxis = reader.frame(in: .global).minY
let height = UIScreen.main.bounds.height / 2
if yAxis < height && !isHide {
DispatchQueue.main.async {
withAnimation {
isHide = true
}
}
}
if yAxis > 0 && isHide {
DispatchQueue.main.async {
withAnimation {
isHide = false
}
}
}
return AnyView(
Text("")
.frame(width: 0, height: 0)
)
}
.frame(width: 0, height: 0)
ForEach(0...9, id: \.self) { _ in
TweetRowView()
}
}
}
Spacer()
}
}
}
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView()
}
}
extension ProfileView {
var headerView: some View {
ZStack(alignment: .bottomLeading) {
Color(.systemBlue)
.ignoresSafeArea()
VStack {
Button {
} label: {
Image(systemName: "arrow.left")
.resizable()
.frame(width: 20, height: 16)
.foregroundColor(.white)
.position(x: 30, y: 12)
}
}
Circle()
.frame(width: 72, height: 72)
.offset(x: 16, y: 24)
}.frame(height: 96)
}
var actionButtons: some View {
HStack(spacing: 12){
Spacer()
Image(systemName: "bell.badge")
.font(.title3)
.padding(6)
.overlay(Circle().stroke(Color.gray, lineWidth: 0.75))
Button {
} label: {
Text("Edit Profile")
.font(.subheadline).bold()
.accentColor(.black)
.padding(10)
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.gray, lineWidth: 0.75))
}
}
.padding(.trailing)
}
var userInfoDetails: some View {
VStack(alignment: .leading) {
HStack(spacing: 4) {
Text("Heath Legdet")
.font(.title2).bold()
Image(systemName: "checkmark.seal.fill")
.foregroundColor(Color(.systemBlue))
}
.padding(.bottom, 2)
Text("#joker")
.font(.subheadline)
.foregroundColor(.gray)
Text("Your mom`s favorite villain")
.font(.subheadline)
.padding(.vertical)
HStack(spacing: 24) {
Label("Gothem.NY", systemImage: "mappin.and.ellipse")
Label("www.thejoker.com", systemImage: "link")
}
.font(.caption)
.foregroundColor(.gray)
HStack(spacing: 24) {
HStack {
Text("807")
.font(.subheadline)
.bold()
Text("following")
.font(.caption)
.foregroundColor(.gray)
}
HStack {
Text("200")
.font(.subheadline)
.bold()
Text("followers")
.font(.caption)
.foregroundColor(.gray)
}
}
.padding(.vertical)
}
.padding(.horizontal)
}
var tweetFilterBar: some View {
HStack {
ForEach(TweetFilterViewModel.allCases, id: \.rawValue) { item in
VStack {
Text(item.title)
.font(.subheadline)
.fontWeight(selectionFilter == item ? .semibold : .regular)
.foregroundColor(selectionFilter == item ? .black : .gray)
ZStack {
Capsule()
.fill(Color(.clear))
.frame(height: 3)
if selectionFilter == item {
Capsule()
.fill(Color(.systemBlue))
.frame(height: 3)
.matchedGeometryEffect(id: "filter", in: animation)
}
}
}
.onTapGesture {
withAnimation(.easeInOut) {
self.selectionFilter = item
}
}
}
}
}
}
While the animation between show and hide is running, the GeometryReader is still calculating values – which lets the view jump between show and hide – and gridlock.
You can introduce a new #State var isInTransition = false that checks if a show/hide animation is in progress and check for that. You set it to true at the beginning of the animation and to false 0.5 secs later.
Also I believe the switch height is not exactly 1/2 of the screen size.
So add a new state var:
#State var isInTransition = false
and in GeometryReader add:
GeometryReader { reader -> AnyView in
let yAxis = reader.frame(in: .global).minY
let height = UIScreen.main.bounds.height / 2
if yAxis < 350 && !isHide && !isInTransition {
DispatchQueue.main.async {
isInTransition = true
withAnimation {
isHide = true
}
}
// wait for animation to finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isInTransition = false
}
} else if yAxis > 0 && isHide && !isInTransition {
DispatchQueue.main.async {
isInTransition = true
withAnimation {
isHide = false
}
}
// wait for animation to finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isInTransition = false
}
}
return AnyView(
// displaying values for test purpose
Text("\(yAxis) - \(isInTransition ? "true" : "false")").foregroundColor(.red)
// .frame(width: 0, height: 0)
)
}

Can't sheet with button in ListView SwiftUI

I have a list. In the List there are 2 buttons.
I want to click on each button to present another view with Sheet.
When I first click it it works, but the second time or another tap button it doesn't present the view. Hope you can help me.
My code
My design
Read up on how to use Buttons and sheets. Typically Buttons triggering a sheet is used like this:
EDIT: with suggestion from Philip Dukhov, replace:
Button("Learning") {
}.sheet(isPresented: $isLearning, content: {
LearningView()
})
.onTapGesture {
self.isLearning = true
}
with:
Button(action: {isLearning = true}) {
Text("Learning")
}
.sheet(isPresented: $isLearning) {
LearningView()
}
and do the same for "Test" button, with "isTest" instead of "isLearning" and "TestViewCategory()" instead of "LearningView()".
EDIT 2:
Update "TestView" with:
struct TestView: View {
var title = ""
let models = modelItems
var body: some View {
ScrollView {
VStack {
ForEach(models) { model in
TopicList(model: model)
}
}
}
.navigationBarTitle(title, displayMode: .inline)
.onAppear {
UITableView.appearance().separatorStyle = .none
}
.animation(.spring())
}
}
EDIT 3: here is the test code that works for me:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TestView()
}
}
}
struct ButtonView: View {
#State var isLearning: Bool = false
#State var isTest: Bool = false
var body: some View {
HStack {
Button(action: {isLearning = true}) {
Text("Learning")
}
.sheet(isPresented: $isLearning) {
Text("LearningView")
}
.font(.system(size: 16))
.frame(width: 132 , height: 48)
.background(Color(.white))
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color(#colorLiteral(red: 0.9259920716, green: 0.9261471629, blue: 0.9259716272, alpha: 1)), lineWidth: 1)
)
Button(action: {isTest = true}) {
Text("Test")
}
.sheet(isPresented: $isTest) {
Text("TestViewCategory")
}
.font(.system(size: 16))
.frame(width: 132 , height: 48)
.background(Color(.white))
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color(#colorLiteral(red: 0.9259920716, green: 0.9261471629, blue: 0.9259716272, alpha: 1)), lineWidth: 1)
)
}
}
}
struct TopicList: View {
// for testing
let model: String
#State private var showSubItem = false
var body: some View {
VStack {
HStack {
Image(systemName: showSubItem ? "arrow.up.circle" : "arrow.down.circle")
.resizable()
.frame(width: 26, height: 26)
.onTapGesture {
withAnimation {
showSubItem.toggle()
}
}
VStack {
VStack(alignment: .leading) {
Text("title")
.font(.custom("Poppins-Regular", size: 24))
.padding(.top,9)
.padding(.bottom,4)
HStack {
Text("Open date")
.font(.custom("Poppins-Regular", size: 12))
Text("Open date")
.font(.custom("Poppins-Regular", size: 12))
Text("Due date")
.font(.custom("Poppins-Regular", size: 12))
Text("Due date")
.font(.custom("Poppins-Regular", size: 12))
}
}
.padding(.leading,17)
.frame(width: 320, height: 70)
.fixedSize(horizontal: false, vertical: true)
if showSubItem {
ButtonView()
.padding(.top,12)
.fixedSize(horizontal: false, vertical: true)
.transition(.opacity)
.transition(.slide)
.padding(.bottom,13)
}
}
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color(#colorLiteral(red: 0.9259920716, green: 0.9261471629, blue: 0.9259716272, alpha: 1)), lineWidth: 1)
)
}
}
}
}
struct TestView: View {
var title = "nav title"
let models = ["1","2","3"]
var body: some View {
ScrollView {
VStack {
ForEach(models, id: \.self) { model in
TopicList(model: model)
}
}
}
.navigationBarTitle(title, displayMode: .inline)
.onAppear {
UITableView.appearance().separatorStyle = .none
}
.animation(.spring())
}
}

SwiftUI: Show specific views with an animation delay

I'm trying to achieve the following animation: When I tap on a rectangle the rectangle should be expanded to the full width with an close button in the corner and below this rectangle a ScrollView should appear. This works so far without any problems. Now I would like to display the ScrollView a little bit later then the expanded rectangle. So when I tap on the rectangle: First the expanded rectangle with the close button should appear and 3 seconds later the ScrollView.
struct Playground: View {
#Namespace var namespace
#State var show = false
private let gridItems = [GridItem(.flexible())]
var body: some View {
if show {
VStack{
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: show)
.frame(height: 300)
.frame(maxWidth: .infinity)
Image(systemName: "xmark")
.font(.system(size: 25))
.foregroundColor(.white)
.background(Color.red)
.padding(20)
.onTapGesture {
withAnimation(.spring()){
self.show = false
}
}
}
// SHOW THIS SCROLLVIEW 3 SECONDS LATER
ScrollView{
LazyVGrid(columns: gridItems){
ForEach(0..<10){ cell in
Text("\(cell)")
}
}
}
.animation(Animation.spring().delay(3)) // doesn't work!
}
} else {
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: !show)
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation(.spring()){
self.show = true
}
}
}
}
}
We need to make separated animation (and related state) for ScrollView in this scenario.
Here is possible approach. Tested with Xcode 12.1 / iOS 14.1
struct Playground: View {
#Namespace var namespace
#State var show = false
private let gridItems = [GridItem(.flexible())]
#State private var showItems = false
var body: some View {
if show {
VStack{
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: show)
.frame(height: 300)
.frame(maxWidth: .infinity)
Image(systemName: "xmark")
.font(.system(size: 25))
.foregroundColor(.white)
.background(Color.red)
.padding(20)
.onTapGesture {
withAnimation(.spring()){
self.show = false
}
}
}
VStack {
if showItems {
ScrollView{
LazyVGrid(columns: gridItems){
ForEach(0..<10){ cell in
Text("\(cell)")
}
}
}
} else {
Spacer()
}
}
.onAppear { showItems = true }
.onDisappear { showItems = false }
.animation(Animation.spring().delay(3), value: showItems)
}
} else {
Rectangle()
.matchedGeometryEffect(id: "A", in: namespace, isSource: !show)
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation(.spring()){
self.show = true
}
}
}
}
}

SwiftUI list row layout

Back with a couple of SwiftUI layout questions :
I'm trying to display 2 lists side by side with custom cells.
I created the cell views (EventRow.swift) and I display them in my content view.
I added a border to my lists, for better visibility.
As you can see from the picture below, the result is ghastly:
I would like the gradient effect to be applied to the whole cell, width and height wise.
I tried setting the frame of my EventRow (using .infinity for width and height), but this crashes the app.
Since the size of EventRow is inferred, I also don't know how to adapt my row cells height to its size : you can see the horizontal delimitating bars are not fitted to my custom EventRow...
If anyone has some pointers for this, it would be greatly appreciated.
The sample project can be found here
But I also post my code below:
ContentView :
import SwiftUI
struct ContentView: View {
struct listsSetup: ViewModifier {
func body(content: Content) -> some View {
return content
.frame(maxHeight: UIScreen.main.bounds.size.height/3)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.blue, lineWidth: 1))
.padding([.top, .bottom])
}
}
var body: some View {
VStack {
HStack {
VStack { // 1 list Vstack
VStack {
Text("List 1")
.padding(.top)
List {
EventRow()
EventRow()
} // END of 1st List
}
.modifier(listsSetup())
} // END of 1st list VStack
VStack { // 2nd Vstack
VStack {
Text("List 2")
.padding(.top)
List {
EventRow()
EventRow()
} // END of Landings List
}
.modifier(listsSetup())
} // End of 2nd List VStack
} // End of 1st & 2nd lists HStack
.padding(.top)
Spacer()
} // END of VStack
} // END of body
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
EventRow :
import SwiftUI
struct EventRow: View {
var body: some View {
LinearGradient(gradient: Gradient(colors: [Color.white, Color.blue]), startPoint: .top, endPoint: .bottom)
.edgesIgnoringSafeArea(.all)
.overlay(
VStack{
HStack {
Text("Text one")
Spacer()
Text("Text two")
}
HStack {
Spacer()
Image(systemName: "flame")
.font(.body)
Spacer()
} // END of second HStack
.padding(.top, -14)
} //END of Vstack
)
}
}
struct EventRow_Previews: PreviewProvider {
static var previews: some View {
EventRow().previewLayout(.fixed(width: 300, height: 60))
}
}
Edit after trying Asperi's solution :
My actual EventRow code is as follows, and the listRowBackground modifier doesn't seem to have any effect :
import SwiftUI
import CoreData
struct EventRow: View {
var event: Events
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "dd MMM yy"
return formatter
}
var body: some View {
VStack{
HStack {
Text(event.airportName ?? "")
.font(.headline)
Spacer()
Text(self.dateFormatter.string(from: event.eventDate!))
.font(.body)
}
HStack {
Text(event.flightNumber ?? "")
.font(.body)
Spacer()
if event.isSimulator {
Image(systemName: "s.circle")
.font(.body)
} else {
Image(systemName: "airplane")
.font(.body
)
}
Spacer()
if event.aircraftType == 0 {
Text("")
.font(.body)
} else if event.aircraftType == 1 {
Text("330")
.font(.body)
} else if event.aircraftType == 2 {
Text("350")
.font(.body)
}
} // END of second HStack
.padding(.top, -14)
} //END of Vstack
.listRowBackground(LinearGradient(gradient: Gradient(colors: [Color.white, Color.blue]), startPoint: .top, endPoint: .bottom))
}
}
struct EventRow_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let newEvent = Events(context: context)
newEvent.eventDate = Date()
newEvent.aircraftType = 1
newEvent.airportName = "LDG tst"
newEvent.flightNumber = "AF TEST"
newEvent.id = UUID()
newEvent.isLanding = true
newEvent.isSimulator = false
return EventRow(event: newEvent).environment(\.managedObjectContext, context)
.previewLayout(.fixed(width: 300, height: 60))
}
}
Here is a solution to make gradient row-wide
struct EventRow: View {
var body: some View {
VStack{
HStack {
Text("Text one")
Spacer()
Text("Text two")
}
HStack {
Spacer()
Image(systemName: "flame")
.font(.body)
Spacer()
} // END of second HStack
.padding(.top, -14)
} //END of Vstack
.listRowBackground(LinearGradient(gradient: Gradient(colors: [Color.white, Color.blue]), startPoint: .top, endPoint: .bottom)
)
}
}