- 投稿日:2019-12-05T23:48:04+09:00
今更MVP勉強したら2種類あると言われた。
はじめに
AdeventCalendar5日目です。
ちょっとボリューム少ないので休憩の片手間にでもどーぞ。
土日頑張るので許してください。
今回はiOSのMVPです。MVPとは
画面処理とプレゼンテーションロジックを分離したアーキテクチャです。
主にModel,View,Presenterに分かれています。
Model
データを持ったりします。API叩いたりデータベースと繋いだりするのがここです。
MVCのModelと一緒です。
Presenter
ViewとModelの仲介をする。
その名の通りプレゼンテーションロジックを担当する。
例えばViewでボタンが押されたとしたらそれに応じてPresenterがModelからデータ持ってきてViewに返したりなど。
View
viewは画面表示を担当したりユーザーからのアクション(ボタン押したとか)をPresenterに伝えます。
viewControllerとかstoryboradとかがここに入ります。2種類ある
どうやら2種類あるというのはフロー同期かオブザーバー同期かということらしいです。
フロー同期の方のMVPの流れ
フロー同期の方の流れは下の画像のような感じで
フロー同期の方だと毎回Presenterを通す必要はありますがViewが画面処理の実装しか必要なく、主な処理はPresenterに丸投げするためPresenterのテストとかがしやすくなるらしいです。
オブザーバー同期の方
オブザーバー同期だとModelのデータがアップデートされるとそれをViewに通知しView側でそのデータをよしなに使って表示し直すみたいな流れがあるのがフロー同期とは違う点です。
なのでデータの流れが追いやすいなどのメリットがあります。
最後に
今回はとりあえずMVPは細かく分けると2種類あるということとそれぞれメリットがあるので場合によって使い分ける必要があるということが知れました。
正直ざっくりしか書けていないので説明不足感はありますがもっと知りたい方は調べてみてください。色々奥が深いです。
明日か明後日にMVP使ったアプリの記事出すのでコードの方はそちらをみてイメージつかんでください。
まあちょっと忙しくて今日はそんなに質が高くありませんでした。次回に期待してください。
もっと言い訳したいのですが「※言い訳していいわけあるか」というダジャレが頭をよぎるので言い訳はこれくらいにしておきます。※言い訳していいわけあるか・・・週刊少年ジャンプに連載していたあの日本が誇る超有名漫画NARUTO疾風伝575話で3代目土影オオノキがうちはマダラと戦っているときに口にしたセリフである。
- 投稿日:2019-12-05T22:08:42+09:00
ARKitについて まとめ
はじめに
こんにちは!
ARアドベントカレンダー1日目です。(いきなり忘れてました。すみません。。。)
カレンダーのいくつか日数が空いていたので、ARKitについては分けて記載していこうと思います。まず今回は、ARKitの概要についてまとめます。
ARCoreとの比較や技術調査、これからARKitをはじめてみようと思う人向けの記事です。
とりあえず試してみたいと言う方は こちらの記事(ARKitのはじめかた その1)をどうぞ。ARKitとは
・Apple社が提供するAppleデバイス用のAR開発ツール
・カメラ越しの画面で、現実に即した3D表現可能にする
・2017年にARKit(ARKit1)が発表され、2019年現在はARKit3がリリースこれまでのバージョンと機能は、
Version 機能 2017.09 ARKit(ARKit 1.0) ポジショントラッキング, 平面検出(床), ポイントクラウド(3次元の点座標情報)取得, 周囲の明るさ推定, 現実空間への当たり判定(HitTest), 現実空間とのスケール一致, 顔認識 2018.01 ARKit 1.5 マーカー検出, マーカートラッキング(追跡), 平面検出(垂直) 2018.09 ARKit 2.0 マーカートラッキング(2D複数・3D), 取得した情報の再現(エクスポート/インポート), 現実空間の位置との関連付け, 即時ユーザー共有, 顔認識(視線・舌) 2019.09 ARKit 3.0 モーションキャプチャ, ピープルオクルージョン, 両面同時AR利用, ARKit2までの機能向上, (RealityKit・Reality Composer) バージョンが上がることに、より多くの現実の情報を判断出来るようになっており、ARKit2からは他端末との共有や再現が出来るようになりました。(AR=拡張現実と言う意味では、多くの人と認識出来るものこそ現実と言えるので重要なアップデートでした。)
また、今年発表されたARKit3では人の判定が出来るようになってきたので、より現実に合わせた表現が可能になっています。ただし、基本的には現実の何かの情報を検知する為のツールなので、
検知した後の表現には SceneKit, SpriteKit, MetalKitなど 別のツールとの組み合わせが必要です。
※2019発表のRealityKitは表現にも一部対応しています。仕組み(検知する方法)
ARKitが "どのように"動いているか
カメラを使った視覚型ARでは「カメラに映ったものを判定すること」と、
「現実の位置とカメラ上の仮想オブジェクトを適合して表現すること」が重要ですが、
画像の解析だけではなく 端末位置との差分を割り出し、画面に反映させ続けるには高い計算機能が必要となります。
※ARKitは、60FPSでカメラからの画像を取得し処理しています。ARKitでは これを実現する為に、SLAM(a)とVIO(b)を複合した「vSLAM」という技術が利用されています。
SLAM(Simultaneous Localization and Mapping)と VIO(visual inertial odometry)については、去年のk-boyさんの記事がとてもわかりやすいです。
Wikitudeや6d.aiといったSLAMベースのARツールは他にもあるのですが、
vSLAMを利用することで、比較的古めの端末(iPhone6s等)でも高品質なARを利用出来ています。
※ARKit発表時iPhone6sはまだ新しめの端末でしたが6D.AIを使ってる時の画面はこんな感じです。
SLAM(ジャイロセンサー無し)なのでバスでも位置がズレません。トラッキングモード
では、"何を検知するか"
ARKitが現実の情報を検知する為に、ARConfigurationという設定をします。
現在のトラッキングモードは、全7種類です。
ARConfiguration モードの説明 ARWorldTrackingConfigurationとの併用 ARWorldTrackingConfiguration 最も汎用的なモードです。ARKitがデバイスのバックカメラを使用して検出および追跡する可能性のある空間、人物、または既知の画像やオブジェクトに対するデバイスの位置と方向を追跡します。 - ARBodyTrackingConfiguration 人の動きを取得するのに相応しいモードです。デバイスの背面カメラを使用して、人物、平面、画像を追跡できます。 × AROrientationTrackingConfiguration バックカメラを使用してデバイスの向きのみを追跡します。 × ARImageTrackingConfiguration 動く画像の場所を取得し続けるのに相応しいモードです。デバイスのバックカメラを使用して既知の画像のみを追跡します。 × ARFaceTrackingConfiguration 顔の表情を取得するのに相応しいモードです。動きや表情など、デバイスのセルフィーカメラの顔のみを追跡します。 △ ARObjectScanningConfiguration 開発者用です。オブジェクト検知の準備に利用します。バックカメラを使用して、後で実行時にアプリに認識させる特定のオブジェクトに関する忠実度の高いデータを収集します。 × ARPositionalTrackingConfiguration 3D空間でのデバイスの位置のみを追跡します。 × 恐らく処理の問題から多くの機能が併用出来ませんが、
ARFaceTrackingConfigurationだけはARkit3より同時利用が可能になりました。
ただし、「両カメラの画像を同時に表示することは出来ない」「depthデータの取得は出来ない」という制限があります。
将来的に端末スペックが良くなれば多くの機能が併用可能になるでしょう。対応端末と性能
利用出来る端末には制限があります。
以下の端末は最新バージョンのARKit3(iOS13)を利用出来ます。
端末 chip(CPU) World Tracking Face Tracking Body Tracking People Occlusion iPhone 6s/6s Plus A9 ◯ - - - iPhone SE A9 ◯ - - - iPhone 7/7 Plus A10 ◯ - - - iPhone 8/8 Plus A10 ◯ - - - iPhone X A11 ◯ ◯ - - iPhone XS/XS Max A12 ◯ ◯ ◯ ◯ iPhone XR A12 ◯ ◯ ◯ ◯ iPhone 11 A13 ◯ ◯ ◯ ◯ iPhone 11 Pro/Pro Max A13 ◯ ◯ ◯ ◯ Face Trackingにはインカメラ(trueDepthCamera)が必要な為、iPhone X以上の端末。
高い計算量が必要な Body Tracking / People Occlusion には、A12以上のchipを搭載したiPhone XS以上が必要です。
また、トラッキングモードに変化はありませんが、iPhoneXRなど、単眼バックカメラの端末は視差によるDepthの取得が出来ません。よって深度を利用した環境取得は難しいです。(フロントでは取得出来ます。)まとめ
ARKitについての振り返りでした。
発表から2年しか経っていないわりにアップデートが多いのですが、
どんどん簡単になってるので始めるハードルは低くなりました。
次回は、ARKitの各バージョンで変化してきた実装方法について書こうと思います。以上です。ここまで読んで頂きありがとう御座いました!
次のアドベントカレンダーは、@KoukiNAGATA さんです。参考・関連記事
- 投稿日:2019-12-05T22:08:42+09:00
ARKitとは何か (ARKit1〜3のまとめ)
はじめに
こんにちは!
ARアドベントカレンダー1日目です。(いきなり忘れてました。すみません。。。)
カレンダーのいくつか日数が空いていたので、ARKitについては分けて記載していこうと思います。まず今回は、ARKitの概要についてまとめます。
ARCoreとの比較や技術調査、これからARKitをはじめてみようと思う人向けの記事です。
とりあえず試してみたいと言う方は こちらの記事(ARKitのはじめかた その1)をどうぞ。ARKitとは
今更ですが ARKitとは
・Apple社が提供するAppleデバイス用のAR開発ツール
・カメラ越しの画面で、現実に即した3D表現可能にする
・2017年にARKit(ARKit1)が発表され、2019年現在はARKit3がリリース です。これまでのバージョンと機能は、
Version 機能 2017.09 ARKit(ARKit 1.0) ポジショントラッキング, 平面検出(床), ポイントクラウド(3次元の点座標情報)取得, 周囲の明るさ推定, 現実空間への当たり判定(HitTest), 現実空間とのスケール一致, 顔認識 2018.01 ARKit 1.5 マーカー検出, マーカートラッキング(追跡), 平面検出(垂直) 2018.09 ARKit 2.0 マーカートラッキング(2D複数・3D), 取得した情報の再現(エクスポート/インポート), 現実空間の位置との関連付け, 即時ユーザー共有, 顔認識(視線・舌) 2019.09 ARKit 3.0 モーションキャプチャ, ピープルオクルージョン, 両面同時AR利用, ARKit2までの機能向上, (RealityKit・Reality Composer) バージョンが上がることに より多くの現実の情報を判断出来るようになっており、ARKit2から他端末との共有や再現が出来るようになりました。
また、今年発表されたARKit3では人の判定が出来るようになってきたので、より現実に合わせた表現が可能になっています。
AR=拡張現実と言う意味では多くの人と認識出来るものこそ現実と言えるので、重要なアップデートが毎年続いています。ただし、基本的には現実の何かの情報を検知する為のツールなので、
検知した後の表現には SceneKit, SpriteKit, MetalKitなど 別のツールとの組み合わせが必要です。
※2019発表のRealityKitは表現にも一部対応しています。
仕組み(検知する方法)
ARKitが "どのように"動いているか
カメラを使った視覚型ARでは「カメラに映ったものを判定すること」と、
「現実の位置とカメラ上の仮想オブジェクトを適合して表現すること」が重要ですが、
画像の解析だけではなく 端末位置との差分を割り出し、画面に反映させ続けるには高い計算機能が必要となります。
※ARKitは、60FPSでカメラからの画像を取得し処理しています。
ARKitでは これを実現する為に、SLAM(a)とVIO(b)を複合した「vSLAM」という技術が利用されています。
SLAM(Simultaneous Localization and Mapping)と VIO(visual inertial odometry)については、去年のk-boyさんの記事がとてもわかりやすいです。
Wikitudeや6d.aiといったSLAMベースのARツールは他にもあるのですが、
vSLAMを利用することで、比較的古めの端末(2015発売のiPhone6s等)でも高品質なARを利用出来ています。因みに6D.AIを使ってる時の画面はこんな感じです。
SLAM(ジャイロセンサー無し)なので 移動中のバスでも位置がズレませんでした。トラッキングモード
では、"何を検知するか"
ARKitでは現実の情報を検知する為に、ARConfigurationという設定をします。
現在の設定は、全7種類です。
ARConfiguration 設定の説明 備考 ARWorldTrackingConfiguration 最も汎用的なモードです。ARKitがデバイスのバックカメラを使用して検出および追跡する可能性のある空間、人物、または既知の画像やオブジェクトに対するデバイスの位置と方向を追跡します。 空間検知出来ます。 ARBodyTrackingConfiguration 人の動きを取得するのに相応しいモードです。デバイスの背面カメラを使用して、人物、平面、画像を追跡できます。 2Dと3Dのパターンがありますが、2Dの場合はARWorldTrackingConfigurationと併用出来ます。 AROrientationTrackingConfiguration バックカメラを使用してデバイスの向きのみを追跡します。 デバイスの向きはARWorldTrackingConfigurationでも適宜取得可能です。 ARImageTrackingConfiguration 動く画像の場所を取得し続けるのに相応しいモードです。デバイスのバックカメラを使用して既知の画像のみを追跡します。 理論値以下ですが、同時追跡は4,5枚が限界かもといった所感です。 ARFaceTrackingConfiguration 顔の表情を取得するのに相応しいモードです。動きや表情など、デバイスのセルフィーカメラの顔のみを追跡します。 ARWorldTrackingConfigurationとの併用可能です。 ARObjectScanningConfiguration 開発者用です。オブジェクト検知の準備に利用します。バックカメラを使用して、後で実行時にアプリに認識させる特定のオブジェクトに関する忠実度の高いデータを収集します。 精度は微妙です。 ARPositionalTrackingConfiguration 3D空間でのデバイスの位置のみを追跡します。 デバイスの位置はARWorldTrackingConfigurationでも適宜取得可能です。 ARFaceTrackingConfigurationはARKit3より同時利用が可能になりましたが、「両カメラの画像を同時に表示することは出来ない」「depthデータの取得は出来ない」という制限があります。
将来的に端末スペックが良くなれば多くの機能が併用可能になるでしょう。対応端末と性能
利用出来る端末には制限があります。
以下の端末は最新バージョンのARKit3(iOS13)を利用出来ます。
端末 chip(CPU) World Tracking Face Tracking Body Tracking People Occlusion iPhone 6s/6s Plus A9 ◯ - - - iPhone SE A9 ◯ - - - iPhone 7/7 Plus A10 ◯ - - - iPhone 8/8 Plus A11 ◯ - - - iPhone X A11 ◯ ◯ - - iPhone XS/XS Max A12 ◯ ◯ ◯ ◯ iPhone XR A12 ◯ ◯ ◯ ◯ iPhone 11 A13 ◯ ◯ ◯ ◯ iPhone 11 Pro/Pro Max A13 ◯ ◯ ◯ ◯ Face Trackingにはインカメラ(trueDepthCamera)が必要な為、iPhone X以上の端末。
高い計算量が必要な Body Tracking / People Occlusion には、A12以上のchipを搭載したiPhone XS以上が必要です。
また、トラッキングモードに変化はありませんが、iPhoneXRなど、単眼バックカメラの端末は視差によるDepthの取得が出来ません。よって深度を利用した環境取得は難しいです。(フロントでは取得出来ます。)まとめ
ARKitについての振り返りでした。
発表から2年しか経っていないわりにアップデートが多いのですが、
どんどん簡単になってるので始めるハードルは低くなりました。
次回は、ARKitの各バージョンで変化してきた実装方法について書こうと思います。以上です。ここまで読んで頂きありがとう御座いました!
次のアドベントカレンダーは、@KoukiNAGATA さんです。参考・関連記事
- 投稿日:2019-12-05T21:54:19+09:00
Swift Network.framework Study 20191205「Server TCP」
Study
Network.framework
Study:Server側環境
Client:Java、NetBeans
Server:Swift、XcodeClient Source Java
package example.java.network; import java.io.PrintWriter; import java.net.Socket; public class ExampleClientSocket { public static void main(String[] args) { try(Socket socket = new Socket("localhost", 7777); PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);) { printWriter.println("Example Send Data"); } catch(Exception e) { System.out.println(e); } } }Server Source Swift
NWListerを使用する
コネクション確立後のNWConnectionに受信処理登録、開始実施main.swift
import Foundation import Network var networkServer = NetworkServer() networkServer.startListener() while networkServer.running { sleep(1) }NetworkServer.swift
import Foundation import Network class NetworkServer { public var running = true func startListener() { let myQueue = DispatchQueue(label: "ExampleNetwork") do { let nWListener = try NWListener(using: .tcp, on: 7777) nWListener.newConnectionHandler = { (newConnection) in print("New Connection!!") newConnection.receiveMessage(completion: { (data, context, flag, error) in print("receiveMessage") let receiveData = [UInt8](data!) print(receiveData) }) newConnection.start(queue: myQueue) } nWListener.start(queue: myQueue) print("start") } catch { print(error) } } }
- 投稿日:2019-12-05T20:58:36+09:00
iOS13のセルのclipsToBoundsにつまずいた
はじめに
iOS13 にアップデートすると下記のような画面に遭遇。
しんぐるとんが見切れてる
対応
Accessory
にDisclosure Indicator
に設定しているのでcontentView
の範囲は下の青い範囲。とりあえずclipsToBounds
があやしい。デフォルトでセルの
contentView
のclipsToBounds
がtrue
になってるのでfalse
に設定。いけた
調査
とりあえず表示できましたが一応調査。
iOS13 以上でなる模様。
iOS12.4.1 iOS13.0 そもそもなんで iOS13 未満は
clipsToBounds
がtrue
なのに表示されるんだ??調べてみると下記がヒットした。(公式はみつからなかった...)
詳しくはわかりませんが iOS7.1 の頃から
UITableViewCell
のcontentView
はclipsToBounds
がtrue
に設定されていてもfalse
になるらしい。検証
iOS12.4.1 と iOS13 で
UITableViewCell
のcontentView.clipsToBounds
がどうなってるか調べてみました。実装はこんな感じ
ViewController.swiftoverride func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TestTableViewCell return cell }TestTableViewCell.swiftoverride func awakeFromNib() { super.awakeFromNib() print("awakeFromNib clipsToBounds: \(contentView.clipsToBounds)") } override func layoutSubviews() { print("layoutSubviews clipsToBounds: \(contentView.clipsToBounds)") }iOS12.4.1 の結果
awakeFromNib clipsToBounds: true layoutSubviews clipsToBounds: falseiOS13 の結果
awakeFromNib clipsToBounds: true layoutSubviews clipsToBounds: true layoutSubviews clipsToBounds: trueなんか2回呼ばれたけどとりあえず
clipsToBounds
がtrue
のままになってるさいごに
clipsToBounds
をfalse
にすることで対応しましたが、これでいいのかは不明...今のとこ iOS12 でも iOS13 でもおかしな表示にはなっていません。そもそも
View
からはみ出したレイアウトってどうなんだっていうのはありますが
- 投稿日:2019-12-05T20:20:55+09:00
StoryboardReferenceを用いて実装したTabBarControllerをカスタマイズする時の注意点
状況
Youtubeチャンネルの素材用にTinderの模擬アプリを作成する際、
1つのStoryboard
でTabBarController
と遷移先のVCを置く場合
カスタマイズは
Storyboard
上でそのままやってしまうTabBarController
かTabBar
のcustomclassを作って紐づけるのどちらかだと思うのですが
StoryboardReference
を用いてTabBarController
を実装した際
これだとうまくいかなかったのでメモしておきます参考
1つの
Storyboard
でTabBarController
と遷移先のVCを置く場合の参考文献です
・tabBarControllerとUINavigationControllerを同時に使いたい! - Takahiro Octopress Blog
・NavigationController とTabBarControllerを一緒に使う方法 - marikoootaの日記
・【Swift4】StoryboardでUINavigationControllerやUITabBarControllerを設定する方法【Xcode9】 | ニートに憧れるプログラム日記
・iOSアプリ開発入門#3 ~UITabBarController~ - Qiita対策
StoryboardReference
を用いてTabBarController
を実装した場合tabBarをカスタマイズ
TabBarController
のTabBar
をStoryboard
で直接編集する
->TabBar
表示されない
Storyboard Reference
で紐づいている遷移先のVCでカスタマイズしてみる
->tabBarController, tabBarのカスタムクラス作ってtabController.storyboard
のそれに紐づければおそらくいける
->VCとtabbarのstoryboardが別だから見た目変化なし
tabBarItem
でcustomclass作る
->tagで仕分けて実装したらカスタマイズできた
->見た目に関しては各VCで設置したtabBarItemしか反応しないのかもimport UIKit class StyledTabBarItem: UITabBarItem { //storyboardで設置した時 required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! setAppearance() } private func setAppearance() { //ライフサイクル内ならselfでVCから参照すれば使わなくてもいける->NCのみ //tabBarの画像は30*30px(Retinaだと60*60) switch self.tag { case 0: self.setTabBarItem(title: "", image: UIImage(named: "girlTabIcon")!, selectedImage: UIImage(named: "girlTabIcon-selected")!) case 1: self.setTabBarItem(title: "", image: UIImage(named: "emperorTabIcon")!, selectedImage: UIImage(named: "emperorTabIcon-selected")!) default: break } } func setTabBarItem(title: String, image: UIImage, selectedImage: UIImage) -> Void { self.title = title self.image = image//.withRenderingMode(.alwaysOriginal)<-PDFの時はこの設定いらない self.selectedImage = selectedImage//.withRenderingMode(.alwaysOriginal)<-PDFの時はこの設定いらない } }アイコン画像の設定
- KeynoteとかAdobeXDで適当にアイコン用に正方形の画像(300*300pxとか)作って設定したらむちゃくちゃはみ出した
->30*30pxの画像を用意したらできた
->アイコン画像にPDFを使うと
single scale
かつPreserve Vector data
にチェックで
30*30pxのデータのみでretina(60*60px)
にも対応できる
->しかしこれだとtabBar.tintColor = UIColor.pink
,tabBar.unselectedItemTintColor = UIColor.gray
などが使えないので
選択時と非選択時のデータを両方用意する必要がある
->後述のwithRenderingMode(.alwaysOriginal)
を使えば選択時の画像のみで良い
- アイコン画像のカスタマイズ時の注意点(PDFとPNGの違い)
->PDFだと用意するサイズは1種類でいい(↑の設定でretinaサイズにも対応できる)
->withRenderingMode(.alwaysOriginal)
を使えばPDFでも選択時の画像のみで良い//PNGのみ tabBar.tintColor = UIColor.pink tabBar.unselectedItemTintColor = UIColor.grayまたは
//PNGでもPDFでも使える tabBarItem.selectedImage = selectedImage.withRenderingMode(.alwaysOriginal) //↑これしないとtemplate扱いになって勝手に他の色に塗りつぶされる tabBar.image = image //↑非選択時で勝手にグレーになってもいいならこっち(あるいは.withRenderingMode(.alwaysTemplate))tabbarの上線消す方法
これに関しては
tabBarItem
からカスタマイズした影響でうまくやるやり方がわかりませんでした
->tabBar.shadowImage = UIImage
というやり方をしたいけど
tabBar
にアクセスしてカスタマイズする ortabBar(Controller)
からtabBarItem
をカスタマイズする
方法が分かっていないから別の方法
storyboardReference
側からtabBarItem
を編集する->
tabbBarControlller
やtabBar
の変更が反映されないのは
遷移先でtabbarItem
を新たに作っているせいかもしれない
->遷移先VCのtabBarItem
を消去 &storyboardReference
のtabBarItem
をcustom class
に準拠
->tabBar
が表示されない
tabBarItem
をNCではなくVCに移動する->VC側で
self.tabBarController.tabBar.shadowImage = UIImage()
->VCに移動しても影響はなかったが変更が反映されなかった参考
・【Swift4】Storyboardを分割してプロジェクトを作成する方法【iOS11】【Xcode9】 | ニートに憧れるプログラム日記
・【Xcode】Storyboard Referenceを活用してみた - Qiita・Storyboard Reference がいい感じ - SH Lab の アプリ開発部屋
->遷移先のVCをそれぞれのStoryboard
にする場合と遷移先のVCのみ1つのStoryboard
にまとめてしまう(TabBarController
は別)場合別のやり方が書いてある・UITabBarControllerをStoryboardで設定する時にTabBarItemが表示されない - Qiita
->本記事でも書いたようにTabBarController
と遷移先VCを分けた場合はTabBarItem
を遷移先VCにそれぞれ加えないとダメだと書いてある
・iOS Swiftチートシート・ライブラリまとめ UITabBar・UITabBarItem編 - Qiita
->tabBar.unselectedItemTintColor = UIColor.white
など
・iOS - UITabbarの中央アイコンをタブバーをはみ出したデザインにしたい|teratail
->HELargeCenterTabBarControllerの紹介
・【Swift5】UITabBarの色、背景画像、サイズやアイコン文字の色、フォント、サイズ、位置の調整まで網羅フルカスタマイズまとめ【Objective-C】 | ニートに憧れるプログラム日記
・UINavigationBar・UITabBar のカスタマイズ方法について - Qiita
->カスタマイズのTips.(appearance()
なくてもいけるからこれの存在意義はよくわからない。。。)
・UITabBarControllerの基本
->storyboardReference側からtabbarItemを編集するやり方が書いてある・[Swift]UIImageのレンダリングモードまとめ - Qiita(rederingModerでのやり方)
・[Swift] iOS で画像の色を動的に変える - Qiita(rederingModerでのやり方)
・[Swift] 画像の色を変更する - Qiita(tintColorでのやり方)
・[Swift] iOS で画像の色を動的に変える - Qiita(rederingModeでのやり方)
->今回のようにPDFではなくPNGでtabbarのアイコン作った場合は必要。
・【Swift】 アセット画像を3種類も書き出したくない - Qiita
->PDFでのやり方はこっち。
・【iOS】Tabbar メニューの画像のサイズや解像度に関して - 東京伊勢海老通信
->tabbarの画像サイズは30*30でないとダメ。
- 投稿日:2019-12-05T18:30:00+09:00
UIView.animateにおけるEasingへの疑いを晴らす - そして信用へ -
こいつ、ほんまにEasingしてっか?
ってときありません?
僕はあったんですよ。
やから、今回。ほんまにEasingしてっか、ちゃんと確かめよう
ってなったわけです。実証
ついでやし、
- linear
- easeIn
- easeOut
- easeInOut
の4つで2パターン試してみることにします。
(とりあえず試すために、コードは各所はいぱー簡易的です)UIView.animateのoptionsに、各Easingが指定できまして、それぞれ
- linear:curveLinear
- easeIn:curveEaseIn
- easeOut:curveEaseOut
- easeInOut:curveEaseInOut
こんな感じです。
(勘の良い方はお気づきだと思いますが、optionsっていうわざわざ複数形になっているので、他のoptionも同時に設定することができます。その辺はキャッツアイ。)
パターンA:AutoLayoutで試す
@IBOutlet private weak var viewATopConstraint: NSLayoutConstraint! @IBOutlet private weak var viewBTopConstraint: NSLayoutConstraint! @IBOutlet private weak var viewCTopConstraint: NSLayoutConstraint! @IBOutlet private weak var viewDTopConstraint: NSLayoutConstraint! private func animateAutoLayout() { self.viewATopConstraint.constant = 300 UIView.animate(withDuration: 1.0, delay: 0, options: .curveLinear, animations: { self.view.layoutIfNeeded() }, completion: nil) self.viewBTopConstraint.constant = 300 UIView.animate(withDuration: 1.0, delay: 0, options: .curveEaseIn, animations: { self.view.layoutIfNeeded() }, completion: nil) self.viewCTopConstraint.constant = 300 UIView.animate(withDuration: 1.0, delay: 0, options: .curveEaseOut, animations: { self.view.layoutIfNeeded() }, completion: nil) self.viewDTopConstraint.constant = 300 UIView.animate(withDuration: 1.0, delay: 0, options: .curveEaseInOut, animations: { self.view.layoutIfNeeded() }, completion: nil) }これで動かすと
Easingしてんな!
パターンB:y位置調整で試す
@IBOutlet private weak var viewA: UIView! @IBOutlet private weak var viewB: UIView! @IBOutlet private weak var viewC: UIView! @IBOutlet private weak var viewD: UIView! private func animateFrame() { UIView.animate(withDuration: 1.0, delay: 0, options: .curveLinear, animations: { self.viewA.frame.origin.y += 300 }, completion: nil) UIView.animate(withDuration: 1.0, delay: 0, options: .curveEaseIn, animations: { self.viewB.frame.origin.y += 300 }, completion: nil) UIView.animate(withDuration: 1.0, delay: 0, options: .curveEaseOut, animations: { self.viewC.frame.origin.y += 300 }, completion: nil) UIView.animate(withDuration: 1.0, delay: 0, options: .curveEaseInOut, animations: { self.viewD.frame.origin.y += 300 }, completion: nil) }こいつはどうだろう
いや、Easingしてんな!
余談
これ余談なんですけど。
Easingしてるっていうのは、言葉的にはアレなんですが、今回カミ○リ風に仕上げるためにこういう表現をさせていただいています。おす。
(あくまで口調が風なだけです)
その辺を把握した上で、もう一回読んでいただけたらなるほど
ってなる。(ってくれたら嬉しいです。)
あと、おはぎはきなこ派です。結果
Easingしとりましたわ。
(いや、そりゃそうやろって話なのだが)
(まぁ、当たり前を当たり前だと思わない的なあれですよ)ちなみに、まぁこれも当たり前って言ってしまえばそこまでですが。
パターンAとBが違う動画であることを強調するためにわざわざViewの色を赤と青にしていますが、全く同じ動きをしていました。
(これが真の余談だったりもする、説)何が言いたいかというとですね、ええ
つまり、UIView.animateはちゃんとEasingしていて、同じ時間をかけてのアニメーションでも動き方はこんなに違ってくるから、使用箇所使用タイミングに応じて使い分けてあげましょうってことですね、ええ。
(ごまかした)みなさんも、この子のことは疑わずに信用して実装してあげましょう。
(M-1が近いということで、もれなくそっち方面の色が強めに出ていますが、多めに見てください)
- 投稿日:2019-12-05T16:24:33+09:00
忘備録-Swiftのパターン
趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。
参考文献
この記事は以下の書籍の情報を参考にして執筆しました。
switch文でwhere節を使う
func trashDate(weekly: Int, weekday: String){ let week = (weekly, weekday) // タプル型 switch week { case (_, "月曜日"): print("燃えるゴミの日") case (let i, "木曜日") where i == 1 || i == 3 : // where節の条件に一致する場合処理を実行 print("燃えないごみの日") case let (i, j): print("第\(i)週 \(j)") } } trashDate(weekly: 4, weekday: "月曜日") // 燃えるゴミの日 trashDate(weekly: 2, weekday: "木曜日") // 第2週 木曜日 trashDate(weekly: 1, weekday: "木曜日") // 燃えないごみの日 trashDate(weekly: 99, weekday: "hoge") // 第99週 hogeswitch文でオプショナル型を使う
func ageConfirmation(people: (String, Int?)) { switch people { case let (name,age?) where age >= 20: print("\(name),成人済み") case let (name,age?) where age < 20 && age >= 0: print("\(name),未成年") case (let name, nil) : print("\(name),年齢不詳") default: break } } ageConfirmation(people: ("hoge", 20)) //hoge,成人済み ageConfirmation(people: ("fuga", 8)) //fuga,未成年 ageConfirmation(people: ("foo", nil)) //foo,年齢不詳 ageConfirmation(people: ("bar", -8))列挙型のメソッド
enum Coin { case front, back func reverse() -> Coin{ switch self { case .front: return .back case .back: return .front } } } let coin = Coin.back print(coin) // back print(coin.reverse()) // front値型の列挙型
enum Coin: Int { case front = 0, back //backは1になる func reverse() -> Coin{ switch self { case .front: return .back case .back: return .front } } } let coin = Coin.back print(coin) // back print(coin.rawValue) // 1 print(coin.reverse().rawValue) //0列挙型のケースを全て含むコレクション
enum Coin: Int, CaseIterable { case front = 0, back //backは1になる } print(Coin.allCases) // [CodeTest.Coin.front, CodeTest.Coin.back] for elm in Coin.allCases{ print(elm) // front back }if-case文
let p = Payment.クレジット(120, true) // pがクレジットカードかを知りたい if p == .クレジット{ // error print("クレカ") } // switch文でも書けるが少し面倒 switch p { case .クレジット: print("クレカ") default: break } // if-caseを使う if case .クレジット = p { print("クレカ") }for-in文でcaseパターン
enum Payment { case クレジット(_ 残高:Int, _ 使用可能: Bool) case アプリ(_ 残高: Int, _ 還元率: Double = 0.02) case 現金 } let paylist: [Payment] = [.クレジット(120, true), .アプリ(1120), .現金, .クレジット(12000, true), .クレジット(99999, false), .クレジット(1200, true)] // 1100円以上の残高があり、使用可能なクレジットカードを取り出す for case let .クレジット(money, b) in paylist where b == true && money >= 1100 { print(money) } // このように書くのと同じ for t in paylist { switch t { case let .クレジット(money, b): if b == true && money >= 1100{ print(money) } default: break } }再帰的な列挙型
// 再帰的に呼び出すcaseの前にindirectをつける enum メッセージ : CustomStringConvertible{ // プロトコルを採用 case 文書(_ 差出人: String, _ 文章: String) case 添付(_ 差出人: String, _ データ: String) indirect case 転送(_ 差出人: String, メッセージ) var description: String{ switch self { case let .文書(from, str): return from + " : " + str case let .添付(from, data): return from + " : " + data case let .転送(from, message): return from + " : " + " <- \(message)" } } } // XcodeでFIXしたとき indirect enum メッセージ : CustomStringConvertible{ // プロトコルを採用 case 文書(_ 差出人: String, _ 文章: String) case 添付(_ 差出人: String, _ データ: String) case 転送(_ 差出人: String, メッセージ) var description: String{ switch self { case let .文書(from, str): return from + " : " + str case let .添付(from, data): return from + " : " + data case let .転送(from, message): return from + " : " + " <- \(message)" } } } let m1 = メッセージ.文書("hoge", "Hello") let m2 = メッセージ.転送("fuga", m1) let m3 = メッセージ.転送("foo", m2) print(m1) // hoge : Hello print(m2) // fuga : <- hoge : Hello print(m3) // foo : <- fuga : <- hoge : Helloパターンマッチ演算子
Swiftのパターンマッチ :
スクリプト言語などが持っている文字列と正規表現のマッチングではない。
どの構造が何に対応するかに関してのルールに基づくデータ構造の対応づけ~= : パターンマッチ演算子
let hoge = Coin.front print(hoge ~= Coin.front) // trueパターンマッチ演算子のルールを追加
ただし、パターンマッチ演算子の新たな定義を追加すると、switchやif-case文の挙動に直接影響するので必要性について考えること。enum Coin: Int { case front, back func reverse() -> Coin{ switch self { case .front: return .back case .back: return .front } } } // パターンマッチ演算子の定義を追加 func ~= (c: Coin, s: String) -> Bool{ "\(c)" == s } let coin = Coin.front print(coin ~= "front") // true print(coin.reverse() ~= "front") // false
- 投稿日:2019-12-05T16:02:24+09:00
逆ポーランド記法のアルゴリズム(Xcodeでswiftを使った電卓アプリ作り)No.1 備忘録
swiftでの逆ポーランド記法を調べても出てこなかったため投稿します。
初心者のため間違っている箇所があればコメントしていただけたら幸いです。環境
Xcode Version 11.0
swift Version 5.0.1このコードでは括弧、記号の重複、小数点(Double)に対応しています。
また、オートレイアウトやtagの付け方、アプリのアイコンの付け方、launchscreenに画像をつける方法など上げていこうかと思います。
まだ未完成で 0.1+0.1+0.1 などの問題が山積みですが参考にしていただけたら幸いです。
calc.swift// // ViewController.swift // calc_test // // Created by USER on 2019/11/19. // Copyright © 2019 Akidon. All rights reserved. // /* 数式 ⇒ 逆ポーランド記法(Reverse Polish Notation) "5 + 4 - 3" ⇒ "5 4 3 - +" "5 + 4 * 3 + 2 / 6" ⇒ "5 4 3 * 2 6 / + +" "(1 + 4) * (3 + 7) / 5" ⇒ "1 4 + 3 7 + 5 * /" OR "1 4 + 3 7 + * 5 /" "T ( 5 + 2 )" ⇒ "5 2 + T" "1000 + 5%" ⇒ "1000 * (100 + 5) / 100" <<1000の5%増:税込み>> シャープ式 "1000 - 5%" ⇒ "1000 * 100 / (100 + 5)" <<1000の5%減:税抜き>> シャープ式 "1000 * √2" ⇒ "1000 * (√2)" ⇒ "1000 1.4142 *" <<ルート対応 */ //参考フローチャート https://images.app.goo.gl/chKZHfKrDXzptg5i6 // 数式 ⇒ 逆ポーランド記法(Reverse Polish Notation) ⇒ 答え import UIKit class ViewController: UIViewController { var formulas:[String] = [] //計算式 例えとして "1+2-3*4/5" を使用 //formulas = ["1","+","2","-","3","*","4","/","5"] var stacks:[String] = [] //逆ポーランドへ変換する際に一時的に四則記号を保持する //stacks = ["+","-","*","/"] ##最終的には空のリストとなる var buffa:[String] = [] //逆ポーランドへ変換した式を保持 //buffa = ["1","2","3","4","*","5","/","-","+"] var num:String = "" //formulas[num]を保持 var formulas_num:String = "" //formulasへ追加する前に数字を組み合わせるための変数 //例えば formulas_num = "1" + "0" → formulas_num = "10" var only_formulas:String = "" //式を画面に(Fotmulas.text)に計算式として表示 それ以外の機能は持たない var signal_TF = true //記号の重複を防ぐ //--numが数値かどうかを判断する------------------------------------ //*引用元 https://teratail.com/questions/54252 func isOnlyNumber(_ str:String) -> Bool { let predicate = NSPredicate(format: "SELF MATCHES '\\\\d+'") return predicate.evaluate(with: str) } //--数式 ⇒ 逆ポーランド記法(Reverse Polish Notation) へ変換-------- func Formula_To_Polish(){ for i in 0..<formulas.count{ //formulas = ["1","+","2"] のリストの要素数分forで回す num = formulas[i] if isOnlyNumber(num) { // numが数字ならばbuffaに追加する buffa.append(num) }else if num == ")"{ // numが ”)” かどうかを判断 for _ in 0 ..< stacks.firstIndex(of: "(")!{ buffa.append(stacks[0]) stacks.removeFirst() } }else if num == "("{ stacks.insert("(", at:0) //stacksのIndex = 0 に追加 }else{ //--------------stacksが空になるまでループする。------------------ while true{ if stacks.isEmpty{ //stacksが空になったらbreak stacks.insert(num, at: 0) break }else{ //四則演算の優先順位を検査する。 "/" > "*" > "+" = "-" > "(" if num == "+" || num == "-"{ //stacksの一番上にある記号をbuffaに追加する。 if stacks[0] == "(" { //num == ")"の時にstacks内の "(" が使用される stacks.insert(num, at: 0) break }else{ //numよりstacksの最上位ある記号の方が優先順位が高い buffa.append(stacks[0]) stacks.removeFirst() } }else if num == "*"{ if stacks[0] == "/"{ // "/" > "*" buffa.append(stacks[0]) stacks.removeFirst() }else{ //stacks[0] == "*" or "+" or "-" の時 stacks.insert(num, at: 0) break } }else{ //numが"/"の時ここを通る。 stacks.insert(num, at: 0) break } } // stacksは積み木のように上からnumを入れていき、上から取り出していく } //--------------ループ区間ここまで---------------------- } }//for i in 0..<formulas.count 終わり for i in 0 ..< stacks.count{ if stacks[i] == "(" { // }else{ buffa.append(stacks[i]) } } }//func Formula_To_Polish() 終わり //--逆ポーランド記法(Reverse Polish Notation) ⇒ 答え へ変換--------------- func Polish_To_Answer(){ stacks.removeAll() for i in 0 ..< buffa.count{ //buffa=["1","2","+"]のリストの要素数分forで回す if buffa[i] == "+" || buffa[i] == "-" || buffa[i] == "*" || buffa[i] == "/"{ switch buffa[i]{ //ifより綺麗にコードを書ける case "+": num = String(Double(stacks[1])! + Double(stacks[0])!) case "-": num = String(Double(stacks[1])! - Double(stacks[0])!) case "*": num = String(Double(stacks[1])! * Double(stacks[0])!) case "/": num = String(Double(stacks[1])! / Double(stacks[0])!) default: break } stacks.remove(at:1) //計算で使用した値を消去 stacks.remove(at:0) stacks.insert(num, at: 0)//計算後の値を追加 }else{ //数値はここを通る stacks.insert(buffa[i], at: 0) } } Ans.text = stacks[0] //答えを画面に表示 } // -数値を入力-------------------------------------------------------- // "1" = tag 1 , "2" = tag 2 , ....., "0" = tag 0 @IBAction func Button(_ sender: UIButton) { if formulas_num == String(sender.tag) { //"01"などを防ぐ }else{ //"12340"など数字の組み合わせに対応する formulas_num += String(sender.tag) only_formulas += String(sender.tag) //計算式を表示するだけ Formula.text = only_formulas signal_TF = true //新しい四則演算記号を入力できるようにする } } // -四則演算記号を入力-------------------------------------------------- // "+" = tag 100 , "-" = tag 101 , "*" = tag 102 , "/" = tag 103 @IBAction func Signal(_ sender: UIButton) { if signal_TF == true{ //記号の重複に対応 true = 新しい記号を追加できる、false = 追加済み signal_TF = false if formulas.last == ")"{ }else{ formulas.append(formulas_num) //数値を追加 formulas_num = "" //次に入力される数値を初期値に戻す } }else{ formulas.removeLast() //前追加した演算記号を消去 } switch String(sender.tag){ case "100": only_formulas += "+" formulas.append("+") case "101": only_formulas += "-" formulas.append("-") case "102": only_formulas += "*" formulas.append("*") case "103": only_formulas += "/" formulas.append("/") default: break } Formula.text = only_formulas //式を表示 } // -括弧記号を入力---------------------------------------------------- // "(" = tag 104 , ")" = tag 105 @IBAction func parentheses(_ sender: UIButton) { if formulas_num == ""{ }else{// (2+3) の時 3 を formulasに追加するために通る formulas.append(formulas_num) formulas_num = "" } switch String(sender.tag){ case "104": only_formulas += "(" formulas.append("(") case "105": only_formulas += ")" formulas.append(")") default: break } Formula.text = only_formulas //式を表示 } //--イコールを入力された時----------------------------- @IBAction func Equal(_ sender: Any) { if formulas_num == ""{ }else{ formulas.append(formulas_num) formulas_num = "" } Formula_To_Polish() //計算式から逆ポーランドへ変換する関数 Polish_To_Answer() //逆ポーランドから答えを表示する関数 //for i in 0 ..< buffa.count{ // formulas_num += buffa[i] //} } //--ACを入力された時--------------------------------- @IBAction func AllClear(_ sender: Any) { formulas.removeAll() stacks.removeAll() buffa.removeAll() num = "" formulas_num = "" only_formulas = "" Formula.text = "" Ans.text = "0" signal_TF = true } //--小数点を入力された時------------------------------ @IBAction func decimal_point(_ sender: Any){ if formulas_num.contains(".") == true{ //小数点の重複を防ぐ }else if formulas_num == ""{ formulas_num = "0." only_formulas += "0." }else{ formulas_num += "." only_formulas += "." } Formula.text = only_formulas } @IBOutlet weak var Formula: UILabel! @IBOutlet weak var Ans: UILabel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } }
- 投稿日:2019-12-05T13:20:43+09:00
R.swiftを使っていてビルドしたらビルドエラーが発生した話
こんにちは。私です。
最近はswiftを頑張ってます。
今回はライブラリ導入頑張ったのにR.swiftでビルドエラーが発生して泣いたお話です。R.swiftを導入した
先輩から「コード規約に沿って綺麗にソースコードを書けるよう『swiftlint』と『R.swift』を入れておいてね。」と言い渡された私。
まずライブラリ導入が苦手な私。
ライブラリ導入手順についてはこちらのGitHubを参照してください。
R.swift_GitHub
頑張りました。手順通りやったのにエラーが発生した!
エラー内容を確認すると『Run Script』に記載した内容でエラーが発生してました。
GitHubに倣ってRun Scriptに
"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"
を記載していたのですが、調べてみるとこちらの記法は古いようです。バージョンアップされていた!
バージョンアップでスクリプトの記法が変更されたようです。
R.swift_GitHubアップグレード
要約させていただくと
!スクリプトが変更されました
→"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/[YOUR_PATH]/R.generated.swift"
!Input Filesに以下を入れてください!
→$TEMP_DIR/rswift-lastrun
!Output Filesに以下を入れてください!
→$SRCROOT/[YOUR_PATH]/R.generated.swift
こちらを対応させるとビルドエラーが解消されました!
アップグレードで明示的な出力ファイル指定が必要になったようです!
[YOUR_PATH]には自身のプロジェクトに合わせて変更してください!さいごに
調べてもなかなか事象がなく、困りましたが同じ現象が起きている方の助けになれればと思います。
では皆様、綺麗で良いswiftライフを٩( 'ω' )و
- 投稿日:2019-12-05T11:51:51+09:00
String(describing:) で Any と Enum case を文字列へ
マッピンやデバッグやテストで使えそうなので、どんな状況使えそうかメモしときます。
ドキュメント
- String(describing:)
環境
この記事のコードは
- Xcode 11.2.1
で実行できます
id のタイプが揺れてる時
id のタイプが雑で取り扱われて、 String か Int になってりすることありますよね。
そしてそれに対してモバイルがわなんとかしたい時使えますlet id1: Any = "1" // String let id2: Any = 1 // IntString(describing:) を使えば中身間違えなく String 型に
let stringId1 = String(describing: id1) // "1" let stringId2 = String(describing: id2) // "1"で、こういうような書き方はいらなくなるでしょう
// value のタイプは Any if let value = value as? String { return value } else if let value = value as? Int { return String(value) } else { // ry }enum の名前をプリントアウトする
携わってるプロジェクトにこういうようなテストヘルパがあります。
// 定義された enum enum FruitType: Int { // Just put some random numbers over here case dragonFruit = 777 case apple = 988 }// テストヘルバ func createActivity(type: FruitType, raw: Int) { XCTContext.runActivity(named: "") { _ in XCTAssertEqual(raw, type.rawValue) } }// テストケース import XCTest @testable import YourTargetName class FruitTypeTests: XCTestCase { func testDefinitions() { XCTContext.runActivity(named: "test definitions of FruitType's cases") { _ in createActivity(type: .dragonFruit, raw: 777) createActivity(type: .apple, raw: 988) } } }テストヘルパのところ、 name は定義されてないから、テスト結果はこうなってよくわからないレポートになってしまいます:
rawValue をその name の引数に入れるのも可能なのですが、これはさすがにわかり辛いなのでは。という気持ちはありました。
どうしてもケース名表示したい!
改善したいためヘルパーをこうやって改造してみたら
func createActivity(type: FruitType, raw: Int) { let typeName = String(describing: type) XCTContext.runActivity(named: "test the raw value of .\(typeName) is \(raw)") { _ in XCTAssertEqual(raw, type.rawValue) } }テストレポートもこうなりました
締め
とりあえず二つを書いてみました!以上です。
おかしいところ、間違えたところありましたぜひコメント欄へよろしくお願いします。
最後
台湾産のドラゴンフルーツはめっちゃうまいよ!
- 投稿日:2019-12-05T11:09:27+09:00
RailsとSwiftで動画アップローダーを作る?
まえがき
知らぬ間にまたアドベントカレンダーの季節ですね?
今年もノリと勢いが前のめりすぎて立候補しましたがギリギリです。
(いつになったら一日は30時間になるのか。。)
(ちなみに去年こんなの書きました✌?10分クッキング!誰でもインフルエンサーになれるinstabotの作り方?)今年のテーマは、動画アップロード機能です。
画像は馴染みがあるけど動画はやったことないなんて人、多くないでしょうか?
そんな私を含めた方達に向けて、ゆるく解説していこうと思います◎?こんなの作ります
機能は、動画選択・プレビュー・アップロード・最新の動画をフェッチして再生です。たぶんレアな記事なので是非お付き合いください?
(あといいね欲しいですね。。!もう押してもらって大丈夫です???)お勧めしたい人
- Ruby(Rails)やったことあるよ!
- HTTPリクエストなんとなくわかるよ!
- RailsのAPIモードちょっと触ってみたいよ!
- モバイルアプリやってみたいよ!
- Swiftやってみたいよ!
- Swift初心者だよ!
- 動画アップロードやってみたいよ!
- というか開いてくれた人 みんなですね!
(せっかく一年に一回のアドベントカレンダーってお祭りなので、みんな読んでね!おねがいします!)
ようこそ〜〜〜?
使用技術
サーバーサイド(API)にRuby
フロントエンド(モバイル)にSwiftの構成です。Ruby 2.6.2
Ruby on Rails 5.2.3
Swift 5.0.1
Xcode 10.2.1
(ちょっと古いので上げます?)まだまだやれるぜって人へ
実際にアプリをストアにあげるのに、Railsプロジェクトのデプロイは必須なのでHerikuデプロイとか挑戦してみてください?
【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】いざ実装??
全体のザックリした流れ
- Swiftで動画アップローダーを作る
- SwiftでAPIクライアントを作る
- RailsでAPIサーバーを作る
- Swiftで動画プレイヤーを作る
~ 完成 ~
1. [Swift] 動画アップローダーの実装?
ザックリした流れ
- プロジェクトの作成
- シュミレーターにサンプル動画を追加
- 今回使用するファイルの説明
- 動画選択の機能を作成する
- 動画再生の機能を作成する
- 動画アップロードの機能を作成する
プロジェクトの作成
- Xcodeを起動する
- Create a new Xcode projectを選択する
- Single View Appを選択してNextを選択する
- Product Name(好きなアプリ名)を入力してNextを選択する
- 保存先を指定してCreateを選択する
これでSwiftプロジェクトの作成ができました!
シュミレーターにサンプル動画を追加
シュミレーターとは、書いたプログラムの動作を、mac内で確認するためのものです。
(Railsで動作確認する時、ブラウザにlocalhost:3000と入力して見る画面のようなものです。)シュミレーターには元々サンプル画像しか入っていないので、ここで動画を追加します。
まだ何もコードは書いていませんが起動(ビルド)してみましょう!
- 起動したいデバイス(自分はiPhoneXR)を指定して左上の再生ボタンを押します
![]()
- シュミレーターを起動したら真っ白な画面が出ますがメニューに戻り、写真アプリを開きます
- macのfinderから好きな動画を選択します(なければスマホなどからmacに送ってください)
- シュミレーターにドラッグアンドドロップで追加します
今回使用するファイルの説明
ここから実際に画面を作っていきますが、
先に今回使用するファイルについてザックリ説明しておきます。
Main.storyboard(自動生成・自作も可)
storyboardはUI部品(ボタンや画像表示するのに必要なパーツなど)を置いて、
直感的にレイアウトを作成するファイルです。
細かい機能やレイアウトを実装はこのファイルでは実装しきれません。あくまでレイアウトをザックリ構築するファイルです。ViewController.swift(自動生成・自作も可)
storyboardでカバーできない機能的な実装や細かいレイアウトを記述するファイルです。
例えば、ボタンが押された時の挙動やAPIから受け取ったデータをUIに受け渡す処理なんかを書きます。今回はVideoUploaderViewController.swiftを作成します。
APIClient.swift(自作)
バックエンド(APIサーバ)へのリクエストやレスポンスを受け取るのに使用します。Info.plist(自動生成・自作も可)
設定ファイルです。今回はここでフォトライブラリへのアクセス許可の設定をします。動画選択の機能を作成する
ここでやること
1. VideoUploaderViewController.swiftを作成する
2. Main.storyboardに動画選択用のボタンを追加する
3. ユーザーにカメラロールの使用許可をとる実装を追加する
4. ボタンをViewControllerに接続する
5. ボタンが押されたときにフォトライブラリを開いて動画を選択する
6. 選択された動画のサムネイルをViewControllerに表示する1. VideoUploaderViewController.swiftを作成する
基本的に、1ページを作るのにUI部品を配置するStoryboard(ViewController)と機能を実装するViewControllerがセットで必要です。
はじめに動画の選択からアップロードまでを行うページを作っていきます。
このページは
storyboardに自動生成されるMain.storyboard(ViewController)と、
ViewControllerは自動生成されるViewController.swiftをリネームして作成していきます。storyboardは自動生成なので、まずはViewController.swiftをリネームていきます。
Xcodeの左側にあるナビゲーターからプロジェクターナビゲーター
(左上のファイルアイコンを押すと出てきます。ファイルの一覧です)を選択し、
ViewControllerをクリックして名前をVideoUploaderViewController.swiftに変更してください。
次に、セットで使用するstoryboardに紐づいているViewController名を変更します。
左側のナビゲーターからMain.storyboardを選択します。ここにはViewControllerの白いパーツがあるだけかと思います。
この黄色いアイコンを選択してください。
右側のインスペクターで、Identity InspectorのCustom Classを変更します。
ここを先ほどのVideoUploaderViewController.swiftに変更すると接続が完了します。
2. Main.storyboardに動画選択用のボタンを追加する
Main.storyboardを開いているのでついでに動画選択のボタンを設置していきます。
これをドラッグアンドドロップでViewController内におきます。
青い淵の状態だとパーツが固定されていないのでUI部品に制約(縦横のサイズやトップからの距離などのルール)を追加していきます。
基本的に必要な制約は、縦横・座標(画面に対しての位置)です。width/height
widthを設定します。
Ctrを押しながらボタンを選択、ボタン内で選択をやめるとこのようなメニューが出てきます。
ここでwidthを選択してください。
赤くなっているのは無視してください。(ちゃんと制約が指定できたら青くなります!)
制約部分を選択すると右側にAttributes Inspectorが開きます。
ここでConstantを80にしてください。
同じようにして、heightのConstantを80で指定してください。
座標
次にボタンの座標を決めていきます。
画面のトップやボトムから距離を決めたり、画面の真ん中で指定したり方法は色々あります。
今回はトップからの距離と左からの距離を指定します。
先ほどと同じように、Ctrを押しながらボタンを選択、
今度はボタンの外までカーソルを引っ張り選択を解除します。
以下のメニューが出てくるのでTop Space to Safe Areaを選択してください。
これもConstantを548に変更してください。同じように画面の左からの制約を付けていきます。
メニューを開いたら、今度はLeading Space to Safe Areaを選択してください。
ここはConstantを71に指定してください。最後にbuttonという文字をクリックして、Selectに変更してください。
これでボタンの追加は終了です。(好きに色とか付けてもらったり、位置も自由で大丈夫です。)
(指定に中途半端な値を指定しているのは、デモで作ったアプリを
AutoLayoutで実装しているのですが、そのレイアウトに近づけるためです。(起動に使う端末はXRです。)
AutoLayoutとは、UI部品同士を相対的に配置して、どのサイズの端末でも同じような配置でレイアウトを実現できる機能です。
後でソースコードを載せるので参考にしてみてください??♀️)
3. ユーザーにカメラロールの使用許可をとる実装を追加する
カメラアプリをインストールした時にこのようなモーダルに遭遇することがあると思います。
ユーザーから、フォトライブラリにアクセスする許可を取らないと、
動画を選択することができないのでこれを実装していきます。まず、Info.plistでフォトライブラリを使用する利用目的を設定します。
アプリ名のディレクトリ以下のInfo.plistを開いてください。(テスト用のディレクトリにもあるので注意してください)
Information Property Listの横にある+ボタンを選択して、Privacy - Photo Library Usage Descriptionを入力してください。
行が追加されたら、Valueの部分には利用目的を記述します。
ここまででInfo.plistの設定は終わりです。
次に、アラートを出す実装をします。
iOS11以降.plistに設定してもアクセス権限の確認が自動的に行われなくなったので
意図的にタイミングを指定してアラート表示する必要があります。
最初の画面が表示されたタイミングでアラートを表示するようにしていきましょう。最初に開かれるページになる、VideoUploaderViewController.swiftを開いてください。
今回必要なフォトライブラリへアクセスするのでPhotosというフレームワークを使用します。
Appleが用意してくれているものなのでimport Photosを記述することで使用できるようになります。
以下のように追加してください。VideoUploaderViewController.swiftimport UIKit import Photos自動生成されるviewDidLoadの下に、confirmPhotoLibraryAuthentication()という関数を作成します。
ここで、フォトライブラリの使用許可の確認を行います。VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthentication() { }privateは、このclass内でのみ呼び出しを許可したい時に使用します。
(railsでもストロングパラメーターを記述する時に使ったことがあるかと思います。同じやつです。)この関数内で、アクセス許可をされているかの現状の確認と許可されていなかった時の挙動を記述していきます。
VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthenticationStatus() { //権限の現状確認(許可されているかどうか) if PHPhotoLibrary.authorizationStatus() != .authorized { //許可(authorized)されていない・ここで初回のアラートが出る PHPhotoLibrary.requestAuthorization { status in switch status { //もし状態(status)が、初回(notDetermined)もしくは拒否されている(denied)の場合 case .notDetermined, .denied: //許可しなおして欲しいので、設定アプリへの導線をおく self.appearChangeStatusAlert() default: break } } } }
PHPhotoLibrary.requestAuthorization
の時に先ほどのアラートが出現します。
その結果をstatusとしてクロージャー({})内に展開します。
このアラートは基本的に一度きりなので
アプリを閉じられたりして初回(notDetermined)のままだったり
アクセスを許可していない(denied)ユーザーに対して再度設定を促すアラートを出そうと思います。appearChangeStatusAlert()という関数を作成します。
confirmPhotoLibraryAuthenticationStatus関数の下に追加してください。VideoUploaderViewController.swiftprivate func confirmPhotoLibraryAuthenticationStatus() { //略 } //ここから private func appearChangeStatusAlert() { //フォトライブラリへのアクセスを許可していないユーザーに対して設定のし直しを促す。 //タイトルとメッセージを設定しアラートモーダルを作成する let alert = UIAlertController(title: "Not authorized", message: "we need to access photo library to upload video", preferredStyle: .alert) //アラートには設定アプリを起動するアクションとキャンセルアクションを設置 let settingAction = UIAlertAction(title: "setting", style: .default, handler: { (_) in guard let settingUrl = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(settingUrl, options: [:], completionHandler: nil) }) let closeAction = UIAlertAction(title: "cancel", style: .cancel, handler: nil) //アラートに上記の2つのアクションを追加 alert.addAction(settingAction) alert.addAction(closeAction) //アラートを表示させる self.present(alert, animated: true, completion: nil) }ここまで書けたら、画面が読み込まれた際に呼び出されるviewDidLoad()から
confirmPhotoLibraryAuthenticationStatus()を呼び出してみましょう。VideoUploaderViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() // MARK: allow access to camera roll self.confirmPhotoLibraryAuthenticationStatus() }実際に起動して、動作を確認してみましょう。
Info.plistで指定した利用目的はここに出てきます!
Don't Allowを押すと先ほどタイトルなどを設定したアラートが出現します。
settingを押すと設定アプリも開けるようになってると思います。デバッグの注意点:
今回の実装では最初のアラートは初回起動時一回しか出てこないので、
再度確認したいときは、シュミレーターからアプリを削除してビルドし直してください。4. ボタンをViewControllerに接続する
ユーザーからフォトライブラリへのアクセス許可を貰えるようになったので、
ここからはフォトライブラリから動画を選択する機能を作っていきます。まずVideoUploaderViewController.swiftと先ほどstoryboardに配置したボタンを接続していきます。
Main.storyboardを開いてください。
optionを押しながら、VideoUploaderViewController.swiftを開きます。
左側にMain.storyboard、右側にVideoUploaderViewController.swiftが開けていればOKです。
storyboardでCtrを押しながらボタンを選択して、ViewcontrollerのviewDidLoadの上まで
カーソルを引っ張ってください。
選択をやめると、このようなメニューが出てくるかと思います。
以下のようにConnectionをAction
に変更して、
NameにdidTapSelectButton
と入力してConnectを押してください。
ConnectionにAction
を指定することで
ボタンをタップされた後に呼び出される関数を自動生成することができます。
ここまでで、VideoUploaderViewController.swiftと動画選択のボタンの接続は終わりです。
5. ボタンが押されたときにフォトライブラリを開いて動画を選択する
ここでやることは、UIImagePickerControllerを使って
フォトライブラリから動画を選択できるようにします。UIImagePickerControllerとは、
メディア周り(フォトライブラリにアクセスしたりやカメラを起動したり)の機能を簡単に扱えるようにするクラスです。Photosフレームワークをインポートしてあるだけで使用できます。
(今回はフォトライブラリのアクセス許可をとる時に既にインポートしてあるのですぐに使えます。)早速実装していきます。
VideoUploaderViewControllerクラスに、UIImagePickerControllerインスタンスを生成します。VideoUploaderViewController.swiftclass VideoUploaderViewController: UIViewController{ let imagePickerController = UIImagePickerController() //略 }次に、VideoUploaderViewController.swiftにselectVideo関数を作成します。
ここにフォトライブラリを開いて動画を選択する機能を実装していきます。VideoUploaderViewController.swiftprivate func appearChangeStatusAlert() { //略 } //ここから private func selectVideo() { //選択できるメディアは動画のみを指定 self.imagePickerController.mediaTypes = ["public.movie"] //選択元はフォトライブラリ self.imagePickerController.sourceType = .photoLibrary //実際にimagePickerControllerを呼び出してフォトライブラリを開く self.present(self.imagePickerController, animated: true, completion: nil) }最後に、自動生成したdidTapSelectButtonの関数からselectVideo()を呼び出すことで
ボタンがタップされた時にフォトライブラリを開き動画を選択することができるようになります。VideoUploaderViewController.swift//選択ボタンがタップされた時に呼び出される @IBAction func didTapSelectButton(_ sender: Any) { selectVideo() }ビルドして確認してみましょう。動画を選択できるようになったと思います。
6. 選択された動画のサムネイルをViewControllerに表示する
最後に選択した動画のサムネイルをVideoUploaderViewControllerに表示します。
少しやることが多いので、流れをまとめます。以下の通りです。
- Main.storyboardにサムネイルを表示するためのUIImageViewを置く
- UIImageViewをViewControllerに接続する
- ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる
- imagePickerControllerが閉じられる時に呼び出される関数を実装する
- 取得した動画のパスを元にサムネイルを生成するMain.storyboardにサムネイルを表示するためのUIImageViewを置く
Main.storyboardを開いてください。
今回はサムネイル(画像)を表示することが目的です。画像を表示するにはUIImageViewを使用します。
UIButtonの時と同じように、LibraryからUIImageViewを検索して追加します。
高さはConstantを250ポイントに指定してください。
横幅は画面いっぱいに指定してください。
画面いっぱいにするには、Ctrを押しながらUIImageViewを選択してカーソルをUIImageViewの外側まで持っていきます。
ここで選択を解除すると以下のメニューが出てきます。
Equal Widthsを選択すると画面に対して横幅がいっぱいになります。次に横軸の座標を指定します。今回は画面に対して中央にします。
同様にメニューを出してCenter Horizontally in Safe Areaを指定してください。
このように真ん中にUIImageViewが移動すると思います。
最後に縦軸の座標を指定します。今回は動画選択のボタンのトップよりUIImageViewのボトムが54ポイント上になるように指定します。
Ctrを押しながら、UIImageViewを選択してください。カーソルは選択ボタンまで持っていってメニューを表示させます。
ここでVertical Spaceを選択してください。
Constantは54ポイントを指定してください。UIImageViewをViewControllerに接続する
選択ボタンの時と同じようにMain.storyboardを開いて、隣にVideoUploaderViewcontroller.swiftを開いてください。
(optionを押しながらVideoUploaderViewcontroller.swiftを選択してください)
Ctrを押しながら、UIImageViewをVideoUploaderViewcontroller.swiftに繋いでください。
今度はConnectionはOutlet
のままで、Connectしてください。
以下のコードが生成されたら接続は完了です。
ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる
ここが一番わかりづらいと思います?
そもそもDelegate(デリゲート)とはSwiftでよく使われる考え方で、ここら辺の記事がわかりやすいです。
【swift】イラストで分かる!具体的なDelegateの使い方。
簡単にいうと、あるクラスが別のクラスに処理をまかせる(委譲する)ことです。
なんで批准という表現が使われるかという話は、例え話がわかりやすいです。
今回扱うデリゲートやプロトコルは、所謂条約のような決まり事です。
必ず守らなければならないルールがあったり、出来ることが増えたりします。
またそこには加盟する国々(ViewControllerなど)がいます。
この関係を国々が条約に批准するというように、
ViewControllerもデリゲートやプロトコルに批准すると考えると理解しやすいです!(個人談)今回は、デリゲートに批准することで、VideoUploaderViewControllerに
UIImagePickerControllerの関数を使って、選択した動画やサムネイルを受け取ってもらいます。実装方法は、まずクラスを定義している部分にUIViewControllerと同じように必要なDelegateを記述します。
次に、imagePickerControllerの代わりにVideoUploaderViewControllerが役割を肩代わりしますよ!って宣言をします。
VideoUploaderViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() self.imagePickerController.delegate = self //略これで批准完了です!
imagePickerControllerが閉じられる時に呼び出される関数を実装する
まず、選択された動画のurlを保持するための変数を定義します。
VideoUploaderViewController.swiftclass VideoUploaderViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { //ここを追加 private var videoUrl: NSURL?先ほどのデリゲートに批准したので、動画を選択した後imagePickerが閉じられる時に呼び出される関数を利用できるようになります。
VideoUploaderViewController.swift//imagePickerが閉じられる時に呼ばれる func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { //キーを指定して選択された動画のパスを取得する let key = UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerMediaURL") videoUrl = info[key] as? NSURL //動画の絶対パスを元にサムネイルを生成(generateThumbnailFromVideoの実装は後述します。) //先ほど接続したthumbnailImageViewのimageにサムネイルをセット thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!) //サムネイルの縦横比を変えずに長い辺を画面サイズに合わせる設定 thumbnailImageView.contentMode = .scaleAspectFit //imagePickerControllerを閉じる imagePickerController.dismiss(animated: true, completion: nil) }取得した動画のパスを元にサムネイルを生成する
先ほど後述すると書いたgenerateThumbnailFromVideoという関数を実装します。
VideoUploaderViewController.swiftprivate func generateThumbnailFromVideo(_ url: URL) -> UIImage? { //以下の3行で縦動画から画像を取り出しても横向きの画像にならないようにしてる let asset = AVAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true //切り取るタイミングの指定 var time = asset.duration time.value = min(time.value, 2) //サムネイルの生成 do { let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil) return UIImage(cgImage: imageRef) } catch { return nil } }この関数は
-> UIImage?
でサムネイルを返り値として指定しています。
生成に成功するとreturn UIImage(cgImage: imageRef)
で画像を返してくれます。
これが先ほど実装した関数のこの部分に返るのでサムネイルが表示されるようになります。
thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!)
ビルドして確認してみましょう!
動画選択後にサムネイルが表示されてればOKです。
動画選択の機能は以上です。
動画再生の機能を作成する
ここでやること
1. 動画再生用ボタンを設置する
2. ViewControllerに接続する
3. 動画再生の関数を実装する1. 動画再生用ボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画再生用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に対してcenter
こんな感じです(急に色付きでスミマセン?)
2. ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
3. 動画再生の関数を実装する
動画を再生するにあたって、再生プレーヤーが必要となります。
Appleが提供しているAVKitフレームワークに、
ビデオコンテンツを再生するためのインターフェイスが用意されているのでこれを使用していきます。VideoUploaderViewController.swiftimport UIKit import Photos //ここを追加 import AVKitVideoUploaderViewController.swiftprivate let imagePickerController = UIImagePickerController() //ここを追加 private let playerViewController = AVPlayerViewController()次に、動画再生を行う
playVideo(from url: URL)
を実装します。VideoUploaderViewController.swiftprivate func playVideo(from url: URL) { //プレイヤーに受けとったurlをセット let player = AVPlayer(url: url) //先ほど初期化したplayerViewControllerのプレイヤーに上記のプレイヤーをセット playerViewController.player = player //playerViewControllerの表示・再生 self.present(playerViewController, animated: true) { print("playing video") self.playerViewController.player!.play() } }最後に再生ボタンを押された後に
playVideo(from url: URL)
を呼び出します。VideoUploaderViewController.swift@IBAction func didTapPlayButton(_ sender: Any) { //選択された動画の絶対パスがオプショナル(nilの可能性がある)ので //guard(railsでいうunless)でパスがnilなら早期リターンにしてる guard let url = videoUrl?.absoluteURL else { return } playVideo(from: url) }ここまで実装できたら、ビルドして再生ボタンを押して確認してみましょう!
動画再生の機能は以上です。
動画アップロードの機能を作成する
ここでやること
1. 動画アップロード用のボタンを設置する
2. ViewControllerに接続する
3. 動画アップロードの関数を実装する1. 動画アップロード用のボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に右端対してconstantを71ポイント
こんな感じです。
2. ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
3. 動画アップロードの関数を実装する
動画のアップロードを行う
uploadVideo()
を実装します。
今回アップロードするのは動画のみです。
アップロードに必要な情報は、
・選択された動画のurl
・選択された動画の名前
になります。VideoUploaderViewController.swiftprivate func uploadVideo() { //urlと名前がなければ早期リターンさせる guard let videoClipPath = videoUrl?.absoluteURL, //urlの最後がファイル名になる let videoClipName = videoUrl?.lastPathComponent else { print("not found video path or name"); return } //バックエンドにリクエストを送る //ここは次の章で実装するので(APIクライアント)エラーのままで大丈夫です。 API.postData(videoClipPath: videoClipPath, videoClipName: videoClipName) }
uploadVideo()
もアップロード用のボタンが押された時に呼び出されるようにしておきます。VideoUploaderViewController.swift@IBAction func didTapUploadButton(_ sender: Any) { uploadVideo() }この機能は、APIクライアントとバックエンドのAPIを作って完成します。
2. [Swift] APIクライアントの実装?
SwiftからRailsにデータを送信するための機能を作っていきます。が、
その前に。動画データの扱い方について
一回やったのですが動画変換はbase64だと重くてツライので、
APIのリクエストヘッダーのContent-Typeにmultipart/form-dataを指定して
動画をAppleの標準の拡張子.MOVのままバックエンドにアップロードできるように実装します。ザックリと用語の解説
base64
base64はバイナリーデータ(今回は動画)を
String(ASCIIテキスト: アスキーと読みます)に変換する方法です。
バイナリーデータをStringに変換できるのでjson形式で扱えて便利です。
有名な変換方法なので詳しい話はこちらをどうぞ!
base64ってなんぞ??理解のために実装してみた
弱点は、上でも書いた通りめちゃくちゃに重たいです。
理由はASCIIテキストに変換していく中でデータサイズが33%増加することです。
例えば、1枚66KBのサンタを変換するとこんな感じ
(ちなみに文字列はまだまだ続く。
base64は100MB未満のデータに適していると言われているのでこのサンタはちょろい方)
??base64エンコーダー
こんな感じなので動画を全部文字列で扱ったらbase64⇄動画の変換は地獄です。
(1分くらいの動画をbase64でコンソールに出力したらXcodeが固まった)ということで今回は不採用?
multipart/form-data
HTML4から導入された方法で、複合型のコンテンツをMIME Typeを指定することで形式を変えずに送信することができます。(textだったりjpegだったりいろんな形式のデータを一緒に送信することが可能ということ)
HTTPリクエストのヘッダー部分で指定するContent-typeの一種です。
(Content-typeとは「このリクエストの中身はこんな形式のデータが入ってますよ〜」って宣言。)
詳しくはこちら。[フロントエンド] multipart/form-dataを理解してみよう
今回送信するデータはvideo/quicktime(.MOVファイル)だけですが、拡張性があるのでmultipart/form-dataを指定しています。ザックリした流れ
- Alamofireのインストール
- APIClient.swiftの作成
- POSTリクエストを実装
- http通信の許可
Alamofireのインストール
APIクライアントを簡単に実装できるライブラリです。CocoaPodsでインストールします。
導入方法はこちらが詳しいので参考にしてください。(【Swift】CocoaPods導入手順)[https://qiita.com/ShinokiRyosei/items/3090290cb72434852460]
今回pod 'Alamofire', '~> 4.7.2'
を指定してください。
インストール後、Xcodeを閉じて
プロジェクトディレクトリ以下に出てくる.xcworkspaceファイルで開き直したら完了です。
APIClient.swiftの作成
Xcodeが開けたら、左側に表示されるナビゲーターからプロジェクト名のディレクトリを指定してください。
?この状態
左下の+ボタン > File... > Swift File の順で、APIClient.swiftというファイルを作ってください。
ナビゲーターのプロジェクト名のディレクトリ以下にファイルが追加されていたら完了です。POSTリクエストを実装
Alamofireを使って、APIClient.swiftにVideoのPOSTリクエストを書いていきます。
まずはAlamofireをインポートします。APIClient.swiftimport AlamofireAPIクライアントを書いていきます。
ここでやりたいのは、
- Content-type: multipart/form-dataでリクエストを作成
- 動画データの追加
- 送信(エラーハンドリング)
です。APIClient.swiftstruct API { //APIのエンドポイント。 static let baseUrl = URL(string: "http://localhost:3000/api/v1/videos")! static func postData(videoClipPath: URL, videoClipName: String){ //multipart/form-dataでデータを送信する Alamofire.upload(multipartFormData: { multipartFormData in //multipartFormDataオブジェクトに対してデータの追加を行う //withNameはrailsのActiveStorage側で保存するときのキーと同じ multipartFormData.append(videoClipPath, withName: "clip", fileName: videoClipName, mimeType: "video/quicktime") }, to: baseUrl) { encodingResult in //encodingが成功するとこのハンドラが呼ばれる switch encodingResult { case.success(let upload, _ ,_): print(upload) upload .uploadProgress(closure: { (progress) in //進捗率の取得 print("Upload Progress: \(progress.fractionCompleted)") }) case.failure(let error): print(error) } } }こんな感じでPOSTリクエストの実装は完了です。
http通信の許可
http通信を許可する
iOS9以降、意図的にドメインを許可する設定をしないとXcodeでhttp通信できなくなりました。
設定しないと以下のようなエラーを吐きます。。The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.今回、rails側のlocalhostを叩きたいのでこの設定が必要になります。
この記事がわかりやすいので設定してみてください??
【swift】XcodeでiOSアプリのhttp通信を許可する方法
[おまけ]
ここではバックエンドやフロントエンドでCORSを指定する時でいう
ワイルドカードの指定と同じようなことをやっているので(全ドメインを許可)
本番環境で必要になる場合は(herokuデプロイとか)Exception Domainで特定のドメインのみを許可してください。
iOS9でHTTP通信ができない時の解決法以上でAPIクライアントの実装は終わりです!
3. [Rails] APIサーバーの実装?
ザックリした流れ
- プロジェクトの作成
- モデルの作成
- コントローラーの作成
- ルーティングの作成
プロジェクトの作成
環境構築は割愛??
1. ターミナルを開いて$ cd /path/to/プロジェクトを作成したいディレクトリ
で移動します
2.$ rails _5.2.3_ new video_uploader_server --api --skip-test
を実行します(アプリ名はvideo_uploader_serverを好きな名前に変更してください)
オプション 内容 _5.2.3_
railsのバージョン指定(6以降だと色々面倒なので今回はこちら) --api APIモード --skip-test 今回テスト書きません。書く方は抜いてください その他optionはrails newの書き方について徹底解説!を確認してみてください。
モデルの作成
モデルを作成します。
今回必要なのは
- Videoモデル
- ActiveStrangeで使用するモデル
です。ActiveRecordのVideoモデルの作成
$ cd アプリ名
作成したプロジェクトに移動$ rails g model video
でVideoモデルを作ります(gはgenerateのgです。今回カラム使わないのでこれだけで大丈夫です。)ActiveStorageの設定を行うのでマイグレーションはまだ行わなくて大丈夫です!
続きをどうぞ!ActiveStorageの設定
今回扱うのは動画(.MOV)なのでActiveStorageを使って保存していきます。
Active Storage の概要に詳しく解説されているので参考にしてください。
1.$ rails active_storage:install
ログはこんな感じになります
2.$ rails db:migrate
でデータベースを作ります
3. お好きなエディタでプロジェクトファイルを開きます
4.video.rb
を開いて編集しますVideoモデルに紐付ける動画ファイルをclipという名前でActiveStorageから呼び出せるように指定します。
video.rbclass Video < ApplicationRecord #ここを追加 has_one_attached :clip endこれはActiveStoregeで使用するモデルとVideoモデルのリレーションを定義しています。
Videoモデルにカラムを追加しなくてもVideoモデルに動画を保存しているかのように
ActiveStorageで動画を保存することができるようになります。コントローラーの作成
$ rails g controller api/v1/videos
でコントローラーを作成します
v1はバージョン1という意味です。
このように階層を分けてAPIのバージョンを管理するプロジェクトが多いです。
今回は簡単シンプルな構成で機能追加も想定していないので無くても大丈夫です。videos_controller.rbを編集します
videos_controller.rbclass Api::V1::VideosController < ApplicationController # videoの保存 def create video = Video.save(video_params) if video.save render json: { status: 'ok' } else render json: { status: 'ng' } end end # videoの取得 def index # とりあえず最後に保存したもの一件だけを表示 video = Video.last.clip # ActiveStorageで保存したビデオのurlをjsonで返す url = url_for(video) render json: { url: url } end private def video_params params.permit(:clip) #clipはActiveStorageに保存する時のキー end endルーティングの作成
controllerの階層を考慮した上でvideos_controllerのルーティングが設定できれば問題ないです。
routes.rbRails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html namespace 'api' do namespace 'v1' do resources :videos end end endこんな感じ。今回使わないアクションが多いのでonlyで絞り込んでもらってもいいです。
これでルーティングの実装も終わりです。
Xcodeでアプリをビルドして、railsもサーバーを起動してアップロードの確認をしてください。[ちなみに]
swiftからのリクエストは、rails側のコンソールでこんな感じで受け取ってるのを確認できます。Parameters: {"data"=>"testVideo", "clip"=>#<ActionDispatch::Http::UploadedFile:0x00007f982a2e9c70 @tempfile=#<Tempfile:/var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV>, @original_filename="34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV", @content_type="video/quicktime", @headers="Content-Disposition: form-data; name=\"clip\"; filename=\"34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV\"\r\nContent-Type: video/quicktime\r\n">}ターミナルで
open /var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV
で送られてきた動画の再生ができたりします。4. [Swift] 動画プレイヤーの実装?
ザックリした流れ
- 最新の動画再生用ボタンを設置する
- ViewControllerに接続する
- 最新の動画をフェッチするリクエストを実装する
- 最新の動画を再生する
動画アップロード用のボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅は文字の長さ。高さは文字の大きさを24ポイント
・縦の座標は再生ボタンから42ポイント下
・横の座標は画面に右端対してcenter
こんな感じです。
(画像追加します!)ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
(画像追加します!)最新の動画をフェッチするリクエストを実装する
APIClient.swiftに以下を追加してください。
APIClient.swift//completionを使うことで呼び出し側でリクエストと動画再生を同期的に扱えるようにしてる static func fetchLatestVideoUrl(completion: @escaping (URL) -> ()) { //レスポンスの型。今回はurlのみ struct FetchResult: Codable { let url: String } //今回パラメーターは特に必要ないので[:](空)で Alamofire.request(baseUrl, method: .get, parameters: [:]) .responseJSON { response in switch response.result { case .success: print("Success!") //レスポンスをFetchResultに変換する guard let data = response.data, let result = try? JSONDecoder().decode(FetchResult.self, from: data), //取得できたFetchResultオブジェクトのurl(String?)からURLを生成 let fetchedUrl = URL(string: result.url) else { return } //取得できたURLをcompletionに渡す completion(fetchedUrl) case .failure: print("Failure!") } } }最新の動画を再生する
最新の動画を再生するための関数
playUploadedLatestVideo()
を実装します。VideoUploaderViewController.swiftprivate func playUploadedLatestVideo() { //バックエンドからファイルのurlを返してもらう(http://localhost:3000で始まるもの) //先ほどのcompletionはここの{}(クロージャ)。この中でurlを受け取る API.fetchLatestVideoUrl() { url in //このurlを使ってプレビューと同様にplayVideo(from url: URL)でビデオを開く self.playVideo(from: url) } }最後にボタンが押されたタイミングで
playUploadedLatestVideo()
を呼び出さすように記述して完成です!VideoUploaderViewController.swift@IBAction func didTapLatestVideoButton(_ sender: Any) { playUploadedLatestVideo() }あとがき
お疲れ様でした!
今回は最低限の機能実装でした。
エラーハンドリングしてアラート出したり、一覧ページを作ったり、
削除機能つけたりお好みでいろいろ追加してみてください!あとそもそもアプリをストアにあげるとかですね!
気になったこととかもっといいやり方あるよとかご指摘は
お気軽にバシバシください??読んでくださりありがとうございました?
- 投稿日:2019-12-05T09:52:09+09:00
忘備録-Swiftの基本的なデータ型
範囲型と片側範囲型
例
0..<10
“a”…“f”
3.4…
…16
..<1.7範囲型と片側範囲型のプロパティ
let hoge = 0...10 print(hoge.lowerBound) print(hoge.upperBound)ストライド型
for x in stride(from: 0.0, to: 12.0, by: 0.5){ print(x,terminator:"") }可変個の引数が利用できる関数
func add(_ number: Int...) -> Int{ var num = 0 for n in number { num += n } return num } print(add(1,2,3)) // 6 print(add(0,2,3,4,4,9)) // 22 print(add(121)) //121部分文字列の取り出し
let hoge = "hogehoge" let start = hoge.index(hoge.startIndex, offsetBy:1) let end = hoge.index(start, offsetBy:3) print(hoge[start]) // o print(hoge[start ..< end]) // oge
- 投稿日:2019-12-05T09:52:01+09:00
忘備録-Swiftのプロトコル
趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。
参考文献
この記事は以下の書籍の情報を参考にして執筆しました。
Swiftのプロトコル
Swiftのプロトコル : 型が持つべきメソッドやプロパティの宣言をまとめる仕組み。
(JavaやC#のインターフェースに当たる)異なる型の共通した操作
例として
Int、Double、Stringは共通して、演算子を使って「互いに比較をできる」機能を持っている。
この「互いに比較をできる」機能というのは、Swift標準ライブラリで定義されているComparableプロトコルがまとめている。何が便利なのかというと
例えばソート関数は「互いに比較をできる」必要がある。
なので型にComparableプロトコルを適応すると並び替えの対象にできる。プロトコル思考
Swiftはインターフェースを定義するためのプロトコルをクラス・構造体・列挙型にも適応可能な仕組みとして導入している。
そのためプロトコルを中心にソフトウェアの設計、プログラミングを進めることができる。プロトコル指向と呼ぶ。プロトコルの採用
Swiftの標準ライブラリにあるCustomStringConvertibleプロトコルを採用してみる。
public protocol CustomStringConvertible { var description: String { get } }CustomStringConvertibleプロトコルはdescriptionを定義するが、このときに適当な文字列を代入して、
構造体がも文字列として呼ばれた時の動作を確認した。struct Hoge : CustomStringConvertible { let hoge = "hoge" var description: String{ hoge + "HogeHoge" } } struct Fuga { var fuga: String = "fuga" } let hoge = Hoge() print("\(hoge)") // hogeHogeHoge let fuga = Fuga() print("\(fuga)") // Fuga(fuga: "fuga")CustomStringConvertibleを採用した構造体が文字列として出力されたときに、
descriptionに入った値を返すことが確認できた。型としてのプロトコル
プロトコルはプログラム内で型としても使用できる。
型に一致したプロトコルを持つ値を使用できる。protocol Dog { var size: Int { get } } struct Pochi: Dog { var size: Int } struct Taro: Dog { var size: Int } struct Tama { var size: Int } let hoge = Pochi(size: 10) let fuga = Taro(size: 20) let aaa = Tama(size: 5) var list: [Dog] = [] // protocol Dogを型に指定 list.append(hoge) list.append(fuga) list.append(aaa) // error Dogプロパティを採用していない構造体は配列に加えれないプロトコルの継承
protocol Dog { var size: Int { get } } protocol GuideDog : Dog { var rank: Int { get } }プロトコルの合成
protocol Dog { var size: Int { get } } protocol Porice { var strength: Int { get } } protocol PoriceDog : Dog ,Porice{ var strength: Int { get } } var hoge: PoriceDog一方プロトコルを定義せずに、プトとコルの和集合として型を指定する場合
var hoge: Dog & Poriceプロトコルと付属型
プロトコルの型定義内部に付属型を定義できる。
associatedtypeというキーワードを使う。protocol SimpleVector { associatedtype Element //xとyの型を統一する var x : Element { get set } var y : Element { get set } } struct VectorFlat : SimpleVector { typealias Element = Float //型を明示 var x, y : Float } struct VectorDouble : SimpleVector, CustomStringConvertible { var x, y : Double // 型推論される var description: String { "x: \(x),y: \(y)"} } struct VectorGrade : SimpleVector, CustomStringConvertible { enum Element : String { case A, B, C, D, X } //型をcase型で列挙した文字に指定 var x, y: Element var description: String { "x: \(x),y: \(y)"} } var a = VectorFlat(x: 10.0, y: 15.0) print(a) // VectorFlat(x: 10.0, y: 15.0) var b = VectorDouble(x: 20.0, y: 40.0) print(b) // x: 20.0,y: 40.0 var c = VectorGrade(x: .A, y: .X) print(c) // x: A,y: X型パラメータに制約を指定
(1)associatedtype A
制約なし
(2)associatedtype A : プロトコル
型Aはプロトコルに適応する
(3)associatedtype A = 型名
このプロトコルを採用した型のAに規定値がなければ型の規定値を使用する
(2)と(2)を組み合わせて
associatedtype A : プロトコル = 型名
(1)~(3)の条件の後にwhere条件を記述できる。
条件は(a)(b)のどっちか
(a)型: プロトコル
型は指定したプロトコルに適応
(b)型1 == 型2
型1と2の一致プロトコルを継承する際にプロトコルの付属型に対して制約をつけることができる。
protocol B : プロトコルA where 条件
条件は(a)(b)のどっちか
(a)型パラメータ: プロトコル
型パラメータは指定したプロトコルに適応
(b)型パラメータ== 型
型パラメータと型の一致protocol SimpleVector : Equatable{ associatedtype Element : Equatable //Equatableを条件に指定 var x : Element { get set } var y : Element { get set } } struct VectorDouble : SimpleVector, CustomStringConvertible { var x, y : Double // Equatableに適合する var description: String { "x: \(x),y: \(y)"} } struct ShopOnMap : SimpleVector, CustomStringConvertible { static func == (lhs: ShopOnMap, rhs: ShopOnMap) -> Bool { return lhs.x == rhs.x && lhs.y == rhs.y } var shop: (name : String, comment : String?) //タプル型なのでEquatableに適合しない var x, y : Double init(n: String, c: String? = nil, X: Double, Y: Double){ shop = (name: n, comment: c) x = X y = Y } var description: String { "\(shop.name) x: \(x),y: \(y) \(shop.comment ?? "”)"} } var a = ShopOnMap(n: "a”, X:11.0, Y:30) print(a) // a x: 11.0,y: 30.0 var b = ShopOnMap(n: "b”, c:"bbbb”, X:11.0, Y:30) print(a==b) // true プロパティのx,yが等しいだけで同じとみなされる var c = ShopOnMap(n: "a”, X:2.0, Y:2) print(a==c) // falseCopy-On-Weite
Swiftでは参照型のデータはクラスのインスタンスやクロージャーくらいしかない。
実数や配列や構造体などは値型のデータ。
値型のデータは値全体がコピーされるように思うが、Swiftでは値型の受け渡しの際に、内部的には参照を行い、値の変更があった時点でコピーを取ることで、メモリを効率的に使っている。
部分配列を作る時も同様にシークエンスの一部を取り出す時に、シークエンスの複製を作るのではなく部分配列の情報を効率よく格納できる型を作り、元のシークエンスの値は共有。
- 投稿日:2019-12-05T07:58:56+09:00
|Swiftでバイナリ???を扱う?!>?
Swiftでバイナリファイル扱うことはそんなに頻繁にはないと思うのですが、
案件の中でAndroidチームの人が、巨大なJSONファイルをバイナリに圧縮して処理していて、
iOSアプリでも同じようにできるんじゃないかということで、バイナリを扱うことになったので、その知見を残します。なお僕がやりたかったのは、倍精度浮動小数点数(漢字多すぎですよね)のバイナリファイルを、
Swift上でDouble型の配列に落として使いたい、というのがゴールでした。※バイナリの絵文字として01が出したかったのですが、?しかなかったので、仕方なく?を使っています。
x進数のリテラル
バイナリファイルを扱う前に、Swiftのリテラルを確認しましょう。
x進数let decimalInteger = 17 let binaryInteger = 0b10001 // 17 の 2 進数 let octalInteger = 0o21 // 17 の 8 進数 let hexadecimalInteger = 0x11 // 17 の 16 進数2進数、8進数を直書きすることはあまりないと思います。
0xを先頭につけると16進数扱いになる、が重要です。
特にデバッグで、バイナリファイルじゃなくて、バイナリを扱う処理のロジックを確認したいときに、
サンプルデータとして0x1234みたいにしてよく使いました。バイナリファイルの扱い
バイナリファイルを扱うのがはじめてだったので、何かお作法があるのかなあと思っていましたが、特別なことは何もなく、
バイナリファイルをXcodeから追加して、そのファイルをData型として読むだけでした。let filename = "xxx" //<-バイナリファイル?の名前を入れてね guard let path = Bundle.main.path(forResource: filename, ofType: nil) else { return } guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { fatalError("load failed.") }これで読み込みは完了です?
Data型→String型の変換は意外と簡単
Dataの中身をprintしてみましょう。
何も考えずprintprint(data)結果4016 bytes____________
| あれっ? >?
‾‾‾‾‾‾‾‾‾‾‾‾
Data型はprintしてもByte数が表示されます。
個人的には16進数で吐いてくれると嬉しいのですが、そういう仕様ではありません。
(※NSDataを使うと中身見れます)
単にデバッグなら、Stringに変換するのが楽でした。こちらを参考にしつつ、
Data型→String型print(data.map { String(format: "%02X", $0)})としてやると……
結果["12", "34", "56", "78",……(略)と出力できました〜?
ちなみに。
Stringに変換しない場合、つまり、
Data型→?print(data.map { $0 })とするとどうなるのでしょう?
興味本位でやってみました。結果[12, 52, 86, 120,……(略)____________
| ?! >?
‾‾‾‾‾‾‾‾‾‾‾‾
なんかよくわからないですが、数字が出力されましたね。実はData型のmapはデフォルトがUInt8で処理する仕様なので、
1Byte(8bit)の符号なし整数型として処理されました。
つまり0x12→12
0x34→52
0x56→86
0x78→120と変換がかかったわけです。
さらにちなみに、String(format: "%02X", $0)のformatの指定子ですが、
Stringの公式ドキュメント見ても指定子の詳細なフォーマットが発見できませんでした。
(もし公式知ってる人いたら教えてください?)
C言語のprintf()に指定するフォーマット指定子と一緒らしいので、
Objective-CからSwiftに来ている人には常識なんでしょうか……?
(僕はC言語系ちゃんとやったことないので……)"%02X"で、インプット整数値をアウトプット0詰めアリの2文字の16進数で表示して、という指定になるっぽいです。
Stringも奥が深いですね。浮動小数点数・バイナリを扱う上でのツール
浮動小数点数内部表現シミュレーター
ところで皆さんは浮動小数点数の扱い得意ですか???
頭の中で普通に変換できますか???
僕は無理です。
でも世の中には便利なものがありました。これを駆使して、ロジックが正しいか検証しましょう。
バイナリエディタ
Macの標準アプリで、バイナリを表示できるものはありません(たぶん)。
前職ではWindows環境だったので、Windows上でバイナリ見なきゃいけないときはBzというソフトを使っていた気がします。
(基本的にはメインフレーム入って見てましたが。メインフレーム(というかTSO)は逆にバイナリ見やすかったですね)Mac環境であれば、下記のフリーソフトがいいでしょう。
計算機
Mac標準の電卓アプリ(計算機)を、プログラマモードにすると16進数が扱えるので、活用しましょう???
Data型→数値型の変換
String型に比べると、Data型→数値型の変換はちょっとしんどいです。
なんでこんなめんどくさいんだとムカつきながらやっていましたが、
冷静に考えると、Stringってただのバイトストリームで、バイト長の問題さえ認識があえば処理できるのに対して、
数値型はデータ形式がちょっと複雑なのかなと思ったりしました。
まあでもStringも文字コードが絡むと、文字化けの問題がしんどいですね。。。
あくまで数値のバイナリに限定した話として。Swift5 での Data.withUnsafeBytes
Data型→Double型への変換をゴールとして、話を進めます。
Swift5.0以降で微妙にData型のwithUnsafeBytesの仕様が変わっているので、
テキトーにググってサンプルコード使うと動かないので気をつけてください。
8Byte(64bit)だけ処理するのでいいのであれば、let value = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: Double.self) }でできます。
10進数の1.0が、Doubule型の16進数表記だと0x3FF00000_00000000らしいので、
もしdataの中身が0x3FF00000_00000000であればvalueの値は1.0になります!バイトオーダー
……が、しかし、何度デバッグしても想定していた値とは違う、めちゃくちゃな少数が出てきました。
インプットには正の値しかないはずなのに、なんならマイナスの値が出てきます。iOSはバイトオーダーがリトルエンディアンです。
大事なことなのでもう1度いいますが、iOSはバイトオーダーがリトルエンディアンです。var hoge = UInt32(0x1234ABCD) print(NSData(bytes: &hoge, length: 4))結果<cdab3412>____________
| えー?! >?
‾‾‾‾‾‾‾‾‾‾‾‾つまりどういうことだってばよ
つまりこういうことです。
ビッグエンディアンであれば、0x3FF00000_00000000
↓
3F→F0→00→00→00→00→00→00という順で処理をします。
これは直感的ですね。IBMのメインフレーム(及び互換機)、モトローラのMC68000(及び後継)、サン・マイクロシステムズのSPARC等はビッグエンディアンを採用し、DECのVAX、インテルのx86等はリトルエンディアンを採用している。ARMアーキテクチャ、PowerPCなど、エンディアンを切り替えられるバイエンディアン (bi-endian) のプロセッサも存在する。
言語処理系などの仮想マシンの類では、プラットフォームに応じ使い分ける設計のものもあれば、片方に寄せる設計のものもある。例えば、Java仮想マシンはプラットフォームを問わずビッグエンディアンである。
iPhoneはARMアーキテクチャのCPUなので、バイエンディアンってやつだと思うのですが、デフォルトはリトルエンディアンになっています。
Int型であれば、直前にエンディアン変えられるオプションもあるんですが、Float/Doubleだとちょっと厳しそうですし、
やったとして可読性がはちゃめちゃに悪くなるので、元データの生成をリトルエンディアンでやりなおしました。Data型→[Double]型に
長い道のりでしたが、これで完成です!
let value = data.withUnsafeBytes { Array( UnsafeBufferPointer( start : $0.baseAddress!.assumingMemoryBound( to: Double.self ), count : $0.count / MemoryLayout<Double>.size ) ) }$0にUnsafeRawBufferPointer型のポインタが入っているわけですが、
Arrayに食わせるためにRawじゃないUnsafeBufferPointerにDouble型のアライメントを指定してポインタを再作成しているのが、
ちょっと冗長に感じるので、何かUnsafeRawBufferPointerをちょっと変えてArrayにできないか試行錯誤してみたんですが、結局できませんでした。
Swiftのポインタは雰囲気ではわかるんですが、種類が多くて、イマイチ全容をつかめていない感じがあります。配列に落としたあとは、煮るなり焼くなり。
まとめ
無事Swiftでバイナリファイルを扱うことができました。
Swiftでマジメに低レイヤーの処理するのははじめてで、
「そもそもできるのか?」「結局Objective-Cの方がやりやすかったりするんじゃないの?」と思いながらスタートしましたが、一通りのことはできるみたいです。
Data型でも配列や辞書型みたいに高階関数が使えたのはちょっと感動しましたが、それでもバイナリが出てくると途端に泥臭くなりますね。。。?何かのご参考になれば幸いです。
- 投稿日:2019-12-05T07:54:47+09:00
SwiftUIアプリ開発実践ポイント
こんにちは、たなたつです
![]()
SwiftUIが発表されて半年ほど経ちましたね。あっという間に時間は過ぎていき、iOS 13以降じゃないと使えないし、まだ気にしなくていいでしょなんて言ってられなくなるのもあっという間な気がします。
iOS Advent Calendarの5日目ということで今回は、いくつかSwiftUIでサンプルアプリを作ったり、実際にアプリをリリースしたりした中でたまってきた知見を書こうと思います。
SwiftUIは様々なプラットフォームで動きますがiOSアプリに注目し、開発する前に知っておきたい実践的なポイントなどを共有します。
※ Xcode 11.2.1、iOS 13.2.1 での動作を元に記事を書いています。
SwiftUIの特徴
- 少ないコードでUIを作れる (コードレイアウト)
- 宣言的に記述できる
- Appleのすべてのプラットフォームで動く
- ただし、iOS 13、macOS 10.15、tvOS 13.0、watchOS 6.0以上
このような特徴がよく言われていますが、実際にアプリを作るうえで現状のSwiftUIはどうなのでしょうか。
実際のところSwiftUIってどう?
SwiftUIでiOSアプリをいくつか作ってみてこのように感じました。
- 細かなUIの動きまでアプリの仕様に沿って実装する必要がある場合はかなり難しい
- アプリの仕様をSwiftUIが得意としている仕様に柔軟に変更できる場合は採用しても良い
アプリの仕様通りに細かいUI/UXを実現するのは大変
SwiftUIのAPIはまだUIKitほど柔軟ではないため、UIKitでは実現できるUI/UXを再現できない場合があります。
SwiftUIはUIKitと組み合わせて利用することができるため、SwiftUIではできない部分をUIKitで代わりに実装するというアプローチもできます。
しかし、実際に試してみると組み合わせるために必要なボイラープレートコードが多く、負担になります。また、SwiftUIの特徴的な機能の一つにStateをバインディングしてUIを自動的に更新するというものがありますが、UIKitと組み合わせたときにそれらの機能の活用が難しくなるケースがあります。
そしてそれを回避するためのワークアラウンド的なコードが必要となり、本質的な実装に集中できなくなりがちです。SwiftUIの挙動に合わせてUIを変更可能ならあり
SwiftUIが不得意としている仕様を実装するのは、開発の大きなボトルネックになってしまうため、実験的なアプリや個人アプリのように、アプリの仕様を柔軟に変更できる場合は採用してみてもよいと思いました。 (もちろん対応OSバージョンが狭まることを許容できる場合です)
ユーザーファーストの視点とは全く逆になってしまいますが、開発者視点でUI変更できれば、SwiftUIの強みを活かして爆速でアプリを開発することができるかもしれません。
開発の進め方
ここからは実際にSwiftUIでアプリを作るときにおすすめな開発の進め方を紹介します。
現時点ではSwiftUIの情報が少なく、開発者の知識もUIKitほどはないと思いますので、その状況を想定しています。前述したようにSwiftUIには不得意なUIがあるため、想定しているUIが実現しやすいものなのかどうかを作りながら判断し、難しい場合はアプリの仕様を調整するというサイクルを回していきます。
また、SwiftUIの優れた機能の一つにプレビュー機能があります。
プレビュー機能を使うことで早いサイクルでUIの実現性の確認とレイアウトの調整ができるため、積極的に活用したほうが良いです。画面の漸進的な開発
まずは作りたいレイアウトになるように、Viewの
body
にべた書きしていくとレイアウトしやすいです。struct ListCell: View { var body: some View { HStack { Button(action: { #warning("TODO") }, label: { Image("usericon") .resizable() .scaledToFit() .clipShape(Circle()) .frame(width: 60, height: 60) .padding(8) }) VStack(alignment: .leading) { HStack { Text("たなたつ") Text("@tanakasan2525・10m") .foregroundColor(.gray) } Text("ここは本文が表示されるテキスト領域です。改行することもできます。") .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) // workaround HStack { Button(action: { #warning("TODO") }, label: { Image(systemName: "bubble.left") }) Spacer() Button(action: { #warning("TODO") }, label: { Image(systemName: "arrow.2.squarepath") }) Spacer() Button(action: { #warning("TODO") }, label: { Image(systemName: "heart") }) Spacer() Button(action: { #warning("TODO") }, label: { Image(systemName: "square.and.arrow.up") }) } .foregroundColor(.gray) .padding(8) } } .padding(8) } } struct ListCell_Previews: PreviewProvider { static var previews: some View { ListCell() .previewLayout(.sizeThatFits) } }レイアウトがある程度出来たら、bodyの可読性を上げるためにメソッドに切り出します。
この時にXcodeのリファクタリング機能を使うこともできます。
var body: some View { HStack { userIconView() VStack(alignment: .leading) { userNameView() messageView() bottomButtonView() .padding(8) } } .padding(8) }どこまでメソッド化するか悩ましいですが、bodyを見ればざっくりのレイアウトがわかるくらいまで切り出すのが良いと思います。
また、他の画面でも使いそうなViewのレイアウトはカスタムViewとして切り出していきましょう。
上記の場合、画像のボタンはImageButton
というカスタムクラスを作っても良さそうです。画面のレイアウトのポイント
SwiftUIのエラーは読みにくいので細かく分ける
現状ではSwiftUIのエラーは非常に読みにくく、Xcodeの気持ちを読み取るエスパー力が必要な状態です。
例えばこの実装、どこが悪いでしょうか?
正解は
var body: some View { VStack { Text("エラーわかりにくい") .frame(width: 300, height: 60) TextField("名前", text: self.$viewModel.name) } }TextFieldの第二引数で渡しているtextが期待している型は
Binding<String>
です。エラーの実装は間違えてPublishedのnameに$
をつけてしまっています。ですが、Xcodeが提示しているエラーはなぜか
frame
のところになっています。
上記は短い実装なのでパッと見てわかるかもしれませんがbodyがかなり長い行数になっていた場合、見つけるのは非常に困難です。そのためにもできるだけメソッドやカスタムViewに切り出して
body
部分を短いコードに留めるようにしておきたいです。プレビューしやすいView
Viewを作っていく際に、SwiftUIのプレビュー機能を使うとリアルタイムで表示を確認できるだけでなく、自然と依存関係の少ない (正しい) Viewができていくように思いました。
プレビューするためにはダミーの値を用意してViewに渡す必要があるため、例えば、後述するEnvironmentObject
の良くない使い方をしていると「あれ、プレビューするためにいろいろデータを用意しないといけないぞ、面倒だ」ということに気づき、Viewの粒度やStateの設計などを早いサイクルで改めることができます。なので、積極的にプレビューを利用しながら、プレビューしやすいViewになっているかを常に意識して開発をすると綺麗なViewを保っていけると思いました。
画面遷移の実装
次に画面遷移の実装をします。
画面遷移のよくあるパターンとしては3通りです。
- present (モーダル遷移)
- push (プッシュ遷移)
- 今の画面を新しい画面に置き換える
個人的に感じたSwiftUIでの実装の難易度は簡単順に 3 > 1 >>> 2 です。UIKitでは 1 > 2 > 3 だと思っています。
モーダル遷移
モーダル遷移は
sheet
を使います。@State private var isPresented = false var body: some View { Button("Present") { self.isPresented = true } .sheet(isPresented: $isPresented) { NextView() } }シンプルで柔軟性があり、実装が容易です。
ただし、iOS 13から
pageSheet
スタイルがデフォルトになったため、SwiftUIでもそのスタイルになります。
画面を全部覆うfullScreen
スタイルをSwiftUIで実現するにはUIKitのpresent
を使うか、モーダル遷移アニメーションを自作し、ZStack
などを使って似た表示を再現する必要があります。https://stackoverflow.com/questions/56756318/swiftui-presentationbutton-with-modal-that-is-full-screen
https://stackoverflow.com/questions/56709479/how-to-modally-push-next-screen-to-be-full-in-swiftuiプッシュ遷移
プッシュ遷移は
NavigationLink
とNavigationView
を使います。var body: some View { NavigationView { NavigationLink("Push", destination: NextView()) .navigationBarTitle("Title") } }リンクボタンをタップしてプッシュ遷移する場合は、このようにシンプルになりますが、例えば、通信成功後にプッシュ遷移する場合はこのようになります。
@State private var isPushed = false var body: some View { NavigationView { VStack { Button("Fetch data") { // 通信の代わりに遅延させる DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // データの取得後Push self.isPushed = true } } // 見えないリンクを置いて遷移先を設定する NavigationLink(destination: NextView(), isActive: $isPushed, label: EmptyView.init) } .navigationBarTitle("Title") } }ちょっと違和感のある実装になってしまいますね。調べた限り、今のAPIではこのようになります。
そして、非同期で取得したデータを次の画面に渡す方法はどのようになるでしょうか。
何通りもやり方はありますが、素直に実装する場合はこのようになると思います。@State private var isPushed = false @State private var fetchedData: String? var body: some View { NavigationView { VStack { Button("Fetch data") { // 通信の代わりに遅延させる DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // データの取得後Push self.isPushed = true // 取得したデータの設定(new dataというデータを取得できたとする) self.fetchedData = "new data" } } if fetchedData != nil { // ViewBuilder内ではif letは使えない NavigationLink(destination: NextView(data: fetchedData!), isActive: $isPushed, label: EmptyView.init) } // if letの代わりにmapを使うこともできる(こっちの方が綺麗) // fetchedData.map { // NavigationLink(destination: NextView(data: $0), isActive: $isPushed, label: EmptyView.init) // } } .navigationBarTitle("Title") } }ちょっとずつらみが出てきましたね。画面遷移とデータ渡しがセットになっているケースを単純に実装するとプロパティの数がどんどん増えてしまいます。
そのため、ObservableObject
やカスタムView、structでデータをきれいにまとめるなどの工夫によって、Viewを清潔に保つように頑張る必要があります。この辺りは次のState設計の部分でいくつかパターンを紹介します。
プッシュ遷移周りはUIKitよりも明らかに面倒です。ナビゲーションバーやエッジスワイプ周りでも厄介な部分があるので、後で軽く紹介します。
今の画面を新しい画面に置き換える
今の画面をまるっと新しい画面に置き換える実装はかなり簡単です。
@State private var isBlueView = false var body: some View { VStack { if isBlueView { BlueView() } else { RedView() } Button(isBlueView ? "Red" : "Blue") { self.isBlueView.toggle() } } }bodyの中でif文を使うことができるので、そこで表示するViewを出し分けるだけで簡単に画面切替が可能です。
画面遷移実装のポイント
基本的にはUX優先で遷移方法を選択して良いと思います。ですが現在のSwiftUIのバグなどによっては問題を回避するワークアラウンドを考えるよりも遷移方法を見直すほうが良い場合も多いため、柔軟に仕様を変えられるようにしておきたいところです。
執筆時現在に起きているいくつかの問題/複雑なポイントを紹介します。
NavigationBarItemに置いたNavigationLinkでPushした後、Popするとクラッシュする
ナビゲーションバーにボタンを置いてPush遷移する動作はよくあるものですが、iOS 13.2 では
NavigationBarItem
に置いたNavigationLink
でPushした後、Popするとクラッシュしてしまいます。https://forums.developer.apple.com/thread/124757
回避方法としては
NavigationLink
ではなく、普通のButton
を置くようにし、前述したようなEmptyView
を持つNavigationLink
を用いて画面遷移するようにするとうまくいきます。navigationBarHiddenは親の設定が優先される
struct ContentView: View { var body: some View { NavigationView { NavigationLink("Push", destination: NextView()) .navigationBarHidden(true) .navigationBarTitle("") } } } private struct NextView: View { var body: some View { Color.blue .navigationBarHidden(false) .navigationBarTitle("Next View") } }このようなコードなら、プッシュ後にナビゲーションバーが表示されるようになりそうですが、実際はこのようになります。
動作を観察すると、ViewGraphの親の設定が優先されるようでした。
この現象を回避するためにはこのように親側の状態を更新する必要があります。
struct ContentView: View { @State private var isPushed = false var body: some View { NavigationView { NavigationLink(destination: NextView(), isActive: $isPushed, label: { Text("Push") }) .navigationBarHidden(!isPushed) .navigationBarTitle("") } } } private struct NextView: View { var body: some View { Color.blue .navigationBarTitle("Next View") } }sheetをメソッドチェインor入れ子にすると最後(親)のsheetしか動かなくなる
複数のモーダルを表示したいときに以下のように書きたくなりますが、Modal 1が動かなくなります。
struct SheetChain: View { @State private var isModal1Presented = false @State private var isModal2Presented = false var body: some View { VStack { Button("Modal 1") { self.isModal1Presented = true } Button("Modal 2") { self.isModal2Presented = true } } .sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) }) .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) }) } } private struct NextView: View { let color: Color var body: some View { color } }
sheet
がチェインしたり入れ子になっていると、最後(親)のsheet
しか動かなくなるようです。これを回避するためには
sheet
がチェインしないように各ボタンにsheet
を付けるようにします。struct SheetChain: View { @State private var isModal1Presented = false @State private var isModal2Presented = false var body: some View { VStack { Button("Modal 1") { self.isModal1Presented = true } .sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) }) Button("Modal 2") { self.isModal2Presented = true } .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) }) } } }シンプルな画面であればあまり問題になりませんが、複雑な画面でViewを細かくコンポーネント化し、入れ子にしていくと発生しやすいので
sheet
はできるだけ子のViewにつけるようにしたほうが良いです。
ちなみに入れ子で動かなくなるというのはこのような例です。var body: some View { VStack { Button("Modal 1") { self.isModal1Presented = true } Button("Modal 2") { self.isModal2Presented = true } .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) }) }.sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) }) }この場合は、Modal 2が動かなくなります。最後(親)のsheetしか動かなくなるためです。
State設計
SwiftUIの便利な機能の一つであるバインディングに必要なStateはアプリを作り進めていくとだんだんと増えていき、Viewの可読性が落ちていきがちです。
そこで、Stateをできるだけきれいに保つためにいくつかの便利なテクニックを紹介します。
関連性のあるStateはstructにまとめる
いろいろなサンプルコードで
@State
や@Published
をプリミティブ型に対して利用していることが多いですが、structでも利用可能です。前述したAPIからデータを取得した後にPush遷移する時のStateはこのように書くこともできます。
struct NavigationStateWithData<T> { var isActive: Bool = false { didSet { if !isActive, data != nil { data = nil } } } var data: T? { didSet { // 無限ループしないように代入前のチェックが必要 if (data == nil) == isActive { isActive.toggle() } } } } struct ContentView: View { @State private var fetchedData = NavigationStateWithData<String>() var body: some View { NavigationView { VStack { Button("Fetch data") { // 通信の代わりに遅延させる DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.fetchedData.data = "new data" } } fetchedData.data.map { NavigationLink(destination: NextView(data: $0), isActive: $fetchedData.isActive, label: EmptyView.init) } } .navigationBarTitle("Title") } } }まとまるとStateの関連性がわかりやすくなってよいですね。
上記の場合はデータがあるときに自動でPush遷移をするカスタムNavigationLinkを作るのもありかもしれません。ObeservableObject
@State
とstructの組み合わせでは表現しにくいViewの全体の状態を管理する時はObservableObject
を使います。
ObservableObject
はクラスにしか適合できないプロトコルのため、structでは値がコピーされてしまうようなViewを跨ぐ状態管理や、値更新時にViewを更新する必要がないプロパティなどを保持したりするのに便利です。また、
ObservableObject
で使用する@Published
は$
でアクセスすることで値を監視できるPublisher
として扱えるので、値の変化に伴って処理を挟むことができます。struct SettingView: View { @ObservedObject private var viewModel = SettingViewModel() var body: some View { Picker("テーマ", selection: $viewModel.theme) { Text("ライトモード").tag(UIUserInterfaceStyle.light) Text("端末の設定に従う").tag(UIUserInterfaceStyle.unspecified) Text("ダークモード").tag(UIUserInterfaceStyle.dark) }.pickerStyle(SegmentedPickerStyle()) } } class SettingViewModel: ObservableObject { @Published var theme = UIUserInterfaceStyle.unspecified private var cancellables: Set<AnyCancellable> = [] init() { $theme.sink { [weak self] theme in // 外観モードを切り替える let keyWindow = UIApplication.shared.windows.first { $0.isKeyWindow } keyWindow?.overrideUserInterfaceStyle = theme }.store(in: &cancellables) } }ロジックを
ObservableObject
に詰め込むと状態の更新タイミングが複雑になりがちです。できるだけUIに関係のある処理だけをここに記述するようにして、複雑なロジックは別の型に記述したほうが良いと思いました。EnvironmentObject
EnvironmentObject
は子View全てにオブジェクトを伝搬させることができる機能で、これを使うとViewを細かくコンポーネント化していったときに毎回initでオブジェクトを渡す必要がなくなるためとても便利です。ただ、なんでも
EnvironmentObject
で渡してしまうと、データがどこで更新されたのか分かりにくくなったり、Viewの使いまわしにくくなったりします。実際に使ってみて思ったGood/Badパターンはこちらです。
EnvrionmentObjectのGoodパターン
- アプリ全体で使う表示に関係する状態を管理する
- ログイン状態など
- 他の画面で使いまわさない子Viewにオブジェクトを渡す
- Fluxで実装した場合のStoreを子Viewに渡すときなど
EnvrionmentObjectのBadパターン
- 表示に全く関係しない状態を管理する
- それはシングルトンなオブジェクトで管理するほうが適しているかもしれません
- 子Viewに必要のないデータを含むオブジェクトを
EnvironmentObject
で渡す
- そのデータの監視方法は
@State
や@ObservedObject
に置き換えられるかもしれません- 他の画面でも使い回されるViewが特定のViewに依存した
EnvironmentObject
を参照している
- そのデータは
init
で渡すようにしたほうが良いかもしれませんState実装時のTips
Single Source of Truth
Appleは「Single Source of Truth」を推奨しています。これはデータソースは1つにしましょうという意味です。
SwiftUIの実装的には同じ意味を持つデータを別々の
@State
や@Published
で保持しないようにするということになります。値を保持せず参照だけしたい時は@Binding
を使いましょう。Bindingを使ったチュートリアル
https://developer.apple.com/tutorials/swiftui/handling-user-inputまた、あるStateに変更があった時に別のStateを変更したいというケースでは、Combineフレームワークを使って値を監視すると、同一ソースを複数のStateで保持する (Single Source of Truthに反する) 必要があるときにも多少安全です。
// ユーザーの入力によってリストの表示をフィルターする処理 class SearchViewModel: ObservableObject { @Published var keyword = "" @Published private(set) var items: [Item] = [] @Published private(set) var filteredItems: [Item] = [] // ↑computed propertyにすることも可能ですが、結果をキャッシュしたいという意図です // ... private var cancellables: Set<AnyCancellable> = [] init() { // 入力キーワードがnameに含まれているものをfilteredItemsにセットする $keyword.combineLatest($items).map { keyword, items in items.filter { item in item.name.localizedCaseInsensitiveContains(keyword) } } .assign(to: \.filteredItems, on: self) .store(in: &cancellables) } }
@State
プロパティはprivate
に
@State
のプロパティをViewのbody
外から操作すると実行時エラーになります。
想定外の用途を防ぐために、@State
のプロパティにはprivate
を付けるようにしたほうが良いです。こちらはSwiftUIのドキュメントにも明記されています。
you should declare your state properties as private, to prevent clients of your view from accessing it.
https://developer.apple.com/documentation/swiftui/stateまた、
@Published
のプロパティもprivate(set)
にできるケースは結構多いので、できるだけViewを更新可能な人物を減らすように意識していくと良いと思います。よく使うサービス/ツールとの相性
fastlane
今のところ何も問題なく利用できています。App Store Connectへのアップロードも全く問題ありませんでした。
Firebase Crashlytics
SwiftUIのViewはView BuilderとOpaque Result TypeによってView構造が型になっているため、クラッシュログのスタックトレースがこのようになります。
※自作の型名などはマスク処理しています。
一見複雑ですが、よく見るとViewのどの部分から起きている問題なのかが以外と分かるため、特に大きな問題は感じていません。
Admob
Admobを利用する場合は、ネイティブ広告で自前のSwiftUI Viewを組み立てるか、またはAdmobが提供しているUIKitのインターフェースをラップして利用することになります。
ラップして利用する場合はこのような実装になります。
バナー
struct AdBanner: UIViewControllerRepresentable { let adUnitId: String private var adSize: GADAdSize { UIDevice.current.userInterfaceIdiom == .pad ? kGADAdSizeFullBanner : kGADAdSizeBanner } func expectedFrame() -> some View { let size = adSize.size return frame(width: size.width, height: size.height, alignment: .center) } func makeUIViewController(context: Context) -> UIViewController { let view = GADBannerView(adSize: adSize) let viewController = UIViewController() view.adUnitID = adUnitId view.rootViewController = viewController viewController.view.addSubview(view) viewController.view.frame.size = adSize.size view.load(GADRequest()) return viewController } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } // body内 --- AdBanner(adUnitId: "***").expectedFrame()インタースティシャル
final class Interstitial: NSObject { private let adUnitId: String private var interstitial: GADInterstitial! private var completion: (() -> Void)? required init(adUnitId: String) { self.adUnitId = adUnitId super.init() load() } private func load() { interstitial = GADInterstitial(adUnitID: adUnitId) interstitial.load(GADRequest()) interstitial.delegate = self } func show(completion: @escaping () -> Void) { guard canShow(), interstitial.isReady, // ViewControllerの取得処理を簡略化していますが、場合により適切なWindowを選択して取得するように変える必要があります let root = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController else { completion() return } self.completion = completion interstitial.present(fromRootViewController: root) // 何度も表示されないように調整する場合はこの辺りに処理を書く } private func canShow() -> Bool { // 条件を満たしたら表示する return true } } extension Interstitial: GADInterstitialDelegate { func interstitialDidDismissScreen(_ ad: GADInterstitial) { completion?() load() // 再表示に備えて再読み込み } } // View内 --- private let interstitial = Interstital(adUnitId: "***") var body: some View { YourCustomView() .onAppear { self.interstital.show { // 広告を閉じた後の処理 } } }まとめ
現段階でSwiftUIを使う時の進め方や注意点、Tipsなどを紹介しました。
まだまだAPIが足りず、複雑な画面仕様を実現するには難しいケースもありますが、ある程度SwiftUIにアプリの仕様を寄せることができれば使っても良さそうです。
UIKitでレイアウトを作るよりも圧倒的に早く見た目が作れ、作った後のレイアウト変更も簡単なので、SwiftUIのバグさえ回避できればかなり楽にアプリが作れました。SwiftUIの破壊的変更や不具合と戦いながらその進化を見ていくのはエンジニアとしては面白い経験かと思うので、SwiftUIアプリ作りに挑戦してみてはどうでしょうか。
その他参考