I pick Dog-API to improve my REST API skill because I could not do it in a progrmming test some months ago. The Alamofire and ObjectMapper make my life much easier. The code is also shorter and clean.
- Featch data form Dog-API with
Alamofire
only when the App loaded and useObjectMapper
save to Model object[BreedModel]
. - Use
@EnvironmentObject var dogVM: BreedVM
to pass values between views. - Check orientation mode with
SceneDelegate
then save to@AppStorage
. - Use
StaggeredGrid
helper view for the better GridView.
- Clone project to your Mac
- run pod install in Terminal
run pod install
After you install you will see Podfile
pod 'Alamofire', '~> 4.7'
pod 'ObjectMapper', '~> 3.5'
pod 'Kingfisher', '~> 6.0'
pod 'AlamofireObjectMapper', '~> 5.2'
Note :
The orentation is detected by Scene Delegate and Xcode 13 you may notice it doesn’t have an Info.plist file. See setup below to get Info.plist .
You will put @AppStorage
to every view that you want to be rotated.
@AppStorage("isPortrait") private var isPortrait: Bool = false
- Go to Application Scene Minifest -> Scene Configuration -> Application Role
- Delegate Class Name put
$(PRODUCT_MODULE_NAME).SceneDelegate
- Configuration Name put
Default Configuration
- Set
Allow Arbitrary Loads
->App Transport Security Settings
for requst API.
public let baseURL: URL = URL(string: "https://dog.ceo")! //This will never fail
public let apiURL: URL = baseURL.appendingPathComponent("api")
public let kBreedsListURL: URL = apiURL.appendingPathComponent("breeds/list")
public let kBreedsListAllURL: URL = apiURL.appendingPathComponent("breeds/list/all")
public func kImagesByBreedURL(for breedName: String) -> URL {
return apiURL.appendingPathComponent("/breed/\(breedName)/images")
}
import Foundation
struct FetchImageModel: Identifiable,Hashable {
var id: String
let imageUrl: String
}
struct FetchBreedModel: Identifiable,Hashable {
var id: String
var nameOfDog: String
}
struct BreedModel: Identifiable, Hashable {
var id: String
var nameOfDog: String
var thumbnail: String
var isFavorite: Bool
var imageUrls: [FetchImageModel]
}
import SwiftUI
import ObjectMapper
import AlamofireObjectMapper
// Note: - Map response form https://dog.ceo/api/breeds/list/all
class BreedsListAllResponse: Mappable {
var status: String = ""
var breeds: [FetchBreedModel] = []
required init?(map: Map){
if let breedsStr: [String:Any] = try? map.value("message") {
for breed in breedsStr {
let subBreedItems = breed.value as! [String]
if (subBreedItems.count > 0) {
for item in subBreedItems {
breeds.append(FetchBreedModel(id: UUID().uuidString, nameOfDog: "\(breed.key) \(item)"))
}
} else {
breeds.append(FetchBreedModel(id: UUID().uuidString, nameOfDog: breed.key))
}
}
}
}
func mapping(map: Map) {
status <- map["status"]
}
}
// Note: - Map response form https://dog.ceo/api/breeds/list/
struct BreedsListResponse: Mappable {
var breeds: [FetchBreedModel] = []
var status: String = ""
init?(map: Map) {
if let breedsStr: [String] = try? map.value("message") {
self.breeds = breedsStr.map { FetchBreedModel(id: UUID().uuidString, nameOfDog: $0) }
}
}
mutating func mapping(map: Map) {
status <- map["status"]
}
}
// Note: - Map response from https://dog.ceo/api/breed/affenpinscher/images
struct ImagesByBreedResponse: Mappable {
var images: [FetchImageModel] = []
var status: String = ""
init?(map: Map) {
if let imagesStr: [String] = try? map.value("message") {
self.images = imagesStr.map { FetchImageModel(id: UUID().uuidString, imageUrl: $0) }
}
}
mutating func mapping(map: Map) {
status <- map["status"]
}
}
class BreedVM : ObservableObject {
@Published var contentRows: [BreedModel] = []
@Published var selectedRow: BreedModel?
@Published var favoriteBreeds: [BreedModel] = []
init(){
getDogBreedInfo()
}
func getDogBreedInfo(){
self.getAllBreed { breedRows in
for breed in breedRows {
self.getBreedImages(nameOfDog: breed.nameOfDog) { breedImages in
// Note: - Every App loaded will have random thumbnail
var thumbnail: String = ""
if let image = breedImages.randomElement() {
thumbnail = image.imageUrl
}
if breedImages.count > 0 {
self.contentRows.append(BreedModel(id: UUID().uuidString,
nameOfDog: breed.nameOfDog,
thumbnail: thumbnail,
isFavorite: false,
imageUrls: breedImages))
}
}
}
}
}
func setFavoriteByObjectId(objectId: String) {
if let index = contentRows.firstIndex(where: { $0.id == objectId }) {
contentRows[index].isFavorite.toggle()
}
}
}
// Note: - Featch Data from APIs
extension BreedVM {
func getAllBreedsWithSubBread(completion: @escaping ([FetchBreedModel]) -> Void) {
// Note: - Map reponse data with BreedsListAllResponse
Alamofire.request(kBreedsListAllURL).responseObject { (response: DataResponse<BreedsListAllResponse>) in
let breedsResponse = response.result.value
completion(breedsResponse?.breeds ?? [])
}
}
func getAllBreed(completion: @escaping ([FetchBreedModel]) -> Void) {
// Note: - Map reponse data with BreedsListResponse
Alamofire.request(kBreedsListURL).responseObject { (response: DataResponse<BreedsListResponse>) in
let breedsResponse = response.result.value
completion(breedsResponse?.breeds ?? [])
}
}
func getBreedImages(nameOfDog: String, completion: @escaping ([FetchImageModel]) -> Void) {
// Note: - Map reponse data with ImagesByBreedResponse
let url = kImagesByBreedURL(for: nameOfDog)
Alamofire.request(url).responseObject { (response: DataResponse<ImagesByBreedResponse>) in
let breedsResponse = response.result.value
completion(breedsResponse?.images ?? [])
}
}
}