Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Swift Concurrency #265

Merged
merged 26 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions Shared/Extensions/Foundation/URLSession+Async.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// URLSession+Async.swift
// UhooiPicBook
//
// Created by uhooi on 2021/12/14.
//

import Foundation

@available(iOS, introduced: 13.0, deprecated: 15.0, message: "Use the built-in API instead")
extension URLSession {

func data(from url: URL) async throws -> (Data, URLResponse) {
try await withCheckedThrowingContinuation { continuation in
self.dataTask(with: url) { data, response, error in
if let error = error {
return continuation.resume(throwing: error)
}
guard let data = data, let response = response else {
return continuation.resume(throwing: URLError(.badServerResponse))
}
continuation.resume(returning: (data, response))
}.resume()
}
}

}
13 changes: 13 additions & 0 deletions Shared/Extensions/UncheckedSendable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// swiftlint:disable:this file_name
//
// UncheckedSendable.swift
// UhooiPicBook
//
// Created by uhooi on 2021/12/16.
//

import UIKit

extension URL: @unchecked Sendable {}
extension IndexPath: @unchecked Sendable {}
uhooi marked this conversation as resolved.
Show resolved Hide resolved
extension UIImage: @unchecked Sendable {}
uhooi marked this conversation as resolved.
Show resolved Hide resolved
53 changes: 25 additions & 28 deletions Shared/Repository/Monsters/MonstersRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import FirebaseFirestore

/// @mockable
protocol MonstersRepository: AnyObject { // swiftlint:disable:this file_types_order
func loadMonsters(_ completion: @escaping (Result<[MonsterDTO], Error>) -> Void)
func loadMonsters() async throws -> [MonsterDTO]
}

final class MonstersFirebaseClient {
Expand All @@ -18,35 +18,32 @@ final class MonstersFirebaseClient {

extension MonstersFirebaseClient: MonstersRepository {

func loadMonsters(_ completion: @escaping (Result<[MonsterDTO], Error>) -> Void) {
let monstersRef = self.firestore.collection("monsters")
monstersRef.getDocuments { querySnapshot, error in
if let error = error {
completion(.failure(error))
return
func loadMonsters() async throws -> [MonsterDTO] {
let monstersRef = firestore.collection("monsters")
let querySnapshot = try await monstersRef.getDocuments()

var monsters: [MonsterDTO] = []
for document in querySnapshot.documents.filter({ $0.exists }) {
let monster = document.data()
guard let name = monster["name"] as? String,
let description = monster["description"] as? String,
let baseColorCode = monster["base_color"] as? String,
let iconUrlString = monster["icon_url"] as? String,
let dancingUrlString = monster["dancing_url"] as? String,
let order = monster["order"] as? Int else {
continue
}

guard let querySnapshot = querySnapshot else {
fatalError("Fail to unwrap `querySnapshot`.")
}

var monsters: [MonsterDTO] = []
for document in querySnapshot.documents.filter({ $0.exists }) {
let monster = document.data()
guard let name = monster["name"] as? String,
let description = monster["description"] as? String,
let baseColorCode = monster["base_color"] as? String,
let iconUrlString = monster["icon_url"] as? String,
let dancingUrlString = monster["dancing_url"] as? String,
let order = monster["order"] as? Int else {
continue
}

monsters.append(MonsterDTO(name: name, description: description, baseColorCode: baseColorCode, iconUrlString: iconUrlString, dancingUrlString: dancingUrlString, order: order))
}

completion(.success(monsters))
monsters.append(MonsterDTO(
name: name,
description: description,
baseColorCode: baseColorCode,
iconUrlString: iconUrlString,
dancingUrlString: dancingUrlString,
order: order
))
}
}

return monsters
}
}
33 changes: 9 additions & 24 deletions Shared/Util/ImageCacheManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,25 @@ enum ImageCacheError: Error {

/// @mockable
protocol ImageCacheManagerProtocol: AnyObject {
func cacheImage(imageUrl: URL, completion: @escaping (Result<UIImage, Error>) -> Void)
func cacheImage(imageUrl: URL) async throws -> UIImage
func cacheGIFImage(imageUrl: URL) -> UIImage?
}

final class ImageCacheManager: ImageCacheManagerProtocol {

static let imageCache = NSCache<AnyObject, AnyObject>()

func cacheImage(imageUrl: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
func cacheImage(imageUrl: URL) async throws -> UIImage {
if let imageFromCache = ImageCacheManager.imageCache.object(forKey: imageUrl as AnyObject) as? UIImage {
completion(.success(imageFromCache))
return
return imageFromCache
}

var imageToCache = UIImage()

URLSession.shared.dataTask(with: imageUrl) { data, _, error in
if let error = error {
completion(.failure(error))
return
} else {
guard let data = data, let image = UIImage(data: data) else {
completion(.failure(ImageCacheError.loadingFailure))
return
}

imageToCache = image
ImageCacheManager.imageCache.setObject(imageToCache, forKey: imageUrl as AnyObject)
}

DispatchQueue.main.async {
completion(.success(imageToCache))
}
}.resume()
let (data, _) = try await URLSession.shared.data(from: imageUrl)
guard let image = UIImage(data: data) else {
throw ImageCacheError.loadingFailure
}
ImageCacheManager.imageCache.setObject(image, forKey: imageUrl as AnyObject)
return image
}

func cacheGIFImage(imageUrl: URL) -> UIImage? {
Expand Down
1 change: 1 addition & 0 deletions UhooiPicBook/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Gedatsu
#endif

@UIApplicationMain
@MainActor
class AppDelegate: UIResponder, UIApplicationDelegate {

// swiftlint:disable:next discouraged_optional_collection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import UIKit.UIImage

@MainActor
protocol MonsterDetailEventHandler: AnyObject {
func viewDidLoad()
func didTapDancingImageView(dancingImage: UIImage?)
Expand All @@ -18,6 +19,7 @@ protocol MonsterDetailEventHandler: AnyObject {
protocol MonsterDetailInteractorOutput: AnyObject {
}

@MainActor
final class MonsterDetailPresenter {

// MARK: Type Aliases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import UIKit

/// @mockable
@MainActor
protocol MonsterDetailRouterInput: AnyObject {
func popupDancingImage(_ dancingImage: UIImage)
func showActivity(_ senderView: UIView, text: String, icon: UIImage)
}

@MainActor
final class MonsterDetailRouter {

// MARK: Stored Instance Properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import UIKit

/// @mockable
@MainActor
protocol MonsterDetailUserInterface: AnyObject {
}

@MainActor
final class MonsterDetailViewController: UIViewController {

// MARK: Type Aliases
Expand Down Expand Up @@ -89,13 +91,10 @@ final class MonsterDetailViewController: UIViewController {
}

private func configureView() {
self.imageCacheManager.cacheImage(imageUrl: monster.iconUrl) { [weak self] result in
switch result {
case let .success(icon):
DispatchQueue.main.async {
self?.iconImageView.image = icon
}
case let .failure(error):
Task {
do {
iconImageView.image = try await imageCacheManager.cacheImage(imageUrl: monster.iconUrl)
uhooi marked this conversation as resolved.
Show resolved Hide resolved
} catch {
// TODO: エラーハンドリング
print(error)
}
Expand Down
2 changes: 2 additions & 0 deletions UhooiPicBook/Modules/MonsterList/Entities/MonsterEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ struct MonsterEntity: Equatable {
}

extension MonsterEntity: Codable {}

extension MonsterEntity: Sendable {}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

/// @mockable
protocol MonsterListInteractorInput: AnyObject {
func fetchMonsters(_ completion: @escaping (Result<[MonsterDTO], Error>) -> Void)
func saveForSpotlight(_ monster: MonsterEntity)
func fetchMonsters() async throws -> [MonsterDTO]
func saveForSpotlight(_ monster: MonsterEntity) async
}

final class MonsterListInteractor {
Expand Down Expand Up @@ -44,14 +44,14 @@ final class MonsterListInteractor {

extension MonsterListInteractor: MonsterListInteractorInput {

func fetchMonsters(_ completion: @escaping (Result<[MonsterDTO], Error>) -> Void) {
monstersRepository.loadMonsters { completion($0) }
func fetchMonsters() async throws -> [MonsterDTO] {
try await monstersRepository.loadMonsters()
}

func saveForSpotlight(_ monster: MonsterEntity) {
func saveForSpotlight(_ monster: MonsterEntity) async {
let key = "spotlight_\(monster.name)"
monstersTempRepository.saveMonster(monster, forKey: key)
spotlightRepository.saveMonster(monster, forKey: key)
await spotlightRepository.saveMonster(monster, forKey: key)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@

import Foundation

@MainActor
protocol MonsterListEventHandler: AnyObject {
func viewDidLoad()
func didSelectMonster(monster: MonsterEntity)
func viewDidLoad() async
func didSelectMonster(monster: MonsterEntity) async

// Menu
func didTapContactUs()
Expand All @@ -23,6 +24,7 @@ protocol MonsterListEventHandler: AnyObject {
protocol MonsterListInteractorOutput: AnyObject {
}

@MainActor
final class MonsterListPresenter {

// MARK: Type Aliases
Expand All @@ -49,23 +51,18 @@ final class MonsterListPresenter {

extension MonsterListPresenter: MonsterListEventHandler {

func viewDidLoad() {
self.view.startIndicator()
self.interactor.fetchMonsters { [weak self] result in
guard let self = self else {
return
}
switch result {
case let .success(monsters):
let monsterEntities = monsters
.sorted { $0.order < $1.order }
.map { self.convertDTOToEntity(dto: $0) }
self.view.showMonsters(monsterEntities)
self.view.stopIndicator()
case let .failure(error):
// TODO: エラーハンドリング
self.view.stopIndicator()
}
func viewDidLoad() async {
do {
view.startIndicator()
let monsters = try await interactor.fetchMonsters()
let monsterEntities = monsters
.sorted { $0.order < $1.order }
.map { convertDTOToEntity(dto: $0) }
view.showMonsters(monsterEntities)
view.stopIndicator()
} catch {
// TODO: エラーハンドリング
view.stopIndicator()
}
}

Expand All @@ -85,19 +82,19 @@ extension MonsterListPresenter: MonsterListEventHandler {
router.showAboutThisApp()
}

func didSelectMonster(monster: MonsterEntity) {
interactor.saveForSpotlight(monster)
func didSelectMonster(monster: MonsterEntity) async {
router.showMonsterDetail(monster: monster)
await interactor.saveForSpotlight(monster)
}

// MARK: Other Private Methods

private func convertDTOToEntity(dto: MonsterDTO) -> MonsterEntity {
guard let iconUrl = URL(string: dto.iconUrlString) else {
fatalError("Fail to load icon.") // TODO: エラーハンドリング
fatalError("Fail to load icon.")
}
guard let dancingUrl = URL(string: dto.dancingUrlString) else {
fatalError("Fail to load dancing image.") // TODO: エラーハンドリング
fatalError("Fail to load dancing image.")
}

return MonsterEntity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import UIKit

/// @mockable
@MainActor
protocol MonsterListRouterInput: AnyObject {
func showMonsterDetail(monster: MonsterEntity)

Expand All @@ -19,6 +20,7 @@ protocol MonsterListRouterInput: AnyObject {
func showAboutThisApp()
}

@MainActor
final class MonsterListRouter {

// MARK: Stored Instance Properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import UIKit

@MainActor
final class MonsterCollectionViewCell: UICollectionViewCell {

// MARK: Stored Instance Properties
Expand Down