20210511のiOSに関する記事は8件です。

【連載】初心者のためのMacCatalyst講座

この講座について この講座では「MacCatalystって何?」「MacCatalystを使ってみたい」という方向けに解説を 行っていきます。この記事を見てくれた方がMacCatalystを説明できる、MacCatalystが使える、 といったことを目標に解説していきます。 目次 第1回 iOSアプリ開発を考えているなら知っておくべきMacCatalystの基礎知識 第2回 作成中...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発を考えているなら知っておくべきMacCatalystの基礎知識

MacCatalystとは 「MacCatalyst」とは、Appleの統合開発環境である「Xcode」の機能の一つです。 かつては「Project Marzipan」と呼ばれていましたが2019年に「Project Catalyst」として発表され同年リリースされたXcode11から「MacCatalyst」を使用した開発が可能になりました。 iPadアプリからMacアプリを作成が可能 アプリのiPad版さえあれば簡単にMac版を作ることができます。 細かく言うと、iPadアプリのソースコードを使って、Mac版のアプリをビルドすることができます。 1つのソースコードから2つのアプリができるので、開発コストは半分になります。 (そんな単純な訳ないですが…) デバイスに合わせた機能追加も可能 また、Mac版かどうかで処理を分岐させることも可能で、Mac版のデザインや機能をiOS版と分ける事もできます。 例えば、Mac版は画面の広さを生かしてボタンを増やしたり、カメラアプリなら編集機能の追加などができます。 以上がMacCatalystの概要でした。 次回はMacCatalystを使うメリットを紹介します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリ開発の勉強記録1

スタンフォード大学の講義 SwiftUIによるアプリ開発 ある程度何らかのプログラミング言語を知っている人向け 1〜2時間の講義が14回(上がっている動画をカウントした) 音声:英語、字幕:英語 Lecture 1: Course Logistics and Introduction to SwiftUI 内容 やる:Swift言語、関数型プログラミング、リアクティブUI開発(MVVM含) やらない:UIKit、テスト手法 Xcodeインストールし、Hello Worldをそのままビルド、シミュレーターで表示 出てくるキーワードの説明は少なめ これから作る神経衰弱的なカードゲームのデモ Hello Worldを20行ほど修正追加して、カード的な表示を作成 ZStack, HStack, RoundedRectangle, ForEach 感想気付き 引数用の丸括弧など、いろいろと省略できる。確かに見やすいが省略条件がまだよくわからない。 return ZStack(content: { RoundedRectangle(cornerRadius: 10.0) Text("👻") }) ZStack { RoundedRectangle(cornerRadius: 10.0) Text("👻") } ドットで繋げて処理を書くことができる。それぞれ異なる処理。まるでパイプのよう。 RoundedRectangle(cornerRadius: 10.0).stroke().foregroundColor(Color.orange) 0から4未満の範囲の書き方 0..<4。4回を明示するためだと思われ。 クラスのインスタンス生成時にメンバ初期値を引数で渡すことができる。事前のインターフェース宣言は必要ない。 CodeView(isFaceUp: true) struct CodeView: View { var isFaceUp: Bool } 今後の課題 someの意味 struct, class の違い ライブラリやAPIの知識を増やす(CSS文法などに寄せている感があるので覚えやすそう)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Guideline 2.1 - Information Needed】IDFA関連のApple審査リジェクト対処法

アップデート時にAppleの審査で、「Guideline 2.1 - Information Needed」によってリジェクトされました。 「情報が足りない」という内容で、今回はiOS14.5で必須となっているIDFA取得に関連していました。 同様のリジェクトを受ける可能性がある方もいると思うので、記録しておきます。 Guideline 2.1 - Information Needed 「Information Needed」についたコメントは以下の通りです。 We're looking forward to completing the review of your app, but we need more information to continue. Specifically, we noticed that your app uses the AppTrackingTransparency framework, but we haven't been able to locate the App Tracking Transparency permission request. 要約すると、 「AppTrackingTransparencyフレームワークを使用しているのは分かるけど、どこで使っているのか分からない」 と言っているようです。 IDFAの取得にはiOS14のAppTrackingTransparencyフレームワークが必須となっているため、前回のアップデートで既に導入済みでした。 前回は一発で審査を通っていたのに、なぜか今回はリジェクトされることに……。 審査担当者によってもジャッジが微妙に異なるのかもしれませんね。 IDFA取得のダイヤログ起動タイミング IDFA取得の許可を求めるダイヤログは、初回起動時にのみ表示するようにしています。 方法は色々あると思いますが、私の場合は初回起動かどうかを変数のBool型の値でジャッジしていました。 それが分かりにくいと判断されたようです。 【解決方法】どのように実装したのかメモ欄に記載 実装方法に間違いがなければ、具体的な実装方法を返信で説明すればOKです。 そもそもこうしたリジェクトを回避するために、申請時に「App Reviewに関する情報」のメモ欄で説明しておくことをおすすめします。 メモ欄に具体的な実装方法を記述した上で、実際のコードのスクショをファイルで添付すればカンペキです。 自分も含めて英語が苦手な人にはちょっと手間ですが、Google翻訳を使って作成した文章でも十分伝わると思います。 気になる人は冒頭に 「I want to apologize in advance, I am not fluent in English.」 とでも入れておけば、英語が苦手なので意味を上手く伝えられないかもしれない…ということは伝わるかと。 日本からの申請である以上、先方も英語が不得手な人が多いことは理解してくれていると思いますが。 IDFAの許可タイミングは難しいですね。 AdMobを使用していますが、広告主の多くが離れるとか、ユーザーの許諾率がめちゃくちゃ低いとかネガティブな話題ばかりなので、Apple様が何らかの緩和策を講じてくれることを期待します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】RxSwift勉強してみたPart9

はじめに 前回 今回はOperatorをいくつか練習していきます Operators Filtering Operators ignoreElements onNextを無視する let publishSubject = PublishSubject<String>() let disposeBag = DisposeBag() publishSubject .ignoreElements() .subscribe { _ in print("subscription is called") } .disposed(by: disposeBag) publishSubject.onNext("1") // 呼ばれない publishSubject.onNext("2") // 呼ばれない publishSubject.onNext("3") // 呼ばれない publishSubject.onCompleted() // "subscription is called" element(at:) 指定した箇所のイベントを購読 let publishSubject = PublishSubject<String>() publishSubject .element(at: 1) .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) publishSubject.onNext("1") publishSubject.onNext("2") // 2 publishSubject.onNext("3") filter 条件に合うものだけを購読 Observable.of("aa", "bbb", "cccc", "ddddd", "eeeeee") .filter { $0.count > 3 } .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) //cccc //ddddd //eeeeee skip 指定した要素の数だけイベントを飛ばす Observable.of(1, 2, 3, 4, 5, 6) .skip(3) .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) // 4 // 5 // 6 skip(while:) 条件に合うイベントまでスキップする Observable.of(1, 1, 1, 2, 2, 2, 3, 3, 3) .skip(while: { $0 != 2 }) // 2じゃない間はとばし、2になったらそれ以降購読 .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) // 2 // 2 // 2 // 3 // 3 // 3 skip(until: ) 基準のイベントが流れてきたらそれ以降を購読 let trigger = PublishSubject<String>() publishSubject.skip(until: trigger) .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) publishSubject.onNext("1") publishSubject.onNext("2") trigger.onNext("X") publishSubject.onNext("3") publishSubject.onNext("4") // 3 // 4 take 引数の数だけ、先頭から購読 Observable.of(1, 2, 3, 4, 5, 6) .take(3) .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) // 1 // 2 // 3 take(while: ) 条件に合う限り購読、一度でも条件を満た差なくなるとそれ以降購読しない Observable.of(1, 2, 3, 4, 5, 6) .take(while: { $0 < 4 } ) .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) // 1 // 2 // 3 take(until: ) 引数に基準となるオブザーバブルを取り、それが購読されるまでのイベントを購読する let trigger = PublishSubject<String>() publishSubject .take(until: trigger) .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) publishSubject.onNext("1") publishSubject.onNext("2") trigger.onNext("X") publishSubject.onNext("3") publishSubject.onNext("4") // 1 // 2 Transforming Operators toArray 配列にする Observable.of(1, 2, 3, 4, 5) .toArray() .subscribe(onSuccess: { print($0) }, onFailure: { print($0) }, onDisposed: { print("onDisposed") } ).disposed(by: disposeBag) // [1, 2, 3, 4, 5] // onDisposed map 全てのイベントを変換 Observable.of(1, 2, 3, 4, 5) .map { $0 * 10 } .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) //10 //20 //30 //40 //50 flatMap map + mergeのようなイメージ struct Student { var score: BehaviorRelay<Int> } let reon = Student(score: BehaviorRelay(value: 10)) let tom = Student(score: BehaviorRelay(value: 20)) let student = PublishSubject<Student>() student.asObservable() .flatMap { $0.score.asObservable() } .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) student.onNext(reon) // 10 reon.score.accept(100) // 100 tom.score.accept(200) // 200 student.onNext(tom) // 呼ばれない flatMapLatest map + switchLatest(次のObservableが来たらそちらを優先する) struct Student { var score: BehaviorRelay<Int> } let reon = Student(score: BehaviorRelay(value: 10)) let tom = Student(score: BehaviorRelay(value: 20)) let student = PublishSubject<Student>() student.asObservable() .flatMapLatest { $0.score.asObservable() } .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) student.onNext(reon) student.onNext(tom) reon.score.accept(100) tom.score.accept(200) Combing Operators startsWith 先頭のイベントを任意のものに変更できる let numbers = Observable.of(2, 3, 4) let observable = numbers.startWith(1) observable .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) // 1 // 2 // 3 // 4 concat Observableを結合できる let first = Observable.of(1, 2, 3) let second = Observable.of(4, 5, 6) let observable = Observable.concat([first, second]) observable .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) // 1 // 2 // 3 // 4 // 5 // 6 merge こちらもObservableを結合できますが、concatとの違いは、時系列順になるということです。 let left = PublishSubject<Int>() let right = PublishSubject<Int>() let source = Observable.of(left.asObservable(), right.asObservable()) let observable = source.merge() observable .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) left.onNext(1) left.onNext(2) right.onNext(100) left.onNext(3) right.onNext(200) right.onNext(300) //1 //2 //100 //3 //200 //300 combineLatest 近い最新の値同士を組み合わせたイベントを作成 let left = PublishSubject<String>() let right = PublishSubject<String>() let observable = Observable.combineLatest(left, right) { lastLeft, lastRight in return lastLeft + lastRight } let disposable = observable.subscribe(onNext: { print($0) } ) left.onNext("A") left.onNext("B") right.onNext("1") left.onNext("C") right.onNext("2") right.onNext("3") left.onNext("D") right.onNext("4") // B1 // C1 // C2 // C3 // D3 // D4 withLatestFrom 任意のイベント発生時に、最新のイベントを発生させる。 今回は、ボタンを押した後に、テキストフィールドの最新のイベント(Swift)を流している。 let button = PublishSubject<Void>() let textField = PublishSubject<String>() let observable = button.withLatestFrom(textField) let disposable = observable.subscribe(onNext: { print($0) }) textField.onNext("S") textField.onNext("Sw") textField.onNext("Swi") textField.onNext("Swif") textField.onNext("Swift") // これがtextFieldの最新の値 button.onNext(()) // ここで初めて呼ばれる button.onNext(()) // ここでも呼ばれる // Swift // Swift reduce イベントを一つにまとめる加工を行う let observable = Observable.of(1, 2, 3) observable.reduce(0, accumulator: +) .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) // 6 scan reduceの時と違い、日々イベントを更新していく let observable = Observable.of(1, 2, 3) observable.scan(0, accumulator: +) .subscribe(onNext: { print($0) }) .disposed(by: disposeBag) // 1 // 3 // 6 おわりに 他にもたくさん種類があるので、気になる方はマーブルダイアグラムを見てみてください! マーブルダイアグラム
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】AlamofireでAPI通信をする

はじめに 今回はQiitaAPIを叩いてAlamofireで通信していみたいと思います。 作るもの GitHub 実装 モデル struct Article: Codable { let title: String let user: User } struct User: Codable { let id: String } コントローラー final class ArticleListViewController: UIViewController { @IBOutlet private weak var tableView: UITableView! private var articles = [Article]() override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self getArticles() } } private extension ArticleListViewController { func getArticles() { APIClient().request { result in switch result { case .success(let articles): self.articles = articles DispatchQueue.main.async { self.tableView.reloadData() } case .failure(let error): self.showAPIAlert(error: error) } } } func showAPIAlert(error: APIError) { let alert = UIAlertController(title: error.title, message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "閉じる", style: .cancel, handler: nil)) self.present(alert, animated: true, completion: nil) } } // MARK: - UITableViewDataSource extension ArticleListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return articles.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = articles[indexPath.row].title return cell } } API import Alamofire typealias ResultHandler<T> = (Result<T, APIError>) -> Void struct APIClient { func request(handler: @escaping ResultHandler<[Article]>) { let urlString = "https://qiita.com/api/v2/items" let url = URL(string: urlString) guard let url = URL(string: urlString) else { handler(.failure(.invalidURL)) return } AF.request(urlString) .responseJSON { response in guard let data = response.data else { handler(.failure(.invalidResponse)) return } do { let articles = try JSONDecoder().decode([Article].self, from: data) handler(.success(articles)) } catch { handler(.failure(.unknown(error))) } } } } その他 enum APIError: Error { case invalidURL case invalidResponse case unknown(Error) } extension APIError { var title: String { switch self { case .invalidResponse: return "無効なレスポンスです。" case .invalidURL: return "無効なURLです。" case .unknown(let error): return "予期せぬエラーが発生しました。\(error)" } } } 解説 大切なところのみ解説します。 本来はurlをそのまま書いておいたりするのは良くないのですが、今回はAamofireの解説ということで、省略します。 ここでリクエストを送信しています。 AF.request(urlString) 以下のようにして、通信を行います。 APIClient().request { result in switch result { case .success(let articles): self.articles = articles DispatchQueue.main.async { self.tableView.reloadData() } case .failure(let error): self.showAPIAlert(error: error) } } URLSessionとの比較 let request = URLRequest(url: url) let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { handler(.failure(.unknown(error))) return } if let data = data { do { let articles = try JSONDecoder().decode([Article].self, from: data) handler(.success(articles)) } catch { handler(.failure(.unknown(error))) } } } task.resume() Alamofireの方が明らかに簡潔にかけていますね。 おわりに 終わりです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

週刊 DICOMデコーダーを創る (1) 創刊号

DICOMデコーダーの作成 医療用画像を扱う上でDICOMは避けては通ることができない形式です. 近年はPythonなど様々な言語で使用可能なDICOMプラグインが登場しているし,そもそもDICOM→扱いやすい形式へ変換可能なソフトウェアはフリーでもたくさんある.なので,今更自力でDICOMを表示することに需要はないかもしれない・・・ DICOM規格は経年的にどんどん膨れ上がっており,その全体を理解することや,すべてを実装することは個人の力では到底できない.(少なくとも自分には)(なのでライブラリを使え,と言う話だが・・・) そんな中でも,DICOMを実装したい!!と考えている人がいるはず!と期待し,少しでも誰かの役に立つといいなと思い,DICOMファイルのデコーダーの基礎部分を何回かに分けて実装したいと思います.(最後にいきなりズルしてますが) DICOMデータ DICOMデータは1つのファイルに 患者名,患者ID,生年月日,年齢,性別など患者情報 検査方法(CT, レントゲン, エコー, カテーテル, MRI, ...),検査日,検査時間,使用した機器など検査に関わる情報 画像サイズ,画像Bit数, 動画かどうか,フレームレートなど画像のメタデータ 画像の転送形式(≒圧縮形式) 画像データの集合 など多くの情報が埋め込まれている. DICOM構造の定義上は,あらゆるデータが想定されていて, 軍の階級,照会医師の住所など,そこに埋め込む必要あるのか?というようなデータ項目も定義されている. JIRAにDICOM規格書の日本語訳があり,実装には非常に参考になるので目を通してほしい. PS 3.6がDICOMで実装可能なデータセットの一覧だが,これをすべて実装するのは到底無理. DICOM binary dataの中から,適宜扱う予定のデータに必要な部分だけ取り出して実装していけば良い. DICOM dataの中の,途中の情報はすべてすっ飛ばして,画像形式・大きさ・深度情報だけを読み取って,ファイル最後尾にある,画像情報のbinaryだけを取り出せば画像は表示可能である. 雑で申し訳ない・・・・ しかし,本刊ではせっかくなので,途中に含まれるデータも階層だけは辿ってデコードしていくことにする. 題材 DICOMの画像形式はいくつかあるのだが, 全くの非圧縮 可逆圧縮(Jpeg Lossless) Jpeg圧縮 Jpeg2000圧縮 などがある.この中で,Jpeg2000はwindowsでは標準で表示できないようだが,macは表示可能(iOSは見てないのでわからない). JPEG LosslessはDICOM以外ではほとんど使用されていないと思う. ただし,一部メーカーのデジカメRAW形式はかなり近い. JPEG Losslessのデコードも需要がありそうなら挑戦するが,今回はLossy JPEGと非圧縮を題材にデコードしていくことにする. (Lossy JPEGは言ってしまえば普通のJPEGなので,DICOMファイルから該当部分のbinaryデータだけをとってきて,拡張子を.jpgにすればそれだけで表示できる) 今回は下記サイトからDICOMデータを拝借することにする まずは,リンク先の一番上 DEMO 0002 1702Kb を使用する. (これはカテーテル画像の動画になる予定です) DICOMデコーダー始動!! まずはBinaryを開けてみる. ここではバイナリエディタとして[0xED]を使用した.(WindowsならBzがおすすめ) 上記JIRAにあるDICOM規格書翻訳 PS 3.10 医療におけるデジタル画像と通信 (DICOM)第10部:媒体相互交換のための媒体保存とファイルフォーマットのp20にDICOMファイルの構造は 最初の128byteは基本0x00 続く4byteはプレフィックスでDICMと記載されている. 2.がDICOMファイルかどうかを決めているようです. この0002.DCMはサンプルファイルなので,最初の128byteに色々書いてありますが気にしないでおきましょう. AssetsにDICOMファイルである0002.DCMを登録した. DICOMファイルはバイナリデータで扱っていくので,Dataで読み込んでおく let fileName = "0002" guard let dicomData = NSDataAsset(name: fileName)?.data else { print("file not found") return } dicomDataの128バイトは無視して,続く4byteにDICMと記載されているかを確認する. Data型は単純に取り出してもbyte数を吐いてしまいます. 各byteはUInt8で表現されますが,Data型のmapはデフォルトがUInt8で処理してくれるので, dicomData[128...131].map{$0} とすれば [68, 73, 67, 77] となるはずです. これは,ファイルアドレス 0x80〜0x83にある0x44, 0x49, 0x43, 0x4DがUInt8,つまり符号なし整数(10進法)で表示されたことになります. これをasciiコードから文字列に直します. 68(0x44) = D 73(0x49) = I 67(0x43) = C 77(0x4D) = M と出力したいわけです. print(Unicode.Scalar(68)!) -> D のようにUnicode.Scalarで取り出してあげればいいわけです. この変換を今後,山程使うので, extension Data{ func getStringWithRange(start:Int, length:Int) -> String{ let aa = self[start...start+length-1] return aa.map{String(Unicode.Scalar($0))}.joined() } } として指定した場所と長さを指定すると文字が取り出せるよう,Data型を拡張しておきました.これで print(dicomData.getStringWithRange(start: 128, length: 4)) のようにすれば,DICMと出力されるようになります. // dicomデータかチェック // ヘッダは128バイトのファイルプリアンブル // つづく4バイトにプレフィックスが続く if dicomData.count < 132{ print("file size error") return } if dicomData.getStringWithRange(start: 128, length: 4) != "DICM"{ print("file type error") return } DICOMデータの基本構造 DICOMファイルかどうかのチェックをしたあとのデータには, タグ (0x0002, 0x0000)のような(Group, Element)構造 tagは規格書PS 3.6と照らし合わせて,何を示すかを確認しないとわからない VR (value representation) タグで示した場所に存在するデータの種類を表す アルファベット2文字で,この後ろに何が入っているかを表している 詳細はリンク参照 例えば,ASは年齢を表す文字列.DAは日付を表す文字列.USはunsigned integer 16 bits long(符号なし16ビット整数)が後ろに格納されていると明示している 値長さ 後ろに格納されているデータのByte数を示す 値領域 何が入っているかはVR次第だが,直前に示されたbyte数分だけデータが入っている このように, 128byte〜DICM 132byte〜   Tag, (Group, Element), VR, Length, Data   Tag, (Group, Element), VR, Length, Data   Tag, (Group, Element), VR, Length, Data   ...   Tag, (Group, Element), VR, Length, Data   Tag, (Group, Element), VR, Length, Data の繰り返し です, Tagは(0x1111, 0x2222)と4byte使って表され,続く2byteでVRを示し,続く2byteが格納されているデータのbyte数を示します.(一部特殊なVRがあり,それはlengthの示し方が異なりますが,今は無視) 先程のバイナリの例では,DICMに続く02 00 00 00がTagで(0002, 0000)を示しています,Little endianで表示することに注意してください!! 続く2byteの55 4CがasciiコードでULを示します. ULはUInt32で符号なし32bit整数値を示している,と先程の規格書に書いてあるので,後ろには数字が入っています. 続く2byteは04 00です.これは後ろに入っているデータ長を表すわけですから,後ろには4byteのUInt32が入っていることになります. 04 00はLittle endianで読むので,0x400 = 1024ではなく0x0004 = 4と解釈してください 続く4byteを読んでみます 96 00 00 00ですから150です. tag (0002, 0000)はファイルメタ情報の一部のようですので,デコードにはあまり関係ないことがわかります. つづく,02 00 01 00はtag (0002, 0010)を示していますが,これもメタデータで関係ありません.続いて,VR, length, data, tag, VR, length, data, ...と順々に読み取っていくことになります. あるソフトで読み取ってみると以下の様になっていました.これを再現していきます. 今日の最後に... 今日はいきなり長くなりました. DICOMデータは膨大ですし,データ規格が鬼のようにあるので,そのごく一部を再現することになると思います.どんな検査データにも対応できるようなDICOMソフトは実装が非常に大変だと思います. 終わりが見えないのも嫌なので,ちょっと最後にズルをしておきましょう. DICOMのtag情報から,このDICOMのImageDataはファイルアドレス 0x2B76から始まり,1枚目は17912byte格納されていることがわかりました. let pix1 = dicomData[0x2B76...0x2B76+17912-1] let img = NSImage(data: pix1) imgView.image = img どうですか?これは,このDICOMがLossy JPEGで圧縮されているので,imageのbinary dataさえ読めればかんたんに画像はデコードできます.これがJpeg losslessだとややこしいのですが・・・ ちょっと先読みでズルしましたが,次回はtag, VR, length, dataを読んでいくところを実装しようとおもいます! それではまた!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NavigationBarのボタンの位置を左右に動かす方法

こんにちは! 最近のアプリでよく見る、NavigationBarのleftやrightにボタンを複数追加しているようなUI、 実際あの手のUIを作る場合、Customで作るかそれっぽいのをViewで作ったりなどなど、ちょっと手間のかかるやり方があったりしますよね。 今回はそんなことしないで済む方法を見つけたので、ちょっと裏技的な感じになるのかもしれませんが、似たような記事がなかったので、書くことにしました。実装の参考にして頂ければと思います。 結論 ButtonのItemImageのInsetsを調節すればそれなりに見える 調べたこと いつからか正確にはわかりませんがNavigationBarのItemを複数設置する場合画像のようになっておりまして UIButtonBarStackViewというViewが存在するので何も考えずに、 BarButtonItemにButtonを追加していくと、自動的にボタン同士のmarginが決まってしまいます。 (画像の青くなっているものがUIButtonBarStackViewです) この間隔(多分20pt以上ある)だと、以下画像のようになり、人によりますがちょっと間隔あり過ぎじゃない?って思う人もいると思います。 実際、以下画像のようになったている場合ちょっと間抜けな感じもします。 僕が調べた限りですが、そもそもこれに近い間隔で実装しているアプリはほぼないです。 appleのミュージックですらこの間隔で作ってません。 調べてみるとどうやら以下のようにAutolayoutが効いておりました. 上の画像のように、で左右の感覚が11pt あることが分かったので、その分Insetを入れて詰めてやれば良いのではと閃き、実際にやってみたところ以下のようにずらすことに成功しました。 Codeは以下になります let item1 = UIBarButtonItem(image: .add, style: .plain, target: self, action: #selector(taped)) let item2 = UIBarButtonItem(image: .checkmark, style: .plain, target: self, action: #selector(taped)) item1.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) // 緑色のプラスボタン item2.imageInsets = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 0) // 青色のチェックボタン  left に30のInsetsを入れております。 実際の感じがこちら こんな感じで実装をすることができました。 三つにするならこんな感じでできます。 (40ずつずらしています) item1.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) item2.imageInsets = UIEdgeInsets(top: 0, left: 40, bottom: 0, right: 0) item3.imageInsets = UIEdgeInsets(top: 0, left: 80, bottom: 0, right: 0) leftはこんな感じ item1.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) item2.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 40) item3.imageInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 80) navigationItem.setLeftBarButtonItems([item1, item2, item3], animated: true) まとめ 今回はこんな感じで実装することができました。 しかし本来での位置で良いなら(多分そのほうが上記のようなことをしなくて良い) それに越したことはないので、特に特別間隔を詰めなければいけない場合 (世の中にはそういう時もある)以外はこのやり方はしなくて良いなと思いました。 まあ、どうしてもやらなければならないような時だけやってみてください。 多分邪道なやり方です。 あくまでも視覚的にいい感じになる方法です。 また、これではない方法があるのであればぜひ教えてください! 今回は読んでいただきありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む