20190527のiOSに関する記事は8件です。

Swiftのバージョンアップ手順(Swift 4.2→5.0)

はじめに

個人アプリでSwiftを4.2から5.0にバージョンアップし、そのときの手順をまとめました。

Xcodeの更新

SwiftはXcodeのバージョンと紐付いているため、App StoreからXcodeをアップデートすることで、Swiftをバージョンアップします。
スクリーンショット 2019-05-27 22.06.35.png

ライブラリの更新

ライブラリ管理ツールと、そのツールで管理しているライブラリを更新します。

エラーや警告が発生しない限り、更新しなくても問題ないことが多いですが、私はSwiftのバージョンアップと同時に更新しています。
ただ、Swiftの新バージョンのリリース直後はライブラリが対応していないこともあります。

私はCocoaPodsとCarthageを使っているので、その更新のみ紹介します。
この記事では省略していますが、必要に応じて「Podsfile」や「Cartfile」を書き換えたり、全ライブラリでなく必要なライブラリのみ更新したりしてください。

# CocoaPodsの更新
# 「/usr/local/bin」にインストールしている場合
$ sudo gem update -n /usr/local/bin cocoapods

# CocoaPodsで管理している全ライブラリの更新
$ pod update

# Carthageの更新
# Homebrewでインストールしている場合
$ brew upgrade carthage

# Carthageで管理している全ライブラリの更新
$ carthage update --platform iOS

警告の解消

Xcodeで発生している警告を解消します。
基本的には警告をダブルクリック→自動修正でOKです。

プロジェクトファイルの更新

以下の警告は、プロジェクトファイルを更新することで解消します。

Swift Conversion
Conversion to Swift 5 is available

警告をダブルクリックします。
スクリーンショット 2019-05-26 23.04.34.png

全ターゲットのチェックがONになっていることを確認し、[Next]ボタンをクリックします。
スクリーンショット_2019-05-26_23_05_16.jpg

[Update]ボタンをクリックします。
スクリーンショット 2019-05-26 23.08.01.png

これで修正が完了です。

自動修正の適用後、プロジェクトファイルは以下のように更新されます。(一部のみ抜粋)

project.pbxproj
- objectVersion = 50;
+ objectVersion = 51;
+ LastSwiftMigration = 1020;
- SWIFT_VERSION = 4.2;
+ SWIFT_VERSION = 5.0;

スキーマの更新

以下の警告は、スキーマを更新することで解消します。

Validate Project Settings
Update to recommended settings

警告をダブルクリックします。
スクリーンショット 2019-05-27 22.25.24.png

自動修正の適用後、スキーマは以下のように更新されます。(一部のみ抜粋)

{プロジェクト名}.xcscheme
- LastUpgradeVersion = "1010"
+ LastUpgradeVersion = "1020"

Migrate “English.lproj”

Migrate “English.lproj” (Deprecated)
Pods.xcodeproj

警告をダブルクリックします。
スクリーンショット 2019-05-27 22.38.38.png

[Perform Changes]ボタンをクリックします。
スクリーンショット 2019-05-27 20.22.45.png

[Migrate]ボタンをクリックします。
スクリーンショット 2019-05-27 20.25.21.png

これで修正が完了です。

自動修正の適用後、Git上では更新が確認できませんでした。
私がバージョン管理していないファイルが更新されたようです。

Enable Base Internationalization

Enable Base Internationalization
Pods.xcodeproj

警告をダブルクリックします。
スクリーンショット 2019-05-27 22.42.10.png

[Enable]ボタンをクリックします。
スクリーンショット 2019-05-27 20.26.04.png

これで修正が完了です。

自動修正の適用後、こちらもGit上では更新が確認できませんでした。

ACL(アクセス制御レベル)の省略

親と同等のACLを明記しているメソッドは警告が表示されるようになったようです。

'public' modifier is redundant for instance method declared in a public extension
Replace 'public ' with ''

[Fix]ボタンをクリックして自動修正を適用します。
スクリーンショット 2019-05-26 23.22.17.png

public extension UIViewController {
-   public func showAlert(title: String, message: String, actions: [UIAlertAction]) {
+   func showAlert(title: String, message: String, actions: [UIAlertAction]) {
}

メソッドから public が削除されました。

おわりに

小規模の個人アプリなので、エラーや警告が少なく、自動修正のみで対応が完了しました。

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

PythonistaでGPS

はじめに

PythonistaでGPSが使えるらしいので試してみた。

実装

import location

location.start_updates() # GPSデータ更新を開始
gps_data=location.get_location() # GPSデータを取得する
location.stop_updates()# GPSデータ更新を終了

print(gps_data['latitude'])
print(gps_data['longitude'])

結果

かなり簡単にGPS情報を取得できた。
しかし、このままでは緯度、経度の生データなのでどこなのかはわからない。
住所変換ライブラリもあるらしいが、Pythonistaで使えるのだろうか?

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

Swift5での正方形カメラ

はじめに

 Swift5で正方形の写真を撮影するカメラの作り方を勉強してみました。
 iPhoneのWidthと同じ長さの正方形の写真をカメラロールに保存してみます。

カメラアプリを作る2種類の方法

・UIImagePickerController

 簡単にカメラで撮影するアプリが作れます。正方形で撮影するのは難しそう。

・AVFoundation

 動画を撮影したり、いろいろできる賢いやつです。←今回はこちらです。

実装方法

あらかじめinfo.plistに二つの記述が必要です。

・Privacy - Camera Usage Description
 「カメラを使います」的な内容を記載
・Privacy - Photo Library Additions Usage Description
 「カメラロールに保存します」的な内容を記載

ソースコード

ViewController.swift
import UIKit
import AVFoundation

class ViewController: UIViewController, AVCapturePhotoCaptureDelegate {
    var device: AVCaptureDevice!
    var session: AVCaptureSession!
    var output: AVCapturePhotoOutput!
    var tempImage:UIImage!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        setPicture()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    //カメラ準備
    func setPicture(){
        //セッションを生成
        session = AVCaptureSession()
        //背面カメラを選択
        device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
        //背面カメラからキャプチャ入力生成
        guard let input = try? AVCaptureDeviceInput(device: device) else {
            print("例外発生")
            return
        }
        session.addInput(input)
        output = AVCapturePhotoOutput()
        session.addOutput(output)
        session.sessionPreset = .photo
        // プレビューレイヤーを生成
        let pvSize = self.view.frame.width
        let pvLayer = AVCaptureVideoPreviewLayer(session: session)
        pvLayer.frame = view.bounds
        pvLayer.frame = CGRect(x: 0, y: 90, width: pvSize, height: pvSize)
        pvLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
        view.layer.addSublayer(pvLayer)
        // セッションを開始
        session.startRunning()
        // 撮影ボタンを生成
        let shutterBtn = UIButton()
        shutterBtn.setTitle("◯", for: .normal)
        shutterBtn.titleLabel?.font = UIFont.systemFont(ofSize: 74)
        shutterBtn.contentMode = .center
        shutterBtn.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
        shutterBtn.layer.cornerRadius = 0.5 * shutterBtn.bounds.size.width
        shutterBtn.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
        shutterBtn.setTitleColor(UIColor.black, for: UIControl.State())
        shutterBtn.layer.position = CGPoint(x: view.frame.width / 2, y: self.view.bounds.size.height - 80)
        shutterBtn.addTarget(self, action: #selector(photoshot), for: .touchUpInside)
        view.addSubview(shutterBtn)
        //キャンセルボタンを生成
        let cancelBtn = UIButton()
        cancelBtn.setTitle("×", for: .normal)
        cancelBtn.titleLabel?.font = UIFont.systemFont(ofSize: 32)
        cancelBtn.contentMode = .center
        cancelBtn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
        cancelBtn.setTitleColor(UIColor.white, for: UIControl.State())
        cancelBtn.layer.position = CGPoint(x: 20, y: 40)
        cancelBtn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
        view.addSubview(cancelBtn)
    }
    //撮影
    @objc func photoshot(_ sender: AnyObject) {
        let settings = AVCapturePhotoSettings()
        settings.flashMode = .off
        settings.isAutoStillImageStabilizationEnabled = true
        output.capturePhoto(with: settings, delegate: self)
    }
    //撮影結果・再撮影・保存ボタンの表示
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if error == nil {
            let outputImageView = UIImageView()
            let pvSize = self.view.frame.width
            outputImageView.frame = CGRect(x: 0, y: 90, width: pvSize, height: pvSize)
            outputImageView.image = UIImage(data: photo.fileDataRepresentation()!)?.croppingToCenterSquare()
            tempImage = UIImage(data: photo.fileDataRepresentation()!)?.croppingToCenterSquare()
            self.view.addSubview(outputImageView)
            session.stopRunning()
            //再撮影ボタン
            let retryBtn = UIButton()
            retryBtn.setTitle("再撮影", for:.normal)
            retryBtn.frame = CGRect(x:self.view.frame.width/2 - 150,y:self.view.frame.height - 115,width: 70,height: 70)
            retryBtn.addTarget(self, action: #selector(retryPhoto), for: .touchUpInside)
            view.addSubview(retryBtn)
            //保存ボタン
            let saveBtn = UIButton()
            saveBtn.setTitle("保存", for: .normal)
            saveBtn.frame = CGRect(x:self.view.frame.width/2 + 80,y:self.view.frame.height - 115,width: 70,height: 70)
            saveBtn.addTarget(self, action: #selector(savePhoto), for: .touchUpInside)
            view.addSubview(saveBtn)
        }
    }
    //再撮影
    @objc func retryPhoto(sender:UIButton){
        let subViews = view.subviews
        for subView in subViews {
            subView.removeFromSuperview()
        }
        setPicture()
    }
    //カメラロールへの保存
    @objc func savePhoto(sender:UIButton){
        UIImageWriteToSavedPhotosAlbum(tempImage, self, nil, nil)
        let subViews = view.subviews
        for subView in subViews {
            subView.removeFromSuperview()
        }
        setPicture()
    }
    //キャンセル
    @objc func cancelAction(sender:UIButton){
        //
    }
}

extension UIImage {
    func croppingToCenterSquare() -> UIImage {
        let cgImage = self.cgImage!
        var newWidth = CGFloat(cgImage.width)
        var newHeight = CGFloat(cgImage.height)
        if newWidth > newHeight {
            newWidth = newHeight
        } else {
            newHeight = newWidth
        }
        let x = (CGFloat(cgImage.width) - newWidth)/2
        let y = (CGFloat(cgImage.height) - newHeight)/2
        let rect = CGRect(x: x, y: y, width: newWidth, height: newHeight)
        let croppedCGImage = cgImage.cropping(to: rect)!
        return UIImage(cgImage: croppedCGImage, scale: self.scale, orientation: self.imageOrientation)
    }
}

環境

・Xcode 10.2.1
・Swift 5
・iOS 12.3.1

参考文献

AVCapturePhotoSettings

おわりに

Mahalo

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

Swift 5でユニバーサルリンク(Universal link)でアプリを開いても、何も起きない時

こんにちは、こげばんです
Swift5にバージョンアップした時に起きた問題になりました。

タイトルの通り、
ユニバーサルリンクを開いて、アプリを立ち上げたけど何も画面遷移しないし、動かないくて少し困りました。

  1. 原因
  2. 対処方法
  3. 影響範囲

1. 原因

元々のメソッドは、下記を使っていました。

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
  guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL, let _ = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
    return 
  }
  print(url)
}

buildしながら見てみると、ユニバーサルリンクでアプリに来た時に稼働していないことに気がつきました。
リリース直前に気が付いて、焦っていましたw
Swift5にバージョンアップしたタイミングで、メソッドの呼ばれ方が変わったんだと思います。。。。

2. 対処方法

そこでいろいろ見ていましが、teratailのこちらを参考にしました。
https://teratail.com/questions/171281
下記のように修正しました。

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
  guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL, let _ = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
    return false
  }
  print(url)
}

⬇️

func application(_ application: UIApplication, continue continueUserActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
  guard continueUserActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = continueUserActivity.webpageURL, let _ = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
    return false
  }
  print(url)

}

そしたら動くようになりました!
めでたしめでたし〜

3. 影響範囲

今のところは、ユニバーサルリンクの範囲でそれ以上は発生していない感じです。
何か新しいことがあったら、追記していきたいと思いますmm

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

2FAが必須になった今、CircleCIでfastlaneを使ってdsymのダウンロード/アップロードはできなさそう

タイトルの通りCircleCIでは実現が難しいことがわかったので紹介します。
背景として、先日、AppleIDの2FAが必須になりました。

fastlaneは2FAにも対応しており、詳しくは下記に記載があります。

Continuous Integration - fastlane docs #authentication-with-apple-services

具体的には fastlane spaceauth -u user@email.com を実行し、出力された値を環境変数に設定するというものです。
ただし、上記を実行した環境と利用する環境でregionが違うと失敗する可能性があるようで、私の環境ではCircleCIに上記で得られた値を設定しても正常に動作しませんでした。

少し古いISSUEになりますが、2FAを無効にすると良いという回答もあります。

Locally generated FASTLANE_SESSION does not work on CI machine · Issue #9518 · fastlane/fastlane

今回はAccount Holderではないアカウントを使用したのですが、そのアカウントも2FAを無効にできず、その解決策が使えなくなっているようでした。

下記の通り、無効にする機能が表示されていません。
スクリーンショット 2019-05-27 13.17.22.png

ちなみに、Bitraseでは2FAを行う環境が用意されているようです。
App Store Connect 2FA solved on Bitrise | Bitrise Blog

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

Flutterウィークリー #60

Flutterウィークリーとは?

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

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

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

読み物&チュートリアル

PeanutであなたのFlutter / Dart Webアプリを披露する

https://medium.com/@kevmoo/show-off-your-flutter-dart-web-app-with-peanut-c0307f2b733c


Kevin Mooreが、自分のWebプロジェクトをghページに簡単に公開するために作成したCLIユーティリティ、Peanutの使い方を説明します。

ペディスティックDart

https://medium.com/dartlang/pedantic-dart-1c7d365510de


David Morganが、Pedantic、Googleの内部で使用されている糸くずの構成、およびそれを構築するために行った決定について話します。

CodemagicでFlutter Webアプリケーションを構築してホストする

https://blog.codemagic.io/build-and-host-your-flutter-web-apps-on-codemagic/


Codemagicの人々は、新機能、静的ページをリリースし、Codemagic CIからFlutter Webビルドをデプロイする方法を説明します。

CircularRevealAnimationをFlutterライブラリとして実装し、その途中でpub.devに公開する方法

https://medium.com/flutter-community/how-to-implement-circularrevealanimation-as-flutter-library-and-publish-it-on-pub-dev-on-the-way-34e8cd21a46


Alexander Zhdanovが、Circularの啓示を作成し、それをpub.devのライブラリとして公開する方法を説明しています。

初心者のためのFlutterでFuturesを使うためのガイド

https://medium.com/flutter-community/a-guide-to-using-futures-in-flutter-for-beginners-ebeddfbfb967


あなたがそれらを初めて使うとき、Futuresは少し気が遠くなるかもしれません。 Dane Mackierによるこの記事で、あなたは短時間でプロのようにそれらを使用するでしょう。

Widget-Async-Bloc-Service: Flutterアプリのための実用的なアーキテクチャ

https://medium.com/coding-with-flutter/widget-async-bloc-service-a-practical-architecture-for-flutter-apps-250a28f9251b


Andrea BizzottoがBLoCアーキテクチャに新たな工夫を加え、彼が行った微調整を実用的なパターンにすることを説明します。

マテリアルスタイルのセグメント化コントロール

https://medium.com/@sebbenkra/a-material-styled-segmented-control-1e92310c3a67


Sebastian KraatzはFlutterでセグメント化されたコントロールを必要としていたので、彼はそれを作り、自分でそれを行う方法を説明しました。

Flutterクリッピング

https://medium.com/flutter-community/clipping-in-flutter-e9eaa6b1721a


Raouf Rahicheによるこの記事で、クリッピングの力を解き放ってください。

Flutter PageViewのスクロールとウィジェットアニメーションの同期

https://medium.com/flutter-community/synchronising-widget-animations-with-the-scroll-of-a-pageview-in-flutter-2f3475fcffa3


AntonelloGalipòによる、アニメーションとPageViewスクロールとの接続方法に関するチュートリアル。

Flutterテキストリーダーアニメーション

https://medium.com/aubergine-solutions/text-reader-animation-in-flutter-12f81a47ec7f


Pinkesh Darjiは、Google I / Oに表示されるアニメーションを再現して、AIがテキストを読み取る方法をシミュレートします。

FlutterレスポンシブUIを構築する

https://medium.com/flutter-community/build-responsive-uis-in-flutter-fd450bd59158


モバイル、Web、デスクトップ用のFlutterアプリを作成するには、レスポンシブUIを作成する必要があります。 Raouf Rahicheがあなたに基本とFlutterからそれを処理する方法を示します。

なぜFlutterようなツールが成功する必要があるのか

https://medium.com/flutter-community/why-tools-like-flutter-need-to-succeed-965a9408e8dc


Ryan Edgeによる開発作業と、なぜFlutter (およびその他のフレームワーク)が存在すべきかについての興味深い考察。

実行時にテーマを変更する

https://medium.com/@bimsina/changing-theme-at-run-time-flutter-d634c307de8a


ダークモードはMaterialスペックの中の1つですので、Bibek Timsinaが実行時にアプリのテーマを変更する方法を説明します。

Flutterウィジェットパーフェクトステート管理出来ますか?

https://medium.com/flutter-community/widget-perfect-state-management-in-flutte-is-it-possible-73e76c205620


州の管理を容易かつ効果的にするためのMellati Meftahによる努力。

Flutter - バウンスボタンアニメーション

https://medium.com/flutter-community/flutter-bouncing-button-animation-ece660e19c91


アニメーションボタンの作成に関するDaniele Cambiによるチュートリアル。

Shimmerパッケージを使ったFlutter簡単なスケルトンビュー

https://medium.com/flutter-community/easy-skeleton-views-in-flutter-using-shimmer-package-acdde0288e1b


情報をロードしている間にスケルトンを表示することは最近かなり流行しています。 Dane MackierがFlutterそれを行う方法を説明します。

ビデオ&メディア

MediaQuery(今週のFlutterウィジェット)

https://www.youtube.com/watch?v=A3WrA4zAaPw


MediaQueryを使用して、さまざまな画面サイズに基づいてアプリのUIレイアウトを調整します。

Flutter UI - クリーンデザイン - レンタルサービス

https://www.youtube.com/watch?v=y_hX5AAFEB8&feature=youtu.be


もう一つのUIチャレンジ。今回は、レンタルサービスのアプリです。

Flutter :LinearGradientの背景

https://www.youtube.com/watch?v=FZiw9MWLlys


LinearGradientデコレーションを使用してウィジェット内に興味深い背景を作成する方法についてのビデオ。

ソース生成とあなた自身のパッケージの書き方(The Boring Flutter Development Show、Ep。23)

https://www.youtube.com/watch?v=mYDFOdl-aWM&list=PLjxrf2q8roU3ahJVrSgAnPjzkpGmL9Czl&index=32&t=6s&linkId=67913821


ボーリングショーのこのエピソードでは、FilipはSwavとDiegoの2人のゲストが参加します。 Swavを使って、カスタムウィジェットを自動的に作るためのソース生成の使い方を紹介します。 Diegoでは、彼らはDiegoのパーセントインジケーターFlutterパッケージでCustomPainter関連の機能要求に取り組んでいます。

Flutter Web:GithubページにFlutter UIKitをデプロイするピーナッツチュートリアル

https://www.youtube.com/watch?v=TJDSQBm51cI&feature=youtu.be


Flutter UI Kit WebアプリケーションをGitHubページまたはgithub.ioにデプロイする方法に関するチュートリアル

Flutter Web:レスポンシブポートフォリオアプリケーションを作成するパート1

https://www.youtube.com/watch?v=QAHqlsAky_4&feature=youtu.be


Flutter For Webを使用してレスポンシブポートフォリオアプリを作成する方法

プロバイダと接続性を使用したFlutterネットワークセンシティブUI

https://www.youtube.com/watch?v=u9O8NOnQi_A&feature=youtu.be


接続しているネットワークの種類に基づいてUIを更新するNetworkSensitiveウィジェットの作成方法を学びましょう。

フラッタルートを使用したグローバル通知

https://www.youtube.com/watch?v=FRCvqkyeCzQ


より分離されたコードのためのルートミドルウェアと通知ミドルウェアを持つredux付きの本物のアプリ。

ライブラリ&コード

appspector / flutter-plugin

https://github.com/appspector/flutter-plugin

AppSpectorをあなたのFlutterプロジェクトに統合するプラグイン。

pinkeshdarji / SuperHeroInteraction:スーパーヒーローインタラクション

https://github.com/pinkeshdarji/SuperHeroInteraction

Flutter開発したスーパーヒーローインタラクション

lesnitsky / network_state

https://github.com/lesnitsky/network_state

サービスアウェアネットワーク状態プラグイン

ayush221b / wallio

https://github.com/ayush221b/wallio


Android、iOS、Web、およびデスクトップ用のJSON解析およびアニメーションを実装する単純なフラッターアプリケーション。

TakeoffAndroid /フラッター・サンプル

https://github.com/TakeoffAndroid/flutter-examples

キュレーションデザインの究極のチートブック

Ethiel97 / nice_button

https://github.com/Ethiel97/nice_button

あなたのアプリに手間をかけずに素敵なボタンをデザインするためのAndroidとIOS用のFlutterパッケージ。

kalismeras61 / flutter_web_dashboard

https://github.com/kalismeras61/flutter_web_dashboard

Flutterブートストラップスタイル管理UI

MeshackMusundi /国

https://github.com/MeshackMusundi/Countries

Countries GraphQL APIを使用して国のリストを表示するFlutterアプリ

andrewackerman / bloc_lite

https://github.com/andrewackerman/bloc_lite

合理化されたBLoC実装ライブラリ

evant / streamqflite

https://github.com/evant/streamqflite

sqlbriteに触発されたsqfliteの周りのFlutter反応型ストリームラッパー

メゾニ/マーシャリング

https://github.com/mezoni/marshalling

整列化ライブラリを使用すると、オブジェクトの整列化と非整列化(シリアル化/逆シリアル化も可能)を行うことができます(たとえば、json互換型への変換)。

roughike / streaming_shared_preferences

https://github.com/roughike/streaming_shared_preferences

Flutterプロジェクト用の反応型Key-Valueストア。 shared_preferencesと似ていますが、Streamsを使用します。

ayush221b / hack19ハンドブック

https://github.com/ayush221b/hack19-handbook


Hack'19の参加者を手助けするための、一連の資料がまとめられている、便利なハンドブック。

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

RxSwift初心者が公開したアプリの一部晒す

はじめに

アプリを使っていて、検索とかで表示レスポンスがリアルタイムなアプリとそうでないアプリがありました。
どうやってるのかなー?と調べてみるとリアクティブプログラミングなるものがあるらしい...
挙動としてはExcelとかがよく例えられます。
こちらの記事がイメージ掴みやすかったです。
リアクティブプログラミングへの理解がイマイチだったのでまとめてみた
で、iOSで実現するためのライブラリとしてはRxSwiftが他の言語にも流用しやすくて、おすすめらしい..
SwiftでReactive Programming
という記事を目にしてから数年立ってました。
今回勉強できる時間があったので、自作アプリをリリースがてらRxSwiftを使ってみて、
リアクティブプログラミングを実践してみようと思いました。

今回リリースしたアプリをベースにして、一部機能を切り出してサンプルとして公開して、
やってみたことを備忘録として記事に起こそうと思います。

元アプリ紹介

May-27-2019 11-42-57.gif

よくあるアプリですが、一見電卓に見えてパスコードを入力したら、
隠していた動画リストが出てくるというアプリです。

結婚してムフフな動画も気楽に観れなくなったということもあり、同様のアプリを探してみたのですが、
使い勝手が悪かったり、昔に作られたままメンテがされてないアプリばかりだったので、
「ぼくがかんがえた さいきょう プライバシー動画ビューアーアプリ」として、
勉強がてら自作してみました。(結局このアプリが作ってることは嫁にバレている..)

この手のアプリは「隠す」ことに重点が置かれていて、
動画のファイラーとしての使用感がよろしくないので、
その点についてこだわってみました(動画検索/圧縮機能あり)。

現在は動画は写真アプリかファイルアプリから取り込むしかありませんが、
今後はurlから動画ダウンロード機能も追加していきたいと思います。(Apple的にNGかな?)

生意気にも有料販売しているので、気が向いた方は是非ともお買い求めお願いします!
https://itunes.apple.com/jp/app/id1464652243

目標

あんまり最初からハードル上げないよう

  • MVVMアーキテクチャに触る
  • RxSwiftを使ってViewControllerとViewModelのバインディングをやってみる

くらいにしました。

サンプル解説

元のアプリ
電卓画面 → パスワードを入れると動画ビューアー出現
というアプリでしたが、この中の電卓機能のみを切り出してサンプル作りました。
サンプルのリンクはこちら

・処理の流れ(MVVMアーキテクチャ)

こちらこちらの記事を参考にしました。

Untitled.png

各クラスの役割
  • CalculatorViewController(View)

ユーザーのインターフェース
ViewModelとデータバインディングし、自動描画する
→ボタンタップをViewModelに伝えて結果を更新

  • CalculatorViewModel(ViewModel)

Viewのための状態保持
ViewからのデータをModelに伝達
→ViewのボタンタップをModel用に加工。結果をまた加工してViewに通知

  • CalculatorModel(Model) ViewModelから受け取ったデータを処理する →実際の計算処理

View→ViewModel→Model
という参照/処理の流れで、
ViewはViewModelを参照するが、Modelを参照しない。
ViewModelはModelを参照するが、Viewは参照しない。
Modelは参照されるのみ。

・RxSwiftのライブラリを使った部分

ViewとViewModelの表示のデータバインディング

今回表示系の更新は電卓の入力&計算結果を表示する

displayNumLabel

のみ。
viewModelのBehaviorRelay変数displayNumと下記のようにバインディングさせる。
BehaviorRelayはacceptで値を変更するとき、onNextのイベントを受け取ることができる変数です。

viewModel.displayNum
  .asObservable()
  .bind(to: displayNumLabel.rx.text)
  .disposed(by: disposeBag)
ボタンタップのイベント処理

RxSwiftを利用しない時はタップ等のイベント処理で@IBActionを使用してましたが、
rx.tapを利用すれば@IBActionを利用しなくてもタップを検知し、処理を実行できるようになります。

zeroButton.rx.tap
  .subscribe { [weak self] _ in
      guard let `self` = self else { return }
      // 処理を実行
      self.setOperationButtonSelected(tapOperationButton: .none)
      self.viewModel.tapNumberButton(inputNum: "0")
  }
  .disposed(by: disposeBag)

使ってみた感想

RxSwiftを使う前は

1. View入力受け取り
2. View入力受け取り値をViewModelへ伝達
3. ViewModelで処理
4. ViewModelの処理結果をViewへ伝達
5. View更新

というプロセスを経なければならなかったですが、

RxSwiftを使えば、変数がView・ViewModelとバインディングされているので、

1. View入力受け取り
2. View入力受け取り値をViewModelへ伝達
3. ViewModelで処理(勝手にViewへ通知が行き、更新される)

で済むようになりスッキリ書けるようになりました。

今後改善したいこと&疑問

・ ViewModelとModelのバインディング(?)

MVVMではRxSwiftなどのライブラリを用いてViewControllerとViewModelをバインディングするという記事をよく見かけるけど、
ViewModelとModelもバインディングするものなのかな??
計算機の部分のViewModelとModelだとシンプルだから必要ない気がする...

参考にさせていただいた記事

上記以外の参考にさせていただいた記事を載せます。

オブザーバーパターンから始めるRxSwift入門
全体的な流れや概念の部分で参考にさせていただきました。

RxSwift 再入門
イベント処理の流れについて参考にさせていただきました。

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

NativeBaseでタブ表示やリスト表示を試して雰囲気を掴む

NativeBaseとは?

ReactNative用のUIライブラリ

:computer:環境構築


yarnを使ってライブラリをインストール

$ yarn add native-base

:pencil: 実装


試しに、リスト表示+クリックした箇所をログ表示するまでをやってみます。

リスト表示

App.tsx
import { Platform } from "react-native";
import {
  createAppContainer,
  createStackNavigator,
  NavigationContainer
} from "react-navigation";
import Home from "./screens/home";

const headerNavigationOptions = {
  headerStyle: {
    backgroundColor: "gray",
    marginTop: Platform.OS === "android" ? 24 : 0
  },
  headerTitleStyle: { color: "white" },
  headerTintColor: "white"
};

const AppNavigator: NavigationContainer = createStackNavigator({
  Home: {
    screen: Home,
    navigationOptions: {
      ...headerNavigationOptions,
      headerTitle: "サンプル"
    }
  }
});

export default createAppContainer(AppNavigator);
Home.tsx
import {
  Container,
  Content,
  List,
  ListItem,
  Text,
  Body,
  Right,
  Icon
} from "native-base";
import React from "react";

// tslint:disable-next-line: no-empty-interface
interface IProps {}
export default class Home extends React.Component<IProps> {
  public render() {
    // 表示するデータ
    const items: string[] = ["itemA", "itemB", "itemC"];
    return (
      <Container>
        <Content>
          <List
            dataArray={items}
            renderRow={item => (
              <ListItem
                button={true}
                onPress={() => console.log(`onPress ${item}`)}
              >
                <Body>
                  <Text>{item}</Text>
                </Body>
                <Right>
                  <Icon name="arrow-forward" />
                </Right>
              </ListItem>
            )}
          />
        </Content>
      </Container>
    );
  }
}

実行してiPhoneシュミレータで確認すると、以下の様な感じになりました :eyes:

アイテムをクリックするとコンソールログに出力されます :sparkles:


クリップボードに長押ししたテキストを保存する

clipboard操作に関しては以下のモジュールを使用します。

ライブラリをインストール

$ yarn add @react-native-community/react-native-clipboard
clips.tsx
...
      <Container>
        <Content>
          <List
            dataArray={items}
            renderRow={item => (
              <ListItem
                button={true}
                onPress={() => console.log(`onPress ${item}`)}
                onLongPress={() => this.onLongClick(item)}
              >
                ...
            )}
          />
        </Content>
      </Container>
...

  private onLongClick(item: string) {
    Clipboard.setString(item);
    Alert.alert("information", `「${item}」をクリップボードにコピーしました。`);
  }
}

これで長押しするとクリップボードに選択した文字列が設定されるようになりました :tada:


タブバーを下部に設置する

Footer Tabsを使用する

  • まずはサンプル通りに実装
export default class Home extends React.Component<IProps> {
  public render() {
    return (
      <Container>
        <Content />
        <Footer>
          <FooterTab>
            <Button vertical={true}>
              <Icon name="clipboard" />
              <Text>タブ1</Text>
            </Button>
            <Button vertical={true}>
              <Icon name="settings" />
              <Text>タブ2</Text>
            </Button>
          </FooterTab>
        </Footer>
      </Container>
    );
  }
}

iOSシュミレータでの実行結果
スクリーンショット 2019-05-27 8.09.19.png (511.8 kB)

表示だけは出来ました。:tada:
次はタブの切り替えや、現在表示されているタブが分かるようにしたいと思います。

import {
  Body,
  Button,
  Container,
  Content,
  Footer,
  FooterTab,
  Icon,
  Text
} from "native-base";
import React from "react";

import Tab1 from "./clips";
import Tab2 from "./settings";

interface IState {
  selectedTab: string;
}

// tslint:disable-next-line: no-empty-interface
interface IProps {}

export default class Home extends React.Component<IProps, IState> {
  constructor(props: IProps) {
    super(props);

    this.state = {
      selectedTab: "clips"
    };
  }

  public renderSelectedTab() {
    switch (this.state.selectedTab) {
      case "clips":
        return <Tab1 />;
      case "settings":
        return <Tab2 />;
      default:
    }
  }

  public render() {
    return (
      <Container>
        <Content>{this.renderSelectedTab()}</Content>
        <Footer>
          <FooterTab>
            <Button
              vertical={true}
              active={this.state.selectedTab === "clips"}
              onPress={() => this.setState({ selectedTab: "clips" })}
            >
              <Icon name="clipboard" />
              <Text>タブ1</Text>
            </Button>
            <Button
              vertical={true}
              active={this.state.selectedTab === "settings"}
              onPress={() => this.setState({ selectedTab: "settings" })}
            >
              <Icon name="settings" />
              <Text>タブ2</Text>
            </Button>
          </FooterTab>
        </Footer>
      </Container>
    );
  }
}

clips.tsxsettings.tsx を別ページとして作成し、FooterTabの Button
押された時に state でどのページを表示するか管理しています。

iOSシュミレータでの実行結果
5.png

タブ切り替えもちゃんと出来てます :tada:
6.png


Androidの場合

上のコードをAndroidで動かすと以下のようになりました。

7.png

タブバーの色等細かい所は調整が必要そうです :eyes:

:link: 参考URL


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