ttlog

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

TestFlightで「輸出コンプライアンスがありません」を表示されないようにする方法

TestFlightにアプリをアップロードした際に表示されるこの質問、fastlaneでアップロードする際にも表示され、自動でアプリが配信されなくなってしまうのですが、予めプロジェクト設定を追加しておくことで表示されないようにすることが可能です。

設定方法

f:id:tommy10344:20200429020942p:plain

Info.plist から ITSAppUsesNonExemptEncryption (表示名: App Uses Non-Exempt Encrption) を NO に設定します。 この設定により、暗号化を使用していない、もしくは免除の対象になる暗号化のみを使用していることを示します。 (HTTPSや、OSに組み込まれた標準の暗号化については免除の対象になるみたいです)

免除の対象にならないような独自の暗号化を含む場合は「YES」を設定し、輸出コンプライアンス書類を提出してAppleから受け取ったキーを ITSEncryptionExportComplianceCode に設定する必要があるようです。

参考

SwiftUIで共有メニューを表示する

アプリのコンテンツをメールやSNS等の外部サービスに渡す共有メニューをSwiftUIで表示する方法です。

f:id:tommy10344:20191105213901p:plain

共有メニュー表示用Viewの定義

UIKitではUIActivityViewControllerを使用して表示するこの共有メニューですが、現状SwiftUIで特に専用のViewがある訳ではないようです。 そのため、UIViewControllerRepresentableプロトコルに準拠させる形でUIActivityViewControllerをSwiftUI上に表示出来るようにします。

struct ActivityView: UIViewControllerRepresentable {

    let activityItems: [Any]
    let applicationActivities: [UIActivity]?

    func makeUIViewController(
        context: UIViewControllerRepresentableContext<ActivityView>
    ) -> UIActivityViewController {
        return UIActivityViewController(
            activityItems: activityItems,
            applicationActivities: applicationActivities
        )
    }

    func updateUIViewController(
        _ uiViewController: UIActivityViewController,
        context: UIViewControllerRepresentableContext<ActivityView>
    ) {
        // Nothing to do
    }
}

共有メニュー表示

表示の際はsheet()を使用し、先ほど作成したUIViewControllerRepresentableプロトコルに準拠したViewを返すようにすれば共有メニューが表示されます。

@State private var showActivityView: Bool = false

var body: some View {
    ...
    Button(action: {
        self.showActivityView = true
    }) {
        Image(systemName: "square.and.arrow.up")
    }
    .sheet(isPresented: self.$showActivityView) {
        ActivityView(
            activityItems: ["abc"],
            applicationActivities: nil
        )
    }
    ...
}

参考

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);
                }
            )
        )
    );
  }
}

参考