- 投稿日:2019-12-21T23:22:24+09:00
SwiftUIでgit-blame-pr.plをMacアプリにしてみた
この記事は、ZOZOテクノロジーズ #4 AdventCalendar2019の記事です。
昨日は@satto_sannさんの「FastAPIの負荷実験環境を作ってみる」でした。さて、あるコードが、どういった背景や意図で変更されたのかを知りたいことは、よくあることだと思います。そういった場合にgit-blame-pr.plを使うと、そのコードがどのプルリクエストで変更されたかを簡単に知ることができます。
これはCLIでそのまま使うだけでも十分便利です。
ただ使用頻度が高いこともあり、ユースケースに沿ったGUIが欲しくなりました。
そこで、git-blame-pr.plが出力した結果の行をクリックするだけで、該当のWebページに遷移できるMacアプリを作ってみました。出来上がったものは下記のリポジトリで公開しています。
https://github.com/maoyama/GitBlamePR
GitHubのリリースから最新のGitBlamePR.app.zipをダウンロード、解凍するだけで使えます。ぜひ試してみてください。
※macOS Catalinaが必要です。実装内容の紹介
UIはAppKitではなくSwiftUIで実装しています。
以下のコードがUIにおいては全てです。SwiftUIを使っているので、Storyboardやxibファイルもありません。struct GitBlamePRView: View { var model: GitBlamePRViewModel var textOnCommit: (String) -> Void @State private(set) var fullPath: String = "" var body: some View { VStack(alignment: .leading, spacing: 0) { TextField( "Enter full path", text: $fullPath, onEditingChanged: {_ in }, onCommit: { self.textOnCommit(self.fullPath) } ).lineLimit(1) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() Divider() ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading) { if !model.error.isEmpty { Text(model.error) } ForEach(model.lines, id: \.id) { line in HStack(alignment: .top, spacing: 12) { Text(line.message) .font(Font.system(.caption, design: .monospaced)) .foregroundColor(.accentColor) .frame(width: 80, height: nil, alignment: .trailing) .onTapGesture { NSWorkspace.shared.open(line.url) } Text(line.code) .font(Font.system(.caption, design: .monospaced)) .frame(width: nil, height: nil, alignment: .leading) } } HStack { Spacer() EmptyView() } }.padding() }.background(Color(NSColor.textBackgroundColor)) } } }「Better apps. Less code.」とAppleが謳っている通り、とても少ないコードで実現できています。
またSwiftUIは、全てのAppleプラットフォームで使えます。そのため、僕のような普段はiOSアプリの開発をしているプログラマでも、macOSで動くアプリを作る敷居がとても低くなりました。この記事は以上です。
明日は@alpha_gotoさんです。お楽しみに!
- 投稿日:2019-12-21T23:15:15+09:00
iOS13の設定アプリにつまずいた
はじめに
みなさんご存知かと思いますが iOS13 は設定アプリのクラッシュ祭りでした
設定アプリをクラッシュさせるためのアップデートといっても過言ではありません!!某マッチングアプリの話
iOS13.2.3 で某マッチングアプリがインストールされていると設定アプリがクラッシュするという話もありました。
参考
【iPhone】設定が開けない、落ちる問題がiOS13.2.3で報告 Pairsの削除で改善する事例も
こちらは iOS13.2.3 から各アプリ情報を先読みするようになり某アプリの plist ファイルの値不正(型不正?)によって設定アプリを開くと数秒後にクラッシュするようになっていたようです。
私も調査のためインストールしてみたところ無事クラッシュしました
こちらは某アプリの値不正が原因でしたが、サードパーティ製のアプリが設定アプリに影響するというのは中々のバグなような気もします。(なんか色々悪いことできそう)
某アプリのバグはすぐに修正され今はもうクラッシュしませんみんな大好きLicensePlistの話
ライセンス表示がめっちゃ楽なみんな大好き LicensePlist も iOS13 の設定アプリでクラッシュするというのに遭遇していました
(たぶん全部 iOS 側のバグ
)
押したらクラッシュ!!
とりあえずLicensePlistの各バージョンについて動作を確認してみました。Root.plist の記載は下記。
Root.plist<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>PreferenceSpecifiers</key> <array> <dict> <key>Type</key> <string>PSGroupSpecifier</string> <key>FooterText</key> <string>Copyright</string> </dict> <dict> <key>Type</key> <string>PSChildPaneSpecifier</string> <key>Title</key> <string>Licenses</string> <key>File</key> <string>com.mono0926.LicensePlist</string> </dict> <dict> <key>Type</key> <string>PSTitleValueSpecifier</string> <key>DefaultValue</key> <string>1.0.0</string> <key>Title</key> <string>Version</string> <key>Key</key> <string>sbVersion</string> </dict> </array> <key>StringsTable</key> <string>Root</string> </dict> </plist>LicensePlist 2.6.0
これ 2.6.0: SwiftPM (Swift Package Manager) Support
2.7.1: iOS 13 support の前のバージョンです。(2.7.0入れたかったんですが pod install が失敗するのであきらめました)
iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3 なんかたまにへん(gif わかりにくい場合この Issue の this behavior に動画がありました。)
LicensePlist 2.7.1
これ 2.7.1: iOS 13 support
iOS13 でのクラッシュを受けすぐに有志が動きました。(さすがは人気ライブラリ)
生成される plist ファイルにType: PSGroupSpecifier
を加えたらいけるんじゃないか?とのこと。
iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3 13.1 と 13.2.2 はライセンス詳細から一覧に戻るとクラッシュ!!!
LicensePlist 2.10.0(2019/12/21時点で最新)
これ 2.10.0: Add
--single-page
option
iOS13 対応後もクラッシュの報告は続き対応されたのがこのバージョン。その後も有志が調査してどうやら3階層(Licenses -> ライブラリ一覧 -> ライセンス情報)push だとクラッシュするとのことでライブラリ一覧を削除した Licenses -> ライセンス情報の2階層にするオプション--single-page
が追加されました。
こんな感じ
--single-page
なし
iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
--single-page
あり
iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3 クラッシュしていた原因の詳細はわかりませんが、クラッシュログにやたらと SwiftUI とあったのでそのへんなんだと思います...
このあたりのバグは iOS13.3 で無事修正されたそうです
クラッシュしないけど一度表示して戻ると License を押しても画面遷移しない...(まあクラッシュしないし一回目はちゃんとライセンスがみれるのでOK
)
さいごに
LicensePlist はもう iOS のアップデートを待つしかないと思ってましたが対応されてよかったです
(某アプリは今後もクラッシュしないか調査を続行しようと思います)
ぎりぎりセーフ
- 投稿日:2019-12-21T22:18:01+09:00
Firebaseで超簡単にiOSアプリを配布する?
今回はFirebaseのApp Distributionを使って手動でアプリを配布する方法を紹介いたします
準備するもの
- Firebaseプロジェクトの登録
- Apple Developer Programの登録
- 共有するメンバーのデバイスをDeveloperアカウントに登録
- 共有するメンバーのメールアドレス
- iOS DistributionタイプのCertificates登録
- AdHoc用のProvisioning Profile登録
アプリのipaファイルを取得
アプリのアーカイブを生成
まずは、配布するアプリのビルドをipaとしてアーカイブします。既にアーカイブしている場合はOrganizerからアーカイブを選択します。
アーカイブからipaファイルを生成
AdHocを選択し、手順にそってipaを作成していきます。Select Certificates&ProfileではiOS DistributionタイプのCertificatesとAdHoc用のProvisioning Profileを選択し作成します。
ipaファイルをFirebaseにアップロード
ドラッグ&ドロップまたは
参照
を押してFirebaseにipaファイルをアップロードします.次に共有するメンバーのメールアドレスを登録します。なお、この共有するメンバーのデバイスはApple Developer ProgramのDevicesに登録しておく必要があります。
リリースノートを記入して
配信しました
ボタンを押すとメンバーにFirebaseからメールが届きます。アプリをデバイスにインストール
Download tha latest build
をタップしてProfileをInstallします。Safariでリンクを開き
Download
をタップします次のようなポップアップダイアログが出るのでInstallを押してデバイスにProfileをインストールしていきます。
下記の手順の通り
設定
に移動しProfileをインストールします。ProfileをインストールするとApp Distributionのアプリが追加させていることが確認できます。
アプリをタップすると配信したVersionsの一覧が表示されるので任意のVersionをタップしアプリをダウンロードします。
最後に
今回はiOSをアプリを手動で配布する方法について紹介しましたが、FastlaneとFirebaseCLIでアップロードを行うと配布を自動化することができるので、次はFastlaneでの配信方法を紹介したいと思います?
公式: https://firebase.google.com/docs/app-distribution/ios/distribute-console?authuser=2
- 投稿日:2019-12-21T22:12:57+09:00
Firebase StorageでMetadataを使う
はじめに
こんにちは。
沖縄でエンジニアをしている @yuji_azama です。Firebase Storage、いろんなデータを手軽に保存できて便利ですよね。
Firebase Storage にデータをアップロードするとき、Metadata を指定できることをご存知ですか?
この記事では、ちゃんと使うと便利な Firebase Storage の Metadata について説明します。※サンプルコードは Swift で記述します。
Firebase Storage Metadata とは
Firebase Storage で扱うデータには、Metadata と呼ばれるそのデータに関する付加的な情報(ファイルサイズや contentType など)を含めることができます。
メタデータを設定することで、検索性が良くなったり、Firebase console 上での可視性(イメージプレビュー)が良くなったりします。
実際に使ってみるのがわかりやすいので、さっそく解説していきます。
Metadata を使わないとどうなるか
まずは Metadata を使用せずに JPEG 画像を保存してみます。
let storage = Storage.storage() let storageRef = storage.reference() let ref = storageRef.child("image.jpg") let uploadImage = imageView.image!.jpegData(compressionQuality: 1.0)! as NSData ref.putData(uploadImage as Data, metadata: nil) { _, error in if (error != nil) { print("upload error!") } else { print("upload successful!") } }上記を実行すると、Firebase console 上では以下のように確認できます。
ファイル名を選択することで、画面右側にファイルの詳細が表示できます。
先ほどアップロードした image.jpg のタイプがapplication/octet-stream
となっており、JPEGのファイルとして認識されていないことがわかります。ファイルを保存する目的だけであればこれでも問題はありませんが、ファイルの数が増えてくると console 上で一目で何のファイルなのか確認できないのは何かと不便です。
Metadata を使ってみる
それでは、Metadata を用いてファイルのタイプを指定してみます。
Metadata を使うには、StorageMetadata クラスを使用します。
StorageMetadata をインスタンス化したら、contentType
にimage/jpeg
を設定し、putData メソッドの引数に指定します。let storage = Storage.storage() let storageRef = storage.reference() let ref = storageRef.child("image.jpg") let uploadImage = imageView.image!.jpegData(compressionQuality: 1.0)! as NSData let metadata = StorageMetadata() metadata.contentType = "image/jpeg" ref.putData(uploadImage as Data, metadata: metadata) { _, error in if (error != nil) { print("upload error!") } else { print("upload successful!") } }上記を実行し、Firebase console 上で確認してみましょう。
以下の様に、ファイルの詳細を表示するとタイプにimage/jpeg
が設定されていることがわかります。
それにともなって、画像ファイルのプレビューも表示されているため、ファイルの可視性が向上したと思います。おわりに
この様に、metadata を適切に設定することで Firebase Storage が少し使いやすくなります。
他にも様々な metadata があったり、カスタムメタデータを設定したりできます。
メタデータ利用についてはまだあまり情報がないので、効果的な使い方がどんどん出てくると良いですね。参考
- 投稿日:2019-12-21T22:03:01+09:00
SwiftUIにおけるForEach内からのBindingオブジェクトの渡し方
はじめに
SwiftUIでリストコンテンツを表示するような画面を作る際に、
List
やForEach
等のビューを使用する場面があるかと思います。下のコードでは、
RankingList
というリストビューを持つ画面が、自身のviewModel
のmovies
配列の値を基に、リスト内の各行のビューRankingRow
を生成しています。class RankingViewModel: ObservableObject { @Published var movies: [Movie] ... } struct RankingList: View { @ObservedObject var viewModel: RankingViewModel var body: some View { List { RankingHeader(&viewModel.genre) ForEach(viewModel.movies) { movie in RankingRow(movie: movie) } } } }この方法で
RankingRow
に渡しているのはmovies
配列の要素のコピーとなります。ForEach(viewModel.movies) { movie in RankingRow(movie: movie) // Movieオブジェクトのコピーを渡している }
RankingRow
がMovie
型の実体を受け取る設計であれば何ら問題ありませんが、もしRankingRow
がMovie
のBinding
オブジェクトを受け取りたいとなった場合にはどうすればいいでしょうか。ForEachのイニシャライザ
次のように
viewModel.movies
のBinding
オブジェクトをそのままForEach
に渡したいところですが、残念ながらできません。ForEach($viewModel.movies) {...} // NG: Binding<[Movie]>を渡そうとしている ForEach(viewModel.$movies) {...} // NG: Published<[Movie]>.Publisherを渡そうとしている定義を見てみると分かりますが、
ForEach
の3つのイニシャライザはRandomAccessCollection
に準拠したオブジェクトか、もしくはRange<Int>
型の値しか受け取らないためです。struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable { // 1 init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content) // 2 init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content) // 3 init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content) } // ※ 一部抜粋何らかの配列を渡す方法を別途考えてみます。
Array.indices
Swiftの
Array
には、自身の要素のインデックス範囲を返すindices
プロパティがあります。@inlinable public var indices: Range<Int> { get }
Range<Int>
は上記3のinit(_, content:)
に渡すことのできる型ですが、Range
自体がRandomAccessCollection
に適合しているため、つまり上記1、2のイニシャライザにも渡すことができます。今回はこの値を上記2の
init(_, id:, content:)
に渡すことで当初の問題を解決します。ForEach(viewModel.movies.indices, id: \.self) {...}まとめ
class RankingViewModel: ObservableObject { @Published var movies: [Movie] ... } struct RankingList: View { @ObservedObject var viewModel: RankingViewModel var body: some View { List { RankingHeader(&viewModel.genre) ForEach(viewModel.movies.indices, id: \.self) { movie in RankingRow(movie: self.$viewModel.movies[index]) // OK: Binding<Movie>を渡している! } } } }
ForEach
内で各要素のindex
を受け取ることができるようになった為、viewModel.movies
配列の各要素を子ビューにバインディングして渡せるようになりました。
注意点として、
ForEach
生成時の引数に忘れずにid:
を指定し、上記2のinit(_, id:, content:)
を呼び出す必要があります。引数
id:
を指定しない場合、上記3のinit(_, content:)
を呼び出してしまうのですが、このイニシャライザはImmutable
なRange
オブジェクト向けとなっており、Range
オブジェクトに変化があったとしてもSwiftUIはその変更をキャッチしてビューの更新を行ってくれません。
id:
を指定せずにRange<Int>
を渡した場合、初期化後のオブジェクトの更新時に以下のようなログが出力されます。ForEach<Range<Int>, Int, RankingRow> count (3) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!この警告が表示された際は、今一度
id:
を付け忘れていないか確認してみましょう。
- 投稿日:2019-12-21T18:06:51+09:00
DKImagePickerControllerで写真選択画面をカスタマイズしたい!
自己紹介
はじめまして!社会人1年目でiosアプリ開発を仕事で行っている駆け出しエンジニアです。勉強したことを発信したくて投稿を始めました。至らぬ部分が多々ありますが、参考にしていただけると幸いです笑
環境
- macOS Catalina(10.15.1)
- xcode(11.2.1)
- Swift5
- CocoaPods(1.8.4)
DKImagePickerControllerって何??
DKImagePickerControllerは簡単に言うと写真撮影と写真選択が簡単に実装できる便利なライブラリです。撮影に関しては他にも標準のAVFoundation.framework、写真選択にはUIImagePickerControllerなどがあります。撮影に関しては標準のフレームワークの方が柔軟性がありますが、選択は複数選択できるメリットがあるのでこちらを利用すると便利です。
※事前にxcodeで新規のプロジェクトを作成しておいてください。
物は試し、早速簡単なアプリを作成しながら紹介していきます!
写真選択画面 写真選択画面のアラート表示 1.CocoaPodsでDKImagePickerControllerを導入
ターミナルで以下のように入力してください。(CocoaPodsが既にある方は読み飛ばしてください。)
sudo gem install cocoapodsインストールが完了したら、以下のように入力してください。
pod setup次に、作成したプロジェクトまで移動して、以下のように入力してください。
pod initここまでで、podfileを作成することができました!このファイルは簡単にいうと、自分のプロジェクトにあるライブラリの一覧表みたいなものです。
次に、今作成したpodfileを開いてください。target 'アプリ名' do pod 'DKImagePickerController' end target 'アプリ名Tests' do end target 'アプリ名UITests' do end1行目の「target 'アプリ名' do」の下に「pod 'DKImagePickerController'」と記述して、保存してください。(※わかりやすくするために、初期のpodfileとは少し変えてあります。)
最後に、ターミナルで以下のように入力してください。
pod installこれで、DKImagePickerControllerを自分のプロジェクトに導入することができました!!
2.写真選択画面の作成
次に写真選択画面の作成をします!
予め用意されているViewController.swiftを開き、DKImagePickerControllerをインポートし、継承してください。
ViewController.swiftimport UIKit import DKImagePickerController class ViewController: DKImagePickerController { override func viewDidLoad() { super.viewDidLoad() } }実は、たったこれだけて写真選択画面が作れてしまうんです笑。
しかし、今回はカスタマイズすることが目的なので、もう少しコードを加えていきます!ViewController.swiftclass ViewController: DKImagePickerController { override func viewDidLoad() { super.viewDidLoad() //選択できる写真の最大数を指定 self.maxSelectableCount = 3 //カメラモード、写真モードの選択 self.sourceType = .photo //キャンセルボタンの有効化 self.showsCancelButton = true //UIのカスタマイズ self.UIDelegate = CustomUIDelegate() } }CustomUIDelegate.swiftimport Foundation import DKImagePickerController class CustomUIDelegate: DKImagePickerControllerBaseUIDelegate { //右上のdoneボタンのカスタマイズ override func createDoneButtonIfNeeded() -> UIButton { let button = UIButton(type: UIButton.ButtonType.custom) button.setTitle("選択", for: .normal) button.setTitleColor(UINavigationBar.appearance().tintColor ?? self.imagePickerController.navigationBar.tintColor, for: .normal) return button } //写真選択超過時のアラートのカスタマイズ override func imagePickerControllerDidReachMaxLimit(_ imagePickerController: DKImagePickerController) { let alert = UIAlertController.init(title: "注意", message: "これ以上選択できません!", preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.cancel, handler: nil) alert.addAction(okAction) imagePickerController.present(alert, animated: true, completion: nil) } //cancelボタンのカスタマイズ override func imagePickerController(_ imagePickerController: DKImagePickerController, showsCancelButtonForVC vc: UIViewController) { let cancelButton = UIBarButtonItem() cancelButton.title = "戻る" cancelButton.style = .plain cancelButton.action = #selector(imagePickerController.dismiss as () -> Void) cancelButton.target = imagePickerController vc.navigationItem.leftBarButtonItem = cancelButton } }これらのコードで上記の写真のような写真選択画面が作れます!
アプリを実機で確認してもらいたいのですが、その前にカメラ機能に関する許可をinfo.plistに追加しなければいけません。以下のように項目を追加してください。valueの部分は実際表示される文言なのでなんでも構いません。
key value Privacy - Camera Usage Description カメラ撮影を許可します。 Privacy - Photo Library Usage Description 写真アクセスを許可します。 補足
1点、難しいポイントとして以下のコードがあります。
//UIのカスタマイズ self.UIDelegate = CustomUIDelegate()このライブラリではUIDelegateの定義を以下のように行っています。
/// Use UIDelegate to Customize the picker UI. @objc public var UIDelegate: DKImagePickerControllerBaseUIDelegate! { willSet { newValue?.imagePickerController = self } }①DKImagePickerControllerBaseUIDelegateを継承したクラスを用意し、カスタマイズしたいメソッドをオーバーライドする。
②そのクラスのインスタンスをUIDelegateプロパティにセットする。コメントにもあるように、UIDelegateをこのように使うことで写真選択画面のカスタマイズを行うことができます!
気になる方は、以下のURLを参考にしてみてください!(https://github.com/zhangao0086/DKImagePickerController/tree/develop/Example/DKImagePickerControllerDemo/CustomUIDelegate)最後に
ここまで読んでいただきありがとうございます。これから積極的に記事を投稿していくので、みなさんのアプリ開発の参考に少しでもなるように努力していきます!
- 投稿日:2019-12-21T17:10:37+09:00
Swift MkMapViewで地図アプリ作成してみた(12)- ロングタップした位置と現在位置の標高差をピンに表示する
記事一覧
Swift MkMapViewで地図アプリ作成してみた(記事一覧)
国土地理院から現在位置の標高を取得する
国土地理院のWebAPIから標高を取得する方法の詳細は、「ロングタップした位置の標高を国土地理院から取得する」を参照してください。
URLを生成する
国土地理院から現在位置の標高をJSONで取得するためのURLを作成する。
ViewController.swift:URLを生成する// 国土地理院のURL let baseUrl = "https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?" // 現在位置でクエリを設定する let lonUrl = "&lon=" + mapView.userLocation.coordinate.longitude.description let latUrl = "&lat=" + mapView.userLocation.coordinate.latitude.description // アウトプット形式をJSONに設定する let outtypeUrl = "&outtype=JSON" // URLとクエリを連結 let listUrl = baseUrl + lonUrl + latUrl + outtypeUrl // URLを生成する guard let url = URL(string: listUrl) else { return }現在位置を取得するで現在位置受信を有効にしていれば、mapView.userLocationに現在位置が自動で設定されている。
標高を取得する
URLSessionで生成したURLにアクセスして標高を取得する。
ViewController.swift:標高を取得する// URLを生成する guard let url = URL(string: listUrl) else { return } URLSession.shared.dataTask(with: url) { (data, response, error) in if error != nil { print(error!.localizedDescription) } guard let data = data else { return } // JSONを取得する let json = try? JSONDecoder().decode(JsonElevation.self, from: data) if nil != json { // mainスレッドで処理する DispatchQueue.main.async { // JSONから標高を取得する self.currentElevation = (json?.elevation)! } }.resume() }サブスレッドからメインスレッドに切り替えるためには、DispatchQueue.main.asyncを使用する。
ロングタップした位置と現在位置の標高差をピンに表示する
標高差を計算する
ロングタップした位置の標高を国土地理院から取得するで取得した標高との差を
ViewController.swift:標高差を計算するself.lblDiffElevation = "高低差:" + (round(((self.longTapElevation - self.currentElevation)*10))/10).description + " m" // ロングタップしたアノテーション情報を更新する updateLongTapPointAno()少数第1桁となる様に、"*10))/10"としている。
標高差をピンに表示する
ロングタップした位置にピンを立て距離を表示するで設定したピンのタイトルに連結する。
ViewController.swift:標高差をピンに表示する// ロングタップしたアノテーション情報を更新する func updateLongTapPointAno() { // 現在位置とタッウプした位置の距離(m)を算出する let distance = calcDistance(mapView.userLocation.coordinate, pointAno.coordinate) // ピンに設定する文字列を生成する var str:String = Int(distance).description str = str + " m" // yard let yardStr = Int(distance * 1.09361) str = str + " / " + yardStr.description + " y" // 標高差を連結する str = str + "\n" + lblDiffElevation.description if pointAno.title != str { // ピンまでの距離に変化があればtitleを更新する pointAno.title = str mapView.addAnnotation(pointAno) } }シミュレータの実行結果は以下の様になる。
参考文書
- 投稿日:2019-12-21T17:06:27+09:00
【Swift】Vision.frameworkでカメラ画像の顔認識を行う【iOS】
iOS11より、iOS標準フレームワーク
Vision.framework
を使うと、顔認識ができるらしいので今更ながら使ってみました。概要
カメラ画像から顔を検出し、顔部分に矩形を表示します。
試した環境
- Xcode 11.3
- iOS 13.2
- swift 5
実行サンプル
ぱくたそフリー素材で実験
ディスプレイ画質の問題のせいもありそうですが、顔にちょっと髪がかかってたりすると少し認識が悪い。Google画像検索「顔」で実験
顔が沢山あっても、アップだと良く認識します。
(画像はぼかしてますコード説明
VNImageRequestHandler
を利用して、pixelBuffer
から、顔情報を配列取得します。結果は
VNDetectFaceRectanglesRequest
に非同期で戻されます。
顔情報はVNFaceObservation
です。/// 顔認識情報の配列取得 (非同期) private func getFaceObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNFaceObservation])->())) { let request = VNDetectFaceRectanglesRequest { (request, error) in guard let results = request.results as? [VNFaceObservation] else { completion([]) return } completion(results) } let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) try? handler.perform([request]) }
pixcelBuffer
は カメラから取得したsampleBuffer
をCMSampleBufferGetImageBuffer
を使って変換します。
imageView
には、sampleBuffer
から取得した生成をセット。/// カメラからの映像取得デリゲート func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } getFaceObservations(pixelBuffer: pixelBuffer) { [weak self] faceObservations in guard let self = self else { return } let image = self.getFaceRectsImage(sampleBuffer: sampleBuffer, faceObservations: faceObservations) DispatchQueue.main.async { [weak self] in self?.previewImageView.image = image } } }またその際、
VNFaceObservation
から正規化された画像の位置が取得できるので、
その情報をもとに、矩形を画像に書き込みます。let imageSize = CGSize(width: width, height: height) let faseRects = faceObservations.compactMap { getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize) } faseRects.forEach{ self.drawRect($0, context: newContext) }/// 正規化された矩形位置を指定領域に展開 private func getUnfoldRect(normalizedRect: CGRect, targetSize: CGSize) -> CGRect { return CGRect( x: normalizedRect.minX * targetSize.width, y: normalizedRect.minY * targetSize.height, width: normalizedRect.width * targetSize.width, height: normalizedRect.height * targetSize.height ) }/// コンテキストに矩形を描画 private func drawRect(_ rect: CGRect, context: CGContext) { context.setLineWidth(4.0) context.setStrokeColor(UIColor.green.cgColor) context.stroke(rect) }コード全体
import UIKit import AVFoundation import Vision class FaceViewController: UIViewController { @IBOutlet weak var previewImageView: UIImageView! private let avCaptureSession = AVCaptureSession() override func viewDidLoad() { super.viewDidLoad() setupCamera() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.avCaptureSession.stopRunning() } /// カメラのセットアップ private func setupCamera() { self.avCaptureSession.sessionPreset = .photo let device = AVCaptureDevice.default(for: .video) let input = try! AVCaptureDeviceInput(device: device!) self.avCaptureSession.addInput(input) let videoDataOutput = AVCaptureVideoDataOutput() videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)] videoDataOutput.alwaysDiscardsLateVideoFrames = true videoDataOutput.setSampleBufferDelegate(self, queue: .global()) self.avCaptureSession.addOutput(videoDataOutput) self.avCaptureSession.startRunning() } /// コンテキストに矩形を描画 private func drawRect(_ rect: CGRect, context: CGContext) { context.setLineWidth(4.0) context.setStrokeColor(UIColor.green.cgColor) context.stroke(rect) } /// 顔認識情報の配列取得 (非同期) private func getFaceObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNFaceObservation])->())) { let request = VNDetectFaceRectanglesRequest { (request, error) in guard let results = request.results as? [VNFaceObservation] else { completion([]) return } completion(results) } let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) try? handler.perform([request]) } /// 正規化された矩形位置を指定領域に展開 private func getUnfoldRect(normalizedRect: CGRect, targetSize: CGSize) -> CGRect { return CGRect( x: normalizedRect.minX * targetSize.width, y: normalizedRect.minY * targetSize.height, width: normalizedRect.width * targetSize.width, height: normalizedRect.height * targetSize.height ) } /// 顔検出位置に矩形を描画した image を取得 private func getFaceRectsImage(sampleBuffer :CMSampleBuffer, faceObservations: [VNFaceObservation]) -> UIImage? { guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil } CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) guard let pixelBufferBaseAddres = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else { CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) return nil } let width = CVPixelBufferGetWidth(imageBuffer) let height = CVPixelBufferGetHeight(imageBuffer) let bitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) ) guard let newContext = CGContext( data: pixelBufferBaseAddres, width: width, height: height, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo.rawValue ) else { CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) return nil } let imageSize = CGSize(width: width, height: height) let faseRects = faceObservations.compactMap { getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize) } faseRects.forEach{ self.drawRect($0, context: newContext) } CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) guard let imageRef = newContext.makeImage() else { return nil } let image = UIImage(cgImage: imageRef, scale: 1.0, orientation: UIImage.Orientation.right) return image } } extension FaceViewController : AVCaptureVideoDataOutputSampleBufferDelegate{ /// カメラからの映像取得デリゲート func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } getFaceObservations(pixelBuffer: pixelBuffer) { [weak self] faceObservations in guard let self = self else { return } let image = self.getFaceRectsImage(sampleBuffer: sampleBuffer, faceObservations: faceObservations) DispatchQueue.main.async { [weak self] in self?.previewImageView.image = image } } } }github
becky3/face_detection: 【Swift】Vision.frameworkでカメラ画像の顔認識を行う【iOS】
https://github.com/becky3/face_detection参考サイト
[iOS 11] 画像解析フレームワークVisionで顔認識を試した結果
https://dev.classmethod.jp/smartphone/iphone/ios-11-vision/[iOS]リアルタイムで画像処理をする時の カメラの内部パラメーターの取得方法 - Qiita
https://qiita.com/shirahama_x/items/421d0d343d9629e66794
- 投稿日:2019-12-21T16:08:08+09:00
端末の画面サイズごとにレイアウトを変更できる"Vary for Traits"の使い方
はじめに
いきなりですが、XcodeのVary for Traits という機能をご存知ですか?
InterfaceBuilderの下のバーにある、グレーのボタンで使うことができます。
一体どういうことができる機能かというと、コードの記述なしに端末の画面サイズや回転状況に応じて、AutoLayoutの制約を付け替えたり、制約の数値を変更、色の変更などのレイアウト周りの設定の変更を行うことができます。
それによってiPhoneとiPad間でのレイアウト崩れを防いだり、画面回転時に全く違うレイアウトを表示するといった活用ができます。
今回は、そんなVary for Traits機能を使用する方法を紹介します。
画面サイズの定義を知る
Vary for Traits機能は画面サイズの定義ごとにレイアウト設定を変更するので、使用する前に、まずは画面サイズの定義を知ることが必要不可欠です。
画面サイズの定義は、横幅と縦幅のw(Width)
とh(Height)
、サイズの大小のR(Regular)
とC(Compact)
、の二つの概念の組み合わせでできています。以下の画像はInterfaceBuilderでiPhone 11 Pro Maxの縦向き状態でViewを表示した際のスクリーンショットです。
左下に、View as: iPhone 11 Pro MAX(wC hR)
と書かれています。iPhone 11 Pro Maxを縦向きにしている状態の画面サイズは
wC hR
、つまり横幅が小さく、縦幅が大きい端末と定義されていることがわかります。ちなみに、それぞれの端末がどのように定義されているかは以下の表に起こしたので、参考にしてください
w h iPhone(縦) C R 4s・SE・8・11 Pro(横) C C 8Plus・11・11 Pro MAX(横) R C iPad(縦横両方) R R 縦向きのiPhoneの
wC hR
、横向きのiPhoneがhC
、iPadのwR hR
などはよく使うので覚えておくと良いでしょう。Vary for Traitsを使う
それぞれの端末のサイズ定義がわかったところで実際にVary for Traits機能を使用してみましょう。
準備
例として、まずは画面の中心に横幅300×高さ400の制約を設定したViewを配置します。
その状態で画面を横向きに変更すると、画面自体の高さが400以下になってしまうので、画面から高さ400の制約を設定したViewが見切れて表示されてしまいます。
この状態からVary for Traits機能を使って、画面が横になった場合にレイアウトを変えていきます。
定義したい画面サイズを選択する
早速、Vary for Traitsのボタンをクリックすると、以下の画像の吹き出しが現れます。
WidthとHeightのチェックボックスは、現在表示しているInterfaceBuilderで選択されている画面のサイズ定義で個別に設定したいものにチェックを入れます。現在表示している画面はiPhone 11 Pro Maxの横向きなので、画面サイズの定義は
wR hC
です。両方にチェックを入れると
wR hC
の組み合わせのである、8Plus・11・11 Pro MAXの横向き
で表示した際に、有効になるレイアウトを設定できます。Widthのみにチェックを入れた場合は
wR
が指定されている、横向きの8Plus・11・11 Pro MAXとiPad
で表示した際に、有効になるレイアウトを設定できます。Heightのみにチェックを入れた場合は
hC
が指定されている、横向き状態のiPhone
で表示した際に、有効になるレイアウトを設定できます。今回の場合、iPhoneの横向きで表示した際にViewが画面外にはみ出してしまうので、Heightの方にのみチェックを入れます。
これで
hC
の画面サイズのみで有効になるレイアウトが設定できる状態になりました。特定の画面サイズでレイアウトを設定する
1.制約の数値を変更する
まずは、Viewの高さの制約を変更してみましょう。
高さの制約の設定を確認すると以下のようになっています。この中の
Constant
の左側の+をクリックすると、以下の画像の吹き出しが出ます。
そのままAdd Variation
をクリックします。そうするとConstantの入力欄の下に
hC
の欄が追加されるので、そこに250を設定してみます。そうすると、Viewの高さがちゃんと画面内に収まるようになりました。
hC
での数値を変更したので、もちろん縦向きに変更しても影響はありません。2.制約の有効・無効を設定する
次は横向きの時には、赤いViewの中央揃えではなく、左上に配置するように設定してみましょう。
先ほどのように、
View.Center Y
の制約を選択し、今度はInstalled
の横の+をクリックして、吹き出しのAdd Variation
をクリックします。すると、Installedの下に
hC
用のチェックボックスが追加されるので、チェックを外すと、制約が無効化されます。同じように
View.Center X
の制約もhC
のInstalledのチェックボックスを追加して、チェックを外しておきましょう。すると、以下の画像のような状態になるので、ここから新たに制約を設定していきましょう。
今回は左上に配置するので、赤いViewのTopとLeadingに各10ずつの制約を設定します。
設定した
View.Leading
の制約の詳細を見てみると、Installed
のチェックが外れていて、hC
の方のInstalled
はチェックが入っています。つまり、Vary for Traits機能を使用している時に、新たに設定した制約はVary for Traitsボタンをクリックしたときに表示された吹き出しでチェックを入れた画面サイズでのみ有効になるということです。
(今回は、Heightに選択を入れていたので、hC
時にのみ有効になる制約ということになります。)そして、制約の一覧で現在表示している画面サイズで有効な制約はアイコンが明るく表示され、無効な制約はアイコンが暗く表示されるので、それでチェックすることもできます。
この設定が完了すれば、横向きの画面で以下のような表示になります。
3.Viewの背景色の変更を設定する
次に横向きの時に、赤いViewの背景色を別の色にしてみましょう。
今度はViewを選択して、Bacgkroundの横の+をクリックして
Add Variation
をクリックします。
hC
用の背景色設定が表示されるので、適当な色を設定すると、横向きの時にのみその色になります。
4.特定のViewを消去・配置する
次は、Viewの上のラベルを設定しましょう。
ラベルには、「300(w)×400(h)」と記載されていますが、横向きの時には、Viewのサイズが300(w)×250(h)なので、「300(w)×250(h)」に変更したいところですが、UILabelのTextは、Vary for Traits機能で差し替えることはできません。
なので、今回の場合は「300(w)×400(h)」のラベルを
hC
の時に消去して、「300(w)×250(h)」と記載されたラベルをhC
の時にのみ表示されるようにすれば望んだ通りの表示になりそうです。まずは今までの手順と同じように「300(w)×400(h)」ラベルの詳細から、
hC
のInstalled
のチェックを外します。あとは、新たにラベルを追加します。
制約を追加した時と同じように、新たに追加された「300(w)×250(h)」Labelは
hC
でのみInstalledになります。制約と同じようにViewも
Installed
かどうかをアイコンの明暗で確認できます。5.Vary for Traitsを終了する
Done VryingをクリックするとVary for Traitsを終了することができます。
6.確認する
InterfaceBuilderのOrientationを変えて確認してみましょう。
まとめ
Vary for Traits機能を使うことで、縦画面と横画面それぞれに対応したレイアウトを作成する方法を紹介しました。
自分もこの機能を知るまでは、Viewの比率の制約や、制約のPriorityを細かく設定したり、コードで画面サイズや方向で制約を操作して、複数の画面サイズに対応していましたが、この機能を使うことで、コードを使うことなく、制約もシンプルな形で想定している表示を実現できたので、皆さんも使ってみてはいかがでしょうか?
- 投稿日:2019-12-21T15:56:39+09:00
SwiftUIにおけるForEach内からのBindingオブジェクトの渡し方
SwiftUIにて
List
ビューを作る際に、多くのケースでForEach
が用いられると思います。下のコードでは、
RankingList
というリストビューを持つ画面が、自身のviewModel
のmovies
配列の値を基に、リスト内の各行のビューRankingRow
を生成しています。class RankingViewModel: ObservableObject { @Published var movies: [Movie] ... } struct RankingList: View { @ObservedObject var viewModel: RankingViewModel var body: some View { List { ForEach(viewModel.movies) { movie in RankingRow(movie: movie) } } } }この方法で
RankingRow
に渡しているのはmovies
配列の要素のコピーとなります。ForEach(viewModel.movies) { movie in RankingRow(movie: movie) // Movieオブジェクトのコピーを渡している }
RankingRow
がMovie
型の実体を受け取る設計であれば何ら問題ありませんが、もしRankingRow
がMovie
のBinding
オブジェクトを受け取りたいとなった場合にはどうすればいいでしょうか。
次のように
viewModel.movies
のBinding
オブジェクトをそのままForEach
に渡したいところですが、残念ながらできません。ForEach($viewModel.movies) {...} // NG: Binding<RankingViewModel>を渡そうとしている ForEach(viewModel.$movies) {...} // NG: Published<[Movie]>.Publisherを渡そうとしている
ForEach
の定義を見てみると分かりますが、RandomAccessCollection
に準拠したオブジェクトか、もしくはRange<Int>
型の値しか受け取らないためです。ForEach.swiftstruct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable { // ① init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content) // ② init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content) // ③ init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content) } // ※ 一部抜粋何らかの配列を渡す方法を別途考えてみます。
Swiftの
Array
には、自身の要素のインデックス範囲を返すプロパティindices
があります。@inlinable public var indices: Range<Int> { get }
Range<Int>
は上記③のinit(_, content:)
に渡すことのできる型ですが、Range
自体がRandomAccessCollection
に適合しているため、つまり上記①、②のイニシャライザにも渡すことができます。今回は上記②の
init(_, id:, content:)
に渡すことで当初の問題を解決します。ForEach(viewModel.movies.indices, id: \.self) { index in RankingRow(movie: self.$viewModel.movies[index]) }
ForEach
内で各要素のindex
を受け取ることができるようになった為、viewModel.movies
配列の各要素を子ビューにバインディングして渡せるようになりました。注意点として、忘れずに引数に
id:
を指定して上記②のinit(_, id:, content:)
を確実に呼び出す必要があります。引数
id:
を指定しない場合、上記③のinit(_, content:)
を呼び出してしまうのですが、このイニシャライザは静的なRange
オブジェクト向けとなっており、Range
オブジェクトに変化があったとしてもSwiftUIはその変更をキャッチしてビューの更新を行ってくれません。
id:
を指定せずForEach
にRange<Int>
を渡した場合、初期化後のオブジェクトに変化があると、Xcodeに以下のようなログが出力されます。ForEach<Range<Int>, Int, RankingRow> count (3) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!この警告が表示された際は、今一度
id:
を付け忘れていないか確認してみましょう。
- 投稿日:2019-12-21T14:57:02+09:00
SwiftUIでiOSのAppStoreアプリのUIを表現してみる
Ateam Lifestyle Advent Calendar 2019の22日目は
株式会社エイチームライフスタイルでWebとiOSをメインに開発している @hytkgamiが担当します。SwiftUIでアプリを作ってみようと思い、その制作過程を記事にまとめました。
設計やSwiftUIの実装等、至らない点が多々あるかと思いますが、編集リクエストやコメントをいただけますと幸いです!想定読者
- SwiftUIに興味がある方
- iOSの開発に興味がある方
SwiftUIの
View
について、細かく説明はしていきません。
詳しく知りたい場合はドキュメントやチュートリアルをご覧ください。SwiftUIとは
WWDC 2019で発表された、新しいUI構築のフレームワークです。
iOS、macOS、watchOSなどすべてのAppleプラットフォーム上で動作します。より優れたAppを、より少ないコードで。
(中略)
SwiftUIは宣言型シンタックスを使用しているため、ユーザーインターフェイスの動作をシンプルに記述することができます。たとえば、テキストフィールドからなるアイテムのリストを作成すると書いてから、各フィールドの配置、フォント、色を記述するといった具合です。これにより、コードがかつてないほどシンプルで読みやすくなり、時間の節約と保守作業の負担軽減につながります。SwiftUIは宣言的な構文が特徴的で、Xcode - SwiftUI - Apple Developerでも上記のように紹介されています。
より詳しく知りたい方には以下の記事もおすすめです。主なツール・環境など
開発環境
- macOS Catalina 10.15.1
- Swift version 5.1
- Version 11.3 (11C29)
デザインツール
- Figma
開発開始
今回はiOSのAppStoreアプリを開いてすぐに目に入るTodayタブのコンテンツを実装していきます。
イメージはこんな感じです。便宜的に、各コンポーネントに名前をつけました。ヘッダーの作成
まずは簡単にできそうな、ヘッダー部分から作っていきます。
ヘッダーの要素は、テキストラベルが2つと画像が1つです。
以下のように作ります。struct HeaderView: View { let title: String init(title: String) { self.title = title } var body: some View { VStack { HStack { Text("12月22日 日曜日") .foregroundColor(.gray) .font(.system(size: 14)) .padding(16) Spacer() }.frame(height: 16, alignment: .topLeading) HStack { Text(title) .font(.largeTitle) .fontWeight(.bold) .padding(16) Spacer() Image("avator") .resizable() // 画像のサイズを変更可能にする .aspectRatio(contentMode: .fit) .frame(width: 36, height: 36, alignment: .center) .clipShape(Circle()) // 正円形に切り抜く .padding(.trailing, 16) } } } }プレビューは次のとおりです。
タイトルとアイコンが水平方向に並び、日付がその上に積まれるようなUIになっていたため、
HStack
を2つVStack
に積んでいます。
また、タイトルとアイコンはそれぞれ左端と右端に表示したいためSpacer()
を噛ませてスペースを作っています。
なお、アイコンの画像は http://flat-icon-design.com/ から拝借しました。日付の部分はフォーマットを作ってオブジェクトを渡して…が手間だったので固定値を入れています。
カードの作成
次にカードを作成します。
iOSのAppStoreアプリでは、カードをタップするとフルスクリーンで中身が表示される仕様になっています。
イメージはこちらです↓メインイメージの作成
struct ItemMainView: View { var body: some View { ZStack(alignment: .top) { Image("item_main_image") .resizable() .frame(height: 420) HStack { VStack(alignment: .leading, spacing: 0) { Text("title") .font(.headline) .foregroundColor(.white) .shadow(radius: 4.0) Text("APP OF THE DAY") .font(.largeTitle) .foregroundColor(.white) .shadow(radius: 4.0) } .padding() Spacer() } } } }プレビューは次のとおりです。
画像にテキストが重なる構造のため、
ZStack
を利用しています。
引数にあるalignment
によって、上下左右中央のどこを基準として配置するか指定することができます。
画像は https://picsum.photos/ からダウンロードしたものをImageAssetsに追加して利用しています。インストールバナーの作成
struct AppInstallBanner: View { var body: some View { HStack { Image("icon") .resizable() .frame(width: 48, height: 48) .padding() VStack(alignment: .leading) { Text("WeatherApp") .font(.headline) .lineLimit(1) Text("Deliver the weather forecast") .font(.footnote) .lineLimit(1) } Spacer() VStack(alignment: .center, spacing: 0) { Button(action: { // }) { Text("GET") .bold() .foregroundColor(Color.blue) } .padding(.vertical, 4) .padding(.horizontal, 16) .background(Color.white) .clipShape(Capsule()) Text("In-app purchase") .lineLimit(1) .font(.caption) } .fixedSize() .padding() } .foregroundColor(Color.white) .background(Color("gray3")) } }プレビューは次のとおりです。
HStack
やVStack
を使った構造はだいぶ見慣れてきたかと思います。
ここでのポイントはButton
です。ボタン押下時のアクションと見た目を一度に定義します。
今回インストール機能までは実装しないため、コメントアウトするだけにしています。
アプリアイコンはFigmaで適当に作りました。紹介文の作成
struct ItemIntroduceTextView: View { let description: String init(with description: String) { self.description = description } var body: some View { Text(description) .lineLimit(nil) .fixedSize(horizontal: false, vertical: true) .padding() } }長文が入る箇所なので、
.lineLimit(nil)
としています。
また、.fixedSize(horizontal: false, vertical: true)
で、要素に応じて垂直方向にViewのサイズが変わるようにしています。ここまでのコンポーネントをまとめてカードを作成する
struct ItemDetailView: View { var body: some View { ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .center, spacing: 0) { ItemMainView() AppInstallBanner() Divider() .background(Color.gray) ItemIntroduceTextView() } .background(Color("gray3")) }.edgesIgnoringSafeArea(.all) } }フルスクリーンを表現するために、
.edgesIgnoringSafeArea(.all)
を利用しています。
本来Viewの端はセーフエリアとの境界までとして描画されるのですが、.edgesIgnoringSafeArea(.all)
によってその制約を無視します。
全画面に拡張したカードはスクロール可能なため、ScrollView
で囲います。
クローズボタンはフルスクリーン時にのみ出現するため、後ほど実装します。コレクションの作成
まずはカードを一つ置いてみます。
struct RecommendCollectionView: View { var body: some View { ScrollView(.vertical, showsIndicators: true) { HeaderView(title: "Today") ItemDetailView() .frame( width: 380, height: 400, alignment: .top) .cornerRadius(20) .disabled(true) } } }ポイントは
.disabled(true)
です。ScrollView
の中にScrollView
がある構造なので、
親のスクロールビューを操作したいのに、子のスクロールビューがスクロールされてしまう…といった状況を起こさないようにします。
それっぽい見た目にはなってきましたが、タップしてフルスクリーン表示される挙動がまだ実装できていません。
ここで@State
を使います。
@State
を使ってフルスクリーン表示を実装するstruct RecommendCollectionView: View { @ObservedObject var store = RecommendItemStore() @State private var presentationMode = false let item = RecommendItem(id: 1, appName: "appName", title: "title", caption: "caption", recommendReason: "reason", imageUrl: "https://picsum.photos/id/1000/474/520", description: "description") var body: some View { GeometryReader { geometry in ZStack { ScrollView(.vertical, showsIndicators: true) { if !self.presentationMode { HeaderView(title: "Today") } ItemDetailView(item: item, presentationMode: self.$presentationMode) .frame(width: 340, height: 380, alignment: .top) .cornerRadius(20) .disabled(true) .onTapGesture { self.presentationMode = true }.padding() } if self.presentationMode { ItemDetailView(item: item, presentationMode: self.$presentationMode) .background(Color("gray3")) .edgesIgnoringSafeArea(.all) } } } } } struct ItemDetailView: View { let item: RecommendItem @Binding var presentationMode: Bool var body: some View { ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .center, spacing: 0) { ZStack(alignment: .topTrailing) { ItemMainView(with: item) if presentationMode { Button(action: { self.presentationMode = false }) { Image("ic_close") }.padding() } } AppInstallBanner(with: item) Divider() .background(Color.gray) ItemIntroduceTextView(with: item.description) } .background(Color("gray3")) }.edgesIgnoringSafeArea(.all) } }いくつか新しい要素が出てきます。
GeometryReader
- 親レイアウトのサイズと、親に対して相対的な自身の座標を持つコンテナ
@State
- データバインディングのために用いる修飾子
$
@State
プロパティをBinding
に変換するためにつけるプレフィクス
presentationMode
という変数をステートとして管理し、それをItemDetailView
にバインディング変換して渡します。
RecommendCollectionView
ではpresentationMode
がtrue
の場合にフルスクリーンでアイテムの詳細画面を表示します。本来フルスクリーンのモーダルで表示するべきかと思いますが、SwiftUIのリファレンスを見てもそのようなメソッドは見当たらず、
ZStack
で実現しました。こちらの記事にも記載されていますが、他の手段としてはUIKitとの組み合わせでも実現できるようです。
ItemDetailView
では新たにボタンを追加し、アクションの中でバインドされたpresentationMode
に対して変更を加えています。
こうすることで、presentationMode
への変更がRecommendCollectionView
にも伝わりフルスクリーンを解除することが可能です。仕上げ APIを通してカードを生成する
カードが1つでは寂しいので、複数表示したいところですが
すべて固定値では手間がかかるので、APIからデータを取得し、モデルに変換して表示するようにします。モデルの準備
struct RecommendItem: Identifiable, Codable { let id: Int let appName: String let title: String let caption: String let recommendReason: String let imageUrl: String let description: String }単純な
Codable
オブジェクトです。ForEach
でループさせるためにIdentifiable
にも準拠させます。APIの用意
ちょうどいいダミーデータをjson形式で秒で生成してくれる「faker」を紹介 を参考にして、ローカルにAPIサーバを用意します。
ほとんどコピペですが、画像URLの部分やテキストの長さを調整したかったので、ダミーデータ生成用のプログラムを以下のようにしています。let faker = require("faker") let db = { products: [] } for(let i = 0;i < 20; ++i) { db.products.push({ id: i+1, app_name: faker.lorem.word(), title: faker.lorem.words(), caption: faker.lorem.lines(), recommend_reason: faker.lorem.word(), image_url: `https://picsum.photos/id/${1000 + i}/474/520`, description: faker.lorem.sentences(), }) } console.log(JSON.stringify(db))URLから
Image
を生成できるようにするUIKit向けのライブラリはたくさんありますが、SwiftUIに対応したものはほとんどありませんでした。
自前で書くか、UIKit向けのライブラリを使ってUIImage
を取得したあとにImage(uiImage: UIImage())
のように生成するか迷っていましたが、KingfisherのREADMEにSwiftUIの文字が…!import KingfisherSwiftUI var body: some View { KFImage(URL(string: "https://example.com/image.png")!) }ということで、SwiftPackageManagerを使ってインストールします。
API呼び出し処理の実装
先ほど作成したローカルAPIサーバに向けてリクエストを送る処理を作ります。
RecommendItemStore
クラスを作成します。class RecommendItemStore: ObservableObject { @Published var items: [RecommendItem] = [] init() { self.fetch() } private func fetch() { guard let url = URL(string: "http://localhost:3001/products") else { return } URLSession.shared.dataTask(with: url) { (data, _, err) in if err != nil { print(err.debugDescription) } let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase guard let data = data, let items = try? jsonDecoder.decode([RecommendItem].self, from: data) else { return } DispatchQueue.main.async { self.items = items } }.resume() } }
@Published
をプロパティに付与することで、値の更新時に通知が行われるようになります。
つまり、上記のクラスではfetch()
によってAPIから値がセットされたときに通知が発行されます。APIのデータを受け取って描画する
通知を受け取る側では以下のように実装します。
struct RecommendCollectionView: View { @ObservedObject var store = RecommendItemStore() @State private var presentationMode = false @State private var selection: RecommendItem? // 省略 var body: some View { // 省略 ForEach(self.store.items, id: \.id) { item in ItemDetailView(item: item, presentationMode: self.$presentationMode) .frame(width: 340, height: 380, alignment: .top) .cornerRadius(20) .disabled(true) .onTapGesture { self.selection = item self.presentationMode = true }.padding() } } if self.presentationMode { ItemDetailView(item: item, presentationMode: self.$presentationMode) .background(Color("gray3")) .edgesIgnoringSafeArea(.all) } } } } }
ForEach
を使ってRecommendItemStore
に保持しているアイテムのリストを描画していきます。
フルスクリーン表示に対応するため、現在選択されているアイテムをselection
変数に保持し、それをフルスクリーンに表示する要素として渡します。SwiftUIで実装してみての所感
本当は
TabView
を使って実装していたのですが、フルスクリーンモーダルを実装しようとすると
どうしても最前面にタブバーが表示されてしまいうまく実現できませんでした。
今回は複雑な要件とは言い難いかもしれませんが、そうした要件の実現のため自由にカスタマイズしながら使う場合は、まだUIKitのほうが効率が良いように思えます。(もちろんSwiftUIに対する理解不足も大きいです。)とはいえ、普段のStoryBoardでの開発に比べると圧倒的にスムーズに実装&検証が進みますし、Viewの組み立てもやりやすくなったと思います。
また、イベント処理がしやすくなったことによる恩恵は大きいのではないでしょうか。
今回はまだSwiftUIのほんの一部にしか触れられていませんが、これを機にもっと色んなものを作ってみたいと思っています。終わりに
Ateam Lifestyle Advent Calendar 2019 の23日目は、@masatomasato1224がお送りします。お楽しみに!
“挑戦”を大事にするエイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/
- 投稿日:2019-12-21T13:27:42+09:00
Swift MkMapViewで地図アプリ作成してみた(11)- 国土地理院からロングタップした位置の標高を取得する
記事一覧
Swift MkMapViewで地図アプリ作成してみた(記事一覧)
国土地理院のサーバ
国土地理院に、標高を取得するWebAPI が公開されています。
使用方法は上記のリンクを参照してください。使用例
ブラウザで上記のURLにアクセスしてみてください。
{"elevation":25.3,"hsrc":"5m\uff08\u30ec\u30fc\u30b6\uff09"}
上記の情報が表示されるWebページが表示されます。
elevationが標高ですので、指定した位置は標高25.3mとなります。国土地理院のWebAPIをコールするコードを組んでみる
ViewController.swift:ロングタップ開始時に古いピンを削除する// 国土地理院のURL let baseUrl = "https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?" // ロングタップ地点の標高を取得する let lonUrl = "&lon=" + pointAno.coordinate.longitude.description let latUrl = "&lat=" + pointAno.coordinate.latitude.description // アウトプットの形式 let outtypeUrl = "&outtype=JSON" // URLとクエリを連結 let listUrl = baseUrl + lonUrl + latUrl + outtypeUrl // http:は「Info.plis」に「App Transport Security Settings」を設定しないとエラーになる guard let url2 = URL(string: listUrl) else { return } URLSession.shared.dataTask(with: url2) { (data, response, error) in if error != nil { print(error!.localizedDescription) } guard let data = data else { return } let json = try? JSONDecoder().decode(JsonElevation.self, from: data) if nil != json { // mainスレッドで処理する DispatchQueue.main.async { self.longTapElevation = (json?.elevation)! } }.resume() }参考文書
国土地理院 ヘルプ > 標高を求めるプログラム > サーバサイドで経緯度から標高を求めるプログラム
補足:標高意外にも素晴らしい技術情報があります。
- 投稿日:2019-12-21T13:27:42+09:00
Swift MkMapViewで地図アプリ作成してみた(11)- ロングタップした位置の標高を国土地理院から取得する
記事一覧
Swift MkMapViewで地図アプリ作成してみた(記事一覧)
国土地理院のサーバ
国土地理院に、標高を取得するWebAPI が公開されています。
使用方法は上記のリンクを参照してください。使用例
ブラウザで上記のURLにアクセスしてみてください。
{"elevation":25.3,"hsrc":"5m\uff08\u30ec\u30fc\u30b6\uff09"}
上記の情報が表示されるWebページが表示されます。
elevationが標高ですので、指定した位置は標高25.3mとなります。国土地理院からロングタップした位置の標高を取得する
URLを生成する
ロングタップした位置にピンを立て距離を表示するで設定したピンの標高を取得するURLを生成する。
ViewController.swift:URLを生成する// 国土地理院のURL let baseUrl = "https://cyberjapandata2.gsi.go.jp/general/dem/scripts/getelevation.php?" // ロングタップ地点でクエリを設定する let lonUrl = "&lon=" + pointAno.coordinate.longitude.description let latUrl = "&lat=" + pointAno.coordinate.latitude.description // アウトプット形式をJSONに設定する let outtypeUrl = "&outtype=JSON" // URLとクエリを連結 let listUrl = baseUrl + lonUrl + latUrl + outtypeUrl // URLを生成する guard let url = URL(string: listUrl) else { return }標高を取得する
URLSessionで生成したURLにアクセスして標高を取得する。
ViewController.swift:標高を取得する// URLを生成する guard let url = URL(string: listUrl) else { return } URLSession.shared.dataTask(with: url) { (data, response, error) in if error != nil { print(error!.localizedDescription) } guard let data = data else { return } // JSONを取得する let json = try? JSONDecoder().decode(JsonElevation.self, from: data) if nil != json { // mainスレッドで処理する DispatchQueue.main.async { // JSONから標高を取得する self.longTapElevation = (json?.elevation)! } }.resume() }サブスレッドからメインスレッドに切り替えるためには、DispatchQueue.main.asyncを使用する。
余談:HTTPを使用する場合に設定すること(HTTPSは不要)
HTTP通信が非推奨のためHTTP通信する場合は、Info.plistを設定しないと実行時エラーとなる。
App Transport Security Settingsを追加して、Allow Arbitrary LoadsにYESを設定する。
これでHTTP通信が可能となる。参考文書
- 投稿日:2019-12-21T08:55:26+09:00
Apple Watch で名言表示アプリを作る
この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。
こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
「遊んでいる時にも時々思い出したいな。。」
「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
「エンジニアチームだけずるいな。。」
(※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)そのようなお客様の思いを解決するために、このアプリを作成しました!!
実装
まず、Xcodeで新しいProjectを作り Watch App を選択します。
Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelとButtonのみ追加します。作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。
@IBOutlet weak var maximLabel: WKInterfaceLabel! @IBAction func onTapButton() { }このonTapButtonでボタンが押された際の処理を書いていきます。
まず、classの先頭で以下のコードを追加します。
let maxim_words = [ "プログラミング言語界のノンシリコンシャンプー", "食は質より量", "俺の辞書の中にカロリーはない", "夕方の12時", "もうそろそろグルコサミンの時期かな", "1の3乗は1だから" ]これは名言の中でも、自分が選び抜いた精鋭たちになります。
この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。
let maximum_word = self.maxim_words.randomElement() maximLabel.setText(maximum_word)これで完成です!!!!!!!!!
View
最後に
これで仕事モードでないOFFの時も名言を思い出すことができます。
Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。
参考記事
- 投稿日:2019-12-21T08:55:26+09:00
10分で作成!名言表示アプリ for Apple Watch
この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。
こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
「遊んでいる時にも時々思い出したいな。。」
「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
「エンジニアチームだけずるいな。。」
(※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)そのようなお客様の思いを解決するために、このアプリを作成しました!!
実装
まず、Xcodeで新しいProjectを作り Watch App を選択します。
Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelとButtonのみ追加します。作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。
@IBOutlet weak var maximLabel: WKInterfaceLabel! @IBAction func onTapButton() { }このonTapButtonでボタンが押された際の処理を書いていきます。
まず、classの先頭で以下のコードを追加します。
let maxim_words = [ "プログラミング言語界のノンシリコンシャンプー", "食は質より量", "俺の辞書の中にカロリーはない", "夕方の12時", "もうそろそろグルコサミンの時期かな", "1の3乗は1だから" ]これは名言の中でも、自分が選び抜いた精鋭たちになります。
この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。
let maximum_word = self.maxim_words.randomElement() maximLabel.setText(maximum_word)これで完成です!!!!!!!!!
View
最後に
これで仕事モードでないOFFの時も名言を思い出すことができます。
Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。
参考記事
- 投稿日:2019-12-21T08:55:26+09:00
[入門] 10分で開発!名言表示アプリ for Apple Watch
この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。
こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
「遊んでいる時にも時々思い出したいな。。」
「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
「エンジニアチームだけずるいな。。」
(※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)そのようなお客様の思いを解決するために、このアプリを作成しました!!
実装
まず、Xcodeで新しいProjectを作り Watch App を選択します。
Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelとButtonのみ追加します。作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。
@IBOutlet weak var maximLabel: WKInterfaceLabel! @IBAction func onTapButton() { }このonTapButtonでボタンが押された際の処理を書いていきます。
まず、classの先頭で以下のコードを追加します。
let maxim_words = [ "プログラミング言語界のノンシリコンシャンプー", "食は質より量", "俺の辞書の中にカロリーはない", "夕方の12時", "もうそろそろグルコサミンの時期かな", "1の3乗は1だから" ]これは名言の中でも、自分が選び抜いた精鋭たちになります。
この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。
@IBAction func onTapButton() { let maximum_word = self.maxim_words.randomElement() maximLabel.setText(maximum_word) }これで完成です!!!!!!!!!
View
最後に
これで仕事モードでないOFFの時も名言を思い出すことができます。
Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。
参考記事
- 投稿日:2019-12-21T01:00:36+09:00
swiftの勉強をはじめた人へ
はじめに
みなさんdelegateとかのイメージはついてますか?
この記事では、私が勉強を始めて1,2ヶ月目で理解するのに苦労した部分(今回は7選)を中心に、
こんな解説があったらよかったなぁ〜と思っていたシリーズを集めて、
初心者がひっかかりやすいであろうところ、基本事項をQ&A方式でまとめました!
確認の意味でやりたい場合は、Questionだけみて答えを考えてください!
全くわからない人は、読みたいところを読んでみてください!☺️①letとvarの違い
まずは、一番初めの宣言の部分から!簡単な人には簡単です!
(ちなみに私は始めた頃さっぱりわかってませんでした、、?)Question let と var の違いは?Answer
letは定数、varは変数です。
varietyのvarですね!varは、1度データを入れても、後でデータを書き換えることができます。
一方、letは1度入れたデータを書き換えることができません。ここで多くのみなさんはこう思うはず。
「全てvarで良いのでは、、?」
実際当時のわたしも、こう思っていました!笑確かに、できないことはないのですが、もしletで良いところをvarで書いてしまうと、
変更するつもりのない変数を間違えて、変更してしまった時に気づかず、余計なエラーを自ら引き起こしてしまう、、という懸念があるので(複雑になるほどそうなるかもです)、しっかり使い分けていきましょう!var number : Int = 0 //最初は0(初期値) number = 2 //上書き保存されます let number : Int = 0 number = 2 //これはアウト letで宣言したので初期値0から書き換え不可能です②オプショナル型
次に、非オプショナル型・オプショナル型・暗黙的オプショナル型についてです。
宣言のところで "!" をよく見かけませんか?
これってなんだろう、、とりあえずよくわからないけど、書いとこう!って人多いと思います!
そこで、以下の例題を踏まえ考えていきましょう!Question それぞれのprint関数は何が出力されるでしょうか? //非オプショナル型 var number0 : Int = 2 print(number0) //オプショナル型 var number1 : Int? number1 = 2 print(number1) print(number1!) //暗黙的オプショナル型 var number2 : Int! number2 = 2 print(number2) print(number1 + number2) print(number1 + number2!)以下、答え合わせです。
Answer //非オプショナル型 var number0 : Int = 2 print(number0) //2↑これは非オプショナル型といって、nilを代入できないデータ型です。
そのため、宣言の時点ですでに初期値が入っているので、print関数にはそのまま初期値の2が出力されます。Answer //オプショナル型 var number1 : Int? number1 = 2 print(number1)//Optional(2) print(number1!) //2オプショナル型はnilを代入できるデータ型です。
宣言した段階では、値は何も入っていないnilの状態です。
普通、nilだとクラッシュしますが、オプショナル型にすることで、(型の最後に!や?をつけることで)、number1という変数は包み紙(ラップ)で覆われている状態となるので、中身がnilでもクラッシュしません。
ここで、もし初期値を代入しないのに、!や?をつけないとnilが宙に浮いている状態となってクラッシュします。次に、出力の部分です。
今回のnumber1は2を途中で代入しています。
しかしそのままprint(number1)と出力すると、先ほど説明した包み紙がついた状態なので、Optional(2)と出力されます。
この包み紙(ラップ)を外して数字の"2"として扱いたい場合は、オプショナル型の変数の後ろに!をつけてあげます。
このことをアンラップ(unwrap)と言います。Answer //暗黙的オプショナル型 var number2 : Int! number2 = 2 print(number2) //2暗黙的に宣言する(宣言の時に型の最後に"!"をつける)と、
その変数を次使う時に、包み紙(ラップ)が外れている状態になるので、先ほどのようにアンラップ(number2!とすること)する必要がありません。以上のことを踏まえて、最後の足し算の部分をみてみます。
答えの1つ目は違う型同士の足し算になってしまうのでエラーとなります。
そのため、number1は2つ目のようにアンラップしてから使いましょう!
オプショナル型で宣言したときは、使用時にアンラップをお忘れなく!Answer print(number1 + number2) //Optional(2)+2 print(number1! + number2)//4そもそもなんでOptional型いるの??について
例えばSNSでは、最初にユーザー登録をしますよね!
Facebookなどでは、名前は初めから必要ですが、プロフィール画像は初めから追加しなくてもアプリとして使えるはずです。
つまり、プロフィール画像はユーザーが写真を追加するまで、値が代入されない"nil"の状態を維持します。
このように全てのものに初期値があるわけではないのでOptional型が必要ってわけです!③引数
引数って言葉聞いたことありますか?
"いんすう"ではなく、"ひきすう"って読みます!Question ()は何を表してますか? //パターン1 func sample(){ //ここに何か処理をかく }Answer
()は、引数(ひきすう)です!
引数とは、関数やプログラムなどに渡す値のこと。処理が行われるためのトリガーのような存在です。例えば、2進数⇄10進数に変換してくれる機械があったとします。
その機械に"1010"(2進数)と入力すると、"10"(10進数)と変換されます。
この場合、最初に入れた"1010"が引数、処理後に出てきた"10"が戻り値となります。パターン1の場合、
()←これは、引数がnil(空)であるという意味です。
つまり、この関数が発動して、中に書いた処理が読み込まれるには、この関数がどこかで呼ばれる必要があります。
(トリガーとしての役割を担う引数が存在しないケースですからね!)
例えば、画面を開いた瞬間に、この関数の中身の処理が行われて欲しい場合は、viewDidLoadの中などに書いてあげましょう!以下のように書きます!override func viewDidLoad() { super.viewDidLoad() sample() }こんな感じです。
もう一つみてみましょう!
//パターン2 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 }パターン2に書いたtableViewのカッコ内
(_ tableView: UITableView, numberOfRowsInSection section: Int)
この部分には色々と書かれていますね!これも引数です!
これは元から引数が存在するので、先ほどのパターン1のように、この関数をどこかに書く必要はありません!きちんと読み込まれます!④delegate
実際に、tableViewの画面遷移を扱うあたりから登場しますよね!
最初は、とりあえず書いとけ!みたいなノリで書いてるかもしれませんが、他のところでも色々お世話になると思うので理解しておきましょう!
tableViewを例にdelegateについて考えてみます。Question ViewController の上に TableView を配置した場合delegateはどうやってかくでしょうか? //delegate周りで押さえておきたい3つのポイントを意識しよう!Answer //TableViewの場合 class ViewController: UIViewController,UITableViewDelegate { //①ここにUITableViewDelegate(protocol)をかく @IBOutlet var TableView : UITableView! override func viewDidLoad() { super.viewDidLoad() TableView.delegate = self //②上で追加したプロトコルの実装をお任せ } //③delegateを元にした実装 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // セルがタップされたら行われる処理 }詳しく説明していきます。
まず、delegateを実装するには、3つの要素が必要になります。
1.protocol
2.処理を依頼するクラス(この場合はTableView)
3.依頼された処理を実行するクラス(この場合はViewController)ViewControllerの上にtableViewを配置している構造を、わかりやすく、例えて
1階がViewController、2階がtableViewのマンションがあるとします。
この1,2階は繋がっていません。(という設定)
delegateが書かれていないと、2階にあるものを1階に届けたいけどどうしよう繋がってないし、、っていう状況です。この状況打破のため、2階のtableViewが実はわたしハシゴ持ってるよ!っていう手段を提示します。
これがprotocolです。(上でいう①)次に、tableView自身が
「ハシゴは持ってるけど、設置はできないから、1階のViewControllerがやってね!実装は任せたよ!」
っていうのがprotocolの実装を任せている上の②の部分です。③はハシゴが設置された結果できるようになったこと。つまり今回は、tableViewがタップされると、その下のViewcontrollerが"タップされたこと"を認識できて、タップされたらどうしたいかの処理を(コーディングしたものを)ViewControllerがやってくれることになります。
それではtextfieldで練習です!
Question //ViewControllerの上にtextfieldを配置した場合 textfield.delegate = self これはなんのために書いてるのでしょうか?Answer
これも先ほどの説明同様に、textfieldのprotocolをViewControllerに任せているって意味です!皆さんはtextfield(例えばSNSのログインでユーザーIDとかパスワードとか)で文字を打ち込んだ後、enterを押してキーボードを閉じていませんか?これができているのはこのdelegateのおかげなのです!
textfieldだけではキーボードを閉じることができないので、delegateを使ってViewControllerにその処理をお任せして閉じてもらっています!1.protocol
→UITextFieldDelegate
2.処理を依頼するクラス(この場合はTextField)
→enter押されたら、押されたタイミングをViewControllerに知らせてキーボード閉じる処理を依頼
3.依頼された処理を実行するクラス(この場合はViewController)
→ViewControllerが2の依頼を実行(キーボードを閉じる)delegate周りの3点は覚えておきましょう!
⑤append
英語のappendって意味知ってますか?
appendはaddの派生語ですね!つまり"追加"って感じです。
以下で、配列にappendするコードをみていきます。Question var memoArray = ["りんご","ゴール","ルール"] @IBAction func sample(){ memoArray.append("日本") print(memoArray)//ここで出力されるものは? }Answer
["りんご","ゴール","ルール","日本"]
となります。このように普通にappend(追加)した場合は1番最後に追加されます!
では、複数追加したい場合や、元の配列の途中に追加したい場合はどうなるでしょうか、、?Question var memoArray = ["りんご","ゴール","ルール"] @IBAction func sample(){ //①"ルビー"と"ビー玉"を配列の最後に追加するコードを書いてください→["りんご","ゴール","ルール","ルビー","ビー玉"]となればおけ //②"うり"をmemoArrayの最初に追加してください→["うり","りんご","ゴール","ルール"]となればおけ }Answer
①memoArray.append(contentsOf: ["ルビー","ビー玉"])
②memoArray.insert("うり", at: 0)それぞれ上記のようにかけば対応できます!
②で、配列の一番初めは0番目になるので、1にしないようにそこだけ注意しましょう!追加されるもの.append(追加したいもの)
となります!⑥protocol
先ほど④のところで少しだけ登場したprotocolです!
④では、UITableViewDelegateなど1ワードで登場しましたが、今回は少し長いprotocolの登場です。TimelineTableViewCell.swiftQuestion Twitterのいいね機能(セルの上のボタンをタップ) はどのような仕組みになっているでしょうか? ViewController に TimelineTableViewCell のカスタムセルを配置した場合を考えます! protocol TimelineTableViewCellDelegate{ func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton) } class TimelineTableViewCell: UITableViewCell { var delegate: TimelineTableViewCellDelegate? @IBAction func Button(button: UIButton){ self.delegate?.didTapButton(timelineTableviewCell: self ,button: button) } }ViewController.swiftclass ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TimelineTableViewCellDelegate { func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton) { //ここにボタンをタップした後の処理 } }Answer
protocolは④のdelegateで書いた通り、delegate処理に必要な要素の1つです。
今回は④とは違い、デフォルトに存在しないものを使いたいので、protocol宣言をします。TimelineTableViewCell.swift//protocol宣言で、これが使えますっていう手の内を公開 protocol TimelineTableViewCellDelegate{ func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton) }delegate周りの時に登場する3つの役割は、今回の場合こんな感じです!
1.protocol
→TimelineTableViewCellDelegate(宣言して自分で置いたもの)
2.処理を依頼するクラス(この場合はTimelineTableViewCell)
→ボタン押されたら、押されたタイミングをViewControllerに知らせる
3.依頼された処理を実行するクラス(この場合はViewController)
→2の依頼を受けて、ボタンが押された後の処理を実行⑦for文とwhile文
for文とwhile文は繰り返しのループ文ですよね!
それぞれ使い分けしていますか?
一度わかれば簡単です見ていきましょう!Question それぞれprint関数には何が出力されるでしょうか? for i in 0...9{ number = Int(arc4random_uniform(20)) print(number) } while number < 10{ number = Int(arc4random_uniform(20)) print(number) }Answer
1つ目は、乱数発生を10回繰り返すので、0~19までの20個の数字で発生した乱数が10個出力されます。
2つ目は、0~19までの中で乱数が発生して、その発生した数字が10未満だったら、while文が繰り返されますが、10以上の乱数が表示された場合はそこで繰り返し文の読み込みは終了します。なので、出力される個数は一定ではありません。回数が決まってる時はfor文を使います。
今回は10回、回したいっていう繰り返し文なので
for i in 0...9
としました!一方、ある条件に達するまで繰り返したい場合はwhile文を使います。
今回の条件は number < 10 ということ。
numberの値が0~9の場合は永遠にwhile文が繰り返されるという条件が前提です。ちなみにfor文は配列と組み合わせるのに最適です!
let memoArray = ["りんご","ゴール","ルール"] for i in memoArray{ }このように書くと、宣言した配列memoArrayのそれぞれの単語にアクセスすることが可能になります!
また、配列の何番がなんの言葉か、というindexも取得できます
let memoArray = ["りんご","ゴール","ルール"] for (index, memo) in memoArray.enumerated() { print(index, memo) // 0 りんご // 1 ゴール // 2 ルール }以上のように、print関数に出力されるので、indexを取得すれば、配列の何番目以上はこうしたい、みたいな条件分岐などをすることができます!
- 投稿日:2019-12-21T01:00:36+09:00
swift はじめの一歩
はじめに
みなさんdelegateとかのイメージはついてますか?
この記事では、私が勉強を始めて1,2ヶ月目で理解するのに苦労した部分(今回は7選)を中心に、
こんな解説があったらよかったなぁ〜と思っていたシリーズを集めて、
初心者がひっかかりやすいであろうところ、基本事項をQ&A方式でまとめました!
確認の意味でやりたい場合は、Questionだけみて答えを考えてください!
全くわからない人は、読みたいところを読んでみてください!☺️①letとvarの違い
まずは、一番初めの宣言の部分から!簡単な人には簡単です!
(ちなみに私は始めた頃さっぱりわかってませんでした、、?)Question let と var の違いは?Answer
letは定数、varは変数です。
variableのvarですね!varは、1度データを入れても、後でデータを書き換えることができます。
一方、letは1度入れたデータを書き換えることができません。ここで多くのみなさんはこう思うはず。
「全てvarで良いのでは、、?」
実際当時のわたしも、こう思っていました!笑確かに、できないことはないのですが、もしletで良いところをvarで書いてしまうと、
変更するつもりのない変数を間違えて、変更してしまった時に気づかず、余計なエラーを自ら引き起こしてしまう、、という懸念があるので(複雑になるほどそうなるかもです)、しっかり使い分けていきましょう!var number : Int = 0 //最初は0(初期値) number = 2 //上書き保存されます let number : Int = 0 number = 2 //これはアウト letで宣言したので初期値0から書き換え不可能です②オプショナル型
次に、非オプショナル型・オプショナル型・暗黙的オプショナル型についてです。
宣言のところで "!" をよく見かけませんか?
これってなんだろう、、とりあえずよくわからないけど、書いとこう!って人多いと思います!
そこで、以下の例題を踏まえ考えていきましょう!Question それぞれのprint関数は何が出力されるでしょうか? //非オプショナル型 var number0 : Int = 2 print(number0) //オプショナル型 var number1 : Int? number1 = 2 print(number1) print(number1!) //暗黙的オプショナル型 var number2 : Int! number2 = 2 print(number2) print(number1 + number2) print(number1 + number2!)以下、答え合わせです。
Answer //非オプショナル型 var number0 : Int = 2 print(number0) //2↑これは非オプショナル型といって、nilを代入できないデータ型です。
そのため、宣言の時点ですでに初期値が入っているので、print関数にはそのまま初期値の2が出力されます。Answer //オプショナル型 var number1 : Int? number1 = 2 print(number1)//Optional(2) print(number1!) //2オプショナル型はnilを代入できるデータ型です。
宣言した段階では、値は何も入っていないnilの状態です。
普通、nilだとクラッシュしますが、オプショナル型にすることで、(型の最後に!や?をつけることで)、number1という変数は包み紙(ラップ)で覆われている状態となるので、中身がnilでもクラッシュしません。
ここで、もし初期値を代入しないのに、!や?をつけないとnilが宙に浮いている状態となってクラッシュします。次に、出力の部分です。
今回のnumber1は2を途中で代入しています。
しかしそのままprint(number1)と出力すると、先ほど説明した包み紙がついた状態なので、Optional(2)と出力されます。
この包み紙(ラップ)を外して数字の"2"として扱いたい場合は、オプショナル型の変数の後ろに!をつけてあげます。
このことをアンラップ(unwrap)と言います。Answer //暗黙的オプショナル型 var number2 : Int! number2 = 2 print(number2) //2暗黙的に宣言する(宣言の時に型の最後に"!"をつける)と、
その変数を次使う時に、包み紙(ラップ)が外れている状態になるので、先ほどのようにアンラップ(number2!とすること)する必要がありません。以上のことを踏まえて、最後の足し算の部分をみてみます。
答えの1つ目は違う型同士の足し算になってしまうのでエラーとなります。
そのため、number1は2つ目のようにアンラップしてから使いましょう!
オプショナル型で宣言したときは、使用時にアンラップをお忘れなく!Answer print(number1 + number2) //Optional(2)+2 print(number1! + number2)//4そもそもなんでOptional型いるの??について
例えばSNSでは、最初にユーザー登録をしますよね!
Facebookなどでは、名前は初めから必要ですが、プロフィール画像は初めから追加しなくてもアプリとして使えるはずです。
つまり、プロフィール画像はユーザーが写真を追加するまで、値が代入されない"nil"の状態を維持します。
このように全てのものに初期値があるわけではないのでOptional型が必要ってわけです!③引数
引数って言葉聞いたことありますか?
"いんすう"ではなく、"ひきすう"って読みます!Question ()は何を表してますか? //パターン1 func sample(){ //ここに何か処理をかく }Answer
()は、引数(ひきすう)です!
引数とは、関数やプログラムなどに渡す値のこと。処理が行われるためのトリガーのような存在です。例えば、2進数⇄10進数に変換してくれる機械があったとします。
その機械に"1010"(2進数)と入力すると、"10"(10進数)と変換されます。
この場合、最初に入れた"1010"が引数、処理後に出てきた"10"が戻り値となります。パターン1の場合、
()←これは、引数がnil(空)であるという意味です。
つまり、この関数が発動して、中に書いた処理が読み込まれるには、この関数がどこかで呼ばれる必要があります。
(トリガーとしての役割を担う引数が存在しないケースですからね!)
例えば、画面を開いた瞬間に、この関数の中身の処理が行われて欲しい場合は、viewDidLoadの中などに書いてあげましょう!以下のように書きます!override func viewDidLoad() { super.viewDidLoad() sample() }こんな感じです。
もう一つみてみましょう!
//パターン2 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 }パターン2に書いたtableViewのカッコ内
(_ tableView: UITableView, numberOfRowsInSection section: Int)
この部分には色々と書かれていますね!これも引数です!
これは元から引数が存在するので、先ほどのパターン1のように、この関数をどこかに書く必要はありません!きちんと読み込まれます!④delegate
実際に、tableViewの画面遷移を扱うあたりから登場しますよね!
最初は、とりあえず書いとけ!みたいなノリで書いてるかもしれませんが、他のところでも色々お世話になると思うので理解しておきましょう!
tableViewを例にdelegateについて考えてみます。Question ViewController の上に TableView を配置した場合delegateはどうやってかくでしょうか? //delegate周りで押さえておきたい3つのポイントを意識しよう!Answer //TableViewの場合 class ViewController: UIViewController,UITableViewDelegate { //①ここにUITableViewDelegate(protocol)をかく @IBOutlet var TableView : UITableView! override func viewDidLoad() { super.viewDidLoad() TableView.delegate = self //②上で追加したプロトコルの実装をお任せ } //③delegateを元にした実装 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // セルがタップされたら行われる処理 }詳しく説明していきます。
まず、delegateを実装するには、3つの要素が必要になります。
1.protocol
2.処理を依頼するクラス(この場合はTableView)
3.依頼された処理を実行するクラス(この場合はViewController)ViewControllerの上にtableViewを配置している構造を、わかりやすく、例えて
1階がViewController、2階がtableViewのマンションがあるとします。
この1,2階は繋がっていません。(という設定)
delegateが書かれていないと、2階にあるものを1階に届けたいけどどうしよう繋がってないし、、っていう状況です。この状況打破のため、2階のtableViewが実はわたしハシゴ持ってるよ!っていう手段を提示します。
これがprotocolです。(上でいう①)次に、tableView自身が
「ハシゴは持ってるけど、設置はできないから、1階のViewControllerがやってね!実装は任せたよ!」
っていうのがprotocolの実装を任せている上の②の部分です。③はハシゴが設置された結果できるようになったこと。つまり今回は、tableViewがタップされると、その下のViewcontrollerが"タップされたこと"を認識できて、タップされたらどうしたいかの処理を(コーディングしたものを)ViewControllerがやってくれることになります。
それではtextfieldで練習です!
Question //ViewControllerの上にtextfieldを配置した場合 textfield.delegate = self これはなんのために書いてるのでしょうか?Answer
これも先ほどの説明同様に、textfieldのprotocolをViewControllerに任せているって意味です!皆さんはtextfield(例えばSNSのログインでユーザーIDとかパスワードとか)で文字を打ち込んだ後、enterを押してキーボードを閉じていませんか?これができているのはこのdelegateのおかげなのです!
textfieldだけではキーボードを閉じることができないので、delegateを使ってViewControllerにその処理をお任せして閉じてもらっています!1.protocol
→UITextFieldDelegate
2.処理を依頼するクラス(この場合はTextField)
→enter押されたら、押されたタイミングをViewControllerに知らせてキーボード閉じる処理を依頼
3.依頼された処理を実行するクラス(この場合はViewController)
→ViewControllerが2の依頼を実行(キーボードを閉じる)delegate周りの3点は覚えておきましょう!
⑤append
英語のappendって意味知ってますか?
appendはaddの派生語ですね!つまり"追加"って感じです。
以下で、配列にappendするコードをみていきます。Question var memoArray = ["りんご","ゴール","ルール"] @IBAction func sample(){ memoArray.append("日本") print(memoArray)//ここで出力されるものは? }Answer
["りんご","ゴール","ルール","日本"]
となります。このように普通にappend(追加)した場合は1番最後に追加されます!
では、複数追加したい場合や、元の配列の途中に追加したい場合はどうなるでしょうか、、?Question var memoArray = ["りんご","ゴール","ルール"] @IBAction func sample(){ //①"ルビー"と"ビー玉"を配列の最後に追加するコードを書いてください→["りんご","ゴール","ルール","ルビー","ビー玉"]となればおけ //②"うり"をmemoArrayの最初に追加してください→["うり","りんご","ゴール","ルール"]となればおけ }Answer
①memoArray.append(contentsOf: ["ルビー","ビー玉"])
②memoArray.insert("うり", at: 0)それぞれ上記のようにかけば対応できます!
②で、配列の一番初めは0番目になるので、1にしないようにそこだけ注意しましょう!追加されるもの.append(追加したいもの)
となります!⑥protocol
先ほど④のところで少しだけ登場したprotocolです!
④では、UITableViewDelegateなど1ワードで登場しましたが、今回は少し長いprotocolの登場です。TimelineTableViewCell.swiftQuestion Twitterのいいね機能(セルの上のボタンをタップ) はどのような仕組みになっているでしょうか? ViewController に TimelineTableViewCell のカスタムセルを配置した場合を考えます! protocol TimelineTableViewCellDelegate{ func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton) } class TimelineTableViewCell: UITableViewCell { var delegate: TimelineTableViewCellDelegate? @IBAction func Button(button: UIButton){ self.delegate?.didTapButton(timelineTableviewCell: self ,button: button) } }ViewController.swiftclass ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TimelineTableViewCellDelegate { func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton) { //ここにボタンをタップした後の処理 } }Answer
protocolは④のdelegateで書いた通り、delegate処理に必要な要素の1つです。
今回は④とは違い、デフォルトに存在しないものを使いたいので、protocol宣言をします。TimelineTableViewCell.swift//protocol宣言で、これが使えますっていう手の内を公開 protocol TimelineTableViewCellDelegate{ func didTapButton(timelineTableviewCell: UITableViewCell, button: UIButton) }delegate周りの時に登場する3つの役割は、今回の場合こんな感じです!
1.protocol
→TimelineTableViewCellDelegate(宣言して自分で置いたもの)
2.処理を依頼するクラス(この場合はTimelineTableViewCell)
→ボタン押されたら、押されたタイミングをViewControllerに知らせる
3.依頼された処理を実行するクラス(この場合はViewController)
→2の依頼を受けて、ボタンが押された後の処理を実行⑦for文とwhile文
for文とwhile文は繰り返しのループ文ですよね!
それぞれ使い分けしていますか?
一度わかれば簡単です見ていきましょう!Question それぞれprint関数には何が出力されるでしょうか? for i in 0...9{ number = Int(arc4random_uniform(20)) print(number) } while number < 10{ number = Int(arc4random_uniform(20)) print(number) }Answer
1つ目は、乱数発生を10回繰り返すので、0~19までの20個の数字で発生した乱数が10個出力されます。
2つ目は、0~19までの中で乱数が発生して、その発生した数字が10未満だったら、while文が繰り返されますが、10以上の乱数が表示された場合はそこで繰り返し文の読み込みは終了します。なので、出力される個数は一定ではありません。回数が決まってる時はfor文を使います。
今回は10回、回したいっていう繰り返し文なので
for i in 0...9
としました!一方、ある条件に達するまで繰り返したい場合はwhile文を使います。
今回の条件は number < 10 ということ。
numberの値が0~9の場合は永遠にwhile文が繰り返されるという条件が前提です。ちなみにfor文は配列と組み合わせるのに最適です!
let memoArray = ["りんご","ゴール","ルール"] for i in memoArray{ }このように書くと、宣言した配列memoArrayのそれぞれの単語にアクセスすることが可能になります!
また、配列の何番がなんの言葉か、というindexも取得できます
let memoArray = ["りんご","ゴール","ルール"] for (index, memo) in memoArray.enumerated() { print(index, memo) // 0 りんご // 1 ゴール // 2 ルール }以上のように、print関数に出力されるので、indexを取得すれば、配列の何番目以上はこうしたい、みたいな条件分岐などをすることができます!