- 投稿日:2020-10-21T21:48:12+09:00
SwiftUI 2.0 でカスタムタブビューを作ってみる
SwiftUI2.0で追加されたAPI
SwiftUI 2.0 で追加された
PageTabViewStyle
を使ってみたかったので、カスタムタブのようなものを作ってみました。とりあえずの完成形
こういったものを作っていこうと思います。
GitHubはこちらです。
https://github.com/hoshi005/custom-tab開発環境
- Xcode 12.1
- iOS 14.1
事前準備
- アニメーションgifを利用したかったのでSDWebImageSwiftUIを利用しています。
- ぴよたそさんからアニメーションgifファイルを利用させてもらっています。
タブ部分の作成
アニメーションgifファイルはこのように名前をつけて配置したので、名前を合わせる形でenumを定義しています。
enum TabItem: String, CaseIterable { case piyo case pen case neko case tobipen var name: String { "\(self.rawValue).gif" } }タブの一つ一つを表すための
TabItemView
を追加して、以下のように定義しました。struct TabItemView: View { let tabItem: TabItem @Binding var selected: TabItem var body: some View { // SDWebImageSwiftUIのimportが必要. AnimatedImage(name: tabItem.name) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40) .onTapGesture { selected = tabItem // タップしたら自身をselectedに. } } }メインとなる
ContentView
には、以下のようにタブビューを定義しました。struct ContentView: View { // タブの選択値と初期値. @State private var selected: TabItem = .piyo var body: some View { // タブビュー部分. HStack { ForEach(TabItem.allCases, id: \.self) { tabItem in TabItemView(tabItem: tabItem, selected: $selected) } } .padding(.vertical, 10.0) .padding(.horizontal, 20.0) .background(Color.white.clipShape(Capsule())) .shadow(color: Color.black.opacity(0.3), radius: 5, x: -5, y: 5) } }出来上がったのはこちらです。このままだと、選択状態がよくわからないですね。
選択状態がわかるように見た目を調整する
選択時/非選択時で見た目を切り替えるため、
TabItemView
を以下のように書き換えます。
- frameを調整
- paddingを調整
- offsetを調整
- タップ時の処理にアニメーションを伴わせる
var body: some View { AnimatedImage(name: tabItem.name) .resizable() .aspectRatio(contentMode: .fit) // 選択状態によって、サイズや間隔を調整する. .frame(width: tabItem == selected ? 100 : 40) .padding(.vertical, tabItem == selected ? -30 : 0) .padding(.horizontal, tabItem == selected ? -14 : 16) .offset(y: tabItem == selected ? -15 : 0) .onTapGesture { withAnimation(.spring()) { selected = tabItem // タップしたら自身をselectedに. } } }見た目はこのようになります。選択状態が一目でわかるようになりました。
背景色の設定と、タブの配置調整
ContentView
の見た目を調整します。
- 全体を
ZStack
で囲う- 最背面に
Color("bg").ignoresSafeArea()
を配置して背景色とする- タブビュー部分を
VStack
とSpacer
を利用して画面下部に配置var body: some View { ZStack { // 背景色. Color("bg").ignoresSafeArea() VStack { Spacer(minLength: 0) // タブビュー部分. HStack { ForEach(TabItem.allCases, id: \.self) { tabItem in TabItemView(tabItem: tabItem, selected: $selected) } } .padding(.vertical, 10.0) .padding(.horizontal, 20.0) .background(Color.white.clipShape(Capsule())) .shadow(color: Color.black.opacity(0.3), radius: 5, x: -5, y: 5) } } }画面をタブで切り替える
タブは用意したので、このタブに連動して画面が切り替わるようにします。
まずはダミーで画面部分を用意します。
適当なので、こちらは好きに作ってもらって良いと思います。struct HomeView: View { var body: some View { Text("Home") .font(.largeTitle) .fontWeight(.heavy) .foregroundColor(.red) } } struct ListView: View { var body: some View { Text("List") .font(.largeTitle) .fontWeight(.heavy) .foregroundColor(.green) } } struct SearchView: View { var body: some View { Text("Search") .font(.largeTitle) .fontWeight(.heavy) .foregroundColor(.blue) } } struct SettingView: View { var body: some View { Text("Setting") .font(.largeTitle) .fontWeight(.heavy) .foregroundColor(.yellow) } }最後に、これらのViewを
TabView
で定義し、カスタムタブと連動するようにします。
TabView
の引数にselected
を指定することで、カスタムタブと連動させるPageTabViewStyle
を指定することで、横スワイプでの切り替えを可能にするZStack { // 背景色. Color("bg").ignoresSafeArea() // メイン画面部分はTabViewで定義. TabView(selection: $selected) { HomeView() .tag(TabItem.piyo) ListView() .tag(TabItem.pen) SearchView() .tag(TabItem.neko) SettingView() .tag(TabItem.tobipen) } // PageTabスタイルを利用する(インジケータは非表示). .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) VStack { // 省略. } }まとめ
タブの定義がずいぶん簡単にできる印象ですが、それ以上に「切り替え用のUI」を簡単に作成できるのは嬉しいですね。
こちらの記事で作った切替ビューでも同じようなことができそうです。もしよかったら試してみてください。
- 投稿日:2020-10-21T21:26:41+09:00
【iOS】FloatingPanelを使ってセミモーダルビューを表示する
今回の目的
これを表示してみたいって人向けの話です。
環境
Xcode 12.0.1
Swift5
CocoaPods 1.10.0環境設定
podファイルに下記を追加してinatall
pod 'FloatingPanel'本題
では早速作っていこう
Storyboard
今回のゴールは先ほどのgifの通りボタンをタップするとセミモーダルビューが出てくるところ。
まずは2画面用意し、片方はボタンを設置する。(segueは不要)ViewController : ボタンを配置した方
SemiModalViewController :表示するモーダル。見やすいようにオレンジにした
ViewController
ViewController.swiftimport UIKit import FloatingPanel class ViewController: UIViewController,FloatingPanelControllerDelegate{ var floatingPanelController: FloatingPanelController! @IBOutlet weak var button: UIButton! override func viewDidLoad() { super.viewDidLoad() //ここは普段のボタンの処理 button.addTarget(self,action: #selector(self.tapButton(_ :)),for: .touchUpInside) floatingPanelController = FloatingPanelController() self.delegate = SemiModalViewController() } @objc func tapButton(_ sender: UIButton){ // セミモーダルビューとなるViewControllerを生成し、contentViewControllerとしてセットする let semiModalViewController = self.storyboard?.instantiateViewController(withIdentifier: "fpc") as? SemiModalViewController floatingPanelController.set(contentViewController: semiModalViewController) // セミモーダルビューを表示する floatingPanelController.addPanel(toParent: self, belowView: nil, animated: false) floatingPanelController.delegate = self floatingPanelController.addPanel(toParent: self, belowView: nil, animated: false) } //画面を去るときにセミモーダルビューを非表示にする override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // セミモーダルビューを非表示にする floatingPanelController.removePanelFromParent(animated: true) } // カスタマイズしたレイアウトに変更(デフォルトで使用する際は不要) func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { return CustomFloatingPanelLayout() } //tipの位置に来たときにセミモーダルビューを非表示にする func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) { if targetPosition == .tip{ vc.removePanelFromParent(animated: true) } } }SemiModalViewController
特に記述の必要なし
CustomFloatingPanelLayout
CustomFloatingPanelLayout.swiftimport Foundation import FloatingPanel class CustomFloatingPanelLayout: FloatingPanelLayout { // カスタマイズした高さ func insetFor(position: FloatingPanelPosition) -> CGFloat? { switch position { case .full: return 16.0 case .half: return 216.0 case .tip: return 44.0 default: return nil } } // 初期位置 var initialPosition: FloatingPanelPosition { //half位置から始める return .half } // サポートする位置 var supportedPositions: Set<FloatingPanelPosition> { //full,half.tipの3種類が存在 今回は3種類とも使えるように設定 return [.full,.half,.tip] } }まとめ
見事セミモーダルビューを表示することに成功
次はセミモーダルビュー に値渡しする方法を書く参考
- 投稿日:2020-10-21T17:29:56+09:00
[Tips]didEndEditingRowAtを扱う際の注意点
UITableViewDelegateのtableView(_:didEndEditingRowAt:)は、セルのスワイプやEditModeを扱う際に触る機会のあるDelegateのAPIですが、注意点が1つあります。
それはスワイプアクションによってセルが削除されても呼ばれる可能性があるという事です。
スワイプによる削除は以下の2種類あると認識しています。
tableView(_:commit:forRowAt:)
を実装しつつ、tableView(_:trailingSwipeActionsConfigurationForRowAt:)
をnil
で返している時の標準スワイプ削除tableView(_:trailingSwipeActionsConfigurationForRowAt:)
で削除アクションを含むConfigurationを返している時の独自スワイプ削除もしかしたら標準スワイプ削除を利用している場合は、内部の挙動によって
tableView(_:didEndEditingRowAt:)
が呼ばれないことがあるのかもしれませんが、後者の実装をしている場合は呼ばれることがあります。そのため、以下のようなコードを書いているとクラッシュします。
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { let indexPath = indexPath! let cell = tableView.cellForRow(at: indexPath)! // クラッシュ! // cellに対するアクション }独自スワイプ削除でセルを削除した場合、
indexPath
自体はnil
ではありませんが、tableView.cellForRow(at: indexPath)
の結果がnil
になりクラッシュします。また、API設計理由を考えても何かしらの理由で
indexPath
自体がnil
で渡されるケースが存在するはずのため、indexPath
のケアも必要です。このような意図しないクラッシュを避けるために、以下のようにOptional-Bindingを正しく利用して記述しましょう。
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { // もしくはguard-else節など if let indexPath = indexPath, let cell = tableView.cellForRow(at: indexPath) { // cellに対するアクション } }
- 投稿日:2020-10-21T16:48:17+09:00
xcode12(iOS14SDK)でビルドした際に missing one or more architectures required by this target: arm64 で error になる場合
Xcode12 iOS14SDK環境で、xcodebuildなどしたときに下記エラーが出たら
error: The linked framework 'Pods_XXXXXXX.framework' is missing one or more architectures required by this target: arm64. (in target 'XxxxxxExtension' from project 'XXXXXX')iOSシミュレータ で必要ないarm64用バイナリがビルドされてしまっている可能性が高いです。
Xcode12から
Valid Architectures(VALID_ARCHS)
が不要になったようなので、まずBuild Settings
にこの項目があったら消します次に
Excluded Architectures
に以下のように追加しますこれでarm64でのバイナリビルドが除外されます
cocoapods を使っている場合
Podfile
に下記を追加して、pod install
すればExcluded Architectures
が設定されますpost_install do |installer| installer.pods_project.build_configurations.each do |config| config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" end end参考
stackoverflowで詳しく説明されてる方がおりました
- 投稿日:2020-10-21T13:26:12+09:00
NavigationControllerのBackをコードから
一つ前のViewControllerにもどる
self.navigationController?.popViewController(animated: true)?
お仕事のご相談こちらまで
rockyshikoku@gmail.comCore MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-10-21T12:07:17+09:00
大学生が研究室配属選考での自己アピールのためにGitHubで製作物を公開した話
経緯
大学の情報系学科に通っている大学3年生です!
私の通っている大学・学科では、3年生の後半(10~11月)に研究室配属があります。この研究室配属では、各研究室が成績+面接等によって希望者の中から配属者を選考するのですが、面接では研究への興味・プログラミング能力・継続力・学習意欲などの自己アピールを求められます。そして、ここで重要なのは自己アピールにはGitHub等にあげた製作物も利用することができるという点です。
今回、私も研究室配属選考に備え、自己アピールに使うために過去の製作物をGitHubで公開し、「せっかく公開するんだったら記事でも書いてみようかな」と思って記事を書いて見ました。
製作物
Qiita_for_iOS
iOS開発の学習のために作成した
俺得Qiita閲覧アプリです。
- 記事の閲覧・検索やLGTM・ストック、ストック記事の確認などができます。
- Qiitaの公開APIを使用
- iOS13のキャッチアップを兼ねて作成したので、
UICollectionViewCompositionalLayout
やProperty Wrapper
などを盛り込んでいます。discord_clone_firebase
React開発の学習のために作成したチャットサービスDiscordのクローンアプリです。
デモ: https://discord-clone-36c89.web.app/
※ ユーザー名の入力が求められますが、「test」等を入力していただければ大丈夫です。
- メッセージ送信, 画像・ファイル送信, AddReactionなどができます
- バックエンドにFirebaseを利用
- React + Typescript + React Hooks
大学2年生以下の開発者様へ
製作物があると自己アピールに使える武器が手に入るので、作っておいて損はないと思いますし、研究室配属選考に限らず、その自己アピールが使えるケースも多いのではないでしょうか。
私の場合は製作物がiOS, Reactアプリケーションなので研究内容に直結しにくいですが、それでもプログラミング能力・継続力・学習意欲などのアピールになると思いますし、更に近年は機械学習・画像処理・音声認識などの比較的研究に関連しやすい分野も個人開発で手を出せるので、製作物で研究への興味や経験をアピールすることも可能だと思います。
もし、大学2年生以下で開発をしているのであれば、1つで良いのである程度形になった・公開できる製作物を作っておくと研究室配属で役に立つかもしれません!!
まとめ
(大学の先輩から聞いた話なのですが)
研究室配属選考の面接だと、「〇〇に興味があります。」 「プログラミングは得意です。」等の口頭での自己アピールのみの学生も多いらしく、その中で「〇〇に興味があります。〇〇を作りました。ソースコードはGitHubに上げてあります。」 「プログラミングは得意です。〇〇を作りました。△△にリリースしてあります。」と言った様に 製作物という証拠と一緒にアピールをすると信憑性が高く評価されやすい みたいです。
もちろん今までの成績も評価されますが、成績のみで全てを決める場合はむしろ少ない様です(?)。
やはり、自分がやってきたことのアウトプットは大事だなと思いました。今回、自分の過去のリポジトリで公開できる物を探したところ、とりあえずできそうだったのは2つでしたが、これを機にこれからは製作物をよりPublicに発信して行こうと思います。
私も研究室配属選考で勝ち残れる様に頑張ります!!!客観的評価がついているとより強いと思うので、製作物が良いと思ったらリポジトリにスターください(小声)(願望)(切実)
おまけ
GitHubのプロフィールも作成し、それっぽくしてみました!
- 投稿日:2020-10-21T11:30:14+09:00
Xcode12でplaygroundを作成する
Xcode12は、Welcome to Xcode画面から「Get started with a playground」がなくなり、かわりに「Open a project or file」が追加になりました。
「Create a new Xcode project」を選択しても、playgroundの項目はありません。
既存のプロジェクトを開いた状態で下記画像の操作を行い、playgroundを作成することができました。
- 投稿日:2020-10-21T08:50:45+09:00
[Swift5]"IBM Watson ToneAnalyzer"で取得した分析結果をJSONに変換する
投稿のポイント
今回は前回投稿した記事の続きなので、前回記事をまだみられていない方はまず、そちらをご覧ください。
▼[Swift5]"IBM Watson ToneAnalyzer"を使用して感情分析を行う
https://qiita.com/nkekisasa222/items/2933e46b22c17d3eedfb今回行うことと、記述したコード
前回取得した下記情報をJSON形式に変換します。
ToneAnalysis(documentTone: ToneAnalyzer.DocumentAnalysis(tones: Optional([ToneAnalyzer.ToneScore(score: 0.6165, toneID: "sadness", toneName: "Sadness"), ToneAnalyzer.ToneScore(score: 0.829888, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, warning: nil), sentencesTone: Optional([ToneAnalyzer.SentenceAnalysis(sentenceID: 0, text: "Team, I know that times are tough!", tones: Optional([ToneAnalyzer.ToneScore(score: 0.801827, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil), ToneAnalyzer.SentenceAnalysis(sentenceID: 1, text: "Product sales have been disappointing for the past three quarters.", tones: Optional([ToneAnalyzer.ToneScore(score: 0.771241, toneID: "sadness", toneName: "Sadness"), ToneAnalyzer.ToneScore(score: 0.687768, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil), ToneAnalyzer.SentenceAnalysis(sentenceID: 2, text: "We have a competitive product, but we need to do a better job of selling it!", tones: Optional([ToneAnalyzer.ToneScore(score: 0.506763, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil)]))今回記述したのは以下のコード。
ViewController.swift//ステータスコードの定数を作成し条件分岐 let statusCode = response?.statusCode switch statusCode == Optional(200) { case true: print("分析成功: \(statusCode)") //分析結果の定数を作成 let analysisResult = result //JSONへ変換するencoderを用意 let encoder = JSONEncoder() //可読性を高めるためにJSONを整形 encoder.outputFormatting = .prettyPrinted //分析結果をJSONに変換 guard let jsonValue = try? encoder.encode(analysisResult) else { fatalError("Failed to encode to JSON.") } //JSONデータ確認 print("感情分析結果(JSON): \(String(bytes: jsonValue, encoding: .utf8)!)") case false: //ステータスコードの表示(200範囲は成功、400範囲は障害、500範囲は内部システムエラー) print("分析失敗: \(statusCode)") }コード解説
まず、
ToneAnalyzer
に送ったリクエストに対して帰ってくる値respose
のstatusCode
のインスタンスを作成し、コード(200)
を条件としてswitch文
で分岐します。ステータスコードの概要は以下の画像を参考にしてください。
続いてこちらのコードを解説。
ViewController.swift//分析結果を代入 let analysisResult = result //JSONへ変換するencoderを用意 let encoder = JSONEncoder() //可読性を高めるためにJSONを整形 encoder.outputFormatting = .prettyPrinted //分析結果をJSONに変換 guard let jsonValue = try? encoder.encode(analysisResult) else { fatalError("Failed to encode to JSON.") //JSONデータ確認 print("感情分析結果(JSON): \(String(bytes: jsonValue, encoding: .utf8)!)") }まず、分析結果を
analysisResult
に代入し、次にJSONEncoder()
をencoderに代入。このJSONEncoder()
を使ってJSON形式に変換します。続いて、JSONを人間の視覚的に感知しやすい形式にフォーマットを整形。そして、
encoder.encode(#ここに分析結果)
メソッドを用いて変換。最後に、
分析成功: Optional(200) 感情分析結果(JSON): { "sentences_tone" : [ { "sentence_id" : 0, "text" : "Team, I know that times are tough!", "tones" : [ { "score" : 0.80182699999999996, "tone_id" : "analytical", "tone_name" : "Analytical" } ] }, { "sentence_id" : 1, "text" : "Product sales have been disappointing for the past three quarters.", "tones" : [ { "score" : 0.77124099999999995, "tone_id" : "sadness", "tone_name" : "Sadness" }, { "score" : 0.68776800000000005, "tone_id" : "analytical", "tone_name" : "Analytical" } ] }, { "sentence_id" : 2, "text" : "We have a competitive product, but we need to do a better job of selling it!", "tones" : [ { "score" : 0.50676299999999996, "tone_id" : "analytical", "tone_name" : "Analytical" } ] } ], "document_tone" : { "tones" : [ { "score" : 0.61650000000000005, "tone_id" : "sadness", "tone_name" : "Sadness" }, { "score" : 0.82988799999999996, "tone_id" : "analytical", "tone_name" : "Analytical" } ] } }デバックエリアにこのように表示されていれば変換成功です!
一応、前回コードと合わせたものを記述しておきます。
ViewController.swift//ToneAnalyzer(感情分析)用メソッド func toneAnalyzer() { //WatsonAPIキーのインスタンス作成 let authenticator = WatsonIAMAuthenticator(apiKey: "") //WatsonAPIのversionとURLを定義 let toneAnalyzer = ToneAnalyzer(version: "2017-09-21", authenticator: authenticator) toneAnalyzer.serviceURL = "" //分析用サンプルテキスト let sampleText = """ Team, I know that times are tough! Product \ sales have been disappointing for the past three \ quarters. We have a competitive product, but we \ need to do a better job of selling it! """ //SSL検証を無効化(不要?) //toneAnalyzer.disableSSLVerification() //エラー処理 toneAnalyzer.tone(toneContent: .text(sampleText)){ response, error in if let error = error { switch error { case let .http(statusCode, message, metadata): switch statusCode { case .some(404): // Handle Not Found (404) exceptz1zion print("Not found") case .some(413): // Handle Request Too Large (413) exception print("Payload too large") default: if let statusCode = statusCode { print("Error - code: \(statusCode), \(message ?? "")") } } default: print(error.localizedDescription) } return } //データ処理 guard let result = response?.result else { print(error?.localizedDescription ?? "unknown error") return } //ステータスコードの定数を作成し条件分岐 let statusCode = response?.statusCode switch statusCode == Optional(200) { case true: print("分析成功: \(statusCode)") //分析結果の定数を作成 let analysisResult = result //JSONへ変換するencoderを用意 let encoder = JSONEncoder() //可読性を高めるためにJSONを整形 encoder.outputFormatting = .prettyPrinted //分析結果をJSONに変換 guard let jsonValue = try? encoder.encode(analysisResult) else { fatalError("Failed to encode to JSON.") } //JSONデータ確認 print("感情分析結果(JSON): \(String(bytes: jsonValue, encoding: .utf8)!)") //ヘッダーパラメータ print(response?.headers as Any) case false: //ステータスコードの表示(200範囲は成功、400範囲は障害、500範囲は内部システムエラー) print("分析失敗: \(statusCode)") } } }最後に
この後は取得したJSONから値を取得していこうと考えております。もし、コード内容に修正の余地があればぜひご教授頂ければ幸いです。
最後までご覧いただきありがとうございました!
- 投稿日:2020-10-21T08:02:31+09:00
俺的RxSwiftまとめ②
RxSwiftの特徴をさらに詳しく
この記事は、俺的RxSwiftまとめ①の続きです。
RxSwiftの特徴
非同期処理には、気を付けるポイントが2点ある。
- コードの実行順序
- 共有されたmutableなデータをどのように取り扱うか
である。
RxSwiftは、これらの問題に対して、以下の2つの概念を取り入れて、対処している。
- 宣言的プログラミング → 例:
a = b + c
と定義すれば、a
の値はb
とc
の値が変化するごとに更新する。- 関数型プログラミング → 関数の入出力の連なりで処理を記述する方式
そして、以下の5つの特徴を手に入れている。(=Reactive System)
- Resposive→UIにアプリの最新状態を常に反映すること
- Resilient→各処理が分離されていて、エラーリカバリが容易であること
- Elastic→変動ワークロードに対して、遅延読み込みやスロットリング、リソースシェアなどの機能で対処する
- Message-driven→コンポーネント間のやりとりをメッセージベースの通信を使用して非同期に行い、疎結合にして再利用性を高め、クラスのライフサイクルと別に実装すること
RxSwiftの構成要素
RxSwiftは
Observable/operator/scheduler
の3つの構成要素を持っている。Observable
Observable<type>
で定義する、観察対象のこと。
時間の経過とともに生成される一連のデータの不変なsnapshot(その瞬間のデータのコピー)を流す一連のイベントを、非同期に生成することができる。複数の
observer
(観察者)がリアルタイムにイベントに反応して、UIを更新したり、データを利用できる。
ObservableType protocol
は以下の3つのイベントを生成することができる。
next
→ 次のデータをobserver
に持ってくるイベント。completed
が起こるまで、値をobserver
に持ってき続ける。completed
→ 一連のイベントをsuccess
で終了させ、observerに通知する。error
→observable
がerror
で終了したことを通知する。Operators
Observable<type>
には、非同期、イベントに基づいた処理を行うメソッド(Operators
)が多数含まれている。これらはSide effect(ユーザー側にUIで反映すること)なしに出力のみを生成するので、Operator
を組み合わせて入力を任意の値に変換することができる。
代表的なOperator
を挙げると、
filiter
→ 条件に合う値のみを抽出するmap
→Observable
で流れてきた全ての値に対して処理を行うskip
→ 特定の値をスキップするなどがあります。(後ほど別記事でまとめたいと思います。)
Scehduler
Scheduler
はDispatch queue
と同じようなもので、処理をメインスレッドとサブスレッドで分けることができる。RxSwiftは定義済みのScheduler
がたくさんあるので、便利!
*UIの更新はメインスレッドで行うRxCocoa
RxSwift
は、Swift
に限らないRxの共通使用に関するものだ。Swift
特有のUIKit
などは、RxCocoa
を用いて扱っていく。
RxCocoa
は多くのUIパーツ
にリアクティブな機能を追加しているライブラリである。
- 投稿日:2020-10-21T02:14:44+09:00
【Swift】複数行のUILabelの余白を設定する:上下左右
はじめに
環境は
・Xcode 11.6
・Swift 5
になります。ラベルの余白(パディング)を設定したいとき、ありますよね?
例えばtextViewの場合は
textContainerInset
、UIButtonの場合はcontentEdgeInsets
なんかを使って、比較的楽に設定できるかと思います。しかしUILabelの場合は少し面倒で、複数行のテキストだと尚更。
まず試すこと
特に横方向の余白については、
attributedText
できれいに解決するかも。
以下は一例。let style = NSMutableParagraphStyle() // horizontal setting style.headIndent = 0 style.tailIndent = 0 // vertical setting style.lineSpacing = 0 style.maximumLineHeight = 16 style.minimumLineHeight = 16 style.paragraphSpacingBefore = 10 style.paragraphSpacing = 30 let attr: [NSAttributedString.Key : Any] = [ .font: ..., .paragraphStyle : style, ] let attributedText = NSAttributedString(string: "hoge", attributes: attr) let label = UILabel() label.attributedText = attributedText縦方向については、行間隔と行の高さを指定できるものの、直接余白を設定できないのが悔しい。
ただしテキストが1行の場合は、行の高さとフォントサイズをうまく設定して、実質的に上下の余白をコントロールするやり方も。大抵はこれでオッケー
検索すればよく出てくるやり方だが、以下のようにUILabelを継承したカスタムLabelを作ってやれば、大抵は解決する。
きちんと上下左右の余白を設定できる。
class PaddingLabel: UILabel { var padding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) override func drawText(in rect: CGRect) { let newRect = rect.inset(by: padding) super.drawText(in: newRect) } override var intrinsicContentSize: CGSize { var contentSize = super.intrinsicContentSize contentSize.height += padding.top + padding.bottom contentSize.width += padding.left + padding.right return contentSize } override func sizeThatFits(_ size: CGSize) -> CGSize { var contentSize = super.sizeThatFits(size) contentSize.width += padding.left + padding.right contentSize.height += padding.top + padding.bottom return contentSize } }ここで
sizeThatFits(_ size: CGSize)
は無くても良いが、これがあるとsizeToFit()
したときに余白を含んだ大きさで自動調整してくれるので、ありがたい。
もちろん、sizeToFit()
で余白を含めたくなければ、オーバーライドしないでおく。最終手段
以上で解決しない問題があったら、おそらく最終手段は
UILabel( ) in UIView( )
として、ラベルは通常の
sizeToFit()
で余白なしの状態にしてから親ビュー内の任意の位置に配置することによって、親ビューを余白つきのラベル(もしくはボタン)として扱える。この場合は親ビューのサイズをこちらで指定しないといけないが、
sizeToFit()
+AutoLayout
で半自動化はできるはず。最後に
他に良い方法があったら教えてください!
- 投稿日:2020-10-21T01:13:51+09:00
【Swift】生成した動画のサムネイル画像の画質が悪いと思ったら
悩んだこと
カメラロールを自力で実装すると、動画の場合はプレビュー画像を自分で作ってあげることになると思います。
作成したプレビュー画像を確かめてみたら、どうも画質が悪くてぼやけている...
この原因を探すのに少し時間がかかってしまいました。解決方法
以下のソース中で
generator.maximumSize
に適切な値を設定してあげることで解決しました!let asset: AVAsset = ... let generator = AVAssetImageGenerator(asset: asset) generator.maximumSize = CGSize(width: w, height: h) generator.appliesPreferredTrackTransform = true let img = try! generator.copyCGImage(at: CMTime.zero, actualTime: nil)わかってしまえば何のことはないが、意外とハマった。