Skip to content

Instantly share code, notes, and snippets.

@SergeiMeza
Created January 12, 2021 15:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SergeiMeza/d2f92a6db9d5a908ab68e88d45676586 to your computer and use it in GitHub Desktop.
Save SergeiMeza/d2f92a6db9d5a908ab68e88d45676586 to your computer and use it in GitHub Desktop.
// Models
import SwiftUI
struct Tab: Identifiable {
var id = UUID().uuidString
var tab: String
var foods: [Food]
}
var tabItems = [
Tab(tab: "Order Again", foods: foods.shuffled()),
Tab(tab: "Picked For You", foods: foods.shuffled()),
Tab(tab: "Starters", foods: foods.shuffled()),
Tab(tab: "Gimpub Sushi", foods: foods.shuffled()),
]
struct Food: Identifiable {
var id = UUID().uuidString
var title: String
var description: String
var price: String
var image: String
}
var foods = [
Food(title: "Chocolate Cake", description: "Chocolate cake or chocolate gateau is a cake flavored with melted chocolate, cocoa powder, or both", price: "$19", image: "chocolates"),
Food(title: "Cookies", description: "A biscuit is a flour-baked food product. Outside North America the biscuit is typically hard, flat, and unleavened", price: "$10", image: "cookies"),
Food(title: "Sandwich", description: "Trim the bread from all sides and apply butter on one bread, then apply the green chutney all over.", price: "$9", image: "sandwich"),
Food(title: "French Fries", description: "French fries, or simply fries, chips, finger chips, or French-fried potatoes, are batonnet or allumette-cut deep-fried potatoes.", price: "$15", image: "fries"),
Food(title: "Pizza", description: "Pizza is a savory dish of Italian origin consisting of a usually round, flattened base of leavened wheat-based dough tapped", price: "$39", image: "pizza")
]
// View Models
import SwiftUI
class HomeViewModel: ObservableObject {
@Published var offset: CGFloat = 0
@Published var selectedTab = tabItems.first!.tab
}
// View Modifiers
import SwiftUI
// Button Modifiers...
struct NavigationButtonModifier: ViewModifier {
func body(content: Content) -> some View {
return content
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.padding(10)
.background(
Circle()
.foregroundColor(.black)
.opacity(0.5)
)
}
}
extension Image {
func navigationButton() -> some View {
self.modifier(NavigationButtonModifier())
}
}
// Typography Modifiers ...
struct TitleModifier: ViewModifier {
func body(content: Content) -> some View {
return content
.font(Font.title.weight(.bold))
}
}
struct HeadlineModifier: ViewModifier {
func body(content: Content) -> some View {
return content
.font(Font.title2.weight(.bold))
}
}
struct SubheadlineModifier: ViewModifier {
func body(content: Content) -> some View {
return content
.font(Font.body.weight(.bold))
}
}
struct BodyModifier: ViewModifier {
func body(content: Content) -> some View {
return content
.font(.caption)
}
}
extension View {
func title() -> some View {
self.modifier(TitleModifier())
}
func headline() -> some View {
self.modifier(HeadlineModifier())
}
func subheadline() -> some View {
self.modifier(SubheadlineModifier())
}
func body() -> some View {
self.modifier(BodyModifier())
}
}
// Image Modifiers...
struct ImageCardModifier: ViewModifier {
var size: CGFloat = 130
func body(content: Content) -> some View {
return content
.frame(width: size, height: size)
.background(Color(.systemGray5))
.cornerRadius(5)
}
}
extension Image {
func card(size: CGFloat = 130) -> some View {
self
.resizable()
.aspectRatio(contentMode: .fill)
.modifier(ImageCardModifier(size: size))
}
}
// Views
import SwiftUI
struct Home: View {
@StateObject var homeData = HomeViewModel()
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 15, pinnedViews: [.sectionHeaders], content: {
// Parallax Header...
HomeHeader()
// Content...
HomeContent()
})
}
.background(Color(.systemBackground))
.overlay(
Color(.systemBackground)
.frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top)
.ignoresSafeArea(.all, edges: .top)
.opacity(homeData.offset > 250 ? 1 : 0)
, alignment: .top
)
.environmentObject(homeData)
}
}
// Parallax Header...
struct HomeHeader: View {
@EnvironmentObject var homeData: HomeViewModel
var body: some View {
GeometryReader { reader -> AnyView in
let offset = reader.frame(in: .global).minY
if -offset >= 0 {
DispatchQueue.main.async {
self.homeData.offset = -offset
}
}
return AnyView(
Image("food")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(
width: UIScreen.main.bounds.width,
height: 250 + (offset > 0 ? offset : 0)
)
.cornerRadius(2)
.offset(y: (offset > 0 ? -offset : 0))
.overlay(
HStack {
Button(action: {}) {
Image(systemName: "arrow.left")
.navigationButton()
}
Spacer()
Button(action: {}) {
Image(systemName: "suit.heart.fill")
.navigationButton()
}
}
.padding(),
alignment: .top
)
)
}
.frame(height: 250)
}
}
// Home Content...
struct HomeContent: View {
@EnvironmentObject var homeData: HomeViewModel
var body: some View {
Section(header: HeaderView()) {
ForEach(tabItems, id: \.id) { tab in
VStack(alignment: .leading, spacing: 15, content: {
Text(tab.tab)
.headline()
.padding([.bottom, .horizontal])
ForEach(tab.foods) { food in
CardView(food: food)
}
Divider()
.padding(.top)
})
.tag(tab.tab)
.overlay(
GeometryReader { reader -> Text in
let offset = reader.frame(in: .global).minY
// Top Area + Header Size 100
let height = UIApplication.shared.windows.first!.safeAreaInsets.top + 100
if offset < height && offset > 50 && homeData.selectedTab != tab.tab {
DispatchQueue.main.async {
homeData.selectedTab = tab.tab
}
}
return Text("")
}
)
}
}
}
}
struct CardView: View {
var food: Food
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 10, content: {
Text(food.title)
.subheadline()
Text(food.description)
.body()
.lineLimit(3)
Text(food.price)
.subheadline()
})
Spacer(minLength: 10)
Image(food.image)
.card(size: 130)
}
.padding(.horizontal)
}
}
struct HeaderView: View {
@EnvironmentObject var homeViewModel: HomeViewModel
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) {
// BackButton
Button(action: {}) {
Image(systemName: "arrow.left")
.font(.system(size: 20, weight: .bold))
.frame(width: getSize(), height: getSize())
.foregroundColor(.primary)
}
Text("Meza Backery")
.title()
Spacer()
Button(action: {}) {
Image(systemName: "suit.heart.fill")
.font(.system(size: 20, weight: .bold))
.frame(width: getSize(), height: getSize())
.foregroundColor(.primary)
}
}
ZStack {
VStack(alignment: .leading, spacing: 10) {
Text("Asiatisch • Koreanisch • Japanisch")
.body()
HStack(spacing: 0) {
Image(systemName: "clock")
.body()
Text("30-40 Min")
.body()
Text("4.3")
.body()
Image(systemName: "star.fill")
.body()
Text("$6.40 Fee")
.body()
.padding(.leading, 10)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.opacity(homeViewModel.offset > 200 ? 1 - Double((homeViewModel.offset - 200) / 50) : 1)
// Custom Scroll View
ScrollViewReader { reader in
ScrollView(.horizontal, showsIndicators: false, content: {
HStack(spacing: 0) {
ForEach(tabItems) { tab in
Text(tab.tab)
.underline(homeViewModel.selectedTab == tab.tab ? true : false)
.body()
.padding(10)
.id(tab.tab)
}
.onChange(of: homeViewModel.selectedTab, perform: { value in
withAnimation(.default) {
reader.scrollTo(homeViewModel.selectedTab, anchor: .leading)
}
})
}
})
// Visible Only when scrolls up...
.opacity(homeViewModel.offset > 200 ? Double((homeViewModel.offset - 200) / 50) : 0)
}
}
// Default Frame = 60...
// Top Frame = 40
// so Total = 100
.frame(height: 60)
if homeViewModel.offset > 250 {
Divider()
}
}
.padding(.horizontal)
.frame(height: 100)
.background(Color(.systemBackground))
}
func getSize() -> CGFloat {
if homeViewModel.offset > 200 {
let progress = (homeViewModel.offset - 200) / 50
if progress <= 1.0 {
return progress * 40
} else {
return 40
}
} else {
return 0
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment