20211224のiOSに関する記事は5件です。

自前でDIコンテナを作ってみる試みとRxSwiftを利用した構成への適用を試してみる

1. はじめに 皆様お疲れ様です。「iOS Advent Calendar」の24日目を担当させて頂きます、fumiyasac(Fumiya Sakai)と申します。何卒よろしくお願い致します。 Swift/Kotlin愛好会Advent Calendarでは、「Androidアプリでバックグラウンド再生機能を実現するためのヒントとiOSアプリとの見比べた際の特徴を簡単にまとめてみた」というタイトルにて、iOS/Androidアプリでのバックグラウンド再生機能を実現する上で、ポイントになり得る点を双方の比較をしながら解説した記事を書きましたので、こちらもご覧頂けますと嬉しく思います。 今回はUIまわりのトピックではなく、実務や個人開発を通じて、OSSライブラリのDI(Dependency Injection)を実現するためのライブラリを参考にしながら、自前で実装する&リプレイスするが機会がありましたので、自分なりの事例に関して簡単ではありますがご紹介ができればと思います。 以前登壇した際の発表資料: また、今回の内容につきましては、potatotips #76 (iOS/Android開発Tips共有会)にて登壇の際に利用した資料を下記のリンクにて共有しておりますので、こちらも是非ご活用頂けますと幸いです。紹介しているスライドの方では、実際の業務内で導入するまでのアイデアや検証の過程、そして導入をしていくところまでの流れを簡単にまとめています。 Slide: https://www.slideshare.net/fumiyasakai37/didiy 2. 自作したDIコンテナにおいて参考にした資料やドキュメント等の紹介 以前にも「ライブラリを利用しない実現方法のアイデア」の一例として【実装MEMO】PropertyWrappersの機能を利用したDependency Injectionのコードに触れた際の備忘録をMedium内で軽くまとめた事もあり、これを応用できないかを最初は考えました。ですが、実際のプロジェクトへ導入するための検証を重ねていく中で少しそぐわなかったので、よく利用されているOSSライブラリでの実装方法等も参考にしながら下記の様な形をとってみました。 ⭐️ 2-1. 今回自作を試みたDIコンテナ例で主に参考にした資料 今回参考にした手法は、「How to : create your SUPER simple dependency injector container in Swift」で紹介されていた方法になります。この記事内で紹介されている方法でのポイントは、大きなシングルトンの中に型と名前で管理された必要な責務のインスタンスを格納するという点になるかと思います。 今回、適用を考えていたアプリにおいて一番大事な部分は、 1. コンストラクタインジェクションができること 2. Local / Remoteで同じProtocolを使う際も名前をつけ方で管理できること という2点だったので、この方針で大きな問題はなさそうと判断しました。 まずは、DIコンテナ用のクラスを準備は下記の様な形で準備をすることとなります。 【1. 責務毎のインスタンスを登録するためのDIコンテナ用のクラス】 DependeciesContainer.swift import Foundation final class DependeciesContainer { // MARK: - DIコンテナ自体はSingletonとして保持する static let shared = DependeciesContainer() private init() {} // MEMO: DependencyKeyクラスを利用して「型」と「名前」を利用して分類する private var dependecies: [DependencyKey: Any] = [:] // MARK: - Function func register<T>( _ type: T.Type, impl: Any, name: String? = nil ) { let dependencyKey = DependencyKey(type: type, name: name) dependecies[dependencyKey] = impl } func resolve<T>( _ type: T.Type, name: String? = nil ) -> T { let dependencyKey = DependencyKey(type: type, name: name) if let dep = dependecies[dependencyKey] as? T { return dep } else { // MEMO: 設定し忘れがあった場合にはfatalErrorで検知できるようにする let protocolTypeName = NSString(string: "\(type)").components(separatedBy: ".").last! fatalError("\(protocolTypeName)の依存性を解決できませんでした。当該実装クラス:\(protocolTypeName).") } } } final class DependencyKey: Hashable, Equatable { // MARK: - Properties private let type: Any.Type private let name: String? // MARK: - Initializer init(type: Any.Type, name: String? = nil) { self.type = type self.name = name } // MARK: - Hashable, Equatable func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(type)) hasher.combine(name) } static func == (lhs: DependencyKey, rhs: DependencyKey) -> Bool { return lhs.type == rhs.type && lhs.name == rhs.name } } そして次に、それぞれの責務におけるクラス同士の依存関係の解決を下記の様な形で図っていくためのクラスを定義し、このクラスのインスタンスをAppDelegate.swiftへ適用するような形とします。 【2. 依存関係の解決を図る部分の処理】 DependenciesDefinition.swift final class DependenciesDefinition { // MARK: - Function func inject() { // MEMO: インスタンスを保持するための場所 let dependecies = DependeciesContainer.shared // ※途中省略 // MARK: - Infrastructure // → アプリ内DataStore処理やRestAPI/GraphQLのクライアントに関する処理 dependecies.register( MovieQualityLocalStore.self, impl: MovieQualityLocalStoreImpl() ) dependecies.register( ApiClient.self, impl: ApiClientManager.shared ) // ※以降はInfrastructure層の依存関係解決処理が続きます... // MARK: - Repository // → アプリ内DataStore処理からのデータ取得&保存処理やRestAPI/GraphQLからのデータ取得処理 dependecies.register( FeaturedMovieRepository.self, impl: FeaturedMovieRepositoryImpl( apiClient: dependecies.resolve(ApiClient.self), backgroundScheduler: dependecies.resolve(ImmediateSchedulerType.self, name: background) // API通信処理はバックグラウンドスレッドで実施したい ) ) // ※以降はRepository層の依存関係解決処理が続きます... // MARK: - UseCase // → Repositoryクラス/Serviceクラスで定義した処理を組み合わせて実現するビジネスロジックに関する処理 dependecies.register( GetMainUseCase.self, impl: GetMainUseCaseImpl( mainBannerRepository: dependecies.resolve(MainBannerRepository.self), mainNewsRepository: dependecies.resolve(MainNewsRepository.self), featuredMovieRepository: dependecies.resolve(FeaturedMovieRepository.self), mainMovieRepository: dependecies.resolve(MainMovieRepository.self) ) ) // ※以降はUseCase層の依存関係解決処理が続きます... // MEMO: 上記の処理例では、Infrastructure → Repository → UseCaseという順番で依存関係の解決を図っていく形となります。 } } 【3. アプリ起動時に必要なインスタンスを初期化する】 AppDelegate.swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // MEMO: 自作でのDependeciesContainerをインスタンス化する DependenciesDefinition().inject() // ※途中省略 } } 形としてはかなり愚直な形の記述方法にはなってしまいますが、それぞれの責務毎にある程度分離された形となっているならば整理はしやすいのではないかと思います。ただ裏を返せば、責務の分離を曖昧な感じにしてしまうと必要のない順番を気にしなければいけなくなるので、その点には少し気をつけると良いかもしれません。 ⭐️ 2-2. iOS/AndroidでのDIライブラリやその他Dependency Injectionを理解する上での参考資料 ライブラリに依存しない形ではあるけれども、できるだけ似た感覚で書ける形を模索する際においても、OSSライブラリを活用したDIの導入事例や他のプロジェクトの実装事例に関する記事等にも幅広く触れることで、現在開発中のアプリに導入する際の検討材料や知見になるかと思いますので、まずは簡単なサンプル実装を試してみたりすることで事前のイメージを掴んでおくと更に良さそうに思います。 【iOSでのライブラリ例】 Swinject DITranquillity needle 【Androidでのライブラリ例】 Dagger2 Dagger Hilt 【今回改めて復習する際に参考にした資料】 SwinjectとDIKitでDependency Injectionしてみた Swiftで外部ライブラリを使わないDIの管理方法 3. RxSwiftを利用した構成に対しての適用していく部分の解説 ここからは、前述したDIを適用した際の記載例についてもいくつかの例をピックアップしてみます。以降で紹介するコード例については、下記の図で示す様な形のアーキテクチャを想定しています。構成の基本的な概要としては、画面の表示処理部分についてはMVPパターンを基本とし、Modelに相当する部分については、UseCase / Domain / Infrastructureの3層に処理を分離することでできるだけ疎結合な形をとっている点がポイントになります。また、ロジックに近い部分についてはRxSwiftの Single / Maybe / Completable を利用することで各種処理をつなげていく様なイメージになります。 RxSwift 3.3.0で追加された3つのUnit(Single, Maybe, Completable) ⭐️ 3-1. 処理レイヤーによって利用するスレッドを適切に指定するために第2引数を活用する 前述したDIコンテナについては第2引数に名前を定義することを利用して、メインスレッド/バックグラウンドスレッドでの操作を必要な責務の処理に応じて適用できるようにする例になります。 // ---------- // (1) DependenciesDefinitionクラスのinject()内での記述 // ---------- // 処理を実行する際にバックグラウンドスレッドにしたい際につける名前 let background = "background" // メインスレッド動作 dependecies.register( ImmediateSchedulerType.self, impl: MainScheduler.instance ) // バックグラウンドスレッド動作 dependecies.register( ImmediateSchedulerType.self, impl: SerialDispatchQueueScheduler(qos: .default), name: background // 名前を付与する ) // ---------- // (2) Presenter側での処理はUIへの取得データ反映処理なのでメインスレッドで実行 // ---------- static func createMainPresneter() -> MainPresenter { return MainPresenterImpl( getMainUseCase: dependecies.resolve(GetMainUseCase.self), getFavoriteMainMoviesUseCase: dependecies.resolve(GetFavoriteMainMoviesUseCase.self), saveFavoriteMainMovieUseCase: dependecies.resolve(SaveFavoriteMainMovieUseCase.self), // MEMO: 名前がない場合はメインスレッドでの処理となる mainScheduler: dependecies.resolve(ImmediateSchedulerType.self) ) } // ---------- // (3) Repository側での処理はAPI / GraphQLでの処理なのでバックグラウンドスレッドで実行 // ---------- dependecies.register( CategoryRepository.self, impl: CategoryRepositoryImpl( graphQLClient: dependecies.resolve(GraphQLClient.self), readApiCacheClient: dependecies.resolve(ReadApiCacheClient.self), // MEMO: 第2引数で名前を付与した場合はバックグラウンドスレッドでの処理となる backgroundScheduler: dependecies.resolve(ImmediateSchedulerType.self, name: background) ) ) ⭐️ 3-2. Cacheを利用するか?サーバーとの通信を利用するか適切に指定するために第2引数を活用する 先程のメインスレッド/バックグラウンドスレッドでの操作と同じ様な要領で、Localでのキャッシュ機構からのデータ取得処理/GraphQLから非同期通信でのデータ取得処理についても、第2引数での名前を利用した管理を使う例になります。記載しているコードについてはRxSwiftを利用した処理での事例になりますが、責務に応じたスレッドの適用と同時にLocalでのキャッシュ機構からのデータ取得処理/GraphQLから非同期通信でのデータ取得処理についても適切な名前をつけながら整理していくイメージを持って頂くと分かりやすいかもしれません。 // ---------- // (1)-1: DependenciesDefinitionクラスのinject()内での記述 // ---------- // Cacheからのデータ取得をする際につける名前 let local = "local" // 処理を実行する際にバックグラウンドスレッドにしたい際につける名前 let background = "background" // ---------- // (1)-2: GraphQLから非同期通信でデータを取得する // ---------- dependecies.register( CourseRepository.self, impl: CourseRepositoryImpl( graphQLClient: dependecies.resolve(GraphQLClient.self), readApiCacheClient: dependecies.resolve(ReadApiCacheClient.self), backgroundScheduler: dependecies.resolve(ImmediateSchedulerType.self, name: background) ) ) // ---------- // (1)-3: Localでのキャッシュ機構からデータを取得する // ---------- dependecies.register( CourseRepository.self, impl: LocalCourseRepositoryImpl( readApiCacheClient: dependecies.resolve(ReadApiCacheClient.self), backgroundScheduler: dependecies.resolve(ImmediateSchedulerType.self, name: background) ), // MEMO: 第2引数で名前を付与した場合は同じ型でもLocalから取得したデータを利用する name: local ) // ---------- // (2)-1 Localのキャッシュ機構またはGraphQLでの処理を利用したRepository層の例(RxSwiftを利用した処理記載例) // ---------- import RxSwift protocol CourseRepository { func findAll() -> Single<[Course]> } // ---------- // (2)-2: Localのキャッシュ機構からデータを取得する処理例 // ---------- final class LocalCourseRepositoryImpl: CourseRepository { private let readApiCacheClient: ReadApiCacheClient private let backgroundScheduler: ImmediateSchedulerType init( readApiCacheClient: ReadApiCacheClient, backgroundScheduler: ImmediateSchedulerType ) { self.readApiCacheClient = readApiCacheClient self.backgroundScheduler = backgroundScheduler } func findAll() -> Single<[Course]> { // キャッシュ機構に格納されているEntityデータを取得してDomainModelへ変換して返す return readApiCacheClient.getCourseEntities() .subscribe( on: backgroundScheduler // ← Repository処理なのでバックグラウンドスレッドで実行したい ).map { courseEntities in courseEntities.map { courseEntity in Course(courseEntity) } } } } // ---------- // (2)-3: GraphQLでの処理からデータを取得する処理例 // ---------- final class CourseRepositoryImpl: CourseRepository { private let graphQLClient: GraphQLClient private let readApiCacheClient: ReadApiCacheClient private let backgroundScheduler: ImmediateSchedulerType init( graphQLClient: GraphQLClient, readApiCacheClient: ReadApiCacheClient, backgroundScheduler: ImmediateSchedulerType ) { self.graphQLClient = graphQLClient self.readApiCacheClient = readApiCacheClient self.backgroundScheduler = backgroundScheduler } func findAll() -> Single<[Course]> { // GraphQLからEntityデータを取得してDomainModelへ変換して返す return graphQLClient.getCourses() .subscribe( on: backgroundScheduler // ← Repository処理なのでバックグラウンドスレッドで実行したい ).do( afterSuccess: { [weak self] entities in guard let weakSelf = self else { return } // GraphQLからEntityデータを取得が成功した後にキャッシュ機構に保存する weakSelf.readApiCacheClient.saveCourseEntities(entities) } ).map { entities in entities.map { entity in Course(entity) } } } } // ---------- // (3) Local・Remote対応のUseCaseをそれぞれ作成する // ---------- dependecies.register( GetCoursesUseCase.self, impl: GetCoursesUseCaseImpl( courseRepository: dependecies.resolve(CourseRepository.self) ) ) dependecies.register( GetCoursesUseCase.self, impl: GetCoursesUseCaseImpl( courseRepository: dependecies.resolve(CourseRepository.self, name: local) ), // MEMO: 第2引数で名前を付与した場合は同じ型でもLocalから取得したデータを利用する name: local ) // ---------- // (4) Presenter側での処理を記載する(RxSwiftを利用した処理記載例) // ※ こちらの処理については実際のPresenter処理内部における一部分を抜粋したものになります // ---------- private let getCoursesUseCase: GetCoursesUseCase private let localGetCoursesUseCase: GetCoursesUseCase private let mainScheduler: ImmediateSchedulerType private let disposeBag = DisposeBag() func viewWillAppear() { // まずはキャッシュ取得処理を実行する prefetchAndSetup { [weak self] in guard let weakSelf = self else { return } // まずはキャッシュ取得処理が終了次第、続けてAPI取得処理を実行する weakSelf.fetchAndSetup() } } private func prefetchAndSetup(completion: @escaping () -> Void) { localGetCoursesUseCase.execute() .do( onSubscribe: { [weak self] in guard let weakSelf = self else { return } // 処理開始時に実行したい処理 }, onDispose: { // キャッシュ取得処理の購読が終わったら実行したい処理をクロージャーで引き渡す // 例: キャッシュデータ反映処理後にAPI通信処理を試みる流れ completion() } ) .observe( on: mainScheduler // ← Presenter処理なのでメインスレッドで実行したい ) .subscribe( onSuccess: { [weak self] courseDtos in guard let weakSelf = self else { return } // キャッシュ取得処理が成功した場合 // 例: キャッシュデータをまずは画面に一時的に表示する }, onFailure: { _ in // キャッシュ取得処理が失敗した場合 } ) .disposed(by: disposeBag) } private func fetchAndSetup() { getCoursesUseCase.execute() .observe( on: mainScheduler // ← Presenter処理なのでメインスレッドで実行したい ) .subscribe( onSuccess: { [weak self] courseDtos in guard let weakSelf = self else { return } // API取得処理が成功した場合 // 例: API取得データを正式に画面に表示する }, onFailure: { [weak self] _ in guard let weakSelf = self else { return } // API取得処理が失敗した場合 // 例: 通信エラーを通知するダイアログを表示する } ) .disposed(by: disposeBag) } ⭐️ 3-3. PresenterとViewController部分に関する処理例 最後に画面を生成するタイミングでPresenterクラスを渡す部分に関する例になります。画面生成時にPresenterクラスを初期化したいので、この部分についてはDIコンテナに直接登録しないでFactoryメソッドを別途定義する形をとっています。ViewControllerを初期化するタイミングで一緒にPresenterを初期化するFactoryメソッドを適用する点がこの部分のポイントになります。 // ---------- // (1)-1: Infrastructure/Repository/UseCaseまでの依存関係をDIコンテナに登録する // ---------- final class DependenciesDefinition { // MARK: - Function func inject() { // MEMO: Cacheからのデータ取得をする際につける名前 let local = "local" // MEMO: 処理を実行する際にバックグラウンドスレッドにしたい際につける名前 let background = "background" // MEMO: インスタンスを保持するための場所 let dependecies = DependeciesContainer.shared // ※以降はInfrastructure/Repository/UseCaseの依存関係の解決処理が続く... } } // ---------- // (1)-2: Presenterに適用するためのFactoryメソッドを定義する // ---------- final class PresenterFactory { // MEMO: Cacheからのデータ取得をする際につける名前 private static let local = "local" // MEMO: 処理を実行する際にバックグラウンドスレッドにしたい際につける名前 private static let background = "background" // MEMO: インスタンスを保持するための場所 private static let dependecies = DependeciesContainer.shared // MARK: - Static Function static func createMainPresenter() -> MainPresenter { return MainPresenterImpl( getCoursesUseCase: dependecies.resolve(GetCoursesUseCase.self), localGetCoursesUseCase: dependecies.resolve(GetCoursesUseCase.self, name: local), mainScheduler: dependecies.resolve(ImmediateSchedulerType.self) ) } // ※以降は各種Presenterに適用するFactoryメソッド定義が続く... } // ---------- // (2) PresenterについてはFactoryメソッドを利用して適用する // ---------- final class MainViewController: UIViewController { // MARK: - Property private let presenter: MainPresenter // ※途中省略 // MARK: - Override override func viewDidLoad() { super.viewDidLoad() presenter = PresenterFactory.createMainPresenter() } // ※途中省略 } 余談になりますが、Presenterクラスの初期化処理をviewDidLoad()のタイミングで実施していますが、iOS13以降ではStoryboardでもDependencyInjectionができる様になったので、そのタイミングで適用する形でも実現する事ができます。 4. その他RxSwiftを利用した処理に関する処理例についての補足 ここからは補足として、DIコンテナ側と直接関係する処理ではありませんが、RxSwiftを利用した適切な形へ変換していく際の処理事例を掲載しておきます。 ここで紹介するのは、下図の様な形で複数のRepositoryの処理(実際はGraphQLやRestAPIからの非同期通信処理)を利用して、画面表示に必要なデータへ変換する際の事例となります。RxSwiftの.flatMapや.zip等のオペレータを活用することで、適切な形に変換をかける処理や取得処理のタイミングを合わせていく処理を組み合わせていく点に注目しながら見ていくと良さそうに思います。 import Foundation import RxSwift final class GetMainUseCaseImpl: GetMainUseCase { private let categoryRepository: CategoryRepository private let rankingContentsRepository: RankingContentsRepository private let courseRepository: CourseRepository private let userCourseRepository: UserCourseRepository private let stockedCourseRepository: StockedCourseRepository init( categoryRepository: CategoryRepository, rankingContentsRepository: RankingContentsRepository, courseRepository: CourseRepository, userCourseRepository: UserCourseRepository, stockedCourseRepository: StockedCourseRepository ) { self.categoryRepository = categoryRepository self.rankingContentsRepository = rankingContentsRepository self.courseRepository = courseRepository self.userCourseRepository = userCourseRepository self.stockedCourseRepository = stockedCourseRepository } func execute( recentCoursesLimit: Int, recommendedCoursesLimit: Int ) -> Single<MainDto> { // MEMO: まずはお気に入り登録中のコース情報を取得する処理を実行し、取得できた情報を利用して後続の処理を実行する。 return stockedCourseRepository.findAll().flatMap { [weak self] stockedCourses in guard let weakSelf = self else { return Single.error(CommonError.notExistSelf) } // MEMO: MainDtoを作成するために必要となるデータ取得をそれぞれ試みる。 // - 1. カテゴリー一覧 // - 2. 最近見たコース一覧 // - 3. おすすめコース一覧 // - 4. トピック別ランキング let categoryDtosSingle = weakSelf.getCategoryDtosSingle() let recentCourseDtosSingle = weakSelf.getRecentCourseDtosSingle( limit: recentCoursesLimit, stockedCourses: stockedCourses ) let recommendedCourseDtosSingle = weakSelf.getRecommendedCourseDtosSingle( limit: recommendedCoursesLimit, stockedCourses: stockedCourses ) let getRankingCoursesSingle = weakSelf.getRankingContentsDtosSingle( stockedCourses: stockedCourses ) // MEMO: Single.zipを利用し、画面表示に必要な値が全て取得できた場合にだけ、画面表示に必要なMainDtoを返す形とする。 return Single.zip( categoryDtosSingle, recentCourseDtosSingle, recommendedCourseDtosSingle, getRankingCoursesSingle ).map { tuple -> MainDto in // ← 取得結果をMainDtoに変換する必要がある let ( categoryDtos, recentCourseDtos, recommendedCourses, rankingContentsDtos ) = tuple return MainDto( categoryDtos: categoryDtos, recentCourseDtos: recentCourseDtos, recommendedCourseDtos: recommendedCourseDtos, rankingContentsDtos: rankingContentsDtos ) } } } private func getCategoryDtosSingle() -> Single<[CategoryDto]> { // MEMO: カテゴリー一覧については、データを取得してCategoryDtoへ変換するだけとなる。 return categoryRepository.findAll() .map { categories in categories.map { category in CategoryDto( category: category ) } } } private func getRecentCourseDtosSingle( limit: Int, stockedCourses: [StockedCourse] ) -> Single<[CourseDto]> { // MEMO: まずは、最近見たコース一覧のマスタデータ一覧を取得する。 return courseRepository.findRecentWithLimit( limit ).flatMap { courses in // ← .flatMapでの変換 // MEMO: 次に、最近見たコース一覧のidに紐づく自分が取り組んでいるコース情報一覧を取得する。 self.userCourseRepository.findByIds( courses.map { course in course.id } ).map { userCourses in // MEMO: CourseDtoの作成 // - 1. 取得できた最近見たコース一覧のマスタデータ一覧 // - 2. 引数で受け取ったお気に入り登録中のコース情報 // - 3. 取得できた自分が取り組んでいるコース情報一覧 courses.map { course in CourseDto( course: course, stockedCourses: stockedCourses, userCourses: userCourses ) } } } } private func getRecommendedCourseDtosSingle( limit: Int, stockedCourses: [StockedCourse] ) -> Single<[CourseDto]> { // MEMO: まずは、おすすめコース一覧のマスタデータ一覧を取得する。 return courseRepository.findRecommendedWithLimit( limit ).flatMap { [weak self] courses in // ← .flatMapでの変換 guard let weakSelf = self else { return Single.error(CommonError.notExistSelf) } // MEMO: 次に、最近見たコース一覧のidに紐づく自分が取り組んでいるコース情報一覧を取得する。 return weakSelf.userCourseRepository.findByIds( courses.map { course in course.id } ).map { userCourses in // MEMO: CourseDtoの作成 // - 1. 取得できた最近見たコース一覧のマスタデータ一覧 // - 2. 引数で受け取ったお気に入り登録中のコース情報 // - 3. 取得できた自分が取り組んでいるコース情報一覧 courses.map { course in CourseDto( course: course, stockedCourses: stockedCourses, userCourses: userCourses ) } } } } private func getRankingContentsDtosSingle( stockedCourses: [StockedCourse] ) -> Single<[RankingContentsDto]> { // MEMO: まずは、ランキングコンテンツを全て取得する。 return getAllRankingContents() .flatMap { [weak self] allRankingContents in // ← .flatMapでの変換 guard let weakSelf = self else { return Single.error(CommonError.notExistSelf) } // MEMO: このままの状態では[Single<FeaturedContentsDto>]となってしまうので、Single<[FeaturedContentsDto]>に変換するためにSingle.zipを適用する。 return Single.zip( allRankingContents.map { rankingContents in // MEMO: 取得したランキングコンテンツを元にしてRankingContentsDtoを取得する。 weakSelf.getRankingContentsDto(rankingContents: rankingContents, stockedCourses: stockedCourses) } ) } } // 特集コンテンツの表示に必要なDtoを取得する(特集セクション1つ分に相当するランキングデータをDtoへ変換する) private func getRankingContentsDto( rankingContents: RankingContents, stockedCourses: [StockedCourse] ) -> Single<RankingContentsDto> { // MEMO: まずは、ランキングコンテンツに格納されているCourseのID一覧から、ランキングで表示するコースのマスタデータ一覧を取得する。 return courseRepository.findByIds( rankingContents.courseIds ).flatMap { [weak self] courses in // ← .flatMapでの変換 guard let weakSelf = self else { return Single.error(CommonError.notExistSelf) } // MEMO: 次に、ランキングで表示するコース一覧のidに紐づく自分が取り組んでいるコース情報一覧を取得する。 return weakSelf.userCourseRepository .findByIds( courses.map { course in course.id } ) .map { userCourses in // MEMO: CourseDtoの作成 // - 1. 取得できた最近見たコース一覧のマスタデータ一覧 // - 2. 引数で受け取ったお気に入り登録中のコース情報 // - 3. 取得できた自分が取り組んでいるコース情報一覧 courses.map { course in CourseDto( course: course, stockedCourses: stockedCourses, userCourses: userCourses ) } }.map { CourseDtos in // MEMO: RankingContentsDtoの作成 // - 1. 作成したCourseDtoの一覧 // - 2. ランキングコンテンツからの必要な情報 RankingContentsDto( label: rankingContents.label, labelId: rankingContents.labelId, courseDtos: CourseDtos ) } } } // ランキングコンテンツを全て取得する(セクションが複数存在することを想定) private func getAllRankingContents() -> Single<[RankingContents]> { return rankingContentsRepository.getAllRankingContentsByTopics() } } 5. あとがき iOSではSwinjectやDIKit・AndroidではDagger2やDaggerHiltについては多少自分の手元でも軽く試した経験はあったものの、DIコンテナ部分を自前で作成して実際のアプリ内に適用したり、既存のライブラリからこれまでの実装方針をできるだけ崩すことない形でのリプレイスを経験を通して、iOS/Android共にDI用の有名ライブラリがあり、それぞれ特徴的な部分があるので、平素の開発においては慣習的となっている部分に改めて深く触れることでその特徴やメリット・デメリットを再認識する良い機会になったと共に、既存である程度の規模感があるプロジェクトでは、チームの中で「しっくり来る形」を見出す準備の大切さを感じると同時に、自前でこの部分を実装する際にはチームの状況等を鑑みて良い特徴となり得る部分を積極的に取り入れていくと良さそうに思いました。 今年を改めて軽く振り返ってみると、AndroidやFlutterに関するインプットは不定期ではありますが、実践できてはいたものの特にiOS側のアウトプットは若干少な目になってしまった点は反省の余地があるかと思います。そして、まだまだ新しい技術を利用したUI実装については実践し切れていない部分もあったので、来年はバランスを考えながらアウトプットできればと考えておりますので、引き続きよろしくお願い致します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Playgroundsを用いるApp開発において,パッケージを追加する

概要 Playgrounds及びiOS, iPadOSのアップデートに伴い,Mac上のXcodeを用いずともAppの開発を行うことが可能となった。Playgroundsにおいて,外部ライブラリを使用したいと思った。XcodeであればcocoaPodsを使用するが,Playgroundsでは使えなさそうだった。そこで,Playgroundsにおいて,外部ライブラリ(パッケージ)を追加する方法を記載する。 前提 今回は,アプリ内でグラフを描画したかったので,ChartsというSwiftのライブラリを導入しようと思った。基本ライブラリであれば, import.swift import SwiftUI import Accelerate のようにすれば簡単に導入できる。 ところが,外部ライブラリであるChartsはこのままimport文を記述しても"No such module 'Charts'"と,怒られる。 方法 ①Playgroundsのアプリ内で,ファイルを追加するボタンをタップする。 ②最下部に「Swiftパッケージ」とあるので,これをタップする。 ③パッケージURLを入れる欄があるので,ここにimportしたいライブラリのURLを入力する。 このパッケージURLがどのようにすれば発見できるのかが不明だった。ただ,ChartsはGitHubに上がっており,そのリポジトリのURLを入力すると適切なバージョンと導入されるものが自動で表示された。そのまま右上のプロジェクトに追加をタップすると,パッケージとしてChartsが追加された。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOSアプリ】VIPERアーキテクチャのプロダクトで配属されて3週間の新卒社員のOJTをした話

この記事はレコチョク Advent Calendar 2021の最終日の記事となります。 はじめに はじめまして。レコチョクの河野です。19年4月に新卒入社し、現在はiOSアプリエンジニアとして、Eggs・新規プロダクトのiOSアプリの開発・運用などを担当しています。また、21年度は新卒エンジニア研修全体の主担当としてカリキュラム策定から新卒メンバーへのフォローなど研修期間を通じて様々な業務を担当しました。音楽の専門領域はアイドルまわりで、個人的今年の1曲は日向坂46 「どうする?どうする?どうする?」です。よろしくお願いします。 レコチョクでは例年、エンジニアとして新卒入社したメンバーが新卒研修を終えた後に各部署に配属されてからOJT期間を経て、段階的に実務的な開発・運用業務に参画する仕組みがあります。そして、今年度iOSチームでは21年入社のotouto3・YukaFukayamaの2名を仲間として迎え入れました。 今回の記事ではメンター役として実プロダクトを題材にしながら、研修を終えた後のOJT期間の中で2人をどのように受け入れたのかを紹介しようと思います。 レコチョクの新卒エンジニア研修について レコチョクでは例年、4月〜5月のビジネス研修の後、6月〜9月の4ヶ月間で新卒エンジニア研修を実施します。入社までにプログラミングの経験が無い未経験者でもついてこられるように、基礎的なUNIXコマンドの使い方から始まり、AWS・データ分析・ネイティブアプリなど専門的な内容まで網羅的に学習するカリキュラムが用意されています。詳細についてはここでは割愛しますが、弊社のs1250200が研修を受けた側の視点での記事をアップしていますので、あわせてこちらもご覧ください。 レコチョク iOSチームのOJT レコチョクのiOSチームでは例年、OJT期間中にこなすような決まったカリキュラムはありません。(マネジメント側と調整した上で、)新規で開発が始まるプロジェクトに開発メンバーとして参加する年度もあれば、すでにリリースされている社内のプロダクトのクローンアプリを作り、その画面を実際に作った先輩にレビューをしてもらいアプリ開発に必要な知識を習得する年度もあります。受け入れ時のプロダクトの開発状況や、指導に当たる先輩社員(以下、メンター)のリソース、新卒メンバーのタイプなど、状況に応じて臨機応変にテーマや進め方を決めています。 ただし、年度ごとに方針が違っていても「可能な限り実プロダクトに近いところで開発を体験してもらう」というコンセプトは共通しています。実際に世に出る(出ている)画面を実装してもらうことで、自分たちが学んでいる事柄の必要性をより理解してもらえると考えているからです。そして、何よりも自分たちが頑張って作った機能や処理が実際にアプリとして世に出るんだ、ということは何より指導を受ける新卒たちのモチベーションにつながるからです。 今年度は絶賛開発中の新規プロダクトの開発のなかでOJTの課題を設定し、アプリ開発に必要な知識を習得してもらう方針に決めました。 プロダクトのアーキテクチャ レコチョクのiOSアプリの開発では、汎用的で堅牢な内部設計を意識した開発を推進しています。 とりわけ、OJTの題材となる新規プロダクトでは、VIPERアーキテクチャをベースに開発を進めています。詳細は割愛しますが、VIPERの構成要素(以下、コンポーネント)となるView・Interactor・Presenter・Entity・Routerを基本とし、さらに画面(View)の描画等の責務を持つPresentation層、Viewに表示するデータの取得処理のビジネスロジックを責務とするDomain層・各データソースからのデータ取得/更新を責務とするDataStore層の3つのレイヤーをベースとしたマルチモジュール構成も導入しています。 このアーキテクチャを採用している実プロダクトにコミットをするには、VIPERの各コンポーネント毎の責務を理解した上でコードを書いていく必要があります。しかし、いきなり全ての理解を要求するのはOJTの難易度設定として無理があります。そこで、今回はマルチモジュールの各レイヤー(層)に対応する形で課題を設定し、段階的にコミットしてもらうことで徐々にアプリのアーキテクチャの全貌を理解してもらえるようにOJTを進めていくことにしました。 どう進めたか Swiftの基礎を学習する まず、10月1日に配属されてから実プロダクトのコードを読み書きするために必要最低限のSwiftの知識を学習する期間としました。前述したように参加してもらう予定のプロジェクトがUIKitで開発されているため、Swift × UIKitで基礎的な項目を解説しているカリキュラムを最初から順にこなしていくドリル形式で学習を進めてもらいました。 今年度のOJTでは株式会社ゆめみさん、株式会社ミクシィさんがそれぞれ公開されている資料をSwift5の仕様に合わせてメンター側でフォローを入れつつ、Swiftの基礎を学習してもらいました。 株式会社ゆめみ iOS研修資料 株式会社ミクシィ トレーニングコース 2週間ほどでカリキュラムベースでの学習は一旦区切りとし、実プロダクトのコードを使用した実践形式の演習へと進めていきました。 実プロダクト上でコミットする アーキテクチャという概念を導入する まず初めにメンバー全員がオフィスに集合し、3時間くらいのまとまった時間でアプリ内部の設計やSwiftにおけるプロトコルの概念について一通り話をする解説会を開きました。 解説会ではいきなり理論を説明するのではなく、メンターと新卒たち全員でグループディスカッションのような形式で会話をしていく中でアプリ内部の処理にまつわる「ロジック」の概念を明らかにしていくところからはじめて行きました。 会話の中で、アプリ内部の処理にまつわる「ロジック」が 画面の描画・更新にまつわるロジック(プレゼンテーションロジック) 要件を実現するためのデータ自体の取得や更新などにまつわるロジック(ビジネスロジック) 遷移処理などにまつわるロジック(ルーティングロジック) などに大別できるよねという共通認識が持てるようになった段階で、VIPERの基本的な構成と実プロダクトでの構成について解説をしました。 VIPERの概念を導入した後、手元のMacと検証端末で実際にアプリをビルドしながら操作してもらい、Xcode上でブレイクポイントを貼りながら、挙動を追いかけて確認をしてもらいました。この段階では主に画面描画・更新に関係するView(ViewController) → Presenterへの処理の流れや、遷移処理のView(ViewController) → Presenter → Routerと処理が進んでいく一連の動きを中心に見てもらい、アプリの画面上で起きていることとコード上での処理の対応関係を明確に持ってもらうようにしました。 Presentation層ではじめてのコミットをしてもらう 初日の導入を終えた後、デビュー戦として最初に任せたのは当時開発を進めていた、ある機能についてのViewController・Presenterの実装でした。この機能は「設定画面 > 画面A > 画面B」というプッシュ遷移での導線が用意されていて、新卒2人には画面A・画面Bそれぞれを並行して担当してもらいました。 この時点では、1機能にまつわる全てのロジックの実装を丸投げせず、プレゼンテーションロジックについての実装に範囲を限定して作業を進めてもらいました。具体的には、本来UseCaseから取得するプロパティ・データをPresenterの内部で静的に保持させた状態で、View・Presenterの2つの実装に集中してもらうという形式です。 作業を進めてもらうときにメンター側から、 「Presenterがどこからかデータを取得した前提でそのデータをView上で表示するようにイメージして、View側の実装を進めてみてね」 「設定値を変更するUIイベントを検知して、Presenterに通知して、Presenterの内部で自身が持つプロパティを書き換えるところまで実装してみてね。実際はUseCaseのメソッドに値を渡して、メソッドを通して値の更新をするんだけどね。」 と適宜フォローして、全体処理の完成形をイメージしてもらいながら進めていきました。ある程度実装が形になった段階で、通常の開発フロー同様にプルリクを出してもらい、コードレビューを経てマージされる流れでコミットしてもらいました。 初手でPresentation層のみの実装に集中してもらうことで、自分たちの実装範囲外の要因で作業が止まってしまうということを最小限に抑え、開発とOJTを並行させることができました。また、手元でデバッグビルドして直感的にできた/できないがわかりやすいView側の実装に範囲を限定することで、実装がうまくいかなかった時に自分たちで調べて、実装を試してみて解決するというサイクルを自律的に回しやすくなったというというメリットもありました。 そして、2人が担当するそれぞれの画面が両方ともができたタイミングで画面A > 画面Bの遷移処理をモブプロ形式で実装してもらい、本来の遷移導線を完成させるドッキング作業までを体験してもらいました。初日の解説会だけではRouterとDI(依存性注入)の概念を掴み取るのは難しかったようですが、自分たちがここまで実装してきたコードに追加する形でRouterやDIの処理について実装を進めていくことで、より理解が深まった体験ができたようです。 Domain層・DataStore層でのコミットにトライしてもらう プレゼンテーションロジックについての実装を体験してもらった次に、Domain層・DataStore層で実装するミッションにトライしてもらいました。ここでは、 画面上でユーザーが設定した設定値をUserDefaultsに保存・読み取りができるようにするI/Fを提供するDataStoreの作成 楽曲をダウンロードする際の音質(128kbps/320kbps)の設定値を読み取り・保存するロジックを持つUseCaseの作成 の2つのタスクにトライしてもらいました。 このタスクでは、処理のフローを意識してもらうことに集中してもらうため、フローで受け渡す値の型がシンプルなUserDefaultsにまつわるDataStoreを題材として選択しました。また、最初にTyporaで使えるSequence記法でフロー図を書いてもらってから実装を進めてもらいました。 (Typoraで使えるSequence記法について詳しくはこちら) このフロー図を書く習慣をつけることで、DataStore・Repository・UseCaseと複数のコンポーネントを横断する複雑な処理フローでも見通しを持って実装を進めてもらえるようになりました。また、Slack上で質問・相談をする際、テキストで全ての内容を説明しなくても図を交えて、コミュニケーションを取ることもできるのだということを認識してもらうことができました。 DataStore・UseCaseを作成してもらったあとは、ダウンロード音質の設定値を使うアプリ内の各画面のPresenterでUseCaseのメソッドで設定値を取得し、後続の処理に使用したり、設定画面上で設定値を変えるUIイベントを受け取った後にUseCaseを介して、設定値を更新(保存)したりする処理の実装まで進めてもらいました。複数のPresenterから共通のUseCaseを参照する実装をしてもらうことで、共通の機能(ここではダウンロード音質設定)に関するビジネスロジックを共通化して実装するという概念・メリットを理解してもらうことができたと思います。 ここまでの流れで、全てのレイヤーでのタスクを一通り体験することができたので、これ以降の期間は開発の状況に応じて発生した細かめのタスクを都度こなしてもらいました。この際、〇〇の実装について片方しか経験できないという状況を避けるように配慮しながら、タスクを振っていきました。 メンターとして気をつけたこと この方式でOJTを進めるのにあたって、課題設定以外にもメンターとして以下の点を意識しました。 Twitter感覚でSlack上で報告・相談ができる雰囲気を作る 質問したいと思ったタイミングで遠慮なく質問してもらえるように、いい時も悪い時も逐一、Slack上で分報をあげてもらうように周知しました。綺麗な文章じゃなくてもOK(箇条書きでもOK)、違和感を感じたらアプリ画面のスクショだけ貼ってくれるだけでOK、テキストにすることが難しかったら、すぐにハドルで呼んでね、と「先輩は忙しいから、質問するのはやめよう」と質問しなくなってしまう状況を無くすような雰囲気作りを意識しました。 各タスク着手前に見積もりを事前に宣言してもらってから実装に取り組んでもらう 見積もりが外れた場合にペナルティを与えるなどは一切せず、この機能を実装するならおおよそ◯時間くらいでいけるかな…というあたりをつけてもらう習慣をつけてもらいました。 その場で全て教え切ろうとしない 新卒からの質問に回答できるものは全て回答しますが、完全な回答をするために新しくもう1段階インプットが必要だという質問に対しては、その場で全てを教え切ることを敢えてせずに進みました。その際は「この部分は必ず後でもう一回戻ってくるから、今は〇〇の部分まで理解できていればOK」と伝えて、理解すべき事柄の線引きをこちらから明確に提示して新卒側にも納得してもらうように意識しました。 結果どうだったか この形式で実プロダクトにコミットしてもらうことによって、メンター役の社員からのフォローを受けつつ開発が進められるようになりました。今回のプロダクトではスクラム開発の形式で開発を進めていますが、そのスクラム開発においてスプリントバックログに対応するプルリクエストを出すことができるまでになりました。 良かったところ VIPERのプロジェクトを題材にOJTを進めたことで、前述したようにアプリ内部の設計に則して、段階的に課題の難易度を設定することができました。 メンター側の視点では、 実際のプロダクトのコードで指導することで、新卒たちが触っている処理が「このアプリの〇〇の画面の□□の表示に使われてるんだよね」と必要性や関連性を具体的に説明することができた。 教えるときに「今の時点でどこまで話をするべきか」という難易度の調整がしやすかった。 実プロダクトのコードとOJTのカリキュラムの二重管理がなくなったので、効率よく指導そのものに力を注ぐことができた。 などのメリットがありました。 さらに新卒メンバーからも、 カリキュラムで単元・項目毎に学んでいた内容を実際の使い方を想像しながら試してアウトプットすることができた。 実プロダクトにコミットしてることで成果を出していることが実感できたのでモチベーションになった。 という声も聞かれました。 これ以外にも、今回のOJT期間では週5日のうち1日をドキュメントデーと題して、自分がインプットした内容をまとめでドキュメントとして蓄積していく日として設定しました。ドキュメントデーの中で、学んだ内容を整理できたり自発的にプロジェクト内の他のコードを読んでみたりできたことも理解を深めるために良い作用をもたらしたようです。 改善できるところ 一方で、開発と並行して実施する形式ならではの課題もありました。とりわけ、メンター側が開発側のタスクで精一杯になっているときに、新卒側への指示出しが間に合わず手持ち無沙汰になる期間ができてしまったことは今後解決すべき課題です。これはメンター側の課題ではありますが、リポジトリのissueなどで緊急度が低いリファクタやViewの微調整などを集めたチャレンジ課題集のようなものを事前に用意して、非同期的にメンターと新卒が並行稼働できる仕組みを作る必要がありそうです。 また、リモートワーク中心の勤務体制だったので、オフィスのようにホワイトボードを使っての図解ができず苦労するシーンも多々ありました。特に、リモート環境下でも簡単にフリーハンドで図解できるようなコミュニケーション方法は積極的に模索すべきだと感じました。 今後は 新卒たちのOJTはこれからも年度内続きます。今後は、3つのレイヤーを横断したより複雑で分量の多い実装にトライしてもらう予定です。また、OJTという枠組みの中では与えられた設計を前提として処理を実装していくことを体験してもらいましたが、開発チームのメンバーとしてより深く関わっていくためにアーキテクチャ自体を振り返り、改善していくサイクルも回す必要があります。これは開発キャリア問わず、メンバー全員で対等に議論し、進めていくことが重要です。OJTの枠組みを超えて開発チームのカルチャー的な部分ではありますが、将来的にはこの部分まで各メンバーが考えていけるような開発サイクルを回せるところを目指せればと思います。 おわりに レコチョクでは新卒・中途の採用フェーズを問わず、音楽愛に溢れ、共にものづくりに熱中できる素敵な仲間を募集しています。興味がある方はこちらの採用サイトをご覧ください。 最後まで今年のレコチョク Advent Calendar 2021をお読みいただきありがとうございました。今日はひなくり2021の最終日。自分は現場で今年のライブ納めをしてきます! それでは皆様、メリークリスマス? 参考 iOSアプリ設計パターン入門 VIPER研究読本1 クリーンアーキテクチャ解説編 iOS/Androidアプリへドメイン駆動設計(DDD)を導入してCleanArchitectureを目指す [導入編] まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて この記事はレコチョクのエンジニアブログの記事を転載したものとなります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでFormatStyleを使って色々なデータをTextで表示

概要 SwiftUIで文字を表示するTextでは文字列を表示できますが、色々なデータも変換を通して表示できます。 以前からFormatterを使った表示がサポートされていましたが、iOS 15から、FormatStyleというものが加わり、Formatterを作らずとも色々なデータをそのままTextで表示できるようになりました。 表示方法 日付 (Date) inputにDate、formatにDate.FormatStyleを指定します。 struct SwiftUIView: View { let date: Date = Date() var body: some View { Text(date, format: Date.FormatStyle.dateTime) .font(.largeTitle) } } 対応するFormat Date.ComponentsFormatStyle Date.FormatStyle Date.ISO8601FormatStyle Date.IntervalFormatStyle Date.RelativeFormatStyle Date.VerbatimFormatStyle バイトカウント inputにInt64、formatにByteCountFormatStyleを指定します。 struct SwiftUIView: View { let value: Int64 = 100000000 var body: some View { Text(value, format: ByteCountFormatStyle.byteCount(style: .binary)) .font(.largeTitle) } } 対応するFormat ByteCountFormatStyle ByteCountFormatStyle.Attributed 数値(Decimal) inputにDecimal、formatにDecimal.FormatStyleを指定します。 struct SwiftUIView: View { let value: Decimal = 100000000 var body: some View { Text(value, format: Decimal.FormatStyle.Currency.currency(code: "JPY")) .font(.largeTitle) } } 対応するFormat Decimal.FormatStyle Decimal.FormatStyle.Attributed Decimal.FormatStyle.Currency Decimal.FormatStyle.Percent FloatingPointFormatStyle inputにFloatなどのBinaryFloatingPoint、formatにFloatingPointFormatStyleを指定します。 struct SwiftUIView: View { let value: Float = 0.01 var body: some View { Text(value, format: FloatingPointFormatStyle.Currency.currency(code: "USD")) .font(.largeTitle) } } 対応するformat FloatingPointFormatStyle FloatingPointFormatStyle.Attributed FloatingPointFormatStyle.Currency FloatingPointFormatStyle.Percent Measurement inputにMeasurement<UnitTemperature>、formatにMeasurement.FormatStyleを指定します。 現状は温度にしか対応していません。 コードは摂氏を華氏で表示しています。 struct SwiftUIView: View { let value: Measurement<UnitTemperature> = .init(value: 36.5, unit: .celsius) var body: some View { Text(value, format: Measurement.FormatStyle.measurement(width: .abbreviated)) .font(.largeTitle) } } 対応するformat Measurement.FormatStyle IntegerFormatStyle inputにInt、formatにIntegerFormatStyleを指定します。 struct SwiftUIView: View { let value: Int = 10000 var body: some View { Text(value, format: IntegerFormatStyle.Currency.currency(code: "JPY")) .font(.largeTitle) } } 対応するformat IntegerFormatStyle IntegerFormatStyle.Attributed IntegerFormatStyle.Currency IntegerFormatStyle.Percent ListFormatStyle inputにStringの配列、formatにListFormatStyleを指定します。 struct SwiftUIView: View { let value = ["a","b","c"] var body: some View { Text(value, format: ListFormatStyle.list(type: .and)) .font(.largeTitle) } } 対応するformat ListFormatStyle PersonNameComponents.FormatStyle inputにPersonNameComponentsの配列、formatにPersonNameComponents.FormatStyleを指定します。 struct SwiftUIView: View { let value: PersonNameComponents = PersonNameComponents(namePrefix: "Esq.", givenName: "Thomas", middleName: "Louis", familyName: "Clark", nameSuffix: "Esq.", nickname: "Tom") var body: some View { Text(value, format: PersonNameComponents.FormatStyle.name(style: .abbreviated)) .font(.largeTitle) } } 対応するformat PersonNameComponents.AttributedStyle PersonNameComponents.FormatStyle
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最新の超解像モデルReal ESRGANをiOSでつかう

最新の超解像機械学習モデルがiOSでつかえる 320*320 ↓ 超解像 ↓ 1280*1280 小さなサイズの画像を鮮明に、大きなサイズにしてくれる超解像モデル。 2021年発表のReal ESRGANもiOSで使える。 機械学習モデルを扱うのは難しそう? とはいえ、最新の機械学習モデルをつかうのはたくさん手順が必要なんじゃないの? 変換済みのCoreML Modelをつかえばかんたん Real ESRGANも変換済みのものがあります。これをつかえばすぐにiOSで超解像ができます。 かんたん手順 CoreML-ModelsからReal-ESRGANのCoreMLモデルをダウンロードします。 プロジェクトにドラッグします。 Visionで実行します。 guard let model = try? VNCoreMLModel(for: real_esrgan(configuration: MLModelConfiguration()).model) else {fatalError("model initialization failed")} let coreMLRequest = VNCoreMLRequest(model: model) let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) try? handler.perform([coreMLRequest]) guard let result:CVPixelBuffer = coreMLRequest.results?.first as? VNPixelBufferObservation else { return } これで1280*1280のサイズに超解像された画像が取得できます。 超解像をプレビューするだけなら、ダウンロードしたmlmodelファイルを開いてお手持ちの画像をドロップすると、 プレビューができます。 ? フリーランスエンジニアです。 お仕事のご相談こちらまで 簡単な開発内容をお書き添えの上、お気軽にご連絡ください。 rockyshikoku@gmail.com Core MLやARKitを使ったアプリを作っています。 機械学習/AR関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む