ttlog

日々の開発で得た知見の技術メモ。モバイルアプリネタが多いです。

iOSアプリでSystem Imageを使用する(iOS13以降)

iOS13ではSystem ImageとしてSF Symbolsというものが追加され、UIKitのUIImageやSwiftUIのImageから使用出来るようになりました。

使用方法

それぞれ、以下のようにして名前を指定して表示します。

  • UIKit(UIImage)
UIImage(systemName: "xxx")
  • SwiftUI(Image)
Image(systemName: "xxx")

名前の確認方法

ここに指定する名前は以下のリンクからSF Symbolsアプリをダウンロードし、Macにインストールする事で確認出来ます。

Apple Design Resources - Apple Developer

f:id:tommy10344:20191103141401p:plain

SF Symbolsアプリに表示される各アイコンの下にある名前を引数に指定する事で、そのアイコンを使用する事が出来ます。

例えば以下のアイコンの場合、

f:id:tommy10344:20191103141421p:plain

SwiftUIでは以下のような指定になります。

Image(systemName: "square.and.arrow.up")

参考

UINavigationBarの透明化

まずは透明化/解除のExtensionを作成

public extension UINavigationBar {
    /// ナビゲーションバーを透明化する
    func enableTransparency() {
        setBackgroundImage(UIImage(), for: .default)
        shadowImage = UIImage()
    }

    /// ナビゲーションバーを透明化を解除する
    func disableTransparency() {
        setBackgroundImage(nil, for: .default)
        shadowImage = nil
    }
}

↑をUIViewControllerのviewWillAppear/viewWillDisappearで呼び出し

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    navigationController?.navigationBar.enableTransparency()
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    navigationController?.navigationBar.disableTransparency()
}

256bit乱数を生成する方法

256bitに限った話ではないのですが、大きめの乱数を生成する方法です。

乱数の生成は以下のように様々な方法がありますが、

  • RandomNumberGenerator
  • arc4random()
  • arc4random_uniform()
  • rand()
  • random()

いずれにしても最大で64bitまでしか生成出来ないようです。

これ以上の長さの乱数を生成する場合、Security FrameworkのSecRandomCopyBytes()を使用します。

  • 生成
func generate256bitRandom() -> Data? {
    let count = 32  // 32byte = 256bit
    var data = Data(count: count)
    let status = data.withUnsafeMutableBytes { body in
        SecRandomCopyBytes(kSecRandomDefault, count, body.baseAddress!)
    }
    if status == errSecSuccess {
        return data
    }
    return nil
}
  • 呼び出し
print(generate256bitRandom()!.base64EncodedString()) // "oyICfMgwGXUVeML+Lmhb5ixnAB1io+mNI4BlxNYXVaA=" (呼び出し毎に変わる)

let count = 32の部分を調整してあげれば、128bitでも512bitでも生成出来ると思います。

UITextViewのパディングを削除する

UITextViewにはデフォルトで若干のパディングが含まれています。このままだとレイアウトに支障をきたす場合もあるので、パディングを削除する方法を記載します。

コードで削除する

以下のようなUITextViewを定義したとして、

@IBOutlet weak var textView: UITextView!

viewDidLoad()等のタイミングで以下のコードを実行します。

textView.textContainerInset = UIEdgeInsets.zero
textView.textContainer.lineFragmentPadding = 0

Storyboardで削除する

コードを使わず、StoryboardのUser defined runtime attributesを使ってパディングを削除することも出来ます。User defined runtime attributesについての詳細は省きますが、簡単に言うとInterface Builder(のAttributes inspector)で設定出来ないようなViewのプロパティを設定出来る機能です。

以下のように2つのプロパティを設定します。

  • textContainer.lineFragmentPadding
  • textContainerInset

f:id:tommy10344:20190608173727p:plain

サンプルコード

GitHub - tommy10344/UITextViewRemovePadding

Flutterで画面遷移

Flutterにおける画面遷移の基本です。 以下の2通りの遷移方法があるようです。

  • 直接画面を生成して遷移
  • ルーティングを定義して名前で遷移

ここで紹介する方法は、iOS的にはNavigationスタイル(Push/Pop)の画面遷移になります。

概要

Navigatorクラスを使用します。

このクラスはRouteオブジェクトをスタックとして管理するものです。 Flutterでは、一般的に画面だとかページだとか言われるものをRouteと呼ぶそうです。

Navigator.pushもしくはNavigator.pushNamed関数により遷移し、Navigator.pop関数で前画面に戻ります。

画面の定義

まずは遷移用の画面を定義します。

遷移前の画面(FirstScreen)にRaisedButtonを2つ置き、タップ時にそれぞれのパターンで遷移をします。

遷移後の画面(SecondScreen)にはRaisedButtonを1つ置き、タップ時に前画面(FirstScreen)に戻ります。

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('First Screen'),
        ),
        body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                    child: Text('Navigate directly'),
                    onPressed: () {
                      // 直接画面を生成して遷移
                    }
                ),
                RaisedButton(
                    child: Text('Navigate with named route'),
                    onPressed: () {
                      // ルーティングを定義して名前で遷移
                    }
                ),
              ],
            )
        )
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Second Screen'),
        ),
        body: Center(
            child: RaisedButton(
                child: Text('Go back!'),
                onPressed: () {
                  // 前画面に戻る
                }
            )
        )
    );
  }
}

直接画面を生成して遷移

Navigator.push関数を使用し、Routeを指定して遷移します。

RouteにはMaterialPageRouteを指定しておけば、自動でプラットフォーム固有のトランジションアニメーションを実現してくれます。 (むしろ他のRouteをどうやって作るのかはまだ分かってません)

RaisedButton(
    child: Text('Navigate directly'),
    onPressed: () {
      Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => SecondScreen())
      );
    }
),

ルーティングを定義して名前で遷移

MaterialAppの初期化時にルーティングを定義し、 Navigator.pushNamed関数を使用して遷移します。

  • ルーティング定義
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
    
      ...
      
      // 初回起動画面を指定
      initialRoute: '/',

      // ルーティングの定義
      routes: {
        '/': (context) => FirstScreen(),
        '/second': (context) => SecondScreen(),
      },
    
      ...
    
    );
  }
}
  • 遷移処理
RaisedButton(
    child: Text('Navigate with named route'),
    onPressed: () {
      // ルーティングで定義した名前を指定して遷移
      Navigator.pushNamed(context, "/second");
    }
),

前画面に戻る

Navigator.pop関数を使用します。

RaisedButton(
    child: Text('Go back!'),
    onPressed: () {
      Navigator.pop(context);
    }
)

ちなみに、画面のWidgetScaffoldを使用している場合はAppBarの左側に自動で戻るボタンが表示されるので、そちらから戻ることも出来ます。

ソース全体

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => FirstScreen(),
        '/second': (context) => SecondScreen(),
      },
    );
  }
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('First Screen'),
        ),
        body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                    child: Text('Navigate directly'),
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => SecondScreen())
                      );
                    }
                ),
                RaisedButton(
                    child: Text('Navigate with named route'),
                    onPressed: () {
                      Navigator.pushNamed(context, "/second");
                    }
                ),
              ],
            )
        )
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Second Screen'),
        ),
        body: Center(
            child: RaisedButton(
                child: Text('Go back!'),
                onPressed: () {
                  Navigator.pop(context);
                }
            )
        )
    );
  }
}

参考

GitのリモートURLをクリップボードにコピーする(macOS Only)

リモートリポジトリをcloneする際、既にある別のローカルリポジトリからリモートURLをコピー&ペーストして末尾を少し書き換えてcloneするということをよくやるのですが、

  1. git remote -vでURL表示
  2. マウスで選択して⌘+cでコピー

とやっていたので地味に面倒でした。

ということで、コマンド一発でコピー出来るスクリプトRubyで書いてみました。 originブランチのリモートURLをクリップボードにコピーします。

即席&Rubyほぼ初心者なのでかなり適当です。 また、内部でmacOSpbcopyコマンドを使用しているため、macOSでのみ動作します。

#!/usr/bin/env ruby

git_command = 'git remote -v'
git_result = `#{git_command}`
if git_result.empty?
  puts "Failed to run command: '#{git_command}'"
  exit
end

url = git_result.split("\n")
  .select {|line| line.start_with?("origin")}.first
  .split(" ")[1]

`printf #{url} | pbcopy`

puts "Copied '#{url}'"

初めて知りましたが、String.split(" ")(半角スペース1文字)で空白文字を全部分割してくれるんですね。 なんという便利機能...

docs.ruby-lang.org

fastlaneでAdHoc配布用ipaの作成

環境

  • macOS: 10.14.2(Mojave)
  • Xcode: 10.1(10B61)
  • fastlane: 2.112.0
  • Ruby: 2.5.3

laneの定義

gymアクション(もしくはbuild_ios_appアクション)でビルドやパッケージングに関する処理が出来ます。lane名はここではadhocとします。

gym - fastlane docs

また、署名周りの設定はあらかじめXcode上で完了させておきます。確認時はAutomatic Signingを使用しました。

default_platform(:ios)

platform :ios do

  ...

  desc 'Generate IPA file for AdHoc'
  lane :adhoc do
    gym(
      project: 'XXX.xcodeproj',
      configuration: 'Debug',
      scheme: 'XXX',
      clean: true,
      include_bitcode: false,
      output_directory: './build',
      output_name: 'XXX.ipa',
      export_method: 'ad-hoc'
    )
  end
  
  ...
  
end
  • project: プロジェクトファイル(.xcodeproj)のパスを指定。
  • configuration: Build Configuration(Debug/Release)を指定。
  • scheme: Scheme名称を指定。必ずShared指定になっていることを確認すること。
  • clean: ビルド前にクリーンするかどうか。念の為クリーンするようにしておく。
  • include_bitcode: bitcodeを含めるかどうか。AdHoc配布であれば恐らく不要だと思うのでここでは含めない。
  • output_directory: ipaの出力先ディレクトリ。
  • output_name: ipaファイル名。
  • export_method: AdHoc配布なので'ad-hoc'を指定。

もしworkspace(.xcworkspace)を使っている場合は引数のprojectworkspaceに変更すれば良いでしょう。

gym(
  workspace: XXX.xcworkspace`,
  ...
)

実行

$ fastlane adhoc

以上でプロジェクトルート直下にbuildディレクトリが作成され、その中にipaファイル(XXX.ipa)が作成されるかと思います。