20220201のSwiftに関する記事は5件です。

【備考録】今更ながらSwift Combineを入門したので簡単なまとめ

この記事でわかること Combineを利用する上で理解が必須になってくる Publisher Subscriber Subject Operator の関係性をシンプルなサンプルコードを使って説明していきます。 Combineとは iOS13から追加されたApple純正のFrameworkです。 Swiftのみで利用可能で、Objective-Cでは利用できないようです。 Combineの特徴を簡単に言葉にしてみると、 イベントを発行する側と受け取る側に分かれて、あるイベントが発行されたら、それを受け取った側の処理が走ることを容易にしたフレームワーク という感じで捉えています。 公式ドキュメントを呼んでみるとこんな説明が、 By adopting Combine, you’ll make your code easier to read and maintain, by centralizing your event-processing code and eliminating troublesome techniques like nested closures and convention-based callbacks. 要するに、Swiftの特徴である、クロージャを使ったコールバックでコードの可読性が下がったり、処理が難しくなってしまうことを防ぎ、コードのメンテナンス性の向上が期待できる。 という感じです。個人的にクロージャのコールバック地獄のコードは辛いのでこれは嬉しいです。 個人的なメモ 今回、Combineのサンプルを漁っている時に感じたことは、 「これって、RxSwiftにめちゃくちゃ似てるじゃないですか。」 MVVMアーキテクチャの開発は、RxSwift一択だと思っていましたが、純正って響きが良きです。 あと、iOS13〜ということで、SwiftUIとの相性が良さそうなのも?‍♂️ 2021年にiOS15がリリースされたので、これから新規開発するアプリは、iOS13-15をサポートすればいいと考えると、そろそろSwiftUIが本格的に導入され、それに伴ってCombineも来るのか!?って思っちゃいました。 Combineの主な登場人物 名前 役割 Publisher 発行者イベントを発行する側の人間 Subscriber 購読者イベントを受け取って処理をする側の人間 Operator 再発行者流れてきたイベントを加工する側の人間 この人達の関係性のイメージはこんな感じですかね。 PublisherとSubscriberについて Combineを利用する上で、極めてシンプルで簡単なサンプルを書いてみました。 import UIKit import Combine // 発行者を生成する let publisher = ["スパイダーマン", "タイタニック", "ワイルド・スピード"].publisher // 購読登録を行う publisher.sink { string in print("\(string)を鑑賞します") } // 出力 スパイダーマンを鑑賞します タイタニックを鑑賞します ワイルド・スピードを鑑賞します ここで、AmazonPrimeやNetflixといった、各種配信サブスクを想像してください。 配列の中身は、「発行者」の持っている「コンテンツ」であり、 このコードの関係性が、「発行者」->「購読者」の関係になっていることがなんとなくわかると思います。 sinkについて ここで、sinkというメソッドが登場しましたが、こちらについては、ドキュメントに Attaches a subscriber with closure-based behavior to a publisher that never fails. と書かれていますが、 クロージャ処理をするところが「Subscriber(購読者)」の振る舞いとなり、それを定義済の「Publisher(発行者)」と紐付ける。 という解釈で良いと思います。 Subjectについて 先程のコードは、Subjectを使わないで実装しましたが、次は、Subjectを使っています。 SubjectはPublisherを継承したプロトコルです。 Subjectを使うことで、外部から値を受け取ってそれを通知することができるようになります。 とりあえず、サンプルコードを見てみることにします。 let subject = PassthroughSubject<String, Never>() subject.send("アイアンマン") // 購読登録 subject.sink { completion in print("complete") } receiveValue: { string in print("\(string)") } subject.send("アベンジャーズ") subject.send(completion: .finished) subject.send("エターナルズ") // 出力 アベンジャーズを鑑賞します 1番目のサンプルとは、Subscriberが値を受け取る方法が異なるのがわかると思います。 さっきは最初から定義された値がSubscriberに渡されましたが、今回は、外部から値を受け取っているのがわかります。 また、購読登録前、completionを投げた後は、Subjectに対してsend()しても、購読者が持っている振る舞いは実行されないことがわかります。 Operatorについて これまでのサンプルコードは、Operatorをあえて登場させませんでしたので、今回はOperetorを登場させたサンプルコードです。 Operatorについては、冒頭でも説明しましたが、 Publisherによって発行された値を、Subscriberが購読する前に、値をキャッチして変換することができます。 今回は、map(_:)というOperatorを例にしたサンプルです。 let publisher = [1,2,3].publisher publisher.sink { value in // valueはint型で取得される print("アベンジャーズ\(value)を鑑賞します。") } //アベンジャーズ1を鑑賞します。 //アベンジャーズ2を鑑賞します。 //アベンジャーズ3を鑑賞します。 let formatter = NumberFormatter() formatter.numberStyle = .spellOut publisher.map { value in // 値をString型へ変更 formatter.string(from: NSNumber(integerLiteral: value)) ?? "" }.sink { string in print("アベンジャーズ\(string)を鑑賞します。") } //アベンジャーズoneを鑑賞します。 //アベンジャーズtwoを鑑賞します。 //アベンジャーズthreeを鑑賞します。 map(_:)はシンプルなので、ほぼ説明不要かと思いますが、このOperatorを利用することで、Subscriberに値が渡る時は、String型へ変換されて渡されていることがわかります。 おわりに 今回は以上となります。 とりあえず、Operatorの種類が多いのと、サンプルアプリ作ってみないと実感わかない。 ってことで、まだまだ学習が必要ですね。 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Combine入門レベルのまとめ

この記事でわかること Combineを利用する上で理解が必須になってくる Publisher Subscriber Subject Operator の関係性をシンプルなサンプルコードを使って説明していきます。 Combineとは iOS13から追加されたApple純正のFrameworkです。 Swiftのみで利用可能で、Objective-Cでは利用できないようです。 Combineの特徴を簡単に言葉にしてみると、 イベントを発行する側と受け取る側に分かれて、あるイベントが発行されたら、それを受け取った側の処理が走ることを容易にしたフレームワーク という感じで捉えています。 公式ドキュメントを呼んでみるとこんな説明が、 By adopting Combine, you’ll make your code easier to read and maintain, by centralizing your event-processing code and eliminating troublesome techniques like nested closures and convention-based callbacks. 要するに、Swiftの特徴である、クロージャを使ったコールバックでコードの可読性が下がったり、処理が難しくなってしまうことを防ぎ、コードのメンテナンス性の向上が期待できる。 という感じです。個人的にクロージャのコールバック地獄のコードは辛いのでこれは嬉しいです。 個人的なメモ 今回、Combineのサンプルを漁っている時に感じたことは、 「これって、RxSwiftにめちゃくちゃ似てるじゃないですか。」 MVVMアーキテクチャの開発は、RxSwift一択だと思っていましたが、純正って響きが良きです。 あと、iOS13〜ということで、SwiftUIとの相性が良さそうなのも?‍♂️ 2021年にiOS15がリリースされたので、これから新規開発するアプリは、iOS13-15をサポートすればいいと考えると、そろそろSwiftUIが本格的に導入され、それに伴ってCombineも来るのか!?って思っちゃいました。 Combineの主な登場人物 名前 役割 Publisher 発行者イベントを発行する側の人間 Subscriber 購読者イベントを受け取って処理をする側の人間 Operator 再発行者流れてきたイベントを加工する側の人間 この人達の関係性のイメージはこんな感じですかね。 PublisherとSubscriberについて Combineを利用する上で、極めてシンプルで簡単なサンプルを書いてみました。 import UIKit import Combine // 発行者を生成する let publisher = ["スパイダーマン", "タイタニック", "ワイルド・スピード"].publisher // 購読登録を行う publisher.sink { string in print("\(string)を鑑賞します") } // 出力 スパイダーマンを鑑賞します タイタニックを鑑賞します ワイルド・スピードを鑑賞します ここで、AmazonPrimeやNetflixといった、各種配信サブスクを想像してください。 配列の中身は、「発行者」の持っている「コンテンツ」であり、 このコードの関係性が、「発行者」->「購読者」の関係になっていることがなんとなくわかると思います。 sinkについて ここで、sinkというメソッドが登場しましたが、こちらについては、ドキュメントに Attaches a subscriber with closure-based behavior to a publisher that never fails. と書かれていますが、 クロージャ処理をするところが「Subscriber(購読者)」の振る舞いとなり、それを定義済の「Publisher(発行者)」と紐付ける。 という解釈で良いと思います。 Subjectについて 先程のコードは、Subjectを使わないで実装しましたが、次は、Subjectを使っています。 SubjectはPublisherを継承したプロトコルです。 Subjectを使うことで、外部から値を受け取ってそれを通知することができるようになります。 とりあえず、サンプルコードを見てみることにします。 let subject = PassthroughSubject<String, Never>() subject.send("アイアンマン") // 購読登録 subject.sink { completion in print("complete") } receiveValue: { string in print("\(string)") } subject.send("アベンジャーズ") subject.send(completion: .finished) subject.send("エターナルズ") // 出力 アベンジャーズを鑑賞します 1番目のサンプルとは、Subscriberが値を受け取る方法が異なるのがわかると思います。 さっきは最初から定義された値がSubscriberに渡されましたが、今回は、外部から値を受け取っているのがわかります。 また、購読登録前、completionを投げた後は、Subjectに対してsend()しても、購読者が持っている振る舞いは実行されないことがわかります。 Operatorについて これまでのサンプルコードは、Operatorをあえて登場させませんでしたので、今回はOperetorを登場させたサンプルコードです。 Operatorについては、冒頭でも説明しましたが、 Publisherによって発行された値を、Subscriberが購読する前に、値をキャッチして変換することができます。 今回は、map(_:)というOperatorを例にしたサンプルです。 let publisher = [1,2,3].publisher publisher.sink { value in // valueはint型で取得される print("アベンジャーズ\(value)を鑑賞します。") } //アベンジャーズ1を鑑賞します。 //アベンジャーズ2を鑑賞します。 //アベンジャーズ3を鑑賞します。 let formatter = NumberFormatter() formatter.numberStyle = .spellOut publisher.map { value in // 値をString型へ変更 formatter.string(from: NSNumber(integerLiteral: value)) ?? "" }.sink { string in print("アベンジャーズ\(string)を鑑賞します。") } //アベンジャーズoneを鑑賞します。 //アベンジャーズtwoを鑑賞します。 //アベンジャーズthreeを鑑賞します。 map(_:)はシンプルなので、ほぼ説明不要かと思いますが、このOperatorを利用することで、Subscriberに値が渡る時は、String型へ変換されて渡されていることがわかります。 おわりに 今回は以上となります。 とりあえず、Operatorの種類が多いのと、サンプルアプリ作ってみないと実感わかない。 ってことで、まだまだ学習が必要ですね。 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カメラロールの写真をUIImageで上書き保存するミニマムなサンプル

ライブラリの写真をPHAssetで取得して、UIImageで更新保存するサンプルです。 ググっても意外と出てこない模様なので。 ちゃんと読もう Apple 公式のサンプル 環境 Xcode 13.0 ミニマムなサンプルコード import UIKit import Photos private lazy var formatIdentifier = Bundle.main.bundleIdentifier! private let formatVersion = "1.0" private func update(asset: PHAsset, toPhoto: UIImage, completionHandler: @escaping ((Bool, Error?) -> Void)) { let options = PHContentEditingInputRequestOptions() options.canHandleAdjustmentData = { _ in return true } asset.requestContentEditingInput(with: options) { input, info in guard let input = input else { fatalError("Can't get the content-editing input: \(info)") } DispatchQueue.global(qos: .userInitiated).async { let output = PHContentEditingOutput(contentEditingInput: input) let adjustmentData = PHAdjustmentData(formatIdentifier:formatIdentifier, formatVersion:formatVersion, data: "my app".data(using: .utf8)!) output.adjustmentData = adjustmentData let jpg = toPhoto.jpegData(compressionQuality: 0.9) do { try jpg?.write(to: output.renderedContentURL) } catch let error { fatalError("Can't save image to PHContentEditingOutput.renderedContentURL : \(error).") } PHPhotoLibrary.shared().performChanges { let request = PHAssetChangeRequest(for: asset) request.contentEditingOutput = output } completionHandler: { success, error in completionHandler(success, error) } } } } なんとかしたい renderedContentURLに吐き出せるのがなぜかjpg一択?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CoreML Modelに柔軟な入力形状を設定する

色々な入力形状に対応できるCoreMLモデルに変換する方法です 固定ではなくさまざまなサイズの入力を使いたい デフォルト変換では、CoreMLモデルは入力サイズを固定します。 しかし、実際のユースケースでは、色々なサイズの入力を使いたい場合があります。 柔軟な入力を使う方法 CoreMLToolsは、柔軟な入力の設定が可能です。 変換時に設定する方法 import coremltools as ct input_shape = ct.Shape(shape=(1,3, ct.RangeDim(512, 1024), ct.RangeDim(512, 1024))) model_input = ct.ImageType(shape=input_shape) mlmodel = ct.convert(model, inputs=[model_input]) これで、入力画像の縦と横で512~1024の範囲を受け入れられるようになりました。 ct.RangeDim()とすると、無制限の入力形状を受け入れるようになります。 範囲指定ではなく、いくつかのデフォルト値を設定して、メモリを事前に割り当てることで、範囲指定よりも高速になります。 import coremltools as ct shapes = [(1,3, 512, 512),(1,3, 768, 768),(1,3, 1024, 1024)] input_shape = ct.EnumeratedShapes(shapes=shapes, default=(1,3, 512, 512)) model_input = ct.TensorType(shape=input_shape) mlmodel = ct.convert(model, inputs=[model_input]) これで、3つの選択肢で入力形状を指定できます。 変換した後で設定する方法 from coremltools.models.neural_network import flexible_shape_utils spec = coremltools.utils.load_spec('mymodel.mlmodel') input_name = spec.description.input[0].name img_size_ranges = flexible_shape_utils.NeuralNetworkImageSizeRange() img_size_ranges.add_height_range((256, 4096)) img_size_ranges.add_width_range((256, 4096)) flexible_shape_utils.update_image_size_range(spec, feature_name=input_name, size_range=img_size_ranges) from coremltools.models.utils import save_spec save_spec(spec, 'updatedMyModel.mlmodel') ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLやARKitを使ったアプリを作っています。 機械学習/AR関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WidgetKitで天気予報アプリ作ってみた〜位置情報取得&保存編〜

投稿の経緯 前回投稿したWidgetKitで天気予報アプリ作ってみた〜天気情報取得編〜の続編です。 今回は位置情報を取得して保存するところまでを記事にしようと思います。 前回の記事を見てない方は先に↓こちら↓を確認してください。 開発環境 Swift 5.5 Xcode 13.2.1 サンプルプロジェクト GitHubにPushしています。気になる方はご覧ください。 https://github.com/ken-sasaki-222/WeatherWidget 位置情報取得 今回はウィジェットの開発がメインなので、位置情報の取得はあまり作り込まずに進めたいと思います。 CoreLocationを追加 Targets > Build Phases > Link Binaries with LibrariesにCoreLocationを追加。 Info.plistへ追加 Praivacy - Location When In Use Usage Descriptionを追加してアラートに表示するテキストを設定します。 AppDelegateを追加 WeatherWidgetApp.swift import SwiftUI @main struct WeatherWidgetApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } } AppDelegate.swift import SwiftUI class AppDelegate: UIResponder, UIApplicationDelegate { private let locationManagerHelper = LocationManagerHelper() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { locationManagerHelper.callLocationManager() return true } } @UIApplicationDelegateAdaptorを使ってAppDelegateを用意します。 今回はアプリ起動時に位置情報を選択してない場合、位置情報許諾アラートを表示するぐらいの実装でいいので、didFinishLaunchingWithOptionsの中で位置情報を扱うHelperクラスを呼びます。 位置情報を扱うHelperクラスを追加 LocationManagerHelper.swift import CoreLocation class LocationManagerHelper: NSObject, CLLocationManagerDelegate { private var locationManager: CLLocationManager override init() { self.locationManager = CLLocationManager() super.init() self.locationManager.delegate = self } func callLocationManager() { locationManager.requestWhenInUseAuthorization() if CLLocationManager.locationServicesEnabled() { locationManager.pausesLocationUpdatesAutomatically = false locationManager.desiredAccuracy = kCLLocationAccuracyKilometer locationManager.startUpdatingLocation() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let newLocation = locations.last else { return } let location = CLLocationCoordinate2D( latitude: newLocation.coordinate.latitude, longitude: newLocation.coordinate.longitude ) print("緯度:", location.latitude, "経度:", location.longitude) locationManager.stopUpdatingLocation() } } 位置情報を取得する処理です。先に記載したように、今回はLocationManagerに関する処理をあまり作り込んでいませんので、Helperクラスを用意してその中に位置情報を扱う処理を逃しています。 続いて、取得した位置情報をUserDefaultsに保存していきます。 位置情報保存 WidgetKitで天気予報アプリ作ってみた〜天気情報取得編〜で書いているように今回はアーキテクチャにリポジトリパターンを採用しています。UserDefaultsとのやりとりはRepositoryを経由してDataStoreに任せようと思います。 DataStore UserDefaultsDataStore.swift import Foundation final class UserDefaultsDataStore { private enum DefaultsKey: String { case lat case lng } private var defaults: UserDefaults { UserDefaults.standard } var lat: Double { get { defaults.double(forKey: DefaultsKey.lat.rawValue) } set(newValue) { defaults.set(newValue, forKey: DefaultsKey.lat.rawValue) } } var lng: Double { get { defaults.double(forKey: DefaultsKey.lng.rawValue) } set(newValue) { defaults.set(newValue, forKey: DefaultsKey.lng.rawValue) } } } UserDefaultsとやりとりをするDataStoreです。緯度経度の保存と取り出しを担当し、取り出した値をRepositoryへ返します。 Repository UserRepositoryInterface.swift import Foundation protocol UserRepositoryInterface { var lat: Double { get set } var lng: Double { get set } } UserRepository.swift import Foundation class UserRepository: UserRepositoryInterface { private let userDefaultsDataStore = UserDefaultsDataStore() var lat: Double { get { userDefaultsDataStore.lat } set(newValue) { userDefaultsDataStore.lat = newValue } } var lng: Double { get { userDefaultsDataStore.lng } set(newValue) { userDefaultsDataStore.lng = newValue } } } UserDefaultsDataStoreとやりとりをするRepositoryです。緯度経度の取得と保存は必ずこのRepositoryを経由します。 Helperクラスを書き換える RepositoryRocator.swift import Foundation class RepositoryRocator { static func getWeatherRepository() -> WeatherRepositoryInterface { WeatherRepository() } static func getUserRepository() -> UserRepositoryInterface { UserRepository() } } LocationManagerHelper.swift import CoreLocation class LocationManagerHelper: NSObject, CLLocationManagerDelegate { private var userRepository: UserRepositoryInterface private var locationManager: CLLocationManager init(userRepository: UserRepositoryInterface) { self.userRepository = userRepository self.locationManager = CLLocationManager() super.init() self.locationManager.delegate = self } override convenience init() { self.init(userRepository: RepositoryRocator.getUserRepository()) } func callLocationManager() { locationManager.requestWhenInUseAuthorization() if CLLocationManager.locationServicesEnabled() { locationManager.pausesLocationUpdatesAutomatically = false locationManager.desiredAccuracy = kCLLocationAccuracyKilometer locationManager.startUpdatingLocation() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let newLocation = locations.last else { return } let location = CLLocationCoordinate2D( latitude: newLocation.coordinate.latitude, longitude: newLocation.coordinate.longitude ) print("緯度:", location.latitude, "経度:", location.longitude) userRepository.lat = location.latitude userRepository.lng = location.longitude locationManager.stopUpdatingLocation() } } RepositoryLocatorを経由してUserRepositoryを取得しています。位置情報が更新されたタイミングで緯度経度を保存するように書き換えました。 保存した位置情報でリクエストを送る WeatherViewModel.swift import Foundation class WeatherViewModel: NSObject { private let weatherRepository: WeatherRepositoryInterface private let userRepository: UserRepositoryInterface init(weatherRepository: WeatherRepositoryInterface, userRepository: UserRepositoryInterface) { self.weatherRepository = weatherRepository self.userRepository = userRepository super.init() } override convenience init() { self.init(weatherRepository: RepositoryRocator.getWeatherRepository(), userRepository: RepositoryRocator.getUserRepository()) } func createRequestModel() -> WeatherRequestModel { let requestModel = WeatherRequestModel( lat: userRepository.lat, lng: userRepository.lng ) return requestModel } func fetchWeathers() async { do { let response = try await weatherRepository.fetchWeathers(requestModel: createRequestModel()) print("Success fetch weathers:", response.hourly) } catch { print("Error fetch weathers:", error) } } } WidgetKitで天気予報アプリ作ってみた〜天気情報取得編〜の時点では緯度経度を直接指定していましたが、保存した緯度経度を使ってWeatherRequestModelを作るように書き換えました。これで現在地から取得した位置情報を使ってリクエストを送れるようになりました。 おわりに 今回はWidgetKitで天気予報アプリ作ってみた〜位置情報取得&保存編〜について書きました。 次はいよいよウィジェットの開発です! ご覧いただきありがとうございました。 こうしたほうがいいや、ここはちょっと違うなど気になる箇所があった場合、ご教示いただけると幸いです。 お知らせ 現在副業でiOSアプリ開発案件を募集しています。 Twitter DMでご依頼お待ちしております! ↓活動リンクはこちら↓ https://linktr.ee/sasaki.ken
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む