NavigationStack {
ScrollView {
SearchAndFilterBar()
.onTapGesture {
withAnimation(.easeIn) {
showDestinationSearchView.toggle()
}
}
LazyVStack(spacing: 32) {
ForEach(viewModel.listings) { listing in
NavigationLink(value: listing) {
ListingItemView(listing: listing)
.frame(height: 400)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}
}
.navigationDestination(for: Listing.self) { listing in
ListingDetailView(listing: listing)
.navigationBarBackButtonHidden()
}
}
NavigationLink
์ Hashable
ํ value๋ฅผ ๋ฃ์ด์ฃผ๋ฉด .navigationDestination
๋ฉ์๋๋ก ๊ฐ์ ๋ฐ์์ ๊ฐ์ ๋ฐ๋ผ ์ํ๋ ๋ทฐ๋ฅผ ๊ทธ๋ ค์ค ์ ์๋ค.
LazyVStack(spacing: 32) {
ForEach(viewModel.listings) { listing in
NavigationLink(value: listing) {
ListingItemView(listing: listing)
.frame(height: 400)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}
item์ด ํ๋ฉด์ ๋ ๋๋ง ๋ ๋ ์์ฑ๋๋ค.
TabView {
ExploreView()
.tabItem { Label("Explore", systemImage: "magnifyingglass") }
WishlistView()
.tabItem { Label("Wishlists", systemImage: "heart") }
ProfileView()
.tabItem { Label("Profile", systemImage: "person") }
}
TabView {
ForEach(listing.imageUrls, id: \.self) { image in
Image(image)
.resizable()
.scaledToFill()
}
}
.tabViewStyle(.page)
import MapKit
@State private var cameraPosition: MapCameraPosition
init(listing: Listing) {
// ..
let region = MKCoordinateRegion(
center: listing.city == "San Jose" ? .sanjose : .nugegoda,
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
self._cameraPosition = State(initialValue: .region(region))
}
Map(position: $cameraPosition)
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
ScrollView {
\\ ...
}
.overlay(alignment: .bottom) {
ReserveBar(listing: listing)
}
VStack {
\\ ...
}
.onTapGesture {
withAnimation(.easeIn) { selectedOption = .guests }
}
VStack {
\\ ...
}
.modifier(CollapsibleDestinationViewModifier())
struct CollapsibleDestinationViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding()
.shadow(radius: 10)
}
}
Group {
VStack {
}
VStack {
}
// ...
}
View๊ฐ ๋๋ฌด ๋ง์ ์ ์๋ฌ๊ฐ ๋๋๋ฐ Group์ ์ด์ฉํ๋ฉด ๋๋ค.
struct Listing: Identifiable, Codable, Hashable {
let id: String
let ownerUid: String
let ownerName: String
let ownerImageUrl: String
// ...
var features: [ListingFeatures]
var amenities: [ListingAmenities]
let type: ListingType
}
enum ListingFeatures: Int, Codable, Identifiable, Hashable {
case selfCheckIn
case superHost
var title: String {
switch self {
case .selfCheckIn: return "Self check-in"
case .superHost: return "Superhost"
}
}
var subtitle: String {
switch self {
case .selfCheckIn: return "Check yourself in with the keypad"
case .superHost: return "Superhosts are experienced, highly rated hosts who are commited to providing greate stars for guests."
}
}
var imageName: String {
switch self {
case .selfCheckIn: return "door.left.hand.open"
case .superHost: return "medal"
}
}
var id: Int { return self.rawValue }
}
-
Identifiable
: list๋ collection์์ ๊ฐ ํญ๋ชฉ์ ๊ตฌ๋ณํ๋ id๋ฅผ ์ ๊ณตํ๋ ๋ฐ ์ฌ์ฉํ๋ค. -
Codable
: ์๋ฒ์์ ๋ฐ์ดํฐ ๊ตํ์์ JSON ํ์์ผ๋ก ๋ณํํ ์ ์๋๋ก ์ฌ์ฉํ๋ค. -
Hashable
: Set์ด๋ Dictionary์ ์ฌ์ฉํ๋ค.
Service
import Foundation
class ExploreService {
func fetchListings() async throws -> [Listing] {
return DeveloperPreview.shared.listing
}
}
- ์ธ๋ถ ๋ฐ์ดํฐ์ ์ํธ ์์ฉ์ ๋ด๋น
- ๋ฐ์ดํฐ ๋ก์ง์ ๋ทฐ๋ ๋ทฐ๋ชจ๋ธ์์ ์ง์ ์ํํ์ง ์๊ณ ์ธ๋ถ ์๋น์ค๋ก ๋ถ๋ฆฌํจ์ผ๋ก์ ์ ์ง๋ณด์๋ ํ ์คํธ๋ฅผ ์ฉ์ดํ๊ฒ ํ๋ค.
View
import SwiftUI
struct ExploreView: View {
// ...
@StateObject var viewModel = ExploreViewModel(service: ExploreService())
var body: some View {
// ...
LazyVStack(spacing: 32) {
ForEach(viewModel.listings) { listing in
NavigationLink(value: listing) {
ListingItemView(listing: listing)
.frame(height: 400)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}
// ...
}
}
- ์ ์ ์๊ฒ ๋ณด์ฌ์ง๋ ํ๋ฉด์ ์ ์, ์ ์ ์ ๋ ฅ์ ์ฒ๋ฆฌํ๊ณ ๋ทฐ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ทฐ๋ชจ๋ธ์์ ๊ฐ์ ธ์ ๋ ๋๋งํ๋ค.
ViewModel
import Foundation
class ExploreViewModel: ObservableObject {
@Published var listings: [Listing] = []
@Published var searchLocation = ""
private let service: ExploreService
init(service: ExploreService) {
self.service = service
Task { await fetchListings() }
}
func fetchListings() async {
do {
self.listings = try await service.fetchListings()
} catch {
print("DEBUG: Failed to fetch listng with error: \(error.localizedDescription)")
}
}
func updateListingForLocation() {
let filteredListings = listings.filter( {
$0.city.lowercased() == searchLocation.lowercased() ||
$0.state.lowercased() == searchLocation.lowercased()
})
self.listings = filteredListings.isEmpty ? listingsCopy : filteredListings
}
}
- ๋ทฐ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณต, ๋ทฐ์ ์๋น์ค๊ฐ์ ์ค๊ฐ๊ณ์ธต ์ญํ ์ ํ๋ค.
์ด๋ค ๊ฐ์ฒด๊ฐ ํ์๋ก ํ๋ ๋ค๋ฅธ ๊ฐ์ฒด๋ฅผ ์ธ๋ถ์์ ์ ๋ฌ๋ฐ์ ์ฌ์ฉํ๋ ๊ฒ
// ViewModel
class ExploreViewModel: ObservableObject {
@Published var listings: [Listing] = []
@Published var searchLocation = ""
}
// View
@StateObject var viewModel = ExploreViewModel(service: ExploreService())
- SwiftUI์์์ ์์กด์ฑ ์ฃผ์
์
ObervableObject
๋ฅผ@ObservedObject
๋๋@StateObject
์ ์กฐํฉํ์ฌ ๊ตฌํํ๋ค. - ์ํ ๋ณ๊ฒฝ์ด ์์ ๋
@ObservedObject
๋ ๋ทฐ๋ฅผ ๋ค์ ์์ฑํด์ ๊ทธ๋ฆฌ์ง๋ง@StateObject
๋ ๋ทฐ๋ฅผ ๋ค์ ์์ฑํ์ง ์๊ณ ๋์ผํ ๋ทฐ๋ฅผ ์ฌ์ฉํ์ฌ ํจ์จ์ฑ์ด ์ข๋ค. - ๊ธฐ๋ณธ์ ์ผ๋ก
@StateObject
๋ฅผ ์ฌ์ฉํ๋ ํด๋น ํ๋กํผํฐ๋ฅผ subview์์ธ๋ ์ฃผ์ ์์ผ์ผ ํ๋ค๋ฉด@ObservedObject
๋ฅผ ์ฌ์ฉํ๋ค.
ํ์ ๋ฐฐ๊ฒฝ
๊ธฐ์กด์ DispatchQueue๋ completionHandler๋ฅผ ์ด์ฉํ๋ฉด
- ํด๋ก์ ์์ ํด๋ก์ ์ด๋ฐ์์ผ๋ก ์ฝ๋๊ฐ ์ ์ข๊ฒ ๋ณด์ผ ์ ์๋ค.
- ์๋ฌ ํธ๋ค๋ง์ด ๋ณต์กํ๊ณ ์กฐ๊ฑด๋ฌธ์์ ์ฒ๋ฆฌ๊ฐ ํ๋ค๋ค.
์ฌ์ฉ ๋ฐฉ๋ฒ
// Service
func fetchListings() async throws -> [Listing] {
return DeveloperPreview.shared.listing
}
async
: ๋น๋๊ธฐ์ ์คํ์ ํ ์ ์์ (๋ชจ๋ ๋ช ๋ น์ด ๊ทธ๋ ์ง ์์)throws
: ์๋ฌ๋ฅผ ๋์ง ์ ์์
// ViewModel
init(service: ExploreService) {
self.service = service
Task { await fetchListings() }
}
func fetchListings() async {
do {
self.listings = try await service.fetchListings()
self.listingsCopy = listings
} catch {
print("DEBUG: Failed to fetch listng with error: \(error.localizedDescription)")
}
}
await
: ๋น๋๊ธฐ์ ์ผ๋ก ์์ ํ๋ ๊ณณ์ด๋ผ๊ณ ๋ช ์์ ์ผ๋ก ์๋ ค์ค (์ด ํค์๋๋ฅผ ์ฌ์ฉํด์ ๋น๋๊ธฐ์ ์ผ๋ก ์๋ํจ)