I created a tabview with 4 views. Each view has a viewModel where I load the data needed in the view from ana api. It works fine except the first view is always empty unless I tap in a tab then tap on the first view tap again.
Any idea how I could make sure all the first view's data is there as soon as it's loaded?
Find my code below
thanks
-Contentview
#ViewBuilder
var body: some View {
if isLoggedIn() {
MainScreen()
} else {
UnAuthenticatedScreen()
}
}
-MainView
#ObservedObject var eventsVM: EventsVM = EventsVM()
var body: some View {
TabView(){
HomeScreen(events: self.eventsVM.events)
.tabItem {
Image(systemName: "house")
Text("Home")
}
.navigationBarHidden(true)
EventsScreen(events: self.eventsVM.events)
.tabItem {
Image(systemName: "calendar")
Text("Events")
}.navigationBarHidden(true)
}
}
- EventsVM
import Foundation
import Combine
class EventsVM: ObservableObject {
let didChange = PassthroughSubject<[EventModel], Never>()
private let eventsService: EventService
#Published var events = [EventModel]()
init() {
self.eventsService = EventService()
self.fetchEvents()
}
private func fetchEvents(){
self.eventsService.getAllEvents { (_events, _error) in
guard let events = _events else { return }
self.events = events
}
}
}
The home view
import SwiftUI
struct HomeScreen: View {
var eventsVM: EventsVM
#State var news: [NewsModel] = []
#State var albums = [AlbumModel]()
init(eventsVM: EventsVM){
self.eventsVM = EventsVM()
}
var body: some View {
GeometryReader { gr in
VStack(alignment: .leading, spacing: 0) {
HStack{
Spacer()
}
HStack {
Spacer()
Text("About Us")
Image("logo_squad")
.resizable()
.frame(width: 50, height: 50)
}
Text("Events").font(Font.custom("Francois One", size: 30)).foregroundColor(.red)
ScrollView(.horizontal, showsIndicators: false){
HStack {
ForEach(self.eventsVM.events, id: \.self) { event in
HomeEventRow(event: event).frame(width: gr.size.width - 60, height: 170)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1).shadow(radius: -2)
)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}
// Text("News").font(Font.custom("Francois One", size: 30)).foregroundColor(.red)
// ScrollView(.horizontal, showsIndicators: false){
// HStack {
// ForEach(self.news, id: \.self){ _news in
// HomeNewsRow(news: _news)
// .frame(width: gr.size.width - 60, height: 150)
// .overlay(
// RoundedRectangle(cornerRadius: 10)
// .stroke(Color.gray, lineWidth: 1).shadow(radius: -2)
// )
// .clipShape(RoundedRectangle(cornerRadius: 10))
// }
// }
// }
// Text("Albums").font(Font.custom("Francois One", size: 30)).foregroundColor(.red)
// ScrollView(Axis.Set.horizontal, showsIndicators: true){
// HStack {
// ForEach(self.albums, id: \.self){ album in
// HomeMediaRow(album: album)
// .frame(width: gr.size.width - 60, height: 150)
// }.overlay(
// RoundedRectangle(cornerRadius: 10)
// .stroke(Color.gray, lineWidth: 1).shadow(radius: -2)
// )
// .clipShape(RoundedRectangle(cornerRadius: 10))
// }
// }
Spacer()
}
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.onAppear {
print("self.eventsVM.events \(self.eventsVM.events)")
}
}.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
}
}
struct HomeScreen_Previews: PreviewProvider {
static var previews: some View {
HomeScreen(eventsVM: EventsVM())
}
}
Related
I have a view with a filter bar. One says "Shirts" & the other says "Trousers". When "Shirts" is selected, it displays a grid view of shirts. When "Trousers" is selected in the filter bar, it shows a grid view of trousers.
When I tap on an item (a shirt or a pair of trousers) it takes me to a different view (showing the product in more detail). However, the filter bar remains. I need to hide the filter bar when an item is tapped and it takes the user to a detail view.
I need to hide the marketplacefilterbar view when a product is tapped.
My ViewModel for the filter bar is:
enum MarketplaceFilterViewModel: Int, CaseIterable {
case shirts
case trousers
var title: String {
switch self {
case .shirts: return "Shirts"
case .trousers: return "Trousers"
}
}
}
Some code from the MarketplaceViewModel:
class MarketplaceViewModel: NSObject, ObservableObject {
#Published var search = ""
// Shirt Data...
#Published var shirts: [Shirt] = []
#Published var filteredShirt: [Shirt] = []
// Trouser Data...
#Published var trousers: [Trouser] = []
#Published var filteredTrouser: [Trouser] = []
}
Code for the marketplace view is:
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
marketplaceFilterBar
if selectedMarketplaceFilter == .raise {
MarketplaceShirtView()
}
if selectedMarketplaceFilter == .save {
MarketplaceTrouserView()
}
}
}
var marketplaceFilterBar: some View {
VStack {
HStack(spacing: 15){
Image("Logo")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60.0, height: 60.0)
.padding(EdgeInsets.init(top: 0, leading: 45, bottom: 0, trailing: 0))
Spacer(minLength: 0)
}
.padding([.horizontal,.top])
HStack {
ForEach(MarketplaceFilterViewModel.allCases, id: \.rawValue) { item in
VStack {
Text(item.title)
.font(.headline)
.fontWeight(selectedMarketplaceFilter == item ? .semibold: .regular)
.foregroundColor(selectedMarketplaceFilter == item ? .black: .gray)
if selectedMarketplaceFilter == item {
Capsule()
.foregroundColor(Color("LightBlue"))
.frame(height: 3)
.matchedGeometryEffect(id: "filter", in: animation)
} else {
Capsule()
.foregroundColor(Color(.clear))
.frame(height: 3)
}
}
.onTapGesture {
withAnimation(.easeInOut) {
self.selectedMarketplaceFilter = item
}
}
}
}
.overlay(Divider().offset(x: 0, y: 16))
}
}
Code for the Shirt Detail View:
struct ShirtDetailView: View {
#Binding var shirtData : Shirt!
#Binding var showDetailShirt: Bool
#Namespace var animation: Namespace.ID
// Shared Data Model...
#EnvironmentObject var sharedData: SharedDataModel
#EnvironmentObject var marketplaceData: MarketplaceViewModel
ScrollView {
VStack{
HStack {
Button(action: {
withAnimation(.easeOut){showDetailShirt.toggle()}
}) {
Image(systemName: "arrow.backward.circle.fill")
.foregroundColor(.gray)
.frame(width: 40, height: 40)
.background(Color.white,in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.shadow(color: .black.opacity(0.1), radius: 5, x: 5, y: 5)
}
Text(shirtData.shirt_name)
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.black)
}
VStack(spacing: 10){
WebImage(url: URL(string: shirtData.shirt_image))
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.cornerRadius(15)
Text("Details")
.opacity(0.7)
.frame(maxWidth: .infinity,alignment: .leading)
.padding(.bottom)
HStack(spacing: 15) {
Text(shirtData.shirt_details)
.font(.system(size: 18))
.padding()
}
}
}
}
Code for the Trouser Detail View:
struct TrouserDetailView: View {
#Binding var trouserData : Trouser!
#Binding var showDetailTrouser: Bool
#Namespace var animation: Namespace.ID
// Shared Data Model...
#EnvironmentObject var sharedData: SharedDataModel
#EnvironmentObject var marketplaceData: MarketplaceViewModel
ScrollView {
VStack{
HStack {
Button(action: {
withAnimation(.easeOut){showDetailTrouser.toggle()}
}) {
Image(systemName: "arrow.backward.circle.fill")
.foregroundColor(.gray)
.frame(width: 40, height: 40)
.background(Color.white,in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.shadow(color: .black.opacity(0.1), radius: 5, x: 5, y: 5)
}
Text(trouserData.trouser_name)
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.black)
}
VStack(spacing: 10){
WebImage(url: URL(string: trouserData.trouser_image))
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.cornerRadius(15)
Text("Details")
.opacity(0.7)
.frame(maxWidth: .infinity,alignment: .leading)
.padding(.bottom)
HStack(spacing: 15) {
Text(trouserData.trouser_details)
.font(.system(size: 18))
.padding()
}
}
}
}
Code for the SharedDataModel:
class SharedDataModel: ObservableObject {
// Detail Shirt Data...
#Published var detailShirt : Shirt?
#Published var showDetailShirt : Bool = false
// matched Geoemtry Effect from Search page...
#Published var fromSearchPage: Bool = false
}
The updated code for marketplace:
#Binding var charityData : Charity!
#Binding var showDetailCharity: Bool
#Binding var businessData : Business!
#Binding var showDetailBusiness: Bool
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
if !showDetailCharity && !showDetailBusiness {
//MARK: This is the filter bar - the view for this is further down the page
marketplaceFilterBar
}
if selectedMarketplaceFilter == .raise {
MarketplaceRaiseView()
}
if selectedMarketplaceFilter == .save {
MarketplaceSaveView()
}
}
The code for TabView:
#StateObject var appModel: AppViewModel = .init()
#StateObject var sharedData: SharedDataModel = SharedDataModel()
// Animation Namespace...
#Namespace var animation
// Hiding Tab Bar...
init(){
UITabBar.appearance().isHidden = true
}
var body: some View {
VStack(spacing: 0){
// Tab View...
TabView(selection: $appModel.currentTab) {
Marketplace(animation: _animation)
.environmentObject(sharedData)
.tag(Tab.Market)
.setUpTab()
Home()
.environmentObject(sharedData)
.tag(Tab.Home)
.setUpTab()
}
Since you already have showDetailShirt and showDetailTrouser, you can use them to hide the marketplacefilterbar like so:
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10){
if !showDetailShirt && !showDetailTrouser {
marketplaceFilterBar
}
if selectedMarketplaceFilter == .raise {
MarketplaceShirtView()
}
if selectedMarketplaceFilter == .save {
MarketplaceTrouserView()
}
}
}
I've been trying to animate a ScrollView using ScrollViewReader and withAnimation.
I can't figure out why these two animations are not working, either from Button or .onAppear?
import SwiftUI
struct ScrollView2: View {
#State private var scrollText = false
var body: some View {
ScrollViewReader { scrollView in
ScrollView {
Button("Scroll to bottom") {
withAnimation(.linear(duration: 30)) {
scrollView.scrollTo(99, anchor: .center)
}
}
ForEach(0..<100) { index in
Text(String(index))
.id(index)
}
.onAppear(perform: {
withAnimation(.linear(duration: 30)) {
scrollView.scrollTo(scrollText ? 99 : 1, anchor: .center)
}
scrollText.toggle()
})
}
}
}
}
It seems like duration doesn't work within withAnimation. Alternatively, I created a function that executes a repeating Timer that fires over 30 seconds, calling scrollTo withAnimation on each loop.
struct ScrollView2: View {
#State private var scrollText = false
var body: some View {
ScrollViewReader { scrollView in
ScrollView {
Button("Scroll to bottom") {
animateWithTimer(proxy: scrollView)
}
ForEach(0..<100) { index in
Text(String(index))
.id(index)
}
}
}
}
func animateWithTimer(proxy: ScrollViewProxy) {
let count: Int = 100
let duration: Double = 30.0
let timeInterval: Double = (duration / Double(count))
var counter = 0
let timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { (timer) in
withAnimation(.linear) {
proxy.scrollTo(counter, anchor: .center)
}
counter += 1
if counter >= count {
timer.invalidate()
}
}
timer.fire()
}
}
Note: There is a delay when you press the button initially because when it tries to scrollTo the first ~40 of numbers, they are already high on the screen and the scrollView doesn't need to scroll anywhere to center them. You can update the timeInterval and counter variables as needed.
Because duration doesn't seem to work with withAnimation yet, I had to be a bit hacky to get the animation effect I wanted.
Here's what I did:
I added a ScrollViewReader to my ScrollView
I used ForEach and added IDs to my items in my ScrollView
Used an .offset and .animation modifiers to animate the
ScrollView itself (not the items in it)
Used .scrollTo within .onAppear to move at launch the ScrollView
to an item further away from the start to allow the user to both
scroll back and forward the items, even with the ScrollView being
itself animated from right to left
Here's what my code looks like:
import SwiftUI
import AVKit
struct ProView: View {
#State private var scrollText = false
var body: some View {
ZStack {
// VStack {
// Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1))
// .ignoresSafeArea(.all)
// }
//
// VStack {
//
// VideoPlayer(player: AVPlayer(url: Bundle.main.url(forResource: "wave-1", withExtension: "mp4")!)) {
// VStack {
// Image("pro-text")
// .resizable()
// .frame(width: 200, height: .infinity)
// .scaledToFit()
// }
// }
// .ignoresSafeArea(.all)
// .frame(width: .infinity, height: 300)
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { value in
HStack(spacing: 5) {
ForEach(0 ..< 100) { i in
HStack {
Image("benefit-1")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-2")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-3")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-4")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-5")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-6")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-7")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-8")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-9")
.resizable()
.frame(width: 120, height: 120)
Image("benefit-10")
.resizable()
.frame(width: 120, height: 120)
}
.id(i)
}
}
.offset(x: scrollText ? -10000 : 20)
.animation(Animation.linear(duration: 300).repeatForever(autoreverses: false))
.onAppear() {
value.scrollTo(50, anchor: .trailing)
scrollText.toggle()
}
}
}
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ProView()
}
}
I am creating a SwiftUI app and I am using NavigationView and a large title bar. However, I don't like that navigationBarItems buttons are not aligned with the large title bar (3rd picture) like in the Messages app (first and second picture). I tried to reposition the button, but it wasn't clickable anymore. Does anybody have an idea how to solve this? Thanks!
2nd:
3rd:
Solution: (found here: https://www.hackingwithswift.com/forums/swiftui/icons-in-navigationview-title-apple-messages-style/592)
import SwiftUI
struct ContentView: View {
#State private var midY: CGFloat = 0.0
#State private var headerText = "Contacts"
var body: some View {
NavigationView {
List {
HStack {
//text
HeaderView(headerText: self.headerText, midY: $midY)
.frame(height: 40, alignment: .leading)
.padding(.top, 5)
.offset(x: -45)
HStack {
//button 1
Button(action: {
self.action1()
}) {
Image(systemName: "ellipsis.circle")
.font(.largeTitle)
}
//button 2
Button(action: {
self.action2()
}) {
Image(systemName: "pencil.circle")
.font(.largeTitle)
}
}.padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 16))
.foregroundColor(.blue)
} .frame(height: 40, alignment: .leading)
.opacity(self.midY < 70 ? 0.0 : 1.0)
.frame(alignment: .bottom)
ForEach(0..<100){ count in
Text("Row \(count)")
}
}
.navigationBarTitle(self.midY < 70 ? Text(self.headerText) : Text(""), displayMode: .inline)
.navigationBarItems(trailing: self.midY < 70 ? HStack {
//button 1
Button(action: {
self.action1()
}) {
Image(systemName: "ellipsis.circle")
.frame(width: 20, height: 20)
}
//button 2
Button(action: {
self.action2()
}) {
Image(systemName: "pencil.circle")
.frame(width: 20, height: 20)
}
}
:
HStack {
//button 1
Button(action: {
self.action1()
}) {
Image(systemName: "ellipsis.circle")
.frame(width: 0, height: 0)
}
//button 2
Button(action: {
self.action2()
}) {
Image(systemName: "pencil.circle")
.frame(width: 0, height: 0)
}
}
)
}
}
func action1() {
print("do action 1...")
}
func action2() {
print("do action 2...")
}
}
struct HeaderView: View {
let headerText: String
#Binding var midY: CGFloat
var body: some View {
GeometryReader { geometry -> Text in
let frame = geometry.frame(in: CoordinateSpace.global)
withAnimation(.easeIn(duration: 0.25)) {
DispatchQueue.main.async {
self.midY = frame.midY
}
}
return Text(self.headerText)
.bold()
.font(.largeTitle)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
.navigationBarItems(leading:
Button(action: goBack) { HStack { Image("back") } })
.navigationBarItems(trailing: HStack {
Button(action: { self.share() }) { Image("share") }
Button(action: { self.wishlist() }) { Image("wishlist_detail") }})
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
}
}
}
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.