- 投稿日:2019-12-18T23:37:26+09:00
RxSwiftのオペレーターの勉強 その2(結合編)
Atrae Advent Calendar 2019 の18日目を担当するアガツマです。
普段は、ビジネス版マッチングアプリ yenta のiOS版を開発をしています。前回に続いて、RxSwiftのオペレーターについて書きます。
今回は、オペレーターの中でも複数のobservableを合成(結合)するようなものについてまとめました。
RxSwiftを勉強する際の参考になったら嬉しいです。※RxSwiftのオペレーターとは、Observableに用意されているイベントを加工する用のメソッドのことを指します
複数のobservableを合成するオペレーター
combineLatest
処理
2つのObsrvableのを監視してどちらか1つのObsrvableが送信される時、それぞれのObservableから送信される最新の値をまとめて送信する。用途
複数の変数を監視する必要がある場合(複数入力のバリデーションなど)
複数の値を組み合わせる処理コード
let i = PublishSubject<Int>() let s = PublishSubject<String>() _ = Observable.combineLatest(i, s) { "\($0) + \($1)" } .subscribe { print("onNext: ", $0) } i.onNext(1) s.onNext("A") i.onNext(2) s.onNext("B") s.onNext("C") i.onNext(3)出力結果
onNext: next(1 + A) onNext: next(2 + A) onNext: next(2 + B) onNext: next(2 + C) onNext: next(3 + C)withLatestFrom
処理
あるObservableがイベントを送信した際に、もう一方の Observabe の最新のイベントを合成する。用途
1つのアクションが起きた際に、他の値に対して何かしら処理をする場合。
(ボタンが押された時などに、他の要素がどのような値になっているか知りたいときなど)コード
let i = PublishSubject<Int>() let s = PublishSubject<String>() i.withLatestFrom(s) .subscribe({ string in print("onNext: ", string) }) .disposed(by: disposeBag) i.onNext(1) s.onNext("A") i.onNext(2) s.onNext("B") s.onNext("C") i.onNext(3)出力結果
onNext: next(A) onNext: next(C)zip
処理
複数の Observable のイベントを1つづつ順番に合成する。用途
並列処理をしたい場合(API通信で複数のレスポンスを待ってから処理を進めたいときなど)コード
let i = Observable.of(1,2,3,4,5) let s = Observable.of("A","B","C") _ = Observable.zip(i, s) { "\($0) + \($1)" } .subscribe { print("onNext: ", $0) }出力結果
onNext: next(1 + A) onNext: next(2 + B) onNext: next(3 + C) onNext: completedおわりに
今回は、RxSwiftでのObservableの合成についてまとめてみました。
このような、オペレーターを組み合わせることで、様々な処理を実現することができるのだなと思いました。
今後は、RxSwiftを用いたアーキテクチャーなどの理解を深めていきたいと思います!参考
- 投稿日:2019-12-18T20:47:46+09:00
AVCaptureMultiCamSessionを使用してCameraOSS作成
こんにちはフリーランスの永田です。年始からSwiftUI案件で稼働します。案件獲得のコツは技術を磨くです。
AVCaptureMultiCamSession
MovieSession
上記の最新APIは両画面同時撮影する機能を持ちます。このAPIを使用して11月から作成して、OSS化しました。
https://github.com/daisukenagata/BothSidesCamera
宜しければスターお願い致します。
機能
- 前面背面同時撮影
- インナー画面の位置の可変。画面域の可変
- 2画面同じ比率での撮影
- 縦画面、横画面の対応(右側、左側対応)
環境
- Xcode11.3
- Swift5.0
- iOS13~
動作の一例
全部の向きに対応しました。 pic.twitter.com/sZh9StPknR
— DaisukeNagata (@dbank0208) December 17, 2019全部の向きに対応しました。 pic.twitter.com/5XyvygpJZi
— DaisukeNagata (@dbank0208) December 17, 2019発見から、修正まで1時間ぐらい。
— DaisukeNagata (@dbank0208) December 14, 2019
パープルボタンを押した時に2画面表示の場合、撮影画面が2ミリぐらいズレる動作を修正しました。 pic.twitter.com/eqFSi6eZC1iPhone X系で、プレビュー画面と録画画面を合わせました。 pic.twitter.com/I8a12ZxHIz
— DaisukeNagata (@dbank0208) December 9, 2019ポイント
全てのOSS機能を言語化するとかなりの文字量になってしまい、何を見ているのか、プログラムのどこにいるのかなどを保ちながら理解するのが、困難になる場合があるので、ポイントを集約します。
プレビュー画面は自作で合わせる。
BothSidesMixer.swift
とBothSidesMixer.metal
でプレビュー画面を設定しています。
BothSidesMixer.swift
のこの部分です。
プログラム内のコメント、Fixed with memory measuresの部分を同じメソッドでやると高速ループの中に入り、高メモリーがさらに高くなるので、
フラグが変更する度に呼び出すようにして、メモリーが上昇するのを極力制御しています。getMtlSize メソッド
CVPixelBuffer
を新たに作成する機能です。mix メソッド
CVPixelBufferPoolCreatePixelBuffer
はCVPixelBuffer
を作成しています。
fullScreenTexture
は全画面
pipTexture
はインナー画面
outputTexture
はMTLTexture
でfullScreenTexture
とpipTexture
を表示するTextureです。
BothSidesMixer.metal
で書き込み処理をしています。
inputImage
は2画面同時比率を実現しています。
座標を合わせる技はこちら
CGAffineTransform
の合わせ技です。let inputImage = CIImage(cvImageBuffer: fullScreenPixelBuffer, options: nil).transformed(by: CGAffineTransform(scaleX: 0.5, y: 0.5).translatedBy(x: CGFloat(fullScreenTexture.width/2), y: 0))
fullScreenTexture
にMTLTexture
を作成しています。
makeTextureFromCVPixelBuffer
はMTLTexture
を作成するロジックを用意しています。guard let newfullScreenTexture = makeTextureFromCVPixelBuffer(pixelBuffer: pixelBuffer) else { print("AVCaptureMultiCamViewModel_mix") return nil }
var parameters = MixerParameters(pipPosition: pipPosition, pipSize: pipSize)
でBothSidesMixer.metal
に渡すパラメータを用意します。let pipPosition = SIMD2(Float(pipFrame.origin.x) * Float(fullScreenTexture.width),Float(pipFrame.origin.y) * Float(fullScreenTexture.height)) let pipSize = SIMD2(Float(pipFrame.size.width) * Float(pipTexture.width),Float(pipFrame.size.height) * Float(pipTexture.height)) var parameters = MixerParameters(pipPosition: pipPosition, pipSize: pipSize)
commandQueue
はMetalを使用して、GPUに書き込むプログラムになっています。
commandEncoder.setTexture
とMetalFileのreporterMixer
メソッドを見ると理解しやすいと思います。
引数が連動しています。画面を作成しているメソッドの全体像です。
// Fixed with memory measures func getMtlSize(mtl: MTLTexture, sameRatio: Bool) { if sameRatio == true && pixelBuffer == nil { let options = [ kCVPixelBufferCGImageCompatibilityKey as String: true, kCVPixelBufferCGBitmapContextCompatibilityKey as String: true, kCVPixelBufferIOSurfacePropertiesKey as String: [:] ] as [String : Any] cvReturn = CVPixelBufferCreate(kCFAllocatorDefault, Int(mtl.width), Int(mtl.height), kCVPixelFormatType_32BGRA, options as CFDictionary, &pixelBuffer) } } func mix(fullScreenPixelBuffer: CVPixelBuffer, pipPixelBuffer: CVPixelBuffer, _ sameRatio: Bool) -> CVPixelBuffer? { guard let outputPixelBufferPool = outputPixelBufferPool else { return nil } var newPixelBuffer: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, outputPixelBufferPool, &newPixelBuffer) let outputPixelBuffer = newPixelBuffer let outputTexture = makeTextureFromCVPixelBuffer(pixelBuffer: outputPixelBuffer) guard var fullScreenTexture = makeTextureFromCVPixelBuffer(pixelBuffer: fullScreenPixelBuffer) else { return nil} guard let pipTexture = makeTextureFromCVPixelBuffer(pixelBuffer: pipPixelBuffer) else { return nil} if sameRatio == true { // Fixed with memory measures getMtlSize(mtl: fullScreenTexture,sameRatio: sameRatio) if cvReturn == kCVReturnSuccess { guard let pixelBuffer = pixelBuffer else { print("AVCaptureMultiCamViewModel_mix") return nil } let ciContext = CIContext() let inputImage = CIImage(cvImageBuffer: fullScreenPixelBuffer, options: nil).transformed(by: CGAffineTransform(scaleX: 0.5, y: 0.5).translatedBy(x: CGFloat(fullScreenTexture.width/2), y: 0)) let colorSpace = CGColorSpaceCreateDeviceRGB() ciContext.render(inputImage, to: pixelBuffer, bounds: inputImage.extent, colorSpace: colorSpace) guard let newfullScreenTexture = makeTextureFromCVPixelBuffer(pixelBuffer: pixelBuffer) else { print("AVCaptureMultiCamViewModel_mix") return nil } fullScreenTexture = newfullScreenTexture } } let pipPosition = SIMD2(Float(pipFrame.origin.x) * Float(fullScreenTexture.width), Float(pipFrame.origin.y) * Float(fullScreenTexture.height)) let pipSize = SIMD2(Float(pipFrame.size.width) * Float(pipTexture.width), Float(pipFrame.size.height) * Float(pipTexture.height)) var parameters = MixerParameters(pipPosition: pipPosition, pipSize: pipSize) guard let commandQueue = commandQueue, let commandBuffer = commandQueue.makeCommandBuffer(), let commandEncoder = commandBuffer.makeComputeCommandEncoder(), let computePipelineState = computePipelineState else { print("BothSidesMixer_computePipelineState") if let textureCache = textureCache { CVMetalTextureCacheFlush(textureCache, 0) } return nil } commandEncoder.setComputePipelineState(computePipelineState) commandEncoder.setTexture(fullScreenTexture, index: 0) commandEncoder.setTexture(pipTexture, index: 2) commandEncoder.setTexture(outputTexture, index: 3) commandEncoder.setBytes(UnsafeMutableRawPointer(¶meters), length: MemoryLayout<MixerParameters>.size, index: 0) let width = computePipelineState.threadExecutionWidth let height = computePipelineState.maxTotalThreadsPerThreadgroup / width let threadsPerThreadgroup = MTLSizeMake(width, height, 1) let threadgroupsPerGrid = MTLSize(width: (fullScreenTexture.width + width - 1) / width, height: (fullScreenTexture.height + height - 1) / height, depth: 1) commandEncoder.dispatchThreadgroups(threadgroupsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup) commandEncoder.endEncoding() commandBuffer.commit() commandBuffer.waitUntilCompleted() return outputPixelBuffer }肝心のカメラの合成クラス
主な合成メソッドです。この
configureBackCamera
とconfigureFrontCamera
の2つのメソッドは録画をする上で、必須でどうしてもメモリが高メモリになってしまいます。iPhonePro11で260MB前後です。背面カメラの設定
configureBackCamera
メソッド
前面カメラの設定configureFrontCamera
メソッド
音の設定configureMicrophone
メソッド処理の内容はコード見て理解して下さい。それが一番わかりやすいです。
final class BothSidesMultiCamViewModel: NSObject { var session = AVCaptureMultiCamSession() var aModel : BothSidesMultiCamSessionModel? var backCamera : AVCaptureDevice? var backDeviceInput : AVCaptureDeviceInput? var frontDeviceInput : AVCaptureDeviceInput? private var microphoneDeviceInput : AVCaptureDeviceInput? private let backCameraVideoDataOutput = AVCaptureVideoDataOutput() private let frontCameraVideoDataOutput = AVCaptureVideoDataOutput() private let backMicrophoneAudioDataOutput = AVCaptureAudioDataOutput() private let frontMicrophoneAudioDataOutput = AVCaptureAudioDataOutput() private let dataOutputQueue = DispatchQueue(label: "data output queue") override init() { aModel = BothSidesMultiCamSessionModel() super.init() dataSet() } // Flash func pushFlash() { do { try backCamera?.lockForConfiguration() switch backCamera?.torchMode { case .off: backCamera?.torchMode = AVCaptureDevice.TorchMode.on case .on: backCamera?.torchMode = AVCaptureDevice.TorchMode.off default: break } backCamera?.unlockForConfiguration() } catch { print("not be used") } } func dataSet() { aModel?.dataOutput(backdataOutput: backCameraVideoDataOutput, frontDataOutput: frontCameraVideoDataOutput, backicrophoneDataOutput: backMicrophoneAudioDataOutput, fronticrophoneDataOutput: frontMicrophoneAudioDataOutput) } func screenShot(call: @escaping() -> Void, orientation: UIInterfaceOrientation) { aModel?.screenShot(call: call, orientation: orientation) } func changeDviceType() { guard let backDeviceInput = backDeviceInput else { print("AVCaptureMultiCamViewModel_session") return } session.removeInput(backDeviceInput) session.removeOutput(backCameraVideoDataOutput) backCamera = nil } func configureBackCamera(_ backCameraVideoPreviewLayer: AVCaptureVideoPreviewLayer?,deviceType :AVCaptureDevice.DeviceType) { session.beginConfiguration() defer { session.commitConfiguration() } backCamera = AVCaptureDevice.default(deviceType, for: .video, position: .back) guard let backCamera = backCamera else { print("BothSidesMultiCamViewModel_backCamera") return } // Camera support is limited. if deviceType == .builtInWideAngleCamera { do { try backCamera.lockForConfiguration() backCamera.focusMode = .continuousAutoFocus backCamera.unlockForConfiguration() } catch { print("not be used") } } do { backDeviceInput = try AVCaptureDeviceInput(device: backCamera) guard let backCameraDeviceInput = backDeviceInput, session.canAddInput(backCameraDeviceInput) else { print("AVCaptureMultiCamViewModel_backCameraDeviceInput") return } session.addInputWithNoConnections(backCameraDeviceInput) } catch { return } guard let backCameraDeviceInput = backDeviceInput, let backCameraVideoPort = backCameraDeviceInput.ports(for: .video, sourceDeviceType: backCamera.deviceType, sourceDevicePosition: backCamera.position).first else { print("AVCaptureMultiCamViewModel_backCameraVideoPort") return } guard session.canAddOutput(backCameraVideoDataOutput) else { print("AVCaptureMultiCamViewModel_session.canAddOutput") return } session.addOutputWithNoConnections(backCameraVideoDataOutput) backCameraVideoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)] backCameraVideoDataOutput.setSampleBufferDelegate(aModel, queue: dataOutputQueue) let backCameraVideoDataOutputConnection = AVCaptureConnection(inputPorts: [backCameraVideoPort], output: backCameraVideoDataOutput) guard session.canAddConnection(backCameraVideoDataOutputConnection) else { print("AVCaptureMultiCamViewModel_session.canAddConnection") return } session.addConnection(backCameraVideoDataOutputConnection) backCameraVideoDataOutputConnection.videoOrientation = .portrait guard let backCameraVideoPreviewLayer = backCameraVideoPreviewLayer else { print("AVCaptureMultiCamViewModel_backCameraVideoPreviewLayer") return } let backCameraVideoPreviewLayerConnection = AVCaptureConnection(inputPort: backCameraVideoPort, videoPreviewLayer: backCameraVideoPreviewLayer) guard session.canAddConnection(backCameraVideoPreviewLayerConnection) else { print("AVCaptureMultiCamViewModel_session.canAddConnection") return } session.addConnection(backCameraVideoPreviewLayerConnection) } func configureFrontCamera(_ frontCameraVideoPreviewLayer: AVCaptureVideoPreviewLayer?, deviceType :AVCaptureDevice.DeviceType) { session.beginConfiguration() defer { session.commitConfiguration() } guard let frontCamera = AVCaptureDevice.default(deviceType, for: .video, position: .front) else { print("AVCaptureMultiCamViewModel_frontCamera") return } do { frontDeviceInput = try AVCaptureDeviceInput(device: frontCamera) guard let frontCameraDeviceInput = frontDeviceInput, session.canAddInput(frontCameraDeviceInput) else { print("AVCaptureMultiCamViewModel_frontCameraDeviceInput") return } session.addInputWithNoConnections(frontCameraDeviceInput) } catch { return } guard let frontCameraDeviceInput = frontDeviceInput, let frontCameraVideoPort = frontCameraDeviceInput.ports(for: .video, sourceDeviceType: frontCamera.deviceType, sourceDevicePosition: frontCamera.position).first else { print("AVCaptureMultiCamViewModel_frontCameraVideoPort") return } guard session.canAddOutput(frontCameraVideoDataOutput) else { print("AVCaptureMultiCamViewModel_session.canAddOutput") return } session.addOutputWithNoConnections(frontCameraVideoDataOutput) frontCameraVideoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)] frontCameraVideoDataOutput.setSampleBufferDelegate(aModel, queue: dataOutputQueue) let frontCameraVideoDataOutputConnection = AVCaptureConnection(inputPorts: [frontCameraVideoPort], output: frontCameraVideoDataOutput) guard session.canAddConnection(frontCameraVideoDataOutputConnection) else { print("AVCaptureMultiCamViewModel_session.canAddConnection") return } session.addConnection(frontCameraVideoDataOutputConnection) frontCameraVideoDataOutputConnection.videoOrientation = .portrait frontCameraVideoDataOutputConnection.automaticallyAdjustsVideoMirroring = false frontCameraVideoDataOutputConnection.isVideoMirrored = true guard let frontCameraVideoPreviewLayer = frontCameraVideoPreviewLayer else { print("AVCaptureMultiCamViewModel_frontCameraVideoPreviewLayer") return } let frontCameraVideoPreviewLayerConnection = AVCaptureConnection(inputPort: frontCameraVideoPort, videoPreviewLayer: frontCameraVideoPreviewLayer) guard session.canAddConnection(frontCameraVideoPreviewLayerConnection) else { print("AVCaptureMultiCamViewModel_session.canAddConnection") return } session.addConnection(frontCameraVideoPreviewLayerConnection) frontCameraVideoPreviewLayerConnection.automaticallyAdjustsVideoMirroring = false frontCameraVideoPreviewLayerConnection.isVideoMirrored = true } func configureMicrophone() { session.beginConfiguration() defer { session.commitConfiguration() } guard let microphone = AVCaptureDevice.default(for: .audio) else { print("AVCaptureMultiCamViewModel_microphone") return } do { self.microphoneDeviceInput = try AVCaptureDeviceInput(device: microphone) guard let microphoneDeviceInput = microphoneDeviceInput, session.canAddInput(microphoneDeviceInput) else { print("AVCaptureMultiCamViewModel_microphoneDeviceInput") return } session.addInputWithNoConnections(microphoneDeviceInput) } catch { return } guard let microphoneDeviceInput = microphoneDeviceInput, let backMicrophonePort = microphoneDeviceInput.ports(for: .audio, sourceDeviceType: microphone.deviceType, sourceDevicePosition: .back).first else { print("AVCaptureMultiCamViewModel_microphoneDeviceInput") return } guard let frontMicrophonePort = microphoneDeviceInput.ports(for: .audio, sourceDeviceType: microphone.deviceType, sourceDevicePosition: .front).first else { print("AVCaptureMultiCamViewModel_frontMicrophonePort") return } guard session.canAddOutput(backMicrophoneAudioDataOutput) else { print("AVCaptureMultiCamViewModel_session.canAddOutput") return } session.addOutputWithNoConnections(backMicrophoneAudioDataOutput) backMicrophoneAudioDataOutput.setSampleBufferDelegate(aModel, queue: dataOutputQueue) guard session.canAddOutput(frontMicrophoneAudioDataOutput) else { print("AVCaptureMultiCamViewModel_session.canAddOutput") return } session.addOutputWithNoConnections(frontMicrophoneAudioDataOutput) frontMicrophoneAudioDataOutput.setSampleBufferDelegate(aModel, queue: dataOutputQueue) let backMicrophoneAudioDataOutputConnection = AVCaptureConnection(inputPorts: [backMicrophonePort], output: backMicrophoneAudioDataOutput) guard session.canAddConnection(backMicrophoneAudioDataOutputConnection) else { print("AVCaptureMultiCamViewModel_session.canAddConnection") return } session.addConnection(backMicrophoneAudioDataOutputConnection) let frontMicrophoneAudioDataOutputConnection = AVCaptureConnection(inputPorts: [frontMicrophonePort], output: frontMicrophoneAudioDataOutput) guard session.canAddConnection(frontMicrophoneAudioDataOutputConnection) else { print("AVCaptureMultiCamViewModel_session.canAddConnection") return } session.addConnection(frontMicrophoneAudioDataOutputConnection) } }以上、AVCaptureMultiCamSessionを使用して作成したOSSの紹介をさせていただきました。
来年も皆様、良い一年になりますように。
貴重なお時間、お読み下さいまして、誠にありがとうございます。
- 投稿日:2019-12-18T18:48:38+09:00
【iOS】サポートバージョンを上げた時にやったこと
概要
アプリを長い間開発していると、必ず生じるサポートバージョン問題。
そんなサポートバージョンを上げた時にどこを変更するのか忘れそうなので
備忘録的な意味合いも兼ねて更新したいと思います。iOS Deployment Target
まぁ、これは言わずもがなですね。
ここで内部的には判定されているものと思われます。@available属性
旧バージョンに対応していた場合、新しいOSでしか対応していないAPIを使う際には
@available属性などで条件分けして実装していたと思います。
これが必要なくなります。@available(iOS, [version], *)
[version]の値が今回のサポートバージョンに含まれている場合は
消してしまって問題ありませんので消してしまいましょう!また、この表記がある場合、この[version]に満たないバージョンで
代わりに呼ばれるメソッドがあるはずなので、そちらも消してしまいましょう!Podfile
忘れがちなのが、これではないでしょうか。
(今回この記事を書いたのは、これを忘れてたからです。)platform :ios, "9.0"みたいな表記があると思うので、これを今回サポートするバージョンへと変更して
pod install
を叩きましょう。
多くの場合は、ほぼほぼ変更点はない(checksumのみ)と思います。他に皆様でやったことがあれば教えていただけると嬉しいです!
誰かのお役に立てば。
- 投稿日:2019-12-18T18:09:30+09:00
プロパティをクロージャで初期化する(Initialization Closure)
前回の投稿から長い時を経て、就職してiOSエンジニアとして日々勉強をしています。よろしくお願いします。
今回はタイトルの通り、Initialization Closureについてまとめようと思います。(この記事は就職先の技術ブログの内容を転機したものです)
TL; DR
- Initialization ClosureはStored Propertyの初期化に使われる書き方。
- Computed Propertyではないので、
{}
の中は1度しか呼ばれない。未知との遭遇
現在開発中のアプリでUIViewControllerのライフサイクルとAuto Layoutの反映タイミングに悩まされ、
viewDidLayoutSubview()
で最初の一回だけ処理をしたくなり、この記事にたどり着きました。実際に以下のようなコードを書けば問題は解決したのですが、何がどうなっているのか・なぜ上手くいってるのかがわかりませんでした。
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() _ = initCollectionViewFlowLayout } private lazy var initCollectionViewFlowLayout : Void = { // ここで色々設定する // frame、boundsとか使う }()何がわからんのか
当時わからなかったことをまとめるとこんな感じだったと思います。
- 見た目Computed Propertyと似てるけど何が違うのか
- 何故これで
initCollectionViewFlowLayout
の中身が一度しか呼ばれないのか{}
の後の()
は何者なのか最近ようやくこの謎が解けた(っぽい)ので、これらの点について話をします。
Computed Propertyとの違い
変数名の後に
{ return }
みたいなのがあるのでComputed Propertyと混同してしまったのですが、このinitCollectionViewFlowLayout
はInitialization Closureという書き方でStored Propertyとして定義されているみたいです(=
があるのでそれもそうか...という感じ)。Initialization Closureとは
Stored Propertyの初期化の際に、初期値としてクロージャの実行結果を渡す書き方のことです。
例1 ) 要素数50のフィボナッチ数列 (Stored Property)
let fibonacci: [Int] = { var temporaryArray = [1, 1] let numberOfElement = 50 for _ in 0..<numberOfElement - temporaryArray.count { let nextElement = temporaryArray[temporaryArray.count - 2] + temporaryArray[temporaryArray.count - 1] temporaryArray.append(nextElement) } return temporaryArray }()上の例では、フィボナッチ数列の初期化を行うクロージャの返り値を、定数
fibonacci
に渡しています(return temporaryArray
)。
{}
の中で初期化の処理をクロージャとして定義して、最後に()
をつけることでそのクロージャを即実行して[Int]
の値を取得しているという感じです。
最後の()
を付けないと、fibonacci
にクロージャ(この場合() -> [Int]
のクロージャ)を代入する形になってしまい、コンパイルエラーが起きます。何故処理が一度しか呼ばれないのか
答えはシンプルで、
initCollectionViewFlowLayout
がStored Propertyだからです。
Computed Propertyは値を保持しないので参照するたびに{}
の中の計算が走ります(例2)が、
Stored Propertyの場合は値を保持するので、参照しても{}
内の計算は行われず、初期化の時に{}
内でreturn
された値を使うだけになります(例3)。例2 ) 要素数50のフィボナッチ数列 (Computed Property)
var fibonacci: [Int] { var temporaryArray = [1, 1] let numberOfElement = 50 for _ in 0..<numberOfElement - temporaryArray.count { let nextElement = temporaryArray[temporaryArray.count - 2] + temporaryArray[temporaryArray.count - 1] temporaryArray.append(nextElement) } print("おるで") return temporaryArray } for i in 0..<fibonacci.count { print(fibonacci[i]) }おるで // fibonacci.countでの参照で呼ばれている おるで // 以降、print(fibonacci[i])での参照で呼ばれている 1 おるで 1 おるで 2 (中略) おるで 4807526976 おるで 7778742049 おるで 12586269025例3 ) Initialization Closureが1度しか呼ばれていないことの確認
let fibonacci: [Int] = { var temporaryArray = [1, 1] let numberOfElement = 50 for _ in 0..<numberOfElement - temporaryArray.count { let nextElement = temporaryArray[temporaryArray.count - 2] + temporaryArray[temporaryArray.count - 1] temporaryArray.append(nextElement) } print("おるで") return temporaryArray }() for i in 0..<fibonacci.count { print(fibonacci[i]) }おるで // 初期化のタイミングで呼ばれている 1 1 2 (中略) 4807526976 7778742049 12586269025まとめ
ここまでの内容をまとめると、今回の
initCollectionViewFlowLayout
は、Initialization Closureの正規の使い方というよりかは、その仕組みを応用した手法なのかな?と感じました。これ思いついた人賢いな〜〜と感心しました。ここまで理解するのにかなり時間がかかってしまいましたが、また便利そうな手法を1つ知ることができたのでこれから上手く使っていこうと思います。何か間違い等あればご指摘お願いします。
参考
- 投稿日:2019-12-18T15:00:47+09:00
【Swift】Partial application of 'mutating' method is not allowedってなんだ
structを使っていたら、
Partial application of 'mutating' method is not allowed
というエラーに遭遇しました。
僕の場合、原因は別件だったんですが、日本語情報があまりないエラーメッセージだったので、記事にしてみます。僕のエラーの原因
僕の場合、
protocol aDelegate { var aView: ViewClass } struct AIRadarMapBorderRenderer { private weak var delegate: aDelegate? private mutating func aFuncation() { switch aView.aVariable { //この行で"Partial application of 'mutating' method is not allowed" case .any //any default: //any } } }という書き方をしたらこのエラーが出ました。
誤switch aView.aVariableここを
正switch delegate?.aView.aVariableとすればOKでした。
それで終わりなんですが、ついでなんでエラーメッセージの意味も調べてみました。
Partial application of 'mutating' method is not allowedってなんだ
Partial application
Partial applicationは、日本語にすると部分適用と訳される技術用語です。
関数型プログラミングっぽい用語で、日本語記事見ると、だいたいカリー化と一緒に紹介されている用語です。
複数の引数を持った関数に対して、普通は全変数に値を渡して、戻り値を得ます。
たとえば、f(a,b,c) = a + b + c という関数があったら、f(1,1,1) = 1 + 1 + 1 = 3 ですね?
これを部分適用だと、f(a,b,c) = f(a, 1, 1) = f(a) + 2 = f(1) + 2 = 3
ただしf(a) = a, f(b, c) = b + cみたいな計算ができます。
命令形言語使ってると、「何が嬉しいんだ……?」という気になりますが、
関数型言語だと関数が汎用的なので、利便性のために汎用性を減らしたいときがあるみたいです。Partial application of 'mutating' method is not allowedの例
Partial application of 'mutating' method is not allowed
エラーメッセージでググったら一番上に出てくるのがこちらの記事。
例struct MyStruct { var count = 0 mutating func add(amount: Int) { count += amount } } var myStruct = MyStruct() [1, 2, 3, 4].forEach(myStruct.add) // Partial application of 'mutating' method is not allowed想定している結果としては、1+2+3+4でcount=10となるのを想定していると思われます。
そもそもforEachの中にカッコ省略してメソッド書いて動くのか……?とかはあるんですが、とりあえず"Partial application of 'mutating' method is not allowed"でエラー。
記事の中でも動くコードの例は出てますが、下記でも動きます。動く例var count = 0 [1, 2, 3, 4].forEach({ amount in count += amount }) print(count) //10なぜこれがエラーになるかというと、内部的には
myStruct.add(1)
↓
myStruct.add(2)
↓
myStruct.add(3)
↓
myStruct.add(4)
↓
myStruct.add(1,2,3,4) ※この関数の場合ここでは何もせずという順番で処理する必要があり、これが部分適用になる訳ですね。
Swiftで部分適用?
Swiftで部分適用(カリー化)にある、
func addTwoNumbers(a: Int)(b: Int) -> Int { return a + b } let add1 = addTwoNumbers(1) add1(b: 2) //< 3という書き方がその昔Swiftオフィシャルでできたらしいのですが、Swiftいくつからなのかは不明ですが、廃止されました。
- 投稿日:2019-12-18T14:54:56+09:00
Sketchと1対1を目指すAtomic designなSwiftUIの作り方
はじめに
VALU Advent Calendar 2019 11日目 (!) の記事です!
VALU ではデザインに Atomic design (アトミックデザイン) を採用しています。
本記事では,VALU のデザイナーが作成した素敵な Sketch シンボルを差分なく反映し,かつ変更に耐えうる SwiftUI を運用するための方法をご紹介しようと思います。昨年の VALU Advent Calendar にて,Sketchと1対1を目指すAtomic designなStoryboardの作り方 を投稿しました。本記事はそれを SwiftUI で行ったものです。まだご覧になっていない方は先にご覧ください。
Highlights
- SwiftUI を利用することで,「ファイル数増加」「子View更新時の伝播」「親ViewのConstraints破棄」など,UIKit (昨年の実装) での懸念点が全て解決された
- Atomic design x SwiftUI の親和性はとても高く,ほとんど1対1で再現することができた
- SwiftUI が迫る今,我々 iOS エンジニアはどう闘っていくのだろうか
昨年の実装方法を振り返る
昨年作成した Storyboard および Xib を以下に改めて掲載します。
Sketch の Level と同様に,細かい単位で Xib ファイルを生成した後,StackView を利用することにより,増減に強い UI の作成が可能となりました。
一見完全に再現できているように見える一方で,View の変更が追従しない問題 など,開発する上での限界も存在していました。
これらの問題は,SwiftUI によって解決されたのでしょうか?Atomic にファイルを分割することによって発生した問題
Atom レベルから Xib と,それに対応する Swift ファイルを生成したことにより,ファイル数が多くなる問題 が生じます。
この問題は,最終的にコンパイルさえ通らなくなるという予期せぬ事態を生む結果となりました。本件につきましては,別の記事で詳細とその解決策を記しています。Atomic design 自体が悪かった,ということではないエラーでしたが,情報共有のため以下に掲載しておきます。
【Xcode】細分化する iOS Architecture に向き合う上で気をつけなければらないただひとつのエラーについて
Atomic design (アトミックデザイン) とは
Atomic Design で有名なこのイラスト。上のイラストはそれを端的に説明しています。
Atomic Design とは,ひとつのページを個々の細かなコンポーネントを組み合わせて作成するデザインの方針を指します。
Atom (原子),Molecules (分子),Organisms (生体),Templates,Pages
その Atomic (原子-の) の名と矢印の方向が指す通り,個々の小さな汎用部品を元に,より大きな View が構成されています。
Sketch に合わせた View ファイルの作成
Sketch では,Atom,Molecules,といった表現が「Lv」によって示されています。
低レベルのシンボルを元に,より大きなシンボルを作成していく形となっています。以下は,弊社デザイナーさんから受け取った,素敵な Sketch ファイルです。
これを,シンボル毎に SwiftUI ファイルにしていきます。
今回は前回同様,複数の View から構成され,いいね等のアクションを行なう画面 (Lv2/Post/Action/Icon Button) に焦点を当てていきます。
Atom の作成
まずは Atom の作成です。
ActionCountableView
という View を作成します。最初だけ振り返りつつ進みます。昨年の実装は上の通り,Xib ファイルと Swift ファイルを用意し,IB 上またはコード上で値を指定していました。
以下が SwiftUI にて作成した内容です。SwiftUI は,左側のコードと右側のプレビューが対応しており,プレビュー画面での変更が即時にコードとして反映されます。ですので,編集するファイルは
.swift
ファイルのみとなります。
#if DEBUG
で囲まれた部分はデバッグ用のプレビューに当たる部分であり,View の構成に必要な struct 部分はたった 20 行で表現できます (Xib を使った昨年の実装は Xwift ファイル 12 行 + Xib ファイル 50 行 → 2 ファイル 62 行)。驚くほど簡単に View を生成することができたのが分かるでしょう。さらに,プレビュー用の記述を追加することで,画像の差し替えや値のランダム化,ダークモード時の様子まで確認することができるようになっています。
これだけでもいくつか SwiftUI としてのテクニックがあります。
1 .
Button
のハイライト領域SwiftUI では,ボタンは
Button
構造体を利用しています。いくつか初期化方法がありますが,ボタンとして認識させ,かつタップ領域としてハイライトさせるためには,Button<Label> : View where Label : View
のLabel
部分がボタン UI として認識されます。Label
は protocolView
に適合した Generics となっています (SwiftUI では UIKit で言うUILabel
はText
に相当します)。
init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
を利用して ボタン内部に View を描く ことによって,ハイライト領域を指定しています。2 .
Image
の色変更およびサイズ変更
.renderingMode(_:)
を.template
指定することにより,色を変更することができます。
また,.frame(width:height:)
でサイズを変更する前には,.resizable()
を入れサイズが可変であることを明示的に指定しなければなりません。3 .
static func
によるプレビュー内再利用ダークモード対応など,同じ Atom の内容を複数の環境で一覧したい場合があるかと思います。プレビューに用いられる
static var previews: some View
は static ですので,再利用したい View を別の static な変数または関数に切り出しておくと,スタブを注入する手間も省けるためお得です。Molecule の作成
次は,先ほど作成した Atom を複数組み合わせた Molecule に当たる View を作成していきます。
Sketch では,この View に対して Override を活用しつつそれぞれの画像を挿入していました。Atomic Design に従った場合,こちらも同様に変更することが可能です。
昨年と異なる点は,値を保ちつつ複数の画面にコードを共有させることができる※ 点です。
Xib のコピペから開放されることで,子 View での変更が 親 View への変更に常に追従できるようになります。これは UI がコードのみから生成できるようになった利点でもあります。UIKit の
StackView
のようにHStack
に積んでいるだけですが,Spacer()
を利用することで,長さ指定のないUIView
同様伸縮するようになります。※
@IBDesignable
を利用した生成方法等によってこれまでも Atoms または Molecules レベルのソースコードを共有することができました。一方でその方法は,Xcode のコンパイル時間が長くなってしまったり,ソースコードの量によって最終的には IB 上で描画が実行されなくなってしまうなどの問題があり,昨年度は採用しなかった経緯があります。Organism の作成
Organisms に入ります。ここで一旦内容を以下のような struct にまとめておきます。後にデータをリスト状に表示させるため,
Hashable
にも適合させることが必要です (ここでは処理を簡略にするためImage
を直接プロパティとして持たせていますが,Domain 層にある API 通信の結果だと想像してください)。投稿内容を反映する
PostView
を作成します。個々の View は Molecules において作成済みですので,VStack を利用して詰み上げるだけで完成します。投稿画像など,存在しない可能性がある場合は
AnyView
とEmptyView
の組み合わせによって動的に実現が可能です。三項演算子を利用していますが,どちらの View もAnyView
でラップしてあげることで結果を返し,異なる View を切り替えることができます。Page の作成
最後に,Page の作成です。
ScrollView
内部にForEach
を作成し,Hashable
に適合したPost
構造体をもとに投稿内容を反映しています。ダークモードにも対応すると,プレビューも映えますね。画像を見れば分かる通り,高さは内容によって自動で調整が入るようになっています。ここまで来れば,もう描画についての責任は Pages から離れることとなり,スクロール処理や遷移等の動作に関わる状態に集中できるようになるでしょう。
実際の開発では,ここから
@State
として記述している部分を,@ObservedObject var viewModel
に置き替え,通信部分や画面遷移の処理を書いていくことになります。まとめ
今回は,過去に使ったものと同じデザインを利用し,SwiftUI を用いて改めて Atomic design に向き合いました。
その結果,SwiftUI を利用することで,「ファイル数増加」「子 View 更新時の伝播」「親 View の Constraints 破棄」など,昨年の UIKit での懸念点が全て解決され,View をデザイン通りに記述することができるようになりました。さらに,UIKit を用いるよりも格段に Sketch に近づいた印象です。これがコードのみで表現されるのは当時は想像もつかなかったのではないでしょうか。これ,HTML で言う CSS を使ってないんですよ!?
Atomic design x SwiftUI の親和性はとても高く,React の知見と上手く融合していくのだと感じました。
これからコード生成プラグインが React のように SwiftUI にも対応するような流れになれば,UI 部分はもっと進化していくのではないでしょうか。一方で,危惧されることが 1 点あります。
(SwiftUI で 1 アプリを作った程度の人間でも)数時間でデザイナーの意図する画面を 1 対 1 として容易に実現可能な点です。これはひとつに,デザイナーが UI 層に進出することを容易に想像できる1つの証拠です。現在の SwiftUI では,スクロール時のページング処理など,ちゃんとした機能を利用しようとすると View が大変複雑になります。Atomic design との親和性は高まったものの,どこまで似せるのか,SwiftUI の得意不得意にどう対応していくのか。
簡単な UI であれば誰でも作れるようになった世界において,領域を奪われた iOS エンジニアの勝負どころはどこになるでしょうか。証明書周りやリジェクト対応と答えた人はセンスがあると思います。実際には ViewModel 以後ですよね。そうなった際にエンジニアとしての価値が再度問われるわけです。
あなたはどうですか?何ができますか?どこまでできますか? と。
References
Sketchと1対1を目指すAtomic designなStoryboardの作り方
Atomic design
Apple Design Resources
Human Interface Guidelines
センスのある某での呟き
- 投稿日:2019-12-18T12:18:39+09:00
Property Wrapper 入門
Rx を使ってプロパティの変更通知をしたい、だけどクラス外からは値の変更を許したくないというユースケースがよくあると思います。
そうしたとき、以前までは以下のように書いていました。
struct Person { private let _age: BehaviorSubject<Int> var age: Observable<Int> { _age.asObservable() } init(initialAge: Int) { _age = BehaviorSubject(initialAge) } }しかし、変更通知をしたいだけなのにわざわざこんなに書くのは冗長極まりないです(個人の感想です)。
そこで、 Swift 5.1 で追加された新たな機能、 Property Wrappers の出番です。
最初に以下のようなオブジェクトを用意します。
@propertyWrapper struct PropertyPublished<Value> { private(set) var wrappedValue: Value { didSet { subject.onNext(wrappedValue) } } var projectedValue: Observable<Value> { subject.asObservable() } private let subject: BehaviorSubject<Value> init(wrappedValue: Value) { self.wrappedValue = wrappedValue subject = BehaviorSubject(value: wrappedValue) } mutating func onNext(_ newValue: Value) { wrappedValue = newValue } }あとは変更通知したいプロパティの前に
@PropertyPublished
をつけるだけです。struct Person { @PropertyPublished var age: Int init(initialAge: Int) { _age = PropertyPublished(wrappedValue: initialAge) } mutating func increment() { _age.onNext(age + 1) } } // 使い方 var person = Person(initialAge: 10) person.$age.subscribe(onNext: { print("Age: \($0)") }) print(person.age) person.increment() // Output: // Age: 10 // 10 // Age: 11ずいぶんすっきりしました。
さて、すごくシンプルに書けたのはいいのですが、上の例で Swift 5.1 未満では謎な文法がありました。
Person
内ででてきた_age
はPerson
では明示的に定義していません。
使うときに出てきたperson.$age
もPerson
では明示的に定義してません。
また、PropertyPublished
内で定義したwrappedValue
やprojectedValue
は他の名前では代替できません。これらは全て Property Wrapper の機能として組み込まれており、とても柔軟で便利な機能を実現することができます。
ここでは Property Wrapper の基本的な動作について、見ていきます。
基本
Property Wrapper の最低限の用件は以下です。
- Property Wrapper 型にしたい型に
@propertyWrapper
をつける- Property Wrapper 型は
wrappedValue
というインスタンスプロパティを持つ。これだけです。
実際に作っていきましょう。
まず Property Wrapper 型にしたい型に@propertyWrapper
をつけます。こうすることでコンパイラーに「この型は Property Wrapper 型です」ということを伝えます。
@propertyWrapper struct PropertyPublished { }この時点でコンパイルすると、
Property wrapper type 'PropertyPublished' does not contain a non-static property named 'wrappedValue'と怒られます。
Property Wrapper 型はwrappedValue
というプロパティを持つことが最低条件でした。
なのでwrappedValue
を追加します。@propertyWrapper struct PropertyPublished<Value> { var wrappedValue: Value }これで最低条件は満たせました。
実際に使ってみましょう。
使い方も簡単でプロパティの前に@{Property Wrapper 型の名前}
をつけるだけです。struct Person { @PropertyPublished(wrappedValue: 10) var age: Int }こうすることで上のコードをコンパイラーは以下のように展開します。
struct Person { private var _age: PropertyPublished<Int> = PropertyPublished(wrappedValue: 10) var age: Int { get { _age.wrappedValue } set { _age.wrappedValue = newValue } } }まさに Property を Wrap してますね!
ちなみに
@{Property Wrapper 型の名前}
の後に()
をつけることでその Property Wrapper 型のイニシャライザを呼び出すことができるのですが、.init(wrappedValue:)
の呼び出しだけは糖衣構文として以下が用意されています。@PropertyPublished var age: Int = 10 // ↑ は ↓ と等価 // @PropertyPublished(wrappedValue: 10) var age: Intとてもシンプルですね!
ただこのままではプロパティの変更を検知できません。
なので Observable を外に見える形で公開しなければなりません。ここで Property Wrapper で便利な機能である
projectedValue
の出番です。以下のように
projectedValue: Observable<Value>
なプロパティを追加します。@propertyWrapper struct PropertyPublished<Value> { var wrappedValue: Value var projectedValue: Observable<Value> { subject.asObservable() } private let subject: BehaviorSubject<Value> init(wrappedValue: Value) { self.wrappedValue = wrappedValue self.subject = BehaviorSubject(value: wrappedValue) } }値の更新を購読可能にするために
BehaviorSubject
も一緒に追加しました。
そしてprojectedValue
はただBehaviorSubject
をObservable
として公開しているだけです。こうすることにより、
struct Person { @PropertyPublished(wrappedValue: 10) var age: Int }は以下のように展開されます。
struct Person { private var _age: PropertyPublished<Int> = PropertyPublished(wrappedValue: 10) var $age: Observable<Value> { _age.projectedValue } var age: Int { get { _age.wrappedValue } set { _age.wrappedValue = newValue } } }
$age
という変数が追加されました。
また、このときのプロパティの可視性はage
と同じく internal です。
これにより、もともとラップしたプロパティの型は変えずに新たにプロパティの型を外に公開できます。また、今回は
projectedValue
には getter しか定義しませんでした。
当たり前と言えば当たり前ですが、wrappedValue
やprojectedValue
が get-only で setter がない場合や、private(set)
などで setter が外部に公開されていない場合は同じく展開されたプロパティにも setter は生えません。上記までの実装で必要なプロパティは揃えることができました。
あとは実際に新しい値がセットされたとき、PropertyPublished
のsubject
を発火させてあげればいいだけです。ということで発火させる用のメソッドを
PropertyPublished
に追加してあげます。@propertyWrapper struct PropertyPublished<Value> { private(set) var wrappedValue: Value { didSet { subject.onNext(wrappedValue) } } var projectedValue: Observable<Value> { subject.asObservable() } private let subject: BehaviorSubject<Value> init(wrappedValue: Value) { self.wrappedValue = wrappedValue self.subject = BehaviorSubject(value: wrappedValue) } func onNext(_ newValue: Value) { self.wrappedValue = newValue } }変更通知を飛ばすのは
onNext
メソッドを通して欲しいためwrappedValue
をprivate(set)
にしています。これにより
age
が get-only になりました。また、 最初から自動で生成されていた
_age
は private な変数なのでもちろんPerson
内からならアクセスできます。struct Person { @PropertyPublished var age: Int = 10 mutating func increment() { _age.onNext(age + 1) } }この
Person.age
を監視する方法は以下です。let person = Person() person.$age.subscribe(onNext: { /* 処理 */})ずいぶん短いコードで
- 本来見せたいプロパティの値である
Value
- 外部からは見える
Observable<Value>
- 外部からは見えない
Subject<Value>
を達成することができました!
Swift 5.0 以前でこれを実装しようとすると以下のように書かなければなりません。
struct Person { private let _age: BehaviorSubject<Int> // `$` はユーザー定義変数では使えない var observableAge: Observable<Int> { _age.asObservable() } var age: Int = 10 { didSet { _age.onNext(age) } } init() { _age = BehaviorSubject(value: age) } mutating func increment() { age = age + 1 } }長いですね。
また、これはプロパティが増えるごとに追加しなければなりません。
Property Wrappers を使えばただ@PropertyPublished
を追加していけばいいだけです。struct Person { @PropertyPublished var age: Int @PropertyPublished var name: String init(age: Int, name: String) { self._age = PropertyPublished(wrappedValue: age) self._name = PropertyPublished(wrappedValue: name) } }シンプルでいいですね!
ただ、ちょっと残念なのが、
self.age = ageと書けずに
self._age = PropertyPublished(wrappedValue: age)になっている点です。
Property Wrapper の Proposal には
@Lazy var x: Int // ... x = 17 // okay, treated as _x = .init(wrappedValue: 17)とあるので、 init 内ならいけないかなーと思ったのですが、
Cannot assign to property: 'age' is a get-only propertyでコンパイルエラーになるのでダメみたいですね。。。
なので大人しく直接_age
を初期化しています。ただ、前までのように大量にプロパティを追加していく必要もなく、同じような処理を何度も書かなくていいので非常に便利です。
制限
Property Wrapper はとても便利なのですが、便利が故にいろいろ制限があります。
- ラップしたプロパティは protocol で使用できません
- ラップしたインスタンスプロパティを extension では宣言できません
- ラップしたインスタンスプロパティを Enum では宣言できません
- class 内で宣言されたラップしたプロパティは override できません
- ラップしたプロパティには
lazy
、@NSCopying
、@NSManaged
、weak
、unowned
をつけられません- ラップしたプロパティはそれを囲む宣言内で唯一のプロパティでなければなりません
@PropertyPublished var (x, y) = ...
は不正- ラップしたプロパティは getter と setter を持てません
- Property Wrapper 型と wrappedValue は、同じアクセス権を持ちます
- PropertyPublished を public にしたなら wrappedValue も public にしないとダメ
- init(wrappedValue:) を宣言した場合、Property Wrapper 型と init(wrappedValue:) は、同じアクセス権を持ちます
- projectedValue を宣言した場合、Property Wrapper 型と projectedValue は、同じアクセス権を持ちます
- init() を宣言した場合、Property Wrapper 型と init() は、同じアクセス権を持ちます
将来追加される(と思われる)機能
Property Wrappers は今でも結構便利ですが、まだまだフル機能ではありません。
Proposal に将来実装される(予定)機能があるので紹介します。より細かいアクセス制御
現状、
_age
と$age
のアクセス権は固定になっており、制御できません(_age
は private、$age
はラップしたプロパティと同じ)。なのでこれらも使う側で制御できるようにしようという提案があります。
現状考えられているのは以下のような構文。
@PropertyPublished public internal(storage) private(projection) var foo: Int = 0上のように書くと、以下のように展開される予定です。
internal var _foo: PropertyPublished<Int> = PropertyPublished(wrappedValue: 0) private var $foo: PropertyPublished<Int> { _foo.projectedValue } public var foo: Int { _foo.wrappedValue }便利なのですが、初見では困惑しそうですね。
ラップされたプロパティを持つインスタンスを参照する
現状の Property Wrapper 型はラップされたプロパティを所持しているインスタンスにアクセスできません。
なので現状手動で PropertyWrapper 型へ伝える必要があります。
protocol Observed { func broadcastValueWillChange<Value>(newValue: Value) } @propertyWrapper struct BroadcastObservable<Value: Equatable> { private var stored: Value var wrappedValue: Value { get { stored } set { if newValue != stored { observed?.broadcastValueWillChange(newValue: newValue) } stored = newValue } } private var observed: Observed? = nil init(wrappedValue: Value) { self.stored = wrappedValue } mutating func register(_ observed: Observed) { self.observed = observed } } // 今回は話の都合上 class になります class Person: Observed { @BroadcastObservable var age: Int = 0 init() { _age.register(self) } func broadcastValueWillChange<Value>(newValue: Int) { // action } }ただこのコードには様々な問題があります。
まず_age.register(self)
を手動で呼び出す必要があるため、忘れる可能性があります。また、
broadcastValueWillChange<Value>(newValue:)
内でage
プロパティを読むとメモリの排他アクセス違反が起きる可能性があるため、アクセスしてはいけません。試しに以下のようなコードを実行してみます。
class Person: Observed { @BroadcastObservable var age: Int = 0 init() { _age.register(self) } func broadcastValueWillChange<Value>(newValue: Int) { print(age) } } var person = Person() for i in Array(1...5) { person.age = i }
person.age
が変更されるたびに print するようにしています。これを実行すると以下のようなエラーを吐いてクラッシュしてしまいます。
Simultaneous accesses to 0x100cba3a0, but modification requires exclusive access. Previous access (a modification) started at `Person.age.setter + 85 (0x100002db5). Current access (a read) started at: ...
Person.age
の setter 内でPerson.age
を読むのはダメですよと言われていますね。これらの問題を解決するために、以下のような構文が提案されています。
@propertyWrapper struct BroadcastObservable<Value> { private var stored: Value init(wrappedValue: Value) { self.stored = wrappedValue } static subscript<OuterSelf: Observed>( instanceSelf observed: OuterSelf, wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>, storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self> ) -> Value { get { observed[keyPath: storageKeyPath].stored } set { let oldValue = observed[keyPath: storageKeyPath].stored if newValue != oldValue { observed.broadcastValueWillChange(newValue: newValue) } observed[keyPath: storageKeyPath].stored = newValue } } }
var wrappedValue: Value
が消えて代わりにstatic subscript<OuterSelf: Observed>(instanceSelf:wrapped:storage:)
が生えました。そして
BroadcastObservable
へのアクセスはobserved
(ラップしたプロパティを持っているインスタンス) とstorageKeyPath
(e.g._age
プロパティの KeyPath) を使ってアクセスします。これで少し複雑になりましたが、
wrappedValue
の setter アクセス時に getter が呼ばれることがなくなったため、メモリの排他アクセス違反が起きなくなりました。上のような構文を書くと使う側では以下のように展開されます。
class Person: Observed { @BroadcastObservable var age: Int = 0 // ↑ は ↓ に展開される private var _age: BroadcastObservable<Int> = BroadcastObservable(wrappedValue: 0) public var age: Int { get { BroadcastObservable<Int>[instanceSelf: self, wrapped: \Person.age, storage: \Person._age] } set { BroadcastObservable<Int>[instanceSelf: self, wrapped: \Person.age, storage: \Person._age] = newValue } } }これで安全にラップされたプロパティを持つインスタンスへのアクセスができるようになりますね!
また、副次的な効果として、
static subscript<OuterSelf: Observed>(instanceSelf:wrapped:storage:)
は展開されるとself
を引数にとるため、 static プロパティに対して適用できません。
また、現状ReferenceWritableKeyPath
を引数にとるため、 class 以外に適用できません(たぶん)。なので、もし Property Wrapper 型を static プロパティや class 以外に適用されて欲しくない場合は、以下のように書けます。
@availability(*, unavailable) var wrappedValue: Value { get { fatalError("only works on instance properties of classes") } set { fatalError("only works on instance properties of classes") } }たまにラップされたプロパティを持つインスタンスにアクセスしたくなるときがあるので、ぜひ実装されて欲しいです。
他のプロパティに移譲する
現状、
@SomeWrapper
を宣言すると暗黙的に_
がついたバッキングフィールドが追加されます。
ただ、もう既にあるプロパティをバッキングフィールドに使いたい場合があるかもしれません。そのときに以下のように指定できるようにしようぜという提案。
lazy var fooBacking: SomeWrapper<Int> @wrapper(to: fooBacking) var foo: Int上の例では直接プロパティを入れていますが、 KeyPath のように
\.someProperty.someOtherProperty
のようにも指定できるようにするかなども考えられています。
以上、軽くですが Property Wrapper について解説しました。
Property Wrapper は他のプログラミング言語ではあまり見かけない、しかしテンプレート的な記述を減らせる可能性を秘めていると思います。もしプロジェクトでバッキングフィールドを駆使しているプロパティがあったなら、それは Property Wrapper に置き直せるかもしれません。
- 投稿日:2019-12-18T11:30:32+09:00
[Swift5] 動画撮影方向の切替
メモです。
func setup() { // 端末の回転時に通知してもらう let action = #selector(orientationDidChange(_:)) let center = NotificationCenter.default let name = UIDevice.orientationDidChangeNotification center.addObserver(self, selector: action, name: name, object: nil) } /// 端末が回転したときに呼出されるメソッド @objc func orientationDidChange(_ notification: NSNotification) { // 現在の端末方向を取得 let deviceOrientation: UIDeviceOrientation! = UIDevice.current.orientation // VideoLayerのサイズを現在の画面サイズに合わせる // これしないと、画面が最初に設定したものと同じ状態のまま方向が変わってしまう。 videoLayer.frame = self.view.bounds // 画面の方向に合わせて映像を調整 // UIDeviceOrientation自体がenumだったのでそのままswitchで切り替える // - 今回は縦(ホームボタンが上)の状態は利用しなかったので記載していない // 利用する場合はportraitUpsideDownでcaseすればよい // - あとはlandscape時にvideoOrientationに入れるときは // deviceOrientationと逆のを入れないと逆さになるので注意 switch deviceOrientation { case .portrait: // 縦(ホームボタンが下) videoLayer.connection?.videoOrientation = .portrait case .landscapeLeft: // 横(ホームボタンが右)※頭が左側に回転 videoLayer.connection?.videoOrientation = .landscapeRight // 画面右側を頭に case .landscapeRight: // 横(ホームボタンが左)※頭が右側に回転 videoLayer.connection?.videoOrientation = .landscapeLeft // 画面左側を頭に default: break } }
- 投稿日:2019-12-18T06:53:24+09:00
SwiftUI UITextField キャレット動作
こんにちはフリーランスの永田です。最近は法人化の手続きを開始しました。
SwiftUI案件を1月から実施予定で、現在技術を調査中です。
今回はキャレット動作 returnButtonを押下しましたら、水平移動する対応です。
環境
Xcode 11.3
SwiftUISwiftUIではない場合(オリジナルです。)
https://gist.github.com/daisukenagata/253ae79692234dbf89d042f5010f2387
キャレット動作しない場合
https://gist.github.com/daisukenagata/5002c49061f18d72e0a40dfda1290b1a
キャレット動作
https://gist.github.com/daisukenagata/253ae79692234dbf89d042f5010f2387
import UIKit import SwiftUI struct ContentView: View { @State var text: String = "" @State var text2: String = "" @State var spacing: CGFloat = 0 @State var didTap = false var body: some View { VStack { HStack(alignment: .bottom, spacing: spacing) { SATextField(tag: 0, placeholder: "placeholder", changeHandler: { (newString) in self.text = newString }, onCommitHandler: { // write something }) self.text.isEmpty == false ? HorizontalLine(color: self.didTap ? Color.red : Color.black) : HorizontalLine(color: self.didTap ? Color.black : Color.red) SATextField(tag: 1, placeholder: "placeholder2", changeHandler: { (newString) in self.text2 = newString }, onCommitHandler: { // write something }) text2.isEmpty == false ? HorizontalLine(color: didTap ? Color.red : Color.black) : HorizontalLine(color: didTap ? Color.black : Color.red) } }.position(.init(x: UIScreen.main.bounds.width/2+50, y: UIScreen.main.bounds.height/2)) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } struct HorizontalLine: View { private var color: Color? = nil private var height: CGFloat = 1.0 private var shape: HorizontalLineShape? init(color: Color, height: CGFloat = 1.0) { self.color = color self.height = height } var body: some View { HorizontalLineShape().fill(self.color!).frame(minWidth: 0, maxWidth: .infinity, minHeight: height, maxHeight: height) } } struct HorizontalLineShape: Shape { func path(in rect: CGRect) -> Path { let fill = CGRect(x: -rect.size.width, y: 0, width: rect.size.width, height: rect.size.height) var path = Path() path.addRoundedRect(in: fill, cornerSize: CGSize(width: 2, height: 2)) return path } } class Model: ObservableObject { @Published var text = "" var placeholder = "Placeholder" } // check is this // https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2 struct SATextField: UIViewRepresentable { private let tmpView = WrappableTextField() //var exposed to SwiftUI object init var tag:Int = 0 var placeholder:String? var changeHandler:((String)->Void)? var onCommitHandler:(()->Void)? func makeUIView(context: UIViewRepresentableContext<SATextField>) -> WrappableTextField { tmpView.tag = tag tmpView.delegate = tmpView tmpView.placeholder = placeholder tmpView.onCommitHandler = onCommitHandler tmpView.textFieldChangedHandler = changeHandler return tmpView } func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<SATextField>) { uiView.setContentHuggingPriority(.defaultHigh, for: .vertical) uiView.setContentHuggingPriority(.defaultLow, for: .horizontal) } } class WrappableTextField: UITextField, UITextFieldDelegate { var textFieldChangedHandler: ((String)->Void)? var onCommitHandler: (()->Void)? func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField { nextField.becomeFirstResponder() } else { textField.resignFirstResponder() } return false } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let currentValue = textField.text as NSString? { let proposedValue = currentValue.replacingCharacters(in: range, with: string) textFieldChangedHandler?(proposedValue as String) } return true } func textFieldDidEndEditing(_ textField: UITextField) { onCommitHandler?() } }
参考サイト
こちらのロジックを拝借させていただきました。ロジックの部分は、今まで通りのプログラムになります。
https://medium.com/@valv0/textfield-and-uiviewrepresentable-46a8d3ec48e2
プログラムがわかる場合は
changeHandler
を追えば、すぐにわかると思います起動時に
makeUIView
メソッドでtmpView.textFieldChangedHandler = changeHandler
を代入します。
既存のUITextFieldメソッドで実装しています。onCommitHandler
もbind処理をしていますが、
Flowは同じです。このメソッドは文字が1文字づつ変化する事に処理が行われます。
textFieldChangedHandler?(proposedValue as String)
でbindしてchangeHandler
が呼ばれています。func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let currentValue = textField.text as NSString? { let proposedValue = currentValue.replacingCharacters(in: range, with: string) textFieldChangedHandler?(proposedValue as String) } return true }return Buttonを押下時 これでキャレット
nextField.becomeFirstResponder()
UITextFieldのキーボードを開くメソッドです。func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField { nextField.becomeFirstResponder() } else { textField.resignFirstResponder() } return false }以上、とても簡単に解説しました
貴重なお時間お読みくださいまして、誠にありがとうございます。
- 投稿日:2019-12-18T00:33:06+09:00
レガシーアプリのiOS13対応(UISegmentedControl編
はじめに
UISegmentedControl編とタイトルには記載してますが、
続編等は今の所は全くありませんwwwさて、私が普段担当しているアプリは、業務で利用するInHouseの古いアプリなんですが
今年もiOSのバージョンアップ対応を毎度のごとく行っておりました。UISegmentControlに関しても、今年は大幅に見た目が変わると知っていたので
TintColorの設定をして、アプリに合う見た目にして対応する事で十分だろうと思っていました。先方にも、見た目がiOS13から変更された旨を伝え、一度はOKをもらっていましたが
やっぱり、前の見た目に戻してくれないか?
…
オォォーーー!! w(゚ロ゚;w(゚ロ゚)w;゚ロ゚)w オォォーーー!!
AppleがiOS13で変更した見た目に対して
iOS12以前の状態に戻していくことは
Appleに対しての反逆行為やぞーーーー(´;ω;`)(勝手に思っているだけ?)できれば今のiOS13の見た目のままでカスタマイズしたかったのですが、、、
NGが出てしまったのでは仕方ない。iOS12以前の見た目にするしかない。。できるのか。。。結果的に言えば 可能 でした!!
比較すれば元々のiOS12以前のモノと若干違うのは分かりますが、全然許容範囲!実際のイメージをどうぞ!!
ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() if #available(iOS 13.0, *) { let segSizeSingle = CGSize.init(width: self.segment.frame.size.width, height: self.segment.frame.size.height) self.segment.setBackgroundImage(self.makeImage(UIColor.clear, size: segSizeSingle), for: .normal, barMetrics: .default) self.segment.setBackgroundImage(self.makeImage(self.segment.tintColor, size: segSizeSingle), for: .selected, barMetrics: .default) self.segment.setTitleTextAttributes([.foregroundColor : UIColor.white], for: .selected) self.segment.setTitleTextAttributes([.foregroundColor : self.segment.tintColor!], for: .normal) self.segment.layer.borderColor = self.segment.tintColor.cgColor self.segment.layer.borderWidth = 1 } } func makeImage(_ color : UIColor, size : CGSize) -> UIImage { UIGraphicsBeginImageContext(size) let rect = CGRect(origin: CGPoint.zero, size: size) let context = UIGraphicsGetCurrentContext()! context.setFillColor(color.cgColor) context.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! }セグメントのBGImageや、レイヤー等の見た目を色々いじって実現しました!
今回のサンプルでは
ViewDidLoad
に記載しましたが
実際に実装する場合はExtention
やObjective-CだとCategory
で実装すると
アプリ全体で見た目を合わす事も簡単にできると思いますので是非、ご活用ください!!!!
(そんな古い見た目に変える要望ねーよ!とかは言わないで…)
最後までご覧いただき、ありがとうございました!!
- 投稿日:2019-12-18T00:31:28+09:00
R.swiftとSwiftGenの導入方法とどちらを採用した方がいいのか
CA Tech Dojo/Challenge/JOB Advent Calendar 2019の18日目は@ostk0069が書かせていただきます。
次の日、19日目は@hmarfさんです!楽しみにしてます!
自分は、2019年8月にCATechDojo(Kotlin編)に参加させていただいた後、11月にCATechJOBでマッチングエージェントさんでiOSエンジニアとしてインターンをさせていただきました。大変お世話になりました。はじめに
私は現在進行形で個人アプリの開発をしています。その際に初めはR.swiftを導入していたのですが、途中からSwiftGenへ移行したのでそこでわかった、互いの良い面、悪い面について触れていければと思います。
R.swift、SwiftGenの話はこの記事を見たら理解が十分な状態に仕上げられていると思うのでよかったら最後まで読んでいただけるとありがたいです。
R.swift、SwiftGenとは
これらは主に文字列管理のしやすさのために使用するライブラリです。
文字列管理で一番最初に思い浮かべるのは多言語化対応かと思いますが、それだけではありません。
多言語化対応しないアプリでも導入する価値は十分にあります。UIImage
やUIColor
の指定もコードでtypeSafeで利用することができるので利便性があります。image literal
やcolor literal
で指定したものは次に開いたときには何を指定していたかわからなくなるのでそれが気に入らない方はコードで指定している分、管理しやすいとも言えます。結論
結論から言うと、R.swiftとSwiftGen、どちらを導入するか迷っている方で少なくともSwiftGenを導入することを大きなコストと捉えていない方はSwiftGenを導入する方がいいのではという気持ちです。
その理由も含めて、まずは導入方法から色々と説明していこうと思います。
このようなフローで説明していきます。
(飛ばしたい方はこちらから選択して見て行って下さい)本来であれば比較とどっちを選ぶべきであるかのみでもいいかなと思ったのですが、
導入方法について、紹介したいTipsがあったので導入方法も追加することにしました!R.swift
導入方法
GitHubに記載されているドキュメントを参照するとCocoaPodsでの導入が推奨されているのでそちらでの方法で説明していきたいと思います。
ここではCocoaPods自体の導入方法は説明しないのでわからない方はこちらからどうぞ。
Podfileへ以下の一行を追加します。
Podfilepod 'R.swift'その後、Terminal上で以下のコマンドを叩きます。
pod installこの後、ドキュメントでは
New Run Script Phase
を選択したのち、"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"
を追加するように書いてあります。しかし、この記述方法はあまりオススメしません。自動生成されるファイルの
R.generated.swift
が自分のプロジェクトファイルのレポジトリ直下に生成されてしまうからです。GitHubでソースコードを管理している人は特にレポジトリ直下に.swift
のファイルが存在するのは結構違和感があるのではないかと思います。なのでNew Run Script Phase
を選択したのち、以下を追加しましょう。"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/"プロジェクトファイル"/"hoge"/"fuga"/R.generated.swift" # プロジェクトファイル以下は自分の好きな場所に追加すればいいと思いますその後、アプリをビルドすることで
R.generated.swift
のファイルが上記で指定したディレクトリに自動生成されます。自動生成されたファイルは.project
への追加は自動でされないので、手動で追加してあげましょう。その後、自動生成ファイルをgitで管理する意味はないので
.gitignore*.generated.swift
を追加してあげることでひとまず導入は終了となります。
記述方法のサンプル
これはドキュメントから引っ張ってきたものなんですが、
通常で書く場合
let icon = UIImage(named: "settings-icon") let font = UIFont(name: "San Francisco", size: 42) let color = UIColor(named: "indicator highlight") let viewController = CustomViewController(nibName: "CustomView", bundle: nil) let string = String(format: NSLocalizedString("welcome.withName"))
R.swift
で記述した場合let icon = R.image.settingsIcon() let font = R.font.sanFrancisco(size: 42) let color = R.color.indicatorHighlight() let viewController = CustomViewController(nib: R.nib.customView) let string = R.string.localizable.welcomeWithName("Arthur Dent")とこのような違いがあります。すべて
R.
が始まる共通点がありますね。SwiftGen
導入方法
R.swift
に対して導入はSwiftGen
の方が少し複雑です。
まず初めに導入方法を選ぶ必要がある訳ですが、今回はR.swift
のときにCocoaPodsで導入の説明を行ったので揃えた方が違いを理解しやすいと思うのでCocoaPods
を使った場合での説明を行っていこうと思います。Podfileへ以下の一行を追加します。
Podfilepod 'SwiftGen'その後、Terminal上で以下のコマンドを叩きます。
pod install次に設定ファイルを作成します。このファイルの設定を記述するのがこの導入を難しくしています。
touch swiftgen.yml
作成した
swiftgen.yml
を編集していく訳ですが、まず初めにドキュメントに書かれているサンプルを見ていきましょうswiftgen.ymlstrings: inputs: Resources/Base.lproj filter: .+\.strings$ outputs: - templateName: structured-swift4 output: Generated/strings.swift xcassets: inputs: - Resources/Images.xcassets - Resources/MoreImages.xcassets outputs: - templateName: swift4 output: Generated/assets-images.swiftこれがどのような設定をしているのか説明していきます。
このサンプルでは
Base.lproj
のファイルからの生成.xcassets
のファイルからの生成を行っています。もちろん、
SwiftGen
ではそれ以外のStoryboard
とかも生成できる訳ですが、それは別途記述が必要です。さらに具体的にどのように書いているか見ていくと、
inputs
とoutputs
が定義されていて
inputs
は生成したいファイルの指定outputs
は生成したもの出力先気になるのは
filter
とtemplateName
というものが存在することです。filterとは
本来であればinputsの中のファイルの指定は
Base.lproj
ではなく、Base.lproj/Localizable.strings
である方が自然です。しかし、多言語化対応をしているとおそらくLocalizable.strings
は対応している言語分あるはずなので指定が面倒なのでfilter: .+\.strings$
と記述してあげることですべてのLocalizable.strings
をinputsの対象に含めることができますtemplateNameとは
SwiftGen
のなかで指定することのできるtemplateName
は以下です。
structured-swift4
flat-swift4
swift4
structured-swift3
flat-swift3
swift3
があります。現在
Swift5
で開発している人が多いからswift5
は?って思う人もいるかもしれませんが現時点では存在しません(swift4
->swift5
でそんなに影響を受けてないからだと思います)
string
の指定をするときにはswift4
やswift3
の指定は行えないので注意してください。
flat
かstructured
を選択するときは構造的にしたいかしたくないかで選びましょう。string
の場合、多言語化対応が多いと思うのでstructured
を推奨します。この選択肢以外にも自分でよりカスタマイズしたい方は別の方法が存在します。
それはStencilを使うことで実現可能です。元々上記のstructured-swift4
やflat-swift4
もStencilを使って作成されたテンプレートです。気になった方はぜひ見てみてください。(この記事ではこの内容にこれ以上Stencilについては触れません)記述方法のサンプル
通常で書く場合
let icon = UIImage(asset: Asset.Exotic.icon) let displayRegular = UIFont(font: FontFamily.SanFrancisco.regular, size: 42.0) let title = UIColor(named: .articleBody) let string = String(format: NSLocalizedString("welcome.with_name"))
SwiftGen
で記述した場合let icon = Asset.Exotic.banana.image let displayRegular = FontFamily.SFNSDisplay.regular.font(size: 42.0) let title = ColorName.articleBody.color let message = L10n.Welcome.withNameR.swiftとSwiftGenの比較
特徴を並べていきます。
R.swift
- 導入が楽
- リソースが単一
- buildするタイミングで毎回自動生成が走る
SwiftGen
-swiftgen.yml
ファイルでカスタマイズできるので拡張性がある
- リソースが複数
- 差分を見て生成するかどうかを見てくれる今回この記事を書くきっかけにもなったのですが、私はどちらも週数間の運用をしました。
その中でSwiftGenの方がいいと思ったので途中で乗り換えました
理由としては
UIView
はR.string
やSwiftGen
で管理する理由がそこまでないので、網羅性のあるR.swift
の強みが生きなかった- メインで使うlocalizeStringが長いので画面名とか役割とかを付け足すと
R.swift
だと長くなってしまう
R.swift
だと:R.string.localizable.hoge
SwiftGen
だと:L10n.hoge
- 設定する側を
R.swift
だとスネークケースで書くとスネークケースのままで生成されてしまうが、SwiftGen
だとスネークケースで書いたものをキャメルケースで生成してくれる- 末尾に()がつくかどうか
- コードを書いている時の保管として
()なし
、()
、(Void)
の3つの選択肢が出てくるのでめんどいの4つが主なものです。
僕は、特に中2つを利用したいがためにSwiftGenを採用しました。特にlocalizeStringの長さの話はすごく重要だと思っています。
良くあるケースとして{ViewControllerの名前}.{UIViewの名前}.{enumの名前}.title
とかを指定した場合は可読性が下がりやすいのでなるべく呼び出すときのデフォルトのクラス名は少ない方が嬉しいです。
また、スネークケース、キャメルケースの話も、長く運用する上では大事だと思っています。Localizable.Strings
でキャメルケースを書くのは自分はあまり好みには思えませんでした。
これらは実際にどっちも運用したことがないと気づけない話なので参考になれば嬉しいです。一番最初に言った、どちらを導入するか迷っている方でSwiftGenを導入することを大きなコストと捉えていない方はSwiftGenを導入するべきはこれらが理由です。
R.swift
は導入しやすい分、で細かいところに届きにくい設計になってしまいます。長期的な運用を考えるとそこのリスクはとるのは難しいかもしれません。逆に言えば、これらをメリットと感じないのであれば
R.swift
を使う方がいいと言えます。どっちを選ぶべきなのか
最終的には当たり前ですが、好きな方を選べばいいという話に落ち着きます。しかし、この記事を見ている方は長期運用を想定した上で文字列管理をしようとしている方が多いのではないでしょうか。めんどくさい作業が発生するので安易に乗り換えることはできないので将来性を加味して選ぶ必要があると思います(経験談)
参考資料