20201021のiOSに関する記事は11件です。

SwiftUI 2.0 でカスタムタブビューを作ってみる

SwiftUI2.0で追加されたAPI

SwiftUI 2.0 で追加されたPageTabViewStyleを使ってみたかったので、カスタムタブのようなものを作ってみました。

とりあえずの完成形

こういったものを作っていこうと思います。
999.gif
GitHubはこちらです。
https://github.com/hoshi005/custom-tab

開発環境

  • Xcode 12.1
  • iOS 14.1

事前準備

タブ部分の作成

スクリーンショット 2020-10-21 21.12.23.png

アニメーション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)
    }
}

出来上がったのはこちらです。このままだと、選択状態がよくわからないですね。
001.gif

選択状態がわかるように見た目を調整する

選択時/非選択時で見た目を切り替えるため、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に.
           }
       }
}

見た目はこのようになります。選択状態が一目でわかるようになりました。
002.gif

背景色の設定と、タブの配置調整

ContentViewの見た目を調整します。

  • 全体をZStackで囲う
  • 最背面にColor("bg").ignoresSafeArea()を配置して背景色とする
  • タブビュー部分をVStackSpacerを利用して画面下部に配置
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)
        }
    }
}

見た目はこうなりました。それっぽくなってきましたね。
003.gif

画面をタブで切り替える

タブは用意したので、このタブに連動して画面が切り替わるようにします。

まずはダミーで画面部分を用意します。
適当なので、こちらは好きに作ってもらって良いと思います。

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」を簡単に作成できるのは嬉しいですね。
こちらの記事で作った切替ビューでも同じようなことができそうです。もしよかったら試してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】FloatingPanelを使ってセミモーダルビューを表示する

今回の目的

セミモーダルビュー
FloatingPanel.gif

これを表示してみたいって人向けの話です。

環境

Xcode 12.0.1
Swift5
CocoaPods 1.10.0

環境設定

podファイルに下記を追加してinatall

pod 'FloatingPanel'

本題

では早速作っていこう

Storyboard

今回のゴールは先ほどのgifの通りボタンをタップするとセミモーダルビューが出てくるところ。
まずは2画面用意し、片方はボタンを設置する。(segueは不要)

ViewController      : ボタンを配置した方
SemiModalViewController :表示するモーダル。見やすいようにオレンジにした
スクリーンショット 2020-10-21 20.51.06.png

ViewController

ViewController.swift
import 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.swift
import 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]
   }
}

まとめ

見事セミモーダルビューを表示することに成功
次はセミモーダルビュー に値渡しする方法を書く

参考

https://qiita.com/dotrikun/items/369f5c0730f444d97cf1

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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に対するアクション
    }

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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でのバイナリビルドが除外されます

image.png

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で詳しく説明されてる方がおりました

https://stackoverflow.com/questions/63607158/xcode-12-building-for-ios-simulator-but-linking-in-object-file-built-for-ios

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NavigationControllerのBackをコードから

一つ前のViewControllerにもどる

self.navigationController?.popViewController(animated: true)

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

大学生が研究室配属選考での自己アピールのためにGitHubで製作物を公開した話

経緯

大学の情報系学科に通っている大学3年生です!
私の通っている大学・学科では、3年生の後半(10~11月)に研究室配属があります。

この研究室配属では、各研究室が成績+面接等によって希望者の中から配属者を選考するのですが、面接では研究への興味・プログラミング能力・継続力・学習意欲などの自己アピールを求められます。そして、ここで重要なのは自己アピールにはGitHub等にあげた製作物も利用することができるという点です。

今回、私も研究室配属選考に備え、自己アピールに使うために過去の製作物をGitHubで公開し、「せっかく公開するんだったら記事でも書いてみようかな」と思って記事を書いて見ました。

製作物

Qiita_for_iOS

iOS開発の学習のために作成した俺得Qiita閲覧アプリです。

image1.png image2.png image3.png image4.png image5.png
  • 記事の閲覧・検索やLGTM・ストック、ストック記事の確認などができます。
  • Qiitaの公開APIを使用
  • iOS13のキャッチアップを兼ねて作成したので、UICollectionViewCompositionalLayoutProperty Wrapperなどを盛り込んでいます。

discord_clone_firebase

React開発の学習のために作成したチャットサービスDiscordのクローンアプリです。

デモ: https://discord-clone-36c89.web.app/
※ ユーザー名の入力が求められますが、「test」等を入力していただければ大丈夫です。

image1.png

  • メッセージ送信, 画像・ファイル送信, AddReactionなどができます
  • バックエンドにFirebaseを利用
  • React + Typescript + React Hooks

大学2年生以下の開発者様へ

製作物があると自己アピールに使える武器が手に入るので、作っておいて損はないと思いますし、研究室配属選考に限らず、その自己アピールが使えるケースも多いのではないでしょうか。

私の場合は製作物がiOS, Reactアプリケーションなので研究内容に直結しにくいですが、それでもプログラミング能力・継続力・学習意欲などのアピールになると思いますし、更に近年は機械学習・画像処理・音声認識などの比較的研究に関連しやすい分野も個人開発で手を出せるので、製作物で研究への興味や経験をアピールすることも可能だと思います。

もし、大学2年生以下で開発をしているのであれば、1つで良いのである程度形になった・公開できる製作物を作っておくと研究室配属で役に立つかもしれません!!

まとめ

(大学の先輩から聞いた話なのですが)
研究室配属選考の面接だと、「〇〇に興味があります。」 「プログラミングは得意です。」等の口頭での自己アピールのみの学生も多いらしく、その中で「〇〇に興味があります。〇〇を作りました。ソースコードはGitHubに上げてあります。」 「プログラミングは得意です。〇〇を作りました。△△にリリースしてあります。」と言った様に 製作物という証拠と一緒にアピールをすると信憑性が高く評価されやすい みたいです。
もちろん今までの成績も評価されますが、成績のみで全てを決める場合はむしろ少ない様です(?)。
やはり、自分がやってきたことのアウトプットは大事だなと思いました。

今回、自分の過去のリポジトリで公開できる物を探したところ、とりあえずできそうだったのは2つでしたが、これを機にこれからは製作物をよりPublicに発信して行こうと思います。
私も研究室配属選考で勝ち残れる様に頑張ります!!!

客観的評価がついているとより強いと思うので、製作物が良いと思ったらリポジトリにスターください(小声)(願望)(切実)

おまけ

GitHubのプロフィールも作成し、それっぽくしてみました!

https://github.com/kntkymt

スクリーンショット 2020-10-17 12.21.39.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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-21 11.16.49.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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に送ったリクエストに対して帰ってくる値resposestatusCodeのインスタンスを作成し、コード(200)を条件としてswitch文で分岐します。ステータスコードの概要は以下の画像を参考にしてください。
image.png

続いてこちらのコードを解説。

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(#ここに分析結果)メソッドを用いて変換。

最後に、printを記述してビルド。

分析成功: 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から値を取得していこうと考えております。もし、コード内容に修正の余地があればぜひご教授頂ければ幸いです。

最後までご覧いただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

俺的RxSwiftまとめ②

RxSwiftの特徴をさらに詳しく

この記事は、俺的RxSwiftまとめ①の続きです。

RxSwiftの特徴

非同期処理には、気を付けるポイントが2点ある。

  • コードの実行順序
  • 共有されたmutableなデータをどのように取り扱うか

である。

RxSwiftは、これらの問題に対して、以下の2つの概念を取り入れて、対処している。

そして、以下の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に通知する。
  • errorobservableerrorで終了したことを通知する。

Operators

Observable<type>には、非同期、イベントに基づいた処理を行うメソッド(Operators)が多数含まれている。これらはSide effect(ユーザー側にUIで反映すること)なしに出力のみを生成するので、Operatorを組み合わせて入力を任意の値に変換することができる。
代表的なOperatorを挙げると、

  • filiter → 条件に合う値のみを抽出する
  • mapObservableで流れてきた全ての値に対して処理を行う
  • skip → 特定の値をスキップする

などがあります。(後ほど別記事でまとめたいと思います。)

Scehduler

SchedulerDispatch queueと同じようなもので、処理をメインスレッドとサブスレッドで分けることができる。RxSwiftは定義済みのSchedulerがたくさんあるので、便利!
*UIの更新はメインスレッドで行う

RxCocoa

RxSwiftは、Swiftに限らないRxの共通使用に関するものだ。Swift特有のUIKitなどは、RxCocoaを用いて扱っていく。
RxCocoaは多くのUIパーツにリアクティブな機能を追加しているライブラリである。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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で半自動化はできるはず。

最後に

他に良い方法があったら教えてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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)

わかってしまえば何のことはないが、意外とハマった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む