20210127のiOSに関する記事は3件です。

【React Native iOS】外部アプリからファイルを開く方法

※注意:この記事はiOSのことだけ書いてます。

先に結論

  1. Document Typesを設定する
  2. Linkingを使ってファイルパスを取得する

以上。

何がしたいのか

個人でReact Nativeを使ってアプリ開発をしている中で、他のアプリからファイルを受け取る必要があり、今回実装してみました。

正確になんて言うのか、言葉はわからないのですが……これです?

共有シートから、特定のアプリでファイルを開くヤツ。
メールやSafari、AirDropで共有されたファイルも開けるようになります。

……なんて言うのが正しいの?

実装環境

  • React Native 0.63.4
  • XCode 12.3

なおRNプロジェクトの作成方法は割愛します。

? 外部アプリからファイルを取得する

1. 受け取るファイルの種類を設定する

初っ端からXcodeを開きます。
Projectを開いたら、Target < [APP名] < Info < Document Typesを開いて、アコーディオン内の+ボタンを押してください。
image.png

ここに、受け取るファイルの種類を定義していきます。
image.png

  • Nameは適当に、ファイル形式の種類の名前を入れます。
  • Typeには、予め決められているMIMEのようなものを入れます。正しくは Uniform Type Identifier (UTI) と言うらしいです。
    必要な値はここから探せます。その他、独自形式も定義できるみたいです。

  • Handler Rank は指定した形式のファイルに対して、閲覧者なのか作成者なのかを宣言して、他のアプリとのランク付けをするもの、らしい、です。

意味
Owner 指定した種類のファイルを作成します
Default 指定した種類のファイルを開きます
Alternate 指定した種類のファイルのセカンダリビューアー
None 指定した種類のファイルに対して何もしない

2. 受け取ったファイルをRNに送るためのコードを書く

次に、受け取ったファイル(正しくはファイルのパス)を、React NativeのJavaScript側に送るためのコードを書きます。
コードを書くファイルは、AppDelegate.mです。
image.png

追加するコードは、(大分わかりにくいですが……)以下の緑色のコードです。
上の方でReact/RCTLinkingManager.hをインポートして、あとは下の方で3行だけコードを入れれば、ひとまず完成です。

AppDelegate.m
#import "AppDelegate.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
+ #import <React/RCTLinkingManager.h>

~~~中略~~~

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   ~~中略~~
}

+ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
+  return [RCTLinkingManager application:application openURL:url options:options];
+ }

3. JS側でファイルパスを受け取る

残るは、ReactNative側でファイルパスを受け取るだけです。
コードから予想できた方も多いと思いますが、URL Schemeで使うLinkingを使ってパスを取得します。
というか2と3に関しては、ドキュメントそのままです。

const handler = (filePath: string) => {
    // 処理内容
};

useEffect(() => {
  // アプリ起動前に呼ばれる
  Linking.getInitialURL().then((filePath) => {
    if (filePath) handler(filePath);
  });

 // アプリ起動後に呼ばれる
  Linking.addEventListener('url', (event) => handler(event.url));
}, []);

URL Schemeで取得するURLの多くは、 https://~~mailto:~~だと思いますが、ここで取得されるパスは file:///~~~になっていて、iOS内部の絶対パスが渡ってきます。

4. ファイルパスを使って処理する

ここに関しては、必要に応じて処理をしてください。
私はreact-native-fsを導入してreadFileで読み込みました。

? LSSupportsOpeningDocumentsInPlaceと共存させる方法

iOS11から追加されたファイル.appから、各アプリのディレクトリにアクセスできるヤツ。

これの有効に必要なLSSupportsOpeningDocumentsInPlaceをオンにしていると、上記の方法で取得したファイルパスを使っても、No such file or directoryと怒られてしまう場合があります。

LSSupportsOpeningDocumentsInPlaceを有効にすると、ファイルをコピーせず直接オリジナルのファイルを参照しにいきます。
iOSはサンドボックスの制限が厳しいので、(正確な理由はわからんですが)おそらくJS側からオリジナルのファイルがあるパスにアクセスする権限がないのが原因かなと。

回避策として、tmpディレクトリにファイルをコピーしてしまう事にしました。

AppDelegate.m
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
  NSString* urlPath = url.path;
  if(![[NSFileManager defaultManager] isReadableFileAtPath:urlPath])
  {
    if([url startAccessingSecurityScopedResource])
    {
      NSString* destPath = [NSString stringWithFormat:@"%@/%@", NSTemporaryDirectory(), [url.path lastPathComponent]];
      if(![[NSFileManager defaultManager] copyItemAtPath:urlPath toPath:destPath error:nil]) {
        [[NSFileManager defaultManager] removeItemAtPath:destPath error:nil];
        [[NSFileManager defaultManager] copyItemAtPath:urlPath toPath:destPath error:nil];
      }
      [url stopAccessingSecurityScopedResource];
      NSURL* destURL = [NSURL fileURLWithPath:destPath];
      return [RCTLinkingManager application:application openURL:destURL options:options];
    }
  }
  return [RCTLinkingManager application:application openURL:url options:options];
}

読み取り権限がないファイルパスだった場合のみ、アプリのtempDirにファイルをコピーして、RNにコピーしたファイルのパスを渡すように変更しました。
これで、JS側からもファイルを読みに行くことができました?

普段Web言語ばかり扱っているので、こうしてネイティブ部分のコードに触れて理解できるようになると楽しくなってきますね。
時間を作って、Obj-CやSwiftもしっかり勉強したいと思いました。

なおネイティブアプリをまともに書いた経験が無いので、間違いなどあればご指摘ください?‍♂️

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

RxSwift6 の変更点まとめ

RxSwiftの6.0が公開されたので変更点をサンプル付きでまとめました。
GitHubのリリースノートDEV Communityでもまとめられています。

New Logo

dynamicMemberLookup を使用したバインダーの自動合成

今ままでは以下のような ExampleView があったとき

class ExampleView: UIView { 
    var text: String
}

Binder を毎回記述することで

extension Reactive where Base: ExampleView {
    var text: Binder<String> {
       Binder(base) { base, title in 
           base.text = text
       }
    }
}

このように stream をクラスに bind することができました。

let exampleView = ExampleView()
Observable.of("text").bind(to: exampleView.rx.text)

RxSwift 6 からは、どのクラスに対しても、Binderを自動的に合成します。そのため、上記のBinderのコードをすべて削除してコードをすっきりさせることができるようになりました。

Infallible

エラーを流さない Observable です。

Infallible<String>.create { observer in
    observer(.next("next"))
    observer(.completed)
    // これはコンパイルエラー
    // observer(.error(NSError()))

    return Disposables.create {}
}

Observable と比べると微妙にメソッドが違います。

Observable<String>.create { observer in
    observer.on(.next("next"))
    observer.on(.error(NSError()))
    observer.on(.completed)

    return Disposables.create {}
}

また RxCocoa の DriverSignal は常に MainScheduler を使用しリソースを共有するのに対して、Infallible、基本的に Observable である点が異なります。

drive()emit() で複数の observerrelay にバインドできるようになりました

let myButton = UIButton()
Driver.of(true).drive(myButton.rx.isEnabled, myButton.rx.isSelected)
Signal.of(true).emit(myButton.rx.isEnabled, myButton.rx.isSelected)

decode(type:decoder:) が追加されました

Combineと同様に、Dataを出力するObservableに対して動作するデコード演算子が追加されました。

service
    .fetchJSONUsers() // Observable<Data>
    .decode(type: [Example].self, decoder: JSONDecoder()) // Observable<[Example]>

SingleSwiftResult を使うようになりました。

subscribe の命名が変わったため警告が出るようになりました。

// RxSwift 5
// 'subscribe(onSuccess:onError:onDisposed:)' is deprecated: renamed to 'subscribe(onSuccess:onFailure:onDisposed:)'
single.subscribe(
    onSuccess: { value in
        print("Got a value: \(value)")
    },
    onError: { error in
        print("Something went wrong: \(error)")
    }
)

// RxSwift 6
single.subscribe(
    onSuccess: { value in
        print("Got a value: \(value)")
    },
    onFailure: { error in
        print("Something went wrong: \(error)")
    }
)

ReplayRelay が追加されました

新しく ReplaySubject をラップした ReplayReplay が追加されました。
BehaviorRelayPublishRelay と同様にエラーを流しません。

withUnretained が追加されました

いままでは weak キーワードを使い循環参照をせずにクラスに値を渡していました。

viewModel.importantInfo
    .subscribe(onNext: { [weak self] info in 
        guard let self = self else { return }
        self.doImportantTask(with: info)
    })
    .disposed(on: disposeBag)

RxSwift 6 からは withUnretained を使うことで綺麗に記述することができます。

viewModel.importantInfo
    .withUnretained(self) // (self, info) のタプルになる
    .subscribe(onNext: { owner, info in 
        owner.doImportantTask(with: info)
    })
    .disposed(by: disposeBag)

distinctUntilChange(at:) で Key Paths をつかるようになりました。

Observable.of(Example(text: "1"), Example(text: "2")).distinctUntilChanged(at: \.text)

struct Example {
    let text: String
}

DisposeBag のビルダー関数でSwiftUIのようにカンマを使わずに記述することができます。

var disposeBag = DisposeBag { 
    observable1.bind(to: input1)

    observable2.drive(input2)

    observable3.subscribe(onNext: { val in 
        print("Got \(val)")
    })
}

disposeBag.insert {
    observable4.subscribe()

    observable5.bind(to: input5)
}

ignoreElementsObservable<Never> を返すようになりました

※ 修正前

public func ignoreElements() {
    -> Completable {
        return self.flatMap { _ in
            return Observable<Never>.empty()
        }
        .asCompletable()
}

※ 修正後

public func ignoreElements() {
    -> Observable<Never> {
    self.flatMap { _ in Observable<Never>.empty() }
}

UIApplicationRxExtension が追加されました

以下変数が追加されました

  • didEnterBackground
  • willEnterForeground
  • didFinishLaunching
  • didBecomeActive
  • willResignActive
  • didReceiveMemoryWarning
  • willTerminate
  • significantTimeChange
  • backgroundRefreshStatusDidChange
  • protectedDataWillBecomeUnavailable
  • protectedDataDidBecomeAvailable
  • userDidTakeScreenshot

変更のコミット

命名変更

メソッドのリネームがありました。

RxSwift 5 RxSwift 6
catchError(_:) catch(_:)
catchErrorJustReturn(_:) catchAndReturn(_:)
elementAt(_:) element(at:)
retryWhen(_:) retry(when:)
takeUntil(_:) take(until:)
takeUntil(behavior:_:) take(until:behavior:)
takeWhile(_:) take(while:)
takeWhile(behavior:_:) take(while:behavior:)
take(_:) take(for:)
skipWhile(_:) skip(while:)
takeUntil(_:) take(until:)
observeOn(_:) observe(on:)
subscribeOn(_:) subscribe(on:)

前のメソッドを使ってもエラーにはならずに以下のような警告が表示されます。

'catchError' is deprecated: renamed to 'catch(_:)'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ViewcontrollerからTabBarControllerへコードで移動する

ViewControllerからTabBarControllerに移動する

まず、最初に下の図のようにViewControllerからTabBarControllerに紐づいてた緑のViewControllerに遷移しても、ViewControllerへの移動なので、下のタブが表示されません。だから、TabBarViewControllerへ移動する必要があります。

コード

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

    }
    @IBAction func FromViewControllertoTabBarController(_ sender: Any) {

        let storybord=UIStoryboard(name: "Main", bundle: nil)
        let TabBarController=storybord.instantiateViewController(withIdentifier: "TabBarController") as! UITabBarController
        //[0]が緑のViewControllerで[1]にすると赤のViewControllerに遷移する
        let ViewController=TabBarController.viewControllers?[0] as! FirstViewController
        //TabControllerに遷移した時に最初にどの画面を表示するかを選択する
        TabBarController.selectedViewController=ViewController
        TabBarController.modalTransitionStyle = .crossDissolve
        TabBarController.modalPresentationStyle = .fullScreen
        self.present(TabBarController, animated: true, completion: nil)
    }

}

赤色のViewControllerにはFirstViewControllerクラスが紐づいています。

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