- 投稿日:2019-12-01T22:20:07+09:00
xcframeworkを作成する(第1回)
GMOアドマーケティングのT.Oです。
Xcode11でフレームワークの新しい配布形態としてxcframeworkが導入されました。
xcframeworkを使用すると、iOS実機、iOSシミュレーターなどの複数のプラットフォーム用のフレームワークを1つにまとめることができます。またアプリに組み込む際には、それぞれのプラットフォームを意識することなく利用できるようになりました1。
ここではSwiftでxcframerworkを作成、利用する手順についてご紹介します。●前提
作成するxcframeworkは、iOS10以降で利用可能なEmbedded Frameworkをまとめたものとします。
(iOS13, iPadOSにも対応しています)
開発環境としてXcode 11.0を利用しSwift5を使用します。●xcframeworkの作成手順
1. プロジェクト作成
2. xcframework生成のためのビルド設定
3. Objective-C対応
4. テスト用のアプリ作成今回は上記の手順のうちの
「1. プロジェクト作成」
「2. xcframework生成のためのビルド設定」
についてまとめます。1.プロジェクト作成
まずフレームワーク用のテンプレートを選択してプロジェクトを作成します。
Xcodeで「File→New→Project..」メニューを選び、テンプレートの選択画面で「Cocoa Touch Framework」を選択します。
新規プロジェクトのオプション選択画面が表示されるので、Product Nameなどの設定を行います。
iOS10以降で利用可能としたいので、PROJECTのDeployment→iOS Deployment Targetで"iOS10.0"を選択します。
xcframeworkを作成するため、Build Options→Build Libraries for DistributionでYesを選択します。
次に「New File」を選択し、テンプレート選択画面で「Cocoa Touch Class」を選び
フレームワークの処理をSwiftで記述します。ここでは以下のように文字をコンソール出力することにします。
Ore.swiftimport UIKit public class Ore: NSObject { public func oreMethod(){ print("Hello Ore XCFramework"); } }ビルドターゲットをフレームワーク(この例だとOreXCFramework)に変更します。Product→Buildを選択しビルドします。ビルドエラーがないことを確認します。
2.xcframeworkのためのビルド設定
iOS実機、iOSシミュレータ用のフレームワークをビルドして1つのxcframeworkにまとめるためのビルド設定を行います。
「File→New→Target」で新規ターゲットのテンプレート選択画面で「Cross-platform→Aggregate」を選びます。
Product Nameを指定します。ここでは例として"Aggregate"と指定します。
次にTARGETSから作成した"Aggregate"を選択した状態で「Build Phases」を選び、「+」を選択して「New Run Script」を選びます。
Run Scriptが作成されます。
Run Scriptの"#Type a script or drag file from your workspace to insert its path"と記述されている部分に以下のxcframework出力用スクリプトを貼り付けます。
このスクリプトを実行するとフレームワークのプロジェクト以下にOutputディレクトリを作成します。そしてOutputディレクトリ以下にxcframework作成のための中間ファイル、archiveファイル、xcframeworkを出力します。# 出力先ディレクトリ(プロジェクトの直下) OUTPUT_DIR=${PROJECT_DIR}/Output # 中間ファイルの出力先ディレクトリ DERIVED_DIR=${OUTPUT_DIR}/${CONFIGURATION}-derived # archiveの出力先ディレクトリ ARCHIVE_DIR=${OUTPUT_DIR}/${CONFIGURATION}-archive # xcframeworkの出力先ディレクトリ XCFRAMEWORK_DIR=${OUTPUT_DIR}/${CONFIGURATION}-xcframework # 出力先ディレクトリ削除 rm -rf ${OUTPUT_DIR} # 中間ファイルの出力先ディレクトリ作成 mkdir -p ${DERIVED_DIR} # アーカイブファイルの出力先ディレクトリ作成 mkdir -p ${ARCHIVE_DIR} # xcframeworkの出力先ディレクトリ作成 mkdir -p ${XCFRAMEWORK_DIR} # iOS実機用のarchiveファイル ARCHIVE_FILE_IOS=${ARCHIVE_DIR}/ios.xcarchive echo "ARCHIVE_FILE_IOS:${ARCHIVE_FILE_IOS}" # iOSシミュレータ用のarchiveファイル ARCHIVE_FILE_IOS_SIMULATOR=${ARCHIVE_DIR}/iossimulator.xcarchive echo "ARCHIVE_FILE_IOS_SIMULATOR:${ARCHIVE_FILE_IOS_SIMULATOR}" # iOS実機用のarchiveファイル作成 xcodebuild archive -scheme ${PROJECT_NAME} -destination="iOS" -archivePath $ARCHIVE_FILE_IOS -derivedDataPath $DERIVED_DIR -sdk iphoneos SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES # iOSシミュレータ用のarchiveファイル作成 xcodebuild archive -scheme ${PROJECT_NAME} -destination="iOS Simulator" -archivePath $ARCHIVE_FILE_IOS_SIMULATOR -derivedDataPath $DERIVED_DIR -sdk iphonesimulator SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES # xcframework作成 xcodebuild -create-xcframework -framework $ARCHIVE_FILE_IOS/Products/Library/Frameworks/${PROJECT_NAME}.framework -framework $ARCHIVE_FILE_IOS_SIMULATOR/Products/Library/Frameworks/${PROJECT_NAME}.framework -output $XCFRAMEWORK_DIR/${PROJECT_NAME}.xcframework # xcframeworkの出力ディレクトリをFinderで開く open ${XCFRAMEWORK_DIR}ビルドターゲットをAggregateに変更します。そしてProductメニュから→Buildを選択して
ビルドします。
ビルドが成功したら生成されたxcframeworkをFinderやtreeコマンドなどで確認します。
実機用フレームワーク(ios-armv7_arm64以下)とシミュレータ用フレームワーク(ios-i386_x86_64-simulator以下)が両方とも含まれている状態になっていればここまでの手順は完了です。%tree OreXCFramework.xcframework OreXCFramework.xcframework ├── Info.plist ├── ios-armv7_arm64 │ └── OreXCFramework.framework │ ├── Headers │ │ ├── OreXCFramework-Swift.h │ │ └── OreXCFramework.h │ ├── Info.plist │ ├── Modules │ │ ├── OreXCFramework.swiftmodule │ │ │ ├── arm.swiftdoc │ │ │ ├── arm.swiftinterface │ │ │ ├── arm64-apple-ios.swiftdoc │ │ │ ├── arm64-apple-ios.swiftinterface │ │ │ ├── arm64.swiftdoc │ │ │ ├── arm64.swiftinterface │ │ │ ├── armv7-apple-ios.swiftdoc │ │ │ ├── armv7-apple-ios.swiftinterface │ │ │ ├── armv7.swiftdoc │ │ │ └── armv7.swiftinterface │ │ └── module.modulemap │ ├── OreXCFramework │ └── _CodeSignature │ └── CodeResources └── ios-i386_x86_64-simulator └── OreXCFramework.framework ├── Headers │ ├── OreXCFramework-Swift.h │ └── OreXCFramework.h ├── Info.plist ├── Modules │ ├── OreXCFramework.swiftmodule │ │ ├── i386-apple-ios-simulator.swiftdoc │ │ ├── i386-apple-ios-simulator.swiftinterface │ │ ├── i386.swiftdoc │ │ ├── i386.swiftinterface │ │ ├── x86_64-apple-ios-simulator.swiftdoc │ │ ├── x86_64-apple-ios-simulator.swiftinterface │ │ ├── x86_64.swiftdoc │ │ └── x86_64.swiftinterface │ └── module.modulemap ├── OreXCFramework └── _CodeSignature └── CodeResources参考
執筆にあたり以下を参考にさせていただきました。
Binary Frameworks in Swift - WWDC 2019 - Videos - Apple Developer
いけだや技術ノート Xcode 11で導入されるxcframeworkのディレクトリ構造明日は、「xcframeworkを作成する(第2回)」です。
引き続き、GMOアドマーケティング Advent Calendar 2019をお楽しみください!
Xcode10までは、各プラットフォーム用のフレームワークを1つのファイルにまとめることはできていたのですが、それをアプリで利用する際に必要なプラットフォームに適合したフレームワークのみを切り出すためのスクリプトを実行する必要があり、手順がやや煩雑になっていました。 ↩
- 投稿日:2019-12-01T22:17:22+09:00
Working with UI Controlsチュートリアルが動かない件の対処
Working with UI Controlsチュートリアルが動かない件の対処
SwiftUIのユーザ入力画面チュートリアルWorking with UI Controlsですが、チュートリアルの通り実装を行っても、ProfileHome.swiftのPreview(Canvas)が動きません。
以下の2点を解消することで動作させることができます。
画面が表示されない
.onAppear
において、初期化されていないuserData
にアクセスしているため。初期化すればよいので、以下のようにしてEnvironmentObjectを初期化しましょう。
ProfileHost.swiftstruct ProfileHost_Previews: PreviewProvider { static var previews: some View { ProfileHost() + .environmentObject(UserData()) } }サマリー画面が表示されない
Canvas環境ではeditModeの環境変数が初期化されていないため。
これはXCode側の問題ですが、Appleが本件に対処するまでは、以下のように試験対象ビューにラッパーを噛ませて、ラッパー側でeditModeの実体を宣言することで対応できます。
ProfileHost.swift+ +struct PreviewWrapperView: View { + @State var editMode : EditMode = .inactive + + var body: some View { + return ProfileHost().environment(\.editMode, $editMode) + } +} + struct ProfileHost_Previews: PreviewProvider { static var previews: some View { - ProfileHost() + PreviewWrapperView() } }最終形
上記2点を併せたソースがこちらです。
ProfileHost.swiftstruct PreviewWrapperView: View { @State var editMode : EditMode = .inactive var body: some View { return ProfileHost().environment(\.editMode, $editMode) } } struct ProfileHost_Previews: PreviewProvider { static var previews: some View { PreviewWrapperView() .environmentObject(UserData()) } }
- 投稿日:2019-12-01T22:14:52+09:00
SwiftでシンプルなRSSリーダーを作る(Yahooニュース編)
SwiftでシンプルなRSSリーダーを作る
暇だったのでYahooニュースの記事を閲覧できるRSSリーダーを作ってみました。
作ってみてわかりましたが、RSSリーダーの作成はSwift初心者の学習に有効だと思います。
いろんなサイトがRSSを公開していますし、URLを叩いて情報を取ってきて加工してtableViewに表示するというiOSの基本が学べます。<捕捉>そもそもRSSって何?
RSSとは「Really Simple Syndication」の略語であり、ニュースや記事のタイトル、リンクなどの内容を要約して配信する
仕組みのことです。RSSは記事取得用のurlが用意されており、そのurlを叩くとXML形式で記事の内容を返してくれます。用件
- RSSを取得して「タイトル」「日付」「サムネイル画像」の3つをtableViewに表示する。
- 記事をタッチして該当のニュースをwebViewで表示する
- タブメニューで記事の種類を選択できるようにする
開発環境
エディタ・言語など
- Xcode11.1
- Swift5.1
- cocoaPods1.5.3
使用ライブラリ
- XLPagerTabStrip
- ページメニュー実装用
- SDWebImage
- 画像取得+キャッシュ用
- HTMLReader
- サムネイル画像取得用
RSSから記事を取得する
まずRSSから記事を取得してtableViewに渡す処理を作っていきます。
今回使用するのはYahoo!ニュースのRSSです。
https://headlines.yahoo.co.jp/rss/listRSSはXML形式のレスポンスを返します。
XMLの扱うために以下2点の方法を考えました。
* 1.Swift標準のXMLParserを使用する。
* 2.XMLをJsonに変換し、JsonをCodableでモデル化して使用する。それぞれメリットデメリットはありますが、Jsonの方が色々とこねくり回しやすいので、今回は2番目の方法で実装します。
XMLをJsonに変換する
XMLからJsonへの変換はAPIを通して行いました。
以下のサイトがとても便利でした。
[rss to json]https://rss2json.com/#rss_url=http%3A%2F%2Ffeeds.twit.tv%2Fbrickhouse.xmlrssToJsonでは、RSSのurlを入力するだけでJsonに変換したレスポンスを表示し、Json取得用のAPIを作成してくれます。
APIを用意できたら記事を取得する準備をしていきましょう。
やることは以下の2つです。
* レスポンスを変換するモデルを作成する。
* APIを叩いて記事を取得する。レスポンス変換用のモデルを用意する
APIのレスポンス変換用にモデルを作成します。
まずは元のJsonの構造を理解しましょう。
{ "status": "ok", "feed": { "url": "https://news.yahoo.co.jp/pickup/rss.xml", "title": "Yahoo!ニュース・トピックス - 主要", "link": "https://news.yahoo.co.jp/", "author": "", "description": "Yahoo! JAPANのニュース・トピックスで取り上げている最新の見出しを提供しています。", "image": "" }, "items": [ { "title": "官邸「譲らない」GSOMIA折衝", "pubDate": "2019-11-23 02:28:19", "link": "https://news.yahoo.co.jp/pickup/6343284", "guid": "yahoo/news/topics/6343284", "author": "", "thumbnail": "", "description": "", "content": "", "enclosure": { "link": "https://s.yimg.jp/images/icon/photo.gif", "type": "image/gif", "length": 133 }, "categories": [] },この構造を踏まえてモデルを作成します。
以下のようになりました。/// RSSから取得する記事リスト struct ArticleList: Codable { let status: String let feed: Feed let items: [Item] } /// フィード struct Feed: Codable { let url: String let title: String let link: String let author: String let description: String } /// 記事詳細 struct Item: Codable { let title: String let pubDate: String let link: String let guid: String }使わないデータや空のデータはモデルに含めなくても問題ありません。
これでレスポンス変換用のモデルができたので、APIを叩いて記事のJsonを取ってきましょう。APIを叩いて記事を取得する
引数にURLを渡すとそのURLから記事を取得する関数を作ります。
/// RSS取得用クラス class RssClient { /// 記事の一覧を取得します。 /// - Parameter urlString: 取得元RSSのurl /// - Parameter completion: 完了時の処理 static func fetchItems(urlString: String, completion: @escaping (Result<[Item], Error>) -> ()) { // URL型に変換できない文字列の場合は弾く guard let url = URL(string: urlString) else { completion(.failure(NetworkError.invalidURL)) return } let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NetworkError.unknown)) return } let decoder = JSONDecoder() guard let articleList = try?decoder.decode(ArticleList.self, from: data) else { completion(.failure(NetworkError.invalidResponse)) return } completion(.success(articleList.items)) }) task.resume() } }上記のコードでは3つのことをやっています。
- APIのURLをString型からURL型に変換する。
- URLSession.shared.dataTaskに渡して結果を受け取る。
- 受け取った結果をJson型からArticleList型に変換する
ポイントは関数のcompletionクロージャにResult型の引数を使っていることです。
completion: @escaping (Result<[Item], Error>) -> ()こいつを引数に渡してあげると、Result型のcase次第で成功と失敗を同じクロージャで使い分けることができます。
/// A value that represents either a success or a failure, including an /// associated value in each case. public enum Result<Success, Failure> where Failure : Error { /// A success, storing a `Success` value. case success(Success) /// A failure, storing a `Failure` value. case failure(Failure)成功ならsuccessで、失敗ならfailureでcompletionを実行し、
実行時の引数には記事取得結果であるItems
型と、エラーが起きたときのError
型を渡してやります。// 成功 completion(.success(articleList.items)) // 失敗 if let error = error { completion(.failure(error)) return }これでTableViewに記事を表示する準備ができました。
取得した記事をTableViewに表示する
関数を用意できたのでTableViewを表示するクラス側で呼び出してあげます。
/// ホーム画面 class NewsViewController: UITableViewController, IndicatorInfoProvider { /// 記事一覧 private var items: [Item] = [] { didSet { tableView.reloadData() } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) RssClient.fetchItems(urlString: self.newsType.urlStr, completion: { (response) in switch response { case .success(let items): DispatchQueue.main.async() { [weak self] in guard let me = self else { return } me.items = items } case .failure(let err): print("記事の取得に失敗しました: reason(\(err))") } }) }
viewWillAppear
で記事を取得し、記事一覧のプロパティに記事をセットしたらtableViewのリロードが走るようにしています。/// 記事一覧 private var items: [Item] = [] { didSet { tableView.reloadData() } }Result形で定義していたcompetionクロージャは、
Result<[Item], Error>
を引数として、成功時の処理と失敗時の処理をswitchで分けて実行することができています。RssClient.fetchItems(urlString: self.newsType.urlStr, completion: { (response) in switch response { // 成功 case .success(let items): DispatchQueue.main.async() { [weak self] in guard let me = self else { return } me.items = items } // 失敗 case .failure(let err): print("記事の取得に失敗しました: reason(\(err))") } })これで取得した記事をTableViewに表示できるようになりました。
サムネイル画像を取得する
RSSから記事は取得できました。
しかしYahooニュースのRSSにはサムネイル画像用のurlが用意されていません。
そこで記事のHTMLからサムネイル画像のurlを抽出し、そのurlからUIImageを取得してセルに表示することにしました。やることは以下の2つです。
* 記事のURLからHTMLを取得する。
* サムネイル画像用のURLを抽出する。記事のURLからHTMLを取得する
まずはサムネイルを取得したい記事のURLからHTMLのソースを取得してきます。
/// 記事の画像のurlを取得します /// - サムネイル表示のために用意 /// - Warning: URLから取得先のHTML全部取ってきてサムネだけ抜き出した上で画像のURL返してるから非常に冗長。 /// かつロードにも時間がかかるのでキャッシュに持たせるとかして修正を検討してください。 /// /// - Parameter urlStr: 画像取得先のurl /// - Parameter completion: 完了後の処理 static func fetchThumnImgUrl(urlStr: String, completion: @escaping (Result<URL, Error>) -> ()) { // URL型に変換できない文字列の場合は弾く guard let targetURL = URL(string: urlStr) else { completion(.failure(NetworkError.invalidURL)) return } do { // 入力したURLのページから、HTMLのソースを取得します。 let sourceHTML = try String(contentsOf: targetURL, encoding: String.Encoding.utf8) catch { completion(.failure(error)) } }SwiftのString型は指定したURLからHTMLを取得してエンコードすることができるみたいです。
TODO:HTML全部取ってくるのは重いし冗長なのでサムネイルだけ取得するように直したい
今回は記事のHTMLを全て取ってきているんですが、欲しいのはサムネイル画像だけなので冗長な作りになってしまいました。
セルの生成する度に毎度記事のHTMLを全て取得していると、ロードに時間がかかってアプリの操作感が悪くなります。
今回この問題は解決できませんでしたが、部分的に必要な情報だけ取得するように直すのが今後の課題になりました。HTMLからサムネイル画像を抽出する
HTMLが取得できたらHTMLReaderを使ってサムネイル画像だけ抜き出しましょう。
import HTMLReader /// RSS取得用クラス class RssClient { ... /// 記事の画像のurlを取得します /// - サムネイル表示のために用意 /// - Warning: URLから取得先のHTML全部取ってきてサムネだけ抜き出した上で画像のURL返してるから非常に冗長。 /// かつロードにも時間がかかるのでキャッシュに持たせるとかして修正を検討してください。 /// /// - Parameter urlStr: 画像取得先のurl /// - Parameter completion: 完了後の処理 static func fetchThumnImgUrl(urlStr: String, completion: @escaping (Result<URL, Error>) -> ()) { // URL型に変換できない文字列の場合は弾く guard let targetURL = URL(string: urlStr) else { completion(.failure(NetworkError.invalidURL)) return } do { // 入力したURLのページから、HTMLのソースを取得します。 let sourceHTML = try String(contentsOf: targetURL, encoding: String.Encoding.utf8) let html = HTMLDocument(string: sourceHTML) // サムネイルの入ったエレメントを抜き出します。 let htmlElement = html.firstNode(matchingSelector: "p[class^=\"tpcHeader_thumb_img\"]") // エレメントからstyleだけ抽出します。 guard let style = htmlElement?.attributes["style"] else { completion(.failure(AppalicationError.unknown)) return } // 無駄な文字列を削除して整形します。 let imageUrlStr: String = { let startIndex = style.index(style.startIndex, offsetBy: 23) let endIndex = style.index(style.endIndex, offsetBy: -3) return String(style[startIndex..<endIndex]) }() guard let imageUrl = URL(string: imageUrlStr) else { completion(.failure(NetworkError.invalidURL)) return } completion(.success(imageUrl)) } catch { completion(.failure(error)) } } }
HTMLReader
はSwift版のHTMLパーサーライブラリであり、HTMLドキュメントから欲しいタグの情報を抽出することができます。
まずStringのHTMLからHTMLドキュメントを作ります。
僕はHTMLもCSSもよく分からないんですが、どうやらCSSSelectorというものを検索できるみたいです。
今回はtpcHeader_thumb_img
とやらにサムネイルのurlがあったのでそこだけ取得することにしました。<div class="tpcHeader_thumb"> <p class="tpcHeader_thumb_img" style="background-image: url('https://giwiz-tpc.c.yimg.jp/q/iwiz-tpc/images/tpc/2019/11/19/1574165035_20191113-00000622-san-000-view.jpg');"></p> </div>該当の箇所↓
HTMLは正しい用語が分からないのでコメントの内容間違ってたりしたらすみませんmm// 文字列のHTMLからHTMLDocumentを生成します。(ここからHTMLReaderの機能) let html = HTMLDocument(string: sourceHTML) // サムネイルの入ったエレメントを抜き出します。 let htmlElement = html.firstNode(matchingSelector: "p[class^=\"tpcHeader_thumb_img\"]") // エレメントからstyleだけ抽出します。 guard let style = htmlElement?.attributes["style"] else { completion(.failure(AppalicationError.unknown)) return }これでサムネイル画像取得用の関数が用意できました。
そしたらこの画像をTableViewに表示していきましょう。取得したサムネイル画像をTableViewに表示する
サムネイルの取得に成功したらSDWebImageを使ってURLから画像をロードします。
RssClient.fetchThumnImgUrl(urlStr: link, completion: { response in switch response { case .success(let url): SDWebImageManager.shared.loadImage(with: url, options: .progressiveLoad, context: nil, progress: nil, completed: { (image, data, error, cache, finished, url) in articleCell.articleImage.image = image }) case .failure(let err): print("HTMLの取得に失敗しました: reason(\(err))") } })SDWebImageは画像の取得、キャッシュの保存、キャッシュからの画像の読み出し、UIImageへのセットなどをよしなにやってくれるライブラリです。
SDWebImageManager.shared.loadImage
を使って画像をDLすると指定したURLから画像を取得、キャッシュに保存してくれます。キャッシュに既にデータがあれば次回DL時はキャッシュから画像を呼び出しくれるという神仕様です。ここまででRSSから「タイトル」「日付」「サムネイル」を取得し、セルに表示できるようになりました。
記事をタッチして該当のニュースをwebViewで表示する
今度はセルをタッチして記事の詳細を表示します。
RSSでは各記事の本文まで取得できないので、こっちはWebViewを使って作ることにしました。まずはWebKitをインポートし、WebView表示用のクラスを作ります。
import UIKit import WebKit class DetailWebViewController: UIViewController { // MARK: Properties /// 記事表示用webView private let wkWebView = WKWebView() /// 読み込むURL private var urlStr: String? // MARK: Initializer init(urlStr: String) { self.urlStr = urlStr super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: LifeCycle override func viewDidLoad() { super.viewDidLoad() // TODO: WebView適当すぎるのでプログレスバーとか戻るボタンとか付けたい wkWebView.frame = view.frame wkWebView.navigationDelegate = self wkWebView.uiDelegate = self wkWebView.allowsBackForwardNavigationGestures = true let url = URLRequest(url: URL(string: urlStr!)!) wkWebView.load(url) view.addSubview(wkWebView) } } extension DetailWebViewController: WKNavigationDelegate { } extension DetailWebViewController: WKUIDelegate { }ViewControllerのViewと同じ広さのWebViewのインスタンスを作成し、URLを渡して読み込んでもらうだけのシンプルなクラスです。
(プチTODO:ちょっとシンプルすぎるのでプログレスビューとか戻るボタンとか今後付けたい)WebView表示用クラスができたら、セルのタッチで画面遷移するように実装します。
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let link = items[indexPath.row].link let vc = DetailWebViewController(urlStr: link) self.navigationController?.pushViewController(vc, animated: true) }これで記事をWebViewで閲覧できるようになりました。
タブメニューで記事の種類を選択できるようにする
本家Yahooニュースしかりグノシーしかり、ニュースアプリ大体画面上部にタブメニューを持っており、タブの切り替えでニュースの分類を切り替えるように作られています。
というわけでトピックの数だけタブメニューを作成するように実装していきます。タブメニューを作成できるライブラリはいくつかありますが、今回はXLPagerTabStripというライブラリを採用してみました。
- VCを渡すだけでいいから簡単
- ライブラリをインポートしたクラスからnavigationBarにアクセスできる。(他のライブラリはできないやつもあるらしい)
この辺りが採用の決め手です。
それではタブメニューを作っていきます。
やることは以下の3つです。
- XLPagerTabStripをインストール
- タブメニュー実装用親クラスを作成する
- タブメニューの中身になる子VCを作成する
XLPagerTabStripをインストール
CocoaPods、もしくはCarthageを使ってインポートできます。
やり方は本家githubから確認できます。
https://github.com/xmartlabs/XLPagerTabStrip#cocoapodsタブメニュー表示用の親クラスを作る
インストールが完了したら、タブメニューを表示するための親クラスを作っていきます。
今回はstoryboardを使いました。タブメニューを実装する場合、親クラスのstoryboardに以下の2点を持つ必要があります。
- タブメニューの置き場となるCollectionView
- 各ページのVCが配置されるScrollView
各パーツの設定は詳しく説明してくださってる記事があるのでそちらを参考にしてください。(断じて書くのがめんどくさいわけではない。)
XLPagerTabStripの使い方とカスタマイズ
storyboardが用意できたら、今度はコードで必要な設定を書いていきます。
難しいことはなかったです。ポイントは3つあります。
- ButtonBarPagerTabStripViewControllerを継承したクラスを作る
- viewControllersメソッドをoverrideしてその返り値にメニューに詰め込みたいViewControllerを渡してあげる
super.viewDidLoad()
より上にタブメニューのレイアウト設定を書く実際に書くとこんな感じになります。
import XLPagerTabStrip /// ページメニュー用ViewController class PageMenuViewController: ButtonBarPagerTabStripViewController { // MARK: LifeCycle override func viewDidLoad() { setButtonBar() super.viewDidLoad() navigationItem.title = "Yahoo!ニュース" } /// メニューに表示するVCを渡す override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { return createNewsViewController() } // MARK: Private Function /// タブメニューのレイアウトを設定します /// - Warning: 必ずsuper.viewDidLoad()の上で呼び出してください。 private func setButtonBar() { settings.style.buttonBarBackgroundColor = .clear settings.style.selectedBarBackgroundColor = .orange settings.style.buttonBarMinimumLineSpacing = 2 } }タブメニューの中身になる子VCを作成する
今度はメニューに表示する子VCを作ります。
子VC側はstoryboardやxibの設定は必要ありません。
基本的には上で実装したクラスにVCを渡すだけなんですが、2点やることがあるので説明します。
IndicatorInfoProvider
プロトコルにVCを適合させるindicatorInfo
メソッドにタブメニューの情報を追加するimport XLPagerTabStrip /// ホーム画面 class NewsViewController: UITableViewController, IndicatorInfoProvider { ... /// タブメニュー編集用インスタンス private var itemInfo = IndicatorInfo(title: "タブ名") // MARK: - IndicatorInfoProvider func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo { return itemInfo } }
IndicatorInfoProvider
プロトコルに準拠するとindicatorInfo
メソッドの実装を要求されます。
このindicatorInfo
に渡すのがタブメニューのタイトルや各トピックのごとにメニューに表示したい画像などのタブメニューの情報です。IndicatorInfoのstructを見てみるとどんな設定値が用意されてるか分かります。
public struct IndicatorInfo { public var title: String? public var image: UIImage? public var highlightedImage: UIImage? public var accessibilityLabel: String? public var userInfo: Any? ...まとめ
ここまでの紹介した手順でRSSリーダーというか、Yahooニュースもどきができました。
コミットの単位とかめちゃくちゃですが一応githubにコードを載せてます。
https://github.com/kawano108/YahooNewsMirrorRSSから記事を取ってきて表示するだけの予定でしたが、サムネイルのURLがレスポンスに入ってなかったりと結構てこずりました。
けどHTMLから取得した情報をSwiftで使う方法がわかったので良かったです。
YahooニュースのRSSは結構スカスカだったんですが、はてなのRSSは充実してると後から知りました。
今度ははてなのRSSを使って何か作ってみたいです。
- 投稿日:2019-12-01T21:17:59+09:00
【iOS 13】アプリアイコンをダークモード対応させることは可能なのか検証してみる!!
はじめに
皆さんはダークモード対応されていますか?
だいぶ対応したアプリが増えてきたように感じます。
対応するのは大変(主に工数が)ですがやっぱり対応したら謎の満足感あります。私は個人開発のアプリでは対応しましたが,
まだ業務で担当している案件では対応できていないです。Xcode では,画像や色のアセットでライト/ダークモードで
それぞれ使う色,画像を設定できるのですごく楽ですよね。アプリアイコンはダークモード対応できないのかな??
と思って調べてみました。ちなみにアプリアイコンは Xcode にダークモードなどの設定はないですね。
YUMEMI.swift #5 でトークした内容になります。
https://yumemi.connpass.com/event/153206/アプリアイコンを変更する(iOS 10.3 ~)
アプリアイコンの変更や名前取得などは iOS 10.3 から使えます。
// hogeという名前のアプリアイコンを設定する if #available(iOS 10.3, *) { UIApplication.shared.setAlternateIconName("hoge") { error in if let error = error { print(error.localizedDescription) } } }元のアイコンに戻すときは nil をセットすれば良い。
// 元のアイコンに戻す if #available(iOS 10.3, *) { UIApplication.shared.setAlternateIconName(nil) { error in if let error = error { print(error.localizedDescription) } } }注意点として下記があります。
info.plist
に利用するアイコン情報を記載するViewController
に記載する- メインスレッドで実行する
- 変更するアプリアイコンは
xcassets
を利用せず直接導入するこれらのコードとライト/ダークモードが切り替わった際に呼ばれる,
メソッドをoverride
して,Appearance が切り替わった際に
それぞれのモードに対応したアプリアイコンを変更させようとしてみます。実装
実装環境
- Xcode 11.2.1
- macOS Catalina 10.15.1
- iOS 13 and later
サンプルコードは GitHub に用意しました。
必要に応じて参照ください。
https://github.com/MilanistaDev/AppIconCorrespondingToDarkMode
本実装
ライトモードで設定するアプリアイコン画像をセット
ライトモードで設定するアプリアイコンを
xcassets
の AppIcon で設定します。
いわゆるいつものアプリアイコンセット作業です。
ダークモードで設定するアプリアイコン画像を準備
ダークモード用アプリアイコンの画像を用意します。
@2x
,@3x
でそれぞれ 120x120,180x180 ピクセルです。
名前は AppIcon-Dark としました。
用意した画像をプロジェクトに追加
先にあった通り
Assets.xcassets
で AppIcon を追加しても
ダークモード用の設定ができません。
なので直接プロジェクトに追加します。
info.plist を編集
追加した画像を利用するために
info.plist
を編集する必要があります。
下記コードを追加します。<key>CFBundleIcons</key> <dict> <key>CFBundlePrimaryIcon</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIcon</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> <key>CFBundleAlternateIcons</key> <dict> <key>AppIcon-Dark</key> <dict> <key>CFBundleIconFiles</key> <array> <string>AppIcon-Dark</string> </array> <key>UIPrerenderedIcon</key> <false/> </dict> </dict> </dict>見やすいプロパティリストで見ると下記のようになります。
Primary Icon の方がライトモードのアプリアイコン設定で,
黄色の四角の部分が追加したダークモード用アプリアイコン画像のファイル名です。
ライト/ダークモードの設定でアプリアイコンを切り替える
ViewController
でアプリアイコンを変更するコードを書く必要があるので,
例えばViewController.swift
に下記のようなコードを書きます。
このコードで Appearance が変更された際に,
現在のモードを判別してアプリのアイコンを切り替える処理が実行されます。ViewController.swiftoverride func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if self.traitCollection.userInterfaceStyle == .dark { // ダークモード用のアプリを設定する UIApplication.shared.setAlternateIconName("AppIcon-Dark") { error in if let error = error { print(error.localizedDescription) } } } else { // nilをセットしデフォルトのアプリアイコン画像に変更 UIApplication.shared.setAlternateIconName(nil) { error in if let error = error { print(error.localizedDescription) } } } }が,しかし・・・
コントロールセンターでライト/ダークモードを切り替えると
エラー出力部分を通過することがわかります。エラー内容は下記です。
The operation was cancelled.
いろいろ調べて遅延実行するとうまくいくとのことで
下記コードで包んであげると・・・DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { }ブレイクポイント止めるとライト/ダークモード切り替え時に,
ちゃんとアプリアイコン変更ダイアログが表示されます。
しかし,ブレイクポイントを貼らなければ状況は変わりませんでした。
ライト->ダーク ダーク->ライト 結果
ライトモードとダークモードの変更時にアプリアイコンを変更することはできなさそう。
この変更がユーザのアプリ内でのアクションではなく,
アプリ外の操作(iOSによるもの)のため,おそらくキャンセルされてしまうのかなと考えました。
(ユーザが起こしているアクションには違いないのですが?)では,ユーザが起こす,アプリ内のトリガーで今のモードを判別して,
アプリアイコン画像をセットする処理を書くとちゃんと設定できるのかな?ユーザが選択可能にする
というわけで,よくある仮の設定画面を用意し,
アプリアイコン画像を現在の Appearance に合わせる,的な
処理を書いてみます。仮の設定画面はこんな感じで適当に準備しました。
最初のセクションの2番目のセルをタップして
アプリアイコン画像を変える処理を書いてみます。
ライトモード ダークモード
UITableView
の delegate メソッドのコード。SettingsViewController.swiftextension SettingsViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { self.tableView.deselectRow(at: indexPath, animated: true) if indexPath.section == 0 && indexPath.row == 1 { // セルがタップされた際にアプリアイコン画像を変更 self.matchAppIcon() } } }現在の Appearance の状態によってライトモード,ダークモード用の
アプリアイコン画像を設定するコードはこんな感じで書いてみました。SettingsViewController.swift/// Match the App Icon to the current Appearance private func matchAppIcon() { if self.traitCollection.userInterfaceStyle == .dark { // ダークモード用のアプリを設定する UIApplication.shared.setAlternateIconName("AppIcon-Dark") { error in if let error = error { print(error.localizedDescription) } } } else { // nilをセットしデフォルトのアプリアイコン画像に変更 UIApplication.shared.setAlternateIconName(nil) { error in if let error = error { print(error.localizedDescription) } } } }セルをタップしてみたところ,ダイアログが表示され,
アプリアイコン画像を変更することができました。
ライトモード ダークモード 実際の動きは下記のようになります。
ライトモード,ダークモードに切り替わった際にはアプリアイコンはそのまま,
セルのタップをした際に初めてダイアログが出てアプリアイコンが変更されるという感じです。
ライト=>ダーク ダーク=>ライト おわりに
コントロールセンターなどでライトモード,ダークモードを切り替える際に
それぞれのモードに対応したアプリアイコン画像を変更することはできないっぽい。セルのタップやボタンタップなどユーザが起こすアクションをトリガーとすると,
変更した旨のダイアログが出て,各モードに対応したアプリアイコンに変更することは可能でした。ライトモード,ダークモード切り替え時にアプリアイコン変更できるぜ,
もっとこうした方がクレバーですよー等ありましたらご教示いただければ幸いです。ご覧いただきありがとうございました!
- 投稿日:2019-12-01T20:43:17+09:00
SwiftUIでViewを画像としてUIActivityを利用してSNSに共有する
はじめに?
SwiftUIで特定のViewをSNSにシェアするtipsを共有します?
使いたかったLPLinkMetadata
iOS13から使えるようになったLPLinkMetadataを利用します。
これを
↓のようにすることができます。
UIActivityViewControllerをRepresentaぶる
UIActivityViewControllerをSwiftUIで扱えるようにします。
struct ShareSheet: UIViewControllerRepresentable { let photo: UIImage func makeUIViewController(context: Context) -> UIActivityViewController { let text = "?" let itemSource = ShareActivityItemSource(shareText: text, shareImage: photo) let activityItems: [Any] = [photo, text, itemSource] let controller = UIActivityViewController( activityItems: activityItems, applicationActivities: nil) return controller } func updateUIViewController(_ vc: UIActivityViewController, context: Context) { } }iOS13からのLPLinkMetadataを使う
import LinkPresentation class ShareActivityItemSource: NSObject, UIActivityItemSource { var shareText: String var shareImage: UIImage var linkMetaData = LPLinkMetadata() init(shareText: String, shareImage: UIImage) { self.shareText = shareText self.shareImage = shareImage linkMetaData.title = shareText super.init() } func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { return UIImage(named: "AppIcon ") as Any } func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { return nil } func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { return linkMetaData } }ViewをUIViewに変換する
SwiftUI でViewをUIImageに変換する方法
前回書いた記事を参考にSwiftUIのViewをUIImageに変更する方法をご参照ください。作成したShareSheetをモーダルで呼び出す
struct TestPage: View { @State private var rect: CGRect = .zero @State private var uiImage: UIImage? = nil // modalを表示するためのフラグを持つ @State private var showShareSheet = false var body: some View { VStack { HStack { Image(systemName: "sun.haze") .font(.title) .foregroundColor(.white) Text("Hello, World!") .font(.title) .foregroundColor(.white) } .padding() .background(Color.blue) .cornerRadius(8) .background(RectangleGetter(rect: $rect)) .onAppear() { } Button(action: { self.uiImage = UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect) self.showShareSheet.toggle() }) { Image(systemName: "square.and.arrow.up") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 50, height: 50) .padding() .background(Color.pink) .foregroundColor(Color.white) .mask(Circle()) }.sheet(isPresented: self.$showShareSheet) { // 共有したいUIImageを渡す ShareSheet(photo: self.uiImage!) }.padding() } } }結果
ちゃんとツイートできました!
— つっきー@SwiftUI (@tsuzuki817) December 1, 2019全体のコード
https://gist.github.com/tsuzukihashi/d08fce005a8d892741f4cf965533bd56
- 投稿日:2019-12-01T20:42:45+09:00
SwiftUIでのMFMailComposeViewControllerの使用
前置き
やりたいこと
アプリ内からメールを送れるようにしたい。
sheetを用いてメーラーを表示し、ユーザーはそこで編集・送信・キャンセル等を行う。環境
- Xcode: 11.2.1
- Swift: 5.1.2
- 実機: 13.2.3
実装
下記のMailViewというViewを用意して、必要な箇所(今回の場合はContentView)でそれを呼び出すようにします。
MailView.swiftimport SwiftUI import MessageUI struct MailView: UIViewControllerRepresentable { @Binding var isShowing: Bool func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> UIViewController { let controller = MFMailComposeViewController() controller.mailComposeDelegate = context.coordinator controller.setSubject("これが件名") controller.setToRecipients(["hogehoge@hogehoge.com"]) controller.setMessageBody("これが本文", isHTML: false) return controller } func makeCoordinator() -> MailView.Coordinator { return Coordinator(parent: self) } class Coordinator: NSObject, MFMailComposeViewControllerDelegate, UINavigationControllerDelegate { let parent: MailView init(parent: MailView) { self.parent = parent } func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { // 終了時の処理あれこれ self.parent.isShowing = false } } func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<MailView>) { } }呼び出し側
ContentView.swiftimport SwiftUI import MessageUI struct ContentView: View { @State private var isShowingMailView = false var body: some View { Button(action: { self.isShowingMailView.toggle() }) { Text("Open MailView") } .disabled(!MFMailComposeViewController.canSendMail()) .sheet(isPresented: $isShowingMailView, content: { MailView(isShowing: self.$isShowingMailView) }) } }動作gif
ボタンを押してMailViewを開き、その後はキャンセルボタンを押して下の画面に戻っています。
※ 差出人メールアドレスは一時的に変更した仮のものを使用しています。
- 投稿日:2019-12-01T20:37:12+09:00
#17 Xcodeでアプリの向きを縦に固定する1例
- 投稿日:2019-12-01T20:25:25+09:00
Viewの制約が(結果として同じもの)を設定するときの状況別のパターン
動機
Youtubeチャンネルの素材用にTinderの模擬アプリを作成する際、
View
の制約を設定するとき、結果として同じものがいくつかあり
どのパターンで制約を設定するか迷うことが個人的によくあります
->後になって設計や機能が詰まってきた時にあっちのパターンで制約かけておけばよかった。。。ということもしばしば。。今後それをなるべく無くしたいので定期的にメモがわりに残します
状況と対策
superViewに対してViewにmarginをかける&中心は揃える
この場合パターンとして
- 中心座標を
superView
に揃える & Viewのwidth, heightをsuperView
のwidht, height-marginにするsuperView
に対してViewの縁(leading~bottom)をmargin分縮めたものにする例
girlsViewArray[$0].leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10.0).isActive = true girlsViewArray[$0].trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10.0).isActive = true girlsViewArray[$0].topAnchor.constraint(equalTo: self.view.topAnchor, constant: 74.0).isActive = true //84.0 = 44.0(navHeight) + 20.0(statusBarHeight) + 10(margin) girlsViewArray[$0].bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20.0).isActive = trueが挙げられると思います
個人的には
- 中心座標superView
に揃える &superView
のwidht, height-marginにする
->Viewの座標が動く時(ex.ドラッグや大きさの変化)
-superView
に対してmargin分縮めたものにする
->Viewの座標が動かないと決まっている時またわかったら追記します
- 投稿日:2019-12-01T20:13:59+09:00
AutoLayoutをコードで指定する際の注意点(NSLayoutAnchor)[Swift]
Youtubeチャンネルの素材用にTinderの模擬アプリを作成する際、
シンプルだけど長い時間はまってしまったのでメモ。状況
Viewの制約といっても色々あるが、今回ハマったのはViewの縁を設定する制約。
(図のようにViewController
の縁に対して上下左右10pxずつのmarginを設定して配置したい、という感じ。)
Storyboard
での配置だとcustomViewだろうが標準Viewだろうが縁の制約はAdd New Constraints
で
こんな感じで10をひたすら入れていけばできてしまうが
NSLayoutAnchor
を使ってコードで設定したとき同じ容量でこんな感じでやったらgirlsViewArray[$0].leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10.0).isActive = true girlsViewArray[$0].trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 10.0).isActive = true girlsViewArray[$0].topAnchor.constraint(equalTo: self.view.topAnchor, constant: 10.0).isActive = true girlsViewArray[$0].bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 10.0).isActive = true
View
がViewController
からはみ出してしまった対策
NSLayoutAnchor
のconstant
はCGFloatの座標系(x: 右に行くほど増える, y: 下)かつequalTo
で設定した縁からの距離である
->Add New Constraints
はプラスの数値を入れれば選択したViewのsuperView
に対してスペースを作る(縮める)方向に勝手に制約を設定してくれる
以上を理解した上で修正した制約がこう(上下左右ViewControllerのViewに対して10pxずつマージンを持たせたい場合)
->trailingAnchor
は左に詰めたいのでconstant
を-10.0にする必要がある
->bottomAnchor
は上に詰めたいのでconstant
を-10.0にする必要があるgirlsViewArray[$0].leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 10.0).isActive = true girlsViewArray[$0].trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10.0).isActive = true girlsViewArray[$0].topAnchor.constraint(equalTo: self.view.topAnchor, constant: 74.0).isActive = true //84.0 = 44.0(navHeight) + 20.0(statusBarHeight) + 10(margin) girlsViewArray[$0].bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -10.0).isActive = true
- 投稿日:2019-12-01T19:40:47+09:00
写真・ビデオの撮影日時や再生時間を取得する
はじめに
カメラやフォトライブラリを使ったアプリを作ったときに、撮影日時と再生時間を取得する必要があって色々調べてみたので、書き残しておこうと思います。
UIImagePickerControllerを使用しているのですが、カメラで撮影した時と、フォトライブラリに保存されたものを選択した時とで取得方法が違ったので、少し苦戦しました。
とりあえず、これで目的は果たせているのですが、もっといい方法があるような気がしています。
環境:
Xcode 11.2.1
Swift 5.1.2カメラで撮影したとき
撮影後に現在時刻を取得しても大差ない気はするんですけど、なんか意地で。
写真の場合
撮影日時という訳ではなさそうですが、もっとも近い値かと。
簡単と思いきや、Exifの日付形式が意外な落とし穴。ViewController.swiftimport UIKit class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { // 〜カメラ・フォトライブラリの呼び出しや終了については省略〜 // 撮影または写真の選択が完了した時に呼ばれる func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]){ var shootingDate: String = "" // 撮影日時 // メタデータからExifを取り出し、撮影日時を取得 guard let metadata = info[.mediaMetadata] as? NSDictionary else { return } guard let exif = metadata.object(forKey: kCGImagePropertyExifDictionary) as? NSDictionary else { return } let dateTimeOriginal = exif.object(forKey: kCGImagePropertyExifDateTimeOriginal) shootingDate = String(describing: dateTimeOriginal) // 取得したデータはなぜか「0000:00:00 00:00:00」という形式なので、最初の2つだけ「:」を「-」に置き換え var count = 0 while count < 2 { count += 1 if let range = shootingDate.range(of: ":"){ shootingDate.replaceSubrange(range, with:"-") } } print("shootingDate: \(shootingDate)") } }ビデオの場合
ビデオはExifデータを持っていないので、AVURLAssetから撮影日時を取得しています。
ViewController.swiftimport UIKit import Photos // AVURLAsset()やPHAssetを使うのに必要 class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { // 〜カメラ・フォトライブラリの呼び出しはここでは省略〜 // 撮影または写真の選択が完了した時に呼ばれる func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]){ var shootingDate: String = "" // 撮影日時 var duration: String = "" // 再生時間 // 日時の表示形式はお好みで let formatter = DateFormatter() formatter.timeZone = TimeZone.current formatter.locale = Locale.current formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" // AVURLAssetを使ってファイルから撮影日時を取得 guard let fileUrl = info[.mediaURL] as? URL else { return } let video = AVURLAsset(url: fileUrl) let creationDate = video.creationDate?.value as! Date shootingDate = formatter.string(from: creationDate) // ついでにビデオの再生時間も取得 let durationTime = TimeInterval(round(Float(video.duration.value) / Float(video.duration.timescale))) duration = generateDuration(timeInterval: durationTime) print("shootingDate: \(shootingDate)") print("duration: \(duration)") } // ビデオの時間を「00:00」表示に変換して返す func generateDuration(timeInterval: TimeInterval) -> String { let min = Int(timeInterval / 60) let sec = Int(round(timeInterval.truncatingRemainder(dividingBy: 60))) let duration = String(format: "%02d:%02d", min, sec) return duration } }フォトライブラリから選択したとき
info[.referenceURL]がDeprecateになって、代わりにinfo[.phAsset]を使うことになったのかな?
でも情報が見つけられなくて、意外と苦労した箇所(情弱の可能性)。写真もビデオも同じ方法で取得できます。
写真の場合はdurationのくだりをカットしてください。ViewController.swiftimport UIKit import Photos // AVURLAsset()やPHAssetを使うのに必要 class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { // 〜カメラ・フォトライブラリの呼び出しはここでは省略〜 // 撮影または写真の選択が完了した時に呼ばれる func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]){ var shootingDate: String = "" // 撮影日時 var duration: String = "" // 再生時間 // 日時の表示形式はお好みで let formatter = DateFormatter() formatter.timeZone = TimeZone.current formatter.locale = Locale.current formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" guard let asset = info[.phAsset] as? PHAsset else { return } shootingDate = formatter.string(from: asset.creationDate!) duration = generateDuration(timeInterval: asset.duration) print("shootingDate: \(shootingDate)") print("duration: \(duration)") } // ビデオの時間を「00:00」表示に変換して返す func generateDuration(timeInterval: TimeInterval) -> String { let min = Int(timeInterval / 60) let sec = Int(round(timeInterval.truncatingRemainder(dividingBy: 60))) let duration = String(format: "%02d:%02d", min, sec) return duration } }参考にしたURL
【Swift】動画撮影&保存&取得【UIImagePickerController】
AVURLAsset - AVFoundation | Apple Developer Documentation
AVAsset - AVFoundation | Apple Developer Documentation
最後に
至らない点や無駄な記述など、お気付きの点はご指摘いただけるとありがたいです!
- 投稿日:2019-12-01T19:39:49+09:00
[Swift] AnyPokemonリターンズ ~型消去型の弁明~
Swift Advent Calendar 2019 2日目の記事です。
書いたこと
- 型消去型とは
- 型消去型の大変心苦しい立場
型消去型とは
AnyPokemon について
AnyPokemonは、平常心で型を消しさるで紹介された型消去型です。
これを変更して、型消去型とは何なのか、どうして必要なのかを考えてみます。
AnyPokemon おさらい
ポケモンは、雷属性ポケモン、炎属性ポケモンのように属性によって分類することが出来ます。
この属性の違いを型として表す場合、オブジェクト指向で考えれば、ポケモンクラスからサブクラスとして派生させるアプローチが一般的です。
ですが、Swiftの
struct
は継承が許されていません。
そこでProtocolを用います。(※1)Pokemonプロトコル// Pokemonプロトコルは属性を表すtypeを持つ protocol Pokemon { // 属性を表す型 associatedtype PokemonType var type:PokemonType { get } }Pokemonプロトコルを用いると、雷属性のピカチューは以下のように表すことが出来ます。
雷属性型とピカチュー// 雷属性を表す struct Electric {} // ピカチュー struct Pikachu: Pokemon { var type = Electric() }しかしPokemonプロトコルは型宣言として用いることが出来ません。
// NG 型宣言としては使えない var pokemon:Pokemon = Pikachu()これはSwiftの言語仕様として、
Self
もしくはassociatedtype
を持つprotocol
は型宣言として使用することが出来ないためです。そこでAnyPokemonの登場です。
AnyPokemonstruct AnyPokemon<T>:Pokemon where T:Pokemon { var type: T.PokemonType { return self.rawPokemon.type } var rawPokemon: T init(_ pokemon: T) { self.rawPokemon = pokemon } }AnyPokemonを使えば、雷属性のポケモンを受け取る型を宣言することが出来ます。
// OK let electricPokemon:AnyPokemon<Electric> = AnyPokemon(Pikachu()) // Arrayの型変数としてもOK let electrics:[AnyPokemon<Electric>} = [AnyPokemon(Pikachu()), AnyPokemon(Raichu())]AnyPokemonの注目ポイントは2つです。
- 型宣言することが出来る
- Pokemonプロトコルである
つまり型消去型とは何らかのプロトコルの具体型であるといえます。
型消去型 in 標準ライブラリー
標準ライブラリーに用意されている型消去型は、Any~ と命名されています。
その中で以下の2つの型消去型をご紹介します。
- AnyHashable
- AnySequence
AnyHashable
AnyHashable
は、その名の通り、Hashable
プロトコルの具体型です。SwiftのDictionaryは、同時に複数の型をキーとして取るこことが出来ません。
// コンパイルエラー let desc = ["?" : "emoji", 42 : "an Int"];キーに複数の型のインスタンスを受け付けるようにするには、型宣言として
Hashable
プロトコルを宣言する必要がありますが、Self
、associatedtype
を持ちますので型宣言に用いることは出来ません。Hashableを型宣言に用いることが出来ない// NG let desc:[Hashable: String] = ["?" : "emoji", 42 : "an Int"];そこでAnyHashableの出番です。
// OK let desc = ["?" : "emoji", 42 : "an Int"] as [AnyHashable : Any]つまり
AnyHashable
を使えば、個々の型を消去し、AnyHashable
型としてまとめることが出来ます。
AnySequence
AnySequence
はSequence
プロトコルの具体型です。
こちらもSelf
,assciatedtype
を持つため型宣言として利用することが出来ないため、AnySequence
を使うことが出来ます。// OK var numbers: AnySequence<Int> = [1, 2, 3, 4, 5]ですが、上記AnySequenceを用いる理由は全くありません。
ただ[Int]
と宣言すればいいだけです。。。それではAnySequenceはどういった場面において、有効的な使い方が出来るのでしょうか?
それを考えてみます。型消去型の大変心苦しい立場
型消去型には妬ましい強敵が存在します。
ジェネリクスです。そのため影に追いやられている大変辛い存在になってしまっています。
なんとか日陰から日の当たる場所に出してあげたいと思います。そこで一旦ジェネリクスの存在を無視し、
AnySequence
を使って型消去型の意義を考えます。型消去型の意義を考える
文字列をトークンに変換する関数を考えます。
func convertToken(from str: [Character]) -> Token { ...この関数を文字列から直接呼び出す場合、Array型を生成するコストO(N)が発生します。
// 呼び出すたびにコストO(N)が発生する let token = convertToken(Array("◎だこのたこ焼きを食うと胸がやける?"))それならば、文字列として宣言すればいいじゃない?
func convertToken(from str: String) -> Token { ...では文字列をソートしてからトークンに変換するとなったとき、どうしましょうか?
// sortedメソッドの返り値は[Character] let token = convertToken(from: String(str.sorted()))一旦、配列に変換された文字列を再度文字列に変換する必要が発生します。
そこで
AnySequence
の出番です。func convertToken(from chars: AnySequence<Character>) -> Token { ...// 文字列から直接 let token = convertToken(from: AnySequence(str)) // ソートしてから let token2 = convertToken(from: AnySequence(str.sorted()))このように受け取れる型の定義域を広げることで、接続をしやすくすることが出来ます。
これは型が非常に強いプログラミング言語においてはとても重要なことです。ジェネリクスとの比較
残念ながら型消去型を積極的に使われることは殆どありません。
なぜならばジェネリクスを使えば、コンパイル時点で、呼び出し箇所で型を決定することができ、最適化の恩恵も受けることが出来るからです。強いて言うならば、ジェネリクスは読み解くのが難しいと言えるかもしれません。
以下電撃属性のポケモンを受け取る関数を見比べてみれば、一目瞭然です。
型消去型とジェネリクスの見比べ// ジェネリクス func pokemonWithElectric<U>(_ pokemon: U) where U:Pokemon, U.PokemonType == Electric { .... } // 型消去型 func pokemonWithElectric(_ pokemon: AnyPokemon<Electric>) { .... }またジェネリクスは自分で定義する必要もありますが、標準ライブラリーには必要となる型消去型が沢山用意されています。
まとめ
- 型消去型とは あるプロトコルの具体型 であると言える
- 型消去型を使う利点
- 接続がしやすくなる
- ジェネリクスよりも可読性が高い
- ジェネリクスと違って沢山の型消去型が用意されている
Appendix
型消去 (Type erasure) と 型消去型 (type-erased type) について
型消去という名詞は、Javaでも使われていますが、ここでの型消去とは意味が異なります。
(Javaの型消去は、JVM上では型が消去されることを意味します。)
ここでの型消去は、type-erased という形容詞になります。後ろにtypeという名詞が続いています。まぁ型消去は名詞なのでどちらに使っても間違いではないと思いますが、より正確に言うならば型消去型(type-erased type)です。
structとenumについて
※1から、Swiftの
struct
は、直積タイプの代数的データ型であることを意味します。またenum
は直和タイプの代数的データ型です。参照
- 投稿日:2019-12-01T19:05:44+09:00
【Swift】associatedtypeの使いどころ
使いどころがよくわかってなかったけど気になっていたSwiftの
associatedtype
個人開発アプリでいい感じに使えたのでここに書いておきます。associatedtypeとは
- protocolに定義する連想型です
- protocolの準拠時に、具体的な型を指定します(または型推論で指定されます)
- ジェネリクスにおける
T
的なやつです使いどころ
上記の通りなのですが、protocol定義時点では決められず、準拠側で指定したい型があるときが使いどころです。
具体的には、APIを叩いて、レスポンスに含まれるJSONから特定の型を作りたい!ってときに使えました。具体例
以下、AlamofireとQiita APIをサンプルに使った例です。
やりたいこと
- Qiita APIを使ってユーザー情報を取得したい( https://qiita.com/api/v2/docs#get-apiv2usersuser_id )
- リクエストメソッドのクロージャ内で、レスポンスのJSONをデコードした型として扱いたい
これを単純に書くとこんな感じになります。
protocol、associatedtypeを使わないパターン.swiftfunc hoge() { Alamofire .request("https://qiita.com/api/v2/users/akeome") .response(completionHandler: { response in guard let data = response.data, let user = try? JSONDecoder().decode(User.self, from: data) else { return } print(user.id) // akeome print(user.userDescription) // ぁけぉめです。以下略 }) }なお、ここでのUser型はJSONを元に自動生成1したCodableなstructです。
もっとよくできないかしら
上記のような処理を繰り返し書くことがあれば、こんな不満と願望が湧いてきます
- この長い処理を何度も書きたくない → リクエストメソッド自体は共通化したい
- 処理を呼ぶたびにURLとレスポンスJSONの型をいちいち書きたくない → APIの呼び出し先を指定するだけでJSONをデコードする型も決まってほしい
こんな願いを叶えてくれる機能がSwiftにはちゃんと備わっているのです。
それがprotocol、そしてassociatedtypeです。protocolにしてみよう
まずはAPIを呼び出せるprotocolを考えましょう。
リクエストメソッド自体は共通化しつつAPIの呼び出し先を変えるには、protocolが使えそうです。APIConfigure.swiftprotocol APIConfigure { // APIの呼び出し先 static var path: String { get } // リクエストメソッド(※クロージャは後述) static func request() }呼び出し先は準拠する側で決めるとして、リクエスト処理は共通なのでprotoocol extensionで実装します。
APIConfigure.swiftextension APIConfigure { static func request() { Alamofire .request(Self.path) .response(completionHandler: { response in guard let data = response.data, // ???? let xxx = try? JSONDecoder().decode(User.self, from: data) else { return } }) } }さてここで困ったことがあります。
JSONDecoder().decode()の第一引数に渡す型を指定しなければならないのです!共通化したい!でも具体的な型はまだ決められん!準拠側で指定したいんや!
そんなときこそassociatedtypeの出番です。そう、associatedtypeならね
associatedtypeの定義
protocol本体の実装に戻って、「今はまだ決められないけど準拠時に決めてね」という型を定義します。
今回の例の場合、準拠時にUser型に指定する型です。APIConfigure.swiftprotocol APIConfigure { associatedtype ResponseEntity // ?追加 // APIの呼び出し先 static var path: String { get } // リクエストメソッド(※クロージャは後述) static func request() }protocol extension側を修正していきます。
APIConfigure.swiftextension APIConfigure { static func request() { Alamofire .request(Self.path) .response(completionHandler: { response in guard let data = response.data, // ?associatedtype let responseEntity = try? JSONDecoder().decode(ResponseEntity.self, from: data) else { return } }) } }型制約の追加
このままでは以下のエラーになります。
Instance method 'decode(_:from:)' requires that 'Self.ResponseEntity' conform to 'Decodable'`JSONDecoder().decode()の第一引数に渡す型はDecodableに準拠している必要があるのです。
このままでは、ResponseEntityがJSONから変換可能な型なのかどうかがわからないのです。そこで型制約の追加です。
APIConfigure.swiftprotocol APIConfigure { // ?型制約の追加 associatedtype ResponseEntity: Codable // APIの呼び出し先 static var path: String { get } // リクエストメソッド(※クロージャは後述) static func request() }これでResponseEntityはJSONから変換可能な型(=Codable)であると制約をかけられました。
そしてクロージャへ
リクエスト処理にクロージャを追加して、呼び出し元でJSONをデコードした型として扱えるようにします。
APIConfigure.swiftprotocol APIConfigure { associatedtype ResponseEntity: Codable // APIの呼び出し先 static var path: String { get } // リクエストメソッド ?クロージャを追加 static func request(completion: ((ResponseEntity) -> ())?) }APIConfigure.swiftextension APIConfigure { static func request(completion: ((ResponseEntity) -> ())?) { Alamofire .request(Self.path) .response(completionHandler: { response in guard let data = response.data, let entity = try? JSONDecoder().decode(ResponseEntity.self, from: data) else { return } completion?(entity) }) } }ここまでで、悲願の「リクエストメソッドの共通化」「APIの呼び出し先を指定するだけでJSONをデコードする型の確定」を満たせそうなprotocolができあがりました。
準拠側
Qiita APIのユーザー情報取得を扱うstructを作り、protocolに準拠させます。
UserGet.swiftstruct UserGet: APIConfigure { typealias ResponseEntity = User // ?ここで具体的な型を指定 static let path = "https://qiita.com/api/v2/users/akeome" }protocolで定義したassociatedtypeを準拠側で明示的に指定するには
typealias
を使います。
これでUserGetを使ったリクエスト処理のクロージャで受け取る型はUser型に指定できました。APIを呼び出す共通的なstructを作ってまとめていくことが考えられます。
APIClient.swiftstruct APIClient { // ユーザー情報取得 struct UserGet: APIConfigure { typealias ResponseEntity = User static let path = "https://qiita.com/api/v2/users/akeome" } // 記事一覧取得 struct ItemsGet: APIConfigure { typealias ResponseEntity = [Item] static let path = "https://qiita.com/api/v2/items" } // その他いろいろ }associatedtype使ってみた結果
protocol、associatedtypeを使ったパターン.swiftfunc hoge() { APIClient.UserGet.request(completion: { user in print(user.id) // akeome print(user.userDescription) // ぁけぉめです。以下略 }) }こんな感じで、リクエスト処理のクロージャで受け取る型を自動的にUserにできました。
とてもすっきりしたのではないでしょうか。今後ユーザー情報取得だけでなく記事一覧も取得したくなっても
APIClient.GetItems.request(〜
と書くだけです。比較のため、冒頭に記載したassociatedtypeを使わないパターンも再掲しておきます。
protocol、associatedtypeを使わないパターン.swiftfunc hoge() { Alamofire .request("https://qiita.com/api/v2/users/akeome") .response(completionHandler: { response in guard let data = response.data, let user = try? JSONDecoder().decode(User.self, from: data) else { return } print(user.id) // akeome print(user.userDescription) // ぁけぉめです。以下略 }) }まとめ
今回は割愛しましたが、
- protocolにHTTPMethodを持たせてGETやPOSTに対応する
- protocolにパラメーターを持たせる
- protocol extensionでベースURLを定義する
- リクエスト処理のエラーに対応する
などすれば、より実用的なコードになるかと思います。
associatedtype
についての記事がなかなか見つからなかったので参考になる方がいらっしゃれば幸いです。おまけ
associatedtype
を使ってこんなアプリを作ってます。
CardPort - App Store
- 投稿日:2019-12-01T17:54:24+09:00
【Xcode】細分化する iOS Architecture に向き合う上で気をつけなければらないただひとつのエラーについて
はじめに
こんにちは。Reactive な世界に生命の息吹を感じるたかねです。
今年もやってきました,iOS Advent Calendar 2019 1日目です!
みなさまよろしくお願いいたします!本記事は
unable to spawn process (Argument list too long)
という Xcode からのすてきな (!) メッセージについてです。
普段の開発では見慣れないエラーかと存じます。しかし,本問題を知り,本問題を見据えて開発することは,きっと数年後の iOS 開発者であるあなたの役に立つと思います。少し長いですが,ぜひご覧いただけましたら幸いです。目次
- 最近のソフトウェアアーキテクチャに関する流行と振り返り
- プロジェクトが適切に肥大化し続ける中待ち受ける,たったひとつのエラー
unable to spawn process (Argument list too long)
とは? その発生条件- いくつかの対処方法と,Xcode 11 における解決方法
- まとめ
- (おまけ) なぜ日本語記事が存在しなかったのか
- 注釈
- 文献
最近のソフトウェアアーキテクチャに関する流行と振り返り
本題に入る前に少しおさらい、もとい振り返りをさせてください。2018年は iOSアプリ設計パターン入門 を始め,Clean Architecture: A Craftsman's Guide to Software Structure and Design の訳書である Clean Architecture 達人に学ぶソフトウェアの構造と設計 が出版され,日本の iOS 開発者が様々なソフトウェアアーキテクチャに対し知見を得る1年となりました。実際に MVVM を始め、多くの企業は細分化されたアーキテクチャを採用している様子が確認できるようになってきています。我々 iOS エンジニアは設計というスキルを得ることができ,次なる問題に取りかかることができるようになりました。
プロジェクトが適切に肥大化し続ける中待ち受ける,たったひとつのエラー
閑話休題。ここでの「プロジェクト」とは Xcode の Project と同義であり,ソースコードとその周辺環境を指します。
過去に FATViewController などと揶揄されたこともあったUIViewController
へのドメインロジック混入による肥大化は,View と Model を分離 (プレゼンテーションとドメインの分離; Presentation Domain Separation) することを目的とした各種設計により解決されました。素晴しく綺麗な環境で開発される iOS プロジェクトは,適切に分割されたレイヤーによりテストがし易く堅牢で,かつ明快である。その環境下では無限にスケールしてもその効用を維持し続けることができる,その最たる例が Clean Architecture である。
そう,私もそう信じてやみませんでした。件のエラーが発生するまでは。
以下をご覧ください。これは,ある条件下において Xcode でビルドした際に発生するエラーです。
unable to spawn process (Argument list too long)
エラーです。これが Xcode である証拠に,画面全体のスクリーンショット も添付しておきます。
また,
xcodebuild
コマンドによる CLI 経由でも発生します。これは一体何でしょうか? この記事をご覧になっている皆様にも発生する問題なのでしょうか?
unable to spawn process (Argument list too long)
とは? その発生条件
unable to spawn process (Argument list too long)
エラーが発生する原因はただひとつ。エラー文の通り「引数リストが多過ぎる」というエラーです。Compile Swift source files ステップにて, コンパイラに渡されるソースファイルの Full path とオプションの文字数の合計がおよそ 260,000 文字程度※1 を越えるとコンパイルが必ず失敗する というものです。
............遭遇しなければ気がつかないエラーです?????
いくつかの対処方法と,Xcode 11 における解決方法
ファイル数が多い,絶対パスが長くなるような深いディレクトリに存在する,極端に長いファイル名を使用しているなどの原因により本問題は発生します。
その解決方法は
swiftc
(Swift コンパイラ) に対し渡すコマンドの総量をどうにかして減らすことのみでした。今秋リリースされた Xcode 11 ではようやく解決可能なフラグが追加されました。
いくつかの対処方法と,Xcode 11 における解決方法を順に解説いたします。1. ライブラリをビルド済みバイナリ形式でビルドする
こちらは CocoaPods を利用している場合に限ります。Carthage はビルド済みバイナリを利用しますが,CocoaPods の場合ビルドが含まれるため,その分パスが長くなります。どうしても CocoaPods を利用したい方は,cocoapods-binary というプラグインを導入することで,インストール時にバイナリとして事前にコンパイルさせておくことが可能です。
余談ですが,cocoapods-binary はオプションによりソースコードを残しつつバイナリを生成することもできます。本オプションにより,Carthage の弱点であった「開発時に Xcode 内からライブラリ側のソースコードを辿れない」という問題に対応しつつ,ビルド時にはコンパイル済みのバイナリを利用してコンパイルを早くするといったことが可能となります。便利です。
2. スクリプトによってコンパイル対象のソースファイル名を変更する
愚直な解決方法として挙げられるのはこちらです。「ファイル名の長さが原因」なので,実際にコンパイラに渡すまでに短いファイル名に変更してからコンパイルするようにすれば良い,ということになります。
具体的な方法はあまりにも泥臭いため省略します。3. プロジェクトをルートディレクトリに移動させる
コンパイラに渡されるソースファイルのパスは Full Path であるという仕様を利用する方法です。リポジトリを Clone するディレクトリをルート直下に配置するだけです。全てのソースファイルに影響があるためファイル数が多いプロジェクトほど効果があります。ちなみに Apple の Developer forum にも同様の提案がされています。
一方で,これは一時的解決に過ぎず,この状態で開発を続けてもいずれエラーとなることは目に見えています。
(Bitrise がルートディレクトリに Clone する仕様で助かりましたね!)4. Embeded Framework を利用してコンパイルする
Embeded Framework を用い,分割コンパイルすることで一度にコンパイルするソースファイルの数を減らす方法です。これが一番まともであり,根本解決とは異なりますが正攻法と言えるでしょう。
5.
USE_SWIFT_RESPONSE_FILE
をYES
と指定する (Xcode 11 以降)Xcode 11 以上の環境で build settings
USE_SWIFT_RESPONSE_FILE
にYES
を代入する ことで解決できるとの情報が更新されていました。Xcode 10 まででは根本解決に至らなかったこのエラーは,Xcode 11 によってようやく解決されたようです。まとめ
- Xcode には コンパイラへ渡すソースファイルの内容によって
unable to spawn process (Argument list too long)
が発生する致命的な問題を潜在的に抱えていた- Xcode 11 未満は Embeded Framework による Target 分割を始めとした回避方法が存在する
- Xcode 11 以降であれば
USE_SWIFT_RESPONSE_FILE
にYES
を代入することで解決できる秋に発表された Xcode 11 は図らずとも我々に良い結果をもたらしました。
一方で堅牢な設計を維持するためには Embeded Framework を利用することがコンパイル時間の短縮のためにも良いかと思います。投稿が少し遅くなりましたが,ここらで筆を置かせていただきます。(おまけ) なぜ日本語記事が存在しなかったのか
本章は考察です。Xcode には以前よりこのような問題を抱えていました。少なくとも 2017 年には。
私が iOS 開発を始めたのも 2017 年ですので、それ以前の流れは分かりません。設計に関する関心は恐らくこれと同じ時期、もしくはそれ以後に盛況を呈したものと思われます。2017 年以後,設計に関心を持つ開発者が本問題に遭遇することはなかったのか?
この疑問については,周囲から話を聞く限りでは「細分化される Embeded Framework を利用しており,発生する前に自然と回避していた」と考えられます。
Swift における名前空間について
他言語ではディレクトリ名が名前空間として予約され,異なるディレクトリ間でも問題なく扱える機能がありますよね。
Swift における名前空間は暗黙的であり,利用するためには Framework による Target の分離が必要です。Framework によって分割するということはコンパイルソースを分割することにもなるため,名前空間による解決手法は結果的に上で記載した手法に含まれます。
(この仕様自体も,ソースファイルの命名が長くなる要因だったり......?)エラー原因の背景1 – XcodeGen の導入とリファクタリングの順序 –
今回エラーとなった原因の背景には「MVC から Clean Architecture へのリファクタリング → XcodeGen によるコンフリクトの解消 → Embeded Framework による分割」 のステップを検討しており,同時に Framework 化を進められなかった事情がありました (こんなエラーが発生することを予期できていれば最初から Target を分けて作成していましたよ!)。その結果,1 target に全ての層が入りこみ,MVC から分割されたファイルは必然的に増える結果を生みました。
エラー原因の背景2 – Presentation 層への Atomic design を用いた介入 –
さらに,今日までのソフトウェアアーキテクチャにおける欠点のひとつに「View (Presentation) の実装については関心を持たない」という点も加筆しておかねばなりません。昨年 Sketchと1対1を目指すAtomic designなStoryboardの作り方 - Qiita という記事を書きました。Presentation 層に対し無関心であった部分について,Atomic design を利用して再利用可能なレベルに分割する手法を導入しました。こちらも,再利用を意識し過剰な
.xib
ファイルとその対となる.swift
が生成され,ファイル数の増加に繋がりました。Presentation 層におけるかすかな希望
Atomic design 自体が悪かった訳ではありません。実際に再利用によってボタンやフォームを始めとする UI の作成速度は向上し,意図せぬ再実装がなくなりました。また,SwiftUI の登場は Storyboard および Xib ファイルからの開放,コード生成と GUI による生成の共存という結果を生み出しました。これだけでも旧実装よりファイル数,コード数ともに削減させる一手と成り得る希望を生みました (完全に Replace できるかについて,ここでは言及を避けるものとします)。
注釈
※1) 上限の境界については未検証のため正確ではありません。263,627 文字で失敗を確認しています。
文献
- xcodebuild exportLocalization throws "Argument list too long"
- error: unable to spawn process (Argument list too long) in Xcode Build
- iOSDC 2017 前夜祭で「節子、それViewControllerやない…、FatViewControllerや…。」というタイトルで登壇しました! #iosdc | Developers.IO
- CocoaPods/CocoaPods - GitHub
- leavez/cocoapods-binary - GitHub
- Carthage/Carthage - GitHub
- Swiftで名前空間を利用する - Qiita
- プレゼンテーションとドメインの分離 - Martin Fowler's Bliki (ja)
- yonaskolb/XcodeGen - GitHub
- Embedding Frameworks In An App - Apple Developer
- Sketchと1対1を目指すAtomic designなStoryboardの作り方 - Qiita
- Atomic design
- SwiftUI - Apple Developer
- 投稿日:2019-12-01T16:25:43+09:00
XcodeGenを使うとチーム開発が楽になる話
この記事は豊田高専コンピュータ部アドベントカレンダー1日目の記事です
XcodeGenとは
iOSアプリ開発で複数人での開発をすると色々とつらいことがあるのでそのつらいことをなるべく無くそうという感じのツールです。具体的にはプロジェクトへのファイルの追加など行うとプロジェクトファイルが書き換わるのですが、そのプロジェクトファイルは人間におおよそ理解のできない形式で書かれており、チーム開発をしているとそれが恐ろしいコンフリクトを生み、その解消がとてもつらいという問題があります。そのプロジェクトファイルをコード(yaml)から生成することでチーム開発をやりやすくしようという感じのツールになります。
https://github.com/yonaskolb/XcodeGen触ってみる
なにはともあれ百聞は一見にしかずなので触ってみましょう。まずはXcodeGenをインストールします
$ brew install xcodegenXcodeGenを入れられたら次はXcodeのプロジェクトを作成します。プロジェクト名は今回は「SampleGen」とします。プロジェクトの作成手順は今回は割愛します。
プロジェクトを作成できたらプロジェクトファイルの内容を
project.yml
に書き起こしていきます。project.yml# プロジェクト構成・設定 name: SampleGen configs: Debug: debug Release: release options: groupSortPosition: top attributes: ORGANIZATIONNAME: ymgn fileGroups: - SampleGen - SampleGenTests # ターゲット targets: SampleGen: type: application platform: iOS sources: - path: SampleGen settings: base: PRODUCT_BUNDLE_IDENTIFIER: ymgn.SampleGen ASSETCATALOG_COMPIER_APPICON_NAME: AppIcon INFOPLIST_FILE: SampleGen/Info.plist configs: Debug: GCC_OPTIMIZATION_LEVEL: O Release: GCC_OPTIMIZATION_LEVEL: s SampleGenTests: type: bundle.unit-test platform: iOS dependencies: - target: SampleGen settings: TEST_HOSTS: $(BUILT_PRODUCTS_DIR)/SampleGen.app/SampleGen INFORPLSIT_FILE: SampleGenTests/Info.plist sources: - SampleGenTests # スキーム schemes: SampleGen: build: targets: SampleGen: all run: config: Debug test: config: Debug gatherCoverageData: true targets: - SampleGenTests profile: config: Release analyze: config: Debug archive: config: Release要素一つ一つを説明するのはつらいのでXcodeGenのリポジトリにあるドキュメントを参照してもらうとして、プロジェクトファイルの項目に対応した要素が存在します。プロジェクトファイルからyamlへの書き起こしは手で書きましたが、自動で書き起こしてくれる方法などあればぜひコメントで教えてください。
project.yml
はプロジェクトファイルと同じ階層に配置し、プロジェクトに追加する必要はありません。
project.ymlが出来上がったらプロジェクトファイルは削除しても構いませんが、写しミスがあった場合悲しいので残しておくとよいでしょう。
project.ymlがあるディレクトリで$ xcodegen generate
を実行するとプロジェクトファイルを生成します。既存のプロジェクトファイルがある場合は書き換えられます。
最低限の実装になっているのでそのままビルドするとおそらくSigningでエラーが出ると思いますので、Teamは手動で追加してもらうとエラーは消えるはずです。
これで一通りの導入は終了です。
まとめ
上記では手順だけ示したのであまり恩恵がわからないかもしれませんが、targetの追加や、ライブラリ、Embeded Frameworkの追加などもproject.ymlで記述できるのでコマンド一発でどの環境でもコンフリクトのない状態のプロジェクトファイルが生成されます。実際に使用する際はプロジェクトファイルは.gitignoreなどで管理には含めないようにすることに注意してください。
もっと詳しく知りたい場合はXcodeGenのリポジトリや他記事を参照してもらえると多くの設定が利用できたり、設定ファイルを分割できたりすることがわかると思います。iOSアプリのチーム開発で困っている人はぜひ試してみてください。おまけ
XcodeGenを使ったプロジェクトをBitriseなどのCI/CDツールに載せるとプロジェクトファイルが存在しないため、validationが通らなくて焦るかもしれません。しかし、最初のvalidationが通らなくてもPROJECT_PATHには単に生成される.xcodeproj(もしくは.xcworkspace)のファイル名を入力して、workfrowにXcodeGenをビルド前に実行するようにすればビルドできるようになります。
Bitriseであればこのようにxcodegenのコマンドを使用するためのユーティリティが存在します。
- 投稿日:2019-12-01T15:53:55+09:00
【Swift】Delegateとは(別オブジェクトへの処理の委譲)
私がiOSアプリを開発する中で、デリゲートパターンについてなーなーなまま進めて痛い目に遭ってきたので今回ここにまとめたいと思います。おかしいと思うところがあったらどしどしコメントください。
デリゲートパターン
デリゲートパターンは、あるオブジェクトの処理を別のオブジェクトに代替させるパターンです。デリゲートもとのオブジェクトは、適切なタイミングでデリゲート先のオブジェクトに通知を送り、その通知を受けたデリゲート先のオブジェクトは、自身や別のオブジェクトの状態を変更したり、結果をデリゲート元のオブジェクトに返したりすることができます。
デリゲートとして他のオブジェクトに渡す処理は、事前にプロトコルとして宣言しておく必要があります。実装例
今回の例では、デリゲート元であるGreetingクラスの処理(greeting()メソッド内のstrやdidMorning、didAfternoon)を、Greetクラスに委譲します。
まず、protocolにデリゲート元に渡したい処理を記述します。デリゲート先のクラスはこのprotocolに準拠すべき(GreetクラスはGreetDelegateに準拠したクラス
class Greet: GreetDelegate
)で、渡す処理を全て実装しておかなければなりません。そのため、protocol内に記述した処理がない場合はコンパイルエラーになります(処理が実装されてませんってエラー出してくれるからわかりやすい)。delegateSapmple.praygroundimport UIKit //デリゲート先のクラスにて、 //デリゲート元に渡したい処理をprotocolとして宣言する protocol GreetDelegate: class { var str: String { get } func didMorning(_ greet: Greeting) func didAfternoon(_ greet: Greeting) } //デリゲート先のクラス class Greet: GreetDelegate { var str: String { return "Hey!" } func didMorning(_ greet: Greeting) { print("Good morning!") } func didAfternoon(_ greet: Greeting) { print("See you!") } } //デリゲート元のクラス class Greeting { weak var delegate: GreetDelegate? func greeting() { print("\(delegate?.str ?? "Default")") print("In the morning...") delegate?.didMorning(self) print("In the afternoon...") delegate?.didAfternoon(self) } } //結果を確認しよう let delegate = Greet() let greeting = Greeting() //Greetingクラスのインスタンスgreetingのデリゲート先として、 //Greetクラスを指定する greeting.delegate = delegate //greeting()を実行! greeting.greeting()結果↓
Hey! In the morning... Good morning! In the afternoon... See you!Greetingクラスは、greeting()メソッドの中で、delegateプロパティを通じてデリゲート先のクラスにstrを問い合わせています。また、朝の挨拶(didMorning)と昼の挨拶(didAfternoon)のタイミングをデリゲート元に伝えています。
関数の命名規則や例
デリゲートパターンでは、デリゲート先(Greetクラス)にデリゲート元(Greetingクラス)から呼び出されるメソッド群を実装する必要があります。実装する必要のあるメソッドは、プロトコルとして宣言していました。
メソッドやプロトコルの命名には規則が存在するようです。例えば、みなさんがよく使う(と思う)UITableViewクラスのデリゲートメソッドのなかに、UITableViewCellがタップされた時に実行されるメソッドがあります。それは以下のように宣言されています。public protocol UITableViewDelegate: NSObjectProtocol, UIScrollViewDelegate { (...) optional public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)デリゲートパターンではどのタイミングで呼ばれるかということを明示するため、didやwillをメソッドの先頭につけます。そして、デリゲート先が必要とする情報を引数として渡します。この例では、どのインデックスのセルがタップされたかという情報が必要なので、IndexPath型のindexPathが引数になっています。
よく見ると第一引数にデリゲート元のオブジェクトを渡していますね。なぜかというと、複数のプロトコルに準拠する際、デリゲートメソッド同士の名前が衝突してしまうことがあるかもしれないからです。tableView(_:didSelectRowAt)
メソッドの例では、セルが選ばれた(selectRow)後に(did)UITableViewクラスが呼ぶメソッドであるということが、メソッド名を見るだけでわかるようになっています。
以上のことを踏まえ、Appleの提供するCocoa,Cocoa Touchフレームワーク内で利用されているデリゲートパターンでは次の命名規則にしたがっているとまとめることができます。・didやwillを用いてメソッドの呼ばれるタイミングを示す
・メソッド名はデリゲート元のオブジェクト名から始め、続いて処理の内容を説明する
・第一引数にはデリゲート元のオブジェクトを渡す慣れてくるとこれらの規則を満たさなくても記述できると思います。しかし、名前の衝突を回避できたり、初めてコードを読む人にも理解させやすいといった利点があると思うので、命名規則は守る方がいいかなと思いました。
まとめ
今回はSwiftのデリゲートについて細かく紹介しました。デリゲート元とデリゲート先が逆で捉えていたり、具体的な使い方がわからない方(私もまだまだわかっていません…)もいると思います。そう言った方達が少しでも理解してくれたら幸いです。(実践的なコードも更新していけたらいいなと思っています。よろしくお願いします。)
- 投稿日:2019-12-01T15:19:18+09:00
SwiftUI でViewをUIImageに変換する方法
もちべ?
特定のSwiftUIのViewをShareSheetを使ってシェアするために、特定のViewをUIImageに変換する必要がありました。
最初はUIHostingControllerなどを使ってVIewからUIImageを取得しようとしましたが、上手くいかず...SwiftUIの問題は、大抵GeometryReaderを使えば解決できるという巷の噂に身を任せたら解決できました☕️
とにかくコード見せてって人はこちらから
https://gist.github.com/tsuzukihashi/41fbb0c594e26e317cfcec878e9948f4よくわかる解説
まず解説のために適当なViewを用意しました。このViewをUIImageに変換していきます。
struct TestPage: View { var body: some View { HStack { Image(systemName: "sun.haze") .font(.title) .foregroundColor(.white) Text("Hello, World!") .font(.title) .foregroundColor(.white) } .padding() .background(Color.blue) .cornerRadius(8) } }次にUIImageに変換したいViewのサイズと位置を取得したいです。
今回で言うとHStackのCGRect
がわかれば良いでしょう。ここで一つ目のポイントなのですが、Viewの
CGRect
を取得するためにbackground
メソッドを利用します。
backgroundの引数はViewプロトコルに準拠していれば良いので、Viewを返しつつサイズを取得するようなRectangleGetter
を作成します。以下がbackgroundメソッドの定義です。Viewを返せば良いらしい?
func background<Background>(_ background: Background, alignment: Alignment = .center) -> some View where Background : Viewhttps://developer.apple.com/documentation/swiftui/view/3278516-background
@Binding
で親ViewのCGRectを指定します。
ここでGeometryReaderの出番です!
GeometryReaderで親Viewのgeometryを取得し、createView
メソッドでViewのサイズと位置情報をBindingしたrectに与えます。
Viewを返さなくてはいけないので透明なRectangle
Viewを返します。struct RectangleGetter: View { @Binding var rect: CGRect var body: some View { GeometryReader { geometry in self.createView(proxy: geometry) } } func createView(proxy: GeometryProxy) -> some View { DispatchQueue.main.async { self.rect = proxy.frame(in: .global) } return Rectangle().fill(Color.clear) } }先ほど作成した、TestPageに
@State
のrectを用意します。
HStackのbackgroundにRectangleGetterをかませます。struct TestPage: View { //追加 >>> @State private var rect: CGRect = .zero // <<< var body: some View { HStack { Image(systemName: "sun.haze") .font(.title) .foregroundColor(.white) Text("Hello, World!") .font(.title) .foregroundColor(.white) } .padding() .background(Color.blue) .cornerRadius(8) // 追加 >>> .background(RectangleGetter(rect: $rect)) // <<< } }取得したいViewの位置とサイズが分かったので、最前面の画面が取得できる
UIApplication.shared.windows[0].rootViewController
からUIViewを取得します。UIViewからUIImageに変換するのは従来のやり方で
UIGraphicsImageRenderer
を利用します。
UIViewをextensionしてgetImageメソッドを追加します。extension UIView { func getImage(rect: CGRect) -> UIImage { let renderer = UIGraphicsImageRenderer(bounds: rect) return renderer.image { rendererContext in layer.render(in: rendererContext.cgContext) } } }そして、TestPageの必要なタイミングで
UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)
を呼ぶことでUIImageを取得することができます。UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)struct TestPage: View { @State private var rect: CGRect = .zero //追加 >>> @State var uiImage: UIImage? = nil // <<< var body: some View { //追加 >>> VStack { // <<< HStack { Image(systemName: "sun.haze") .font(.title) .foregroundColor(.white) Text("Hello, World!") .font(.title) .foregroundColor(.white) } .padding() .background(Color.blue) .cornerRadius(8) .background(RectangleGetter(rect: $rect)) //追加 >>> Button(action: { self.uiImage = UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect) }) { Text("Button") .padding() .foregroundColor(Color.white) .background(Color.red) .cornerRadius(8) } if uiImage != nil { VStack { Image(uiImage: self.uiImage!) }.background(Color.green) .frame(width: 500, height: 500) } } // <<< } }以上のようになり。ボタンをタップすると
まとめ
SwiftUIのViewからUIImageを取得するためには
1. View全体を得るためにbackgroundを使う
2. GeometryReaderを使ってCGRectを取得する
3. そのCGRectを使ってUIViewからUIImageを取得する完成したコード
https://gist.github.com/tsuzukihashi/41fbb0c594e26e317cfcec878e9948f4問題点
この方法は完全にViewだけをUIImageにしているのではなく、Viewの範囲をとってUIImageに変換しています。
したがって、このViewの上に別のViewがかぶっていた場合でも、かぶった範囲が一緒にUIImageになってしまいます。参考にした記事URL
iOS:最前面に画面を出す/最前面の画面を知る
SwiftUIの肝となるGeometryReaderについて理解を深め
SwiftUIでChildViewのFrameを取得する
- 投稿日:2019-12-01T11:35:56+09:00
cygwinでSwiftを使う方法
はじめに
Windows 7 以降の 64BIT 版 Windows には、64BIT 版の cygwin の中に Apple iOS の標準言語であるところの Swift をインストールして使うことが出来ます。
ただし、予め以下のもののインストールが必要です。
- cygwin 64BIT バージョン。
- cygwin 版 clang ver 8 パッケージ。
- cygwin 版 libiconv パッケージ。x86_64-w64-mingw32 というもの。
- cygwin 版 libicu60 (6.0.2-1) パッケージ。
インストールすると、少なくとも以下のコマンドが使えるようになります:
swiftc : *.swift から cygwin で実行できる native binary を生成するコンパイラ。
print("Hello World") をコンパイルして実行できることを確認済みです。
また、LLVM の可読的中間言語(IR) の *.ll を出力できることも確認済みです。
swiftc は、バックエンドに clang と ld を使っています。swift : 対話式インタプリタ(REPL)。
1 + 2[ret] と入れると 3 と出ます。インストール方法
インストール方法を手順を追って説明します。
Win7, 64BIT に 64BIT 版 cygwin をインストール
cygwin の GUI インストーラーを使って、cygwin 公式サイトからインストールします。
以下で cygwin のインストールフォルダは、c:\cygwin64 とします。その際、インストーラーで以下のパッケージをチェック状態にしておきます。
clang の ver 8 のパッケージ。
libiconv 系パッケージ。x86_64-w64-mingw32 的な名前のもの。これがインストールされると、
/usr/x86_64-w64-mingw32/sys-root/mingw/lib
に libiconv.a と libiconv.dll.a がインストールされる。これは、swiftc コンパイラが使う。Category: libs の Package: libicu60 (6.0.2-1) パッケージ。
ICU とは、International Components for Unicode のこと。
これをインストールすると、少なくとも C:\cygwin64\bin に cygicui18n60.dll, cygicuuc60.dll
がインストールされます。これは、swiftc でコンパイルして生成された実行ファイルが起動されるときに動的ライブラリとして読み込まれます。Swift のダウンロードと展開
https://swiftforwindows.github.io/news/2018/02/12/Swift-for-Windows-Cygwin-20180212/
https://github.com/tinysun212/swift-windows/releases/tag/swift-4.0.3+cygwin.20180212にて、cygwin 用に非公式に公開されている swift-4.0.3.cygwin.20180212-bin.tar.gz をダウンロードします。*.7Z 形式の方がサイズがだいぶ小さいのでそちらの方が良いかも知れません。以下では、*.tar.gz の方で説明しておきます。
$ tar -xvzf swift-4.0.3.cygwin.20180212-bin.tar.gzのようにして展開すると、xxx/usr/bin にコマンド類が、xxx/usr/lib にライブラリが配置されます。
ただし、xxx は、./swift-4.0.3.cygwin.20180212-bin となるはずです。
- xxx/usr/bin には swift.exe と swiftc.exe が出来ています。
- swiftc.exe がコンパイラで、swift.exe が対話式インタプリタです。
- swiftc.exe は、*.ll のような LLVM コードを出力することも出来ます。
パスの設定
上記の xxx/usr/bin のフォルダのフルパスを、PATH 環境変数に追加しておきます。
swift-4.0.3.cygwin.20180212-bin.tar.gz を展開した場所が c:\xxx だった場合、以下の様になります。
$ export PATH=/cygdrive/c/xxx/usr/bin:$PATHライブラリパスの設定
clang のリンカ ld がライブラリの *.a, *.o をリンクする際に検索対象にする のパスを指定するため、cygwin の端末の中から、
$ export LIBRARY_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw/libとします。
テスト
以下の様なテストプログラムを作っておきます:
HelloWorld.swiftprint("Hello World!")コンパイル
$ swiftc -v HelloWorld.swiftとすると、
Swift version 4.0.3 (swift-4.0.3+cygwin.20180212) Target: x86_64-unknown-windows-cygnus /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift -frontend -c -primary-file elloWorld.swift -target x86_64-unknown-windows-cygnus -disable-objc-interop -module-name HelloWorld -o /tmp/HelloWorld-1bba48.o /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift-autolink-extract tmp/HelloWorld-1bba48.o -o /tmp/HelloWorld-40659b.autolink /usr/bin/clang++ -Xlinker -rpath -Xlinker /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin /tmp/HelloWorld-1bba48.o -L /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin -lswiftCore --target=x86_64-unknown-windows-cygnus @/tmp/HelloWorld-40659b.autolink -o HelloWorldと出力されます。
実行テスト
$ ./HelloWorld Hello World!となります。
対話式インタプリタ
$ swiftとすると、対話式インタプリタが起動します。
備忘録
もし、shared library が見つからないと言うエラーが出たような場合、
cygcheck ./HelloWorldとすると、どんな名前の shared library が見つからないかが、stderr に出力されます。ですので、
$ cygcheck ./HelloWorld 1>a 2>bとして、b のファイルをテキストエディタで見ればよいです。それは例えば、
cygcheck: track_down: could not find cygicui18n60.dll cygcheck: track_down: could not find cygicuuc60.dllのように出力されます。
- 投稿日:2019-12-01T11:35:56+09:00
Windows 上の cygwinでSwift コンパイラ/インタプリタを使う方法
はじめに
Windows 7 以降の 64BIT 版 Windows には、64BIT 版の cygwin の中に Apple iOS の標準言語であるところの Swift をインストールして使うことが出来ます。
ただし、予め以下のもののインストールが必要です。
cygwin 64BIT バージョン。
cygwin 版 clang ver 8 パッケージ。
cygwin用のパッケージとしては、ver6, ver8 は有りますが、ver7 は有りません。cygwinパッケージとしてではなく、Windows 用の clang ver7をLLVM公式サイトからインストールしてそこにパスを通している場合は、今回のバージョンの Swift とは恐らくコマンドラインの書き方の一部に互換性が無いようです。よって、必ず ver 8 の clang を使ってください。なお、Swiftにおいて、clang または、clang を実装するためのコアライブラリが、llvm の backend として使われているようです。Swift は、論理的には *.swift を、Swift Intermediate Language (SIL) というSwift 専用の中間言語ファイル *.sil にコンパイルした後、 可読的 LLVM 言語 *.ll に直し、LLVM バイナリ *.bc に直し、native のオブジェクトコード *.o に直し、最後にリンカの ld で native の実行ファイルの *.exe などにリンクする、ということをしています。ただし、ディスク上のファイルを出力せずに一気に *.o に変換することも出来ます。なお、一旦テンポラリディスクファイルを作ってから消しているとは限らず、中間形式はオンメモリで処理されている可能性も有ります。なお *.ll は、オンメモリにすら作られずに、一気に *.bc に直されている可能性があります。cygwin 版 libiconv パッケージ。x86_64-w64-mingw32 というもの。
cygwin 版 libicu60 (6.0.2-1) パッケージ。
インストールすると、少なくとも以下のコマンドが使えるようになります:
swiftc : *.swift から cygwin で実行できる native binary を生成するコンパイラ。
print("Hello World") をコンパイルして実行できることを確認済みです。
また、LLVM の可読的中間言語(IR) の *.ll を出力できることも確認済みです。
swiftc は、バックエンドに clang と ld を使っています。swift : 対話式インタプリタ(REPL)。
1 + 2[ret] と入れると 3 と出ます。インストール方法
インストール方法を手順を追って説明します。
Win7, 64BIT に 64BIT 版 cygwin をインストール
cygwin の GUI インストーラーを使って、cygwin 公式サイトからインストールします。
以下で cygwin のインストールフォルダは、c:\cygwin64 とします。その際、インストーラーで以下のパッケージをチェック状態にしておきます。
clang の ver 8 のパッケージ。
libiconv 系パッケージ。x86_64-w64-mingw32 的な名前のもの。これがインストールされると、
/usr/x86_64-w64-mingw32/sys-root/mingw/lib
に libiconv.a と libiconv.dll.a がインストールされる。これは、swiftc コンパイラが使う。Category: libs の Package: libicu60 (6.0.2-1) パッケージ。
ICU とは、International Components for Unicode のこと。
これをインストールすると、少なくとも C:\cygwin64\bin に cygicui18n60.dll, cygicuuc60.dll
がインストールされます。これは、swiftc でコンパイルして生成された実行ファイルが起動されるときに動的ライブラリとして読み込まれます。Swift のダウンロードと展開
https://swiftforwindows.github.io/news/2018/02/12/Swift-for-Windows-Cygwin-20180212/
https://github.com/tinysun212/swift-windows/releases/tag/swift-4.0.3+cygwin.20180212にて、cygwin 用に非公式に公開されている swift-4.0.3.cygwin.20180212-bin.tar.gz をダウンロードします。*.7Z 形式の方がサイズがだいぶ小さいのでそちらの方が良いかも知れません。以下では、*.tar.gz の方で説明しておきます。
$ tar -xvzf swift-4.0.3.cygwin.20180212-bin.tar.gzのようにして展開すると、xxx/usr/bin にコマンド類が、xxx/usr/lib にライブラリが配置されます。
ただし、xxx は、./swift-4.0.3.cygwin.20180212-bin となるはずです。
- xxx/usr/bin には swift.exe と swiftc.exe が出来ています。
- swiftc.exe がコンパイラで、swift.exe が対話式インタプリタです。
- swiftc.exe は、*.ll のような LLVM コードを出力することも出来ます。
パスの設定
上記の xxx/usr/bin のフォルダのフルパスを、PATH 環境変数に追加しておきます。
swift-4.0.3.cygwin.20180212-bin.tar.gz を展開した場所が c:\xxx だった場合、以下の様になります。
$ export PATH=/cygdrive/c/xxx/usr/bin:$PATHライブラリパスの設定
clang のリンカ ld がライブラリの *.a, *.o をリンクする際に検索対象にする のパスを指定するため、cygwin の端末の中から、
$ export LIBRARY_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw/libとします。
テスト
以下の様なテストプログラムを作っておきます:
HelloWorld.swiftprint("Hello World!")コンパイル
$ swiftc -v HelloWorld.swiftとすると、
Swift version 4.0.3 (swift-4.0.3+cygwin.20180212) Target: x86_64-unknown-windows-cygnus /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift -frontend -c -primary-file elloWorld.swift -target x86_64-unknown-windows-cygnus -disable-objc-interop -module-name HelloWorld -o /tmp/HelloWorld-1bba48.o /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift-autolink-extract tmp/HelloWorld-1bba48.o -o /tmp/HelloWorld-40659b.autolink /usr/bin/clang++ -Xlinker -rpath -Xlinker /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin /tmp/HelloWorld-1bba48.o -L /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin -lswiftCore --target=x86_64-unknown-windows-cygnus @/tmp/HelloWorld-40659b.autolink -o HelloWorldと出力されます。
実行テスト
$ ./HelloWorld Hello World!となります。
対話式インタプリタ
$ swiftとすると、対話式インタプリタが起動します。
備忘録
もし、shared library が見つからないと言うエラーが出たような場合、
cygcheck ./HelloWorldとすると、どんな名前の shared library が見つからないかが、stderr に出力されます。ですので、
$ cygcheck ./HelloWorld 1>a 2>bとして、b のファイルをテキストエディタで見ればよいです。それは例えば、
cygcheck: track_down: could not find cygicui18n60.dll cygcheck: track_down: could not find cygicuuc60.dllのように出力されます。
- 投稿日:2019-12-01T11:35:56+09:00
cygwinでSwiftを使う方法。
はじめに
Windows 7 以降の 64BIT 版 Windows には、64BIT 版の cygwin の中に Swift をインストールして使うことが出来ます。
ただし、予め以下のもののインストールが必要です。
- cygwin 64BIT バージョン。
- cygwin 版 clang ver 8 パッケージ。
- cygwin 版 libiconv パッケージ。x86_64-w64-mingw32 というもの。
- cygwin 版 libicu60 (6.0.2-1) パッケージ。
インストールすると、少なくとも以下のコマンドが使えるようになります:
swiftc : *.swift から cygwin で実行できる native binary を生成するコンパイラ。
print("Hello World") をコンパイルして実行できることを確認済みです。
また、LLVM の可読的中間言語(IR) の *.ll を出力できることも確認済みです。
swiftc は、バックエンドに clang と ld を使っています。swift : 対話式インタプリタ(REPL)。
1 + 2[ret] と入れると 3 と出ます。インストール方法
インストール方法を手順を追って説明します。
Win7, 64BIT に 64BIT 版 cygwin を、GUI インストーラーを使ってインストールする。
以下で cygwin のインストールフォルダは、c:\cygwin64 とする。
cygwin に、以下のパッケージをインストールしておく:
clang の ver 8 のパッケージ。
libiconv 系パッケージ。x86_64-w64-mingw32 的な名前のもの。これがインストールされると、
/usr/x86_64-w64-mingw32/sys-root/mingw/lib
に libiconv.a と libiconv.dll.a がインストールされる。これは、swiftc コンパイラが使う。Category: libs の Package: libicu60 (6.0.2-1) パッケージ。
ICU とは、International Components for Unicode のこと。
これをインストールすると、少なくとも C:\cygwin64\bin に cygicui18n60.dll, cygicuuc60.dll
がインストールされる。これは、swiftc でコンパイルして生成された実行ファイルが起動されるときに動的ライブラリとして読み込まれる。Swift の DL とインストール
https://swiftforwindows.github.io/news/2018/02/12/Swift-for-Windows-Cygwin-20180212/
https://github.com/tinysun212/swift-windows/releases/tag/swift-4.0.3+cygwin.20180212にて、cygwin 用に非公式に公開されている swift-4.0.3.cygwin.20180212-bin.tar.gz を DL する。
例: K:\Commu\FromOther\cygwin\Swift\swift-4.0.3tar -xvzf swift-4.0.3.cygwin.20180212-bin.tar.gzのように展開すると、xxx/usr/bin にコマンド類が、xxx/usr/lib にライブラリができます。
ただし、xxx は、./swift-4.0.3.cygwin.20180212-bin となるはずです。xxx/usr/bin には swift.exe と swiftc.exe が出来ている。
swiftc.exe がコンパイラで、swift.exe が対話式インタプリタである。
swiftc.exe は、*.ll に LLVM コードを出力することも出来る。
パスの設定
上記の xxx/usr/bin のフォルダのフルパスを、PATH 環境変数に追加しておきます。
swift-4.0.3.cygwin.20180212-bin.tar.gz を展開した場所が c:\xxx だった場合、以下の様になります。
$ export LIBRARY_PATH=/cygdrive/c/xxx/usr/bin:$PATH環境変数の設定
clang のリンカ ld がライブラリの *.a, *.o をリンクする際に検索対象にする のパスを指定するため、cygwin の端末の中から、
$ export LIBRARY_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw/libとします。
テスト
以下の様なテストプログラムを作っておく:
HelloWorld.swiftprint("Hello World!")コンパイル
$ ./swiftc -v HelloWorld.swiftとすると、
Swift version 4.0.3 (swift-4.0.3+cygwin.20180212) Target: x86_64-unknown-windows-cygnus /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift -frontend -c -primary-file elloWorld.swift -target x86_64-unknown-windows-cygnus -disable-objc-interop -module-name HelloWorld -o /tmp/HelloWorld-1bba48.o /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/bin/swift-autolink-extract tmp/HelloWorld-1bba48.o -o /tmp/HelloWorld-40659b.autolink /usr/bin/clang++ -Xlinker -rpath -Xlinker /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin /tmp/HelloWorld-1bba48.o -L /cygdrive/k/Commu/FromOther/cygwin/Swift/swift-4.0.3/usr/lib/swift/cygwin -lswiftCore --target=x86_64-unknown-windows-cygnus @/tmp/HelloWorld-40659b.autolink -o HelloWorld となる。対話式インタプリタ
$ ./swiftとすると、対話式インタプリタが起動する。
備忘録
もし、shared library が見つからないと言うエラーが出たような場合、
cygcheck ./HelloWorldとすると、どんな名前の shared library が見つからないかが、stderr に出力される。なので、
$ cygcheck ./HelloWorld 1>a 2>bとして、b のファイルをテキストエディタで見ればよい。それは例えば、
cygcheck: track_down: could not find cygicui18n60.dll cygcheck: track_down: could not find cygicuuc60.dllのように出力される。
- 投稿日:2019-12-01T11:34:26+09:00
修羅の道 to iOSの電波強度取得
概要
2019年も残りあとわずか!
あと少しで、令和初の年越しを体験できそうですね。
そんな2019/12/1の、ユアマイスターアドベントカレンダー1日目です。
https://qiita.com/advent-calendar/2019/yourmystar
この記事では、iOSの電波強度を取得するために色々と調べたことをまとめます。
この記事の最新のiOSバージョンとは、iOS 13.2.3のことを指します。道のり
以前は、iOSの電波強度を取得するために statusBar から情報を取得することができていました。
しかし、最新のiOSバージョンではは statusBar に signalStrength or RSSI の値は保有していないことがわかりました。。。statusBarとは?
statusBar とはiPhoneの上部にある電波マークや電池のマークがある部分のことを言います。
今現在もアプリからstatusBarの色を変更させたり、wifiに接続されているかということは確認ができます。
最新のiOSバージョンでも、アプリからではなく端末からだとRSSI値が確認できます。その方法は下記の参考記事にあります。現在、アプリから電波強度を取得するには
色々と探した結果、statusBarから情報を取得する以外に、NEHotspotHelperを用いる方法があるようです。 NEHotspotHelperはXcodeが用意してくれているAPIでSSIDの情報などを得ることができます。
注意点
NEHotspotHelperでSSIDの情報から電波強度を取得できると言いましたが注意点があります。
其の1 ユーザの許可が必要
SSIDの情報から位置情報などがわかるためユーザに許可を得る必要があります。こちらは、info.plistに以下を追加すればOKです。
<key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> <string>com.apple.networkextension.app-proxy</string> <key>NSExtensionPrincipalClass</key> <string>MyCustomAppProxyProvider</string> </dict>其の2 無料アカウントではダメ
Xcodeの無料アカウントでは使えないAPIのようです。Apple Developer Programに登録して年間1万円ちょい支払う必要があります。
其の3 有料ユーザでも使えるかわからない?
自分用のアプリとして作っているのでお金払って使えなかったら嫌だなと思い、Xcodeのサポートに問い合わせてみました。すると、AppleDeveloperProgramに登録したユーザでも使える訳では無いとのこと。使う旨をXcodeに伝え審査を通ると使えるようになるようです。審査の内容については聞くことはできませんでした。意外と1番の落とし穴かもしれません。参考にした記事
SwiftでWiFi電波強度を取得する
著 p9KYcJ5V さん
https://qiita.com/p9KYcJ5V/items/718ffa057302beff757diPhone 電波マークを数字表示にして電波強度を分かり易くしよう♪
https://www.kototoka.com/entry/2014/07/31/apple-iphone-lte-3g-antennamark-suuji[iOS 10] 接続中のWi-FiのSSIDを取得する
https://dev.classmethod.jp/smartphone/ios-10-cncopysupportedinterfaces/
- 投稿日:2019-12-01T09:29:02+09:00
iOSのSpotlight検索に自分のアプリ内コンテンツを検索対象にする方法
Spotlight検索とは・・・?
ホーム画面で下にスワイプしたりすると出るあれです。
入力欄にキーワードを入力すると、各アプリのコンテンツなどから、一致するものを探してもらえるので、横断的にアプリのコンテンツを検索できるので便利です。
今回は、このSpotlight検索に、自分のアプリのコンテンツを検索結果に表示させる方法についての記事です。
TodayExtensionやリッチプッシュなどのように、追加で証明書などもいらず、簡単に実装できてアプリへの導線が増やせるので、やってみてはいかがでしょうか?
実装方法
1.準備
プロジェクトにMobileCoreServiceとCoreSpotlightのFrameworkを入れます。
2.保存処理を実装する
今回は、例として映画の情報である以下のMovie構造体をSpotlight検索に保存してみましょう。
Movie.swiftstruct Movie { /// 映画固有の識別番号 let id: Int = 0 /// 名前 let title: String = "スパイダーマン" /// あらすじ let summary: String = "平凡な少年、ピーター・パーカーは放射能汚染された蜘蛛に噛まれたことで、超人的な能力を得てしまう・・・" /// 画像 let thumbnail: UIImage? = UIImage(named: "spider") /// 役者名の配列 let actorNames: [String] = ["トビー・マグワイア", "キルスティン・ダンスト", "ジェームズ・フランコ"] }SpotlightManager.swiftimport Foundation import MobileCoreService import CoreSpotlight final class SpotlightManager { func save(_ movie: Movie) { let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String) // ①タイトル attributeSet.title = movie.title // ②説明文 attributeSet.contentDescription = movie.summary // ③画像 attributeSet.thumbnailData = movie.thumbnail.pngData() // キーワード(表示されないが、タイトルや説明文に入ってない文言をここに入れておけば、検索した時に引っかかるようになる) attributeSet.keywords = movie.actorNames /* uniqueIdentifierはAppDelegateで取り出すことができるので、 Spotlight検索経由でアプリを開いた時のためのURLスキームを入れておく */ let item = CSSearchableItem( uniqueIdentifier: "my-app://open/movie?id=\(movie.id)", domainIdentifier: "my-app", attributeSet: attributeSet ) CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil) } }3.起動時の処理を実装する
AppDelegate.swiftimport CoreSpotlight extension AppDelegate { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { // Spotlightで開かれたかどうかをチェックする switch userActivity.activityType { case CSSearchableItemActionType: return self.openApplicationFromSpotlight(userActivity) default: return false } } private func openApplicationFromSpotlight(_ userActivity: NSUserActivity) -> Bool { // userActivityからURLスキームを取得する guard let urlScheme = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else { return false } // URLスキームを開いた時の処理を実装する return true } }4.保存処理を作る
ViewController.swiftfinal class ViewController: UIViewController { let spotlightManager = SpotlightManager() private func show(_ movie: Movie) { self.spotlightManager.save(movie) } }5.確認する
title
、description
、keyword
に設定した文字列を検索欄に入力すると表示されます。
以上で、ざっくりとした実装は以上です。
おまけ
今回紹介した実装では、Spotlightに保存する画像をローカルのものを使用しましたが、URLで画像を取得して表示することも可能です。
例として画像キャッシュライブラリのNuke
を使用した方法を記載しておきます。SpotlightManager.swiftimport Foundation import MobileCoreService import CoreSpotlight import Nuke func save(_ movie: Movie) { ImagePipeline.shared.loadImage( with: movie.thumbnailUrl, progress: nil) { response, _ in guard let thumbnail = response?.image else { return } let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String) attributeSet.title = movie.title attributeSet.contentDescription = movie.summary attributeSet.thumbnailData = thumbnail.pngData() attributeSet.keywords = movie.actorNames let item = CSSearchableItem( uniqueIdentifier: "my-app://open/movie?id=\(movie.id)", domainIdentifier: "my-app", attributeSet: attributeSet ) CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil) } }
- 投稿日:2019-12-01T09:29:02+09:00
iOSのSpotlight検索で自分のアプリ内コンテンツを検索対象にする方法
Spotlight検索とは・・・?
ホーム画面で下にスワイプしたりすると出るあれです。
入力欄にキーワードを入力すると、各アプリのコンテンツなどから、一致するものを探してもらえるので、横断的にアプリのコンテンツを検索できるので便利です。
今回は、このSpotlight検索に、自分のアプリのコンテンツを表示させる方法についての記事です。
TodayExtensionやリッチプッシュなどのように、追加で証明書などもいらず、簡単に実装できてアプリへの導線が増やせるので、やってみてはいかがでしょうか?
実装方法
1.準備
プロジェクトにMobileCoreServiceとCoreSpotlightのFrameworkを入れます。
2.保存処理を実装する
今回は、例として映画の情報である以下のMovie構造体をSpotlight検索に保存してみましょう。
Movie.swiftstruct Movie { /// 映画固有の識別番号 let id: Int = 0 /// 名前 let title: String = "スパイダーマン" /// あらすじ let summary: String = "平凡な少年、ピーター・パーカーは放射能汚染された蜘蛛に噛まれたことで、超人的な能力を得てしまう・・・" /// 画像 let thumbnail: UIImage? = UIImage(named: "spider") /// 役者名の配列 let actorNames: [String] = ["トビー・マグワイア", "キルスティン・ダンスト", "ジェームズ・フランコ"] }SpotlightManager.swiftimport Foundation import MobileCoreService import CoreSpotlight final class SpotlightManager { func save(_ movie: Movie) { let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String) // ①タイトル attributeSet.title = movie.title // ②説明文 attributeSet.contentDescription = movie.summary // ③画像 attributeSet.thumbnailData = movie.thumbnail.pngData() // キーワード(表示されないが、タイトルや説明文に入ってない文言をここに入れておけば、検索した時に引っかかるようになる) attributeSet.keywords = movie.actorNames /* uniqueIdentifierはAppDelegateで取り出すことができるので、 Spotlight検索経由でアプリを開いた時のためのURLスキームを入れておく */ let item = CSSearchableItem( uniqueIdentifier: "my-app://open/movie?id=\(movie.id)", domainIdentifier: "my-app", attributeSet: attributeSet ) CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil) } }3.起動時の処理を実装する
AppDelegate.swiftimport CoreSpotlight extension AppDelegate { func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { // Spotlightで開かれたかどうかをチェックする switch userActivity.activityType { case CSSearchableItemActionType: return self.openApplicationFromSpotlight(userActivity) default: return false } } private func openApplicationFromSpotlight(_ userActivity: NSUserActivity) -> Bool { // userActivityからURLスキームを取得する guard let urlScheme = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else { return false } // URLスキームを開いた時の処理を実装する return true } }4.保存処理を作る
ViewController.swiftfinal class ViewController: UIViewController { let spotlightManager = SpotlightManager() private func show(_ movie: Movie) { self.spotlightManager.save(movie) } }5.確認する
title
、description
、keyword
に設定した文字列を検索欄に入力すると表示されます。
以上で、ざっくりとした実装は以上です。
おまけ
今回紹介した実装では、Spotlightに保存する画像をローカルのものを使用しましたが、URLで画像を取得して表示することも可能です。
例として画像キャッシュライブラリのNuke
を使用した方法を記載しておきます。SpotlightManager.swiftimport Foundation import MobileCoreService import CoreSpotlight import Nuke func save(_ movie: Movie) { ImagePipeline.shared.loadImage( with: movie.thumbnailUrl, progress: nil) { response, _ in guard let thumbnail = response?.image else { return } let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String) attributeSet.title = movie.title attributeSet.contentDescription = movie.summary attributeSet.thumbnailData = thumbnail.pngData() attributeSet.keywords = movie.actorNames let item = CSSearchableItem( uniqueIdentifier: "my-app://open/movie?id=\(movie.id)", domainIdentifier: "my-app", attributeSet: attributeSet ) CSSearchableIndex.default().indexSearchableItems([item], completionHandler: nil) } }
- 投稿日:2019-12-01T09:00:15+09:00
【Swift】nilってなんだろう?
nilってなんだろう?
nilとは、値が存在しないことを示すものです。他の言語ではnillやnoneと言った名前の場合もありますよね。
多くの言語で、変数や定数の初期化がされていない状態や参照先が存在しない状態を示す値として活用されています。一方で、初期化されていない値や参照先が存在しない値へのアクセスによる実行時エラーを招いてしまうという問題もありました。Swiftでのnilの扱い
Swiftでは、nilを許容する特別な型へしかnilを代入できない仕組みになっています。こうすることで、予期せぬところでnilが使用されず、上で述べたような実行時エラーをなくすということを実現しています。
Swiftでnilを扱える代表的な型は、Optional型です。これは、WrappedのところにIntやStringなどの具体的な型を指定して使用します。Intがnilを扱えないのに対し、Optional型はIntとnilの両方を扱えます。下に例を示します。optional.swiftlet number: Int //Int型なのでnilは入れられない let optionalNumber: Optional<Int> //nilを入れられる number = nil //コンパイルエラー optionalNumber = nil //OKまとめ
Swiftでは、基本的にはnilを扱えない仕様となっています。この仕様により、コンパイル時に値の有無を判断しています。これにより、意図していないところでnilが発生し、プログラムやアプリがクラッシュすることを未然に防いでいると言うことができるのではないでしょうか?
- 投稿日:2019-12-01T01:47:04+09:00
[Swift]新規プロジェクト起動するとCould not find a storyboard named ‘Main’ in bundle NSBundleでひたすら動かない
状況
Main.storyboard
を消してHome.storyboard
とかを新しく作ったらCould not find a storyboard named ‘Main’ in bundle NSBundleがでてクラッシュする
ポイント
Home.storybord
にあるViewControllerと(例えば)HomeVC
の紐付けはできているIs Initial View Controller
はチェック入れてるMain storyboard file base name
を消す or Main->Homeに変更も両方やったCopy Bundle Resources
にもドラッグした対策
- 初期で生成される
Main.storyboard
は残したままプロジェクトの作成を続ける ->何らかの形で初期で生成したstoryboardの情報や参照状況が残っているからバグが起こる?- 名前を変えたい場合は
Main.storyboard
を残したまま名前だけ変える orMain.storyboard
の削除時にMove to trash
せずにRemove reference
するにとどめる参考
・Could not find a storyboard named 'Main' in bundle NSBundle 対処方法 - Qiita
・"Could not find a storyboard named 'Main' in bundle NSBundle" のエラーを吐いてしまい実機で動かない | makotton.com
・xibをロードしようとしたらアプリ落ちる問題に少しハマったのでメモ - Qiita
- 投稿日:2019-12-01T00:40:10+09:00
SwiftUI と UIKit 混合環境で開発を行うときの tips 集
はじめに
AkkeyTV という動画視聴アプリはこの記事執筆段階で UIKit と SwiftUI を用いて開発されています。
iOS13 未満は UIKit で構築された UI をロードし、 iOS13 以降では SwiftUI で構築された UI をロードするため、分岐処理が必要になります。また、1画面のために2パターンの UI 設計をする必要があるため、開発効率的にはよくありません。ただ、 SwiftUI で設計する能力は今後必要になるため、勉強も兼ねて UIKit と SwiftUI の混合環境を選んでいます。
この混合環境を実現する上で学んだことをここにまとめたいと思います。SwiftUI.framework の weak link
Carthage などでライブラリを追加した場合、 Xcode でパスの設定も行うと思います。Apple が提供する framework に関してはこれを行わなくても import して呼び出すことができます。これが Auto Link です。
これが iOS13 未満の端末でアプリを起動した際にも動いてしまうため、 SwiftUI が実装されているアプリはクラッシュしてしまいます。解決方法ですが、明示的に
SwiftUI.framework
をweak link
として設定してあげることで対応可能です。
Build Phases > Link Binary With Libraries > SwiftUI.framework [Optional]UIHostingController
SwiftUI で設計されたものを UIKit 側から触るときは
UIHostingController
を使用します。
SwiftUI で設計された SwiftUIView() というものを定義している場合、以下のように画面遷移を記述することができます。guard #available(iOS 13.0.0, *) else { return } let vc = UIHostingController(rootView: SwiftUIView()) self.present(vc, animated: true)また、「すでに表示されてたら画面遷移処理を行わない」といった処理を行うときも
UIHostingController
を用いて対応可能です。guard #available(iOS 13.0.0, *) else { return } let vc = UIHostingController(rootView: SwiftUIView()) let frontVC = ... if !(frontVC is UIHostingController<SwiftUIView>) { self.present(vc, animated: true) }*Representable
UIKit で設計されたものを SwiftUI 側から触るときは *Representable に準拠させてあげます。具体的には、
UIViewController
はUIViewControllerRepresentable
に、UIView
はUIViewRepresentable
に準拠させてあげます。
UIKit で設計された AkkeyView() というものを定義している場合、以下のようにプレビューさせることができます。import UIKit #if canImport(SwiftUI) && DEBUG import SwiftUI @available(iOS 13, *) struct AkkeyViewPreviews: PreviewProvider { static var previews: some View { Group { AkkeyView() .previewLayout(.fixed(width: 414, height: 100)) .previewDevice(PreviewDevice(rawValue: "iPhone XS Max")) } } static var platform: PreviewPlatform? = .iOS } @available(iOS 13, *) extension AkkeyView: UIViewRepresentable { typealias UIViewType = AkkeyView func makeUIView(context: Context) -> AkkeyView { return .init() } func updateUIView(_ uiView: AkkeyView, context: Context) { // Make parameter change for preview } } #endifさいごに
SwiftUI 導入を手助けする方法をいくつか考えていますので、よかったら参考にしてみてください。
AkkeyLab/AutoPreviewable
UIKit で設計された画面をプレビューするクラスを自動生成させるAkkeyLab/XcodePreviewsTemplate
UIKit のクラスファイルを作成するときにプレビューするクラスを自動生成させるAkkeyLab/StoryboardPreviewsBySwiftUI
実際のプロダクトに XcodePreviews を導入してみた