Skip to content

Instantly share code, notes, and snippets.

@leojquinteros
Last active June 19, 2023 16:37
Show Gist options
  • Save leojquinteros/408039d073e63038dc9a5e897f8a3372 to your computer and use it in GitHub Desktop.
Save leojquinteros/408039d073e63038dc9a5e897f8a3372 to your computer and use it in GitHub Desktop.
[XCode 13] new swipeActions and refreshable using SwiftUI
//
// 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