Created
January 12, 2021 15:52
-
-
Save SergeiMeza/d2f92a6db9d5a908ab68e88d45676586 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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") | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// View Models | |
import SwiftUI | |
class HomeViewModel: ObservableObject { | |
@Published var offset: CGFloat = 0 | |
@Published var selectedTab = tabItems.first!.tab | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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