resizable header in SwiftUI - 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)
)
}

Related

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())
}
}

onTapGesture not work in SwiftUI ScrollView

I want to make a segment tab page in SwiftUI such as bellow.
My code is as bellow:
struct ContentView: View {
#State var index = 0
#State private var offset: CGFloat = 0
var body: some View {
VStack(spacing: 0) {
HStack() {
Button("tab1") {
self.index = 0
}
Button("tab2") {
self.index = 1
}
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
VStack() {
List {
VStack {
HStack() {
Text("tab1 line1")
Spacer()
}
.frame(height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab1 line1")
}
}
VStack {
HStack() {
Text("tab1 line2")
Spacer()
}
.frame(height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab1 line2")
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.cornerRadius(30)
VStack() {
List {
VStack {
HStack() {
Text("tab2 line1")
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab2 line1")
}
}
VStack {
HStack() {
Text("tab2 line2")
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab2 line2")
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.onAppear() {
print("load")
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.cornerRadius(30)
}
}
.content
.offset(x: self.getOffset())
.animation(.spring())
.frame(width: UIScreen.main.bounds.size.width, alignment: .leading)
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
.background(Color.gray)
}
private func getOffset() -> CGFloat {
let offset = CGFloat(self.index) * (-UIScreen.main.bounds.size.width)
return offset
}
}
Everything works well expect the onTapGesture in tab2. When tap the line of "tab1",the onTapGesture works well to print "click tab1 line1" or "click tab1 line2".
After click button "tab2", the page will scroll to tab2 page. When click the line of list "tab2", onTapGesture not works.
I try a day, and found two methods to make it works:
After scroll to page "tab2", Scroll up or down the tab2 list, the onTapGesture will works to print "click tab2 line1" and "click tab2 line2".
remove the code .cornerRadius(30), the click in tab2 page will also works.
I just want to keep the radius of list. How to fix this incredible bug?
The problem is here:
.content
.offset(x: self.getOffset())
This duplicates the scrollview's content and offsets it to the right. As a result, what you are tapping is the duplicated content, which probably affects the tap gesture.
Instead, use ScrollViewReader to scroll the ScrollView, and remove your getOffset code.
struct ContentView: View {
#State var index = 0
#State private var offset: CGFloat = 0
var body: some View {
VStack(spacing: 0) {
HStack() {
Button("tab1") {
self.index = 0
}
Button("tab2") {
self.index = 1
}
}
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { proxy in
HStack(alignment: .center, spacing: 0) {
VStack() {
List {
VStack {
HStack() {
Text("tab1 line1")
Spacer()
}
.frame(height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab1 line1")
}
}
VStack {
HStack() {
Text("tab1 line2")
Spacer()
}
.frame(height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab1 line2")
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.cornerRadius(30)
.id(0) /// set id here
VStack() {
List {
VStack {
HStack() {
Text("tab2 line1")
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab2 line1")
}
}
VStack {
HStack() {
Text("tab2 line2")
Spacer()
}
.frame(width: UIScreen.main.bounds.size.width, height: 126)
.contentShape(Rectangle())
.onTapGesture {
print("click tab2 line2")
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.onAppear() {
print("load")
}
}
.frame(width: UIScreen.main.bounds.size.width, height: 300)
.cornerRadius(30)
.id(1) /// also set id here
}
.onChange(of: index) { _ in /// scroll the ScrollView
withAnimation(.spring()) {
proxy.scrollTo(index)
}
}
}
}
.frame(width: UIScreen.main.bounds.size.width, alignment: .leading)
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
.background(Color.gray)
}
}

Pass to Another View after Success View Appears

After seeing the popup view that says your credit card saved successfully. I want to see this popup for 2-3 seconds then to pass another view called AddressView. Maybe it is irrelevant but I also added that popup view and the name is SuccessCardView.
#State private(set) var successAlert = false
ZStack {
HStack {
Button(action: {
self.isListTapped.toggle()
}, label: { CustomButton(title: "Listeden Sec", icon: .none, status: .enable, width: 150)})
Button(action: {
self.isSaved.toggle()
self.creditCards.append(self.creditCard)
print(self.creditCards[0].cardNumber)
if self.creditCards[0].cardNumber == "" {
self.showingAlert = true
} else if self.creditCards[0].cardNumber.count == 16 {
self.successAlert = true
}
}, label: { CustomButton(title: "Kaydet", icon: .none, status: .enable, width: 150)})
.alert(isPresented: $showingAlert) {
Alert(title: Text("Kart Bilgileri Hatali"), message: Text("Tekrar Kontrol Edin"), dismissButton: .default(Text("Got it!")))
}
}
SuccessCardView(isShown: $successAlert) // I want to show that view than jump to another view
}
struct SuccessCardView: View {
#Binding var isShown: Bool
#State var viewState = CGSize.zero
var body: some View {
VStack {
ZStack {
Rectangle()
.foregroundColor(Color(#colorLiteral(red: 0, green: 0.6588235294, blue: 0.5254901961, alpha: 1)))
.cornerRadius(10)
.frame(width: 355, height: 76)
HStack {
VStack(alignment: .leading) {
Text("Tebrikler!")
.font(Font.custom("SFCompactDisplay-Bold", size: 16))
.foregroundColor(.white)
Text("Kart Basariyla Eklendi")
.font(Font.custom("SFCompactDisplay", size: 14))
.foregroundColor(.white)
}
.offset(x: -70)
}
}
Spacer()
}
.offset(y: isShown ? .zero : -UIScreen.main.bounds.size.height)
.offset(y: viewState.height )
.animation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0))
.offset(y: -100)
}
}
It is not clear where/how is AddressView configured, but you can do the following
} else if self.creditCards[0].cardNumber.count == 16 {
self.successAlert = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.successAlert = false // hide popup
self.showAddressView = true // eg. activate navigation link
}
}

SwiftUI List inside ZStack inside NavigationView

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

SwiftUI ActionSheet Picker

I'm trying to create in SwiftUI an action sheet that appears after pressing a button and allow the user to select and return an item throught a picker (like this https://imgur.com/a/IbS7swX).
Any hint on how to do it?
Thanks
struct ContentView: View {
init() {
UITableView.appearance().separatorColor = .clear
}
var inputArray = ["100","101","102"]
#State var slectedSegmant = "ActionSheet"
#State var slectedObj = "101"
#State var enableSheet = false
var test = false
var body: some View {
ZStack {
VStack(spacing: 10) {
Picker(selection: $slectedSegmant, label: Text("Segment")) {
Text("Alert").tag("Alert")
Text("ActionSheet").tag("ActionSheet")
}.pickerStyle(SegmentedPickerStyle())
.labelsHidden()
.padding(EdgeInsets.init(top: 50, leading: 10, bottom: 0, trailing: 10))
Text("Alert & Pickers")
.font(Font.system(size: 35, design: .rounded))
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
List((0...50),id: \.self) { input in
ZStack {
HStack(spacing: 10) {
Image(systemName: "book")
.font(.title)
.padding(.leading, 10)
VStack(alignment: .leading, spacing: 5, content: {
Text("Simple")
Text("3 different buttons")
})
Spacer()
}.padding(.vertical)
.frame(maxWidth:.infinity)
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.white).shadow(radius: 1.5)
)
Button(action: {
self.enableSheet = true
}) {
Text("")
}
}
}.padding()
}.blur(radius: $enableSheet.wrappedValue ? 1 : 0)
.overlay(
$enableSheet.wrappedValue ? Color.black.opacity(0.6) : nil
)
if $enableSheet.wrappedValue {
GeometryReader { gr in
VStack {
VStack {
Text("PickerView")
.font(.headline)
.foregroundColor(.gray)
.padding(.top, 10)
Text("Prefered ContentHeight")
.padding(.top, 5)
Picker("test", selection: self.$slectedObj) {
Text("100").id("100")
Text("101").id("101")
Text("101").id("102")
}.labelsHidden()
}.background(RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.white).shadow(radius: 1))
VStack {
Button(action: {
debugPrint("Done Selected")
self.enableSheet = false
}) {
Text("Done").fontWeight(Font.Weight.bold)
}.padding()
.frame(maxWidth: gr.size.width - 90)
.background(RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.white).shadow(radius: 1))
}
}.position(x: gr.size.width / 2 ,y: gr.size.height - 200)
}
}
}.edgesIgnoringSafeArea(.all)
}
}
OUTPUT