Last active
June 19, 2023 16:37
-
-
Save leojquinteros/408039d073e63038dc9a5e897f8a3372 to your computer and use it in GitHub Desktop.
[XCode 13] new swipeActions and refreshable using SwiftUI
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
// | |
// Refreshable.swift | |
// | |
// Created by Leo Quinteros on 17/06/21. | |
// | |
import SwiftUI | |
import Combine | |
struct Country: Decodable, Hashable { | |
let name: String | |
let population: Int | |
let capital: String | |
let alpha2Code: String | |
let flags: [String: String] | |
static func == (lhs: Country, rhs: Country) -> Bool { | |
lhs.alpha2Code == rhs.alpha2Code | |
} | |
func hash(into hasher: inout Hasher) { | |
hasher.combine(name) | |
hasher.combine(population) | |
hasher.combine(capital) | |
hasher.combine(alpha2Code) | |
hasher.combine(flags) | |
} | |
} | |
struct CountryViewModel: Identifiable, Hashable { | |
let id = UUID() | |
var country: Country | |
var isFavourite: Bool = false | |
var isPinned: Bool = false | |
var flagURL: URL { URL(string: country.flags["png"] ?? "")! } | |
var name: String { country.name } | |
var population: String { String (country.population) } | |
var capital: String { country.capital } | |
static func == (lhs: CountryViewModel, rhs: CountryViewModel) -> Bool { | |
lhs.country == rhs.country | |
} | |
func hash(into hasher: inout Hasher) { | |
hasher.combine(country) | |
} | |
} | |
class ContentViewModel: ObservableObject { | |
private var cancellable: AnyCancellable? | |
@Published var pinned: [CountryViewModel] = [] | |
@Published var fetched: [CountryViewModel] = [] | |
@Published var favourites: [CountryViewModel] = [] | |
func fetchNewCountries() async { | |
guard let url = URL(string: "https://restcountries.com/v2/regionalbloc/eu") else { | |
return | |
} | |
cancellable = URLSession.shared.dataTaskPublisher(for: url) | |
.map({ $0.data }) | |
.decode(type: [Country].self, decoder: JSONDecoder()) | |
.receive(on: DispatchQueue.main) | |
.sink(receiveCompletion: { completion in | |
switch completion { | |
case .finished: | |
break | |
case .failure(let error): | |
print(error.localizedDescription) | |
} | |
}, receiveValue: { result in | |
self.fetched = result.map({ | |
CountryViewModel(country: $0) | |
}) | |
}) | |
} | |
func removeCountry(_ country: CountryViewModel) { | |
fetched.removeAll { $0.id == country.id } | |
pinned.removeAll { $0.id == country.id } | |
favourites.removeAll { $0.id == country.id } | |
} | |
func addToFavourites(_ country: CountryViewModel) { | |
favourites.append(country) | |
} | |
func removeFromFavourites(_ country: CountryViewModel) { | |
favourites.removeAll { $0.id == country.id } | |
} | |
func pin(_ country: CountryViewModel) { | |
pinned.append(country) | |
} | |
func isFavourite(_ country: CountryViewModel) -> Bool { | |
favourites.firstIndex(of: country) != nil | |
} | |
func isPinned(_ country: CountryViewModel) -> Bool { | |
pinned.firstIndex(of: country) != nil | |
} | |
} | |
struct ContentView: View { | |
@StateObject var viewModel: ContentViewModel | |
var body: some View { | |
TabView { | |
CountriesListView(viewModel: viewModel) | |
.tabItem { | |
Text("Countrties") | |
Image(systemName: "globe") | |
} | |
.badge(viewModel.fetched.count) | |
FavouritesView(viewModel: viewModel) | |
.tabItem { | |
Text("Favourites") | |
Image(systemName: "star") | |
} | |
.badge(viewModel.favourites.count) | |
} | |
} | |
} | |
struct CountriesListView: View { | |
@ObservedObject var viewModel: ContentViewModel | |
@State var searchable: String = "" | |
var body: some View { | |
NavigationView { | |
List { | |
if viewModel.fetched.isEmpty { | |
Text("No countries to display. Pull to refresh") | |
} else { | |
ForEach(viewModel.fetched, id: \.self) { country in | |
CountryCell(viewModel: viewModel, country: country) | |
.swipeActions { | |
Button(role: .destructive) { | |
viewModel.removeCountry(country) | |
} label: { | |
Label("Delete", systemImage: "trash.circle.fill") | |
} | |
Button { | |
viewModel.addToFavourites(country) | |
} label: { | |
Label("Favourite", systemImage: "star.circle.fill") | |
} | |
.tint(.orange) | |
} | |
.swipeActions(edge: .leading, allowsFullSwipe: false) { | |
Button { | |
viewModel.pin(country) | |
} label: { | |
Label("Pin", systemImage: "pin.circle.fill") | |
} | |
.tint(.green) | |
} | |
} | |
} | |
}.refreshable { | |
await viewModel.fetchNewCountries() | |
} | |
.navigationTitle("Europe") | |
} | |
.searchable(text: $searchable) { | |
ForEach(viewModel.fetched.filter { model in | |
searchable == "" ? true | |
: model.country.name.lowercased().contains(searchable.lowercased()) | |
}) { country in | |
Text(country.name) | |
} | |
} | |
} | |
} | |
struct FavouritesView: View { | |
@ObservedObject var viewModel: ContentViewModel | |
var body: some View { | |
NavigationView { | |
List { | |
ForEach(viewModel.favourites) { country in | |
CountryCell(viewModel: viewModel, country: country, showBadges: false) | |
.swipeActions { | |
Button(role: .destructive) { | |
viewModel.removeFromFavourites(country) | |
} label: { | |
Label("Remove", systemImage: "trash.circle.fill") | |
} | |
} | |
} | |
} | |
.navigationTitle("Favourites") | |
} | |
} | |
} | |
struct CountryCell: View { | |
@ObservedObject var viewModel: ContentViewModel | |
@State var country: CountryViewModel | |
var showBadges: Bool = true | |
var body: some View { | |
HStack { | |
AsyncImage ( | |
url: country.flagURL, | |
content: { image in | |
image | |
.resizable() | |
.frame(width: 75, height: 50, alignment: .leading) | |
.aspectRatio(contentMode: .fit) | |
.padding(.trailing, 8) | |
}, | |
placeholder: { | |
ProgressView() | |
} | |
) | |
HStack { | |
VStack(alignment: .leading) { | |
Text(country.name) | |
.fontWeight(.bold) | |
.foregroundColor(.primary) | |
Text("Capital: \(country.capital)") | |
.fontWeight(.semibold) | |
.foregroundColor(.secondary) | |
Text("Population: \(country.population)") | |
.fontWeight(.light) | |
.foregroundColor(.secondary) | |
} | |
Spacer() | |
VStack(alignment: .trailing, spacing: 5) { | |
if viewModel.isFavourite(country) && showBadges { | |
Image(systemName: "star.circle.fill") | |
.foregroundColor(.yellow) | |
} | |
if viewModel.isPinned(country) && showBadges { | |
Image(systemName: "pin.circle.fill") | |
.foregroundColor(.green) | |
} | |
Spacer() | |
} | |
.frame(width: 20) | |
} | |
} | |
.padding() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment