20190830のiOSに関する記事は9件です。

Flutter 1.7.x 環境をmacOS上に構築する

なぜこの文章を書いているか?

この文章(Flutter 1.2.x 環境をmacOS上に構築する)を過去に書き間が空いたのと再度Flutterを触り始めたので環境をまっさらな状態から作り直した作業ログとしてまとめたのが以下です。最近はLaravelとかNuxt.jsとかTypeScriptとかいじってますが、そろそろ他のものもということで。
今回はできる限り公式サイトの順番で環境を作っていこうと思います。
https://flutter.dev/docs/get-started/install/macos

環境(2019/08/30(金)現在)

  • macOS Mojave(10.14.6)
  • Xcode 10.3
  • IntelliJ IDEA Ultimate 2019.2.1
  • Flutter 1.7.8 + Hotfix 4 Stable

インストール

Tools

公式サイトに書かれている必須ツールは以下の通り。

  • bash
  • curl
  • git 2.x
  • mkdir
  • rm
  • unzip
  • which

このうち、macOS Mojaveに標準で入っていないのはgitなのでインストールする。

command
$ xcode-select --install

「"xcode-select"コマンドを実行するには、コマンドライン・デベロッパ・ツールが必要です。ツールを今すぐインストールしますか?」とウィンドウが表示されたらインストールをクリック。
「Command Line Tools使用許諾契約」画面が表示されたら内容を読んだ上で同意するをクリック。

インストールが完了したら以下でバージョンが表示されればOK。

command
$ git --version
git version 2.20.1 (Apple Git-117)

Flutter SDKのインストール

公式サイトのflutter_macos_v1.7.8+hotfix.4-stable.zipと書かれている青いボタンをクリックしてZipファイルをダウンロードします。
ダウンロードしたら、~/development以下に展開します。

command
$ mkdir ~/development
$ cd ~/development
$ unzip ~/Downloads/flutter_macos_v1.7.8+hotfix.4-stable.zip

~/development/flutter/bin/が作られるのでPATHを通します。

command
$ echo 'export PATH=$PATH:~/development/flutter/bin' >> ~/.profile
$ exec $SHELL -l

以下でバージョン情報が表示されればOK。

command
$ flutter --version
Flutter 1.7.8+hotfix.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 20e59316b8 (6 weeks ago) • 2019-07-18 20:04:33 -0700
Engine • revision fee001c93f
Tools • Dart 2.4.0

以下でバイナリをダウンロード(キャッシュ)しておきます。瞬間で終わるので実際に何か行われている感じはしないのですが、念の為。

command
$ flutter precache

Flutterの実行に必要な環境が揃っているか以下でチェックします。
途中「"gen_snapshot"はこのMac用に最適化されていないため、アップデートが必要です。」と出ますのでOKをクリックします。

command
$ flutter doctor -v
[✓] Flutter (Channel stable, v1.7.8+hotfix.4, on Mac OS X 10.14.6 18G95, locale
    ja-JP)
    • Flutter version 1.7.8+hotfix.4 at /Users/foobar/development/flutter
    • Framework revision 20e59316b8 (6 weeks ago), 2019-07-18 20:04:33 -0700
    • Engine revision fee001c93f
    • Dart version 2.4.0

[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
      Install Android Studio from:
      https://developer.android.com/studio/index.html
      On first launch it will assist you in installing the Android SDK
      components.
      (or visit https://flutter.dev/setup/#android-setup for detailed
      instructions).
      If the Android SDK has been installed to a custom location, set
      ANDROID_HOME to that location.
      You may also want to add it to your PATH environment variable.


[✗] Xcode - develop for iOS and macOS
    ✗ Xcode installation is incomplete; a full installation is necessary for iOS
      development.
      Download at: https://developer.apple.com/xcode/download/
      Or install Xcode via the App Store.
      Once installed, run:
        sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin
        code that responds to your plugin usage on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To install:
        brew install cocoapods
        pod setup
    ! Brew can be used to install CocoaPods.
      Download brew at https://brew.sh/.

[✗] iOS tools - develop for iOS devices
    ✗ libimobiledevice and ideviceinstaller are not installed. To install with
      Brew, run:
        brew update
        brew install --HEAD usbmuxd
        brew link usbmuxd
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    ✗ ios-deploy not installed. To install:
        brew install ios-deploy
    ! Brew can be used to install tools for iOS device development.
      Download brew at https://brew.sh/.

[!] Android Studio (not installed)
    • Android Studio not found; download from
      https://developer.android.com/studio/index.html
      (or visit https://flutter.dev/setup/#android-setup for detailed
      instructions).

[!] Connected device
    ! No devices available

! Doctor found issues in 5 categories.

色々と未インストール(環境未整備)だと怒られているので順に入れていきます。

Xcodeをインストール

App Storeを起動し、検索窓にxcodeと入力して検索しインストールします。時間がかかるので私は一晩寝かしました。

すでにコマンドラインツールはインストール済みですが、公式ドキュメントのとおりにインストールしたXcodeから設定しておきます。

command
$  sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer

Xcodeのライセンス承諾を行うために以下のコマンドを実行します。Enterを押した後、Spaceキーで最後まで読み、入力を求められたらagreeとキー入力してEnterを押してライセンス条項に対し承諾をします。

command
$ sudo xcodebuild -license

この状態で以下のコマンドが実行できgitのバージョンが表示されればOKです。

command
$ git --version
git version 2.20.1 (Apple Git-117)

iOSシミュレータの設定

以下のコマンドを実行しiOSシミュレータを起動します。

command
$ open -a Simulator

Hardware > Device > Manage Devicesを選択し、「Install additional required components?」画面が出たらInstallをクリックし、パスワードの入力画面が出るのでMacのアカウントのパスワードを入力しOKをクリックします。

インストールが起動したらXcodeが起動するので、一旦iOSシミュレータを終了して再度起動します。私の環境ではiPhone XRのシミュレータが自動的にスタートしました。公式ドキュメントには「64bitのiPhone5S以降をセットアップしてね」とありますが現在のiOSシミュレータではこれ以上特に作業は必要ありません。

テストアプリを作ってみる

公式ドキュメントではここで唐突にテストアプリを作る項目が出てきます。iOSシミュレータがセットアップできているかなどのチェック用でしょうか?ここでは言われたとおりに作ってみることにします。

ターミナルで適当な場所に作業用ディレクトリを作成し、そこで作業を進めます。

command
$ mkdir ~/development/project
$ cd ~/development/project
$ flutter create my_app
$ cd my_app
$ flutter run
No connected devices.

Run 'flutter emulators' to list and start any available device emulators.

If you expected your device to be detected, please run "flutter doctor" to
diagnose
potential issues, or visit https://flutter.dev/setup/ for troubleshooting tips.

怒られました。どうやらiOSシミュレータを終了していたためのようです。iOSシミュレータを起動しiPhone XRのシミュレータが起動している状態でもう一度flutter runを実行してみます。
Flutter Demo Home Pageという画面がシミュレータに表示されればOKです。ターミナルでqを入力するとアプリが終了します。

iOSデバイス(実機)にデプロイするための設定

Homebrewをインストールするように書かれているのでそのとおりにします。Qiitaを読んでらっしゃるMacユーザならインストール済みと思われますので(偏見)説明は省きます。

command
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew update

iOSデバイス(実機)にデプロイするために必要なツール群をbrewコマンドを使ってインストールしていきます。私の環境ではbrew link usbmuxdで「リンク済み」の旨のメッセージが出ましたが特に問題ないと思われます。

command
$ brew install --HEAD usbmuxd
$ brew link usbmuxd
$ brew install --HEAD libimobiledevice
$ brew install ideviceinstaller ios-deploy cocoapods
$ pod setup

先ほど作ったテストアプリをiOSデバイスにデプロイできるようにするためにプロジェクトにサインをします。基本公式ページのとおりですが簡単にまとめます。

  1. ターミナルでopen ios/Runner.xcworkspaceとしてXcodeを開きます
  2. iOSデバイスをMacに接続して「信頼するか?」など聞かれたら通常の方法でMacとiOSデバイスが通信できる状態にします
  3. Xcodeの左上のプルダウンがRunner > iPhone XRとなっていたら、プルダウンをクリックして接続したiOSデバイスを選択します
  4. 左ペインの一番上にあるRunnerプロジェクトをクリックして選択します
  5. Xcode 9と10は真ん中のペインのGeneral > Signing > Teamを、Xcode 11はSigning & Capabilities > TeamにあるTeamメニューに自分のチーム(Apple ID)を設定します
  6. 自分の環境では「You currently don't have access to this membership resource. To resolve this issue, agree to the latest Program License Agreement in your developer account.」と出ていたのでDev Centerにアクセス、その後accountにメニューから移動し「The Apple Developer Program License Agreement has been updated.」と赤背景でメッセージが出ているところから「Review Agreement」をクリック、内容を確認しチェックボックスをチェック、「I Agree」ボタンをクリック、最後にXcodeに戻ってエラーメッセージの下の「Try again」ボタンをクリックしました
  7. iOSシミュレータを終了した上でターミナルからflutter runを実行
  8. 先ほどと同じFlutter Demo Home PageがiOSデバイスで表示されたらOKです

Android Studioの代わりにIntelliJ IDEA Ultimateをインストールする

公式サイトではAndroid Studioをインストールしていますが、私はIntelliJ IDEAを普段使っているのでそちらで利用可能なように設定します。

まず、Homebrew Caskを使ってJetBrains ToolBoxをインストールします。

command
$ brew cask install jetbrains-toolbox

ApplicationからJetBrains ToolBoxを起動し、JetBrainsアカウントでログイン後、IntelliJ IDEA Ultimateをインストールします。
インストールが終わったらIntelliJ IDEAを起動します。初期設定の時にAndroid開発の追加インストールがされているはずですので、Welcome画面からCreate New Projectを選択し、New Project画面でAndroidを選択するとConfigure Android SDK画面が表示されるのでInstall SDKボタンをクリックします。
Nextをクリックしていき最後にFinishをクリックすると${USER_HOME}/Library/Android/sdk/以下にAndroid SDKがインストールされます。Choose your project画面に戻ったら今回はcancelをクリックしてキャンセルします。

Androidデバイスの設定

公式ページを見ながら順に設定していきます。

  1. Androidデバイスの開発者オプションUSBデバッグを有効にします。デバイスやOSのバージョンによりますが通常はビルド番号を何回かタップすると開発者オプションが有効になり、設定 > システム > 開発者オプションからUDBデバッグのオン・オフができます
  2. USBケーブルでMacとAndroidデバイスを接続します。
  3. ターミナルでfultter devicesを実行して接続したデバイスが1 connected device:などと表示されればOKです。表示されない場合やDevice XXXXXXXX is not authorized.と出ている場合は再接続やデバイスの認証、USB設定などを見直してみてください

この状態でターミナルからIntelliJ IDEAを起動し、Androidデバイスにデプロイしてみます。以下でターミナルから開けない場合は手動で~/development/project/my_app/androidをIntelliJ IDEAで開きます。

command
$ cd ~/development/project/my_app
$ idea ./android

最初はプロジェクトのインデックスに時間がかかります。インデックスが終わると私の環境では2つwarningが出ました。

  • Warning: License for package Android SDK Build-Tools 28.0.3 not accepted.
  • Warning: License for package Android SDK Platform 28 not accepted.

ライセンスについて承諾していないのが理由のようです。色々と調べるとAndroid SDKsdkmanagerで承諾することができそうです。とりあえず実行してみます。

command
$ ~/Library/Android/sdk/tools/bin/sdkmanager --licenses
No Java runtime present, requesting install.

おっとJava runtimeのインストールがまだでした。宗教上の理由でanyenvを使います。

command
$ brew install anyenv
$ anyenv install --init
$ echo 'eval "$(anyenv init --no-rehash -)"' >> ~/.profile
$ exec $SHELL -l
$ anyenv install jenv
$ exec $SHELL -l

$ brew tap caskroom/versions
$ brew cask install adoptopenjdk8
$ jenv add $(/usr/libexec/java_home -v 1.8)
$ jenv global 1.8
$ java -version
openjdk version "1.8.0_222"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_222-b10)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.222-b10, mixed mode)

もう一度sdkmanagerを起動します。

command
$ ~/Library/Android/sdk/tools/bin/sdkmanager --licenses

色々とライセンスの承諾を聞かれるのでyを入力していきます。
最後にAll SDK package licenses acceptedと出ればOKです。

BuildタブからBuildを再実行してみるとandroid:successfulとなりWarningは消えました。

Run > Edit configurations...を選択し、左上の+をクリックしてAndroid Appを選択します。Namemy_appに変更、Moduleappを選択しApplyOKをクリックして画面を閉じます。
Run > Run 'my_app'を選択すると、どのデバイスをビルドターゲットにするか聞かれるのでUSBで接続したAndroidデバイスを選択しOKをクリックするとビルドが始まり、自動的にデバイスにデプロイされ、テストアプリが起動します。

Androidエミュレータの設定

IntelliJ IDEAからAndroidエミュレータが動作するように設定します。

  1. Tools > Android > AVD Managerを選択
  2. + Create Virtual Device...をクリック
  3. Phone > Nexus 5Xを選択しNextをクリック
  4. Pieの隣のDownloadをクリックしPieをインストールします
  5. Pieのインストールが終了したらFinishをクリック
  6. 改めてPieを選択しNextをクリック
  7. Finishをクリック
  8. ADV Managerを一旦閉じ、Run > Run 'my_app'を選択
  9. Select deployment target画面にNexus 5Xがあるので選択してOKをクリック
  10. エミュレータが自動で起動します
  11. エミュレータが起動すると自動的にデプロイされ、テストアプリが起動します

落ち穂拾い

fultter doctor -vを実行して問題を確認します。

command
$ flutter doctor -v
(中略)

[!] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at /Users/foobar/Library/Android/sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-29, build-tools 29.0.2
    • Java binary at: /Library/Java/JavaVirtualMachines/adoptopenjdk-12.0.2.jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment AdoptOpenJDK (build 12.0.2+10)
    ✗ Android license status unknown.
      Try re-installing or updating your Android SDK Manager.
      See https://developer.android.com/studio/#downloads or visit https://flutter.dev/setup/#android-setup for detailed instructions.

(中略)

[!] Android Studio (not installed)
    • Android Studio not found; download from https://developer.android.com/studio/index.html
      (or visit https://flutter.dev/setup/#android-setup for detailed instructions).

[!] IntelliJ IDEA Ultimate Edition (version 2019.2.1)
    • IntelliJ at /Users/foobar/Applications/JetBrains Toolbox/IntelliJ IDEA Ultimate.app
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
    • For information about installing plugins, see
      https://flutter.dev/intellij-setup/#installing-the-plugins

(中略)

! Doctor found issues in 3 categories.

Android SDKのライセンスが不明ということなので、flutter doctor --android-licenseを実行してみます。

command
$ flutter doctor --android-licenses
All SDK package licenses accepted.

Android Studioはわざといれていないのでここでは無視します。

IntelliJ IDEAFlutterDartのプラグインが入っていないと言われているのでインストールしてIntelliJ IDEAをリスタートします。

もう一度flutter doctor -vを実行します。

command
$ flutter doctor -v
(中略)
[!] Android Studio (not installed)
    • Android Studio not found; download from https://developer.android.com/studio/index.html
      (or visit https://flutter.dev/setup/#android-setup for detailed instructions).
(中略)
! Doctor found issues in 1 category.

Android Studio以外のエラーが消えました。これで開発環境が揃ったことになります。

 まとめ

前回のインストールログをまとめてから、ちょっとしたサンプルプログラムを作っただけだったので、今度はもうちょっとちゃんとしたものを作りたいなぁと考えています(^^;

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

Bitriseのナンバリングを利用して、バージョンの自動インクリメントを実現(ReactNative, ios)

下記記事でFastlaneをBitriseに組み込み、MyAppへの自動デプロイを実現しました。
FastlaneのコマンドをBitriseに組み込む(ios,ReactNative)
しかし、開発の現場ですとデプロイ毎にバージョン番号を繰り上げて管理したいです。
そこで利用するのが、Bitriseのナンバリング番号です。

android版はこちら↓
Bitriseのナンバリングを利用して、バージョンの自動インクリメントを実現(ReactNative, android)

Bitriseのナンバリング番号とは

Build numbering and app versioning
Bitriseのworkflowが動く際に管理されている番号のことです。
実行毎に番号が繰り上がるため、この番号をバージョン番号として使用しようと思います。

image.png

設定

Bitriseのをworkflowを編集します。
fastlaneフローの前にSet Xcode Project Build Numberを挿入します。
image.png

設定を記述します。
Info.plist file pathにios/アプリ名/info.plist
Build Numberに$BITRISE_BUILD_NUMBER
image.png

これに伴い下記記事で作成したファイルのincrementの機能は不要になるため、該当部分を削除します。
fastfileの編集

ios/fastlane/Fastfile
desc "Push a new release build to the App Store"
  lane :release do
    # increment機能は不要のため、削除
    # increment_build_number(xcodeproj: "ReactNativePlatform.xcodeproj")

    match(type: "appstore")
    build_app(scheme: "ReactNativePlatform")
    upload_to_app_store(
      skip_waiting_for_build_processing: true
    )
    clean_build_artifacts
  end

実行

上記設定でBitriseが動きますと、MyAppにてバージョン番号がBitriseのナンバリング番号と一致しているのが確認できます。
image.png

参考

iOSアプリのビルド番号自動設定をfastlaneまたはBitriseでラクラク実現する方法

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

UICollectionViewでタブブラウザを作ってみた

タブが欲しい!!

 WKWebKitを使用すると非常に簡単にブラウザを作ることができます。ええ。それはいいんですけども、タブがない。うーん、でもタブ欲しいよね。じゃあ、ライブラリはあるのかな。
ない!
...仕方ない。何かで擬似的に作るか。

バージョンとか

Swift5.0

UICollectionViewでタブを擬似的に!

 UICollectionViewならタブを生成してタブがどんどん増えてもスクロールすれば選択できるよね!というわけで、こんな感じに作った。

ViewController
//
//  ViewController.swift
//  CollectionViewTest
//
//  Created by Lily.Mameoka on 2019/08/18.
//  Copyright © 2019 Lily.Mameoka. All rights reserved.
//

import UIKit
import WebKit

let screenSize: CGSize = CGSize(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, WKUIDelegate, WKNavigationDelegate {

    var itemCount = 1
    var selectedIndex: IndexPath = [0,0]
    var selectedNum = 0
    var currentUrl = "https://www.google.com"

    var webView: WKWebView!

    var webTitle: String = "Google"
    var urlArray:[String] = ["https://www.google.com"]
    var webTitleArray: [String] = ["Google"]

    let collectionView: UICollectionView = {
        //セルのレイアウト設計
        let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 0
        let collectionView = UICollectionView(frame: CGRect(x: 0, y: 100, width: screenSize.width, height: 50), collectionViewLayout: layout)
        collectionView.backgroundColor = UIColor.white
        //セルの登録
        collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
        return collectionView
    }()


    //追加ボタンの設定
    func buttonUpDate(){
        let addTabButton:UIButton = UIButton(frame: CGRect(x: 0, y: 50, width: 50, height: 50))
        addTabButton.backgroundColor = .white
        addTabButton.setTitle("+", for: .normal)
        addTabButton.setTitleColor(UIColor(red: 22/225, green: 157/225, blue: 202/225, alpha:0.5), for: .normal)
        addTabButton.addTarget(self, action: #selector(pushAddTabButton), for: .touchUpInside)
        self.view.addSubview(addTabButton)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.dataSource = self
        collectionView.delegate = self
        view.addSubview(collectionView)
        newWeb()
        webView.uiDelegate = self
        webView.navigationDelegate = self
        buttonUpDate()
        closeButtonUpDate()
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {

        if webView.title != nil {
            let webText = webView.title as! String
            print("\(webText)")
            webTitle = "\(webText)"
            webTitleArray[selectedNum] = webTitle
            collectionView.reloadData()
        } else {
            webTitle = webView.url!.absoluteString
            webTitleArray[selectedNum] = webTitle
            collectionView.reloadData()
        }

    }

    //削除ボタンの定義
    func closeButtonUpDate(){
        let tabCloseButton:UIButton = UIButton(frame: CGRect(x: 50, y: 50, width: 150, height: 50))
        tabCloseButton.backgroundColor = .white
        tabCloseButton.setTitle("現在のタブを削除", for: .normal)
        tabCloseButton.setTitleColor(UIColor(red: 22/225, green: 157/225, blue: 202/225, alpha:0.5), for: .normal)
        tabCloseButton.addTarget(self, action: #selector(tapCloseButton), for: .touchUpInside)
        self.view.addSubview(tabCloseButton)
    }
    //cellの個数設定
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return itemCount
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell

        let title = webTitleArray[indexPath.item]
        cell.setupContents(textName: title)

        return cell
    }

    //新規タブの処理
    @objc func pushAddTabButton(sender: UIButton){
        itemCount += 1//add tab
        collectionView.reloadData()//add tab
        currentUrl = self.webView.url!.absoluteString
        urlArray[selectedNum] = currentUrl
        if webView != nil {
            webView.removeFromSuperview()//delete old web
        }
        newWeb()//add new google
        urlArray += ["https://www.google.com"]
        webTitleArray += ["Google"]
        selectedNum = itemCount - 1
        selectedIndex[1] = itemCount - 1
    }

    //タブタップ時の処理
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        currentUrl = self.webView.url!.absoluteString
        urlArray[selectedNum] = currentUrl
        webTitleArray[selectedNum] = webTitle
        selectedIndex = indexPath
        selectedNum = indexPath[1]
        upDateWeb(index: selectedNum)
        collectionView.reloadData()
    }

    //タブ削除の処理
    @objc func tapCloseButton(sender: UIButton){

        if itemCount == 1 && selectedNum == 0 {
            //ラスト1つだけのタブを削除した時→リロード。googleの画面が0番目のタブにセットされる
            if webView != nil {
                webView.removeFromSuperview()//delete old web
            }
            urlArray = ["https://www.google.com"]//urlArrayは初期化
            webTitleArray = ["Google"]
            newWeb()
        } else if selectedNum == 0 && itemCount != 1 {
            //0番目のタブを削除した時→1番目のタブに飛ぶ
            if webView != nil {
                webView.removeFromSuperview()//delete old web
            }
            itemCount -= 1
            collectionView.deleteItems(at: [selectedIndex])
            collectionView.reloadData()
            upDateWeb(index: 1)
            urlArray.remove(at: 0)
            webTitleArray.remove(at: 0)
        } else {
            itemCount -= 1
            collectionView.deleteItems(at: [selectedIndex])
            collectionView.reloadData()
            urlArray.remove(at: selectedNum)//selectされたIndexに対応するURLを削除
            webTitleArray.remove(at: selectedNum)
            //一つ若いタブに格納されたurlでwebview
            selectedNum = selectedNum - 1
            selectedIndex = [0, selectedNum - 1]
            upDateWeb(index: selectedNum)
        }

    }

    func newWeb(){
        webView = WKWebView(frame: CGRect(x: 0, y: 150, width: screenSize.width, height: screenSize.height))
        let urlString = "https://www.google.com"
        let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
        let url = NSURL(string: encodedUrlString!)
        let request = NSURLRequest(url: url! as URL)
        webView.load(request as URLRequest)
        self.view.bringSubviewToFront(webView)
        self.view.addSubview(webView)
        webView.uiDelegate = self
        webView.navigationDelegate = self
    }

    func upDateWeb(index: Int){
        if webView != nil {
            webView.removeFromSuperview()
        }
        print(urlArray)
        print(index)
        webView = WKWebView(frame: CGRect(x: 0, y: 150, width: screenSize.width, height: screenSize.height))
        let urlString = urlArray[index]
        let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
        let url = NSURL(string: encodedUrlString!)
        let request = NSURLRequest(url: url! as URL)
        webView.load(request as URLRequest)
        self.view.bringSubviewToFront(webView)
        self.view.addSubview(webView)
        webView.uiDelegate = self
        webView.navigationDelegate = self
    }

}

//cellのサイズの設定
extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: screenSize.width * 2 / 5, height: 50)
    }
}

CollectionViewCell
//
//  CollectionViewCell.swift
//  CollectionViewTest
//
//  Created by Lily.Mameoka on 2019/08/18.
//  Copyright © 2019 Lily.Mameoka. All rights reserved.
//

import UIKit

class CollectionViewCell: UICollectionViewCell {

    let tabCellLabel: UILabel = {
        let tabCellLabel = UILabel()
        tabCellLabel.frame = CGRect(x: 0, y: 0, width: screenSize.width * 2 / 5, height: 50)
        tabCellLabel.textColor = UIColor(red: 225/225, green: 112/225, blue: 0/225, alpha:1.0)
        tabCellLabel.textAlignment = .center
        return tabCellLabel
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    func setup() {
        layer.borderColor = UIColor.lightGray.cgColor
        layer.backgroundColor = UIColor(red: 22/225, green: 157/225, blue: 202/225, alpha:0.5).cgColor
        layer.borderWidth = 3.0
        contentView.addSubview(tabCellLabel)
        contentView.bringSubviewToFront(tabCellLabel)
    }

    func setupContents(textName: String) {
        tabCellLabel.text = textName
    }
}


AppDelegate
//
//  AppDelegate.swift
//  CollectionViewTest
//
//  Created by Lily.Mameoka on 2019/08/18.
//  Copyright © 2019 Lily.Mameoka. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate{


    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }


}

プロジェクトはこちら

今後

 正直、既存のタブブラウザより不便ではあるので後で改良版を作ります。

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

IOSでいい感じにサイドメニューをスムーズスクロールさせる。

IOSでサイドメニューを開くとスクロールがなんかスムーズじゃない

こういうメニュー、全然スムーズに動いてくれない。
カクカク動く感じ。

image.png

これでスムーズになる。

css style
overscroll-behavior-y: contain;
-webkit-overflow-scrolling: touch;

参考

iOS Safari でスムーススクロールにするやつ
https://developer.mozilla.org/ja/docs/Web/CSS/-webkit-overflow-scrolling
スクロールチェインを制御するやつ
https://developer.mozilla.org/ja/docs/Web/CSS/overscroll-behavior

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

iOS13から位置情報の権限周りの挙動が変わるかも

本記事は公開済みのドキュメントを元に作成しています。今後仕様変更などにより記載内容と異なる場合があります。予めご了承ください。
そして間違ってたところがあったら指摘していただけるとめちゃ嬉しいです

WWDC2019ではSwiftUIやらProject Catalystなど興味深い話題がたくさん出ましたが、その影で(?)iOS13の位置情報権限周りで挙動が結構変わっていたので軽くまとめました。

何が変わったのか

  • 位置情報の許可を求める画面では「常に許可」が選べなくなった
    • 代わりに「1度だけ許可する」が追加された
  • requestAlwaysAuthorizationを呼び出した時の挙動が変わった
    • 最初のポップアップで「使用中のみ許可」を選択すると、「仮の常に許可」が帰ってくるようになった
  • モニタリング系APIは「常に許可」である必要がなくなった
    • Beaconや大まかな位置情報の利用ハードルが若干低くなった。

つまり?

「常に許可」のハードルが高くなった

位置情報の許可を求める画面で「常に許可」が選べなくなった

iOS12では位置情報を取得する際に表示される選択肢は「常に許可」「アプリを使用中のみ許可」「拒否」の3つだったのですが、
iOS13からは「アプリを使用中は許可」「1度だけ許可」「拒否」となり、「常に許可」が選べなくなりました。
705_whats_new_in_core_location-08.png
What's New in Core Locationより引用

それぞれを選択した時の挙動も今までと異なります。

アプリを使用中は許可を選択した場合

リクエストする権限によって挙動が異なります。

requestWhenInUseAuthorizationで呼び出した場合

いつも通りアプリが使用中のみ位置情報の取得が可能です。
アプリ側には.authorizedWhenInUseとしてステータスが返ります。

requestAlwaysAuthorizationで呼び出した場合

位置情報の試用期間に入ります。
ユーザーは「アプリを使用中は許可」として選択していますが、アプリ側には.authorizedAlwaysとして返ります。(WWDC2019の動画では、仮の常に許可として説明されています)

iOSはこの試用期間中、どこかユーザーが忙しくないタイミングでユーザーに対して1回だけ「使用中のみ許可」するか「常に許可」するかを聞いてきます。

705_whats_new_in_core_location-14.png
What's New in Core Locationより引用

ここでユーザーは位置情報の取得をアプリ使用中のみにすることも可能ですし、そのままにすることもできます。

ここまで見てわかるように、iOS13では真の「常に許可」にたどり着くまでのハードルが高くなっています。

今までは最初のポップアップで常に選択を選ばせれば良かったのですが、真の常に許可を得るためには1段階手順を踏まないといけないことと、ユーザーの選択次第でアプリを使用中のみに戻される可能性があります。

今まで位置情報の取得は「常に許可」しか許されなかった系アプリ(位置情報共有アプリとかそうですね)は、
アプリを使用中のみでも動作するように修正するか、ユーザーに設定から「常に許可」を選択するよう促す必要がありそうです。

「1度だけ許可する」が押された場合

iOS13から新しく追加された選択肢です。
選択すると、アプリに対しては「アプリを使用中のみ許可」と同じステータス(.authorizedWhenInUse)が返ります。
なので、ステータスからは1度だけ許可しているかどうかは判断ができません。

また一時的な許可としての扱いなので、バックグラウンドに遷移してからしばらくする、あるいは次回起動時には権限が決まっていない状態に戻ります。

注意するべきなのが、バックグラウンドに遷移してからしばらくすると権限が戻る点です。
バックグラウンドで継続的に位置情報を要求している場合(allowsBackgroundLocationUpdates = true)は戻らないので問題ないのですが、
そうでない場合は、位置情報を取得する際に毎回現在の権限をチェックする必要があります。
特に、位置情報の権限をアプリ起動時、特定のタイミングでしかリクエストしていなかった場合は結構つらみが深いですね。。。

結局どういうこと?

705_whats_new_in_core_location-32.png
What's New in Core Locationより引用

こういうことらしいです。

モニタリング系APIは「常に許可」である必要がなくなった

iOS13では常に許可のハードルが高くなってしまった代わりに、今まで常に許可でないと使えなかったリージョン検知や、大まかな位置情報の変更の検知がアプリを使用中のみでも使用できるようになっています。

今まではワンショットで位置情報が欲しい場合は.authorizedWhenInUseを、リージョン系を使うなら.authorizedAlwaysと機能ごとに必要な権限が分かれていましたが、
今回から使いたい機能によってどの権限が必要かを考えなくてよくなりました。

ただし、アプリが使われていないタイミングでも位置情報の取得を行いたい場合は引き続きアプリは常に許可である必要があります。

感想

iOS13やべ〜〜〜〜〜〜

参考

What's New in Core Location
WWDCの動画って日本語字幕で見れるんですね。めちゃくちゃ助かりました。
CLLocationManager - Core Location | Apple Developer Documentation

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

iTunes Search APIを使ったWEBサービスの備忘録

Apple社が提供するiTunes Search APIを使ったWebサービスを作ったのでその際のちょっとした覚書をまとめておく。

作ったサイトはこちら
Bestap -ベスタップ- iOS,Androidの人気アプリのランキング分析サイト

iTunes Search API(https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/) はAppleのiTunes上の様々なメディアを検索することのできるAPI。アプリや音楽はもちろん、映画や電子書籍の検索も可能。
取得できる情報はタイトルや作者名はもちろん、価格や星の数にリリース日、説明文やスクリーンショットまでiTunes上に表示されるほぼ全ての情報をカバーしている。

APIを使用する際にはAPIのパッケージをインストールしたり、キーを登録したりするなどの手続きは不要で、APIのURLにアクセスすればJSON形式のデータを受け取ることができる。

特定のキーワードに関連するアプリを取得するときは
https:// itunes.apple.com/search?term=検索語句

アプリの詳細情報を取得するときは
https:// itunes.apple.com/lookup?id=アプリのID

言語や検索結果の数などはオプションで細かく指定できるので、詳しくは本家のマニュアルや関連サイトをご参照いただきたい。

アイコンやスクリーンショットのURLも網羅されているのでiOSアプリ関連の情報を集めるにはこれひとつあれば完璧といえる。

呼び出し制限は明示的にはないものの、実質的には制限があるという情報もあるので常識的な範囲での呼び出しを心がけたい。

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

コンテンツブロッカー設定がWebViewの異常動作の原因になることがある

対応に地味に時間がかかったので、備忘録として。

TL; DR

  1. コンテンツブロッカーで強力なブロック設定をされると、JavaScript(JS)無効化と同等の状態になることがある
  2. SFSafariViewControllerもその影響を受けるため、異常動作の原因になる

経緯

  1. SFSafariViewcontrollerを利用している画面での不具合について問い合わせがあった
  2. 設定アプリ内でJSが無効化1されていると当該事象が発生することを確認
  3. 対処方法を案内したところ、無効化設定はしていないとの回答
  4. 各種設定の変更を試した結果、コンテンツブロッカーで強力なブロック設定(※)をしていると、JS無効化時と同様の事象が発生
  • ※: Firefox focus2(v8.1.2)の場合、環境設定 > トラッキング防止 > 他の追跡コンテンツをブロック
    • 画面内一番下の項目でデフォルトでオフ。さらに、オンにする際は正常に表示できなくなる危険性を伝えるアラートが出る

対応

このケースの場合、対応はコンテンツブロッカーの無効化をお願いすることのみ
ただし、方法は2通りある

  1. コンテンツブロッカーの設定を一時的に変更してもらう
    • 対象となる項目は使用しているものにより異なるが、JS無効化と同等の状態になるような設定は上述の通り比較的深い階層にある
  2. ページを表示中にリロードボタン長押し > 「コンテンツブロッカーなしで再読み込み」 をタップ
    IMG_5364.png

画面が1ページしかないような場合には後者の方法が簡便だが、複数ページに渡るような導線の場合には、前者の方法での対応が良いと思われる

参考


  1. 設定 > Safari > 詳細 > JavaScript をオフ 

  2. https://apps.apple.com/jp/app/id1055677337 

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

debファイルをダウンロード

Cydia等のインストーラでインストールすればdebファイルを意識する必要がありませんが、気に入ったAppをバックアップしたい場合はdebファイルを保存しておけば安心なこともあると思います。

インストーラがリポジトリからdebファイルをダウロードして、インストールが終わってもリスプリングするまでならdebファイルが削除されずに残ってるので、リスプリングする前に別な場所へ保存しとくという方法もありました。

リポジトリから直接ダウンロードする。

あるJBTweakのdebファイルを探していたら、素敵な記事を見つけたのでメモを残すことにしました。
リポジトリから直接debダウンロード | NEXTi4HACK.com

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

Flutterウィークリー #73

Flutterウィークリーとは?

FlutterファンによるFlutterファンのためのニュースレター
https://flutterweekly.net/

この記事は#73の日本語訳です
https://mailchi.mp/flutterweekly/flutter-weekly-73

※Google翻訳を使って自動翻訳を行っています。翻訳に問題がある箇所を発見しましたら編集リクエストを送っていただければ幸いです。

読み物&チュートリアル

Flutterチュートリアル:従来のショッピングアプリUIをフラッターで簡単に作成

https://cybdom.tech/flutter-tutorial-shopping-app-ui/


新しいUIチュートリアル、今回はショッピングアプリ。

Flutter Inを使用したFlutter GridViewチュートリアル

https://www.gotut.net/flutter-grid-view-tutorial/


FlutterのGridviewを使用して、フェードイン効果のある画像ギャラリーを作成する方法を学びます。

Flutter電卓アプリを構築する

https://itnext.io/building-a-calculator-app-in-flutter-824254704fe6


Kenneth Reillyは、 Flutter段階的に電卓を作成します。

Flutter iOSアプリのキーボード完了ボタンUX

https://blog.usejournal.com/keyboard-done-button-ux-in-flutter-ios-app-3b29ad46bacc


Ramankit Singhは、iOSのキーボードで[完了]ボタンを再現するソリューションを示しています。

ストリームを使用したFlutterでの非同期プログラミング

https://medium.com/@bertoboh/async-programming-in-flutter-with-streams-c949f74c9cf9


Albert ObohによるFlutterでのStreamsの使用の概要

Flutter認証

https://medium.com/@greg.perry/auth-in-flutter-97275b29b550


Greg Perryは、Firebase authでauthパッケージを使用する方法を詳細に分析して示します。

FlutterとCustom Painterを使用してレーダーチャートを作成する

https://medium.com/@d.panaite92/building-a-radar-chart-with-flutter-and-custom-painter-384c005002f9


Dan Panaiteによるこのチュートリアルで、カスタムペインターを使用して独自のレーダーチャートを作成する方法を学びます。

Flutter for the Web — Githubにデプロイする

https://medium.com/flutter-community/flutter-for-the-web-deploy-to-github-da454e4bc079


Flutter webを使用していますか?アコラ・イングのおかげで、あなたの作品を世界に見せてください。 DKB。GithubPagesにデプロイする方法を教えてくれます。

Flutter iOSアプリのアイコンをプログラムで変更する

https://medium.com/flutter-community/programatically-change-ios-app-icon-in-flutter-c9e84bc541a2


iOS 10.3以降、Appleはアプリのランチャーアイコンをプログラムで変更する機能を導入しました。 Alessandro FaveroはFlutterからそれを行う方法を説明しています。

MVCの神社

https://medium.com/flutter-community/shrine-in-mvc-7984e08d8e6b


MaterialサンプルアプリShrineを知っていますか? Greg Perryが、サンプルを取得してMVCアーキテクチャに変更する方法を説明します。

プロバイダーとBLoCでFlutter状態管理を簡素化

https://medium.com/fluttervn/simplify-flutter-state-management-with-provider-and-bloc-dcfad49bedf2


ラムはプロバイダーライブラリをテストしており、いくつかの問題を見つけた後、プロバイダーとBLoCの組み合わせがどのようにうまく機能するかを説明します。

Flutterネットワークリクエストをデバッグする3つの簡単な方法

https://medium.com/flutter-community/three-easy-ways-to-debug-network-requests-in-flutter-53043e898929


Flutter作業するときにネットワーク要求をデバッグするのに役立ついくつかのヒント。

Flutterレスポンシブデザイン:はじめに

https://www.raywenderlich.com/4324124-responsive-design-for-flutter-getting-started


JB Lorenzoは、 Flutterさまざまな画面サイズと向きを処理する方法に関する洞察を提供します。

ビデオ&メディア

Flutter TDDクリーンアーキテクチャコース

https://www.youtube.com/watch?v=KjE2IDphA_U&feature=youtu.be&t=2s


コードをクリーンに保ち、テストすることは、2つの最も重要な開発プラクティスです。 Clean ArchitectureをFlutterプロジェクトに適用する方法を学びます。

Flutter UI | Stadiaアプリのコンセプト

https://www.youtube.com/watch?v=-rqvZfUdSPw&feature=youtu.be&t=6395s


このビデオでは、CustomPainter、カスタムウィジェット、アニメーション、その他を使用して、レスポンシブで適応性のあるレイアウトを作成する方法を説明します。

Flutterアニメーション切り替えボタン

https://www.youtube.com/watch?v=XI3LFEvagBY&feature=youtu.be&t=610s


アニメーション付きのカスタムスイッチボタンを作成する方法を学びます。

Flutter Desktopの使用開始

https://www.youtube.com/watch?v=V09e5nb95lo&feature=youtu.be&t=236s


このビデオでは、 Flutterを使用してデスクトップアプリケーションを作成するための最初の手順を実行する方法を示します。

AdmobをFlutterに追加する方法

https://www.youtube.com/watch?v=xSrNB6ge66Q&feature=youtu.be&t=205s


firebase_admob dartパッケージを使用してフラッターにadmobを追加する方法に関するビデオチュートリアルのコーディング

セマンティクス(今週のFlutterウィジェット)

https://www.youtube.com/watch?v=NvtMt_DtFrQ&list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG&index=48


Semanticsを使用して、アプリのUIにウィジェットに関する情報を注釈するウィジェットを使用して、 Flutterアプリをより包括的にします。

ライブラリ&コード

henriquearthur / flutter_native_splash

https://github.com/henriquearthur/flutter_native_splash

AndroidおよびiOSでスプラッシュスクリーンを追加するためのネイティブコードを自動的に生成します。特定のプラットフォーム、背景色、スプラッシュ画像でカスタマイズします。

ketanchoyal / extended_navbar_scaffold

https://github.com/ketanchoyal/extended_navbar_scaffold

拡張可能なフローティングナビゲーションバーを備えたカスタム拡張足場

simformsolutions / flutter_showcaseview

https://github.com/simformsolutions/flutter_showcaseview

iOSおよびAndroidで機能を紹介できるFlutterプラグイン。

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