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

[Flutter] FlipCardを使ってカード型のWidgetを回転させてみる

やりたいこと

カード型のWidgetをタップするといい具合にアニメーションつけて裏側を表示するように回転したい。

使用したライブラリ

flip_card

pub get

  1. pubspec.yamlに「flip_card: ^0.4.4」を追記してflutter pub getする
  2. dartソースにimport 'package:flip_card/flip_card.dart';を追記する

実装

FlipCard(
  front: Card(...), // カード前面に表示するWidget
  back: Card(...), // カード背面に表示するWidget
  direction: FlipDirection.HORIZONTAL, // カード回転向き(HORIZONTAL:横<デフォルト>, VERTICAL:縦)
  flipOnTouch: true, // タッチ可否(true:カード部タップで回転する<デフォルト>, false:タップしても回転しない)
  speed: 500, // 回転スピード(500ミリ秒<デフォルト>)
  onFlip: () {}, // タップイベント
  onFlipDone: (isFront) {}, // タップイベント(前面かどうかbool値で判断できる) 
)

サンプル

横回転

縦回転

実装サンプル

https://github.com/unbam/flip_card_sample

(おまけ)カード部分をタップではなくアイコンなどの別Widgetをタップすると回転するようにしたい

GlobalKeyを設定してtoggleCardメソッドを実行する。

final cardKey = GlobalKey<FlipCardState>();

FlipCard(
  key: cardKey, // キーを設定
  front: Card(
    child: IconButton(
      icon: Icon(Icons.compare_arrows),
      onPressed: () => cardKey.currentState.toggleCard(), // 回転イベント実行
    )
  )
)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブラウザが非表示になった時にマイク入力を解放する

困っている事

webAudioAPIを利用してマイク入力データを利用している中で

  • 別タブに切り替えた時に、タブの録音中マークが消えない(PC)
  • ホームボタンを押した時に画面上に赤い帯が残る(iOS)

タブはこういう丸いやつ↓
スクリーンショット 2020-01-03 19.00.22.png

赤い帯はこういうやつ↓
S__107831356.jpg

環境

PC

  • MacBook Pro 2016
  • macOS Mojave 10.14.6
  • Chrome 79.0.3945.88(Official Build) (64 ビット)

スマホ

  • iPhone 6s
  • iOS13.3
  • safari

手順

  1. タブがアクティブじゃなくなった事を検知して
  2. マイクの入力トラックを解放する

タブがアクティブじゃなくなった事を検知

コード

function setEventListener() {
  let hidden, visibilityChange;
  if (typeof document.hidden !== "undefined") { // Opera 12.10 や Firefox 18 以降でサポート
    hidden = "hidden";
    visibilityChange = "visibilitychange";
  } else if (typeof document.msHidden !== "undefined") {
    hidden = "msHidden";
    visibilityChange = "msvisibilitychange";
  } else if (typeof document.webkitHidden !== "undefined") {
    hidden = "webkitHidden";
    visibilityChange = "webkitvisibilitychange";
  }
  document.hiddenStatus = hidden;

  if (hidden === undefined) {
    console.log("This demo requires a browser, such as Google Chrome or Firefox, that supports the Page Visibility API.");
  } else {
    document.addEventListener(visibilityChange, browserBlorFunction, {once: true});
  }
}

async function browserBlurFunction() {
  if (document.hidden) {
    // 非表示状態になった時の動作
    await stopRecording();
  }
}

解説

この記事に全て書いてあります!
[iOS/Android]ブラウザでページが非表示になったことを検知する方法

今回はタブがアクティブではない時の体験を損なわない事を目的としていたので、
Page Visibility APIのみの利用をしています。

簡単に言うと、「ページが表示されているかどうか」を判定してくれるAPIです。
EventListenerに登録して利用可能です!

この機能によって追加されたプロパティであるdocument.hiddenで現在のブラウザの表示状態を取得できるので、
document.hidden === trueの時に、マイク入力解放関数を実行します。

ちなみに

同じような動作をさせたい時のグローバルイベントハンドラーとしてwindow.onblurがあります。
これはウィンドウの切り替えを意味するものでありページの表示を検知するものではありません。
そのためwindow.onblurにマイク入力解放のコードをセットしても、
iPhoneでホームボタンを押した後は冒頭で紹介した赤い帯が出るし、
タブを切り替えてもブラウザの丸いマークは残ったままです。

マイクの入力を解放する

コード

//streamsには、mediaStreamそのものが入っています。
function stopRecording() {
  let tracks = streams.getTracks();
  tracks.forEach(function (track) {
    track.stop();
  });
}

//streamsへの格納
navigator.mediaDevices.getUserMedia({audio: true})
    .then(mediaStream => {
      gotStream(mediaStream);
    }).catch(e => {
    alert('Error getting audio');
  });
}

let streams = null;
function gotStream(stream){
  streams = stream;
  //以下音声の処理...
}

解説

マイク入力はmediaDevices.getUserMediaより取得しており、その際に取得したmediaStreamTrackを停止させる事により、入力を解放します。

trackの停止

mediaStreamTrack.readyStateと言うプロパティの値が"ended"になる時、入力デバイスからのデータを受け取らなくなります。

"ended"は入力デバイスがこれ以上データを提供することがなく、新しいデータも一切提供されないことを示します。
引用元:mediaStreamTrack.readyState

そのための関数として、

MediaStreamTrack.stop()
トラックに関連付けられたソースの再生を停止し、ソースとトラックの関連付けを解除します。トラックの状態はendedに設定されます。

こちらが用意されているので、それを使いましょう!
mediaStream.getTracks

function stopRecording() {
  let tracks = streams.getTracks();
  tracks.forEach(function (track) {
    track.stop();
  });
}

これでmediaStreamTrack.readyStateがendedになるため、記事冒頭で紹介した動作は起こらなくなります。

最後に

人生初Qiita記事書いてみました!
ちなみにsafariのアプリを完全に落とせば同じ事は達成できます()

今年は頑張ってアウトプットしていくぞぉ

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

iOSのショートカットで扱う秘密データの管理方法について

はじめに

iOSのショートカットを作成する上で一度は悩むことは、ショートカット内部で扱う秘密データ(APIで扱うAPIキーやパスワードなどのトークン文字列)をどう管理するかではないでしょうか。

この管理方法について、検討した内容を共有したい思います。

前提

  • iOSのショートカットを使用するため、iOS12以上が必要です。
  • また、iPhoneの1passwordアプリを使用します。

よくある管理方法の問題

いろんな人が作成したショートカットの中身を見てみると、以下のように秘密データを管理しているケースに遭遇します。

  • メモアプリ(あるいは リマインダーアプリ)に秘密データを保存し、ショートカット実行時にメモアプリから検索する。
  • ショートカット内部に秘密データをハードコーディングする。
  • ショートカット実行時にアラートを表示し、秘密データを入力させる。

メモアプリから検索する方法は、メモアプリがパスワードで保護されていないため、秘密データを他人に見られる恐れがあります。同様に、ハードコーディングする方法についても、ショートカットの内部処理を見れば、秘密データを他人に見られる恐れがあります。

ショーカット実行時に秘密データをアラートに表示する方法は、上記方法よりは良さそうに感じますが、秘密データをどこからか検索する手間やコピーしたクリップボードの秘密データが残ったままになる恐れがあります。

解決方法

上記の管理方法の解決方法として、1passwordのパスワード管理アプリを使う方法を提案したいと思います。

1passwordアプリで管理する利点としては、以下の通りです。

  • 秘密データがマスターパスワードで保護されているため、他人に秘密データを容易に見られる心配がない。
  • 1passwordのURLスキームを使えば、1passwordの秘密データを検索する(絞りこむ)ことができる。
    • 例えば、onepassword://search/<検索ワード>の形式で1passwordを起動できます。
    • URLスキームの仕様の詳細については、こちらを参照。
  • 1passwordに秘密データが保存されていなくても、実行時に秘密データを作成できる。
  • 1passwordの秘密データをワンタップでクリップボードでコピーできる。

ショートカットアプリでは1passwordアプリのURLスキームを実行時に呼び出すことができるため、以下のような手順で秘密データをショートカット内部で扱うことができます。

手順詳細

  • ショートカットアプリから1passwrordアプリのURLスキームを起動する。
  • 起動した1passwordアプリから秘密データをクリップボードにコピーする。
  • クリップボードにコピー後、左上のアプリ名を押下し、ショートカットアプリに戻る。
  • ショートカットアプリでクリップボードから秘密データを任意の変数にコピーする。
  • 安全のためにクリップボードの秘密データをクリア(空文字列をコピー)する
  • 任意の変数にコピーした秘密データをショートカット内部で使用する。

ショートカットの作成

ここでは、1passwordアプリからGoogleのアカウントのトークンを表示するショートカットの作成例について説明します。

手順詳細

  • ショートカットの新規作成画面を開きます。
  • アクションのURLを開くを追加し、onepassword://search/googleをURLに設定します。
    • この処理で、1passwordが起動し、googleのキーワードで検索された状態になります。
  • スクリプティングの待機を追加します。
    • この処理で、1passwordのアプリからショートカットアプリに戻るまで、ショートカットアプリが待機状態になります。
  • アクションの変数に設定を追加します。
    • 変数を仮にtokenとして、変数tokenをクリップボードに設定にします。
  • アクションのテキストを追加します。
    • 変数tokenを使用して、秘密データを表示するテキストを作成します。
    • ここでは、トークンはXXXXですのような形式のテキストになります。
  • アクションのクイックルックを追加します。
    • 作成したテキストをクイックルックで表示します。
  • クリップボード上の秘密データをクリアするために、アクションのテキストを追加し、空のテキストを追加します。
  • 最後にアクションのクリップボードにコピーを追加し、空のテキストをクリップボードにコピーします。

参考までに、上記で作成したショートカットのiCloudの共有リンクを貼っておきます。

さいごに

秘密データの管理方法について、1passwordを活用する方法について提案しました。秘密データを管理する際の参考になれば幸いです。

参考

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

XCodeを使わず、MacApp「Tranceporter」でipaをAppStoreConnectに簡単にアップロード

納品されたiOSアプリケーションのipaファイルをXCode使わずにAppStoreConnectにアップロードすることができるTranceferというMac Appが公開されてたので使ってみます。

前提

  • Apple Developerアカウントを保有している
  • App Store ConnectのマイAppに新規アプリ作成していること

Mac App「Trancefer」

Mac App StoreでTransporterを入手できるようになりました
https://developer.apple.com/jp/news/?id=10152019a

Appのリンクはこちら。期待できそう!
image.png

はいはーい同意します。
image.png

サインイン画面が表示されます。
image.png
AppStoreConnectのアカウントでサインインします。
image.png

あら。シンプルな画面。
image.png

ipaファイルをドラッグ&ドロップすると、以下のようにアプリがデリバリできる状態になります。
image.png

このままデリバリしてもいいのですが、デリバリ失敗すると証明書の中のバージョンを上げて再度デリバリし直したりしないといけなくなるので、デリバリ前に検証を行います。デリバリボタン横のオプションボタンをクリックし、[検証]を実行します。
image.png

検証が問題ない場合には正常に検証が終了します。
image.png

もし検証失敗した場合にはエラーが表示しますので、エラーログで原因調べられます。エラーログの見方はこちらこちら参考。
image.png

では、AppStoreConnectにデリバリ転送します。[デリバリ]ボタンをクリックします。
image.png

電源につながった状態になっていないと警告が表示されるみたいです。
image.png

4,5分くらいかかります。
image.png

正常に終わると以下のようにデリバリ済みという表示に変わります。
image.png

AppStoreConnectのApp画面を見ると、ちゃんと転送されてます。
image.png

参考リンク

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

コードによる制約の追加

趣味でiOSアプリ開発をかじっていた自分が、改めてiOS開発を勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。5章を読んでの内容になります。

コードによる制約の追加

3つの方法がある。
・NSLayoutConstraintのイニシャライザ
・VisualFormatLanguage
・NSLayoutAnchor(iOS9~)

NSLayoutConstraintのイニシャライザ

メリット : 最も基本的な方法なのでわかりやすい。
デメリット : 制約1つを定義するのに1つイニシャライザを呼び出さないといけない。

ViewContoroller
    let view1 = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 300, height: 250))
    view1.backgroundColor = .red
    view1.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(view1)

    /*constraint1 : viewとview1のcenterXを等しくする
     constraint2 : viewとview1のcenterYを等しくする
     constraint3 : view1の幅を設定
     constraint4 : view1の高さを設定*/

    // 制約を設定。幅と高さも必要。
    let constraint1 = NSLayoutConstraint(item: view1, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0)
    let constraint2 = NSLayoutConstraint(item: view1, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 0.0)
    let constraint3 = NSLayoutConstraint(item: view1, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0.0, constant: 100)
    let constraint4 = NSLayoutConstraint(item: view1, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0.0, constant: 100)
    view.addConstraints([constraint1, constraint2, constraint3, constraint4])

デバッグ画面で制約を確認
image.png

引数名 渡す値
item 制約を追加する部品
attribute 制約を追加する要素
relatedBy 追加する部品と基準となる部品の関係性
toItem 制約の基準となる部品
attribute 制約の基準となる要素(toItemの要素)
multiplier 制約の割合の数値
constant 制約で追加する数値

relatedByがequalとして式で表すと

item.attribute = toItem.attribute * multiplier + constant

VisualFormatLanguage

VSLと略されたり、視覚的書式言語と呼ばれることもある。
NSLayoutConstraintクラスのconstraintsメソッドを使う。戻り値は配列で帰ってくる。
メリット : 1つの処理で複数の制約を作成することができる。
デメリット : 制約を決める部分が文字列なので実行時エラーが出ない。

ViewContoroller
  let view1 = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 300, height: 250))
  view1.backgroundColor = .red
  view1.translatesAutoresizingMaskIntoConstraints = false
  view.addSubview(view1)

  /*constraint1 : 水平方向の制約。親viewから100ptスペースを空けて幅を指定(metricsで値を指定したパターン)
   constraint2 : 垂直方向の制約。親viewから200ptスペースを空けて高さを指定*/

  let constraints1 = NSLayoutConstraint.constraints(withVisualFormat: "H:|-100-[view1(width)]", options: .alignAllCenterX, metrics: ["width" : 200], views: ["view1" : view1])
  let constraints2 = NSLayoutConstraint.constraints(withVisualFormat: "V:|-200-[view1(200)]", options: .alignAllCenterY, metrics: nil, views: ["view1" : view1])

  // 2つの配列を1つの配列にするためにflatmapを使っている。
  view.addConstraints([constraints1, constraints2].flatMap{ $0 })

デバッグ画面で制約を確認
image.png

withVisualFormat

VHLフォーマットで制約を記述する

レイアウトの方向

記法 意味
V: 垂直方向の制約を追加
H: 水平方向の制約を追加

対象オブジェクト

記法 意味
[view] viewオブジェクト
親ビュー

オブジェクト間のスペース

記法 意味
|-20-[view] 親とviewの距離20pt
|-(-20)-[view] 親とviewの距離-20pt
|-[view] 親とviewの距離デフォルト(8pt)
|[view] 親とviewの距離0pt

オブジェクトのサイズ

記法 意味
H:[view(100)] viewの幅は100pt

H:[view1(==view2)]view1の幅はview2に等しい

関係性 - Relationship

記法 意味
[view1]-(>=50)-[view2] view1とview2の距離は50pt以上
H:[view(>= 50, <=100)] viewの幅は50pt以上、100pt以下


優先度

記法 意味
H:[view(100@750)] viewの幅は優先度750で100pt

options

複数のオブジェクトに対して揃え方を指定する。

例えば、V:[view1]-[view2]のように垂直方向の制約を定義する際は下記のオプションを選択する。

意味
.alignAllLeft 全てのビューを左寄せ
.alignAllRight 全てのビューを右寄せ
.alignAllLeading 全てのビューをLeading寄せ
.alignAllTrailing 全てのビューをTrailing寄せ
.alignAllCenterX 全てのビューをx方向中央に揃える

水平方向の場合

意味
.alignAllTop 全てのビューを上部寄せ
.alignAllBottom 全てのビューを下部寄せ
.alignAllCenterY 全てのビューをy方向中央に揃える
.alignAllBaseline 全てのビューをベースラインに揃える

またVFlで記述したレイアウトを左右どちらから並べるか指定するオプションもある。
基本はLeadingからTrailing方向に並べられる。

意味
.directionLeadingtoTrailing LeadingからTrailing方向に並べる
.directionLeftToRight 左から右に並べる。
.directionRightToLeft 右から左に並べる。

metrics

変数を代入するのに使う。

    let constraints1 = NSLayoutConstraint.constraints(withVisualFormat: "H:[view1(width)]", options: .directionRightToLeft, metrics: ["width" : 200], views: ["view1" : view1])

NSLayoutAnchor

メリット : 処理が読みやすい。比較的コードを書く量が少ない。
デメリット : iOS9以降からなのでiOS9以前では使えない。

ViewContoroller
    let view1 = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 300, height: 250))
    view1.backgroundColor = .red
    view1.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(view1)

    view1.topAnchor.constraint(equalTo: view.topAnchor, constant: 200).isActive = true
    view1.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100).isActive = true
    view1.widthAnchor.constraint(equalToConstant: 200).isActive = true
    view1.heightAnchor.constraint(equalToConstant: 200).isActive = true

デバッグ画面で制約を確認
image.png

制約を編集する

IBOutletで制約を紐づけることができ、コードでプロパティを変更できる。

image.png

  @IBOutlet weak var blueViewTopConstraint: NSLayoutConstraint!
  @IBAction func tapButton(_ sender: Any) {
    blueViewTopConstraint.constant += 50
  }

空間を定義する - UILayoutGuide(iOS9.0~)

空間を定義して、その空間をもとに制約をつけることができる。
空のUIViewを用いるよりも、レンダリングコストが発生しない。余計なビューの階層が増えないというメリットがある。

class ViewController3: UIViewController {


  override func viewDidLoad() {
    super.viewDidLoad()

    let view1 = UIView.init(frame: CGRect.init(x: 0, y: 0, width: 300, height: 250))
    view1.backgroundColor = .red
    view1.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(view1)

    // 空間を定義する
    let space = makeLayoutSpace()

    // spaceを基準に制約をつけることができる
    view1.topAnchor.constraint(equalTo: space.bottomAnchor, constant: 10).isActive = true
    view1.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100).isActive = true
    view1.widthAnchor.constraint(equalToConstant: 200).isActive = true
    view1.heightAnchor.constraint(equalToConstant: 200).isActive = true
  }

  func makeLayoutSpace() -> UILayoutGuide{
    let activeFlag = true
    view.addLayoutGuide(space)
    space.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = activeFlag
    space.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = activeFlag
    space.widthAnchor.constraint(equalToConstant: 200).isActive = activeFlag
    space.heightAnchor.constraint(equalToConstant: 200).isActive = activeFlag

    return space
  }

デバッグ画面で確認すると定義した空間からの制約は見えず、viewの階層にも空間は存在しないが、確かに定義した空間からの制約をもとにviewを表示できている。

image.png

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

Storyboardの設定項目

趣味でiOSアプリ開発をかじっていた自分が、改めてiOS開発を勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。4章を読んでの内容になります。

起動時のstoryboardを設定

GUIで設定
プロジェクトファイルのMain Interfaceでstoryboardを指定する。
下記のように設定してあるとMain.storyboardファイルが読み込まれる。

image.png

storyboardで開始時のビューを設定する方法

矢印をドラッグして持ってくるか、is initial View Controller にチェックを入れる。

image.png

stackボタン

部品を複数選択して下記のボタンを押すとstackViewを作ることができる。
image.png

親子関係にあるビューの制約

子ビューから親ビューへCtrol+ドラッグした時に追加できる制約で、親子間での比率など制約を設定できる。

image.png

xibファイ ルの呼び出し

xibファイルを作成。
Viewを作って背景色を青にする。
image.png

呼び出したい箇所で読み込む。

(guardでnilのチェックをしているが他の方法でもいい)

    // xibファイルの呼び出し
    guard let subView = Bundle.main.loadNibNamed("Xibファイル名", owner: nil, options: nil)?.first as? UIView else { return }
    self.view.addSubview(subView)

カスタムクラスのビューに指定し、カスタムクラスを呼び出す。

xibファイルに属性を追加したりできる。

XibView.swift
import UIKit

class XibView: UIView {
  override init(frame: CGRect){
    super.init(frame: frame)
    loadNib()
  }

  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)!
    loadNib()
  }

  func loadNib(){
    // xibファイル読み込み
    guard let view = Bundle.main.loadNibNamed("View2", owner: nil, options: nil)?.first as? UIView else { return }
    view.frame = self.bounds
    self.addSubview(view)
  }

}

使い方例

    let subView = XibView(frame: CGRect(x: 50, y: 50, width: 400, height: 700))
    self.view.addSubview(subView)

storyboard分割

Storyboard Referenceを用いてstoryboardを分割し、参照することができる。
image.png

分割したい範囲をまとめて選択し、「Editor」→「Reference to Stroryboard」を選択し、新しいstoryboardを名前をつけて保存。
image.png
選択した範囲だけで構成される storyboardができた。
image.png
分割元の方では参照するstoryboardが表示されている。
image.png

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

UIViewControllerのレイアウトに関する処理

趣味でiOSアプリ開発をかじっていた自分が、改めてiOS開発を勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。3章を読んでの内容になります。

UIViewControllerとレイアウトをサポートするクラス

UIViewにおけるレイアウトのサイクル

制約の更新
フレームの更新
レンダリング

の3ステップ。

制約の更新

制約が変更されるとレイアウトにおけるコンポーネントの位置関係が更新されるので、レイアウトエンジンがその条件を満たすための再計算を行う。
UIViewのupdateConstrains()が呼ばれ子から親ビュー(ボトムアップ)に制約の計算が行われる。

制約の更新の条件
・制約のacticeフラグによる有効・無効化。
・制約の優先度変更
・制約の変更・追加・削除。
・制約を与えられたビューの階層変更。

明示的に制約を更新する方法
・self.updateConstraintsIfNeed()を呼ぶ。呼んだ瞬間更新される。
・setNeedUpdateConstraints()を呼ぶ。制約を更新する必要があるフラグが立ち、次のレイアウトパスで再計算される。

フレームの更新

制約情報が更新されると、レイアウトを更新するためにレイアウトエンジンが計算したフレーム情報をビューが受け取り、layoutSubviews()が呼ばれ、親から子(トップダウンに)にフレームの更新が実施される。またlayoutSubviews()が呼ばれるとupdateConstraintsIfNeedが呼ばれる。

フレーム更新を引き起こす条件の例
・ビューのフレームの変更。
・サービューの追加・削除。
・UIScrollviewのサブクラスにおいてcontentOfSetが変更された時。

明示的にフレームを更新する方法
・self.layoutIfneeded()。
呼んだ瞬間更新される。
・setNeedsLayout()を呼ぶ。フレームを更新する必要があるフラグが立ち、次のレイアウトパスで再計算される。

レンダリング

更新されたフレーム情報をレンダリングする。ディスプレイに変更を表示するためにdrawRectメソッドが呼ばれる。

レンダリングのタイミング
・ビューの一部を隠している別のビューの移動または削除。
・hiddenになっていたビューの再表示。
・ビューの画面外までスクロールし、再び画面内に戻す。
・ビューのsetNeedDisplayメソッドまたはsetNeedsDisplayInRect(_ :)メソッドの明示的な呼び出し。

ViewControllerのライフサイクル

ViewController生成
loadView()
viewDidLoad()
viewWillAppear()
viewWillLayoutSubviews()
viewDidLayoutSubviews()
viewDidAppear()
表示完了
viewillDisappear()
viewDidDisappear()
ViewController解放

loadView()

ビューをメモリに読み込み、対象のポインタと関連付けを行うメソッド。
storyboardが存在する場合、storyboardで定義されたビューがセットされる。
storyboardを用いずにカスタムビューを定義する場合はloadviewをオーバーライドし、設定を行う必要がある。

  override func loadView() {
    // 親ビューの定義
    let contentView = UIView(frame: UIView().bounds)
    contentView.backgroundColor = UIColor.black
    self.view = contentView

    // addSubViewでボタン追加とか制約追加したり
 }

viewDidLoad

loadViewが完了した時に呼ばれる。
loadViewで読み込みが完了したオブジェクトにプロパティをセットしたり、クラス内で用いるオブジェクトの初期化をする。

viewWillAppear()

ビューが表示される直前に呼ばれる。
バックグラウンドからアプリを開き直したり、UITabBarControllerにおけるタブの切り替え時も呼ばれる。
この段階ではユーザにUIは提供されていないので計算コストが高い処理は避ける。

viewWillLayoutSubView()

ビューのレイアウト開始時に呼ばれる。
ビューコントローラが読み込まれた時だけでなく、端末の回転やビューの再表示によってビューが新しい大きさになった時に必ず呼ばれる。

viewWillLayoutSubView()が呼ばれた後、ビューコントローラーのレイアウトが開始される。
このタイミングで、UIViewのレイアウトサイクルでいう制約の更新、フレームの更新が行われる。

viewDidLayoutSubviews()

ビューのレイアウトが完了した時に呼ばれる。
この時点で、UIViewのレイアウトサイクルでいうフレームの更新が行われているので、ビューコントローラーの持つビュー内のレイアウトが確定する。

viewDidAppear()

ビューが表示された直後に呼ばれる。
この時点で、UIViewのレイアウトサイクルでいうレンダリングが行われている。
UIがユーザに提供されているのでUXに直接影響する処理はしない。
viewWillAppear()と対になっているため、バックグラウンドから復帰したり、UITabBarControllerにおけるタブの切り替え時も呼ばれる。

viewillDisappear

ビューが階層から削除されたときに呼び出される。
このメソッドをオーバーライドしてビューの非表示、非表示に関する追加の処理を実行できる。

viewDidDisappear

ビューが階層から削除される前に呼び出される「。
このメソッドをオーバーライドして編集の変更をコミットしたり。削除に関連する処理を実行できる。

UIWindow

UIWindow : ディスプレイにおけるウィンドウの役割を担っている。
例えばキーボードやUIAlertViewは新しいウィンドウを生成して必要な部品を生成している。

keyboard表示時にデバックした時の画面

image.png

Windowの重なり順

UIWindowのwindowLevelプロパティの値が大きいものが上に表示される。
UIKitではNormal,Alert,StatusBarの3種類が定義されており、アラートの値が一番大きい。

UIWindowLevelNormal == 0.0
UIWindowLevelAlert == 2000.0
UIWindowLevelStatusBar == 1000.0

Windowの画面サイズ

UIScreenサイズを取得する方法
この方法で画面のサイズは取得できる。

    let screenSize = UIScreen.main.bounds

しかしiOS9.0からiPadにマルチタスク機能が搭載されたので、スクリーンサイズとウィンドウサイズが違う場合がでてきた。
よってWindowのサイズを取得する事で正確な値をとる。

    let winodwSize = UIApplication.shared.keyWindow?.bounds

iPadでマルチタスクにしたとき、オレンジの枠がscreenSizeで赤の枠がwindowSizeとなる。

image.png

UIStackViewとAutoLayout

UIStackViewの中にオブジェクトを並べると自動で並べてくれるが、このオブジェクトには自動的に制約が付与されている。

stackViewにのみ制約をつけて、中にLabel、UIimage、UITextViewを入れる。
表示する画像を決めると中のオブジェクトの制約は自動的に決まる。

image.png

オブジェクトの配置方法はUIStackViewのプロパティによって決まる。

image.png

Spacing : オブジェクト間の距離
Axis : 並べる方向。水平か垂直。
Alignment : 揃え方。軸方向での中央揃え、左揃え、右揃え、大きさを最大にして並べる(Fill)。
(下記の例では垂直方向の例しか取り扱っていないが水平方向の場合そのまま横にしてみたときと同じ)

Fillの場合
image.png

Distribution : 並べ方

Fill : Intrinsic Content Sizeを満たしながら、軸方向にサブビューを並べる。(コンテンツの大きさに合わせて幅が決まる)
image.png

Fill Equally : 軸方向に均等に高さを指定して並べる。
image.png

Fill Proportionaally : Intrinsic Content Sizeの比率に合わせた高さで、軸方向にサブビューを並べる。(コンテンツの比率に合わせて幅が決まる)

image.png

Equal Spacing : Intrinsic Content Sizeを変更せずに、軸方向におけるサブビューのスペースを均一にして並べる。

image.png

Equal Centering : Intrinsic Content Sizeの大きさのサブビューたちの中心距離が均一になるように並べる。

image.png

FillとFill Proportionaallyの違い
stackViewの高さをコンテンツが入らない範囲で固定にすると、Fillの方は高さの制約が必要になるのに対して、Fill Proportionaallyは比率に合わせてコンテンツを小さくした。

Fill
image.png
Fill Proportionaally
image.png

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

Auto Layoutの基本

趣味でiOSアプリ開発をかじっていた自分が、改めてiOS開発を勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。1,2章を読んでの内容になります。

Adaptivityなレイアウトを実現する

画面サイズの多様化により、どのようなウィンドウサイズでも対応できる柔軟性のあるレイアウトが求められるようになった。ウェブ世界のレスポンシブレイアウトとほぼ同じ意味合いで用いられます。

Auto Layoutやトレイコレクションという概念。

Auto Layout : コンテンツをベースとしたレイアウト方法。
トレイトコレクション : 画面情報を抽象的なオブジェクトとして扱う概念。サイズクラスなど。

サイズクラス

画面の高さと幅を抽象的に示す。

端末と向き 高さ
iPad縦 Regular Regular
iPad横 Regular Regular
iPhone縦 Regular Compact
iPhone横 Compact Compact
iPhonePlus縦 Regular Compact
iPhonePlus横 Compact Regular

Auto Layoutの基本概念

制約式

Auto Layoutは制約を表した数式である制約式の連立方程式を解くことでレイアウトを決定している。

例1

画像のButton2はButton1から50pt下の座標にある。
image.png

この時の制約式

Button2.top = Button1.bottom + 50

storyboardのContaintsで制約の式が観れる。
image.png

例2

緑のViewの上端と右端、左端を親ビューに固定し、緑ビューと親ビューの下端に20%のマージンを置く。

image.png

この時の制約式

greenView.top = superView.top
greenView.left = superView.left
greenView.right = superView.right
greenView.bottom = 0.8 * superView.bottom

これらの線形式を一般化すると次の式になる。
y = ax + b

Auto Latoutでは上記の式を複数持つ線型方程式を解いている。

Auto Layoutの基本概念

オブジェクトの要素

image.png

Leading, Trailing

日本語や英語ではRight, Leftと同値だが、アラビア語などではLeftとRightが反転してしまうので。基本的にはこちらが使われる。

Center X, Center Y

x座標,、y座標。

Baseline

テキストを整列するための基準線。文字列最初のBaselineのみFirstBaselineで示される。
gなどBaselineをはみ出す文字ように空白を調整したりする時など使える。

NotAnAttribute

比較オブジェクトがない場合も要素を渡す必要があるので、代わりにNotAnAttributeを使う。

Relation - 要素の関連性

Relationは制約式 y = ax +b の「=」の部分を示している。

enum NSLayoutReration : Int {  case LessThanOrEqual    // 制約より小さな値でレイアウトを実施
  case Equal    // 制約通りの...
  case GreaterThanOrEqual    // 制約より大きな...
}

Priority - 優先度

1~1000の優先度を設定することができる。標準の値は1000で最大。

image.png

Intrinsic Content Size - 固有の寸法

UIButtonなどの文字列の長さ、フォントのサイズによって大きさを変えるオブジェクトに使われている。
値を取得することも可能。

Content Hugging Priority, Content Compression Resistance Priority - コンテンツに対する優先度

「大きくなりにくさ」と「小さくなりにくさ」を定義している
vertical : 縦方向
horizontal : 横方向

storyboardから設定ができ、それぞれ優先度を指定できる。
優先度より低い制約では大きさが変わらない。

image.png

image.png

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Composite~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Compositeパターン概要

  • ディレクトリとファイルのような、ツリー構造を扱うためのパターンです。
  • ディレクトリとファイルのように、「名前」などの同じプロパティや、「削除」などの同じ操作を持つ場合、ディレクトリ(容器)とファイル(中身)を同じように扱うことができます。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

  • そのものズバリ、ツリー構造を扱う場合には、Compositeパターンを思い浮かべると設計に掛かる時間を短縮できる可能性が高いです。
  • UIViewのView Hierarchyもツリー構造であり、Compositeパターンが使われています。

view_hierarchy.gif
引用:Cocoa Design Patterns (Retired Document)

サンプルコード

Swiftバージョンは 5.1 です。

protocol DirectoryEntry {
    var name: String { get }
    func remove()
}

final class File: DirectoryEntry {
    let name: String

    init(name: String) {
        self.name = name
    }

    func remove() {
        print("\(name)を削除しました")
    }
}

final class Directory: DirectoryEntry {
    let name: String
    private var entryList = [DirectoryEntry]()

    init(name: String) {
        self.name = name
    }

    func add(entry: DirectoryEntry) {
        entryList.append(entry)
    }

    func remove() {
        for entry in entryList {
            entry.remove()
        }
        print("\(name)を削除しました")
    }
}

// Usage
let dir1 = Directory(name: "dir1")
let file1 = File(name: "file1")
dir1.add(entry: file1)
// dir1
//   ∟file1

let dir2 = Directory(name: "dir2")
let file2 = File(name: "file2")
let file3 = File(name: "file3")
dir2.add(entry: file2)
dir2.add(entry: file3)
// dir2
//   ∟file2
//   ∟file3

dir1.add(entry: dir2)
// dir1
//   ∟file1
//   ∟dir2
//     ∟file2
//     ∟file3

let file4 = File(name: "file4")
dir1.add(entry: file4)
// dir1
//   ∟file1
//   ∟dir2
//     ∟file2
//     ∟file3
//   ∟file4

dir1.remove()
// "file1を削除しました"
// "file2を削除しました"
// "file3を削除しました"
// "dir2を削除しました"
// "file4を削除しました"
// "dir1を削除しました"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Singleton~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Singletonパターン概要

  • インスタンスが1個しか生成されないことを保証するためのパターンです。
  • GoFのデザインパターンでは生成に関するパターンに分類されます。

使い所

  • iOSフレームワーク内で随所に使われています。典型例は UIApplication.shared です。
  • 実行時に依存性を変更しづらく、ユニットテストがしにくいというデメリットがあります。
  • またグローバル変数と同様に、いつ・どこから・どのように変更されるのか理解が難しくなりますので、注意しながら利用する必要があります。

サンプルコード

Swiftバージョンは 5.1 です。

class SingletonClass {
    class var shared : SingletonClass {

        struct Static {
            static let instance : SingletonClass = SingletonClass()
        }

        return Static.instance
    }
}

// Usage
let instance = SingletonClass.shared

※少々回りくどい実装になっていますが、マルチスレッドでインスタンスが複数生成されてしまうことを回避するための実装例です。

引用:
https://github.com/07cs07/Design-Patterns-In-Swift

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

bazelのiosチュートリアルの修正点

チュートリアルの左上にあるボタンからmasterに切り替えればよかった

ーーーーー

基本的にはこの手順で良い
https://docs.bazel.build/versions/2.0.0/tutorial/ios-app.html

ただ、依存ライブラリが古いまま(2020/1/3現在)なので若干記法などが異なる。
チュートリアルの中で出てくるWORKSPACEとBUILDを次のように変更すればとりあえずビルドまでは行ける。

WORKSPACE
workspace(name = "tutorial")

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
    name = "bazel_skylib",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.0.2/bazel-skylib-1.0.2.tar.gz",
        "https://github.com/bazelbuild/bazel-skylib/releases/download/1.0.2/bazel-skylib-1.0.2.tar.gz",
    ],
    sha256 = "97e70364e9249702246c0e9444bccdc4b847bed1eb03c5a3ece4f83dfe6abc44",
)
load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
bazel_skylib_workspace()

# iOS basic build deps.

http_archive(
    name = "build_bazel_rules_apple",
    sha256 = "bdc8e66e70b8a75da23b79f1f8c6207356df07d041d96d2189add7ee0780cf4e",
    strip_prefix = "rules_apple-b869b0d3868d78a1d4ffd866ccb304fb68aa12c3",
    url = "https://github.com/bazelbuild/rules_apple/archive/b869b0d3868d78a1d4ffd866ccb304fb68aa12c3.tar.gz",
)

load(
    "@build_bazel_rules_apple//apple:repositories.bzl",
    "apple_rules_dependencies",
)

apple_rules_dependencies()

load(
    "@build_bazel_rules_swift//swift:repositories.bzl",
    "swift_rules_dependencies",
)

swift_rules_dependencies()

load(
    "@build_bazel_apple_support//lib:repositories.bzl",
    "apple_support_dependencies",
)

apple_support_dependencies()

BUILD
load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")

objc_library(
    name = "UrlGetClasses",
    srcs = [
         "UrlGet/AppDelegate.m",
         "UrlGet/UrlGetViewController.m",
         "UrlGet/main.m",
    ],
    hdrs = glob(["UrlGet/*.h"]),
    data = ["UrlGet/UrlGetViewController.xib"],
)

ios_application(
    name = "ios-app",
    bundle_id = "Google.UrlGet",
    families = [
        "iphone",
        "ipad",
    ],
    minimum_os_version = "9.0",
    infoplists = [":UrlGet/UrlGet-Info.plist"],
    visibility = ["//visibility:public"],
    deps = [":UrlGetClasses"],
)

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