How can i animate data fetched from API in my ScrollView/LazyVGrid? - ios

This is my main view where i'm using AsyncAwait to fetch the data. I wanna have the effect of nice animation when i receive the data. Tried a lot of things, but none worked. Some animations were successful but only after opening the specific view for the 3-rd + time. On first opening i could never achieve that effect. Would really appreciate if someone could tell me what should i do. Thanks in advance :)
struct PresentationRootView: View {
// setting up the default value for segmented control
#State private var selectedSegmentedOption: SegmentedSelection = .myPresentations
#ObservedObject var viewModel: PresentationsViewModel
var body: some View {
let columns = [GridItem(.flexible())]
VStack(spacing: 0) {
Picker(selection: $selectedSegmentedOption) {
Text("My Presentations").tag(SegmentedSelection.myPresentations)
Text("Templates").tag(SegmentedSelection.templates)
} label: {}
.pickerStyle(.segmented)
}
ScrollView {
if selectedSegmentedOption == .myPresentations {
if viewModel.shouldShowEmptyScreen {
Text("No data")
}
LazyVGrid(columns: columns, spacing: 4, content: {
ForEach($viewModel.myPresentations, id: \.ID) { element in
PresentationSingleView(url: element.ThumbURLWithSas.wrappedValue, title: element.Name.wrappedValue, numberOfResources: 4)
.padding([.leading, .trailing], 13)
}
}).task {
await viewModel.getAllPresentations(page: 1, pageSize: 10)
}
}
}
.background(CustomColor.background.swiftUIColor)
Spacer()
.navigationTitle("Presentations")
}
}
struct PresentationSingleView: View {
// replace this with the real data
var url: URL
var title: String
var numberOfResources: Int
var isIphone = DefaultAppManager.shared.isIphone
#StateObject var image = FetchImage()
var body: some View {
HStack(alignment: .top) {
image.view?
.resizable()
.scaledToFill()
.frame(maxWidth: isIphone ? 90 : 140, maxHeight: isIphone ? 64 : 140)
.cornerRadius(4)
.padding(isIphone ? 10 : 20)
VStack(alignment: .leading) {
Text(title)
.font(Font.custom("Roboto-Regular", size: 12))
.padding(.top, 20)
Spacer()
HStack(alignment: .center) {
Image("presentationResourceIcon")
Text("\(numberOfResources) resources")
.font(Font.custom("Roboto-Medium", size: 12))
Spacer()
Image("more_options")
.onTapGesture {
print("tapped on right image!!!")
}
}
.padding(.bottom, 20)
}
Spacer()
}
.onAppear {
image.priority = .normal
image.load(url)
}
.frame(height: isIphone ? 90 : 150)
.background(Color.white)
.cornerRadius(8)
}
}

Related

How to hide TabView in NavigationLink?

First of all I did a full research for my problem and NOTHING on Google helped me,
the problem is simple, it seems that the solution should also be simple, but I can't hide the tabbar in NavigationLink, and if something works out, then wierd behavior of the buttons and the transition back, etc...
TabView itself
import SwiftUI
struct Main: View {
#State var currentTab: Int = 0
var body: some View {
TabView(selection: $currentTab) {
HomeView().tag(0)
AccountInfoView().tag(1)
SettingsView().tag(2)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.edgesIgnoringSafeArea(.bottom)
.overlay(
TabBarView(currentTab: $currentTab),
alignment: .top
)
}
}
struct TabBarView: View {
#Binding var currentTab: Int
#Namespace var namespace
var tabBarOptions: [String] = ["Search", "Items", "Account"]
var body: some View {
HStack(spacing: 0) {
ForEach(Array(zip(self.tabBarOptions.indices,
self.tabBarOptions)),
id: \.0,
content: {
index, name in
TabBarItem(currentTab: self.$currentTab,
namespace: namespace.self,
tabBarItemName: name,
tab: index)
})
}
.padding(.top)
.background(Color.clear)
.frame(height: 100)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarItem: View {
#Binding var currentTab: Int
let namespace: Namespace.ID
var tabBarItemName: String
var tab: Int
var body: some View {
Button {
self.currentTab = tab
} label: {
VStack {
Spacer()
Text(tabBarItemName)
if currentTab == tab {
CustomColor.myColor
.frame(height: 2)
.matchedGeometryEffect(id: "underline",
in: namespace,
properties: .frame)
} else {
Color.clear.frame(height: 2)
}
}
.animation(.spring(), value: self.currentTab)
}
.fontWeight(.heavy)
.buttonStyle(.plain)
}
}
NavigationLink -> this is just the part of the code that contains the NavigationLink, this VStack of course is inside the NavigationView.
struct HomeView: View {
NavigationView {
...
VStack(spacing: 15) {
ForEach(self.data.datas.filter {(self.search.isEmpty ? true : $0.title.localizedCaseInsensitiveContains(self.search))}, id: \.id) { rs in
NavigationLink(
destination: ItemDetails(data: rs)
){
RecentItemsView(data: rs)
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
ItemDetails
struct ItemDetails: View {
let data: DataType
var body : some View {
NavigationView {
VStack {
AsyncImage(url: URL(string: data.pic), content: { image in
image.resizable()
}, placeholder: {
ProgressView()
})
.aspectRatio(contentMode: .fill)
.frame(width: 250, height: 250)
.clipShape(RoundedRectangle(cornerRadius: 12.5))
.padding(10)
VStack(alignment: .leading, spacing: 8, content: {
Text(data.title)
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .center)
Text(data.description)
.font(.caption)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .center)
})
.padding(20)
}
.padding(.horizontal)
}
.navigationBarBackButtonHidden(true)
}
}
I apologize for the garbage in the code, it seemed to me that there is not much of it and it does not interfere with understanding the code, also during the analysis of this problem on the Google\SO, I did not need to work with other parts of the code anywhere, except for those that I provided above, but if I missed something, then please let me know, thanks.

Data loss on View Class, when keyboard appears

When ever I click on textField and key board appears, my list of data i.e coming from API is vanashing,
import SwiftUI
import ExytePopupView
struct Wallet: View {
#State private var searchText = ""
#State private var showCancelButton: Bool = false
#ObservedObject var walltetVM = ShopViewModel()
#State var showShopDetails: Bool = false
#State var openShowDetails: Bool = false
#State var selectedShopId:String = ""
#State var selectedCouponDetail: CouponModel?
var body: some View {
NavigationView {
VStack {
// Search view
VStack{
HStack {
Button {
} label: {
Image(systemName: "magnifyingglass")
.foregroundColor(AppColors.semiBlue)
.frame(width: 20, height: 20, alignment: .center)
.padding()
}
ZStack(alignment: .leading) {
if searchText.isEmpty {
Text(MaggnetLocalizedString(key: "Restaurant, Beauty shop...", comment: ""))
.foregroundColor(AppColors.blackWhite)
.font(Font(AppFont.lightFont(lightFontWithSize: 15)))
}
TextField("", text: $searchText)
.font(Font(AppFont.regularFont(regularFontWithSize: 15)))
}
.foregroundColor(AppColors.blackWhite)
.padding(.horizontal,10)
.padding(.leading,-15)
Divider()
Button {
} label: {
HStack {
Image("places")
}
.padding(.horizontal,20)
.padding(.leading,-12)
}
}
.frame(height: 45)
.background(AppColors.fadeBackground)
.clipShape(Capsule())
}
.padding(.horizontal)
.padding(.vertical,10)
ScrollView(.vertical) {
VStack{
NavigationLink(destination:ShopDetail(shopId:self.selectedShopId)
.environmentObject(walltetVM),
isActive: $openShowDetails) {
EmptyView()
}.hidden()
Points()
ForEach(0..<walltetVM.finalsCouponList.count,id: \.self){
index in
VStack{
// SHOP LIST HEADERS
HStack{
Text(walltetVM.finalsCouponList[index].name)
.multilineTextAlignment(.leading)
.font(Font(AppFont.mediumFont(mediumFontWithSize: 15)))
.foregroundColor(AppColors.blackWhite)
.padding(.horizontal,10)
Spacer()
Button {
} label: {
Text(MaggnetLocalizedString(key: "viewAll", comment: ""))
.font(Font(AppFont.regularFont(regularFontWithSize: 12)))
.foregroundColor(AppColors.blackWhite)
.padding()
.frame(height: 27, alignment: .center)
.background(AppColors.fadeBackground)
.cornerRadius(8)
}
}
.padding()
// MAIN SHOP LIST
VStack{
ScrollView(.horizontal,showsIndicators: false){
HStack{
ForEach(0..<walltetVM.finalsCouponList[index].couopons.count,id: \.self){
indeX in
Shops(coupons: walltetVM.finalsCouponList[index].couopons[indeX])
.onTapGesture {
selectedShopId = walltetVM.finalsCouponList[index].couopons[indeX].businessId?.description ?? ""
print(selectedShopId)
selectedCouponDetail = walltetVM.finalsCouponList[index].couopons[indeX]
showShopDetails = true
}
}
}
.padding(.horizontal)
}
}
.padding(.top,-5)
}
.padding(.top,-5)
}
}
}
.blur(radius: showShopDetails ? 3 : 0)
.popup(isPresented: $showShopDetails, autohideIn: 15, dismissCallback: {
showShopDetails = false
}) {
ShopDetailPopUp(couponDeatil: self.selectedCouponDetail)
.frame(width: 300, height: 400)
}
.navigationBarTitle(Text("Wallet"),displayMode: .inline)
.navigationBarItems(trailing: HStack{
Button {
} label: {
Image("wishIcon").frame(width: 20, height: 20, alignment: .center)
}
Button {
} label: {
Image("notifIcon").frame(width: 20, height: 20, alignment: .center)
}
})
.resignKeyboardOnDragGesture()
}
.onAppear(){
walltetVM.getWallets()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.openShopDetails))
{ obj in
showShopDetails = false
openShowDetails.toggle()
}
.environment(\.layoutDirection,Preferences.chooseLanguage == AppConstants.arabic ? .rightToLeft : .leftToRight)
}
}
}
struct Wallet_Previews: PreviewProvider {
static var previews: some View {
Wallet()
}
}
bannerList is marked as #Published, API call working fine, but in same View class I have one search text field , when I tap on it all the data I was rendering from API get lost and disappears from list.
You are holding a reference to your view model using ObservedObject:
#ObservedObject var walltetVM = ShopViewModel()
However, because you are creating the view model within the view, the view might tear down and recreate this at any moment, which might be why you are losing your data periodically.
If you use StateObject, this ensures that SwiftUI will retain the object for as long as the view lives, so the object won't be recreated.
#StateObject var walltetVM = ShopViewModel()
Alternatively, you could create the view model outside of the view and inject it into the view and keep using ObservedObject (but you'll still need to make sure the object lives as long as the view).
I found something working but still not optimised solution
instead of calling it on onAppear, I called API function in init method.
init(){
walltetVM.getWallets()
}

How can I scale notification text based on length?

I am making a notification feed where you can view the WHOLE comment a user posts on a blog but I am unable to get the whole multi-line comment to show without affecting the spacing/alignment of elements to match the other notifications.
I was able to make the whole comment show by typing in a constant .frame(height: 100) modifier but then EVERY comment notification has that sized of frame.
Is there a way to make the below VStack scale it's frame dynamically based on the comment length or is there a better way based on my code??
Thank you!
struct CommentNotificationCell: View {
#EnvironmentObject var session: SessionStore
var activity: CommentActivity?
var body: some View {
HStack(spacing: 10) {
if let activity = activity {
VStack {
KFImage(URL(string: "\(url)" )!)
Spacer()
}
// Should I scale this VStack's frame height dynamically based
// on the length of the comment..?
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 3) {
Text(activity.username)
.font(.caption)
Text("commented on your post:")
.font(.caption)
}
Text(activity.comment)
.font(.caption)
Text(activity.createdAt.timeAgoDisplay())
.font(.caption)
}.padding(.leading, 10)
Spacer()
VStack {
ZStack {
Image(systemName: "bubble.left.fill")
.font(.system(size: 13, weight: .regular))
.foregroundColor(.blue)
.zIndex(1)
.offset(x: -25,y: -20)
KFImage(URL(string: "\(url)" )!)
.zIndex(0)
}
Spacer()
}
}
}
}
}
I think your code is already doing that – scaling dynamically based on text length. I cleaned it up a little and for me it works. Is there some other place where you set a specific .frame?
struct ContentView: View {
#State private var test = false
let username = "funnytest"
let shortComment = "kjfdglkj ewoirewoi oikdjsfgdlsfgj 0ewiropsdisg"
let longComment = "kjfdglkj ewoirewoi oikdjsfgdlsfgj 0ewiropsdisg poksaf#ldsifsgali oeirpo dgiodfig odfi ofdgifgpoüi fhfdi mfoidgho miohgfm ogifhogif hiogfh fgihi oogihofgi hofgiho fgopihfgoih pfdgihdfg podfgihofgiho po fdgdfiopugiouü"
var body: some View {
VStack {
CommentNotificationCell(username: username, comment: shortComment)
CommentNotificationCell(username: username, comment: longComment)
}
}
}
struct CommentNotificationCell: View {
var username: String
var comment: String
var body: some View {
HStack(spacing: 10) {
Image(systemName: "person.circle")
.resizable().scaledToFit().frame(width: 60)
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 3) {
Text(username)
.font(.caption).bold()
Text("commented on your post:")
.font(.caption)
}
Text(comment)
.font(.caption)
Text("8 hours ago")
.font(.caption)
}.padding(.leading, 10)
Spacer()
ZStack {
Image(systemName: "bubble.left.fill")
.font(.system(size: 13, weight: .regular))
.foregroundColor(.blue)
.zIndex(1)
.offset(x: -25,y: -20)
// Image("URL(string: "\(url)" )!")
// .zIndex(0)
}
}
}
}
I found out the fix was to remove the Spacer() in the first VStack
VStack {
KFImage(URL(string: "\(url)" )!)
}

How to add more padding bellow a TextView when the keyboard is shown

When i have TextField inside ScrollView and tap on it the keyboard is shown as expected. But it seems that the TextField is moved up just enough to show the input area but i want to be moved enough so that is visible in its whole. Otherwise it looks cropped. I couldn't find a way to change this behaviour.
struct ContentView: View {
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
}
.padding()
.background(.red)
}
}
}
Use a .safeAreaInset modifier.
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
}
.padding()
.background(.red)
}.safeAreaInset(edge: .bottom) { //this will push the view when the keyboad is shown
Color.clear.frame(height: 30)
}
}
You can provide additional padding to the view (and it works even in iOS 13 and 14):
struct ContentView: View {
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
.padding(.bottom, 32) //here, set as much pasding as you want
}
.padding()
.background(.red)
}
}
}

SwiftUI list not showing all items

I am trying to show a list of side-scrolling list in iOS. Some of the lists show but others don't, even though they all have data. I put a debug point and see that the ForEach with EventItemView is called for every section. I am unsure of what I am doing wrong.
EventScreen
struct EventScreen: View {
#State
var currentPage: Int = 0
var viewControllers =
IncentiveSource().getFeaturedIncentives().map({ incentive in
UIHostingController(rootView: EventFeatureView(event: incentive.toEvent()))
})
var response: [EventSection] = IncentiveSource().getIncentives().sections.toEventSections()
var body: some View {
NavigationView {
List {
EventViewController(controllers: self.viewControllers, currentPage: self.$currentPage)
.listRowInsets(EdgeInsets())
.frame(height: 600)
ForEach(self.response) { section in
EventSectionView(eventSection: section)
}
}
.navigationBarTitle(Text("Events").foregroundColor(Color.black), displayMode: .inline)
}
}
}
EventSectionView
struct EventSectionView: View {
var eventSection: EventSection
var body: some View {
VStack(alignment: .leading) {
SectionTextView(text: eventSection.category.typeName())
.frame(alignment: .leading)
ScrollView(.horizontal, showsIndicators: true) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.eventSection.events) { event in
EventItemView(event: event)
}
}
}
}
}
}
struct SectionTextView: View {
var text: String
var body: some View {
return Text(text)
.bold()
.font(.system(size: 18, weight: .heavy))
.foregroundColor(Color(ColorTheme.brandBlue.value))
.padding(.bottom, 4)
}
}
EventItemView
struct EventItemView: View {
var event: Event
var body: some View {
VStack {
Color.red
.frame(width: 100, height: 100, alignment: .center)
.cornerRadius(5)
Text(event.title)
.bold()
.frame(width: 100, alignment: .leading)
.foregroundColor(.white)
.font(.system(size: 10))
Text(event.date)
.frame(width: 100, alignment: .leading)
.foregroundColor(.white)
.font(.system(size: 10))
}
.padding(.trailing, 8)
}
}
It needs to make each horizontal scroller in EventSectionView unique like below
struct EventSectionView: View {
var eventSection: EventSection
var body: some View {
VStack(alignment: .leading) {
SectionTextView(text: eventSection.category.typeName())
.frame(alignment: .leading)
ScrollView(.horizontal, showsIndicators: true) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.eventSection.events) { event in
EventItemView(event: event)
}
}
}.id(UUID().uuidString() // << unique
}
}
}

Resources